Skip to content

Commit ea3bbb4

Browse files
authored
Merge pull request #59 from oaswrap/feat/adjust-validation-openapi
feat: adjust validation openapi
2 parents 9b06177 + 726cc85 commit ea3bbb4

146 files changed

Lines changed: 4164 additions & 1366 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,6 @@ coverage/
3434
.vscode/
3535

3636
# AI assistant data
37-
.claude/
37+
.claude/
38+
39+
references/*

CLAUDE.md

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Commands
6+
7+
```bash
8+
# Run core tests only
9+
go test ./...
10+
11+
# Run all tests (core + all adapters)
12+
make test
13+
14+
# Run adapter tests only
15+
make test-adapter
16+
17+
# Run a single test
18+
go test -run TestGolden ./...
19+
20+
# Update golden YAML fixtures after intentional output changes
21+
make test-update
22+
23+
# Lint (golangci-lint v2.12.2)
24+
make lint
25+
make lint-fix # auto-fix
26+
27+
# Tidy go.mod for core + all adapters
28+
make tidy
29+
30+
# Full local check: sync + tidy + lint + test
31+
make check
32+
33+
# Install dev tools (golangci-lint)
34+
make install-tools
35+
```
36+
37+
## Architecture
38+
39+
`oaswrap/spec` is a framework-agnostic OpenAPI 3.x document builder for Go. It generates spec documents from route registrations and Go struct reflection rather than parsing code annotations.
40+
41+
### Core package (`github.com/oaswrap/spec`)
42+
43+
- `types.go` — Public interfaces: `Generator` (embeds `Router`), `Router`, `Route`. Also re-exports common OpenAPI types.
44+
- `router.go` — Concrete `generator` struct implementing `Generator`. `NewGenerator`/`NewRouter` are entry points. Routes accumulate in a tree; `build()` is called on every `GenerateSchema`, `MarshalYAML`, `MarshalJSON`, or `Validate` call (not cached — rebuilds each time).
45+
- `errors.go``ValidationErrors` aggregating `validate.Error` with severity. Only `SeverityError` items cause `Validate()` / serialization to fail; `SeverityWarning` and `SeverityInfo` are informational.
46+
47+
### Sub-packages
48+
49+
| Package | Role |
50+
|---|---|
51+
| `openapi/` | Data model structs for OpenAPI documents (`Document`, `Schema`, `Operation`, `Config`, etc.). `Config` drives generator behavior. |
52+
| `option/` | Functional options (`OpenAPIOption`, `OperationOption`, `GroupOption`, `ContentOption`) for configuring the generator and individual operations. |
53+
| `internal/builder/` | Converts accumulated route + option data into `openapi.Document` operations via `Builder.AddOperation` and `Builder.AddWebhookOperation`. |
54+
| `internal/reflect/` | Reflects Go types to `openapi.Schema` objects, managing `$components/schemas` de-duplication. |
55+
| `internal/validate/` | Document validation. Issues carry a `Severity` (Error / Warning / Info). `ValidateDocument` is called at the end of every `build()`. |
56+
| `pkg/parser/` | `ColonParamParser` converts `:param` style paths (used by some frameworks) to `{param}` OpenAPI path template format. |
57+
| `internal/testutil/` | Golden-file test helpers used in `_test.go` files. |
58+
59+
### Adapters (`adapter/`)
60+
61+
Eight framework adapters wrap a spec `Generator` alongside a real HTTP router:
62+
63+
```
64+
chiopenapi echoopenapi echov5openapi fiberopenapi
65+
fiberv3openapi ginopenapi httpopenapi muxopenapi
66+
```
67+
68+
Each adapter is a separate Go module (its own `go.mod`). All are included in the root `go.work` workspace. The pattern is:
69+
- `NewRouter(frameworkRouter, ...option.OpenAPIOption)` → returns adapter's `Generator`
70+
- Route registrations go to both the HTTP router and the spec generator simultaneously
71+
- `gen spec.Generator` field must be propagated into every sub-router and group created by the adapter
72+
73+
### Golden tests
74+
75+
Test fixtures live in `testdata/` as `{case}.{version}.yaml` files (e.g. `petstore.v31.yaml`). Tests cover all three version families: `v30`, `v31`, `v32`. Run `make test-update` to regenerate them after intentional output changes.
76+
77+
### Supported OpenAPI versions
78+
79+
- 3.0.x (`openapi.Version304` is the default)
80+
- 3.1.x (`openapi.Version312` recommended)
81+
- 3.2.0 (`openapi.Version320`) — adds `QUERY` method and `$self`
82+
83+
Webhooks require 3.1.x or 3.2.0.
84+
85+
## Key constraints
86+
87+
- Commits must follow [Conventional Commits](https://www.conventionalcommits.org/): `feat:`, `fix:`, `docs:`, `refactor:`, `test:`, `chore:`, etc. (enforced by lefthook).
88+
- Max line length is 120 characters (golines).
89+
- Import groups: stdlib → third-party → `github.com/oaswrap/spec` (enforced by goimports).
90+
- Release is a two-stage process: `make release-prepare VERSION=x.y.z` (tags core, syncs adapter deps) then `make release-publish VERSION=x.y.z` (tags all adapters).

Makefile

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -56,24 +56,24 @@ test: ## Run all tests (core + adapters)
5656
@echo "$(GREEN)✅ Core tests passed$(NC)"
5757
@for a in $(ADAPTERS); do \
5858
echo "$(BLUE)🔍 Testing adapter $$a...$(NC)"; \
59-
(cd "adapter/$$a" && go test ./...) || (echo "$(RED)❌ Adapter $$a tests failed$(NC)" && exit 1); \
59+
(cd "adapter/$$a" && go test ./...) || { echo "$(RED)❌ Adapter $$a tests failed$(NC)"; exit 1; }; \
6060
done
6161
@echo "$(GREEN)🎉 All tests passed!$(NC)"
6262

6363
test-adapter: ## Run tests for all adapters
6464
@echo "$(BLUE)🔍 Running tests for all adapters...$(NC)"
6565
@for a in $(ADAPTERS); do \
6666
echo "$(BLUE)🔍 Testing adapter $$a...$(NC)"; \
67-
(cd "adapter/$$a" && go test ./...) || (echo "$(RED)❌ Adapter $$a tests failed$(NC)" && exit 1); \
67+
(cd "adapter/$$a" && go test ./...) || { echo "$(RED)❌ Adapter $$a tests failed$(NC)"; exit 1; }; \
6868
done
6969
@echo "$(GREEN)🎉 All adapter tests passed!$(NC)"
7070

7171
test-update: ## Update golden files for tests
7272
@echo "$(YELLOW)🔍 Running core tests (updating golden files)...$(NC)"
73-
@go test $(PKG) -args -update || (echo "$(RED)❌ Core test update failed$(NC)" && exit 1)
73+
@go test . -args -update || (echo "$(RED)❌ Core test update failed$(NC)" && exit 1)
7474
@for a in $(ADAPTERS); do \
7575
echo "$(YELLOW)🔍 Updating adapter $$a golden files...$(NC)"; \
76-
(cd "adapter/$$a" && go test ./... -args -update) || (echo "$(RED)❌ Adapter $$a update failed$(NC)" && exit 1); \
76+
(cd "adapter/$$a" && go test . -args -update) || { echo "$(RED)❌ Adapter $$a update failed$(NC)"; exit 1; }; \
7777
done
7878
@echo "$(GREEN)✅ All golden files updated!$(NC)"
7979

@@ -137,7 +137,7 @@ lint: ## Run linting
137137
@for a in $(ADAPTERS); do \
138138
echo "$(BLUE)🔍 Linting adapter/$$a...$(NC)"; \
139139
(cd "adapter/$$a" && golangci-lint run ./...) || \
140-
(echo "$(RED)❌ Adapter $$a linting failed$(NC)" && exit 1); \
140+
{ echo "$(RED)❌ Adapter $$a linting failed$(NC)"; exit 1; }; \
141141
done
142142
@echo "$(GREEN)🎉 All linting passed!$(NC)"
143143

@@ -147,7 +147,7 @@ lint-fix: ## Run linting with auto-fix
147147
@for a in $(ADAPTERS); do \
148148
echo "$(BLUE)🔧 Auto-fixing adapter/$$a...$(NC)"; \
149149
(cd "adapter/$$a" && golangci-lint run --fix ./...) || \
150-
(echo "$(RED)❌ Adapter $$a lint-fix failed$(NC)" && exit 1); \
150+
{ echo "$(RED)❌ Adapter $$a lint-fix failed$(NC)"; exit 1; }; \
151151
done
152152
@echo "$(GREEN)✅ Lint fixes applied!$(NC)"
153153

README.md

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
[![Go Version](https://img.shields.io/github/go-mod/go-version/oaswrap/spec)](https://github.com/oaswrap/spec/blob/main/go.mod)
88
[![License](https://img.shields.io/github/license/oaswrap/spec)](LICENSE)
99

10-
`spec` is a Go library for generating OpenAPI `3.0.x`, `3.1.x`, and `3.2.0` documents. It uses a router and functional options API, and owns its OpenAPI model and schema reflection — no external OpenAPI or JSON Schema generators needed. YAML serialization uses `github.com/goccy/go-yaml`.
10+
Code-first, framework-agnostic OpenAPI 3.x spec builder for Go. Generate docs from route registrations and Go structs — no annotations, no vendor lock-in.
1111

1212
---
1313

@@ -16,7 +16,7 @@
1616
- **Native OpenAPI builder** — paths, operations, components, validation, and schema reflection are all implemented in this repository without third-party OpenAPI dependencies.
1717
- **Framework-agnostic core** — use `spec.NewRouter` for static generation, or drop in adapters for Chi, Echo, Gin, Fiber, net/http, and Mux.
1818
- **Code-first route documentation** — register routes and their documentation together using Go functions and typed options.
19-
- **Version-aware output** — defaults to OpenAPI `3.0.4`, with full support for `3.1.2` and `3.2.0` features when selected.
19+
- **Version-aware output** — defaults to OpenAPI `3.1.2`, with full support for `3.0.x` and `3.2.0` features when selected.
2020
- **Direct model escape hatches** — use typed `openapi` structs, `Extensions` for `x-*` fields, and `Extra` for official or future fields not yet wrapped by a helper option.
2121
- **Deterministic output** — generated documents are stable enough for golden-file snapshot tests and CI documentation checks.
2222

@@ -124,7 +124,8 @@ type User struct {
124124
| `MarshalJSON()` | Validates and serializes pretty-printed JSON. |
125125
| `WriteSchemaTo("openapi.yaml")` | Infers format from file extension (`.yaml`, `.yml`, `.json`). |
126126
| `Document()` | Returns the built `*openapi.Document`. |
127-
| `Validate()` | Builds the document and checks OpenAPI invariants. |
127+
| `Validate()` | Builds the document and checks OpenAPI invariants. Returns only `SeverityError` findings. |
128+
| `ValidateReport()` | Builds and validates, returning all findings including warnings and info as `ValidationErrors`. |
128129
| `Config()` | Returns the effective OpenAPI configuration. |
129130

130131
---
@@ -181,7 +182,7 @@ r := spec.NewRouter(
181182
| Option | Purpose |
182183
| --- | --- |
183184
| `WithOpenAPIConfig(opts...)` | Build an `*openapi.Config` with defaults and apply options. |
184-
| `WithOpenAPIVersion(version)` | Set `openapi`; default is `openapi.Version304`. Constants are available for `3.0.0``3.0.4`, `3.1.0``3.1.2`, and `3.2.0`. |
185+
| `WithOpenAPIVersion(version)` | Set `openapi`; default is `openapi.Version312`. Constants are available for `3.0.0``3.0.4`, `3.1.0``3.1.2`, and `3.2.0`. |
185186
| `WithSelf(uri)` | Set OpenAPI `3.2.0` `$self`. |
186187
| `WithJSONSchemaDialect(uri)` | Set root `jsonSchemaDialect`. |
187188
| `WithTitle(title)` | Set `info.title`. |
@@ -227,7 +228,7 @@ r := spec.NewRouter(
227228
228229
**Tag options:** `TagSummary`, `TagDescription`, `TagExternalDocs`, `TagParent` (3.2.0), `TagKind` (3.2.0).
229230

230-
**Server options:** `ServerDescription`, `ServerVariables`.
231+
**Server options:** `ServerDescription`, `ServerVariables`, `ServerName` (3.2.0).
231232

232233
---
233234

@@ -283,6 +284,7 @@ api.Get("/users/{id}",
283284
| --- | --- |
284285
| `ContentType(contentType)` | Set media type; default is `application/json`. |
285286
| `ContentDescription(description)` | Set request/response description. |
287+
| `ContentSummary(summary)` | Set request/response summary (OpenAPI `3.2.0`). |
286288
| `ContentDefault(isDefault...)` | Mark response as `default`. |
287289
| `ContentEncoding(prop, enc)` | Add media type encoding metadata for a property. |
288290
| `ContentExample(value)` | Set media type `example`. |
@@ -366,6 +368,7 @@ type SearchRequest struct {
366368
| `header:"name"` | Header parameter. |
367369
| `cookie:"name"` | Cookie parameter. |
368370
| `querystring:"name"` | OpenAPI `3.2.0` whole-query-string parameter. |
371+
| `mediaType:"..."` | Media type for `querystring` parameter content; defaults to `application/x-www-form-urlencoded`. OpenAPI `3.2.0` only. |
369372
| `form:"name"` | Form body property name for form content types. |
370373

371374
**Schema tags:**
@@ -483,10 +486,13 @@ Selecting `openapi.Version320` enables the following additional features:
483486
- Custom HTTP methods via `Add`, emitted as `additionalOperations`.
484487
- `querystring` parameter tags.
485488
- Root `$self` field.
489+
- Server `name` field.
490+
- Response `summary` field.
486491
- Tag `parent` and `kind` fields.
487492
- Security scheme metadata and deprecation fields.
488493
- `components.mediaTypes`.
489494
- Media type and encoding fields: `itemSchema`, `prefixEncoding`, `itemEncoding`.
495+
- Discriminator `defaultMapping`.
490496
- Example `dataValue` and `serializedValue` fields.
491497
- XML `nodeType`.
492498

adapter/chiopenapi/internal/constant/constant.go

Lines changed: 0 additions & 7 deletions
This file was deleted.

adapter/chiopenapi/router.go

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,9 @@ import (
77

88
"github.com/oaswrap/spec"
99
specui "github.com/oaswrap/spec-ui"
10-
"github.com/oaswrap/spec/openapi"
10+
"github.com/oaswrap/spec/internal/mapper"
11+
"github.com/oaswrap/spec/internal/validate"
1112
"github.com/oaswrap/spec/option"
12-
"github.com/oaswrap/spec/pkg/mapper"
13-
14-
"github.com/oaswrap/spec/adapter/chiopenapi/internal/constant"
1513
)
1614

1715
type router struct {
@@ -34,9 +32,9 @@ func NewRouter(r chi.Router, opts ...option.OpenAPIOption) Generator {
3432
// It initializes the OpenAPI configuration and sets up the necessary handlers for OpenAPI documentation.
3533
func NewGenerator(r chi.Router, opts ...option.OpenAPIOption) Generator {
3634
defaultOpts := []option.OpenAPIOption{
37-
option.WithTitle(constant.DefaultTitle),
38-
option.WithDescription(constant.DefaultDescription),
39-
option.WithVersion(constant.DefaultVersion),
35+
option.WithTitle("Chi OpenAPI"),
36+
option.WithDescription("OpenAPI documentation for Chi applications"),
37+
option.WithVersion("1.0.0"),
4038
option.WithStoplightElements(),
4139
option.WithCacheAge(0),
4240
}
@@ -124,8 +122,7 @@ func (r *router) Mount(pattern string, h http.Handler) {
124122

125123
func (r *router) Method(method, pattern string, h http.Handler) Route {
126124
r.chiRouter.Method(method, pattern, h)
127-
if method == http.MethodConnect && r.gen.Config().OpenAPIVersion != openapi.Version320 {
128-
// CONNECT requires OpenAPI 3.2, so older specs skip it
125+
if !validate.AllowsOperationMethod(r.gen.Config().OpenAPIVersion, method) {
129126
return &route{}
130127
}
131128
sr := r.specRouter.Add(method, pattern)
@@ -135,8 +132,7 @@ func (r *router) Method(method, pattern string, h http.Handler) Route {
135132

136133
func (r *router) MethodFunc(method, pattern string, h http.HandlerFunc) Route {
137134
r.chiRouter.MethodFunc(method, pattern, h)
138-
if method == http.MethodConnect && r.gen.Config().OpenAPIVersion != openapi.Version320 {
139-
// CONNECT requires OpenAPI 3.2, so older specs skip it
135+
if !validate.AllowsOperationMethod(r.gen.Config().OpenAPIVersion, method) {
140136
return &route{}
141137
}
142138
sr := r.specRouter.Add(method, pattern)
@@ -197,6 +193,10 @@ func (r *router) Validate() error {
197193
return r.gen.Validate()
198194
}
199195

196+
func (r *router) ValidateReport() error {
197+
return r.gen.ValidateReport()
198+
}
199+
200200
func (r *router) GenerateSchema(formats ...string) ([]byte, error) {
201201
return r.gen.GenerateSchema(formats...)
202202
}

adapter/chiopenapi/router_test.go

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,6 @@ func TestRouter_Spec(t *testing.T) {
249249
t.Run(tt.name, func(t *testing.T) {
250250
app := chi.NewRouter()
251251
opts := []option.OpenAPIOption{
252-
option.WithOpenAPIVersion("3.0.3"),
253252
option.WithTitle("Test API " + tt.name),
254253
option.WithVersion("1.0.0"),
255254
option.WithDescription("This is a test API for " + tt.name),
@@ -575,7 +574,7 @@ func TestGenerator_Docs(t *testing.T) {
575574
rr := httptest.NewRecorder()
576575
c.ServeHTTP(rr, req)
577576
assert.Equal(t, http.StatusOK, rr.Code, "expected status OK for /docs/openapi.yaml route")
578-
assert.Contains(t, rr.Body.String(), "openapi: 3.0.4", "expected response body to contain 'openapi: 3.0.4'")
577+
assert.Contains(t, rr.Body.String(), "openapi: 3.1.2", "expected response body to contain 'openapi: 3.1.2'")
579578
})
580579
}
581580

@@ -631,7 +630,7 @@ func TestGenerator_MarshalJSON(t *testing.T) {
631630
schema, err := r.MarshalJSON()
632631
require.NoError(t, err, "failed to marshal OpenAPI schema to JSON")
633632
assert.NotEmpty(t, schema, "expected non-empty OpenAPI schema JSON")
634-
assert.Contains(t, string(schema), `"openapi": "3.0.4"`, "expected OpenAPI version in schema JSON")
633+
assert.Contains(t, string(schema), `"openapi": "3.1.2"`, "expected OpenAPI version in schema JSON")
635634
assert.Contains(t, string(schema), `"title": "Chi OpenAPI"`, "expected title in schema JSON")
636635
}
637636

@@ -650,7 +649,7 @@ func TestGenerator_MarshalYAML(t *testing.T) {
650649
schema, err := r.MarshalYAML()
651650
require.NoError(t, err, "failed to marshal OpenAPI schema to YAML")
652651
assert.NotEmpty(t, schema, "expected non-empty OpenAPI schema YAML")
653-
assert.Contains(t, string(schema), "openapi: 3.0.4", "expected OpenAPI version in schema YAML")
652+
assert.Contains(t, string(schema), "openapi: 3.1.2", "expected OpenAPI version in schema YAML")
654653
assert.Contains(t, string(schema), "title: Chi OpenAPI", "expected title in schema YAML")
655654
}
656655

@@ -674,6 +673,17 @@ func TestGenerator_WriteSchemaTo(t *testing.T) {
674673
schema, err := os.ReadFile(goldenPath)
675674
require.NoError(t, err, "failed to read OpenAPI schema file")
676675
assert.NotEmpty(t, schema, "expected non-empty OpenAPI schema file")
677-
assert.Contains(t, string(schema), "openapi: 3.0.4", "expected OpenAPI version in schema file")
676+
assert.Contains(t, string(schema), "openapi: 3.1.2", "expected OpenAPI version in schema file")
678677
assert.Contains(t, string(schema), "title: Chi OpenAPI", "expected title in schema file")
679678
}
679+
680+
func TestGenerator_ValidateReport(t *testing.T) {
681+
c := chi.NewRouter()
682+
r := chiopenapi.NewRouter(c,
683+
option.WithContact(openapi.Contact{Name: "Support"}),
684+
option.WithLicense(openapi.License{Name: "MIT"}),
685+
option.WithServer("https://example.com"),
686+
)
687+
err := r.ValidateReport()
688+
assert.NoError(t, err)
689+
}

0 commit comments

Comments
 (0)