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 // ErrInvalidResource is returned when a resource type is unknown.
20 ErrInvalidResource = errors.New("invalid resource")
21 // ErrInvalidUUID is returned when the UUID portion is empty or malformed.
22 ErrInvalidUUID = errors.New("invalid UUID")
23)
24
25// Resource represents a Lunatask resource type for deep links.
26type Resource string
27
28// Valid resource types for deep links.
29const (
30 ResourceArea Resource = "areas"
31 ResourceGoal Resource = "goals"
32 ResourceTask Resource = "tasks"
33 ResourceNote Resource = "notes"
34 ResourcePerson Resource = "people"
35 ResourceNotebook Resource = "notebooks"
36)
37
38// Valid reports whether r is a known resource type.
39func (r Resource) Valid() bool {
40 switch r {
41 case ResourceArea, ResourceGoal, ResourceTask, ResourceNote, ResourcePerson, ResourceNotebook:
42 return true
43 }
44
45 return false
46}
47
48// String returns the resource type as a string.
49func (r Resource) String() string {
50 return string(r)
51}
52
53// ValidateUUID checks whether id is a valid UUID string.
54func ValidateUUID(id string) error {
55 if id == "" {
56 return fmt.Errorf("%w: empty", ErrInvalidUUID)
57 }
58
59 if _, err := uuid.Parse(id); err != nil {
60 return fmt.Errorf("%w: %q", ErrInvalidUUID, id)
61 }
62
63 return nil
64}
65
66// ParseDeepLink extracts resource type and UUID from a Lunatask deep link
67// or plain UUID. Accepts "lunatask://tasks/uuid" or plain UUID strings.
68// When a plain UUID is provided, the resource type will be empty.
69// The UUID is validated to be a well-formed UUID string.
70func ParseDeepLink(input string) (Resource, string, error) {
71 if input == "" {
72 return "", "", fmt.Errorf("%w: empty input", ErrInvalidDeepLink)
73 }
74
75 // Check for lunatask:// prefix
76 const prefix = "lunatask://"
77 if !strings.HasPrefix(input, prefix) {
78 // Treat as plain UUID, validate format
79 if err := ValidateUUID(input); err != nil {
80 return "", "", err
81 }
82
83 return "", input, nil
84 }
85
86 // Remove prefix and split
87 remainder := strings.TrimPrefix(input, prefix)
88 parts := strings.SplitN(remainder, "/", 2) //nolint:mnd // split resource/uuid
89
90 if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
91 return "", "", fmt.Errorf("%w: %q", ErrInvalidDeepLink, input)
92 }
93
94 resource := Resource(parts[0])
95 id := parts[1]
96
97 if !resource.Valid() {
98 return "", "", fmt.Errorf("%w: %q", ErrInvalidResource, resource)
99 }
100
101 if err := ValidateUUID(id); err != nil {
102 return "", "", err
103 }
104
105 return resource, id, nil
106}
107
108// BuildDeepLink constructs a Lunatask deep link from resource type and ID.
109// Returns "lunatask://resource/uuid". The ID is validated to be a well-formed UUID.
110func BuildDeepLink(resource Resource, id string) (string, error) {
111 if err := ValidateUUID(id); err != nil {
112 return "", err
113 }
114
115 if !resource.Valid() {
116 return "", fmt.Errorf("%w: %q", ErrInvalidResource, resource)
117 }
118
119 return fmt.Sprintf("lunatask://%s/%s", resource, id), nil
120}