feat(status): add ParseTaskStatus function

Amolith created

Case-insensitive parsing of task status strings to typed TaskStatus
values. Matches ParsePriority pattern with ErrInvalidTaskStatus sentinel
error.

Assisted-by: Claude Sonnet 4 via Crush

Change summary

status.go      | 36 +++++++++++++++++++++++++++++++++
status_test.go | 56 ++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 92 insertions(+)

Detailed changes

status.go 🔗

@@ -0,0 +1,36 @@
+// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+package lunatask
+
+import (
+	"errors"
+	"fmt"
+	"strings"
+)
+
+// Errors returned by TaskStatus operations.
+var (
+	// ErrInvalidTaskStatus is returned when parsing an unknown task status string.
+	ErrInvalidTaskStatus = errors.New("invalid task status")
+)
+
+// ParseTaskStatus parses a string to a TaskStatus value (case-insensitive).
+// Valid values: "later", "next", "started", "waiting", "completed".
+func ParseTaskStatus(str string) (TaskStatus, error) {
+	switch strings.ToLower(str) {
+	case "later":
+		return StatusLater, nil
+	case "next":
+		return StatusNext, nil
+	case "started":
+		return StatusStarted, nil
+	case "waiting":
+		return StatusWaiting, nil
+	case "completed":
+		return StatusCompleted, nil
+	default:
+		return "", fmt.Errorf("%w: %q", ErrInvalidTaskStatus, str)
+	}
+}

status_test.go 🔗

@@ -0,0 +1,56 @@
+// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+package lunatask_test
+
+import (
+	"testing"
+
+	lunatask "git.secluded.site/go-lunatask"
+)
+
+func TestParseTaskStatus(t *testing.T) {
+	t.Parallel()
+
+	tests := []struct {
+		name    string
+		input   string
+		want    lunatask.TaskStatus
+		wantErr bool
+	}{
+		{"later_lower", "later", lunatask.StatusLater, false},
+		{"later_upper", "LATER", lunatask.StatusLater, false},
+		{"later_mixed", "LaTeR", lunatask.StatusLater, false},
+		{"next_lower", "next", lunatask.StatusNext, false},
+		{"next_upper", "NEXT", lunatask.StatusNext, false},
+		{"started_lower", "started", lunatask.StatusStarted, false},
+		{"started_upper", "STARTED", lunatask.StatusStarted, false},
+		{"waiting_lower", "waiting", lunatask.StatusWaiting, false},
+		{"waiting_upper", "WAITING", lunatask.StatusWaiting, false},
+		{"completed_lower", "completed", lunatask.StatusCompleted, false},
+		{"completed_upper", "COMPLETED", lunatask.StatusCompleted, false},
+		{"completed_mixed", "CoMpLeTeD", lunatask.StatusCompleted, false},
+		{"invalid", "invalid", "", true},
+		{"empty", "", "", true},
+		{"numeric", "1", "", true},
+		{"typo", "completd", "", true},
+	}
+
+	for _, testCase := range tests {
+		t.Run(testCase.name, func(t *testing.T) {
+			t.Parallel()
+
+			got, err := lunatask.ParseTaskStatus(testCase.input)
+			if (err != nil) != testCase.wantErr {
+				t.Errorf("ParseTaskStatus(%q) error = %v, wantErr %v", testCase.input, err, testCase.wantErr)
+
+				return
+			}
+
+			if !testCase.wantErr && got != testCase.want {
+				t.Errorf("ParseTaskStatus(%q) = %q, want %q", testCase.input, got, testCase.want)
+			}
+		})
+	}
+}