feat(deeplink): add ParseReference function

Amolith created

Add ResourceUnknown constant and ErrInvalidReference error for clearer
API semantics. ParseReference provides better error messages for invalid
input ("expected UUID or lunatask:// deep link").

ParseDeepLink is deprecated and wraps ParseReference for backwards
compatibility.

Assisted-by: Claude Sonnet 4 via Crush

Change summary

deeplink.go      | 26 ++++++++++++++++++++------
deeplink_test.go | 41 ++++++++++++++++++++++++++++++-----------
2 files changed, 50 insertions(+), 17 deletions(-)

Detailed changes

deeplink.go 🔗

@@ -16,6 +16,8 @@ import (
 var (
 	// ErrInvalidDeepLink is returned when parsing a malformed deep link.
 	ErrInvalidDeepLink = errors.New("invalid deep link")
+	// ErrInvalidReference is returned when input is neither a valid deep link nor UUID.
+	ErrInvalidReference = errors.New("invalid reference")
 	// ErrInvalidResource is returned when a resource type is unknown.
 	ErrInvalidResource = errors.New("invalid resource")
 	// ErrInvalidUUID is returned when the UUID portion is empty or malformed.
@@ -35,11 +37,16 @@ const (
 	ResourceNotebook Resource = "notebooks"
 )
 
+// ResourceUnknown is returned when parsing a raw UUID without resource context.
+const ResourceUnknown Resource = ""
+
 // Valid reports whether r is a known resource type.
 func (r Resource) Valid() bool {
 	switch r {
 	case ResourceArea, ResourceGoal, ResourceTask, ResourceNote, ResourcePerson, ResourceNotebook:
 		return true
+	case ResourceUnknown:
+		return false
 	}
 
 	return false
@@ -63,13 +70,13 @@ func ValidateUUID(id string) error {
 	return nil
 }
 
-// ParseDeepLink extracts resource type and UUID from a Lunatask deep link
+// ParseReference extracts resource type and UUID from a Lunatask deep link
 // or plain UUID. Accepts "lunatask://tasks/uuid" or plain UUID strings.
-// When a plain UUID is provided, the resource type will be empty.
+// When a plain UUID is provided, the resource type will be ResourceUnknown.
 // The UUID is validated to be a well-formed UUID string.
-func ParseDeepLink(input string) (Resource, string, error) {
+func ParseReference(input string) (Resource, string, error) {
 	if input == "" {
-		return "", "", fmt.Errorf("%w: empty input", ErrInvalidDeepLink)
+		return "", "", fmt.Errorf("%w: empty input", ErrInvalidReference)
 	}
 
 	// Check for lunatask:// prefix
@@ -77,10 +84,10 @@ func ParseDeepLink(input string) (Resource, string, error) {
 	if !strings.HasPrefix(input, prefix) {
 		// Treat as plain UUID, validate format
 		if err := ValidateUUID(input); err != nil {
-			return "", "", err
+			return "", "", fmt.Errorf("%w %q: expected UUID or lunatask:// deep link", ErrInvalidReference, input)
 		}
 
-		return "", input, nil
+		return ResourceUnknown, input, nil
 	}
 
 	// Remove prefix and split
@@ -105,6 +112,13 @@ func ParseDeepLink(input string) (Resource, string, error) {
 	return resource, id, nil
 }
 
+// ParseDeepLink extracts resource type and UUID from input.
+//
+// Deprecated: Use ParseReference instead.
+func ParseDeepLink(input string) (Resource, string, error) {
+	return ParseReference(input)
+}
+
 // BuildDeepLink constructs a Lunatask deep link from resource type and ID.
 // Returns "lunatask://resource/uuid". The ID is validated to be a well-formed UUID.
 func BuildDeepLink(resource Resource, id string) (string, error) {
@@ -11,7 +11,7 @@ import (
 	lunatask "git.secluded.site/go-lunatask"
 )
 
-func TestParseDeepLink(t *testing.T) {
+func TestParseReference(t *testing.T) {
 	t.Parallel()
 
 	validUUID := "12345678-1234-1234-1234-123456789012"
@@ -32,51 +32,70 @@ func TestParseDeepLink(t *testing.T) {
 		{"person_link", "lunatask://people/" + validUUID, lunatask.ResourcePerson, validUUID, nil},
 		{"notebook_link", "lunatask://notebooks/" + validUUID, lunatask.ResourceNotebook, validUUID, nil},
 
-		// Plain UUIDs
-		{"plain_uuid", validUUID2, "", validUUID2, nil},
-		{"uuid_only", validUUID, "", validUUID, nil},
+		// Plain UUIDs return ResourceUnknown
+		{"plain_uuid", validUUID2, lunatask.ResourceUnknown, validUUID2, nil},
+		{"uuid_only", validUUID, lunatask.ResourceUnknown, validUUID, nil},
 
 		// Invalid inputs
-		{"empty", "", "", "", lunatask.ErrInvalidDeepLink},
+		{"empty", "", "", "", lunatask.ErrInvalidReference},
 		{"invalid_resource", "lunatask://invalid/" + validUUID, "", "", lunatask.ErrInvalidResource},
 		{"missing_uuid", "lunatask://tasks/", "", "", lunatask.ErrInvalidDeepLink},
 		{"missing_resource", "lunatask:///" + validUUID, "", "", lunatask.ErrInvalidDeepLink},
 		{"malformed", "lunatask://tasks", "", "", lunatask.ErrInvalidDeepLink},
 		{"invalid_uuid_in_link", "lunatask://tasks/not-a-uuid", "", "", lunatask.ErrInvalidUUID},
-		{"invalid_plain_uuid", "not-a-valid-uuid", "", "", lunatask.ErrInvalidUUID},
+		{"invalid_plain_uuid", "not-a-valid-uuid", "", "", lunatask.ErrInvalidReference},
 	}
 
 	for _, testCase := range tests {
 		t.Run(testCase.name, func(t *testing.T) {
 			t.Parallel()
 
-			resource, uuid, err := lunatask.ParseDeepLink(testCase.input)
+			resource, uuid, err := lunatask.ParseReference(testCase.input)
 
 			if testCase.wantErr != nil {
 				if !errors.Is(err, testCase.wantErr) {
-					t.Errorf("ParseDeepLink(%q) error = %v, want %v", testCase.input, err, testCase.wantErr)
+					t.Errorf("ParseReference(%q) error = %v, want %v", testCase.input, err, testCase.wantErr)
 				}
 
 				return
 			}
 
 			if err != nil {
-				t.Errorf("ParseDeepLink(%q) unexpected error = %v", testCase.input, err)
+				t.Errorf("ParseReference(%q) unexpected error = %v", testCase.input, err)
 
 				return
 			}
 
 			if resource != testCase.wantResource {
-				t.Errorf("ParseDeepLink(%q) resource = %q, want %q", testCase.input, resource, testCase.wantResource)
+				t.Errorf("ParseReference(%q) resource = %q, want %q", testCase.input, resource, testCase.wantResource)
 			}
 
 			if uuid != testCase.wantUUID {
-				t.Errorf("ParseDeepLink(%q) uuid = %q, want %q", testCase.input, uuid, testCase.wantUUID)
+				t.Errorf("ParseReference(%q) uuid = %q, want %q", testCase.input, uuid, testCase.wantUUID)
 			}
 		})
 	}
 }
 
+func TestParseDeepLink_Deprecated(t *testing.T) {
+	t.Parallel()
+
+	validUUID := "12345678-1234-1234-1234-123456789012"
+
+	resource, uuid, err := lunatask.ParseDeepLink("lunatask://tasks/" + validUUID)
+	if err != nil {
+		t.Errorf("ParseDeepLink() unexpected error = %v", err)
+	}
+
+	if resource != lunatask.ResourceTask {
+		t.Errorf("ParseDeepLink() resource = %q, want %q", resource, lunatask.ResourceTask)
+	}
+
+	if uuid != validUUID {
+		t.Errorf("ParseDeepLink() uuid = %q, want %q", uuid, validUUID)
+	}
+}
+
 func TestBuildDeepLink(t *testing.T) {
 	t.Parallel()