Skip to content
Closed
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
26 changes: 26 additions & 0 deletions .github/docs/openapi3.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.

Expand Down
128 changes: 128 additions & 0 deletions openapi3/walk_parameters.go
Original file line number Diff line number Diff line change
@@ -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)
}
108 changes: 108 additions & 0 deletions openapi3/walk_parameters_test.go
Original file line number Diff line number Diff line change
@@ -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
}))
}
File renamed without changes.
File renamed without changes.
Loading