deeplink.go

  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}