Skip to content

Commit d54d158

Browse files
authored
doc: complete godoc coverage with testable examples (#85)
* doc: added CLAUDE.md documentation Signed-off-by: Frederic BIDON <[email protected]> * doc: increased godoc coverage, added testable examples Signed-off-by: Frederic BIDON <[email protected]> --------- Signed-off-by: Frederic BIDON <[email protected]>
1 parent 2da66a2 commit d54d158

File tree

8 files changed

+216
-5
lines changed

8 files changed

+216
-5
lines changed

.claude/CLAUDE.md

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Project Overview
6+
7+
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).
8+
9+
## Development Commands
10+
11+
### Testing
12+
```bash
13+
# Run all tests
14+
go test ./...
15+
16+
# Run tests with coverage
17+
go test -v -race -coverprofile=coverage.out ./...
18+
19+
# Run a specific test
20+
go test -v -run TestName ./...
21+
```
22+
23+
### Linting
24+
```bash
25+
# Run golangci-lint (must be run before committing)
26+
golangci-lint run
27+
```
28+
29+
### Building
30+
```bash
31+
# Build the package
32+
go build ./...
33+
34+
# Verify dependencies
35+
go mod verify
36+
go mod tidy
37+
```
38+
39+
## Architecture and Code Structure
40+
41+
### Core Error Types
42+
43+
The package provides a hierarchy of error types:
44+
45+
1. **Error interface** (api.go:20-24): Base interface with `Code() int32` method that all errors implement
46+
2. **apiError** (api.go:26-37): Simple error with code and message
47+
3. **CompositeError** (schema.go:94-122): Groups multiple errors together, implements `Unwrap() []error`
48+
4. **Validation** (headers.go:12-55): Represents validation failures with Name, In, Value fields
49+
5. **ParseError** (parsing.go:12-42): Represents parsing errors with Reason field
50+
6. **MethodNotAllowedError** (api.go:74-88): Special error for method not allowed with Allowed methods list
51+
7. **APIVerificationFailed** (middleware.go:12-39): Error for API spec/registration mismatches
52+
53+
### Error Categorization by File
54+
55+
- **api.go**: Core error interface, basic error types, HTTP error serving
56+
- **schema.go**: Validation errors (type, length, pattern, enum, min/max, uniqueness, properties)
57+
- **headers.go**: Header validation errors (content-type, accept)
58+
- **parsing.go**: Parameter parsing errors
59+
- **auth.go**: Authentication errors
60+
- **middleware.go**: API verification errors
61+
62+
### Key Design Patterns
63+
64+
1. **Error Codes**: Custom error codes >= 600 (maximumValidHTTPCode) to differentiate validation types without conflicting with HTTP status codes
65+
2. **Conditional Messages**: Most constructors have "NoIn" variants for errors without an "In" field (e.g., tooLongMessage vs tooLongMessageNoIn)
66+
3. **ServeError Function** (api.go:147-201): Central HTTP error handler using type assertions to handle different error types
67+
4. **Flattening**: CompositeError flattens nested composite errors recursively (api.go:108-134)
68+
5. **Name Validation**: Errors can have their Name field updated for nested properties via ValidateName methods
69+
70+
### JSON Serialization
71+
72+
All error types implement `MarshalJSON()` to provide structured JSON responses with code, message, and type-specific fields.
73+
74+
## Testing Practices
75+
76+
- Uses forked `github.com/go-openapi/testify/v2` for minimal test dependencies
77+
- Tests follow pattern: `*_test.go` files next to implementation
78+
- Test files cover: api_test.go, schema_test.go, middleware_test.go, parsing_test.go, auth_test.go
79+
80+
## Code Quality Standards
81+
82+
### Linting Configuration
83+
- Enable all golangci-lint linters by default, with specific exclusions in .golangci.yml
84+
- Complexity threshold: max 20 (cyclop, gocyclo)
85+
- Line length: max 180 characters
86+
- Run `golangci-lint run` before committing
87+
88+
### Disabled Linters (and why)
89+
Key exclusions from STYLE.md rationale:
90+
- depguard: No import constraints enforced
91+
- funlen: Function length not enforced (cognitive complexity preferred)
92+
- godox: TODOs are acceptable
93+
- nonamedreturns: Named returns are acceptable
94+
- varnamelen: Short variable names allowed when appropriate
95+
96+
## Release Process
97+
98+
- Push semver tag (v{major}.{minor}.{patch}) to master branch
99+
- CI automatically generates release with git-cliff
100+
- Tags should be PGP-signed
101+
- Tag message prepends release notes
102+
103+
## Important Constants
104+
105+
- `DefaultHTTPCode = 422` (http.StatusUnprocessableEntity)
106+
- `maximumValidHTTPCode = 600`
107+
- Custom error codes start at 600+ (InvalidTypeCode, RequiredFailCode, etc.)

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
secrets.yml
2-
coverage.out
2+
*.out
3+
settings.local.json

api.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,12 @@ type apiError struct {
2828
message string
2929
}
3030

31+
// Error implements the standard error interface.
3132
func (a *apiError) Error() string {
3233
return a.message
3334
}
3435

36+
// Code returns the HTTP status code associated with this error.
3537
func (a *apiError) Code() int32 {
3638
return a.code
3739
}
@@ -78,11 +80,12 @@ type MethodNotAllowedError struct {
7880
message string
7981
}
8082

83+
// Error implements the standard error interface.
8184
func (m *MethodNotAllowedError) Error() string {
8285
return m.message
8386
}
8487

85-
// Code the error code.
88+
// Code returns 405 (Method Not Allowed) as the HTTP status code.
8689
func (m *MethodNotAllowedError) Code() int32 {
8790
return m.code
8891
}

examples_test.go

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package errors_test
5+
6+
import (
7+
"encoding/json"
8+
"fmt"
9+
"net/http"
10+
"net/http/httptest"
11+
12+
"github.com/go-openapi/errors"
13+
)
14+
15+
func ExampleNew() {
16+
// Create a generic API error with custom code
17+
err := errors.New(400, "invalid input: %s", "email")
18+
fmt.Printf("error: %v\n", err)
19+
fmt.Printf("code: %d\n", err.Code())
20+
21+
// Create common HTTP errors
22+
notFound := errors.NotFound("user %s not found", "john-doe")
23+
fmt.Printf("not found: %v\n", notFound)
24+
fmt.Printf("not found code: %d\n", notFound.Code())
25+
26+
notImpl := errors.NotImplemented("feature: dark mode")
27+
fmt.Printf("not implemented: %v\n", notImpl)
28+
29+
// Output:
30+
// error: invalid input: email
31+
// code: 400
32+
// not found: user john-doe not found
33+
// not found code: 404
34+
// not implemented: feature: dark mode
35+
}
36+
37+
func ExampleServeError() {
38+
// Create a simple validation error
39+
err := errors.Required("email", "body", nil)
40+
41+
// Simulate HTTP response
42+
recorder := httptest.NewRecorder()
43+
request := httptest.NewRequest(http.MethodPost, "/api/users", nil)
44+
45+
// Serve the error as JSON
46+
errors.ServeError(recorder, request, err)
47+
48+
fmt.Printf("status: %d\n", recorder.Code)
49+
fmt.Printf("content-type: %s\n", recorder.Header().Get("Content-Type"))
50+
51+
// Parse and display the JSON response
52+
var response map[string]any
53+
if err := json.Unmarshal(recorder.Body.Bytes(), &response); err == nil {
54+
fmt.Printf("error code: %.0f\n", response["code"])
55+
fmt.Printf("error message: %s\n", response["message"])
56+
}
57+
58+
// Output:
59+
// status: 422
60+
// content-type: application/json
61+
// error code: 602
62+
// error message: email in body is required
63+
}
64+
65+
func ExampleCompositeValidationError() {
66+
var errs []error
67+
68+
// Collect multiple validation errors
69+
errs = append(errs, errors.Required("name", "body", nil))
70+
errs = append(errs, errors.TooShort("description", "body", 10, "short"))
71+
errs = append(errs, errors.InvalidType("age", "body", "integer", "abc"))
72+
73+
// Combine them into a composite error
74+
compositeErr := errors.CompositeValidationError(errs...)
75+
76+
fmt.Printf("error count: %d\n", len(errs))
77+
fmt.Printf("composite error: %v\n", compositeErr)
78+
fmt.Printf("code: %d\n", compositeErr.Code())
79+
80+
// Can unwrap to access individual errors
81+
if unwrapped := compositeErr.Unwrap(); unwrapped != nil {
82+
fmt.Printf("unwrapped count: %d\n", len(unwrapped))
83+
}
84+
85+
// Output:
86+
// error count: 3
87+
// composite error: validation failure list:
88+
// name in body is required
89+
// description in body should be at least 10 chars long
90+
// age in body must be of type integer: "abc"
91+
// code: 422
92+
// unwrapped count: 3
93+
}

headers.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,13 @@ type Validation struct { //nolint: errname // changing the name to abide by the
1919
Values []any
2020
}
2121

22+
// Error implements the standard error interface.
2223
func (e *Validation) Error() string {
2324
return e.message
2425
}
2526

26-
// Code the error code.
27+
// Code returns the HTTP status code for this validation error.
28+
// Returns 422 (Unprocessable Entity) by default.
2729
func (e *Validation) Code() int32 {
2830
return e.code
2931
}

middleware.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ type APIVerificationFailed struct { //nolint: errname
1717
MissingRegistration []string `json:"missingRegistration,omitempty"`
1818
}
1919

20+
// Error implements the standard error interface.
2021
func (v *APIVerificationFailed) Error() string {
2122
buf := bytes.NewBuffer(nil)
2223

parsing.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,12 @@ func NewParseError(name, in, value string, reason error) *ParseError {
3737
}
3838
}
3939

40+
// Error implements the standard error interface.
4041
func (e *ParseError) Error() string {
4142
return e.message
4243
}
4344

44-
// Code returns the http status code for this error.
45+
// Code returns 400 (Bad Request) as the HTTP status code for parsing errors.
4546
func (e *ParseError) Code() int32 {
4647
return e.code
4748
}

schema.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ const (
7171

7272
// InvalidTypeCode is used for any subclass of invalid types.
7373
InvalidTypeCode = maximumValidHTTPCode + iota
74+
// RequiredFailCode indicates a required field is missing.
7475
RequiredFailCode
7576
TooLongFailCode
7677
TooShortFailCode
@@ -98,11 +99,12 @@ type CompositeError struct {
9899
message string
99100
}
100101

101-
// Code for this error.
102+
// Code returns the HTTP status code for this composite error.
102103
func (c *CompositeError) Code() int32 {
103104
return c.code
104105
}
105106

107+
// Error implements the standard error interface.
106108
func (c *CompositeError) Error() string {
107109
if len(c.Errors) > 0 {
108110
msgs := []string{c.message + ":"}
@@ -117,6 +119,7 @@ func (c *CompositeError) Error() string {
117119
return c.message
118120
}
119121

122+
// Unwrap implements the [errors.Unwrap] interface.
120123
func (c *CompositeError) Unwrap() []error {
121124
return c.Errors
122125
}

0 commit comments

Comments
 (0)