9.0 KiB
9.0 KiB
Tasks: User Preferences API
Task Order (dependency sequence)
T1: Remove example scaffold (task-001)
- Scope: Delete all example entity files across every layer (domain, port, adapter, service, handlers, tests). This clears the path for preferences code without merge conflicts.
- Files:
- Delete
internal/domain/example.go - Delete
internal/domain/errors.go - Delete
internal/port/example.go - Delete
internal/adapter/memory/example.go - Delete
internal/service/example.go - Delete
internal/service/example_test.go - Delete
internal/api/handlers/example.go - Delete
internal/api/handlers/example_test.go
- Delete
- Depends on: None
- Acceptance criteria:
- All 8 example files are deleted
- No references to
Example,ExampleID,ExampleRepository, orExampleServiceremain in the codebase (routes.go, spec.go, and main.go will be updated in later tasks) - Service still compiles with routes.go/spec.go/main.go temporarily stubbed or commented
T2: Domain layer — preferences entity, validation, merge logic (task-002)
- Scope: Create the core domain types (
Preferences,NotificationSettings,PreferencesUpdate,NotificationSettingsUpdate,UserID), validation logic (Validate()), merge logic (MergeFrom()), default factory (NewDefaultPreferences()), and domain errors. - Files:
- Create
internal/domain/preferences.go - Create
internal/domain/errors.go
- Create
- Depends on: T1
- Acceptance criteria:
UserIDtype withString()andIsZero()methodsPreferencesstruct withUserID,Theme,Language,Notifications,UpdatedAtfieldsNotificationSettingsstruct withEmail,Push,DigestfieldsPreferencesUpdatestruct with pointer fields (*string,*bool) to distinguish provided vs. absentNotificationSettingsUpdatestruct with pointer fieldsNewDefaultPreferences(userID)returns preferences with all defaults:theme=system,language=en,email=true,push=true,digest=weeklyValidate()rejects invalid theme (not inlight/dark/system), invalid language (not matching^[a-z]{2}$), invalid digest (not indaily/weekly/never)MergeFrom()only overwrites fields where update pointer is non-nil; notifications sub-fields merged individually- Domain errors defined:
ErrPreferencesNotFound,ErrInvalidTheme,ErrInvalidLanguage,ErrInvalidDigest,ErrInvalidPreferences - Unit tests cover validation (valid + each invalid case), merge (partial updates, full updates, notifications sub-field merge), and defaults
T3: Port interface — PreferencesRepository (task-003)
- Scope: Define the
PreferencesRepositoryinterface in the port layer withGetandUpsertmethods. - Files:
- Create
internal/port/preferences.go
- Create
- Depends on: T2 (uses domain types)
- Acceptance criteria:
PreferencesRepositoryinterface withGet(ctx context.Context, userID domain.UserID) (*domain.Preferences, error)PreferencesRepositoryinterface withUpsert(ctx context.Context, userID domain.UserID, prefs *domain.Preferences) error- Interface is in
portpackage, imports onlydomainand standard library
T4: In-memory adapter — thread-safe map implementation (task-004)
- Scope: Implement
PreferencesRepositoryusing async.RWMutex-protectedmap[domain.UserID]*domain.Preferences. Returns copies to prevent aliasing. - Files:
- Create
internal/adapter/memory/preferences.go
- Create
- Depends on: T3 (implements port interface)
- Acceptance criteria:
NewPreferencesRepository()constructor returnsport.PreferencesRepositoryGet()returns a copy of stored preferences, ordomain.ErrPreferencesNotFoundif absentUpsert()stores a copy of the provided preferences (insert or replace)- Concurrent reads are safe (RWMutex read lock)
- Writes are serialized (RWMutex write lock)
- Unit tests cover: get existing, get missing (returns error), upsert new, upsert existing (overwrites)
T5: Service layer — PreferencesService with Get and Upsert (task-005)
- Scope: Implement
PreferencesServicewithGet(delegates to repo) andUpsert(fetch-or-create-defaults → merge → validate → persist) methods. - Files:
- Create
internal/service/preferences.go - Create
internal/service/preferences_test.go
- Create
- Depends on: T3 (uses port interface), T4 (test with in-memory adapter or mock)
- Acceptance criteria:
NewPreferencesService(repo port.PreferencesRepository, logger *slog.Logger)constructorGet(ctx, userID)returns preferences or propagatesErrPreferencesNotFoundUpsert(ctx, userID, update)fetches existing or creates defaults if not foundUpsertappliesMergeFrom()thenValidate()thenrepo.Upsert()UpsertsetsUpdatedAtto current time before persisting- Returns validation errors from domain layer unchanged (handler maps them)
- Tests use mock repository implementing
port.PreferencesRepository - Tests cover: get success, get not-found, upsert creates new with defaults + merge, upsert updates existing with merge, upsert rejects invalid values (validation error propagated)
T6: HTTP handlers — GET and PUT with auth, validation, error mapping (task-006)
- Scope: Implement
PreferencesHandlerwithGetandUpdatemethods. Includes UUID validation ofuser_idpath param, authorization check (own-user or admin), request binding with strict unknown-field rejection, domain-to-HTTP error mapping, and response envelope formatting. - Files:
- Create
internal/api/handlers/preferences.go - Create
internal/api/handlers/preferences_test.go
- Create
- Depends on: T5 (uses service layer)
- Acceptance criteria:
NewPreferencesHandler(svc *service.PreferencesService)constructorGet(w, r) error— extractsuser_idfrom chi URL param, validates UUID format, checks authorization, calls service, returnshttpresponse.OKwith envelopeUpdate(w, r) error— extractsuser_id, validates UUID, checks authorization, binds request body withapp.BindAndValidate, rejects unknown top-level preference keys, calls serviceUpsert, returnshttpresponse.OKwith merged resultmapDomainError()mapsErrPreferencesNotFound→ 404,ErrInvalidTheme/Language/Digest/Preferences→ 400- Authorization: extracts user from
auth.GetUser(ctx), compares withuser_idpath param, allows if match oruser.HasRole("admin"); returnshttperror.Forbiddenotherwise - Response types:
PreferencesResponse,PreferencesDataResponse,NotificationSettingsResponseas defined in design - Request types:
UpdatePreferencesRequestwithPreferencesPayloadusing pointer fields andvalidate:"required"tag - Tests cover: GET success, GET not found, GET forbidden (wrong user), PUT success (create), PUT success (merge), PUT bad request (invalid values), PUT bad request (unknown keys), PUT bad request (missing preferences), PUT forbidden
T7: Routes and OpenAPI spec — wire endpoints and document API (task-007)
- Scope: Update
routes.goto mount GET and PUT/preferences/{user_id}under auth middleware. Updatespec.goto document both endpoints with schemas, examples, and error responses. Remove all example endpoint references. - Files:
- Modify
internal/api/routes.go - Modify
internal/api/spec.go
- Modify
- Depends on: T6 (uses handler methods)
- Acceptance criteria:
- All example routes removed from
routes.go GET /api/preferences-api/preferences/{user_id}route registered withapp.Wrap(handler.Get)PUT /api/preferences-api/preferences/{user_id}route registered withapp.Wrap(handler.Update)- Both preference routes are inside an
auth.Middleware()group - Health endpoint remains at
/api/preferences-api/health(unchanged) - OpenAPI spec defines
Preferences,PreferencesUpdate,NotificationSettingsschemas - OpenAPI spec documents GET and PUT endpoints with parameters, request bodies, responses (200, 400, 403, 404)
- All example schemas and paths removed from spec
- All example routes removed from
T8: Wire main.go and integration — connect all layers (task-008)
- Scope: Update
main.goto instantiatememory.NewPreferencesRepository(),service.NewPreferencesService(), and pass them through route registration. Verify the full service starts and endpoints respond correctly. - Files:
- Modify
cmd/server/main.go
- Modify
- Depends on: T7 (routes ready to accept handler)
- Acceptance criteria:
main.gocreatesmemory.NewPreferencesRepository()main.gocreatesservice.NewPreferencesService(repo, logger)main.gopasses service to route registration (handler created in routes or passed through)- All references to
ExampleRepositoryandExampleServiceremoved - Service compiles:
go build ./...succeeds - All tests pass:
go test ./...succeeds - Service starts without error and health endpoint responds