diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md new file mode 100644 index 0000000..3c305d7 --- /dev/null +++ b/.claude/CLAUDE.md @@ -0,0 +1,107 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +This is a shared errors library for the go-openapi toolkit. It provides an `Error` interface and concrete error types for API errors and JSON-schema validation errors. The package is used throughout the go-openapi ecosystem (github.com/go-openapi). + +## Development Commands + +### Testing +```bash +# Run all tests +go test ./... + +# Run tests with coverage +go test -v -race -coverprofile=coverage.out ./... + +# Run a specific test +go test -v -run TestName ./... +``` + +### Linting +```bash +# Run golangci-lint (must be run before committing) +golangci-lint run +``` + +### Building +```bash +# Build the package +go build ./... + +# Verify dependencies +go mod verify +go mod tidy +``` + +## Architecture and Code Structure + +### Core Error Types + +The package provides a hierarchy of error types: + +1. **Error interface** (api.go:20-24): Base interface with `Code() int32` method that all errors implement +2. **apiError** (api.go:26-37): Simple error with code and message +3. **CompositeError** (schema.go:94-122): Groups multiple errors together, implements `Unwrap() []error` +4. **Validation** (headers.go:12-55): Represents validation failures with Name, In, Value fields +5. **ParseError** (parsing.go:12-42): Represents parsing errors with Reason field +6. **MethodNotAllowedError** (api.go:74-88): Special error for method not allowed with Allowed methods list +7. **APIVerificationFailed** (middleware.go:12-39): Error for API spec/registration mismatches + +### Error Categorization by File + +- **api.go**: Core error interface, basic error types, HTTP error serving +- **schema.go**: Validation errors (type, length, pattern, enum, min/max, uniqueness, properties) +- **headers.go**: Header validation errors (content-type, accept) +- **parsing.go**: Parameter parsing errors +- **auth.go**: Authentication errors +- **middleware.go**: API verification errors + +### Key Design Patterns + +1. **Error Codes**: Custom error codes >= 600 (maximumValidHTTPCode) to differentiate validation types without conflicting with HTTP status codes +2. **Conditional Messages**: Most constructors have "NoIn" variants for errors without an "In" field (e.g., tooLongMessage vs tooLongMessageNoIn) +3. **ServeError Function** (api.go:147-201): Central HTTP error handler using type assertions to handle different error types +4. **Flattening**: CompositeError flattens nested composite errors recursively (api.go:108-134) +5. **Name Validation**: Errors can have their Name field updated for nested properties via ValidateName methods + +### JSON Serialization + +All error types implement `MarshalJSON()` to provide structured JSON responses with code, message, and type-specific fields. + +## Testing Practices + +- Uses forked `github.com/go-openapi/testify/v2` for minimal test dependencies +- Tests follow pattern: `*_test.go` files next to implementation +- Test files cover: api_test.go, schema_test.go, middleware_test.go, parsing_test.go, auth_test.go + +## Code Quality Standards + +### Linting Configuration +- Enable all golangci-lint linters by default, with specific exclusions in .golangci.yml +- Complexity threshold: max 20 (cyclop, gocyclo) +- Line length: max 180 characters +- Run `golangci-lint run` before committing + +### Disabled Linters (and why) +Key exclusions from STYLE.md rationale: +- depguard: No import constraints enforced +- funlen: Function length not enforced (cognitive complexity preferred) +- godox: TODOs are acceptable +- nonamedreturns: Named returns are acceptable +- varnamelen: Short variable names allowed when appropriate + +## Release Process + +- Push semver tag (v{major}.{minor}.{patch}) to master branch +- CI automatically generates release with git-cliff +- Tags should be PGP-signed +- Tag message prepends release notes + +## Important Constants + +- `DefaultHTTPCode = 422` (http.StatusUnprocessableEntity) +- `maximumValidHTTPCode = 600` +- Custom error codes start at 600+ (InvalidTypeCode, RequiredFailCode, etc.) diff --git a/.gitignore b/.gitignore index dd91ed6..9a8da7e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ secrets.yml -coverage.out +*.out +settings.local.json diff --git a/api.go b/api.go index d169882..cb13941 100644 --- a/api.go +++ b/api.go @@ -28,10 +28,12 @@ type apiError struct { message string } +// Error implements the standard error interface. func (a *apiError) Error() string { return a.message } +// Code returns the HTTP status code associated with this error. func (a *apiError) Code() int32 { return a.code } @@ -78,11 +80,12 @@ type MethodNotAllowedError struct { message string } +// Error implements the standard error interface. func (m *MethodNotAllowedError) Error() string { return m.message } -// Code the error code. +// Code returns 405 (Method Not Allowed) as the HTTP status code. func (m *MethodNotAllowedError) Code() int32 { return m.code } diff --git a/examples_test.go b/examples_test.go new file mode 100644 index 0000000..745c272 --- /dev/null +++ b/examples_test.go @@ -0,0 +1,93 @@ +// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 + +package errors_test + +import ( + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + + "github.com/go-openapi/errors" +) + +func ExampleNew() { + // Create a generic API error with custom code + err := errors.New(400, "invalid input: %s", "email") + fmt.Printf("error: %v\n", err) + fmt.Printf("code: %d\n", err.Code()) + + // Create common HTTP errors + notFound := errors.NotFound("user %s not found", "john-doe") + fmt.Printf("not found: %v\n", notFound) + fmt.Printf("not found code: %d\n", notFound.Code()) + + notImpl := errors.NotImplemented("feature: dark mode") + fmt.Printf("not implemented: %v\n", notImpl) + + // Output: + // error: invalid input: email + // code: 400 + // not found: user john-doe not found + // not found code: 404 + // not implemented: feature: dark mode +} + +func ExampleServeError() { + // Create a simple validation error + err := errors.Required("email", "body", nil) + + // Simulate HTTP response + recorder := httptest.NewRecorder() + request := httptest.NewRequest(http.MethodPost, "/api/users", nil) + + // Serve the error as JSON + errors.ServeError(recorder, request, err) + + fmt.Printf("status: %d\n", recorder.Code) + fmt.Printf("content-type: %s\n", recorder.Header().Get("Content-Type")) + + // Parse and display the JSON response + var response map[string]any + if err := json.Unmarshal(recorder.Body.Bytes(), &response); err == nil { + fmt.Printf("error code: %.0f\n", response["code"]) + fmt.Printf("error message: %s\n", response["message"]) + } + + // Output: + // status: 422 + // content-type: application/json + // error code: 602 + // error message: email in body is required +} + +func ExampleCompositeValidationError() { + var errs []error + + // Collect multiple validation errors + errs = append(errs, errors.Required("name", "body", nil)) + errs = append(errs, errors.TooShort("description", "body", 10, "short")) + errs = append(errs, errors.InvalidType("age", "body", "integer", "abc")) + + // Combine them into a composite error + compositeErr := errors.CompositeValidationError(errs...) + + fmt.Printf("error count: %d\n", len(errs)) + fmt.Printf("composite error: %v\n", compositeErr) + fmt.Printf("code: %d\n", compositeErr.Code()) + + // Can unwrap to access individual errors + if unwrapped := compositeErr.Unwrap(); unwrapped != nil { + fmt.Printf("unwrapped count: %d\n", len(unwrapped)) + } + + // Output: + // error count: 3 + // composite error: validation failure list: + // name in body is required + // description in body should be at least 10 chars long + // age in body must be of type integer: "abc" + // code: 422 + // unwrapped count: 3 +} diff --git a/headers.go b/headers.go index 0f0b1db..717a51a 100644 --- a/headers.go +++ b/headers.go @@ -19,11 +19,13 @@ type Validation struct { //nolint: errname // changing the name to abide by the Values []any } +// Error implements the standard error interface. func (e *Validation) Error() string { return e.message } -// Code the error code. +// Code returns the HTTP status code for this validation error. +// Returns 422 (Unprocessable Entity) by default. func (e *Validation) Code() int32 { return e.code } diff --git a/middleware.go b/middleware.go index 9bd2c03..f89275f 100644 --- a/middleware.go +++ b/middleware.go @@ -17,6 +17,7 @@ type APIVerificationFailed struct { //nolint: errname MissingRegistration []string `json:"missingRegistration,omitempty"` } +// Error implements the standard error interface. func (v *APIVerificationFailed) Error() string { buf := bytes.NewBuffer(nil) diff --git a/parsing.go b/parsing.go index 25e5ac4..46e6612 100644 --- a/parsing.go +++ b/parsing.go @@ -37,11 +37,12 @@ func NewParseError(name, in, value string, reason error) *ParseError { } } +// Error implements the standard error interface. func (e *ParseError) Error() string { return e.message } -// Code returns the http status code for this error. +// Code returns 400 (Bad Request) as the HTTP status code for parsing errors. func (e *ParseError) Code() int32 { return e.code } diff --git a/schema.go b/schema.go index ec5eb44..2378bae 100644 --- a/schema.go +++ b/schema.go @@ -71,6 +71,7 @@ const ( // InvalidTypeCode is used for any subclass of invalid types. InvalidTypeCode = maximumValidHTTPCode + iota + // RequiredFailCode indicates a required field is missing. RequiredFailCode TooLongFailCode TooShortFailCode @@ -98,11 +99,12 @@ type CompositeError struct { message string } -// Code for this error. +// Code returns the HTTP status code for this composite error. func (c *CompositeError) Code() int32 { return c.code } +// Error implements the standard error interface. func (c *CompositeError) Error() string { if len(c.Errors) > 0 { msgs := []string{c.message + ":"} @@ -117,6 +119,7 @@ func (c *CompositeError) Error() string { return c.message } +// Unwrap implements the [errors.Unwrap] interface. func (c *CompositeError) Unwrap() []error { return c.Errors }