From 27bc6a2a4081414f8e4dd6ae9a1728ab698d4a5d Mon Sep 17 00:00:00 2001 From: Reuven Harrison Date: Tue, 14 Jul 2026 19:34:31 +0300 Subject: [PATCH 1/2] openapi3: add T.WalkParameters to visit every parameter in a document The parameter counterpart of WalkSchemas: visits every parameter reachable from the document exactly once (components.parameters, path items, operations, callbacks, webhooks), following resolved $refs so a shared parameter is visited a single time, at its definition, in deterministic sorted order with RFC 6901 pointers. Useful for validation, linting, and parameter transformation without re-deriving the traversal. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01BqJA1X6suZYtR3tRbX8sLj --- .github/docs/openapi3.txt | 26 +++++++ openapi3/walk_parameters.go | 128 +++++++++++++++++++++++++++++++ openapi3/walk_parameters_test.go | 108 ++++++++++++++++++++++++++ 3 files changed, 262 insertions(+) create mode 100644 openapi3/walk_parameters.go create mode 100644 openapi3/walk_parameters_test.go diff --git a/.github/docs/openapi3.txt b/.github/docs/openapi3.txt index c04d34ec2..725e1a1b8 100644 --- a/.github/docs/openapi3.txt +++ b/.github/docs/openapi3.txt @@ -3343,6 +3343,18 @@ func (doc *T) ValidateSchemaJSON(schema *Schema, value any, opts ...SchemaValida format validators. This is a convenience method that automatically applies the document's format validators. +func (doc *T) WalkParameters(fn WalkParametersFunc) error + WalkParameters visits every parameter reachable from the document exactly + once, invoking fn for each. It follows resolved $ref targets (param.Value), + so each distinct *Parameter is visited a single time regardless of how + many references point at it. Maps are visited in sorted key order, so the + traversal is deterministic. + + It covers components.parameters, path items, operations, callbacks, + and webhooks. It is the parameter counterpart of WalkSchemas: useful for + validation, linting, parameter transformation, and documentation, without + re-deriving the (easy to get wrong) traversal. + func (doc *T) WalkSchemas(fn WalkSchemasFunc) error WalkSchemas visits every schema reachable from the document exactly once, invoking fn for each. It follows resolved $ref targets (schema.Value) and @@ -3691,6 +3703,20 @@ type ValidationOptions struct { } ValidationOptions provides configuration for validating OpenAPI documents. +type WalkParametersFunc func(jsonPointer string, param *ParameterRef) error + WalkParametersFunc is called once for each parameter visited by + WalkParameters. + + jsonPointer is the RFC 6901 JSON Pointer of the parameter + within the document, e.g. "/components/parameters/Limit" or + "/paths/~1pets/get/parameters/0". For a parameter referenced from several + places it is the pointer of the first visit, and components are visited + first, so a shared parameter is reported at its definition. param is non-nil + and param.Value is non-nil; the callback may modify param.Value in place, + so WalkParameters serves transformers and not only read-only inspection. + + Returning a non-nil error aborts the walk and is returned by WalkParameters. + type WalkSchemasFunc func(jsonPointer string, schema *SchemaRef) error WalkSchemasFunc is called once for each schema visited by WalkSchemas. diff --git a/openapi3/walk_parameters.go b/openapi3/walk_parameters.go new file mode 100644 index 000000000..d60edb6c7 --- /dev/null +++ b/openapi3/walk_parameters.go @@ -0,0 +1,128 @@ +package openapi3 + +import ( + "maps" + "slices" + "strconv" + "strings" +) + +// WalkParametersFunc is called once for each parameter visited by +// WalkParameters. +// +// jsonPointer is the RFC 6901 JSON Pointer of the parameter within the +// document, e.g. "/components/parameters/Limit" or +// "/paths/~1pets/get/parameters/0". For a parameter referenced from several +// places it is the pointer of the first visit, and components are visited +// first, so a shared parameter is reported at its definition. param is +// non-nil and param.Value is non-nil; the callback may modify param.Value in +// place, so WalkParameters serves transformers and not only read-only +// inspection. +// +// Returning a non-nil error aborts the walk and is returned by +// WalkParameters. +type WalkParametersFunc func(jsonPointer string, param *ParameterRef) error + +// WalkParameters visits every parameter reachable from the document exactly +// once, invoking fn for each. It follows resolved $ref targets (param.Value), +// so each distinct *Parameter is visited a single time regardless of how many +// references point at it. Maps are visited in sorted key order, so the +// traversal is deterministic. +// +// It covers components.parameters, path items, operations, callbacks, and +// webhooks. It is the parameter counterpart of WalkSchemas: useful for +// validation, linting, parameter transformation, and documentation, without +// re-deriving the (easy to get wrong) traversal. +func (doc *T) WalkParameters(fn WalkParametersFunc) error { + if doc == nil { + return nil + } + w := parameterWalker{fn: fn, seen: make(map[*Parameter]struct{})} + return w.document(doc) +} + +type parameterWalker struct { + fn WalkParametersFunc + seen map[*Parameter]struct{} +} + +func (w *parameterWalker) document(doc *T) error { + if c := doc.Components; c != nil { + for _, name := range slices.Sorted(maps.Keys(c.Parameters)) { + if err := w.parameter("/components/parameters/"+escapeRefString(name), c.Parameters[name]); err != nil { + return err + } + } + for _, name := range slices.Sorted(maps.Keys(c.Callbacks)) { + if cbr := c.Callbacks[name]; cbr != nil && cbr.Value != nil { + if err := w.callback("/components/callbacks/"+escapeRefString(name), cbr.Value); err != nil { + return err + } + } + } + } + if doc.Paths != nil { + items := doc.Paths.Map() + for _, path := range slices.Sorted(maps.Keys(items)) { + if err := w.pathItem("/paths/"+escapeRefString(path), items[path]); err != nil { + return err + } + } + } + for _, name := range slices.Sorted(maps.Keys(doc.Webhooks)) { + if err := w.pathItem("/webhooks/"+escapeRefString(name), doc.Webhooks[name]); err != nil { + return err + } + } + return nil +} + +func (w *parameterWalker) pathItem(ptr string, item *PathItem) error { + if item == nil { + return nil + } + for i, pr := range item.Parameters { + if err := w.parameter(ptr+"/parameters/"+strconv.Itoa(i), pr); err != nil { + return err + } + } + ops := item.Operations() + for _, method := range slices.Sorted(maps.Keys(ops)) { + op := ops[method] + opPtr := ptr + "/" + strings.ToLower(method) + for i, pr := range op.Parameters { + if err := w.parameter(opPtr+"/parameters/"+strconv.Itoa(i), pr); err != nil { + return err + } + } + for _, name := range slices.Sorted(maps.Keys(op.Callbacks)) { + if cbr := op.Callbacks[name]; cbr != nil && cbr.Value != nil { + if err := w.callback(opPtr+"/callbacks/"+escapeRefString(name), cbr.Value); err != nil { + return err + } + } + } + } + return nil +} + +func (w *parameterWalker) callback(ptr string, cb *Callback) error { + items := cb.Map() + for _, expr := range slices.Sorted(maps.Keys(items)) { + if err := w.pathItem(ptr+"/"+escapeRefString(expr), items[expr]); err != nil { + return err + } + } + return nil +} + +func (w *parameterWalker) parameter(ptr string, pr *ParameterRef) error { + if pr == nil || pr.Value == nil { + return nil + } + if _, ok := w.seen[pr.Value]; ok { + return nil + } + w.seen[pr.Value] = struct{}{} + return w.fn(ptr, pr) +} diff --git a/openapi3/walk_parameters_test.go b/openapi3/walk_parameters_test.go new file mode 100644 index 000000000..951509850 --- /dev/null +++ b/openapi3/walk_parameters_test.go @@ -0,0 +1,108 @@ +package openapi3_test + +import ( + "errors" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/getkin/kin-openapi/openapi3" +) + +// walkParametersSpec exercises parameters in components (shared via $ref from +// two operations: dedup), path items, operations, a callback, and a webhook. +var walkParametersSpec = []byte(` +openapi: 3.1.0 +info: {title: t, version: "1"} +paths: + /pets: + parameters: + - name: tenant + in: header + schema: {type: string} + get: + parameters: + - $ref: '#/components/parameters/Limit' + - name: filter + in: query + schema: {type: string} + responses: + "200": {description: ok} + post: + parameters: + - $ref: '#/components/parameters/Limit' + callbacks: + onEvent: + '{$request.body#/url}': + post: + parameters: + - name: attempt + in: query + schema: {type: integer} + responses: + "200": {description: ok} + responses: + "201": {description: created} +webhooks: + newPet: + post: + parameters: + - name: signature + in: header + schema: {type: string} + responses: + "200": {description: ok} +components: + parameters: + Limit: + name: limit + in: query + schema: {type: integer} +`) + +func TestWalkParameters(t *testing.T) { + loader := openapi3.NewLoader() + doc, err := loader.LoadFromData(walkParametersSpec) + require.NoError(t, err) + + visited := map[string]string{} // pointer -> parameter name + require.NoError(t, doc.WalkParameters(func(jsonPointer string, param *openapi3.ParameterRef) error { + require.NotNil(t, param) + require.NotNil(t, param.Value) + _, dup := visited[jsonPointer] + require.Falsef(t, dup, "pointer %q visited twice", jsonPointer) + visited[jsonPointer] = param.Value.Name + return nil + })) + + require.Equal(t, map[string]string{ + // the shared Limit parameter is visited once, at its definition + "/components/parameters/Limit": "limit", + "/paths/~1pets/parameters/0": "tenant", + "/paths/~1pets/get/parameters/1": "filter", + "/paths/~1pets/post/callbacks/onEvent/{$request.body#~1url}/post/parameters/0": "attempt", + "/webhooks/newPet/post/parameters/0": "signature", + }, visited) +} + +func TestWalkParameters_ErrorAborts(t *testing.T) { + loader := openapi3.NewLoader() + doc, err := loader.LoadFromData(walkParametersSpec) + require.NoError(t, err) + + boom := errors.New("boom") + calls := 0 + require.ErrorIs(t, doc.WalkParameters(func(string, *openapi3.ParameterRef) error { + calls++ + return boom + }), boom) + require.Equal(t, 1, calls, "the walk stops at the first error") +} + +func TestWalkParameters_NilDoc(t *testing.T) { + var doc *openapi3.T + require.NoError(t, doc.WalkParameters(func(string, *openapi3.ParameterRef) error { + t.Fatal("no visits on a nil document") + return nil + })) +} From 339ae1577b3c5112a553897ffe49208217309cb6 Mon Sep 17 00:00:00 2001 From: Reuven Harrison Date: Tue, 14 Jul 2026 19:48:45 +0300 Subject: [PATCH 2/2] openapi3: rename walk.go to walk_schemas.go With walk_parameters.go added, name the schema walker's file to match. --- openapi3/{walk.go => walk_schemas.go} | 0 openapi3/{walk_test.go => walk_schemas_test.go} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename openapi3/{walk.go => walk_schemas.go} (100%) rename openapi3/{walk_test.go => walk_schemas_test.go} (100%) diff --git a/openapi3/walk.go b/openapi3/walk_schemas.go similarity index 100% rename from openapi3/walk.go rename to openapi3/walk_schemas.go diff --git a/openapi3/walk_test.go b/openapi3/walk_schemas_test.go similarity index 100% rename from openapi3/walk_test.go rename to openapi3/walk_schemas_test.go