1// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
2//
3// SPDX-License-Identifier: AGPL-3.0-or-later
4
5package lunatask
6
7import (
8 "errors"
9 "fmt"
10 "strings"
11
12 "github.com/google/uuid"
13)
14
15// Errors returned by deep link operations.
16var (
17 // ErrInvalidDeepLink is returned when parsing a malformed deep link.
18 ErrInvalidDeepLink = errors.New("invalid deep link")
19 // ErrInvalidReference is returned when input is neither a valid deep link nor UUID.
20 ErrInvalidReference = errors.New("invalid reference")
21 // ErrInvalidResource is returned when a resource type is unknown.
22 ErrInvalidResource = errors.New("invalid resource")
23 // ErrInvalidUUID is returned when the UUID portion is empty or malformed.
24 ErrInvalidUUID = errors.New("invalid UUID")
25)
26
27// Resource represents a Lunatask resource type for deep links.
28type Resource string
29
30// Valid resource types for deep links.
31const (
32 ResourceArea Resource = "areas"
33 ResourceGoal Resource = "goals"
34 ResourceTask Resource = "tasks"
35 ResourceNote Resource = "notes"
36 ResourcePerson Resource = "people"
37 ResourceNotebook Resource = "notebooks"
38)
39
40// ResourceUnknown is returned when parsing a raw UUID without resource context.
41const ResourceUnknown Resource = ""
42
43// Valid reports whether r is a known resource type.
44func (r Resource) Valid() bool {
45 switch r {
46 case ResourceArea, ResourceGoal, ResourceTask, ResourceNote, ResourcePerson, ResourceNotebook:
47 return true
48 case ResourceUnknown:
49 return false
50 }
51
52 return false
53}
54
55// String returns the resource type as a string.
56func (r Resource) String() string {
57 return string(r)
58}
59
60// ValidateUUID checks whether id is a valid UUID string.
61func ValidateUUID(id string) error {
62 if id == "" {
63 return fmt.Errorf("%w: empty", ErrInvalidUUID)
64 }
65
66 if _, err := uuid.Parse(id); err != nil {
67 return fmt.Errorf("%w: %q", ErrInvalidUUID, id)
68 }
69
70 return nil
71}
72
73// ParseReference extracts resource type and UUID from a Lunatask deep link
74// or plain UUID. Accepts "lunatask://tasks/uuid" or plain UUID strings.
75// When a plain UUID is provided, the resource type will be ResourceUnknown.
76// The UUID is validated to be a well-formed UUID string.
77func ParseReference(input string) (Resource, string, error) {
78 if input == "" {
79 return "", "", fmt.Errorf("%w: empty input", ErrInvalidReference)
80 }
81
82 // Check for lunatask:// prefix
83 const prefix = "lunatask://"
84 if !strings.HasPrefix(input, prefix) {
85 // Treat as plain UUID, validate format
86 if err := ValidateUUID(input); err != nil {
87 return "", "", fmt.Errorf("%w %q: expected UUID or lunatask:// deep link", ErrInvalidReference, input)
88 }
89
90 return ResourceUnknown, input, nil
91 }
92
93 // Remove prefix and split
94 remainder := strings.TrimPrefix(input, prefix)
95 parts := strings.SplitN(remainder, "/", 2) //nolint:mnd // split resource/uuid
96
97 if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
98 return "", "", fmt.Errorf("%w: %q", ErrInvalidDeepLink, input)
99 }
100
101 resource := Resource(parts[0])
102 id := parts[1]
103
104 if !resource.Valid() {
105 return "", "", fmt.Errorf("%w: %q", ErrInvalidResource, resource)
106 }
107
108 if err := ValidateUUID(id); err != nil {
109 return "", "", err
110 }
111
112 return resource, id, nil
113}
114
115// ParseDeepLink extracts resource type and UUID from input.
116//
117// Deprecated: Use ParseReference instead.
118func ParseDeepLink(input string) (Resource, string, error) {
119 return ParseReference(input)
120}
121
122// BuildDeepLink constructs a Lunatask deep link from resource type and ID.
123// Returns "lunatask://resource/uuid". The ID is validated to be a well-formed UUID.
124func BuildDeepLink(resource Resource, id string) (string, error) {
125 if err := ValidateUUID(id); err != nil {
126 return "", err
127 }
128
129 if !resource.Valid() {
130 return "", fmt.Errorf("%w: %q", ErrInvalidResource, resource)
131 }
132
133 return fmt.Sprintf("lunatask://%s/%s", resource, id), nil
134}