From cffcd2ce9853ff4a3b009d6b4ad4649e0cd92ad1 Mon Sep 17 00:00:00 2001 From: Amolith Date: Fri, 26 Dec 2025 15:23:22 -0700 Subject: [PATCH] feat(enum): add All*() and status translation 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 --- 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(-) diff --git a/motivation.go b/motivation.go index e06cd6a4f21070f46d4e65de0505645a8331407a..cc42caaa49c91574aef13c8095a58cd50d8a745d 100644 --- a/motivation.go +++ b/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) { diff --git a/motivation_test.go b/motivation_test.go index a1403d52689d01e3ad23c208572365792df0e053..e13ebbfa0d996fbbc8309b5a6f49c5ff23316dfb 100644 --- a/motivation_test.go +++ b/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() diff --git a/priority.go b/priority.go index e72eb76b1b5b08cca4cd7155c60e62e30b628bb1..6b68cf6acec9eb387cdd2b66fc1b4dcf61ab7845 100644 --- a/priority.go +++ b/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) { diff --git a/priority_test.go b/priority_test.go index 729f214b52a6047a30d558ac0b4a4a33650ee6fb..d77db7baeea89862d2e7ab50ece41da8025df62d 100644 --- a/priority_test.go +++ b/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) { diff --git a/relationship.go b/relationship.go index 292fcc6eaac5e9c6c292e5a99c7a8bdd7574c9ec..3c74a16a89213eee8440320ff0d0ca4456cc05ee 100644 --- a/relationship.go +++ b/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". diff --git a/relationship_test.go b/relationship_test.go index 17d967723d93241aab25d8ea04b76d9fc0669946..8d8801d6ca22e2d04e7d333878a3d7063f4eb5f8 100644 --- a/relationship_test.go +++ b/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() diff --git a/status.go b/status.go index 9a618384329b8be352bbddb66b3f32712076645d..403b007f76c41af6ca7520ea55cae10e021cf98c 100644 --- a/status.go +++ b/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 diff --git a/status_test.go b/status_test.go index 07629193389249c699c208ad601bbe228547fe09..f42c4304c66de9b0509514c3b65ec753df94b9b0 100644 --- a/status_test.go +++ b/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) + } + }) + } +}