Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

flatten command - prune oneOf field on circular references #466

Merged
merged 2 commits into from
Dec 26, 2023
Merged
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
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 @@ -1842,6 +1842,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
Loading