diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 8d9c4de..c8f1d51 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -1,26 +1,26 @@ -{"id":"checkvist-api-0l6","title":"Implement Checklist archive/unarchive","description":"Add P2 (nice-to-have) archive functionality to ChecklistService:\n- Archive(ctx, id) (*Checklist, error)\n- Unarchive(ctx, id) (*Checklist, error)\nUse PUT /checklists/{id}.json with archived field.","status":"open","priority":2,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:30:56.538852525+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T12:30:56.538852525+01:00","dependencies":[{"issue_id":"checkvist-api-0l6","depends_on_id":"checkvist-api-c2k","type":"blocks","created_at":"2026-01-14T12:33:02.869871005+01:00","created_by":"Oliver Jakoubek"}]} -{"id":"checkvist-api-1e6","title":"Project Setup \u0026 Foundation","description":"Phase 1: Set up Go module structure, basic client, authentication, and error handling. This is the foundation for all other work.","status":"open","priority":0,"issue_type":"epic","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:05.916898362+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T12:31:05.916898362+01:00"} -{"id":"checkvist-api-1ki","title":"Write GoDoc examples","description":"Create example_test.go with runnable examples:\n- Example_basicUsage\n- ExampleNewClient\n- ExampleClient_Authenticate\n- ExampleChecklistService_List\n- ExampleTaskService_Create\n- ExampleNewTask (builder pattern)\n- ExampleNewFilter (filter builder)\n- ExampleFilter_Apply\nAll examples should be runnable and appear in GoDoc.","status":"open","priority":0,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:38.443075101+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T12:31:38.443075101+01:00","dependencies":[{"issue_id":"checkvist-api-1ki","depends_on_id":"checkvist-api-c2k","type":"blocks","created_at":"2026-01-14T12:33:14.730667505+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"checkvist-api-1ki","depends_on_id":"checkvist-api-rl9","type":"blocks","created_at":"2026-01-14T12:33:15.039931257+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"checkvist-api-1ki","depends_on_id":"checkvist-api-1ze","type":"blocks","created_at":"2026-01-14T12:33:15.388830036+01:00","created_by":"Oliver Jakoubek"}]} -{"id":"checkvist-api-1ze","title":"Implement client-side Filter builder","description":"Create filter.go with Filter builder (since Checkvist API has no server-side filtering):\n- NewFilter(tasks []Task) *Filter\n- WithTag(tag string) *Filter\n- WithTags(tags ...string) *Filter (AND logic)\n- WithStatus(status TaskStatus) *Filter\n- WithDueBefore(t time.Time) *Filter\n- WithDueAfter(t time.Time) *Filter\n- WithDueOn(t time.Time) *Filter\n- WithOverdue() *Filter\n- WithSearch(query string) *Filter (searches in Content)\n- Apply() []Task\nPerformance target: \u003c10ms for 1000+ tasks\nFilters are combined with AND logic.","status":"open","priority":1,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:30:56.24379077+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T12:30:56.24379077+01:00","dependencies":[{"issue_id":"checkvist-api-1ze","depends_on_id":"checkvist-api-rl9","type":"blocks","created_at":"2026-01-14T12:33:02.543121262+01:00","created_by":"Oliver Jakoubek"}]} -{"id":"checkvist-api-1zf","title":"Write unit tests for Filter","description":"Create filter_test.go with tests:\n- TestFilter_WithTag\n- TestFilter_WithMultipleTags\n- TestFilter_WithStatus\n- TestFilter_WithDueBefore\n- TestFilter_WithDueAfter\n- TestFilter_WithDueOn\n- TestFilter_WithOverdue\n- TestFilter_WithSearch\n- TestFilter_Combined\n- TestFilter_Performance_1000Tasks\nUse table-driven tests.","status":"open","priority":1,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:38.136650557+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T12:31:38.136650557+01:00","dependencies":[{"issue_id":"checkvist-api-1zf","depends_on_id":"checkvist-api-1ze","type":"blocks","created_at":"2026-01-14T12:33:14.4378645+01:00","created_by":"Oliver Jakoubek"}]} +{"id":"checkvist-api-0l6","title":"Implement Checklist archive/unarchive","description":"Add P2 (nice-to-have) archive functionality to ChecklistService:\n- Archive(ctx, id) (*Checklist, error)\n- Unarchive(ctx, id) (*Checklist, error)\nUse PUT /checklists/{id}.json with archived field.","status":"closed","priority":2,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:30:56.538852525+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T14:36:35.386071331+01:00","closed_at":"2026-01-14T14:36:35.386071331+01:00","close_reason":"Archive und Unarchive Methoden implementiert und getestet","dependencies":[{"issue_id":"checkvist-api-0l6","depends_on_id":"checkvist-api-c2k","type":"blocks","created_at":"2026-01-14T12:33:02.869871005+01:00","created_by":"Oliver Jakoubek"}]} +{"id":"checkvist-api-1e6","title":"Project Setup \u0026 Foundation","description":"Phase 1: Set up Go module structure, basic client, authentication, and error handling. This is the foundation for all other work.","status":"closed","priority":0,"issue_type":"epic","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:05.916898362+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T14:29:26.984811878+01:00","closed_at":"2026-01-14T14:29:26.984811878+01:00","close_reason":"Alle zugehörigen Tasks abgeschlossen"} +{"id":"checkvist-api-1ki","title":"Write GoDoc examples","description":"Create example_test.go with runnable examples:\n- Example_basicUsage\n- ExampleNewClient\n- ExampleClient_Authenticate\n- ExampleChecklistService_List\n- ExampleTaskService_Create\n- ExampleNewTask (builder pattern)\n- ExampleNewFilter (filter builder)\n- ExampleFilter_Apply\nAll examples should be runnable and appear in GoDoc.","status":"closed","priority":0,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:38.443075101+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T14:35:43.090941735+01:00","closed_at":"2026-01-14T14:35:43.090941735+01:00","close_reason":"GoDoc examples für alle geforderten Funktionen implementiert","dependencies":[{"issue_id":"checkvist-api-1ki","depends_on_id":"checkvist-api-c2k","type":"blocks","created_at":"2026-01-14T12:33:14.730667505+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"checkvist-api-1ki","depends_on_id":"checkvist-api-rl9","type":"blocks","created_at":"2026-01-14T12:33:15.039931257+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"checkvist-api-1ki","depends_on_id":"checkvist-api-1ze","type":"blocks","created_at":"2026-01-14T12:33:15.388830036+01:00","created_by":"Oliver Jakoubek"}]} +{"id":"checkvist-api-1ze","title":"Implement client-side Filter builder","description":"Create filter.go with Filter builder (since Checkvist API has no server-side filtering):\n- NewFilter(tasks []Task) *Filter\n- WithTag(tag string) *Filter\n- WithTags(tags ...string) *Filter (AND logic)\n- WithStatus(status TaskStatus) *Filter\n- WithDueBefore(t time.Time) *Filter\n- WithDueAfter(t time.Time) *Filter\n- WithDueOn(t time.Time) *Filter\n- WithOverdue() *Filter\n- WithSearch(query string) *Filter (searches in Content)\n- Apply() []Task\nPerformance target: \u003c10ms for 1000+ tasks\nFilters are combined with AND logic.","status":"closed","priority":1,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:30:56.24379077+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T14:33:38.687033358+01:00","closed_at":"2026-01-14T14:33:38.687033358+01:00","close_reason":"Filter builder mit allen geforderten Methoden implementiert","dependencies":[{"issue_id":"checkvist-api-1ze","depends_on_id":"checkvist-api-rl9","type":"blocks","created_at":"2026-01-14T12:33:02.543121262+01:00","created_by":"Oliver Jakoubek"}]} +{"id":"checkvist-api-1zf","title":"Write unit tests for Filter","description":"Create filter_test.go with tests:\n- TestFilter_WithTag\n- TestFilter_WithMultipleTags\n- TestFilter_WithStatus\n- TestFilter_WithDueBefore\n- TestFilter_WithDueAfter\n- TestFilter_WithDueOn\n- TestFilter_WithOverdue\n- TestFilter_WithSearch\n- TestFilter_Combined\n- TestFilter_Performance_1000Tasks\nUse table-driven tests.","status":"closed","priority":1,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:38.136650557+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T14:34:50.361097261+01:00","closed_at":"2026-01-14T14:34:50.361097261+01:00","close_reason":"Alle geforderten Tests implementiert und bestanden","dependencies":[{"issue_id":"checkvist-api-1zf","depends_on_id":"checkvist-api-1ze","type":"blocks","created_at":"2026-01-14T12:33:14.4378645+01:00","created_by":"Oliver Jakoubek"}]} {"id":"checkvist-api-347","title":"Write unit tests for Checklists","description":"Create checklists_test.go with tests:\n- TestChecklists_List\n- TestChecklists_ListArchived\n- TestChecklists_Get\n- TestChecklists_Get_NotFound\n- TestChecklists_Create\n- TestChecklists_Update\n- TestChecklists_Delete\nUse table-driven tests. Create testdata/checklists/ fixtures.","status":"closed","priority":0,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:37.243525209+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T13:41:58.076714398+01:00","closed_at":"2026-01-14T13:41:58.076714398+01:00","close_reason":"Closed","dependencies":[{"issue_id":"checkvist-api-347","depends_on_id":"checkvist-api-c2k","type":"blocks","created_at":"2026-01-14T12:33:13.512887479+01:00","created_by":"Oliver Jakoubek"}]} -{"id":"checkvist-api-47y","title":"Quality \u0026 Documentation","description":"Phase 4: Complete unit tests, GoDoc examples, README, and CHANGELOG. Target \u003e80% test coverage. All public functions documented with examples.","status":"open","priority":0,"issue_type":"epic","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:36.655509387+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T12:31:36.655509387+01:00"} +{"id":"checkvist-api-47y","title":"Quality \u0026 Documentation","description":"Phase 4: Complete unit tests, GoDoc examples, README, and CHANGELOG. Target \u003e80% test coverage. All public functions documented with examples.","status":"closed","priority":0,"issue_type":"epic","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:36.655509387+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T14:37:52.12671299+01:00","closed_at":"2026-01-14T14:37:52.12671299+01:00","close_reason":"Alle zugehörigen Features und Tasks abgeschlossen"} {"id":"checkvist-api-5ab","title":"Implement Note operations","description":"Create notes.go with NoteService:\n- client.Notes(checklistID, taskID) returns NoteService\n- List(ctx) ([]Note, error) - GET /checklists/{id}/tasks/{task_id}/comments.json\n- Create(ctx, comment string) (*Note, error) - POST /checklists/{id}/tasks/{task_id}/comments.json\n- Update(ctx, noteID, comment string) (*Note, error) - PUT /checklists/{id}/tasks/{task_id}/comments/{note_id}.json\n- Delete(ctx, noteID) error - DELETE /checklists/{id}/tasks/{task_id}/comments/{note_id}.json\nContext support for all methods.","status":"closed","priority":0,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:30:54.268124634+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T13:40:31.06124818+01:00","closed_at":"2026-01-14T13:40:31.06124818+01:00","close_reason":"Closed","dependencies":[{"issue_id":"checkvist-api-5ab","depends_on_id":"checkvist-api-rl9","type":"blocks","created_at":"2026-01-14T12:32:55.843507717+01:00","created_by":"Oliver Jakoubek"}]} {"id":"checkvist-api-5wr","title":"Initialize Go module and project structure","description":"Create go.mod with module path code.beautifulmachines.dev/jakoubek/checkvist-api. Set up flat package structure with placeholder files: client.go, checklists.go, tasks.go, notes.go, filter.go, models.go, errors.go, options.go. Create magefiles/ directory with separate go.mod for Mage build targets. Add LICENSE (MIT) file.","status":"closed","priority":0,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:06.285510329+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T12:43:45.392753825+01:00","closed_at":"2026-01-14T12:43:45.392753825+01:00","close_reason":"Closed"} {"id":"checkvist-api-8bn","title":"Write unit tests for Client and Auth","description":"Create client_test.go with tests using httptest.Server:\n- TestNewClient_Defaults\n- TestNewClient_WithOptions\n- TestAuthenticate_Success\n- TestAuthenticate_InvalidCredentials\n- TestAuthenticate_2FA\n- TestTokenRefresh_Auto\n- TestTokenRefresh_Manual\n- TestCurrentUser\n- TestRetryLogic_429\n- TestRetryLogic_5xx\n- TestRetryLogic_NetworkError\nUse table-driven tests. Create testdata/auth/ fixtures.","status":"closed","priority":0,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:36.964610587+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T13:35:19.981723023+01:00","closed_at":"2026-01-14T13:35:19.981723023+01:00","close_reason":"Closed","dependencies":[{"issue_id":"checkvist-api-8bn","depends_on_id":"checkvist-api-lpn","type":"blocks","created_at":"2026-01-14T12:33:12.783142853+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"checkvist-api-8bn","depends_on_id":"checkvist-api-8u6","type":"blocks","created_at":"2026-01-14T12:33:13.232028837+01:00","created_by":"Oliver Jakoubek"}]} -{"id":"checkvist-api-8jh","title":"Implement repeating tasks configuration","description":"Add P2 (nice-to-have) repeat support to TaskBuilder:\n- WithRepeat(pattern string) *TaskBuilder\nSupport Checkvist smart syntax for repeats.\nDocument common patterns in GoDoc.","status":"open","priority":2,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:30:56.826106108+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T12:30:56.826106108+01:00","dependencies":[{"issue_id":"checkvist-api-8jh","depends_on_id":"checkvist-api-tjk","type":"blocks","created_at":"2026-01-14T12:33:03.159849575+01:00","created_by":"Oliver Jakoubek"}]} +{"id":"checkvist-api-8jh","title":"Implement repeating tasks configuration","description":"Add P2 (nice-to-have) repeat support to TaskBuilder:\n- WithRepeat(pattern string) *TaskBuilder\nSupport Checkvist smart syntax for repeats.\nDocument common patterns in GoDoc.","status":"closed","priority":2,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:30:56.826106108+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T14:37:33.200281826+01:00","closed_at":"2026-01-14T14:37:33.200281826+01:00","close_reason":"WithRepeat Methode zu TaskBuilder hinzugefügt mit GoDoc-Dokumentation","dependencies":[{"issue_id":"checkvist-api-8jh","depends_on_id":"checkvist-api-tjk","type":"blocks","created_at":"2026-01-14T12:33:03.159849575+01:00","created_by":"Oliver Jakoubek"}]} {"id":"checkvist-api-8q3","title":"Set up Mage build targets","description":"Create magefiles/magefile.go with:\n- Test() - run go test -v ./...\n- Coverage() - run go test -coverprofile=coverage.out ./...\n- Lint() - run staticcheck ./...\n- Fmt() - run gofmt -w .\n- Check() - run all quality checks (fmt, vet, staticcheck, test)\nEnsure magefiles has its own go.mod importing magefile.org/mage","status":"closed","priority":0,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:09.228450637+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T13:33:08.511791793+01:00","closed_at":"2026-01-14T13:33:08.511791793+01:00","close_reason":"Closed","dependencies":[{"issue_id":"checkvist-api-8q3","depends_on_id":"checkvist-api-5wr","type":"blocks","created_at":"2026-01-14T12:32:48.556022687+01:00","created_by":"Oliver Jakoubek"}]} {"id":"checkvist-api-8u6","title":"Implement HTTP request helper with retry logic","description":"Add internal HTTP helper to client.go:\n- doRequest(ctx, method, path, body) helper for all API calls\n- Automatic authentication check before requests\n- JSON marshaling/unmarshaling\n- Exponential backoff retry for:\n - HTTP 429 (Too Many Requests)\n - HTTP 5xx (Server Errors)\n - Network errors (timeout, connection reset)\n- Respect context cancellation\n- Optional debug logging of requests/responses via slog","status":"closed","priority":0,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:08.780244392+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T13:27:52.914675409+01:00","closed_at":"2026-01-14T13:27:52.914675409+01:00","close_reason":"Closed","dependencies":[{"issue_id":"checkvist-api-8u6","depends_on_id":"checkvist-api-ymg","type":"blocks","created_at":"2026-01-14T12:32:47.973194416+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"checkvist-api-8u6","depends_on_id":"checkvist-api-mnh","type":"blocks","created_at":"2026-01-14T12:32:48.268500727+01:00","created_by":"Oliver Jakoubek"}]} {"id":"checkvist-api-93m","title":"Create CHANGELOG","description":"Create CHANGELOG.md following Keep a Changelog format:\n- [Unreleased] section for ongoing work\n- Initial release preparation notes\n- Document all features implemented","status":"closed","priority":0,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:39.009748936+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T13:37:00.644317886+01:00","closed_at":"2026-01-14T13:37:00.644317886+01:00","close_reason":"Closed"} {"id":"checkvist-api-bbx","title":"Write unit tests for Notes","description":"Create notes_test.go with tests:\n- TestNotes_List\n- TestNotes_Create\n- TestNotes_Update\n- TestNotes_Delete\nUse table-driven tests. Create testdata/notes/ fixtures.","status":"closed","priority":0,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:37.829382141+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T13:47:17.141162679+01:00","closed_at":"2026-01-14T13:47:17.141162679+01:00","close_reason":"Closed","dependencies":[{"issue_id":"checkvist-api-bbx","depends_on_id":"checkvist-api-5ab","type":"blocks","created_at":"2026-01-14T12:33:14.119755191+01:00","created_by":"Oliver Jakoubek"}]} -{"id":"checkvist-api-br3","title":"Core API Operations","description":"Phase 2: Implement CRUD operations for Checklists, Tasks, and Notes. All P0 (must-have) features for the library.","status":"open","priority":0,"issue_type":"epic","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:30:53.20627925+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T12:30:53.20627925+01:00"} +{"id":"checkvist-api-br3","title":"Core API Operations","description":"Phase 2: Implement CRUD operations for Checklists, Tasks, and Notes. All P0 (must-have) features for the library.","status":"closed","priority":0,"issue_type":"epic","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:30:53.20627925+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T14:29:26.991139668+01:00","closed_at":"2026-01-14T14:29:26.991139668+01:00","close_reason":"Alle zugehörigen Tasks abgeschlossen"} {"id":"checkvist-api-c2k","title":"Implement Checklist operations","description":"Create checklists.go with ChecklistService:\n- client.Checklists() returns ChecklistService\n- List(ctx) ([]Checklist, error) - GET /checklists.json\n- Get(ctx, id) (*Checklist, error) - GET /checklists/{id}.json\n- Create(ctx, name) (*Checklist, error) - POST /checklists.json\n- Update(ctx, id, name) (*Checklist, error) - PUT /checklists/{id}.json\n- Delete(ctx, id) error - DELETE /checklists/{id}.json\n- Support archived filter in List\nContext support for all methods.","status":"closed","priority":0,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:30:53.566197933+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T13:38:06.162666425+01:00","closed_at":"2026-01-14T13:38:06.162666425+01:00","close_reason":"Closed","dependencies":[{"issue_id":"checkvist-api-c2k","depends_on_id":"checkvist-api-8u6","type":"blocks","created_at":"2026-01-14T12:32:54.533462004+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"checkvist-api-c2k","depends_on_id":"checkvist-api-lpn","type":"blocks","created_at":"2026-01-14T12:32:54.859645166+01:00","created_by":"Oliver Jakoubek"}]} -{"id":"checkvist-api-cb8","title":"Extended Features","description":"Phase 3: Implement P1 (should-have) features including client-side filtering and builder patterns for fluent interfaces.","status":"open","priority":1,"issue_type":"epic","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:30:55.624242123+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T12:30:55.624242123+01:00"} +{"id":"checkvist-api-cb8","title":"Extended Features","description":"Phase 3: Implement P1 (should-have) features including client-side filtering and builder patterns for fluent interfaces.","status":"closed","priority":1,"issue_type":"epic","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:30:55.624242123+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T14:37:52.141188241+01:00","closed_at":"2026-01-14T14:37:52.141188241+01:00","close_reason":"Alle zugehörigen Features und Tasks abgeschlossen"} {"id":"checkvist-api-e9p","title":"Implement data models","description":"Create models.go with all data structures:\n- Checklist struct (ID, Name, Public, Archived, ReadOnly, TaskCount, TaskCompleted, Tags, TagsAsText, UpdatedAt)\n- Task struct (ID, ChecklistID, ParentID, Content, Status, Position, Priority, Tags, TagsAsText, DueDateRaw, DueDate, AssigneeIDs, CommentsCount, UpdateLine, UpdatedAt, CreatedAt, Children, Notes)\n- TaskStatus enum (StatusOpen=0, StatusClosed=1, StatusInvalidated=2)\n- Note struct (ID, TaskID, Comment, UpdatedAt, CreatedAt)\n- Tags type (map[string]bool)\n- User struct (ID, Username, Email)\n- DueDate struct with constructors (DueAt, DueString, DueInDays) and constants (DueToday, DueTomorrow, DueNextWeek, DueNextMonth)","status":"closed","priority":0,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:06.900391036+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T13:22:22.273934664+01:00","closed_at":"2026-01-14T13:22:22.273934664+01:00","close_reason":"Closed","dependencies":[{"issue_id":"checkvist-api-e9p","depends_on_id":"checkvist-api-5wr","type":"blocks","created_at":"2026-01-14T12:32:46.433908937+01:00","created_by":"Oliver Jakoubek"}]} {"id":"checkvist-api-lpn","title":"Implement authentication with auto token renewal","description":"Add to client.go:\n- Authenticate(ctx context.Context) error - explicit login\n- refreshToken(ctx context.Context) error - token renewal\n- ensureAuthenticated(ctx context.Context) error - auto-auth before requests\n- CurrentUser(ctx context.Context) (*User, error) - get logged in user\n- Token management: store token and expiry, auto-refresh before expiry\n- Thread-safe token access using mutex\n- Support for optional 2FA token\nAPI endpoints:\n- POST /auth/login.json?version=2 (login)\n- POST /auth/refresh_token.json?version=2 (refresh)\n- GET /auth/curr_user.json (current user)\nToken sent via X-Client-Token header","status":"closed","priority":0,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:08.358878117+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T13:26:32.668016856+01:00","closed_at":"2026-01-14T13:26:32.668016856+01:00","close_reason":"Closed","dependencies":[{"issue_id":"checkvist-api-lpn","depends_on_id":"checkvist-api-ymg","type":"blocks","created_at":"2026-01-14T12:32:47.656124681+01:00","created_by":"Oliver Jakoubek"}]} {"id":"checkvist-api-mnh","title":"Implement error types and sentinel errors","description":"Create errors.go with:\n- APIError struct (StatusCode, Message, RequestID, Err) with Error() and Unwrap() methods\n- Sentinel errors: ErrUnauthorized, ErrNotFound, ErrRateLimited, ErrBadRequest, ErrServerError\n- Helper function to create APIError from HTTP response","status":"closed","priority":0,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:07.619359293+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T13:23:43.296883265+01:00","closed_at":"2026-01-14T13:23:43.296883265+01:00","close_reason":"Closed","dependencies":[{"issue_id":"checkvist-api-mnh","depends_on_id":"checkvist-api-5wr","type":"blocks","created_at":"2026-01-14T12:32:46.754134799+01:00","created_by":"Oliver Jakoubek"}]} {"id":"checkvist-api-nrk","title":"Create README with quickstart","description":"Create README.md (in English) with:\n- Project description\n- Installation: go get code.beautifulmachines.dev/jakoubek/checkvist-api\n- Quick start example (init client, list checklists, create task)\n- API overview (Checklists, Tasks, Notes, Filters)\n- Builder pattern examples\n- Error handling examples\n- Configuration options\n- Link to GoDoc\n- License (MIT)","status":"closed","priority":0,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:38.724338606+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T13:48:30.163762004+01:00","closed_at":"2026-01-14T13:48:30.163762004+01:00","close_reason":"Closed","dependencies":[{"issue_id":"checkvist-api-nrk","depends_on_id":"checkvist-api-c2k","type":"blocks","created_at":"2026-01-14T12:33:15.785698203+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"checkvist-api-nrk","depends_on_id":"checkvist-api-rl9","type":"blocks","created_at":"2026-01-14T12:33:16.125115134+01:00","created_by":"Oliver Jakoubek"}]} {"id":"checkvist-api-rl9","title":"Implement Task operations","description":"Create tasks.go with TaskService:\n- client.Tasks(checklistID) returns TaskService\n- List(ctx) ([]Task, error) - GET /checklists/{id}/tasks.json\n- Get(ctx, taskID) (*Task, error) - GET /checklists/{id}/tasks/{task_id}.json (includes parent hierarchy)\n- Create(ctx, builder *TaskBuilder) (*Task, error) - POST /checklists/{id}/tasks.json\n- Update(ctx, taskID, opts) (*Task, error) - PUT /checklists/{id}/tasks/{task_id}.json\n- Delete(ctx, taskID) error - DELETE /checklists/{id}/tasks/{task_id}.json\n- Close(ctx, taskID) (*Task, error) - POST /checklists/{id}/tasks/{task_id}/close.json\n- Reopen(ctx, taskID) (*Task, error) - POST /checklists/{id}/tasks/{task_id}/reopen.json\n- Invalidate(ctx, taskID) (*Task, error) - POST /checklists/{id}/tasks/{task_id}/invalidate.json\nParse DueDate from DueDateRaw when retrieving tasks.","status":"closed","priority":0,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:30:53.90629838+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T13:39:29.443727155+01:00","closed_at":"2026-01-14T13:39:29.443727155+01:00","close_reason":"Closed","dependencies":[{"issue_id":"checkvist-api-rl9","depends_on_id":"checkvist-api-8u6","type":"blocks","created_at":"2026-01-14T12:32:55.247292478+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"checkvist-api-rl9","depends_on_id":"checkvist-api-lpn","type":"blocks","created_at":"2026-01-14T12:32:55.536931433+01:00","created_by":"Oliver Jakoubek"}]} -{"id":"checkvist-api-tjk","title":"Implement TaskBuilder fluent interface","description":"Enhance task creation with builder pattern:\n- NewTask(content string) *TaskBuilder\n- WithTags(tags ...string) *TaskBuilder\n- WithDueDate(due DueDate) *TaskBuilder\n- WithPriority(p int) *TaskBuilder\n- WithParent(parentID int64) *TaskBuilder\n- WithPosition(pos int) *TaskBuilder\n- Build() returns parameters for API call\nTaskBuilder should be chainable and return itself for fluent usage:\n checkvist.NewTask(\"Content\").WithTags(\"tag1\").WithDueDate(checkvist.DueTomorrow).WithPriority(1)","status":"open","priority":1,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:30:55.929907579+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T12:30:55.929907579+01:00","dependencies":[{"issue_id":"checkvist-api-tjk","depends_on_id":"checkvist-api-rl9","type":"blocks","created_at":"2026-01-14T12:33:02.20339206+01:00","created_by":"Oliver Jakoubek"}]} +{"id":"checkvist-api-tjk","title":"Implement TaskBuilder fluent interface","description":"Enhance task creation with builder pattern:\n- NewTask(content string) *TaskBuilder\n- WithTags(tags ...string) *TaskBuilder\n- WithDueDate(due DueDate) *TaskBuilder\n- WithPriority(p int) *TaskBuilder\n- WithParent(parentID int64) *TaskBuilder\n- WithPosition(pos int) *TaskBuilder\n- Build() returns parameters for API call\nTaskBuilder should be chainable and return itself for fluent usage:\n checkvist.NewTask(\"Content\").WithTags(\"tag1\").WithDueDate(checkvist.DueTomorrow).WithPriority(1)","status":"closed","priority":1,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:30:55.929907579+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T14:30:42.405125498+01:00","closed_at":"2026-01-14T14:30:42.405125498+01:00","close_reason":"TaskBuilder war bereits vollständig implementiert und getestet","dependencies":[{"issue_id":"checkvist-api-tjk","depends_on_id":"checkvist-api-rl9","type":"blocks","created_at":"2026-01-14T12:33:02.20339206+01:00","created_by":"Oliver Jakoubek"}]} {"id":"checkvist-api-v2f","title":"Write unit tests for Tasks","description":"Create tasks_test.go with tests:\n- TestTasks_List\n- TestTasks_Get\n- TestTasks_Get_WithParentHierarchy\n- TestTasks_Create\n- TestTasks_Create_WithBuilder\n- TestTasks_Update\n- TestTasks_Delete\n- TestTasks_Close\n- TestTasks_Reopen\n- TestTasks_Invalidate\n- TestDueDate_Parsing\nUse table-driven tests. Create testdata/tasks/ fixtures.","status":"closed","priority":0,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:37.538754679+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T13:46:01.50434559+01:00","closed_at":"2026-01-14T13:46:01.50434559+01:00","close_reason":"Closed","dependencies":[{"issue_id":"checkvist-api-v2f","depends_on_id":"checkvist-api-rl9","type":"blocks","created_at":"2026-01-14T12:33:13.81085058+01:00","created_by":"Oliver Jakoubek"}]} {"id":"checkvist-api-ymg","title":"Implement Client struct with functional options","description":"Create client.go with:\n- Client struct (baseURL, username, remoteKey, token, tokenExp, httpClient, retryConf, logger, mu sync.RWMutex)\n- NewClient(username, remoteKey string, opts ...Option) *Client constructor\n- Create options.go with Option type and functional options:\n - WithHTTPClient(*http.Client)\n - WithTimeout(time.Duration)\n - WithRetryConfig(RetryConfig)\n - WithLogger(*slog.Logger)\n - WithBaseURL(string) for testing\n- RetryConfig struct (MaxRetries, BaseDelay, MaxDelay, Jitter)\n- Default values: 3 retries, 1s base delay, 30s max delay, jitter enabled","status":"closed","priority":0,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:08.021154076+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T13:24:55.101093793+01:00","closed_at":"2026-01-14T13:24:55.101093793+01:00","close_reason":"Closed","dependencies":[{"issue_id":"checkvist-api-ymg","depends_on_id":"checkvist-api-e9p","type":"blocks","created_at":"2026-01-14T12:32:47.077541448+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"checkvist-api-ymg","depends_on_id":"checkvist-api-mnh","type":"blocks","created_at":"2026-01-14T12:32:47.358639632+01:00","created_by":"Oliver Jakoubek"}]} diff --git a/checklists.go b/checklists.go index 4199f5a..dcb680e 100644 --- a/checklists.go +++ b/checklists.go @@ -91,3 +91,32 @@ func (s *ChecklistService) Delete(ctx context.Context, id int) error { path := fmt.Sprintf("/checklists/%d.json", id) return s.client.doDelete(ctx, path) } + +// archiveRequest is the request body for archiving/unarchiving a checklist. +type archiveRequest struct { + Archived bool `json:"archived"` +} + +// Archive archives a checklist by ID. +func (s *ChecklistService) Archive(ctx context.Context, id int) (*Checklist, error) { + path := fmt.Sprintf("/checklists/%d.json", id) + body := archiveRequest{Archived: true} + + var checklist Checklist + if err := s.client.doPut(ctx, path, body, &checklist); err != nil { + return nil, err + } + return &checklist, nil +} + +// Unarchive unarchives a checklist by ID. +func (s *ChecklistService) Unarchive(ctx context.Context, id int) (*Checklist, error) { + path := fmt.Sprintf("/checklists/%d.json", id) + body := archiveRequest{Archived: false} + + var checklist Checklist + if err := s.client.doPut(ctx, path, body, &checklist); err != nil { + return nil, err + } + return &checklist, nil +} diff --git a/checklists_test.go b/checklists_test.go index f8469a5..dac2685 100644 --- a/checklists_test.go +++ b/checklists_test.go @@ -258,3 +258,91 @@ func TestChecklists_Delete(t *testing.T) { t.Error("expected DELETE to be called") } } + +func TestChecklists_Archive(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + switch r.URL.Path { + case "/auth/login.json": + json.NewEncoder(w).Encode(map[string]string{"token": "test-token"}) + case "/checklists/1.json": + if r.Method != http.MethodPut { + t.Errorf("expected PUT, got %s", r.Method) + } + + var req archiveRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + t.Fatalf("failed to decode request: %v", err) + } + if !req.Archived { + t.Error("expected archived=true") + } + + response := Checklist{ + ID: 1, + Name: "Archived Checklist", + Archived: true, + UpdatedAt: time.Now(), + } + json.NewEncoder(w).Encode(response) + default: + t.Errorf("unexpected path: %s", r.URL.Path) + } + })) + defer server.Close() + + client := NewClient("user@example.com", "api-key", WithBaseURL(server.URL)) + checklist, err := client.Checklists().Archive(context.Background(), 1) + + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !checklist.Archived { + t.Error("expected checklist to be archived") + } +} + +func TestChecklists_Unarchive(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + switch r.URL.Path { + case "/auth/login.json": + json.NewEncoder(w).Encode(map[string]string{"token": "test-token"}) + case "/checklists/1.json": + if r.Method != http.MethodPut { + t.Errorf("expected PUT, got %s", r.Method) + } + + var req archiveRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + t.Fatalf("failed to decode request: %v", err) + } + if req.Archived { + t.Error("expected archived=false") + } + + response := Checklist{ + ID: 1, + Name: "Unarchived Checklist", + Archived: false, + UpdatedAt: time.Now(), + } + json.NewEncoder(w).Encode(response) + default: + t.Errorf("unexpected path: %s", r.URL.Path) + } + })) + defer server.Close() + + client := NewClient("user@example.com", "api-key", WithBaseURL(server.URL)) + checklist, err := client.Checklists().Unarchive(context.Background(), 1) + + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if checklist.Archived { + t.Error("expected checklist to not be archived") + } +} diff --git a/example_test.go b/example_test.go new file mode 100644 index 0000000..517c7b5 --- /dev/null +++ b/example_test.go @@ -0,0 +1,163 @@ +package checkvist_test + +import ( + "context" + "fmt" + "time" + + "code.beautifulmachines.dev/jakoubek/checkvist-api" +) + +// This file contains GoDoc examples that appear in the package documentation. +// All examples are runnable but require valid credentials to actually work. + +func Example_basicUsage() { + // Create a new client with your credentials + client := checkvist.NewClient("user@example.com", "your-api-key") + + // List all checklists + ctx := context.Background() + checklists, err := client.Checklists().List(ctx) + if err != nil { + fmt.Println("Error:", err) + return + } + + for _, cl := range checklists { + fmt.Printf("Checklist: %s (%d tasks)\n", cl.Name, cl.TaskCount) + } +} + +func ExampleNewClient() { + // Basic client creation + client := checkvist.NewClient("user@example.com", "your-api-key") + _ = client + + // With custom options + client = checkvist.NewClient("user@example.com", "your-api-key", + checkvist.WithTimeout(60*time.Second), + checkvist.WithRetryConfig(checkvist.RetryConfig{ + MaxRetries: 5, + BaseDelay: 500 * time.Millisecond, + MaxDelay: 30 * time.Second, + }), + ) + _ = client +} + +func ExampleClient_Authenticate() { + client := checkvist.NewClient("user@example.com", "your-api-key") + + // Explicit authentication (usually not needed - auto-authenticates on first request) + ctx := context.Background() + err := client.Authenticate(ctx) + if err != nil { + fmt.Println("Authentication failed:", err) + return + } + fmt.Println("Authenticated successfully") +} + +func ExampleChecklistService_List() { + client := checkvist.NewClient("user@example.com", "your-api-key") + ctx := context.Background() + + // List all active checklists + checklists, err := client.Checklists().List(ctx) + if err != nil { + fmt.Println("Error:", err) + return + } + + for _, cl := range checklists { + fmt.Printf("- %s (ID: %d)\n", cl.Name, cl.ID) + } +} + +func ExampleTaskService_Create() { + client := checkvist.NewClient("user@example.com", "your-api-key") + ctx := context.Background() + + // Create a task using the builder pattern + task, err := client.Tasks(123).Create(ctx, + checkvist.NewTask("Buy groceries"). + WithTags("shopping", "urgent"). + WithDueDate(checkvist.DueTomorrow). + WithPriority(1), + ) + if err != nil { + fmt.Println("Error:", err) + return + } + fmt.Printf("Created task: %s (ID: %d)\n", task.Content, task.ID) +} + +func ExampleNewTask() { + // Simple task + task := checkvist.NewTask("Complete project report") + + // Task with all options + task = checkvist.NewTask("Review pull request"). + WithTags("code-review", "urgent"). + WithDueDate(checkvist.DueTomorrow). + WithPriority(1). + WithParent(456). // Makes this a subtask + WithPosition(1) // First position among siblings + + _ = task +} + +func ExampleNewFilter() { + // Assume we have a list of tasks from the API + tasks := []checkvist.Task{ + {ID: 1, Content: "Buy groceries", TagsAsText: "shopping", Status: checkvist.StatusOpen}, + {ID: 2, Content: "Call doctor", TagsAsText: "health", Status: checkvist.StatusClosed}, + {ID: 3, Content: "Review PR", TagsAsText: "work, urgent", Status: checkvist.StatusOpen}, + } + + // Filter to open tasks with "urgent" tag + filtered := checkvist.NewFilter(tasks). + WithTag("urgent"). + WithStatus(checkvist.StatusOpen). + Apply() + + fmt.Printf("Found %d matching tasks\n", len(filtered)) + // Output: Found 1 matching tasks +} + +func ExampleFilter_Apply() { + // Create sample tasks with due dates + yesterday := time.Now().AddDate(0, 0, -1) + tomorrow := time.Now().AddDate(0, 0, 1) + + tasks := []checkvist.Task{ + {ID: 1, Content: "Overdue task", DueDate: &yesterday, Status: checkvist.StatusOpen}, + {ID: 2, Content: "Future task", DueDate: &tomorrow, Status: checkvist.StatusOpen}, + {ID: 3, Content: "Closed task", DueDate: &yesterday, Status: checkvist.StatusClosed}, + } + + // Find overdue open tasks + overdue := checkvist.NewFilter(tasks). + WithOverdue(). + Apply() + + for _, t := range overdue { + fmt.Printf("Overdue: %s\n", t.Content) + } + // Output: Overdue: Overdue task +} + +func ExampleDueAt() { + // Set due date to a specific date + deadline := time.Date(2026, 3, 15, 0, 0, 0, 0, time.UTC) + task := checkvist.NewTask("Project deadline"). + WithDueDate(checkvist.DueAt(deadline)) + _ = task +} + +func ExampleDueInDays() { + // Set due date to 7 days from now + task := checkvist.NewTask("Follow up"). + WithDueDate(checkvist.DueInDays(7)) + _ = task +} diff --git a/filter.go b/filter.go index 6003ebe..b9eff45 100644 --- a/filter.go +++ b/filter.go @@ -1,5 +1,152 @@ package checkvist +import ( + "strings" + "time" +) + // filter.go contains the Filter builder for client-side task filtering. // The Checkvist API does not support server-side filtering, so filtering // is performed locally after fetching all tasks. + +// Filter provides a builder pattern for filtering tasks client-side. +type Filter struct { + tasks []Task + filters []func(Task) bool +} + +// NewFilter creates a new Filter with the given tasks. +func NewFilter(tasks []Task) *Filter { + return &Filter{tasks: tasks} +} + +// WithTag filters tasks that have the specified tag. +func (f *Filter) WithTag(tag string) *Filter { + f.filters = append(f.filters, func(t Task) bool { + return taskHasTag(t, tag) + }) + return f +} + +// WithTags filters tasks that have all of the specified tags (AND logic). +func (f *Filter) WithTags(tags ...string) *Filter { + f.filters = append(f.filters, func(t Task) bool { + for _, tag := range tags { + if !taskHasTag(t, tag) { + return false + } + } + return true + }) + return f +} + +// WithStatus filters tasks by their status. +func (f *Filter) WithStatus(status TaskStatus) *Filter { + f.filters = append(f.filters, func(t Task) bool { + return t.Status == status + }) + return f +} + +// WithDueBefore filters tasks with due dates before the specified time. +func (f *Filter) WithDueBefore(deadline time.Time) *Filter { + f.filters = append(f.filters, func(t Task) bool { + if t.DueDate == nil { + return false + } + return t.DueDate.Before(deadline) + }) + return f +} + +// WithDueAfter filters tasks with due dates after the specified time. +func (f *Filter) WithDueAfter(after time.Time) *Filter { + f.filters = append(f.filters, func(t Task) bool { + if t.DueDate == nil { + return false + } + return t.DueDate.After(after) + }) + return f +} + +// WithDueOn filters tasks with due dates on the specified day. +func (f *Filter) WithDueOn(day time.Time) *Filter { + year, month, d := day.Date() + f.filters = append(f.filters, func(t Task) bool { + if t.DueDate == nil { + return false + } + ty, tm, td := t.DueDate.Date() + return ty == year && tm == month && td == d + }) + return f +} + +// WithOverdue filters tasks that are overdue (due date is before today). +func (f *Filter) WithOverdue() *Filter { + today := time.Now().Truncate(24 * time.Hour) + f.filters = append(f.filters, func(t Task) bool { + if t.DueDate == nil { + return false + } + return t.DueDate.Before(today) && t.Status == StatusOpen + }) + return f +} + +// WithSearch filters tasks whose content contains the search query (case-insensitive). +func (f *Filter) WithSearch(query string) *Filter { + lowerQuery := strings.ToLower(query) + f.filters = append(f.filters, func(t Task) bool { + return strings.Contains(strings.ToLower(t.Content), lowerQuery) + }) + return f +} + +// Apply applies all filters and returns the filtered tasks. +func (f *Filter) Apply() []Task { + if len(f.filters) == 0 { + result := make([]Task, len(f.tasks)) + copy(result, f.tasks) + return result + } + + result := make([]Task, 0, len(f.tasks)) + for _, task := range f.tasks { + if f.matches(task) { + result = append(result, task) + } + } + return result +} + +// matches checks if a task matches all filters. +func (f *Filter) matches(task Task) bool { + for _, filter := range f.filters { + if !filter(task) { + return false + } + } + return true +} + +// taskHasTag checks if a task has a specific tag. +func taskHasTag(t Task, tag string) bool { + // Check parsed Tags map first + if t.Tags != nil && t.Tags[tag] { + return true + } + // Fall back to checking TagsAsText + if t.TagsAsText == "" { + return false + } + lowerTag := strings.ToLower(tag) + for _, part := range strings.Split(t.TagsAsText, ",") { + if strings.ToLower(strings.TrimSpace(part)) == lowerTag { + return true + } + } + return false +} diff --git a/filter_test.go b/filter_test.go new file mode 100644 index 0000000..c8f7ec2 --- /dev/null +++ b/filter_test.go @@ -0,0 +1,338 @@ +package checkvist + +import ( + "testing" + "time" +) + +func TestFilter_WithTag(t *testing.T) { + tasks := []Task{ + {ID: 1, Content: "Task 1", TagsAsText: "important, urgent"}, + {ID: 2, Content: "Task 2", TagsAsText: "urgent"}, + {ID: 3, Content: "Task 3", TagsAsText: ""}, + } + + tests := []struct { + name string + tag string + expected []int + }{ + {"filter by important", "important", []int{1}}, + {"filter by urgent", "urgent", []int{1, 2}}, + {"filter by nonexistent", "nonexistent", []int{}}, + {"case insensitive", "IMPORTANT", []int{1}}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := NewFilter(tasks).WithTag(tt.tag).Apply() + if len(result) != len(tt.expected) { + t.Errorf("expected %d tasks, got %d", len(tt.expected), len(result)) + return + } + for i, task := range result { + if task.ID != tt.expected[i] { + t.Errorf("expected task ID %d at index %d, got %d", tt.expected[i], i, task.ID) + } + } + }) + } +} + +func TestFilter_WithMultipleTags(t *testing.T) { + tasks := []Task{ + {ID: 1, Content: "Task 1", TagsAsText: "important, urgent, work"}, + {ID: 2, Content: "Task 2", TagsAsText: "urgent, work"}, + {ID: 3, Content: "Task 3", TagsAsText: "important"}, + } + + tests := []struct { + name string + tags []string + expected []int + }{ + {"single tag", []string{"urgent"}, []int{1, 2}}, + {"two tags AND", []string{"urgent", "work"}, []int{1, 2}}, + {"three tags AND", []string{"important", "urgent", "work"}, []int{1}}, + {"no match", []string{"important", "urgent", "missing"}, []int{}}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := NewFilter(tasks).WithTags(tt.tags...).Apply() + if len(result) != len(tt.expected) { + t.Errorf("expected %d tasks, got %d", len(tt.expected), len(result)) + return + } + for i, task := range result { + if task.ID != tt.expected[i] { + t.Errorf("expected task ID %d at index %d, got %d", tt.expected[i], i, task.ID) + } + } + }) + } +} + +func TestFilter_WithStatus(t *testing.T) { + tasks := []Task{ + {ID: 1, Content: "Open task", Status: StatusOpen}, + {ID: 2, Content: "Closed task", Status: StatusClosed}, + {ID: 3, Content: "Invalidated task", Status: StatusInvalidated}, + {ID: 4, Content: "Another open", Status: StatusOpen}, + } + + tests := []struct { + name string + status TaskStatus + expected []int + }{ + {"open tasks", StatusOpen, []int{1, 4}}, + {"closed tasks", StatusClosed, []int{2}}, + {"invalidated tasks", StatusInvalidated, []int{3}}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := NewFilter(tasks).WithStatus(tt.status).Apply() + if len(result) != len(tt.expected) { + t.Errorf("expected %d tasks, got %d", len(tt.expected), len(result)) + return + } + for i, task := range result { + if task.ID != tt.expected[i] { + t.Errorf("expected task ID %d at index %d, got %d", tt.expected[i], i, task.ID) + } + } + }) + } +} + +func TestFilter_WithDueBefore(t *testing.T) { + now := time.Now() + yesterday := now.AddDate(0, 0, -1) + tomorrow := now.AddDate(0, 0, 1) + nextWeek := now.AddDate(0, 0, 7) + + tasks := []Task{ + {ID: 1, Content: "Yesterday", DueDate: &yesterday}, + {ID: 2, Content: "Tomorrow", DueDate: &tomorrow}, + {ID: 3, Content: "Next week", DueDate: &nextWeek}, + {ID: 4, Content: "No due date", DueDate: nil}, + } + + result := NewFilter(tasks).WithDueBefore(now).Apply() + if len(result) != 1 { + t.Errorf("expected 1 task, got %d", len(result)) + return + } + if result[0].ID != 1 { + t.Errorf("expected task ID 1, got %d", result[0].ID) + } +} + +func TestFilter_WithDueAfter(t *testing.T) { + now := time.Now() + yesterday := now.AddDate(0, 0, -1) + tomorrow := now.AddDate(0, 0, 1) + nextWeek := now.AddDate(0, 0, 7) + + tasks := []Task{ + {ID: 1, Content: "Yesterday", DueDate: &yesterday}, + {ID: 2, Content: "Tomorrow", DueDate: &tomorrow}, + {ID: 3, Content: "Next week", DueDate: &nextWeek}, + {ID: 4, Content: "No due date", DueDate: nil}, + } + + result := NewFilter(tasks).WithDueAfter(now).Apply() + if len(result) != 2 { + t.Errorf("expected 2 tasks, got %d", len(result)) + return + } + if result[0].ID != 2 || result[1].ID != 3 { + t.Errorf("expected task IDs [2, 3], got [%d, %d]", result[0].ID, result[1].ID) + } +} + +func TestFilter_WithDueOn(t *testing.T) { + today := time.Now().Truncate(24 * time.Hour) + todayMorning := today.Add(9 * time.Hour) + todayEvening := today.Add(18 * time.Hour) + tomorrow := today.AddDate(0, 0, 1) + + tasks := []Task{ + {ID: 1, Content: "Today morning", DueDate: &todayMorning}, + {ID: 2, Content: "Today evening", DueDate: &todayEvening}, + {ID: 3, Content: "Tomorrow", DueDate: &tomorrow}, + {ID: 4, Content: "No due date", DueDate: nil}, + } + + result := NewFilter(tasks).WithDueOn(today).Apply() + if len(result) != 2 { + t.Errorf("expected 2 tasks, got %d", len(result)) + return + } + if result[0].ID != 1 || result[1].ID != 2 { + t.Errorf("expected task IDs [1, 2], got [%d, %d]", result[0].ID, result[1].ID) + } +} + +func TestFilter_WithOverdue(t *testing.T) { + today := time.Now().Truncate(24 * time.Hour) + yesterday := today.AddDate(0, 0, -1) + tomorrow := today.AddDate(0, 0, 1) + + tasks := []Task{ + {ID: 1, Content: "Overdue open", DueDate: &yesterday, Status: StatusOpen}, + {ID: 2, Content: "Overdue closed", DueDate: &yesterday, Status: StatusClosed}, + {ID: 3, Content: "Future open", DueDate: &tomorrow, Status: StatusOpen}, + {ID: 4, Content: "No due date", DueDate: nil, Status: StatusOpen}, + } + + result := NewFilter(tasks).WithOverdue().Apply() + if len(result) != 1 { + t.Errorf("expected 1 task, got %d", len(result)) + return + } + if result[0].ID != 1 { + t.Errorf("expected task ID 1, got %d", result[0].ID) + } +} + +func TestFilter_WithSearch(t *testing.T) { + tasks := []Task{ + {ID: 1, Content: "Buy groceries"}, + {ID: 2, Content: "Call the doctor"}, + {ID: 3, Content: "Review PR for grocery app"}, + {ID: 4, Content: "Send email"}, + } + + tests := []struct { + name string + query string + expected []int + }{ + {"exact match", "groceries", []int{1}}, + {"partial match", "grocer", []int{1, 3}}, + {"case insensitive", "CALL", []int{2}}, + {"no match", "xyz", []int{}}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := NewFilter(tasks).WithSearch(tt.query).Apply() + if len(result) != len(tt.expected) { + t.Errorf("expected %d tasks, got %d", len(tt.expected), len(result)) + return + } + for i, task := range result { + if task.ID != tt.expected[i] { + t.Errorf("expected task ID %d at index %d, got %d", tt.expected[i], i, task.ID) + } + } + }) + } +} + +func TestFilter_Combined(t *testing.T) { + today := time.Now().Truncate(24 * time.Hour) + yesterday := today.AddDate(0, 0, -1) + tomorrow := today.AddDate(0, 0, 1) + + tasks := []Task{ + {ID: 1, Content: "Important task", TagsAsText: "important", Status: StatusOpen, DueDate: &yesterday}, + {ID: 2, Content: "Important closed", TagsAsText: "important", Status: StatusClosed, DueDate: &yesterday}, + {ID: 3, Content: "Regular task", TagsAsText: "regular", Status: StatusOpen, DueDate: &yesterday}, + {ID: 4, Content: "Important future", TagsAsText: "important", Status: StatusOpen, DueDate: &tomorrow}, + } + + // Filter: important AND open AND due before today + result := NewFilter(tasks). + WithTag("important"). + WithStatus(StatusOpen). + WithDueBefore(today). + Apply() + + if len(result) != 1 { + t.Errorf("expected 1 task, got %d", len(result)) + return + } + if result[0].ID != 1 { + t.Errorf("expected task ID 1, got %d", result[0].ID) + } +} + +func TestFilter_EmptyFilters(t *testing.T) { + tasks := []Task{ + {ID: 1, Content: "Task 1"}, + {ID: 2, Content: "Task 2"}, + } + + result := NewFilter(tasks).Apply() + if len(result) != 2 { + t.Errorf("expected 2 tasks, got %d", len(result)) + } +} + +func TestFilter_EmptyTasks(t *testing.T) { + result := NewFilter([]Task{}).WithTag("test").Apply() + if len(result) != 0 { + t.Errorf("expected 0 tasks, got %d", len(result)) + } +} + +func TestFilter_Performance_1000Tasks(t *testing.T) { + // Create 1000 tasks with various attributes + tasks := make([]Task, 1000) + now := time.Now() + + for i := 0; i < 1000; i++ { + due := now.AddDate(0, 0, i-500) // -500 to +499 days from now + tasks[i] = Task{ + ID: i + 1, + Content: "Task with some content to search through", + TagsAsText: "tag1, tag2, tag3", + Status: TaskStatus(i % 3), + DueDate: &due, + } + } + + // Mark important tasks + for i := 0; i < 100; i++ { + tasks[i].TagsAsText = "important, urgent" + } + + start := time.Now() + + // Run a complex filter + result := NewFilter(tasks). + WithTag("important"). + WithStatus(StatusOpen). + WithDueBefore(now). + WithSearch("content"). + Apply() + + elapsed := time.Since(start) + + // Performance target: <10ms for 1000 tasks + if elapsed > 10*time.Millisecond { + t.Errorf("filter took %v, expected <10ms", elapsed) + } + + // Verify we got some results + if len(result) == 0 { + t.Log("No matching tasks found, but performance test passed") + } +} + +func TestFilter_TagsMap(t *testing.T) { + // Test that Tags map is also checked + tasks := []Task{ + {ID: 1, Content: "Task 1", Tags: Tags{"important": true}, TagsAsText: ""}, + {ID: 2, Content: "Task 2", Tags: nil, TagsAsText: "important"}, + } + + result := NewFilter(tasks).WithTag("important").Apply() + if len(result) != 2 { + t.Errorf("expected 2 tasks, got %d", len(result)) + } +} diff --git a/tasks.go b/tasks.go index 55a5a9a..8dbf623 100644 --- a/tasks.go +++ b/tasks.go @@ -60,6 +60,7 @@ type CreateTaskRequest struct { Due string `json:"due,omitempty"` Priority int `json:"priority,omitempty"` Tags string `json:"tags,omitempty"` + Repeat string `json:"repeat,omitempty"` } // TaskBuilder provides a fluent interface for building task creation requests. @@ -70,6 +71,7 @@ type TaskBuilder struct { due string priority int tags []string + repeat string } // NewTask creates a new TaskBuilder with the given content. @@ -107,6 +109,21 @@ func (b *TaskBuilder) WithTags(tags ...string) *TaskBuilder { return b } +// WithRepeat sets the repeat pattern for the task using Checkvist's smart syntax. +// Common patterns include: +// - "daily" - repeats every day +// - "weekly" - repeats every week +// - "monthly" - repeats every month +// - "yearly" - repeats every year +// - "every 2 days" - repeats every 2 days +// - "every week on monday" - repeats weekly on Monday +// - "every month on 15" - repeats monthly on the 15th +// - "every 2 weeks on friday" - repeats every 2 weeks on Friday +func (b *TaskBuilder) WithRepeat(pattern string) *TaskBuilder { + b.repeat = pattern + return b +} + // build converts the TaskBuilder to a CreateTaskRequest. func (b *TaskBuilder) build() CreateTaskRequest { req := CreateTaskRequest{ @@ -115,6 +132,7 @@ func (b *TaskBuilder) build() CreateTaskRequest { Position: b.position, Due: b.due, Priority: b.priority, + Repeat: b.repeat, } if len(b.tags) > 0 { for i, tag := range b.tags {