From 4b9c5fba1ee7cceb0c46a04f7bf6777e6bd47357 Mon Sep 17 00:00:00 2001 From: Dave Shanley Date: Sat, 8 Oct 2022 14:09:46 -0400 Subject: [PATCH] Building out schema comparison mechanism Which has led to a new wider hashing capability for the low level API. hashing makes it very easy to determine changes quickly, without having to run comparisons to discover changes, could really speed things up moving forward. --- datamodel/low/base/base.go | 1 + datamodel/low/base/discriminator.go | 21 + datamodel/low/base/external_doc.go | 11 + datamodel/low/base/schema.go | 192 +++++++- datamodel/low/base/schema_proxy.go | 27 +- datamodel/low/base/schema_test.go | 97 ++++ datamodel/low/base/xml.go | 16 + datamodel/low/extraction_functions.go | 5 + datamodel/low/reference.go | 6 + datamodel/low/v3/constants.go | 135 ++++-- what-changed/comparison_functions.go | 6 +- what-changed/schema.go | 635 ++++++++++++++++++++++++++ what-changed/schema_test.go | 188 ++++++++ 13 files changed, 1276 insertions(+), 64 deletions(-) create mode 100644 what-changed/schema.go create mode 100644 what-changed/schema_test.go 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) + +}