Skip to content

Commit

Permalink
flatten command - prune oneOf field on circular references (#466)
Browse files Browse the repository at this point in the history
  • Loading branch information
tcdsv authored Dec 26, 2023
1 parent 7e651c5 commit d7c8034
Show file tree
Hide file tree
Showing 3 changed files with 195 additions and 11 deletions.
80 changes: 69 additions & 11 deletions flatten/merge_allof.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,33 +90,90 @@ 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
}
}

// pruneCircularOneOfInHierarchy prunes the 'oneOf' field from a merged schema when specific conditions are met.
// Pruning criteria:
// - The unmerged schema is a child of another parent schema, through the oneOf field.
// - The unmerged schema contains an 'allOf' field with a circular reference to the parent schema.
// - The merged parent and the merged child schemas contain an identical oneOf field.
// - The merged parent schema contains a non-empty propertyName discriminator field.
func pruneCircularOneOfInHierarchy(state *state, merged *openapi3.SchemaRef, allOf openapi3.SchemaRefs) {
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
}

if allOfSchema.Value.Discriminator == nil || allOfSchema.Value.Discriminator.PropertyName == "" {
continue
}

if len(allOfSchema.Value.OneOf) != len(merged.Value.OneOf) {
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
}
}
}

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
}
pruneCircularOneOfInHierarchy(state, merged, allOf)
}

// Merge replaces objects under AllOf with a flattened equivalent
func mergeInternal(state *state, base *openapi3.SchemaRef) (*openapi3.SchemaRef, error) {
if base == nil {
Expand Down Expand Up @@ -151,6 +208,7 @@ func mergeInternal(state *state, base *openapi3.SchemaRef) (*openapi3.SchemaRef,
result.Value.MultipleOf = base.Value.MultipleOf
result.Value.MinLength = base.Value.MinLength
result.Value.Default = base.Value.Default
result.Value.Discriminator = base.Value.Discriminator
if base.Value.MaxLength != nil {
result.Value.MaxLength = openapi3.Uint64Ptr(*base.Value.MaxLength)
}
Expand Down
25 changes: 25 additions & 0 deletions flatten/merge_allof_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1884,6 +1884,31 @@ 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)

merged, err = flatten.Merge(*doc.Components.Schemas["SchemaWithOneOf"])
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
101 changes: 101 additions & 0 deletions flatten/testdata/prune-oneof.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
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
discriminator:
propertyName: test

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

# BaseSchema is the parent of SchemaWithWithOneOf
# the flattened version of SchemaWithWithoutOneOf contains oneOf field, because BaseSchema does not have the discriminator field
BaseSchemaNoDiscriminator:
type: object
oneOf:
- $ref: '#/components/schemas/SchemaWithOneOf'
- type: object
properties:
inlineProperty:
type: string

SchemaWithOneOf:
allOf:
- $ref: '#/components/schemas/BaseSchemaNoDiscriminator'
- 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
discriminator:
propertyName: test

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'
discriminator:
propertyName: test

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 d7c8034

Please sign in to comment.