Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 107 additions & 0 deletions .claude/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -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.)
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
secrets.yml
coverage.out
*.out
settings.local.json
5 changes: 4 additions & 1 deletion api.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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
}
Expand Down
93 changes: 93 additions & 0 deletions examples_test.go
Original file line number Diff line number Diff line change
@@ -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
}
4 changes: 3 additions & 1 deletion headers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
1 change: 1 addition & 0 deletions middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
3 changes: 2 additions & 1 deletion parsing.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
5 changes: 4 additions & 1 deletion schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 + ":"}
Expand All @@ -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
}
Expand Down