diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index f7cf06e..03527f5 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -23,4 +23,4 @@ {"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-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":"open","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-14T12:31:08.021154076+01:00","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"}]} +{"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/client.go b/client.go index 8143b00..c422189 100644 --- a/client.go +++ b/client.go @@ -10,4 +10,74 @@ // checklists, err := client.Checklists().List(ctx) package checkvist +import ( + "log/slog" + "net/http" + "sync" + "time" +) + // client.go contains the Client struct, constructor, and authentication logic. + +const ( + // DefaultBaseURL is the default base URL for the Checkvist API. + DefaultBaseURL = "https://checkvist.com" + // DefaultTimeout is the default timeout for HTTP requests. + DefaultTimeout = 30 * time.Second +) + +// Client is the Checkvist API client. +type Client struct { + // baseURL is the base URL for API requests. + baseURL string + // username is the user's email address. + username string + // remoteKey is the API key (remote key) for authentication. + remoteKey string + // token is the current authentication token. + token string + // tokenExp is the expiration time of the current token. + tokenExp time.Time + // httpClient is the HTTP client used for requests. + httpClient *http.Client + // retryConf is the retry configuration for failed requests. + retryConf RetryConfig + // logger is the logger for debug and error messages. + logger *slog.Logger + // mu protects token and tokenExp for concurrent access. + mu sync.RWMutex +} + +// NewClient creates a new Checkvist API client. +// +// The username should be the user's email address, and remoteKey is the API key +// which can be obtained from Checkvist settings. +// +// Example: +// +// client := checkvist.NewClient("user@example.com", "your-api-key") +// +// With options: +// +// client := checkvist.NewClient("user@example.com", "your-api-key", +// checkvist.WithTimeout(60 * time.Second), +// checkvist.WithRetryConfig(checkvist.RetryConfig{MaxRetries: 5}), +// ) +func NewClient(username, remoteKey string, opts ...Option) *Client { + c := &Client{ + baseURL: DefaultBaseURL, + username: username, + remoteKey: remoteKey, + httpClient: &http.Client{ + Timeout: DefaultTimeout, + }, + retryConf: DefaultRetryConfig(), + logger: slog.Default(), + } + + for _, opt := range opts { + opt(c) + } + + return c +} diff --git a/options.go b/options.go index 8723af3..8a8a31a 100644 --- a/options.go +++ b/options.go @@ -1,3 +1,73 @@ package checkvist +import ( + "log/slog" + "net/http" + "time" +) + // options.go contains functional options for configuring the Client. + +// RetryConfig configures the retry behavior for failed requests. +type RetryConfig struct { + // MaxRetries is the maximum number of retry attempts. + MaxRetries int + // BaseDelay is the initial delay before the first retry. + BaseDelay time.Duration + // MaxDelay is the maximum delay between retries. + MaxDelay time.Duration + // Jitter enables randomized delay to prevent thundering herd. + Jitter bool +} + +// DefaultRetryConfig returns the default retry configuration. +func DefaultRetryConfig() RetryConfig { + return RetryConfig{ + MaxRetries: 3, + BaseDelay: 1 * time.Second, + MaxDelay: 30 * time.Second, + Jitter: true, + } +} + +// Option is a functional option for configuring the Client. +type Option func(*Client) + +// WithHTTPClient sets a custom HTTP client for the Checkvist client. +func WithHTTPClient(client *http.Client) Option { + return func(c *Client) { + c.httpClient = client + } +} + +// WithTimeout sets the timeout for HTTP requests. +// This creates a new HTTP client with the specified timeout. +func WithTimeout(timeout time.Duration) Option { + return func(c *Client) { + c.httpClient = &http.Client{ + Timeout: timeout, + } + } +} + +// WithRetryConfig sets the retry configuration for failed requests. +func WithRetryConfig(config RetryConfig) Option { + return func(c *Client) { + c.retryConf = config + } +} + +// WithLogger sets a custom logger for the client. +func WithLogger(logger *slog.Logger) Option { + return func(c *Client) { + c.logger = logger + } +} + +// WithBaseURL sets a custom base URL for the API. +// This is primarily useful for testing. +func WithBaseURL(url string) Option { + return func(c *Client) { + c.baseURL = url + } +}