validate.go

  1// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
  2//
  3// SPDX-License-Identifier: AGPL-3.0-or-later
  4
  5// Package validate provides input validation helpers.
  6package validate
  7
  8import (
  9	"errors"
 10	"fmt"
 11	"slices"
 12	"strings"
 13
 14	"git.secluded.site/go-lunatask"
 15	"git.secluded.site/lune/internal/config"
 16)
 17
 18// ErrInvalidReference indicates the input is not a valid UUID or deep link.
 19var ErrInvalidReference = errors.New("invalid reference: expected UUID or lunatask:// deep link")
 20
 21// Reference parses a UUID or Lunatask deep link and returns the normalised UUID.
 22// Accepts formats:
 23//   - UUID: "3bbf1923-64ae-4bcf-96a9-9bb86c799dab"
 24//   - Deep link: "lunatask://areas/3bbf1923-64ae-4bcf-96a9-9bb86c799dab"
 25func Reference(input string) (string, error) {
 26	_, id, err := lunatask.ParseReference(input)
 27	if err != nil {
 28		return "", fmt.Errorf("%w: %s", ErrInvalidReference, input)
 29	}
 30
 31	return id, nil
 32}
 33
 34// ErrInvalidStatus indicates the status value is not recognized.
 35var ErrInvalidStatus = errors.New("invalid status")
 36
 37// ErrInvalidStatusForWorkflow indicates the status is not valid for the area's workflow.
 38var ErrInvalidStatusForWorkflow = errors.New("invalid status for workflow")
 39
 40// TaskStatus validates and normalizes a task status string.
 41// Returns the corresponding lunatask.TaskStatus or an error if invalid.
 42func TaskStatus(input string) (lunatask.TaskStatus, error) {
 43	status, err := lunatask.ParseTaskStatus(input)
 44	if err != nil {
 45		return "", fmt.Errorf("%w: %s", ErrInvalidStatus, input)
 46	}
 47
 48	return status, nil
 49}
 50
 51// TaskStatusForWorkflow validates a status string against a specific workflow.
 52// Returns the status if valid for the workflow, or an error listing valid options.
 53func TaskStatusForWorkflow(input string, workflow lunatask.Workflow) (lunatask.TaskStatus, error) {
 54	status, err := lunatask.ParseTaskStatus(input)
 55	if err != nil {
 56		return "", fmt.Errorf(
 57			"%w: '%s' for %s; valid: %s",
 58			ErrInvalidStatusForWorkflow, input, workflow.Description(), formatValidStatuses(workflow),
 59		)
 60	}
 61
 62	if !slices.Contains(workflow.ValidStatuses(), status) {
 63		return "", fmt.Errorf(
 64			"%w: '%s' not valid for %s; valid: %s",
 65			ErrInvalidStatusForWorkflow, input, workflow.Description(), formatValidStatuses(workflow),
 66		)
 67	}
 68
 69	return status, nil
 70}
 71
 72func formatValidStatuses(workflow lunatask.Workflow) string {
 73	statuses := workflow.ValidStatuses()
 74	strs := make([]string, len(statuses))
 75
 76	for i, s := range statuses {
 77		strs[i] = string(s)
 78	}
 79
 80	return strings.Join(strs, ", ")
 81}
 82
 83// ErrInvalidMotivation indicates the motivation value is not recognized.
 84var ErrInvalidMotivation = errors.New("invalid motivation")
 85
 86// Motivation validates and normalizes a motivation string.
 87// Returns the corresponding lunatask.Motivation or an error if invalid.
 88func Motivation(input string) (lunatask.Motivation, error) {
 89	motivation, err := lunatask.ParseMotivation(input)
 90	if err != nil {
 91		return "", fmt.Errorf("%w: %s", ErrInvalidMotivation, input)
 92	}
 93
 94	return motivation, nil
 95}
 96
 97// ErrInvalidRelationship indicates the relationship strength is not recognized.
 98var ErrInvalidRelationship = errors.New("invalid relationship strength")
 99
100// RelationshipStrength validates and normalizes a relationship strength string.
101// Returns the corresponding lunatask.RelationshipStrength or an error if invalid.
102func RelationshipStrength(input string) (lunatask.RelationshipStrength, error) {
103	rel, err := lunatask.ParseRelationshipStrength(input)
104	if err != nil {
105		return "", fmt.Errorf("%w: %s", ErrInvalidRelationship, input)
106	}
107
108	return rel, nil
109}
110
111// ErrInvalidArea indicates the area reference is not a valid UUID, deep link, or config key.
112var ErrInvalidArea = errors.New("invalid area: expected UUID, lunatask:// deep link, or config key")
113
114// AreaRef resolves an area reference to a UUID.
115// Accepts formats:
116//   - UUID: "527a2b42-99fd-490d-8b21-c55451368f4c"
117//   - Deep link: "lunatask://areas/527a2b42-..."
118//   - Config key: "projects"
119func AreaRef(cfg *config.Config, input string) (string, error) {
120	// Try UUID or deep link first
121	if _, id, err := lunatask.ParseReference(input); err == nil {
122		return id, nil
123	}
124
125	// Try config key lookup
126	if area := cfg.AreaByKey(input); area != nil {
127		return area.ID, nil
128	}
129
130	return "", fmt.Errorf("%w: %s", ErrInvalidArea, input)
131}
132
133// ErrInvalidGoal indicates the goal reference is not a valid UUID, deep link, or config key.
134var ErrInvalidGoal = errors.New("invalid goal: expected UUID, lunatask:// deep link, or config key")
135
136// GoalRef resolves a goal reference to a UUID.
137// Requires a valid area ID to look up goals by key (goals are scoped to areas).
138// Accepts formats:
139//   - UUID: "53ca909e-887d-4ed2-9943-d1212adf8ad8"
140//   - Deep link: "lunatask://goals/53ca909e-..."
141//   - Config key: "lunatask" (requires valid areaID for lookup)
142func GoalRef(cfg *config.Config, areaID, input string) (string, error) {
143	// Try UUID or deep link first
144	if _, id, err := lunatask.ParseReference(input); err == nil {
145		return id, nil
146	}
147
148	// Try config key lookup (requires area context)
149	if area := cfg.AreaByID(areaID); area != nil {
150		if goal := area.GoalByKey(input); goal != nil {
151			return goal.ID, nil
152		}
153	}
154
155	return "", fmt.Errorf("%w: %s", ErrInvalidGoal, input)
156}