Skip to content

Commit adbe0b4

Browse files
Merge pull request #52 from puppetlabs/upstream/recursive-schemas
Handle recursive schemas correctly
2 parents db11a30 + e4ad0a3 commit adbe0b4

File tree

7 files changed

+207
-19
lines changed

7 files changed

+207
-19
lines changed

apisprout.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ var (
3535
// ErrNoExample is sent when no example was found for an operation.
3636
ErrNoExample = errors.New("No example found")
3737

38+
// ErrRecursive is when a schema is impossible to represent because it infinitely recurses.
39+
ErrRecursive = errors.New("Recursive schema")
40+
3841
// ErrCannotMarshal is set when an example cannot be marshalled.
3942
ErrCannotMarshal = errors.New("Cannot marshal example")
4043

example.go

Lines changed: 82 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -92,26 +92,76 @@ func excludeFromMode(mode Mode, schema *openapi3.Schema) bool {
9292
return false
9393
}
9494

95-
// OpenAPIExample creates an example structure from an OpenAPI 3 schema
96-
// object, which is an extended subset of JSON Schema.
97-
// https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#schemaObject
98-
func OpenAPIExample(mode Mode, schema *openapi3.Schema) (interface{}, error) {
95+
// isRequired checks whether a key is actually required.
96+
func isRequired(schema *openapi3.Schema, key string) bool {
97+
for _, req := range schema.Required {
98+
if req == key {
99+
return true
100+
}
101+
}
102+
103+
return false
104+
}
105+
106+
type cachedSchema struct {
107+
pending bool
108+
out interface{}
109+
}
110+
111+
func openAPIExample(mode Mode, schema *openapi3.Schema, cache map[*openapi3.Schema]*cachedSchema) (out interface{}, err error) {
99112
if ex, ok := getSchemaExample(schema); ok {
100113
return ex, nil
101114
}
102115

116+
cached, ok := cache[schema]
117+
if !ok {
118+
cached = &cachedSchema{
119+
pending: true,
120+
}
121+
cache[schema] = cached
122+
} else if cached.pending {
123+
return nil, ErrRecursive
124+
} else {
125+
return cached.out, nil
126+
}
127+
128+
defer func() {
129+
cached.pending = false
130+
cached.out = out
131+
}()
132+
103133
// Handle combining keywords
104134
if len(schema.OneOf) > 0 {
105-
return OpenAPIExample(mode, schema.OneOf[0].Value)
135+
var ex interface{}
136+
var err error
137+
138+
for _, candidate := range schema.OneOf {
139+
ex, err = openAPIExample(mode, candidate.Value, cache)
140+
if err == nil {
141+
break
142+
}
143+
}
144+
145+
return ex, err
106146
}
107147
if len(schema.AnyOf) > 0 {
108-
return OpenAPIExample(mode, schema.AnyOf[0].Value)
148+
var ex interface{}
149+
var err error
150+
151+
for _, candidate := range schema.AnyOf {
152+
ex, err = openAPIExample(mode, candidate.Value, cache)
153+
if err == nil {
154+
break
155+
}
156+
}
157+
158+
return ex, err
109159
}
110160
if len(schema.AllOf) > 0 {
111161
example := map[string]interface{}{}
112162

113163
for _, allOf := range schema.AllOf {
114-
candidate, err := OpenAPIExample(mode, allOf.Value)
164+
candidate, err := openAPIExample(mode, allOf.Value, cache)
115165
if err != nil {
116166
return nil, err
117167
}
@@ -188,9 +238,9 @@ func OpenAPIExample(mode Mode, schema *openapi3.Schema) (interface{}, error) {
188238
example := []interface{}{}
189239

190240
if schema.Items != nil && schema.Items.Value != nil {
191-
ex, err := OpenAPIExample(mode, schema.Items.Value)
241+
ex, err := openAPIExample(mode, schema.Items.Value, cache)
192242
if err != nil {
193-
return nil, fmt.Errorf("can't get example for array item")
243+
return nil, fmt.Errorf("can't get example for array item: %+v", err)
194244
}
195245

196246
example = append(example, ex)
@@ -209,24 +259,30 @@ func OpenAPIExample(mode Mode, schema *openapi3.Schema) (interface{}, error) {
209259
continue
210260
}
211261

212-
ex, err := OpenAPIExample(mode, v.Value)
213-
if err != nil {
214-
return nil, fmt.Errorf("can't get example for '%s'", k)
262+
ex, err := openAPIExample(mode, v.Value, cache)
263+
if err == ErrRecursive {
264+
if isRequired(schema, k) {
265+
return nil, fmt.Errorf("can't get example for '%s': %+v", k, err)
266+
}
267+
} else if err != nil {
268+
return nil, fmt.Errorf("can't get example for '%s': %+v", k, err)
269+
} else {
270+
example[k] = ex
215271
}
216-
217-
example[k] = ex
218272
}
219273

220274
if schema.AdditionalProperties != nil && schema.AdditionalProperties.Value != nil {
221275
addl := schema.AdditionalProperties.Value
222276

223277
if !excludeFromMode(mode, addl) {
224-
ex, err := OpenAPIExample(mode, addl)
225-
if err != nil {
226-
return nil, fmt.Errorf("can't get example for additional properties")
278+
ex, err := openAPIExample(mode, addl, cache)
279+
if err == ErrRecursive {
280+
// We just won't add this if it's recursive.
281+
} else if err != nil {
282+
return nil, fmt.Errorf("can't get example for additional properties: %+v", err)
283+
} else {
284+
example["additionalPropertyName"] = ex
227285
}
228-
229-
example["additionalPropertyName"] = ex
230286
}
231287
}
232288

@@ -235,3 +291,10 @@ func OpenAPIExample(mode Mode, schema *openapi3.Schema) (interface{}, error) {
235291

236292
return nil, ErrNoExample
237293
}
294+
295+
// OpenAPIExample creates an example structure from an OpenAPI 3 schema
296+
// object, which is an extended subset of JSON Schema.
297+
// https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#schemaObject
298+
func OpenAPIExample(mode Mode, schema *openapi3.Schema) (interface{}, error) {
299+
return openAPIExample(mode, schema, make(map[*openapi3.Schema]*cachedSchema))
300+
}

example_test.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,28 @@ package main
22

33
import (
44
"encoding/json"
5+
"io/ioutil"
6+
"os"
7+
"path"
58
"strings"
69
"testing"
710

811
"github.com/getkin/kin-openapi/openapi3"
912
"github.com/stretchr/testify/assert"
13+
"github.com/stretchr/testify/require"
1014
)
1115

16+
func exampleFixture(t *testing.T, name string) string {
17+
f, err := os.Open(path.Join("testdata/example", name))
18+
require.NoError(t, err)
19+
defer f.Close()
20+
21+
b, err := ioutil.ReadAll(f)
22+
require.NoError(t, err)
23+
24+
return string(b)
25+
}
26+
1227
var schemaTests = []struct {
1328
name string
1429
in string
@@ -505,3 +520,57 @@ func TestGenExample(t *testing.T) {
505520
})
506521
}
507522
}
523+
524+
func TestRecursiveSchema(t *testing.T) {
525+
loader := openapi3.NewSwaggerLoader()
526+
527+
tests := []struct {
528+
name string
529+
in string
530+
schema string
531+
out string
532+
}{
533+
{
534+
"Valid recursive schema",
535+
exampleFixture(t, "recursive_ok.yml"),
536+
"Test",
537+
`{"something": "Hello"}`,
538+
},
539+
{
540+
"Infinitely recursive schema",
541+
exampleFixture(t, "recursive_infinite.yml"),
542+
"Test",
543+
``,
544+
},
545+
{
546+
"Seeing the same schema twice non-recursively",
547+
exampleFixture(t, "recursive_seen_twice.yml"),
548+
"Test",
549+
`{"ref_a": {"spud": "potato"}, "ref_b": {"spud": "potato"}}`,
550+
},
551+
{
552+
"Cyclical dependencies",
553+
exampleFixture(t, "recursive_cycles.yml"),
554+
"Front",
555+
``,
556+
},
557+
}
558+
for _, test := range tests {
559+
t.Run(test.name, func(t *testing.T) {
560+
swagger, err := loader.LoadSwaggerFromData([]byte(test.in))
561+
require.NoError(t, err)
562+
563+
ex, err := OpenAPIExample(ModeResponse, swagger.Components.Schemas[test.schema].Value)
564+
if test.out == "" {
565+
assert.Error(t, err)
566+
assert.Nil(t, ex)
567+
} else {
568+
assert.Nil(t, err)
569+
// Expected to match the output.
570+
var expected interface{}
571+
json.Unmarshal([]byte(test.out), &expected)
572+
assert.EqualValues(t, expected, ex)
573+
}
574+
})
575+
}
576+
}

testdata/example/recursive_cycles.yml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
components:
2+
schemas:
3+
Front:
4+
type: object
5+
required:
6+
- back
7+
properties:
8+
back:
9+
$ref: '#/components/schemas/Back'
10+
Back:
11+
type: object
12+
required:
13+
- front
14+
properties:
15+
front:
16+
$ref: '#/components/schemas/Front'
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
components:
2+
schemas:
3+
Test:
4+
type: object
5+
required:
6+
- test
7+
properties:
8+
test:
9+
$ref: '#/components/schemas/Test'

testdata/example/recursive_ok.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
components:
2+
schemas:
3+
Test:
4+
type: object
5+
properties:
6+
something:
7+
type: string
8+
example: Hello
9+
test:
10+
$ref: '#/components/schemas/Test'
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
components:
2+
schemas:
3+
Ref:
4+
type: object
5+
properties:
6+
spud:
7+
type: string
8+
example: "potato"
9+
Test:
10+
type: object
11+
required:
12+
- ref_a
13+
- ref_b
14+
properties:
15+
ref_a:
16+
$ref: '#/components/schemas/Ref'
17+
ref_b:
18+
$ref: '#/components/schemas/Ref'

0 commit comments

Comments
 (0)