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	// 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}