Implement Task operations
Add tasks.go with TaskService for full task CRUD: - client.Tasks(checklistID) returns TaskService - List(ctx) - get all tasks in checklist - Get(ctx, taskID) - get single task with parent hierarchy - Create(ctx, builder) - create task using TaskBuilder - Update(ctx, taskID, req) - update task properties - Delete(ctx, taskID) - permanently delete task - Close(ctx, taskID) - mark task as completed - Reopen(ctx, taskID) - reopen closed/invalidated task - Invalidate(ctx, taskID) - mark task as invalidated (strikethrough) TaskBuilder fluent interface for task creation: - NewTask(content) - create builder - WithParent(id) - set parent for subtask - WithPosition(pos) - set position in list - WithDueDate(due) - set due date using DueDate type - WithPriority(level) - set priority (0=normal, 1=highest, 2=high) - WithTags(tags...) - set tags Automatic DueDate parsing from DueDateRaw (ISO 8601 format). Closes checkvist-api-rl9
This commit is contained in:
parent
e5ee947fa4
commit
3aa2a284de
2 changed files with 221 additions and 1 deletions
|
|
@ -20,7 +20,7 @@
|
|||
{"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":"open","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-14T12:31:38.724338606+01:00","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":"open","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-14T12:30:53.90629838+01:00","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-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-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":"open","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-14T12:31:37.538754679+01:00","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"}]}
|
||||
|
|
|
|||
220
tasks.go
220
tasks.go
|
|
@ -1,3 +1,223 @@
|
|||
package checkvist
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// tasks.go contains the TaskService for CRUD operations on tasks within a checklist.
|
||||
|
||||
// TaskService provides operations on tasks within a specific checklist.
|
||||
type TaskService struct {
|
||||
client *Client
|
||||
checklistID int
|
||||
}
|
||||
|
||||
// Tasks returns a TaskService for performing task operations on the specified checklist.
|
||||
func (c *Client) Tasks(checklistID int) *TaskService {
|
||||
return &TaskService{
|
||||
client: c,
|
||||
checklistID: checklistID,
|
||||
}
|
||||
}
|
||||
|
||||
// List returns all tasks in the checklist.
|
||||
func (s *TaskService) List(ctx context.Context) ([]Task, error) {
|
||||
path := fmt.Sprintf("/checklists/%d/tasks.json", s.checklistID)
|
||||
|
||||
var tasks []Task
|
||||
if err := s.client.doGet(ctx, path, &tasks); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Parse due dates
|
||||
for i := range tasks {
|
||||
parseDueDate(&tasks[i])
|
||||
}
|
||||
|
||||
return tasks, nil
|
||||
}
|
||||
|
||||
// Get returns a single task by ID, including its parent hierarchy.
|
||||
func (s *TaskService) Get(ctx context.Context, taskID int) (*Task, error) {
|
||||
path := fmt.Sprintf("/checklists/%d/tasks/%d.json", s.checklistID, taskID)
|
||||
|
||||
var task Task
|
||||
if err := s.client.doGet(ctx, path, &task); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
parseDueDate(&task)
|
||||
return &task, nil
|
||||
}
|
||||
|
||||
// CreateTaskRequest represents the request body for creating a task.
|
||||
type CreateTaskRequest struct {
|
||||
Content string `json:"content"`
|
||||
ParentID int `json:"parent_id,omitempty"`
|
||||
Position int `json:"position,omitempty"`
|
||||
Due string `json:"due,omitempty"`
|
||||
Priority int `json:"priority,omitempty"`
|
||||
Tags string `json:"tags,omitempty"`
|
||||
}
|
||||
|
||||
// TaskBuilder provides a fluent interface for building task creation requests.
|
||||
type TaskBuilder struct {
|
||||
content string
|
||||
parentID int
|
||||
position int
|
||||
due string
|
||||
priority int
|
||||
tags []string
|
||||
}
|
||||
|
||||
// NewTask creates a new TaskBuilder with the given content.
|
||||
func NewTask(content string) *TaskBuilder {
|
||||
return &TaskBuilder{content: content}
|
||||
}
|
||||
|
||||
// WithParent sets the parent task ID for creating a subtask.
|
||||
func (b *TaskBuilder) WithParent(parentID int) *TaskBuilder {
|
||||
b.parentID = parentID
|
||||
return b
|
||||
}
|
||||
|
||||
// WithPosition sets the position of the task within its siblings.
|
||||
func (b *TaskBuilder) WithPosition(position int) *TaskBuilder {
|
||||
b.position = position
|
||||
return b
|
||||
}
|
||||
|
||||
// WithDueDate sets the due date using a DueDate value.
|
||||
func (b *TaskBuilder) WithDueDate(due DueDate) *TaskBuilder {
|
||||
b.due = due.String()
|
||||
return b
|
||||
}
|
||||
|
||||
// WithPriority sets the priority level (1 = highest, 2 = high, 0 = normal).
|
||||
func (b *TaskBuilder) WithPriority(priority int) *TaskBuilder {
|
||||
b.priority = priority
|
||||
return b
|
||||
}
|
||||
|
||||
// WithTags sets the tags for the task.
|
||||
func (b *TaskBuilder) WithTags(tags ...string) *TaskBuilder {
|
||||
b.tags = tags
|
||||
return b
|
||||
}
|
||||
|
||||
// build converts the TaskBuilder to a CreateTaskRequest.
|
||||
func (b *TaskBuilder) build() CreateTaskRequest {
|
||||
req := CreateTaskRequest{
|
||||
Content: b.content,
|
||||
ParentID: b.parentID,
|
||||
Position: b.position,
|
||||
Due: b.due,
|
||||
Priority: b.priority,
|
||||
}
|
||||
if len(b.tags) > 0 {
|
||||
for i, tag := range b.tags {
|
||||
if i > 0 {
|
||||
req.Tags += ", "
|
||||
}
|
||||
req.Tags += tag
|
||||
}
|
||||
}
|
||||
return req
|
||||
}
|
||||
|
||||
// Create creates a new task using a TaskBuilder.
|
||||
func (s *TaskService) Create(ctx context.Context, builder *TaskBuilder) (*Task, error) {
|
||||
path := fmt.Sprintf("/checklists/%d/tasks.json", s.checklistID)
|
||||
body := builder.build()
|
||||
|
||||
var task Task
|
||||
if err := s.client.doPost(ctx, path, body, &task); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
parseDueDate(&task)
|
||||
return &task, nil
|
||||
}
|
||||
|
||||
// UpdateTaskRequest represents the request body for updating a task.
|
||||
type UpdateTaskRequest struct {
|
||||
Content *string `json:"content,omitempty"`
|
||||
ParentID *int `json:"parent_id,omitempty"`
|
||||
Position *int `json:"position,omitempty"`
|
||||
Due *string `json:"due,omitempty"`
|
||||
Priority *int `json:"priority,omitempty"`
|
||||
Tags *string `json:"tags,omitempty"`
|
||||
}
|
||||
|
||||
// Update updates an existing task.
|
||||
func (s *TaskService) Update(ctx context.Context, taskID int, req UpdateTaskRequest) (*Task, error) {
|
||||
path := fmt.Sprintf("/checklists/%d/tasks/%d.json", s.checklistID, taskID)
|
||||
|
||||
var task Task
|
||||
if err := s.client.doPut(ctx, path, req, &task); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
parseDueDate(&task)
|
||||
return &task, nil
|
||||
}
|
||||
|
||||
// Delete permanently deletes a task.
|
||||
func (s *TaskService) Delete(ctx context.Context, taskID int) error {
|
||||
path := fmt.Sprintf("/checklists/%d/tasks/%d.json", s.checklistID, taskID)
|
||||
return s.client.doDelete(ctx, path)
|
||||
}
|
||||
|
||||
// Close marks a task as completed.
|
||||
func (s *TaskService) Close(ctx context.Context, taskID int) (*Task, error) {
|
||||
path := fmt.Sprintf("/checklists/%d/tasks/%d/close.json", s.checklistID, taskID)
|
||||
|
||||
var task Task
|
||||
if err := s.client.doPost(ctx, path, nil, &task); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
parseDueDate(&task)
|
||||
return &task, nil
|
||||
}
|
||||
|
||||
// Reopen reopens a closed or invalidated task.
|
||||
func (s *TaskService) Reopen(ctx context.Context, taskID int) (*Task, error) {
|
||||
path := fmt.Sprintf("/checklists/%d/tasks/%d/reopen.json", s.checklistID, taskID)
|
||||
|
||||
var task Task
|
||||
if err := s.client.doPost(ctx, path, nil, &task); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
parseDueDate(&task)
|
||||
return &task, nil
|
||||
}
|
||||
|
||||
// Invalidate marks a task as invalidated (strikethrough).
|
||||
func (s *TaskService) Invalidate(ctx context.Context, taskID int) (*Task, error) {
|
||||
path := fmt.Sprintf("/checklists/%d/tasks/%d/invalidate.json", s.checklistID, taskID)
|
||||
|
||||
var task Task
|
||||
if err := s.client.doPost(ctx, path, nil, &task); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
parseDueDate(&task)
|
||||
return &task, nil
|
||||
}
|
||||
|
||||
// parseDueDate attempts to parse the DueDateRaw string into a time.Time.
|
||||
// It supports ISO 8601 date format (YYYY-MM-DD).
|
||||
func parseDueDate(task *Task) {
|
||||
if task.DueDateRaw == "" {
|
||||
return
|
||||
}
|
||||
|
||||
// Try to parse as ISO date
|
||||
if t, err := time.Parse("2006-01-02", task.DueDateRaw); err == nil {
|
||||
task.DueDate = &t
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue