deeplink.go

 1// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
 2//
 3// SPDX-License-Identifier: AGPL-3.0-or-later
 4
 5// Package deeplink parses and builds Lunatask deep links.
 6package deeplink
 7
 8import (
 9	"errors"
10	"fmt"
11	"strings"
12
13	"github.com/google/uuid"
14)
15
16// Resource represents a Lunatask resource type that supports deep linking.
17type Resource string
18
19// Supported Lunatask resource types for deep linking.
20const (
21	Area     Resource = "areas"
22	Goal     Resource = "goals"
23	Task     Resource = "tasks"
24	Note     Resource = "notes"
25	Person   Resource = "people"
26	Notebook Resource = "notebooks"
27)
28
29const (
30	scheme            = "lunatask://"
31	deepLinkPartCount = 2
32)
33
34// ErrInvalidReference indicates the input is neither a valid UUID nor deep link.
35var ErrInvalidReference = errors.New("invalid reference: expected UUID or lunatask:// deep link")
36
37// ErrUnsupportedResource indicates the deep link resource type is not recognised.
38var ErrUnsupportedResource = errors.New("unsupported resource type in deep link")
39
40// ParseID extracts a UUID from either a raw UUID string or a Lunatask deep link.
41// Accepts formats:
42//   - UUID: "3bbf1923-64ae-4bcf-96a9-9bb86c799dab"
43//   - Deep link: "lunatask://areas/3bbf1923-64ae-4bcf-96a9-9bb86c799dab"
44func ParseID(input string) (string, error) {
45	input = strings.TrimSpace(input)
46
47	// Try parsing as UUID first
48	if parsed, err := uuid.Parse(input); err == nil {
49		return parsed.String(), nil
50	}
51
52	// Try parsing as deep link
53	if !strings.HasPrefix(input, scheme) {
54		return "", fmt.Errorf("%w: %s", ErrInvalidReference, input)
55	}
56
57	path := strings.TrimPrefix(input, scheme)
58	parts := strings.Split(path, "/")
59
60	if len(parts) != deepLinkPartCount {
61		return "", fmt.Errorf("%w: %s", ErrInvalidReference, input)
62	}
63
64	resource := parts[0]
65	id := parts[1]
66
67	// Validate resource type
68	switch Resource(resource) {
69	case Area, Goal, Task, Note, Person, Notebook:
70		// Valid resource
71	default:
72		return "", fmt.Errorf("%w: %s", ErrUnsupportedResource, resource)
73	}
74
75	// Validate and normalise the UUID
76	parsed, err := uuid.Parse(id)
77	if err != nil {
78		return "", fmt.Errorf("%w: %s", ErrInvalidReference, input)
79	}
80
81	return parsed.String(), nil
82}
83
84// Build constructs a Lunatask deep link for the given resource and ID.
85func Build(resource Resource, id string) (string, error) {
86	parsed, err := uuid.Parse(id)
87	if err != nil {
88		return "", fmt.Errorf("%w: %s", ErrInvalidReference, id)
89	}
90
91	return fmt.Sprintf("%s%s/%s", scheme, resource, parsed.String()), nil
92}