feat(motivation): add ParseMotivation function

Amolith created

Case-insensitive parsing for unknown/must/should/want values.

Assisted-by: Claude Sonnet 4 via Crush

Change summary

motivation.go      | 34 ++++++++++++++++++++++++++++++
motivation_test.go | 54 ++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 88 insertions(+)

Detailed changes

motivation.go 🔗

@@ -0,0 +1,34 @@
+// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+package lunatask
+
+import (
+	"errors"
+	"fmt"
+	"strings"
+)
+
+// Errors returned by Motivation operations.
+var (
+	// ErrInvalidMotivation is returned when parsing an unknown motivation string.
+	ErrInvalidMotivation = errors.New("invalid motivation")
+)
+
+// ParseMotivation parses a string to a Motivation value (case-insensitive).
+// Valid values: "unknown", "must", "should", "want".
+func ParseMotivation(str string) (Motivation, error) {
+	switch strings.ToLower(str) {
+	case "unknown":
+		return MotivationUnknown, nil
+	case "must":
+		return MotivationMust, nil
+	case "should":
+		return MotivationShould, nil
+	case "want":
+		return MotivationWant, nil
+	default:
+		return "", fmt.Errorf("%w: %q", ErrInvalidMotivation, str)
+	}
+}

motivation_test.go 🔗

@@ -0,0 +1,54 @@
+// 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 TestParseMotivation(t *testing.T) {
+	t.Parallel()
+
+	tests := []struct {
+		name    string
+		input   string
+		want    lunatask.Motivation
+		wantErr bool
+	}{
+		{"unknown_lower", "unknown", lunatask.MotivationUnknown, false},
+		{"unknown_upper", "UNKNOWN", lunatask.MotivationUnknown, false},
+		{"unknown_mixed", "UnKnOwN", lunatask.MotivationUnknown, false},
+		{"must_lower", "must", lunatask.MotivationMust, false},
+		{"must_upper", "MUST", lunatask.MotivationMust, false},
+		{"should_lower", "should", lunatask.MotivationShould, false},
+		{"should_upper", "SHOULD", lunatask.MotivationShould, false},
+		{"want_lower", "want", lunatask.MotivationWant, false},
+		{"want_upper", "WANT", lunatask.MotivationWant, false},
+		{"want_mixed", "WaNt", lunatask.MotivationWant, false},
+		{"invalid", "invalid", "", true},
+		{"empty", "", "", true},
+		{"numeric", "1", "", true},
+		{"typo", "muust", "", true},
+	}
+
+	for _, testCase := range tests {
+		t.Run(testCase.name, func(t *testing.T) {
+			t.Parallel()
+
+			got, err := lunatask.ParseMotivation(testCase.input)
+			if (err != nil) != testCase.wantErr {
+				t.Errorf("ParseMotivation(%q) error = %v, wantErr %v", testCase.input, err, testCase.wantErr)
+
+				return
+			}
+
+			if !testCase.wantErr && got != testCase.want {
+				t.Errorf("ParseMotivation(%q) = %q, want %q", testCase.input, got, testCase.want)
+			}
+		})
+	}
+}