Skip to content

Commit

Permalink
prune oneOf field on circular references
Browse files Browse the repository at this point in the history
  • Loading branch information
tcdsv committed Dec 24, 2023
1 parent 593bd07 commit 2d209d9
Show file tree
Hide file tree
Showing 3 changed files with 147 additions and 11 deletions.
62 changes: 51 additions & 11 deletions flatten/merge_allof.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,33 +90,73 @@ func Merge(schema openapi3.SchemaRef) (*openapi3.Schema, error) {
if err != nil {
return nil, err
}
pruneFields(schema)
}

return result.Value, nil
}

// remove fields while maintaining an equivalent schema.
func pruneFields(schema *openapi3.SchemaRef) {
if len(schema.Value.OneOf) == 1 && schema.Value.OneOf[0].Value == schema.Value {
schema.Value.OneOf = nil
}
if len(schema.Value.AnyOf) == 1 && schema.Value.AnyOf[0].Value == schema.Value {
schema.Value.AnyOf = nil
}
}

func mergeCircularAllOf(state *state, baseSchemaRef *openapi3.SchemaRef) error {
allOfCopy := make(openapi3.SchemaRefs, len(baseSchemaRef.Value.AllOf))
copy(allOfCopy, baseSchemaRef.Value.AllOf)

schemaRefs := openapi3.SchemaRefs{baseSchemaRef}
schemaRefs = append(schemaRefs, baseSchemaRef.Value.AllOf...)
err := flattenSchemas(state, baseSchemaRef, schemaRefs)
if err != nil {
return err
}
baseSchemaRef.Value.AllOf = nil
pruneOneOf(state, baseSchemaRef, allOfCopy)
pruneAnyOf(baseSchemaRef)
return nil
}

func pruneAnyOf(schema *openapi3.SchemaRef) {
if len(schema.Value.AnyOf) == 1 && schema.Value.AnyOf[0].Value == schema.Value {
schema.Value.AnyOf = nil
}
}

func pruneOneOf(state *state, merged *openapi3.SchemaRef, allOf openapi3.SchemaRefs) {
if len(merged.Value.OneOf) == 1 && merged.Value.OneOf[0].Value == merged.Value {
merged.Value.OneOf = nil
return
}

for _, allOfSchema := range allOf {
isCircular := state.refs[allOfSchema.Ref]
if !isCircular {
continue
}

// check if merged is a child of allOfSchemna
isChild := false
for _, of := range allOfSchema.Value.OneOf {
if of.Value == merged.Value {
isChild = true
}
}

if !isChild {
continue
}

// check if oneOf field of allOfSchema matches the oneOf field of merged
mismatchFound := false
for i, of := range allOfSchema.Value.OneOf {
if of.Value != merged.Value.OneOf[i].Value {
mismatchFound = true
break
}
}

if !mismatchFound {
merged.Value.OneOf = nil
break
}
}
}

// Merge replaces objects under AllOf with a flattened equivalent
func mergeInternal(state *state, base *openapi3.SchemaRef) (*openapi3.SchemaRef, error) {
if base == nil {
Expand Down
20 changes: 20 additions & 0 deletions flatten/merge_allof_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1842,6 +1842,26 @@ func TestMerge_AnyOfIsNotPruned(t *testing.T) {
require.NotEmpty(t, merged.AnyOf)
}

func TestMerge_ComplexOneOfIsPruned(t *testing.T) {
doc := loadSpec(t, "testdata/prune-oneof.yaml")
merged, err := flatten.Merge(*doc.Components.Schemas["SchemaWithWithoutOneOf"])
require.NoError(t, err)
require.Empty(t, merged.OneOf)
}

func TestMerge_ComplexOneOfIsNotPruned(t *testing.T) {
doc := loadSpec(t, "testdata/prune-oneof.yaml")
merged, err := flatten.Merge(*doc.Components.Schemas["ThirdSchema"])
require.NoError(t, err)
require.NotEmpty(t, merged.OneOf)
require.Len(t, merged.OneOf, 2)

merged, err = flatten.Merge(*doc.Components.Schemas["ComplexSchema"])
require.NoError(t, err)
require.NotEmpty(t, merged.OneOf)
require.Len(t, merged.OneOf, 2)
}

func loadSpec(t *testing.T, path string) *openapi3.T {
ctx := context.Background()
sl := openapi3.NewLoader()
Expand Down
76 changes: 76 additions & 0 deletions flatten/testdata/prune-oneof.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
openapi: 3.0.0
info:
title: Sample API
version: 1.0.0
paths: {}

components:
schemas:
# BaseSchema is the parent of SchemaWithWithoutOneOf
# the flattened version of SchemaWithWithoutOneOf does not contain oneOf field
BaseSchema:
type: object
oneOf:
- $ref: '#/components/schemas/SchemaWithWithoutOneOf'
- type: object
properties:
inlineProperty:
type: string

SchemaWithWithoutOneOf:
allOf:
- $ref: '#/components/schemas/BaseSchema'
- type: object
properties:
additionalProperty:
type: string

# FirstSchema is not a parent of ThirdSchema
# the flattened version of ThirdSchema contains oneOf
FirstSchema:
type: object
oneOf:
- $ref: '#/components/schemas/SecondSchema'
- type: object
properties:
prop1:
type: string

SecondSchema:
type: object
allOf:
- $ref: '#/components/schemas/ThirdSchema'

ThirdSchema:
type: object
allOf:
- $ref: '#/components/schemas/FirstSchema'
- type: object
properties:
thirdProperty:
type: string

# Base is a parent of ComplexSchema
# the flattened version of ComplexSchema contains the oneOf of NestedSchema
Base:
type: object
allOf:
- $ref: '#/components/schemas/ComplexSchema'

ComplexSchema:
type: object
allOf:
- $ref: '#/components/schemas/Base'
- $ref: '#/components/schemas/NestedSchema'

NestedSchema:
type: object
oneOf:
- type: object
properties:
nestedProperty:
type: string
- type: object
properties:
anotherNestedProperty:
type: number

0 comments on commit 2d209d9

Please sign in to comment.