diff --git a/datamodel/low/base/base.go b/datamodel/low/base/base.go index 0bfacf2..1fa6287 100644 --- a/datamodel/low/base/base.go +++ b/datamodel/low/base/base.go @@ -9,3 +9,4 @@ // beats, particularly when polymorphism is used. By re-using the same superset Schema across versions, we can ensure // that all the latest features are collected, without damaging backwards compatibility. package base + diff --git a/datamodel/low/base/discriminator.go b/datamodel/low/base/discriminator.go index beb5518..0a4bc4d 100644 --- a/datamodel/low/base/discriminator.go +++ b/datamodel/low/base/discriminator.go @@ -4,7 +4,10 @@ package base import ( + "crypto/sha256" "github.com/pb33f/libopenapi/datamodel/low" + "sort" + "strings" ) // Discriminator is only used by OpenAPI 3+ documents, it represents a polymorphic discriminator used for schemas @@ -29,3 +32,21 @@ func (d *Discriminator) FindMappingValue(key string) *low.ValueReference[string] } return nil } + +// Hash will return a consistent SHA256 Hash of the Discriminator object +func (d *Discriminator) Hash() [32]byte { + + // calculate a hash from every property. + f := []string{d.PropertyName.Value} + + propertyKeys := make([]string, 0, len(d.Mapping)) + for i := range d.Mapping { + propertyKeys = append(propertyKeys, i.Value) + } + sort.Strings(propertyKeys) + for k := range propertyKeys { + prop := d.FindMappingValue(propertyKeys[k]) + f = append(f, prop.Value) + } + return sha256.Sum256([]byte(strings.Join(f, "|"))) +} diff --git a/datamodel/low/base/external_doc.go b/datamodel/low/base/external_doc.go index 828fe92..129e2c8 100644 --- a/datamodel/low/base/external_doc.go +++ b/datamodel/low/base/external_doc.go @@ -4,9 +4,11 @@ package base import ( + "crypto/sha256" "github.com/pb33f/libopenapi/datamodel/low" "github.com/pb33f/libopenapi/index" "gopkg.in/yaml.v3" + "strings" ) // ExternalDoc represents a low-level External Documentation object as defined by OpenAPI 2 and 3 @@ -38,3 +40,12 @@ func (ex *ExternalDoc) GetExtensions() map[low.KeyReference[string]]low.ValueRef } return ex.Extensions } + +func (ex *ExternalDoc) Hash() [32]byte { + // calculate a hash from every property. + d := []string{ + ex.Description.Value, + ex.URL.Value, + } + return sha256.Sum256([]byte(strings.Join(d, "|"))) +} diff --git a/datamodel/low/base/schema.go b/datamodel/low/base/schema.go index fae472b..31c82aa 100644 --- a/datamodel/low/base/schema.go +++ b/datamodel/low/base/schema.go @@ -1,12 +1,15 @@ package base import ( + "crypto/sha256" "fmt" "github.com/pb33f/libopenapi/datamodel/low" "github.com/pb33f/libopenapi/index" "github.com/pb33f/libopenapi/utils" "gopkg.in/yaml.v3" + "sort" "strconv" + "strings" ) // SchemaDynamicValue is used to hold multiple possible values for a schema property. There are two values, a left @@ -102,6 +105,162 @@ type Schema struct { Extensions map[low.KeyReference[string]]low.ValueReference[any] } +// Hash will calculate a SHA256 hash from the values of the schema, This allows equality checking against +// Schemas defined inside an OpenAPI document. The only way to know if a schema has changed, is to hash it. +// Polymorphic items +func (s *Schema) Hash() [32]byte { + // calculate a hash from every property in the schema. + v := "%v" + d := []string{ + s.SchemaTypeRef.Value, + fmt.Sprintf(v, s.ExclusiveMaximum.Value), + fmt.Sprintf(v, s.ExclusiveMinimum.Value), + fmt.Sprintf(v, s.Type.Value), + fmt.Sprintf(v, s.Title.Value), + fmt.Sprintf(v, s.MultipleOf.Value), + fmt.Sprintf(v, s.Maximum.Value), + fmt.Sprintf(v, s.Minimum.Value), + fmt.Sprintf(v, s.MaxLength.Value), + fmt.Sprintf(v, s.MinLength.Value), + s.Pattern.Value, + s.Format.Value, + fmt.Sprintf(v, s.MaxItems.Value), + fmt.Sprintf(v, s.UniqueItems.Value), + fmt.Sprintf(v, s.MaxProperties.Value), + fmt.Sprintf(v, s.MinProperties.Value), + fmt.Sprintf(v, s.AdditionalProperties.Value), + s.Description.Value, + s.ContentEncoding.Value, + s.ContentMediaType.Value, + fmt.Sprintf(v, s.Default.Value), + fmt.Sprintf(v, s.Nullable.Value), + fmt.Sprintf(v, s.ReadOnly.Value), + fmt.Sprintf(v, s.WriteOnly.Value), + fmt.Sprintf(v, s.Deprecated.Value), + } + + for i := range s.Required.Value { + d = append(d, s.Required.Value[i].Value) + } + for i := range s.Enum.Value { + d = append(d, s.Enum.Value[i].Value) + } + propertyKeys := make([]string, 0, len(s.Properties.Value)) + for i := range s.Properties.Value { + propertyKeys = append(propertyKeys, i.Value) + } + sort.Strings(propertyKeys) + for k := range propertyKeys { + prop := s.FindProperty(propertyKeys[k]).Value + if !prop.IsSchemaReference() { + d = append(d, fmt.Sprintf("%x", prop.Schema().Hash())) + } + } + if s.XML.Value != nil { + d = append(d, fmt.Sprintf(v, s.XML.Value.Hash())) + } + if s.ExternalDocs.Value != nil { + d = append(d, fmt.Sprintf(v, s.ExternalDocs.Value.Hash())) + } + if s.Discriminator.Value != nil { + d = append(d, fmt.Sprintf(v, s.Discriminator.Value.Hash())) + } + + x := "%x" + + // hash polymorphic data + if len(s.OneOf.Value) > 0 { + oneOfKeys := make([]string, 0, len(s.OneOf.Value)) + oneOfEntities := make(map[string]*Schema) + for i := range s.OneOf.Value { + g := s.OneOf.Value[i].Value + if !g.IsSchemaReference() { + k := g.Schema() + r := fmt.Sprintf(x, k.Hash()) + oneOfEntities[r] = k + oneOfKeys = append(oneOfKeys, r) + } + } + sort.Strings(oneOfKeys) + for k := range oneOfKeys { + d = append(d, fmt.Sprintf(x, oneOfEntities[oneOfKeys[k]].Hash())) + } + } + + if len(s.AllOf.Value) > 0 { + allOfKeys := make([]string, 0, len(s.AllOf.Value)) + allOfEntities := make(map[string]*Schema) + for i := range s.AllOf.Value { + g := s.AllOf.Value[i].Value + if !g.IsSchemaReference() { + k := g.Schema() + r := fmt.Sprintf(x, k.Hash()) + allOfEntities[r] = k + allOfKeys = append(allOfKeys, r) + } + } + sort.Strings(allOfKeys) + for k := range allOfKeys { + d = append(d, fmt.Sprintf(x, allOfEntities[allOfKeys[k]].Hash())) + } + } + + if len(s.AnyOf.Value) > 0 { + anyOfKeys := make([]string, 0, len(s.AnyOf.Value)) + anyOfEntities := make(map[string]*Schema) + for i := range s.AnyOf.Value { + g := s.AnyOf.Value[i].Value + if !g.IsSchemaReference() { + k := g.Schema() + r := fmt.Sprintf(x, k.Hash()) + anyOfEntities[r] = k + anyOfKeys = append(anyOfKeys, r) + } + } + sort.Strings(anyOfKeys) + for k := range anyOfKeys { + d = append(d, fmt.Sprintf(x, anyOfEntities[anyOfKeys[k]].Hash())) + } + } + + if len(s.Not.Value) > 0 { + notKeys := make([]string, 0, len(s.Not.Value)) + notEntities := make(map[string]*Schema) + for i := range s.Not.Value { + g := s.Not.Value[i].Value + if !g.IsSchemaReference() { + k := g.Schema() + r := fmt.Sprintf(x, k.Hash()) + notEntities[r] = k + notKeys = append(notKeys, r) + } + } + sort.Strings(notKeys) + for k := range notKeys { + d = append(d, fmt.Sprintf(x, notEntities[notKeys[k]].Hash())) + } + } + + if len(s.Items.Value) > 0 { + itemsKeys := make([]string, 0, len(s.Items.Value)) + itemsEntities := make(map[string]*Schema) + for i := range s.Items.Value { + g := s.Items.Value[i].Value + if !g.IsSchemaReference() { + k := g.Schema() + r := fmt.Sprintf(x, k.Hash()) + itemsEntities[r] = k + itemsKeys = append(itemsKeys, r) + } + } + sort.Strings(itemsKeys) + for k := range itemsKeys { + d = append(d, fmt.Sprintf(x, itemsEntities[itemsKeys[k]].Hash())) + } + } + return sha256.Sum256([]byte(strings.Join(d, "|"))) +} + // FindProperty will return a ValueReference pointer containing a SchemaProxy pointer // from a property key name. if found func (s *Schema) FindProperty(name string) *low.ValueReference[*SchemaProxy] { @@ -465,7 +624,8 @@ func buildSchema(schemas chan schemaProxyBuildResult, labelNode, valueNode *yaml syncChan := make(chan *low.ValueReference[*SchemaProxy]) // build out a SchemaProxy for every sub-schema. - build := func(kn *yaml.Node, vn *yaml.Node, c chan *low.ValueReference[*SchemaProxy]) { + build := func(kn *yaml.Node, vn *yaml.Node, c chan *low.ValueReference[*SchemaProxy], + isRef bool, refLocation string) { // a proxy design works best here. polymorphism, pretty much guarantees that a sub-schema can // take on circular references through polymorphism. Like the resolver, if we try and follow these // journey's through hyperspace, we will end up creating endless amounts of threads, spinning off @@ -476,7 +636,10 @@ func buildSchema(schemas chan schemaProxyBuildResult, labelNode, valueNode *yaml sp.kn = kn sp.vn = vn sp.idx = idx - + if isRef { + sp.referenceLookup = refLocation + sp.isReference = true + } res := &low.ValueReference[*SchemaProxy]{ Value: sp, ValueNode: vn, @@ -484,8 +647,12 @@ func buildSchema(schemas chan schemaProxyBuildResult, labelNode, valueNode *yaml c <- res } + isRef := false + refLocation := "" if utils.IsNodeMap(valueNode) { - if h, _, _ := utils.IsNodeRefValue(valueNode); h { + h := false + if h, _, refLocation = utils.IsNodeRefValue(valueNode); h { + isRef = true ref, _ := low.LocateRefNode(valueNode, idx) if ref != nil { valueNode = ref @@ -497,7 +664,7 @@ func buildSchema(schemas chan schemaProxyBuildResult, labelNode, valueNode *yaml // this only runs once, however to keep things consistent, it makes sense to use the same async method // that arrays will use. - go build(labelNode, valueNode, syncChan) + go build(labelNode, valueNode, syncChan, isRef, refLocation) select { case r := <-syncChan: schemas <- schemaProxyBuildResult{ @@ -512,7 +679,10 @@ func buildSchema(schemas chan schemaProxyBuildResult, labelNode, valueNode *yaml if utils.IsNodeArray(valueNode) { refBuilds := 0 for _, vn := range valueNode.Content { - if h, _, _ := utils.IsNodeRefValue(vn); h { + isRef = false + h := false + if h, _, refLocation = utils.IsNodeRefValue(vn); h { + isRef = true ref, _ := low.LocateRefNode(vn, idx) if ref != nil { vn = ref @@ -524,7 +694,7 @@ func buildSchema(schemas chan schemaProxyBuildResult, labelNode, valueNode *yaml } } refBuilds++ - go build(vn, vn, syncChan) + go build(vn, vn, syncChan, isRef, refLocation) } completedBuilds := 0 for completedBuilds < refBuilds { @@ -551,8 +721,12 @@ func buildSchema(schemas chan schemaProxyBuildResult, labelNode, valueNode *yaml func ExtractSchema(root *yaml.Node, idx *index.SpecIndex) (*low.NodeReference[*SchemaProxy], error) { var schLabel, schNode *yaml.Node errStr := "schema build failed: reference '%s' cannot be found at line %d, col %d" + + isRef := false + refLocation := "" if rf, rl, _ := utils.IsNodeRefValue(root); rf { // locate reference in index. + isRef = true ref, _ := low.LocateRefNode(root, idx) if ref != nil { schNode = ref @@ -564,7 +738,9 @@ func ExtractSchema(root *yaml.Node, idx *index.SpecIndex) (*low.NodeReference[*S } else { _, schLabel, schNode = utils.FindKeyNodeFull(SchemaLabel, root.Content) if schNode != nil { - if h, _, _ := utils.IsNodeRefValue(schNode); h { + h := false + if h, _, refLocation = utils.IsNodeRefValue(schNode); h { + isRef = true ref, _ := low.LocateRefNode(schNode, idx) if ref != nil { schNode = ref @@ -578,7 +754,7 @@ func ExtractSchema(root *yaml.Node, idx *index.SpecIndex) (*low.NodeReference[*S if schNode != nil { // check if schema has already been built. - schema := &SchemaProxy{kn: schLabel, vn: schNode, idx: idx} + schema := &SchemaProxy{kn: schLabel, vn: schNode, idx: idx, isReference: isRef, referenceLookup: refLocation} return &low.NodeReference[*SchemaProxy]{Value: schema, KeyNode: schLabel, ValueNode: schNode}, nil } return nil, nil diff --git a/datamodel/low/base/schema_proxy.go b/datamodel/low/base/schema_proxy.go index c6be610..2f47fd7 100644 --- a/datamodel/low/base/schema_proxy.go +++ b/datamodel/low/base/schema_proxy.go @@ -42,11 +42,13 @@ import ( // it's not actually JSONSchema until 3.1, so lots of times a bad schema will break parsing. Errors are only found // when a schema is needed, so the rest of the document is parsed and ready to use. type SchemaProxy struct { - kn *yaml.Node - vn *yaml.Node - idx *index.SpecIndex - rendered *Schema - buildError error + kn *yaml.Node + vn *yaml.Node + idx *index.SpecIndex + rendered *Schema + buildError error + isReference bool // Is the schema underneath originally a $ref? + referenceLookup string // If the schema is a $ref, what's its name? } // Build will prepare the SchemaProxy for rendering, it does not build the Schema, only sets up internal state. @@ -87,3 +89,18 @@ func (sp *SchemaProxy) Schema() *Schema { func (sp *SchemaProxy) GetBuildError() error { return sp.buildError } + +// IsSchemaReference returns true if the Schema that this SchemaProxy represents, is actually a reference to +// a Schema contained within Components or Definitions. There is no difference in the mechanism used to resolve the +// Schema when calling Schema(), however if we want to know if this schema was originally a reference, we won't +// be able to determine that from the model, without this bit. +func (sp *SchemaProxy) IsSchemaReference() bool { + return sp.isReference +} + +// GetSchemaReference will return the lookup defined by the $ref that this schema points to. If the schema +// is inline, and not a reference, then this method returns an empty string. Only useful when combined with +// IsSchemaReference() +func (sp *SchemaProxy) GetSchemaReference() string { + return sp.referenceLookup +} diff --git a/datamodel/low/base/schema_test.go b/datamodel/low/base/schema_test.go index 6b3f6d4..d13da1b 100644 --- a/datamodel/low/base/schema_test.go +++ b/datamodel/low/base/schema_test.go @@ -1208,3 +1208,100 @@ func TestExtractSchema_OneOfRef(t *testing.T) { res.Value.Schema().OneOf.Value[0].Value.Schema().Description.Value) } + +func TestSchema_Hash_Equal(t *testing.T) { + + left := `schema: + title: an OK message + properties: + propA: + title: a proxy property + type: string` + + right := `schema: + title: an OK message + properties: + propA: + title: a proxy property + type: string` + + var lNode, rNode yaml.Node + _ = yaml.Unmarshal([]byte(left), &lNode) + _ = yaml.Unmarshal([]byte(right), &rNode) + + lDoc, _ := ExtractSchema(lNode.Content[0], nil) + rDoc, _ := ExtractSchema(rNode.Content[0], nil) + + assert.NotNil(t, lDoc) + assert.NotNil(t, rDoc) + + lHash := lDoc.Value.Schema().Hash() + rHash := rDoc.Value.Schema().Hash() + + assert.Equal(t, lHash, rHash) + +} + +func TestSchema_Hash_NotEqual(t *testing.T) { + + left := `schema: + title: an OK message - but different + properties: + propA: + title: a proxy property + type: string` + + right := `schema: + title: an OK message + properties: + propA: + title: a proxy property + type: string` + + var lNode, rNode yaml.Node + _ = yaml.Unmarshal([]byte(left), &lNode) + _ = yaml.Unmarshal([]byte(right), &rNode) + + lDoc, _ := ExtractSchema(lNode.Content[0], nil) + rDoc, _ := ExtractSchema(rNode.Content[0], nil) + + assert.False(t, low.AreEqual(lDoc.Value.Schema(), rDoc.Value.Schema())) +} + +func TestSchema_Hash_EqualJumbled(t *testing.T) { + + left := `schema: + title: an OK message + description: a nice thing. + properties: + propZ: + type: int + propK: + description: a prop! + type: bool + propA: + title: a proxy property + type: string` + + right := `schema: + description: a nice thing. + properties: + propA: + type: string + title: a proxy property + propK: + type: bool + description: a prop! + propZ: + type: int + title: an OK message` + + var lNode, rNode yaml.Node + _ = yaml.Unmarshal([]byte(left), &lNode) + _ = yaml.Unmarshal([]byte(right), &rNode) + + lDoc, _ := ExtractSchema(lNode.Content[0], nil) + rDoc, _ := ExtractSchema(rNode.Content[0], nil) + assert.True(t, low.AreEqual(lDoc.Value.Schema(), rDoc.Value.Schema())) + +} diff --git a/datamodel/low/base/xml.go b/datamodel/low/base/xml.go index d66d6a4..ad9cb7e 100644 --- a/datamodel/low/base/xml.go +++ b/datamodel/low/base/xml.go @@ -1,9 +1,12 @@ package base import ( + "crypto/sha256" + "fmt" "github.com/pb33f/libopenapi/datamodel/low" "github.com/pb33f/libopenapi/index" "gopkg.in/yaml.v3" + "strings" ) // XML represents a low-level representation of an XML object defined by all versions of OpenAPI. @@ -32,3 +35,16 @@ func (x *XML) Build(root *yaml.Node, _ *index.SpecIndex) error { func (x *XML) GetExtensions() map[low.KeyReference[string]]low.ValueReference[any] { return x.Extensions } + +// Hash generates a SHA256 hash of the XML object using properties +func (x *XML) Hash() [32]byte { + // calculate a hash from every property. + d := []string{ + x.Name.Value, + x.Namespace.Value, + x.Prefix.Value, + fmt.Sprintf("%v", x.Attribute.Value), + fmt.Sprintf("%v", x.Wrapped.Value), + } + return sha256.Sum256([]byte(strings.Join(d, "|"))) +} diff --git a/datamodel/low/extraction_functions.go b/datamodel/low/extraction_functions.go index 0197422..a6abc37 100644 --- a/datamodel/low/extraction_functions.go +++ b/datamodel/low/extraction_functions.go @@ -544,3 +544,8 @@ func ExtractExtensions(root *yaml.Node) map[KeyReference[string]]ValueReference[ } return extensionMap } + +// AreEqual returns true if two Hashable objects are equal or not. +func AreEqual(l, r Hashable) bool { + return l.Hash() == r.Hash() +} diff --git a/datamodel/low/reference.go b/datamodel/low/reference.go index c2fb0bb..a635d82 100644 --- a/datamodel/low/reference.go +++ b/datamodel/low/reference.go @@ -22,6 +22,12 @@ type HasValueNode[T any] interface { *T } +// Hashable defines any struct that implements a Hash function that returns a 256SHA hash of the state of the +// representative object. Great for equality checking! +type Hashable interface { + Hash() [32]byte +} + // HasExtensions is implemented by any object that exposes extensions type HasExtensions[T any] interface { GetExtensions() map[KeyReference[string]]ValueReference[any] diff --git a/datamodel/low/v3/constants.go b/datamodel/low/v3/constants.go index ee4fefb..1633e91 100644 --- a/datamodel/low/v3/constants.go +++ b/datamodel/low/v3/constants.go @@ -5,54 +5,89 @@ package v3 // Label definitions used to look up vales in yaml.Node tree. const ( - ComponentsLabel = "components" - SchemasLabel = "schemas" - EncodingLabel = "encoding" - HeadersLabel = "headers" - ParametersLabel = "parameters" - RequestBodyLabel = "requestBody" - RequestBodiesLabel = "requestBodies" - ResponsesLabel = "responses" - CallbacksLabel = "callbacks" - ContentLabel = "content" - PathsLabel = "paths" - WebhooksLabel = "webhooks" - JSONSchemaDialectLabel = "jsonSchemaDialect" - GetLabel = "get" - PostLabel = "post" - PatchLabel = "patch" - PutLabel = "put" - DeleteLabel = "delete" - OptionsLabel = "options" - HeadLabel = "head" - TraceLabel = "trace" - LinksLabel = "links" - DefaultLabel = "default" - SecurityLabel = "security" - SecuritySchemesLabel = "securitySchemes" - OAuthFlowsLabel = "flows" - VariablesLabel = "variables" - ServersLabel = "servers" - ServerLabel = "server" - ImplicitLabel = "implicit" - PasswordLabel = "password" - ClientCredentialsLabel = "clientCredentials" - AuthorizationCodeLabel = "authorizationCode" - DescriptionLabel = "description" - URLLabel = "url" - NameLabel = "name" - EmailLabel = "email" - TitleLabel = "title" - TermsOfServiceLabel = "termsOfService" - VersionLabel = "version" - LicenseLabel = "license" - ContactLabel = "contact" - NamespaceLabel = "namespace" - PrefixLabel = "prefix" - AttributeLabel = "attribute" - WrappedLabel = "wrapped" - PropertyNameLabel = "propertyName" - SummaryLabel = "summary" - ValueLabel = "value" - ExternalValue = "externalValue" + ComponentsLabel = "components" + SchemasLabel = "schemas" + EncodingLabel = "encoding" + HeadersLabel = "headers" + ParametersLabel = "parameters" + RequestBodyLabel = "requestBody" + RequestBodiesLabel = "requestBodies" + ResponsesLabel = "responses" + CallbacksLabel = "callbacks" + ContentLabel = "content" + PathsLabel = "paths" + WebhooksLabel = "webhooks" + JSONSchemaDialectLabel = "jsonSchemaDialect" + GetLabel = "get" + PostLabel = "post" + PatchLabel = "patch" + PutLabel = "put" + DeleteLabel = "delete" + OptionsLabel = "options" + HeadLabel = "head" + TraceLabel = "trace" + LinksLabel = "links" + DefaultLabel = "default" + SecurityLabel = "security" + SecuritySchemesLabel = "securitySchemes" + OAuthFlowsLabel = "flows" + VariablesLabel = "variables" + ServersLabel = "servers" + ServerLabel = "server" + ImplicitLabel = "implicit" + PasswordLabel = "password" + ClientCredentialsLabel = "clientCredentials" + AuthorizationCodeLabel = "authorizationCode" + DescriptionLabel = "description" + URLLabel = "url" + NameLabel = "name" + EmailLabel = "email" + TitleLabel = "title" + TermsOfServiceLabel = "termsOfService" + VersionLabel = "version" + LicenseLabel = "license" + ContactLabel = "contact" + NamespaceLabel = "namespace" + PrefixLabel = "prefix" + AttributeLabel = "attribute" + WrappedLabel = "wrapped" + PropertyNameLabel = "propertyName" + SummaryLabel = "summary" + ValueLabel = "value" + ExternalValue = "externalValue" + SchemaDialectLabel = "$schema" + ExclusiveMaximumLabel = "exclusiveMaximum" + ExclusiveMinimumLabel = "exclusiveMinimum" + TypeLabel = "type" + MultipleOfLabel = "multipleOf" + MaximumLabel = "maximum" + MinimumLabel = "minimum" + MaxLengthLabel = "maxLength" + MinLengthLabel = "minLength" + PatternLabel = "pattern" + FormatLabel = "format" + MaxItemsLabel = "maxItems" + MinItemsLabel = "minItems" + UniqueItemsLabel = "uniqueItems" + MaxPropertiesLabel = "maxProperties" + MinPropertiesLabel = "minProperties" + RequiredLabel = "required" + EnumLabel = "enum" + SchemaLabel = "schema" + NotLabel = "not" + ItemsLabel = "items" + PropertiesLabel = "properties" + AllOfLabel = "allOf" + AnyOfLabel = "anyOf" + OneOfLabel = "oneOf" + AdditionalPropertiesLabel = "additionalProperties" + ContentEncodingLabel = "contentEncoding" + ContentMediaType = "contentMediaType" + NullableLabel = "nullable" + ReadOnlyLabel = "readOnly" + WriteOnlyLabel = "writeOnly" + XMLLabel = "xml" + DeprecatedLabel = "deprecated" + ExampleLabel = "example" + RefLabel = "$ref" ) diff --git a/what-changed/comparison_functions.go b/what-changed/comparison_functions.go index fff0a93..344d47d 100644 --- a/what-changed/comparison_functions.go +++ b/what-changed/comparison_functions.go @@ -161,7 +161,11 @@ func CheckForAddition[T any](l, r *yaml.Node, label string, changes *[]*Change[T // // The Change is then added to the slice of []Change[T] instances provided as a pointer. func CheckForModification[T any](l, r *yaml.Node, label string, changes *[]*Change[T], breaking bool, orig, new T) { - if l != nil && l.Value != "" && r != nil && r.Value != "" && r.Value != l.Value { + if l != nil && l.Value != "" && r != nil && r.Value != "" && r.Value != l.Value && r.Tag == l.Tag { + CreateChange[T](changes, Modified, label, l, r, breaking, orig, new) + } + // the values may have not changed, but the tag (node type) type may have + if l != nil && l.Value != "" && r != nil && r.Value != "" && r.Value != l.Value && r.Tag != l.Tag { CreateChange[T](changes, Modified, label, l, r, breaking, orig, new) } } diff --git a/what-changed/schema.go b/what-changed/schema.go new file mode 100644 index 0000000..74d4cdc --- /dev/null +++ b/what-changed/schema.go @@ -0,0 +1,635 @@ +// Copyright 2022 Princess B33f Heavy Industries / Dave Shanley +// SPDX-License-Identifier: MIT + +package what_changed + +import ( + "fmt" + "github.com/pb33f/libopenapi/datamodel/low/base" + v3 "github.com/pb33f/libopenapi/datamodel/low/v3" +) + +type SchemaChanges struct { + PropertyChanges[*base.Schema] + DiscriminatorChanges *DiscriminatorChanges + AllOfChanges []*SchemaChanges + AnyOfChanges *SchemaChanges + NotChanges *SchemaChanges + ItemsChanges *SchemaChanges + SchemaPropertyChanges map[string]*SchemaChanges + ExternalDocChanges *ExternalDocChanges + ExtensionChanges *ExtensionChanges +} + +func (s *SchemaChanges) TotalChanges() int { + t := s.PropertyChanges.TotalChanges() + if s.DiscriminatorChanges != nil { + t += s.DiscriminatorChanges.TotalChanges() + } + if len(s.AllOfChanges) > 0 { + for n := range s.AllOfChanges { + t += s.AllOfChanges[n].TotalChanges() + } + } + if s.AnyOfChanges != nil { + t += s.AnyOfChanges.TotalChanges() + } + if s.NotChanges != nil { + t += s.NotChanges.TotalChanges() + } + if s.ItemsChanges != nil { + t += s.ItemsChanges.TotalChanges() + } + if s.SchemaPropertyChanges != nil { + for n := range s.SchemaPropertyChanges { + t += s.SchemaPropertyChanges[n].TotalChanges() + } + } + if s.ExternalDocChanges != nil { + t += s.ExternalDocChanges.TotalChanges() + } + if s.ExtensionChanges != nil { + t += s.ExtensionChanges.TotalChanges() + } + return t +} + +func (s *SchemaChanges) TotalBreakingChanges() int { + t := s.PropertyChanges.TotalBreakingChanges() + if s.DiscriminatorChanges != nil { + t += s.DiscriminatorChanges.TotalBreakingChanges() + } + if len(s.AllOfChanges) > 0 { + for n := range s.AllOfChanges { + t += s.AllOfChanges[n].TotalBreakingChanges() + } + } + if s.AnyOfChanges != nil { + t += s.AnyOfChanges.TotalBreakingChanges() + } + if s.NotChanges != nil { + t += s.NotChanges.TotalBreakingChanges() + } + if s.ItemsChanges != nil { + t += s.ItemsChanges.TotalBreakingChanges() + } + if s.SchemaPropertyChanges != nil { + for n := range s.SchemaPropertyChanges { + t += s.SchemaPropertyChanges[n].TotalBreakingChanges() + } + } + if s.ExternalDocChanges != nil { + t += s.ExternalDocChanges.TotalBreakingChanges() + } + if s.ExtensionChanges != nil { + t += s.ExtensionChanges.TotalBreakingChanges() + } + return t +} + +func CompareSchemas(l, r *base.SchemaProxy) *SchemaChanges { + sc := new(SchemaChanges) + var changes []*Change[*base.Schema] + + // Added + if l == nil && r != nil { + CreateChange[*base.Schema](&changes, ObjectAdded, v3.SchemaLabel, + nil, nil, true, nil, r) + sc.Changes = changes + } + + // Removed + if l != nil && r == nil { + CreateChange[*base.Schema](&changes, ObjectRemoved, v3.SchemaLabel, + nil, nil, true, l, nil) + sc.Changes = changes + } + + if l != nil && r != nil { + + // if left proxy is a reference and right is a reference (we won't recurse into them) + if l.IsSchemaReference() && r.IsSchemaReference() { + // points to the same schema + if l.GetSchemaReference() == r.GetSchemaReference() { + // there is nothing to be done at this point. + return nil + } else { + // references are different, that's all we care to know. + CreateChange[*base.Schema](&changes, Modified, v3.RefLabel, + nil, nil, true, l.GetSchemaReference(), r.GetSchemaReference()) + sc.Changes = changes + return sc + } + } + + // changed from ref to inline + if !l.IsSchemaReference() && r.IsSchemaReference() { + CreateChange[*base.Schema](&changes, Modified, v3.RefLabel, + nil, nil, false, "", r.GetSchemaReference()) + sc.Changes = changes + return sc // we're done here + } + + // changed from inline to ref + if l.IsSchemaReference() && !r.IsSchemaReference() { + CreateChange[*base.Schema](&changes, Modified, v3.RefLabel, + nil, nil, false, l.GetSchemaReference(), "") + sc.Changes = changes + return sc // done, nothing else to do. + } + + lSchema := l.Schema() + rSchema := r.Schema() + + leftHash := lSchema.Hash() + rightHash := rSchema.Hash() + + fmt.Printf("%v-%v", leftHash, rightHash) + + var props []*PropertyCheck[*base.Schema] + + // $schema (breaking change) + props = append(props, &PropertyCheck[*base.Schema]{ + LeftNode: lSchema.SchemaTypeRef.ValueNode, + RightNode: rSchema.SchemaTypeRef.ValueNode, + Label: v3.SchemaDialectLabel, + Changes: &changes, + Breaking: true, + Original: lSchema, + New: rSchema, + }) + + // ExclusiveMaximum + props = append(props, &PropertyCheck[*base.Schema]{ + LeftNode: lSchema.ExclusiveMaximum.ValueNode, + RightNode: rSchema.ExclusiveMaximum.ValueNode, + Label: v3.ExclusiveMaximumLabel, + Changes: &changes, + Breaking: true, + Original: lSchema, + New: rSchema, + }) + + // ExclusiveMinimum + props = append(props, &PropertyCheck[*base.Schema]{ + LeftNode: lSchema.ExclusiveMinimum.ValueNode, + RightNode: rSchema.ExclusiveMinimum.ValueNode, + Label: v3.ExclusiveMinimumLabel, + Changes: &changes, + Breaking: true, + Original: lSchema, + New: rSchema, + }) + + // Type + props = append(props, &PropertyCheck[*base.Schema]{ + LeftNode: lSchema.Type.ValueNode, + RightNode: rSchema.Type.ValueNode, + Label: v3.TypeLabel, + Changes: &changes, + Breaking: true, + Original: lSchema, + New: rSchema, + }) + + // Type + props = append(props, &PropertyCheck[*base.Schema]{ + LeftNode: lSchema.Type.ValueNode, + RightNode: rSchema.Type.ValueNode, + Label: v3.TypeLabel, + Changes: &changes, + Breaking: true, + Original: lSchema, + New: rSchema, + }) + + // Title + props = append(props, &PropertyCheck[*base.Schema]{ + LeftNode: lSchema.Title.ValueNode, + RightNode: rSchema.Title.ValueNode, + Label: v3.TitleLabel, + Changes: &changes, + Breaking: false, + Original: lSchema, + New: rSchema, + }) + + // MultipleOf + props = append(props, &PropertyCheck[*base.Schema]{ + LeftNode: lSchema.MultipleOf.ValueNode, + RightNode: rSchema.MultipleOf.ValueNode, + Label: v3.MultipleOfLabel, + Changes: &changes, + Breaking: true, + Original: lSchema, + New: rSchema, + }) + + // Maximum + props = append(props, &PropertyCheck[*base.Schema]{ + LeftNode: lSchema.Maximum.ValueNode, + RightNode: rSchema.Maximum.ValueNode, + Label: v3.MaximumLabel, + Changes: &changes, + Breaking: true, + Original: lSchema, + New: rSchema, + }) + + // Minimum + props = append(props, &PropertyCheck[*base.Schema]{ + LeftNode: lSchema.Minimum.ValueNode, + RightNode: rSchema.Minimum.ValueNode, + Label: v3.MinimumLabel, + Changes: &changes, + Breaking: true, + Original: lSchema, + New: rSchema, + }) + + // MaxLength + props = append(props, &PropertyCheck[*base.Schema]{ + LeftNode: lSchema.MaxLength.ValueNode, + RightNode: rSchema.MaxLength.ValueNode, + Label: v3.MaxLengthLabel, + Changes: &changes, + Breaking: true, + Original: lSchema, + New: rSchema, + }) + + // MinLength + props = append(props, &PropertyCheck[*base.Schema]{ + LeftNode: lSchema.MinLength.ValueNode, + RightNode: rSchema.MinLength.ValueNode, + Label: v3.MinLengthLabel, + Changes: &changes, + Breaking: true, + Original: lSchema, + New: rSchema, + }) + + // Pattern + props = append(props, &PropertyCheck[*base.Schema]{ + LeftNode: lSchema.Pattern.ValueNode, + RightNode: rSchema.Pattern.ValueNode, + Label: v3.PatternLabel, + Changes: &changes, + Breaking: true, + Original: lSchema, + New: rSchema, + }) + + // Format + props = append(props, &PropertyCheck[*base.Schema]{ + LeftNode: lSchema.Format.ValueNode, + RightNode: rSchema.Format.ValueNode, + Label: v3.FormatLabel, + Changes: &changes, + Breaking: true, + Original: lSchema, + New: rSchema, + }) + + // MaxItems + props = append(props, &PropertyCheck[*base.Schema]{ + LeftNode: lSchema.MaxItems.ValueNode, + RightNode: rSchema.MaxItems.ValueNode, + Label: v3.MaxItemsLabel, + Changes: &changes, + Breaking: true, + Original: lSchema, + New: rSchema, + }) + + // MinItems + props = append(props, &PropertyCheck[*base.Schema]{ + LeftNode: lSchema.MinItems.ValueNode, + RightNode: rSchema.MinItems.ValueNode, + Label: v3.MinItemsLabel, + Changes: &changes, + Breaking: true, + Original: lSchema, + New: rSchema, + }) + + // UniqueItems + props = append(props, &PropertyCheck[*base.Schema]{ + LeftNode: lSchema.UniqueItems.ValueNode, + RightNode: rSchema.UniqueItems.ValueNode, + Label: v3.MinLengthLabel, + Changes: &changes, + Breaking: true, + Original: lSchema, + New: rSchema, + }) + + // MaxProperties + props = append(props, &PropertyCheck[*base.Schema]{ + LeftNode: lSchema.MaxProperties.ValueNode, + RightNode: rSchema.MaxProperties.ValueNode, + Label: v3.MaxPropertiesLabel, + Changes: &changes, + Breaking: true, + Original: lSchema, + New: rSchema, + }) + + // MinProperties + props = append(props, &PropertyCheck[*base.Schema]{ + LeftNode: lSchema.MinProperties.ValueNode, + RightNode: rSchema.MinProperties.ValueNode, + Label: v3.MinPropertiesLabel, + Changes: &changes, + Breaking: true, + Original: lSchema, + New: rSchema, + }) + + // Required + j := make(map[string]int) + k := make(map[string]int) + for i := range lSchema.Required.Value { + j[lSchema.Required.Value[i].Value] = i + } + for i := range rSchema.Required.Value { + k[rSchema.Required.Value[i].Value] = i + } + + // added + for g := range k { + if _, ok := j[g]; !ok { + CreateChange[*base.Schema](&changes, PropertyAdded, v3.RequiredLabel, + nil, rSchema.Required.Value[k[g]].GetValueNode(), true, nil, + rSchema.Required.Value[k[g]].GetValue) + } + } + // removed + for g := range j { + if _, ok := k[g]; !ok { + CreateChange[*base.Schema](&changes, PropertyRemoved, v3.RequiredLabel, + lSchema.Required.Value[j[g]].GetValueNode(), nil, true, lSchema.Required.Value[j[g]].GetValue, + nil) + } + } + + // Enums + j = make(map[string]int) + k = make(map[string]int) + for i := range lSchema.Enum.Value { + j[lSchema.Enum.Value[i].Value] = i + } + for i := range rSchema.Enum.Value { + k[rSchema.Enum.Value[i].Value] = i + } + + // added + for g := range k { + if _, ok := j[g]; !ok { + CreateChange[*base.Schema](&changes, PropertyAdded, v3.EnumLabel, + nil, rSchema.Enum.Value[k[g]].GetValueNode(), false, nil, + rSchema.Enum.Value[k[g]].GetValue) + } + } + // removed + for g := range j { + if _, ok := k[g]; !ok { + CreateChange[*base.Schema](&changes, PropertyRemoved, v3.EnumLabel, + lSchema.Enum.Value[j[g]].GetValueNode(), nil, true, lSchema.Enum.Value[j[g]].GetValue, + nil) + } + } + + props = append(props, &PropertyCheck[*base.Schema]{ + LeftNode: lSchema.Required.ValueNode, + RightNode: rSchema.Required.ValueNode, + Label: v3.RequiredLabel, + Changes: &changes, + Breaking: true, + Original: lSchema, + New: rSchema, + }) + + // Enum + props = append(props, &PropertyCheck[*base.Schema]{ + LeftNode: lSchema.Enum.ValueNode, + RightNode: rSchema.Enum.ValueNode, + Label: v3.EnumLabel, + Changes: &changes, + Breaking: true, + Original: lSchema, + New: rSchema, + }) + + // UniqueItems + props = append(props, &PropertyCheck[*base.Schema]{ + LeftNode: lSchema.UniqueItems.ValueNode, + RightNode: rSchema.UniqueItems.ValueNode, + Label: v3.UniqueItemsLabel, + Changes: &changes, + Breaking: true, + Original: lSchema, + New: rSchema, + }) + // TODO: end of re-do + + // AdditionalProperties + props = append(props, &PropertyCheck[*base.Schema]{ + LeftNode: lSchema.AdditionalProperties.ValueNode, + RightNode: rSchema.AdditionalProperties.ValueNode, + Label: v3.AdditionalPropertiesLabel, + Changes: &changes, + Breaking: false, + Original: lSchema, + New: rSchema, + }) + + // Description + props = append(props, &PropertyCheck[*base.Schema]{ + LeftNode: lSchema.Description.ValueNode, + RightNode: rSchema.Description.ValueNode, + Label: v3.MinLengthLabel, + Changes: &changes, + Breaking: false, + Original: lSchema, + New: rSchema, + }) + + // ContentEncoding + props = append(props, &PropertyCheck[*base.Schema]{ + LeftNode: lSchema.ContentEncoding.ValueNode, + RightNode: rSchema.ContentEncoding.ValueNode, + Label: v3.ContentEncodingLabel, + Changes: &changes, + Breaking: true, + Original: lSchema, + New: rSchema, + }) + + // ContentMediaType + props = append(props, &PropertyCheck[*base.Schema]{ + LeftNode: lSchema.ContentMediaType.ValueNode, + RightNode: rSchema.ContentMediaType.ValueNode, + Label: v3.ContentMediaType, + Changes: &changes, + Breaking: true, + Original: lSchema, + New: rSchema, + }) + + // Default + props = append(props, &PropertyCheck[*base.Schema]{ + LeftNode: lSchema.Default.ValueNode, + RightNode: rSchema.Default.ValueNode, + Label: v3.DefaultLabel, + Changes: &changes, + Breaking: true, + Original: lSchema, + New: rSchema, + }) + + // Nullable + props = append(props, &PropertyCheck[*base.Schema]{ + LeftNode: lSchema.Nullable.ValueNode, + RightNode: rSchema.Nullable.ValueNode, + Label: v3.NullableLabel, + Changes: &changes, + Breaking: true, + Original: lSchema, + New: rSchema, + }) + + // ReadOnly + props = append(props, &PropertyCheck[*base.Schema]{ + LeftNode: lSchema.ReadOnly.ValueNode, + RightNode: rSchema.ReadOnly.ValueNode, + Label: v3.ReadOnlyLabel, + Changes: &changes, + Breaking: true, + Original: lSchema, + New: rSchema, + }) + + // WriteOnly + props = append(props, &PropertyCheck[*base.Schema]{ + LeftNode: lSchema.WriteOnly.ValueNode, + RightNode: rSchema.WriteOnly.ValueNode, + Label: v3.WriteOnlyLabel, + Changes: &changes, + Breaking: true, + Original: lSchema, + New: rSchema, + }) + + // Example + props = append(props, &PropertyCheck[*base.Schema]{ + LeftNode: lSchema.Example.ValueNode, + RightNode: rSchema.Example.ValueNode, + Label: v3.ExampleLabel, + Changes: &changes, + Breaking: false, + Original: lSchema, + New: rSchema, + }) + + // Deprecated + props = append(props, &PropertyCheck[*base.Schema]{ + LeftNode: lSchema.Deprecated.ValueNode, + RightNode: rSchema.Deprecated.ValueNode, + Label: v3.DeprecatedLabel, + Changes: &changes, + Breaking: false, + Original: lSchema, + New: rSchema, + }) + + // check properties + CheckProperties(props) + + // check objects. + // AllOf + // if both sides are equal + //if len(rSchema.AllOf.Value) == len(lSchema.AllOf.Value) { + var multiChange []*SchemaChanges + for d := range lSchema.AllOf.Value { + var lSch, rSch *base.SchemaProxy + lSch = lSchema.AllOf.Value[d].Value + + if rSchema.AllOf.Value[d].Value != nil { + rSch = rSchema.AllOf.Value[d].Value + } + // if neither is a reference, build the schema and compare. + //if !lSch.IsSchemaReference() && !rSch.IsSchemaReference() { + multiChange = append(multiChange, CompareSchemas(lSch, rSch)) + //} + // if the left is a reference and right is inline, log a modification, but no recursion. + //if lSch.IsSchemaReference() && !rSch.IsSchemaReference() { + // CreateChange[*base.Schema](&changes, Modified, v3.AllOfLabel, + // nil, nil, false, lSch.GetSchemaReference(), "") + //} + // + //// if the right is a reference and left is inline, log a modification, but no recursion. + //if !lSch.IsSchemaReference() && rSch.IsSchemaReference() { + // CreateChange[*base.Schema](&changes, Modified, v3.AllOfLabel, + // nil, nil, false, "", rSch.GetSchemaReference()) + //} + + } + + //} + + //check if the right is longer that the left (added) + if len(rSchema.AllOf.Value) > len(lSchema.AllOf.Value) { + y := len(lSchema.AllOf.Value) + if y < 0 { + y = 0 + } + for s := range rSchema.AllOf.Value[y:] { + rSch := rSchema.AllOf.Value[s].Value + multiChange = append(multiChange, CompareSchemas(nil, rSch)) + + //if !rSchema.AllOf.Value[s].Value.IsSchemaReference() { + // CreateChange[*base.Schema](&changes, ObjectAdded, v3.AllOfLabel, + // nil, rSchema.AllOf.Value[s].GetValueNode(), false, nil, rSchema.AllOf.Value[s].Value.Schema()) + //} else { + // CreateChange[*base.Schema](&changes, ObjectAdded, v3.AllOfLabel, + // nil, rSchema.AllOf.Value[s].GetValueNode(), false, nil, rSchema.AllOf.Value[s].Value) + //} + } + + } + + if len(multiChange) > 0 { + sc.AllOfChanges = multiChange + } + + // + //// check if the left is longer that the right (removed) + //if len(lSchema.AllOf.Value) > len(rSchema.AllOf.Value) { + // var multiChange []*SchemaChanges + // for s := range lSchema.AllOf.Value[len(rSchema.AllOf.Value)-1:] { + // lSch := lSchema.AllOf.Value[s].Value + // multiChange = append(multiChange, CompareSchemas(lSch, nil)) + // //if !lSchema.AllOf.Value[s].Value.IsSchemaReference() { + // // CreateChange[*base.Schema](&changes, ObjectRemoved, v3.AllOfLabel, + // // lSchema.AllOf.Value[s].GetValueNode(), nil, false, lSchema.AllOf.Value[s].Value.Schema(), nil) + // //} else { + // // CreateChange[*base.Schema](&changes, ObjectRemoved, v3.AllOfLabel, + // // lSchema.AllOf.Value[s].GetValueNode(), nil, false, lSchema.AllOf.Value[s].Value, nil) + // //} + // } + // if len(multiChange) > 0 { + // sc.AllOfChanges = multiChange + // } + //} + + } + + // done + sc.Changes = changes + if sc.TotalChanges() <= 0 { + return nil + } + return sc + +} diff --git a/what-changed/schema_test.go b/what-changed/schema_test.go new file mode 100644 index 0000000..556d1ec --- /dev/null +++ b/what-changed/schema_test.go @@ -0,0 +1,188 @@ +// Copyright 2022 Princess B33f Heavy Industries / Dave Shanley +// SPDX-License-Identifier: MIT + +package what_changed + +import ( + "github.com/pb33f/libopenapi/datamodel" + v3 "github.com/pb33f/libopenapi/datamodel/low/v3" + "github.com/stretchr/testify/assert" + "testing" +) + +// These tests require full documents to be tested properly. schemas are perhaps the most complex +// of all the things in OpenAPI, to ensure correctness, we must test the whole document structure. +func TestCompareSchemas(t *testing.T) { + + // to test this correctly, we need a simulated document with inline schemas for recursive + // checking, as well as a couple of references, so we can avoid that disaster. + // in our model, components/definitions will be checked independently for changes + // and references will be checked only for value changes (points to a different reference) + // left := `openapi: 3.1.0 + //paths: + // /chicken/nuggets: + // get: + // responses: + // "200": + // content: + // application/json: + // schema: + // $ref: '#/components/schemas/OK' + // /chicken/soup: + // get: + // responses: + // "200": + // content: + // application/json: + // schema: + // title: an OK message + // allOf: + // - type: int + // properties: + // propA: + // title: a proxy property + // type: string + //components: + // schemas: + // OK: + // title: an OK message + // allOf: + // - type: string + // properties: + // propA: + // title: a proxy property + // type: string` + // + // right := `openapi: 3.1.0 + //paths: + // /chicken/nuggets: + // get: + // responses: + // "200": + // content: + // application/json: + // schema: + // $ref: '#/components/schemas/OK' + // /chicken/soup: + // get: + // responses: + // "200": + // content: + // application/json: + // schema: + // title: an OK message that is different + // allOf: + // - type: int + // description: oh my stars + // - $ref: '#/components/schemas/NoWay' + // properties: + // propA: + // title: a proxy property + // type: string + //components: + // schemas: + // NoWay: + // type: string + // OK: + // title: an OK message that has now changed. + // allOf: + // - type: string + // properties: + // propA: + // title: a proxy property + // type: string` + + left := `openapi: 3.1.0 +paths: + /chicken/nuggets: + get: + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/OK' + /chicken/soup: + get: + responses: + "200": + content: + application/json: + schema: + title: an OK message + allOf: + - type: int + properties: + propA: + title: a proxy property + type: string +components: + schemas: + OK: + title: an OK message + allOf: + - type: string + properties: + propA: + title: a proxy property + type: string` + + right := `openapi: 3.1.0 +paths: + /chicken/nuggets: + get: + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/OK' + /chicken/soup: + get: + responses: + "200": + content: + application/json: + schema: + title: an OK message that is different + allOf: + - type: int + description: oh my stars + - $ref: '#/components/schemas/NoWay' + properties: + propA: + title: a proxy property + type: string +components: + schemas: + NoWay: + type: string + OK: + title: an OK message that has now changed. + allOf: + - type: string + properties: + propA: + title: a proxy property + type: string` + + leftInfo, _ := datamodel.ExtractSpecInfo([]byte(left)) + rightInfo, _ := datamodel.ExtractSpecInfo([]byte(right)) + + leftDoc, _ := v3.CreateDocument(leftInfo) + rightDoc, _ := v3.CreateDocument(rightInfo) + + // extract left reference schema and non reference schema. + lSchemaProxy := leftDoc.Paths.Value.FindPath("/chicken/soup").Value.Get. + Value.Responses.Value.FindResponseByCode("200").Value. + FindContent("application/json").Value.Schema + + // extract right reference schema and non reference schema. + rSchemaProxy := rightDoc.Paths.Value.FindPath("/chicken/soup").Value.Get. + Value.Responses.Value.FindResponseByCode("200").Value. + FindContent("application/json").Value.Schema + + changes := CompareSchemas(lSchemaProxy.Value, rSchemaProxy.Value) + assert.NotNil(t, changes) + +}