Skip to content

Commit 6b0df72

Browse files
openapi3: add T.WalkSchemas to visit every schema in a document (getkin#1206)
1 parent e56b2a1 commit 6b0df72

3 files changed

Lines changed: 532 additions & 0 deletions

File tree

.github/docs/openapi3.txt

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,12 @@ var IncludeOrigin = false
118118
elements. Deprecated: set Loader.IncludeOrigin instead. This global is read
119119
by NewLoader for backward compatibility but is not safe for concurrent use.
120120

121+
var SkipSubtree = errors.New("skip schema subtree")
122+
SkipSubtree, when returned by a WalkSchemasFunc, tells WalkSchemas not to
123+
descend into the current schema's sub-schemas. The schema itself has already
124+
been visited. Any other non-nil error stops the walk and is returned by
125+
WalkSchemas. It mirrors filepath.SkipDir.
126+
121127

122128
FUNCTIONS
123129

@@ -3336,6 +3342,25 @@ func (doc *T) ValidateSchemaJSON(schema *Schema, value any, opts ...SchemaValida
33363342
format validators. This is a convenience method that automatically applies
33373343
the document's format validators.
33383344

3345+
func (doc *T) WalkSchemas(fn WalkSchemasFunc) error
3346+
WalkSchemas visits every schema reachable from the document exactly once,
3347+
invoking fn for each. It follows resolved $ref targets (schema.Value) and
3348+
guards against reference cycles, so each distinct *Schema is visited a
3349+
single time regardless of how many references point at it. Maps are visited
3350+
in sorted key order, so the traversal is deterministic.
3351+
3352+
It covers schemas under components (schemas, parameters, headers, request
3353+
bodies, responses, callbacks), the paths and their operations (parameters,
3354+
request bodies, responses, headers, callbacks), and webhooks, then recurses
3355+
through every sub-schema keyword: properties, items, allOf/anyOf/oneOf,
3356+
not, additionalProperties, prefixItems, contains, patternProperties,
3357+
dependentSchemas, propertyNames, if/then/else, and $defs.
3358+
3359+
It is useful for validation, code generation, schema transformation,
3360+
$ref/dependency analysis, and documentation: any consumer that needs to act
3361+
on every schema in a document without re-deriving the (easy to get wrong)
3362+
traversal itself.
3363+
33393364
type Tag struct {
33403365
Extensions map[string]any `json:"-" yaml:"-"`
33413366
Origin *Origin `json:"-" yaml:"-"`
@@ -3662,6 +3687,21 @@ type ValidationOptions struct {
36623687
}
36633688
ValidationOptions provides configuration for validating OpenAPI documents.
36643689

3690+
type WalkSchemasFunc func(jsonPointer string, schema *SchemaRef) error
3691+
WalkSchemasFunc is called once for each schema visited by WalkSchemas.
3692+
3693+
jsonPointer is the RFC 6901 JSON Pointer of the schema within
3694+
the document, e.g. "/components/schemas/Pet/properties/tag" or
3695+
"/paths/~1pets/get/responses/200/content/application~1json/schema". It is
3696+
file-agnostic: once the loader has resolved external $refs the document is
3697+
a single tree, so the pointer addresses a position in that tree and never
3698+
encodes a source file. schema is non-nil and schema.Value is non-nil;
3699+
the callback may modify schema.Value in place, so WalkSchemas serves
3700+
transformers and not only read-only inspection.
3701+
3702+
Returning SkipSubtree skips this schema's sub-schemas; returning any other
3703+
error aborts the walk and is returned by WalkSchemas.
3704+
36653705
type WebhookNil struct{ ValidationError }
36663706

36673707
func (e *WebhookNil) As(target any) bool

openapi3/walk.go

Lines changed: 315 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,315 @@
1+
package openapi3
2+
3+
import (
4+
"errors"
5+
"maps"
6+
"slices"
7+
"strconv"
8+
"strings"
9+
)
10+
11+
// SkipSubtree, when returned by a WalkSchemasFunc, tells WalkSchemas not to
12+
// descend into the current schema's sub-schemas. The schema itself has already
13+
// been visited. Any other non-nil error stops the walk and is returned by
14+
// WalkSchemas. It mirrors filepath.SkipDir.
15+
var SkipSubtree = errors.New("skip schema subtree")
16+
17+
// WalkSchemasFunc is called once for each schema visited by WalkSchemas.
18+
//
19+
// jsonPointer is the RFC 6901 JSON Pointer of the schema within the document,
20+
// e.g. "/components/schemas/Pet/properties/tag" or
21+
// "/paths/~1pets/get/responses/200/content/application~1json/schema". It is
22+
// file-agnostic: once the loader has resolved external $refs the document is a
23+
// single tree, so the pointer addresses a position in that tree and never
24+
// encodes a source file. schema is non-nil and schema.Value is non-nil; the
25+
// callback may modify schema.Value in place, so WalkSchemas serves transformers
26+
// and not only read-only inspection.
27+
//
28+
// Returning SkipSubtree skips this schema's sub-schemas; returning any other
29+
// error aborts the walk and is returned by WalkSchemas.
30+
type WalkSchemasFunc func(jsonPointer string, schema *SchemaRef) error
31+
32+
// WalkSchemas visits every schema reachable from the document exactly once,
33+
// invoking fn for each. It follows resolved $ref targets (schema.Value) and
34+
// guards against reference cycles, so each distinct *Schema is visited a single
35+
// time regardless of how many references point at it. Maps are visited in
36+
// sorted key order, so the traversal is deterministic.
37+
//
38+
// It covers schemas under components (schemas, parameters, headers, request
39+
// bodies, responses, callbacks), the paths and their operations (parameters,
40+
// request bodies, responses, headers, callbacks), and webhooks, then recurses
41+
// through every sub-schema keyword: properties, items, allOf/anyOf/oneOf, not,
42+
// additionalProperties, prefixItems, contains, patternProperties,
43+
// dependentSchemas, propertyNames, if/then/else, and $defs.
44+
//
45+
// It is useful for validation, code generation, schema transformation,
46+
// $ref/dependency analysis, and documentation: any consumer that needs to act
47+
// on every schema in a document without re-deriving the (easy to get wrong)
48+
// traversal itself.
49+
func (doc *T) WalkSchemas(fn WalkSchemasFunc) error {
50+
if doc == nil {
51+
return nil
52+
}
53+
w := schemaWalker{fn: fn, seen: make(map[*Schema]struct{})}
54+
return w.document(doc)
55+
}
56+
57+
type schemaWalker struct {
58+
fn WalkSchemasFunc
59+
seen map[*Schema]struct{}
60+
}
61+
62+
// escapeRefString escapes a single JSON Pointer reference token per RFC 6901:
63+
// '~' becomes '~0' and '/' becomes '~1'. It is the inverse of unescapeRefString.
64+
func escapeRefString(s string) string {
65+
if !strings.ContainsAny(s, "~/") {
66+
return s
67+
}
68+
return strings.NewReplacer("~", "~0", "/", "~1").Replace(s)
69+
}
70+
71+
func (w *schemaWalker) document(doc *T) error {
72+
if c := doc.Components; c != nil {
73+
for _, name := range slices.Sorted(maps.Keys(c.Schemas)) {
74+
if err := w.schemaRef("/components/schemas/"+escapeRefString(name), c.Schemas[name]); err != nil {
75+
return err
76+
}
77+
}
78+
for _, name := range slices.Sorted(maps.Keys(c.Parameters)) {
79+
if err := w.parameter("/components/parameters/"+escapeRefString(name), c.Parameters[name]); err != nil {
80+
return err
81+
}
82+
}
83+
for _, name := range slices.Sorted(maps.Keys(c.Headers)) {
84+
if err := w.header("/components/headers/"+escapeRefString(name), c.Headers[name]); err != nil {
85+
return err
86+
}
87+
}
88+
for _, name := range slices.Sorted(maps.Keys(c.RequestBodies)) {
89+
if rbr := c.RequestBodies[name]; rbr != nil && rbr.Value != nil {
90+
if err := w.content("/components/requestBodies/"+escapeRefString(name)+"/content", rbr.Value.Content); err != nil {
91+
return err
92+
}
93+
}
94+
}
95+
for _, name := range slices.Sorted(maps.Keys(c.Responses)) {
96+
if err := w.response("/components/responses/"+escapeRefString(name), c.Responses[name]); err != nil {
97+
return err
98+
}
99+
}
100+
for _, name := range slices.Sorted(maps.Keys(c.Callbacks)) {
101+
if cbr := c.Callbacks[name]; cbr != nil && cbr.Value != nil {
102+
if err := w.callback("/components/callbacks/"+escapeRefString(name), cbr.Value); err != nil {
103+
return err
104+
}
105+
}
106+
}
107+
}
108+
if doc.Paths != nil {
109+
items := doc.Paths.Map()
110+
for _, path := range slices.Sorted(maps.Keys(items)) {
111+
if err := w.pathItem("/paths/"+escapeRefString(path), items[path]); err != nil {
112+
return err
113+
}
114+
}
115+
}
116+
for _, name := range slices.Sorted(maps.Keys(doc.Webhooks)) {
117+
if err := w.pathItem("/webhooks/"+escapeRefString(name), doc.Webhooks[name]); err != nil {
118+
return err
119+
}
120+
}
121+
return nil
122+
}
123+
124+
func (w *schemaWalker) pathItem(ptr string, item *PathItem) error {
125+
if item == nil {
126+
return nil
127+
}
128+
for i, pr := range item.Parameters {
129+
if err := w.parameter(ptr+"/parameters/"+strconv.Itoa(i), pr); err != nil {
130+
return err
131+
}
132+
}
133+
ops := item.Operations()
134+
for _, method := range slices.Sorted(maps.Keys(ops)) {
135+
if err := w.operation(ptr+"/"+strings.ToLower(method), ops[method]); err != nil {
136+
return err
137+
}
138+
}
139+
return nil
140+
}
141+
142+
func (w *schemaWalker) operation(ptr string, op *Operation) error {
143+
if op == nil {
144+
return nil
145+
}
146+
for i, pr := range op.Parameters {
147+
if err := w.parameter(ptr+"/parameters/"+strconv.Itoa(i), pr); err != nil {
148+
return err
149+
}
150+
}
151+
if op.RequestBody != nil && op.RequestBody.Value != nil {
152+
if err := w.content(ptr+"/requestBody/content", op.RequestBody.Value.Content); err != nil {
153+
return err
154+
}
155+
}
156+
if op.Responses != nil {
157+
responses := op.Responses.Map()
158+
for _, code := range slices.Sorted(maps.Keys(responses)) {
159+
if err := w.response(ptr+"/responses/"+escapeRefString(code), responses[code]); err != nil {
160+
return err
161+
}
162+
}
163+
}
164+
for _, name := range slices.Sorted(maps.Keys(op.Callbacks)) {
165+
if cbr := op.Callbacks[name]; cbr != nil && cbr.Value != nil {
166+
if err := w.callback(ptr+"/callbacks/"+escapeRefString(name), cbr.Value); err != nil {
167+
return err
168+
}
169+
}
170+
}
171+
return nil
172+
}
173+
174+
func (w *schemaWalker) callback(ptr string, cb *Callback) error {
175+
items := cb.Map()
176+
for _, expr := range slices.Sorted(maps.Keys(items)) {
177+
if err := w.pathItem(ptr+"/"+escapeRefString(expr), items[expr]); err != nil {
178+
return err
179+
}
180+
}
181+
return nil
182+
}
183+
184+
func (w *schemaWalker) parameter(ptr string, pr *ParameterRef) error {
185+
if pr == nil || pr.Value == nil {
186+
return nil
187+
}
188+
if err := w.schemaRef(ptr+"/schema", pr.Value.Schema); err != nil {
189+
return err
190+
}
191+
return w.content(ptr+"/content", pr.Value.Content)
192+
}
193+
194+
func (w *schemaWalker) header(ptr string, hr *HeaderRef) error {
195+
if hr == nil || hr.Value == nil {
196+
return nil
197+
}
198+
if err := w.schemaRef(ptr+"/schema", hr.Value.Schema); err != nil {
199+
return err
200+
}
201+
return w.content(ptr+"/content", hr.Value.Content)
202+
}
203+
204+
func (w *schemaWalker) response(ptr string, rr *ResponseRef) error {
205+
if rr == nil || rr.Value == nil {
206+
return nil
207+
}
208+
for _, name := range slices.Sorted(maps.Keys(rr.Value.Headers)) {
209+
if err := w.header(ptr+"/headers/"+escapeRefString(name), rr.Value.Headers[name]); err != nil {
210+
return err
211+
}
212+
}
213+
return w.content(ptr+"/content", rr.Value.Content)
214+
}
215+
216+
func (w *schemaWalker) content(ptr string, content Content) error {
217+
for _, mediaType := range slices.Sorted(maps.Keys(content)) {
218+
media := content[mediaType]
219+
if media == nil {
220+
continue
221+
}
222+
if err := w.schemaRef(ptr+"/"+escapeRefString(mediaType)+"/schema", media.Schema); err != nil {
223+
return err
224+
}
225+
}
226+
return nil
227+
}
228+
229+
func (w *schemaWalker) schemaRefs(ptr string, refs SchemaRefs) error {
230+
for i, sub := range refs {
231+
if err := w.schemaRef(ptr+"/"+strconv.Itoa(i), sub); err != nil {
232+
return err
233+
}
234+
}
235+
return nil
236+
}
237+
238+
func (w *schemaWalker) schemaRef(ptr string, sr *SchemaRef) error {
239+
if sr == nil || sr.Value == nil {
240+
return nil
241+
}
242+
s := sr.Value
243+
if _, ok := w.seen[s]; ok {
244+
// Already visited (shared $ref target or reference cycle).
245+
return nil
246+
}
247+
w.seen[s] = struct{}{}
248+
249+
if err := w.fn(ptr, sr); err != nil {
250+
if errors.Is(err, SkipSubtree) {
251+
return nil
252+
}
253+
return err
254+
}
255+
256+
for _, name := range slices.Sorted(maps.Keys(s.Properties)) {
257+
if err := w.schemaRef(ptr+"/properties/"+escapeRefString(name), s.Properties[name]); err != nil {
258+
return err
259+
}
260+
}
261+
if err := w.schemaRef(ptr+"/items", s.Items); err != nil {
262+
return err
263+
}
264+
if s.AdditionalProperties.Schema != nil {
265+
if err := w.schemaRef(ptr+"/additionalProperties", s.AdditionalProperties.Schema); err != nil {
266+
return err
267+
}
268+
}
269+
if err := w.schemaRefs(ptr+"/allOf", s.AllOf); err != nil {
270+
return err
271+
}
272+
if err := w.schemaRefs(ptr+"/anyOf", s.AnyOf); err != nil {
273+
return err
274+
}
275+
if err := w.schemaRefs(ptr+"/oneOf", s.OneOf); err != nil {
276+
return err
277+
}
278+
if err := w.schemaRef(ptr+"/not", s.Not); err != nil {
279+
return err
280+
}
281+
if err := w.schemaRefs(ptr+"/prefixItems", s.PrefixItems); err != nil {
282+
return err
283+
}
284+
if err := w.schemaRef(ptr+"/contains", s.Contains); err != nil {
285+
return err
286+
}
287+
for _, name := range slices.Sorted(maps.Keys(s.PatternProperties)) {
288+
if err := w.schemaRef(ptr+"/patternProperties/"+escapeRefString(name), s.PatternProperties[name]); err != nil {
289+
return err
290+
}
291+
}
292+
for _, name := range slices.Sorted(maps.Keys(s.DependentSchemas)) {
293+
if err := w.schemaRef(ptr+"/dependentSchemas/"+escapeRefString(name), s.DependentSchemas[name]); err != nil {
294+
return err
295+
}
296+
}
297+
if err := w.schemaRef(ptr+"/propertyNames", s.PropertyNames); err != nil {
298+
return err
299+
}
300+
if err := w.schemaRef(ptr+"/if", s.If); err != nil {
301+
return err
302+
}
303+
if err := w.schemaRef(ptr+"/then", s.Then); err != nil {
304+
return err
305+
}
306+
if err := w.schemaRef(ptr+"/else", s.Else); err != nil {
307+
return err
308+
}
309+
for _, name := range slices.Sorted(maps.Keys(s.Defs)) {
310+
if err := w.schemaRef(ptr+"/$defs/"+escapeRefString(name), s.Defs[name]); err != nil {
311+
return err
312+
}
313+
}
314+
return nil
315+
}

0 commit comments

Comments
 (0)