feat(enum): add All*() and status translation

Amolith created

Add convenience functions to enumerate all valid values:
- AllTaskStatuses()
- AllPriorities()
- AllMotivations()
- AllRelationshipStrengths()

Each returns a slice in natural order with roundtrip tests.

Also translate StatusInProgress between user-facing "in-progress" and
API wire format "started" via JSON marshal/unmarshal.

Assisted-by: Claude Opus 4.5 via Crush

Change summary

motivation.go        |  11 ++++
motivation_test.go   |  36 ++++++++++++++
priority.go          |  11 ++++
priority_test.go     |  39 ++++++++++++++++
relationship.go      |  14 +++++
relationship_test.go |  39 ++++++++++++++++
status.go            |  55 +++++++++++++++++++++-
status_test.go       | 111 +++++++++++++++++++++++++++++++++++++++++++++
8 files changed, 311 insertions(+), 5 deletions(-)

Detailed changes

motivation.go 🔗

@@ -33,6 +33,17 @@ func (m Motivation) String() string {
 	return string(m)
 }
 
+// AllMotivations returns all valid motivation values.
+// Includes [MotivationUnknown] which clears/unsets motivation.
+func AllMotivations() []Motivation {
+	return []Motivation{
+		MotivationUnknown,
+		MotivationMust,
+		MotivationShould,
+		MotivationWant,
+	}
+}
+
 // ParseMotivation parses a string to a Motivation value (case-insensitive).
 // Valid values: "unknown", "must", "should", "want".
 func ParseMotivation(str string) (Motivation, error) {

motivation_test.go 🔗

@@ -10,6 +10,42 @@ import (
 	lunatask "git.secluded.site/go-lunatask"
 )
 
+func TestAllMotivations(t *testing.T) {
+	t.Parallel()
+
+	motivations := lunatask.AllMotivations()
+
+	// Check count
+	if got := len(motivations); got != 4 {
+		t.Fatalf("AllMotivations() returned %d values, want 4", got)
+	}
+
+	// Check order
+	expected := []lunatask.Motivation{
+		lunatask.MotivationUnknown,
+		lunatask.MotivationMust,
+		lunatask.MotivationShould,
+		lunatask.MotivationWant,
+	}
+	for i, want := range expected {
+		if motivations[i] != want {
+			t.Errorf("AllMotivations()[%d] = %q, want %q", i, motivations[i], want)
+		}
+	}
+
+	// Check roundtrip: each value should be parseable
+	for _, motivation := range motivations {
+		parsed, err := lunatask.ParseMotivation(motivation.String())
+		if err != nil {
+			t.Errorf("ParseMotivation(%q) failed: %v", motivation.String(), err)
+		}
+
+		if parsed != motivation {
+			t.Errorf("ParseMotivation(%q) = %q, want %q", motivation.String(), parsed, motivation)
+		}
+	}
+}
+
 func TestParseMotivation(t *testing.T) {
 	t.Parallel()
 

priority.go 🔗

@@ -55,6 +55,17 @@ func (p *Priority) Valid() bool {
 	return *p >= PriorityLowest && *p <= PriorityHighest
 }
 
+// AllPriorities returns all valid priority values from lowest to highest.
+func AllPriorities() []Priority {
+	return []Priority{
+		PriorityLowest,
+		PriorityLow,
+		PriorityNormal,
+		PriorityHigh,
+		PriorityHighest,
+	}
+}
+
 // ParsePriority parses a string to a Priority value (case-insensitive).
 // Valid values: "lowest", "low", "normal", "high", "highest".
 func ParsePriority(str string) (Priority, error) {

priority_test.go 🔗

@@ -11,6 +11,45 @@ import (
 	lunatask "git.secluded.site/go-lunatask"
 )
 
+// --- AllPriorities Function ---
+
+func TestAllPriorities(t *testing.T) {
+	t.Parallel()
+
+	priorities := lunatask.AllPriorities()
+
+	// Check count
+	if got := len(priorities); got != 5 {
+		t.Fatalf("AllPriorities() returned %d values, want 5", got)
+	}
+
+	// Check order (lowest to highest)
+	expected := []lunatask.Priority{
+		lunatask.PriorityLowest,
+		lunatask.PriorityLow,
+		lunatask.PriorityNormal,
+		lunatask.PriorityHigh,
+		lunatask.PriorityHighest,
+	}
+	for i, want := range expected {
+		if priorities[i] != want {
+			t.Errorf("AllPriorities()[%d] = %d, want %d", i, priorities[i], want)
+		}
+	}
+
+	// Check roundtrip: each value should be parseable
+	for _, priority := range priorities {
+		parsed, err := lunatask.ParsePriority(priority.String())
+		if err != nil {
+			t.Errorf("ParsePriority(%q) failed: %v", priority.String(), err)
+		}
+
+		if parsed != priority {
+			t.Errorf("ParsePriority(%q) = %d, want %d", priority.String(), parsed, priority)
+		}
+	}
+}
+
 // --- Priority Type Constants ---
 
 func TestPriority_Constants(t *testing.T) {

relationship.go 🔗

@@ -36,6 +36,20 @@ func (r RelationshipStrength) String() string {
 	return string(r)
 }
 
+// AllRelationshipStrengths returns all valid relationship strength values
+// from closest (family) to most distant (almost strangers).
+func AllRelationshipStrengths() []RelationshipStrength {
+	return []RelationshipStrength{
+		RelationshipFamily,
+		RelationshipIntimateFriend,
+		RelationshipCloseFriend,
+		RelationshipCasualFriend,
+		RelationshipAcquaintance,
+		RelationshipBusiness,
+		RelationshipAlmostStranger,
+	}
+}
+
 // ParseRelationshipStrength parses a string to a RelationshipStrength value (case-insensitive).
 // Valid values: "family", "intimate-friends", "close-friends", "casual-friends",
 // "acquaintances", "business-contacts", "almost-strangers".

relationship_test.go 🔗

@@ -10,6 +10,45 @@ import (
 	lunatask "git.secluded.site/go-lunatask"
 )
 
+func TestAllRelationshipStrengths(t *testing.T) {
+	t.Parallel()
+
+	strengths := lunatask.AllRelationshipStrengths()
+
+	// Check count
+	if got := len(strengths); got != 7 {
+		t.Fatalf("AllRelationshipStrengths() returned %d values, want 7", got)
+	}
+
+	// Check order (closest to most distant)
+	expected := []lunatask.RelationshipStrength{
+		lunatask.RelationshipFamily,
+		lunatask.RelationshipIntimateFriend,
+		lunatask.RelationshipCloseFriend,
+		lunatask.RelationshipCasualFriend,
+		lunatask.RelationshipAcquaintance,
+		lunatask.RelationshipBusiness,
+		lunatask.RelationshipAlmostStranger,
+	}
+	for i, want := range expected {
+		if strengths[i] != want {
+			t.Errorf("AllRelationshipStrengths()[%d] = %q, want %q", i, strengths[i], want)
+		}
+	}
+
+	// Check roundtrip: each value should be parseable
+	for _, strength := range strengths {
+		parsed, err := lunatask.ParseRelationshipStrength(strength.String())
+		if err != nil {
+			t.Errorf("ParseRelationshipStrength(%q) failed: %v", strength.String(), err)
+		}
+
+		if parsed != strength {
+			t.Errorf("ParseRelationshipStrength(%q) = %q, want %q", strength.String(), parsed, strength)
+		}
+	}
+}
+
 func TestParseRelationshipStrength(t *testing.T) {
 	t.Parallel()
 

status.go 🔗

@@ -5,12 +5,15 @@
 package lunatask
 
 import (
+	"encoding/json"
 	"errors"
 	"fmt"
 	"strings"
 )
 
 // TaskStatus represents the workflow state of a task.
+//
+//nolint:recvcheck // MarshalJSON must use value receiver, UnmarshalJSON must use pointer
 type TaskStatus string
 
 // Valid task status values.
@@ -18,7 +21,7 @@ const (
 	// StatusLater is the default status for new tasks.
 	StatusLater      TaskStatus = "later"
 	StatusNext       TaskStatus = "next"
-	StatusInProgress TaskStatus = "started"
+	StatusInProgress TaskStatus = "in-progress"
 	StatusWaiting    TaskStatus = "waiting"
 	StatusCompleted  TaskStatus = "completed"
 )
@@ -34,15 +37,61 @@ func (s TaskStatus) String() string {
 	return string(s)
 }
 
+// MarshalJSON implements [json.Marshaler].
+// Translates "in-progress" to "started" for the API.
+func (s TaskStatus) MarshalJSON() ([]byte, error) {
+	val := string(s)
+	if s == StatusInProgress {
+		val = "started"
+	}
+
+	data, err := json.Marshal(val)
+	if err != nil {
+		return nil, fmt.Errorf("marshaling task status: %w", err)
+	}
+
+	return data, nil
+}
+
+// UnmarshalJSON implements [json.Unmarshaler].
+// Translates "started" from the API to "in-progress".
+func (s *TaskStatus) UnmarshalJSON(data []byte) error {
+	var val string
+	if err := json.Unmarshal(data, &val); err != nil {
+		return fmt.Errorf("task status must be a string: %w", err)
+	}
+
+	if val == "started" {
+		*s = StatusInProgress
+
+		return nil
+	}
+
+	*s = TaskStatus(val)
+
+	return nil
+}
+
+// AllTaskStatuses returns all valid task status values in workflow order.
+func AllTaskStatuses() []TaskStatus {
+	return []TaskStatus{
+		StatusLater,
+		StatusNext,
+		StatusInProgress,
+		StatusWaiting,
+		StatusCompleted,
+	}
+}
+
 // ParseTaskStatus parses a string to a TaskStatus value (case-insensitive).
-// Valid values: "later", "next", "started", "in-progress", "waiting", "completed".
+// Valid values: "later", "next", "in-progress", "waiting", "completed".
 func ParseTaskStatus(str string) (TaskStatus, error) {
 	switch strings.ToLower(str) {
 	case "later":
 		return StatusLater, nil
 	case "next":
 		return StatusNext, nil
-	case "started", "in-progress":
+	case "in-progress":
 		return StatusInProgress, nil
 	case "waiting":
 		return StatusWaiting, nil

status_test.go 🔗

@@ -5,11 +5,49 @@
 package lunatask_test
 
 import (
+	"encoding/json"
 	"testing"
 
 	lunatask "git.secluded.site/go-lunatask"
 )
 
+func TestAllTaskStatuses(t *testing.T) {
+	t.Parallel()
+
+	statuses := lunatask.AllTaskStatuses()
+
+	// Check count
+	if got := len(statuses); got != 5 {
+		t.Fatalf("AllTaskStatuses() returned %d values, want 5", got)
+	}
+
+	// Check order
+	expected := []lunatask.TaskStatus{
+		lunatask.StatusLater,
+		lunatask.StatusNext,
+		lunatask.StatusInProgress,
+		lunatask.StatusWaiting,
+		lunatask.StatusCompleted,
+	}
+	for i, want := range expected {
+		if statuses[i] != want {
+			t.Errorf("AllTaskStatuses()[%d] = %q, want %q", i, statuses[i], want)
+		}
+	}
+
+	// Check roundtrip: each value should be parseable
+	for _, status := range statuses {
+		parsed, err := lunatask.ParseTaskStatus(status.String())
+		if err != nil {
+			t.Errorf("ParseTaskStatus(%q) failed: %v", status.String(), err)
+		}
+
+		if parsed != status {
+			t.Errorf("ParseTaskStatus(%q) = %q, want %q", status.String(), parsed, status)
+		}
+	}
+}
+
 func TestParseTaskStatus(t *testing.T) {
 	t.Parallel()
 
@@ -24,10 +62,9 @@ func TestParseTaskStatus(t *testing.T) {
 		{"later_mixed", "LaTeR", lunatask.StatusLater, false},
 		{"next_lower", "next", lunatask.StatusNext, false},
 		{"next_upper", "NEXT", lunatask.StatusNext, false},
-		{"started_lower", "started", lunatask.StatusInProgress, false},
-		{"started_upper", "STARTED", lunatask.StatusInProgress, false},
 		{"in_progress_lower", "in-progress", lunatask.StatusInProgress, false},
 		{"in_progress_upper", "IN-PROGRESS", lunatask.StatusInProgress, false},
+		{"in_progress_mixed", "In-Progress", lunatask.StatusInProgress, false},
 		{"waiting_lower", "waiting", lunatask.StatusWaiting, false},
 		{"waiting_upper", "WAITING", lunatask.StatusWaiting, false},
 		{"completed_lower", "completed", lunatask.StatusCompleted, false},
@@ -56,3 +93,73 @@ func TestParseTaskStatus(t *testing.T) {
 		})
 	}
 }
+
+func TestTaskStatus_MarshalJSON(t *testing.T) {
+	t.Parallel()
+
+	tests := []struct {
+		name  string
+		value lunatask.TaskStatus
+		want  string
+	}{
+		{"later", lunatask.StatusLater, `"later"`},
+		{"next", lunatask.StatusNext, `"next"`},
+		{"in_progress_to_started", lunatask.StatusInProgress, `"started"`},
+		{"waiting", lunatask.StatusWaiting, `"waiting"`},
+		{"completed", lunatask.StatusCompleted, `"completed"`},
+	}
+
+	for _, testCase := range tests {
+		t.Run(testCase.name, func(t *testing.T) {
+			t.Parallel()
+
+			got, err := json.Marshal(testCase.value)
+			if err != nil {
+				t.Fatalf("Marshal error: %v", err)
+			}
+
+			if string(got) != testCase.want {
+				t.Errorf("json.Marshal(%q) = %s, want %s", testCase.value, got, testCase.want)
+			}
+		})
+	}
+}
+
+func TestTaskStatus_UnmarshalJSON(t *testing.T) {
+	t.Parallel()
+
+	tests := []struct {
+		name    string
+		input   string
+		want    lunatask.TaskStatus
+		wantErr bool
+	}{
+		{"later", `"later"`, lunatask.StatusLater, false},
+		{"next", `"next"`, lunatask.StatusNext, false},
+		{"started_to_in_progress", `"started"`, lunatask.StatusInProgress, false},
+		{"in_progress", `"in-progress"`, lunatask.StatusInProgress, false},
+		{"waiting", `"waiting"`, lunatask.StatusWaiting, false},
+		{"completed", `"completed"`, lunatask.StatusCompleted, false},
+		{"invalid_type", `123`, "", true},
+	}
+
+	for _, testCase := range tests {
+		t.Run(testCase.name, func(t *testing.T) {
+			t.Parallel()
+
+			var got lunatask.TaskStatus
+
+			err := json.Unmarshal([]byte(testCase.input), &got)
+
+			if (err != nil) != testCase.wantErr {
+				t.Errorf("Unmarshal error = %v, wantErr %v", err, testCase.wantErr)
+
+				return
+			}
+
+			if !testCase.wantErr && got != testCase.want {
+				t.Errorf("Unmarshal(%s) = %q, want %q", testCase.input, got, testCase.want)
+			}
+		})
+	}
+}