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

Added support for configurable levels of circular reference checking … #173

Merged
merged 8 commits into from
Sep 21, 2023
12 changes: 12 additions & 0 deletions datamodel/document_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,18 @@ type DocumentConfiguration struct {
// BypassDocumentCheck will bypass the document check. This is disabled by default. This will allow any document to
// passed in and used. Only enable this when parsing non openapi documents.
BypassDocumentCheck bool

// IgnorePolymorphicCircularReferences will skip over checking for circular references in polymorphic schemas.
// A polymorphic schema is any schema that is composed other schemas using references via `oneOf`, `anyOf` of `allOf`.
// This is disabled by default, which means polymorphic circular references will be checked.
IgnorePolymorphicCircularReferences bool

// IgnoreArrayCircularReferences will skip over checking for circular references in arrays. Sometimes a circular
// reference is required to describe a data-shape correctly. Often those shapes are valid circles if the
// type of the schema implementing the loop is an array. An empty array would technically break the loop.
// So if libopenapi is returning circular references for this use case, then this option should be enabled.
// this is disabled by default, which means array circular references will be checked.
IgnoreArrayCircularReferences bool
}

func NewOpenDocumentConfiguration() *DocumentConfiguration {
Expand Down
10 changes: 10 additions & 0 deletions datamodel/low/v3/create_document.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,16 @@ func createDocument(info *datamodel.SpecInfo, config *datamodel.DocumentConfigur

// create resolver and check for circular references.
resolve := resolver.NewResolver(idx)

// if configured, ignore circular references in arrays and polymorphic schemas
if config.IgnoreArrayCircularReferences {
resolve.IgnoreArrayCircularReferences()
}
if config.IgnorePolymorphicCircularReferences {
resolve.IgnorePolymorphicCircularReferences()
}

// check for circular references.
resolvingErrors := resolve.CheckForCircularReferences()

if len(resolvingErrors) > 0 {
Expand Down
58 changes: 58 additions & 0 deletions datamodel/low/v3/create_document_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,64 @@ func TestCircularReferenceError(t *testing.T) {
assert.Len(t, err, 3)
}

func TestCircularReference_IgnoreArray(t *testing.T) {

spec := `openapi: 3.1.0
components:
schemas:
ProductCategory:
type: "object"
properties:
name:
type: "string"
children:
type: "array"
items:
$ref: "#/components/schemas/ProductCategory"
description: "Array of sub-categories in the same format."
required:
- "name"
- "children"`

info, _ := datamodel.ExtractSpecInfo([]byte(spec))
circDoc, err := CreateDocumentFromConfig(info, &datamodel.DocumentConfiguration{
AllowFileReferences: false,
AllowRemoteReferences: false,
IgnoreArrayCircularReferences: true,
})
assert.NotNil(t, circDoc)
assert.Len(t, err, 0)
}

func TestCircularReference_IgnorePoly(t *testing.T) {

spec := `openapi: 3.1.0
components:
schemas:
ProductCategory:
type: "object"
properties:
name:
type: "string"
children:
type: "object"
anyOf:
- $ref: "#/components/schemas/ProductCategory"
description: "Array of sub-categories in the same format."
required:
- "name"
- "children"`

info, _ := datamodel.ExtractSpecInfo([]byte(spec))
circDoc, err := CreateDocumentFromConfig(info, &datamodel.DocumentConfiguration{
AllowFileReferences: false,
AllowRemoteReferences: false,
IgnorePolymorphicCircularReferences: true,
})
assert.NotNil(t, circDoc)
assert.Len(t, err, 0)
}

func BenchmarkCreateDocument_Stripe(b *testing.B) {
data, _ := os.ReadFile("../../../test_specs/stripe.yaml")
info, _ := datamodel.ExtractSpecInfo(data)
Expand Down
68 changes: 68 additions & 0 deletions document_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -790,3 +790,71 @@ paths:

assert.Equal(t, spec, strings.TrimSpace(string(rend)))
}

func TestDocument_IgnorePolymorphicCircularReferences(t *testing.T) {

var d = `openapi: 3.1.0
components:
schemas:
ProductCategory:
type: "object"
properties:
name:
type: "string"
children:
type: "object"
anyOf:
- $ref: "#/components/schemas/ProductCategory"
description: "Array of sub-categories in the same format."
required:
- "name"
- "children"`

config := datamodel.NewClosedDocumentConfiguration()
config.IgnorePolymorphicCircularReferences = true

doc, err := NewDocumentWithConfiguration([]byte(d), config)
if err != nil {
panic(err)
}

m, errs := doc.BuildV3Model()

assert.Len(t, errs, 0)
assert.Len(t, m.Index.GetCircularReferences(), 0)

}

func TestDocument_IgnoreArrayCircularReferences(t *testing.T) {

var d = `openapi: 3.1.0
components:
schemas:
ProductCategory:
type: "object"
properties:
name:
type: "string"
children:
type: "array"
items:
$ref: "#/components/schemas/ProductCategory"
description: "Array of sub-categories in the same format."
required:
- "name"
- "children"`

config := datamodel.NewClosedDocumentConfiguration()
config.IgnoreArrayCircularReferences = true

doc, err := NewDocumentWithConfiguration([]byte(d), config)
if err != nil {
panic(err)
}

m, errs := doc.BuildV3Model()

assert.Len(t, errs, 0)
assert.Len(t, m.Index.GetCircularReferences(), 0)

}
6 changes: 4 additions & 2 deletions index/circular_reference_result.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ type CircularReferenceResult struct {
Start *Reference
LoopIndex int
LoopPoint *Reference
IsPolymorphicResult bool // if this result comes from a polymorphic loop.
IsInfiniteLoop bool // if all the definitions in the reference loop are marked as required, this is an infinite circular reference, thus is not allowed.
IsArrayResult bool // if this result comes from an array loop.
PolymorphicType string // which type of polymorphic loop is this? (oneOf, anyOf, allOf)
IsPolymorphicResult bool // if this result comes from a polymorphic loop.
IsInfiniteLoop bool // if all the definitions in the reference loop are marked as required, this is an infinite circular reference, thus is not allowed.
}

func (c *CircularReferenceResult) GenerateJourneyPath() string {
Expand Down
1 change: 1 addition & 0 deletions index/index_model.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ type Reference struct {
Name string
Node *yaml.Node
ParentNode *yaml.Node
ParentNodeSchemaType string // used to determine if the parent node is an array or not.
Resolved bool
Circular bool
Seen bool
Expand Down
71 changes: 64 additions & 7 deletions resolver/resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ type Resolver struct {
indexesVisited int
journeysTaken int
relativesSeen int
ignorePoly bool
ignoreArray bool
}

// NewResolver will create a new resolver from a *index.SpecIndex
Expand Down Expand Up @@ -92,10 +94,21 @@ func (resolver *Resolver) GetNonPolymorphicCircularErrors() []*index.CircularRef
res = append(res, resolver.circularReferences[i])
}
}

return res
}

// IgnorePolymorphicCircularReferences will ignore any circular references that are polymorphic (oneOf, anyOf, allOf)
// This must be set before any resolving is done.
func (resolver *Resolver) IgnorePolymorphicCircularReferences() {
resolver.ignorePoly = true
}

// IgnoreArrayCircularReferences will ignore any circular references that stem from arrays. This must be set before
// any resolving is done.
func (resolver *Resolver) IgnoreArrayCircularReferences() {
resolver.ignoreArray = true
}

// GetJourneysTaken returns the number of journeys taken by the resolver
func (resolver *Resolver) GetJourneysTaken() int {
return resolver.journeysTaken
Expand Down Expand Up @@ -231,7 +244,7 @@ func (resolver *Resolver) VisitReference(ref *index.Reference, seen map[string]b
}

journey = append(journey, ref)
relatives := resolver.extractRelatives(ref.Node, seen, journey, resolve)
relatives := resolver.extractRelatives(ref.Node, nil, seen, journey, resolve)

seen = make(map[string]bool)

Expand All @@ -254,11 +267,17 @@ func (resolver *Resolver) VisitReference(ref *index.Reference, seen map[string]b

visitedDefinitions := map[string]bool{}
isInfiniteLoop, _ := resolver.isInfiniteCircularDependency(foundDup, visitedDefinitions, nil)

isArray := false
if r.ParentNodeSchemaType == "array" {
isArray = true
}
circRef = &index.CircularReferenceResult{
Journey: loop,
Start: foundDup,
LoopIndex: i,
LoopPoint: foundDup,
IsArrayResult: isArray,
IsInfiniteLoop: isInfiniteLoop,
}
resolver.circularReferences = append(resolver.circularReferences, circRef)
Expand Down Expand Up @@ -321,7 +340,7 @@ func (resolver *Resolver) isInfiniteCircularDependency(ref *index.Reference, vis
return false, visitedDefinitions
}

func (resolver *Resolver) extractRelatives(node *yaml.Node,
func (resolver *Resolver) extractRelatives(node, parent *yaml.Node,
foundRelatives map[string]bool,
journey []*index.Reference, resolve bool) []*index.Reference {

Expand All @@ -333,7 +352,30 @@ func (resolver *Resolver) extractRelatives(node *yaml.Node,
if len(node.Content) > 0 {
for i, n := range node.Content {
if utils.IsNodeMap(n) || utils.IsNodeArray(n) {
found = append(found, resolver.extractRelatives(n, foundRelatives, journey, resolve)...)

var anyvn, allvn, onevn, arrayTypevn *yaml.Node

// extract polymorphic references
if len(n.Content) > 1 {
_, anyvn = utils.FindKeyNodeTop("anyOf", n.Content)
_, allvn = utils.FindKeyNodeTop("allOf", n.Content)
_, onevn = utils.FindKeyNodeTop("oneOf", n.Content)
_, arrayTypevn = utils.FindKeyNodeTop("type", n.Content)
}
if anyvn != nil || allvn != nil || onevn != nil {
if resolver.ignorePoly {
continue
}
}
if arrayTypevn != nil {
if arrayTypevn.Value == "array" {
if resolver.ignoreArray {
continue
}
}
}

found = append(found, resolver.extractRelatives(n, node, foundRelatives, journey, resolve)...)
}

if i%2 == 0 && n.Value == "$ref" {
Expand All @@ -357,10 +399,22 @@ func (resolver *Resolver) extractRelatives(node *yaml.Node,
continue
}

schemaType := ""
if parent != nil {
_, arrayTypevn := utils.FindKeyNodeTop("type", parent.Content)
if arrayTypevn != nil {
if arrayTypevn.Value == "array" {
schemaType = "array"
}
}
}

r := &index.Reference{
Definition: value,
Name: value,
Node: node,
Definition: value,
Name: value,
Node: node,
ParentNode: parent,
ParentNodeSchemaType: schemaType,
}

found = append(found, r)
Expand Down Expand Up @@ -398,6 +452,7 @@ func (resolver *Resolver) extractRelatives(node *yaml.Node,
Start: ref,
LoopIndex: i,
LoopPoint: ref,
PolymorphicType: n.Value,
IsPolymorphicResult: true,
}

Expand Down Expand Up @@ -435,6 +490,7 @@ func (resolver *Resolver) extractRelatives(node *yaml.Node,
Start: ref,
LoopIndex: i,
LoopPoint: ref,
PolymorphicType: n.Value,
IsPolymorphicResult: true,
}

Expand All @@ -449,6 +505,7 @@ func (resolver *Resolver) extractRelatives(node *yaml.Node,
}
break
}

}
}
}
Expand Down
Loading