Implement error types and sentinel errors
Add errors.go with: - APIError struct with StatusCode, Message, RequestID, and Err fields - Error() method for human-readable error messages - Unwrap() method for errors.Is() and errors.As() compatibility - Sentinel errors: ErrUnauthorized, ErrNotFound, ErrRateLimited, ErrBadRequest, ErrServerError - NewAPIError helper function to create APIError from HTTP response with automatic status code mapping to sentinel errors Closes checkvist-api-mnh
This commit is contained in:
parent
61431dfaeb
commit
832364a06f
2 changed files with 79 additions and 1 deletions
|
|
@ -18,7 +18,7 @@
|
|||
{"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-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":"open","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-14T12:31:08.358878117+01:00","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":"open","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-14T12:31:07.619359293+01:00","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-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-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"}]}
|
||||
|
|
|
|||
78
errors.go
78
errors.go
|
|
@ -1,3 +1,81 @@
|
|||
package checkvist
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// errors.go contains the APIError type and sentinel errors for common API error conditions.
|
||||
|
||||
// Sentinel errors for common API error conditions.
|
||||
// Use errors.Is() to check for these errors.
|
||||
var (
|
||||
// ErrUnauthorized is returned when authentication fails (HTTP 401).
|
||||
ErrUnauthorized = errors.New("unauthorized: invalid credentials or expired token")
|
||||
// ErrNotFound is returned when a resource is not found (HTTP 404).
|
||||
ErrNotFound = errors.New("not found: the requested resource does not exist")
|
||||
// ErrRateLimited is returned when the API rate limit is exceeded (HTTP 429).
|
||||
ErrRateLimited = errors.New("rate limited: too many requests")
|
||||
// ErrBadRequest is returned for invalid request parameters (HTTP 400).
|
||||
ErrBadRequest = errors.New("bad request: invalid parameters")
|
||||
// ErrServerError is returned for server-side errors (HTTP 5xx).
|
||||
ErrServerError = errors.New("server error: the server encountered an error")
|
||||
)
|
||||
|
||||
// APIError represents an error returned by the Checkvist API.
|
||||
type APIError struct {
|
||||
// StatusCode is the HTTP status code returned by the API.
|
||||
StatusCode int
|
||||
// Message is a human-readable error message.
|
||||
Message string
|
||||
// RequestID is the unique identifier for the request, if available.
|
||||
RequestID string
|
||||
// Err is the underlying sentinel error, if applicable.
|
||||
Err error
|
||||
}
|
||||
|
||||
// Error implements the error interface.
|
||||
func (e *APIError) Error() string {
|
||||
if e.RequestID != "" {
|
||||
return fmt.Sprintf("checkvist API error (status %d, request %s): %s", e.StatusCode, e.RequestID, e.Message)
|
||||
}
|
||||
return fmt.Sprintf("checkvist API error (status %d): %s", e.StatusCode, e.Message)
|
||||
}
|
||||
|
||||
// Unwrap returns the underlying error for use with errors.Is() and errors.As().
|
||||
func (e *APIError) Unwrap() error {
|
||||
return e.Err
|
||||
}
|
||||
|
||||
// NewAPIError creates an APIError from an HTTP response.
|
||||
// It automatically maps the status code to the appropriate sentinel error.
|
||||
func NewAPIError(resp *http.Response, message string) *APIError {
|
||||
if message == "" {
|
||||
message = http.StatusText(resp.StatusCode)
|
||||
}
|
||||
|
||||
apiErr := &APIError{
|
||||
StatusCode: resp.StatusCode,
|
||||
Message: message,
|
||||
RequestID: resp.Header.Get("X-Request-Id"),
|
||||
}
|
||||
|
||||
// Map status codes to sentinel errors
|
||||
switch resp.StatusCode {
|
||||
case http.StatusUnauthorized:
|
||||
apiErr.Err = ErrUnauthorized
|
||||
case http.StatusNotFound:
|
||||
apiErr.Err = ErrNotFound
|
||||
case http.StatusTooManyRequests:
|
||||
apiErr.Err = ErrRateLimited
|
||||
case http.StatusBadRequest:
|
||||
apiErr.Err = ErrBadRequest
|
||||
default:
|
||||
if resp.StatusCode >= 500 {
|
||||
apiErr.Err = ErrServerError
|
||||
}
|
||||
}
|
||||
|
||||
return apiErr
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue