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}