Implement Client struct with functional options

Add client.go with:
- Client struct with baseURL, username, remoteKey, token, tokenExp,
  httpClient, retryConf, logger, and sync.RWMutex for thread safety
- NewClient constructor accepting username, remoteKey, and variadic options
- Default values: 30s timeout, Checkvist base URL

Add options.go with:
- RetryConfig struct (MaxRetries, BaseDelay, MaxDelay, Jitter)
- DefaultRetryConfig() with 3 retries, 1s base, 30s max, jitter enabled
- Option type for functional options pattern
- WithHTTPClient, WithTimeout, WithRetryConfig, WithLogger, WithBaseURL

Closes checkvist-api-ymg
This commit is contained in:
Oliver Jakoubek 2026-01-14 13:25:03 +01:00
commit 90c48d9323
3 changed files with 141 additions and 1 deletions

View file

@ -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"}]}

View file

@ -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
}

View file

@ -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
}
}