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.
This commit is contained in:
Dave Shanley
2022-10-08 14:09:46 -04:00
parent 7f61a7624d
commit 4b9c5fba1e
13 changed files with 1276 additions and 64 deletions

View File

@@ -9,3 +9,4 @@
// beats, particularly when polymorphism is used. By re-using the same superset Schema across versions, we can ensure // 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. // that all the latest features are collected, without damaging backwards compatibility.
package base package base

View File

@@ -4,7 +4,10 @@
package base package base
import ( import (
"crypto/sha256"
"github.com/pb33f/libopenapi/datamodel/low" "github.com/pb33f/libopenapi/datamodel/low"
"sort"
"strings"
) )
// Discriminator is only used by OpenAPI 3+ documents, it represents a polymorphic discriminator used for schemas // 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 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, "|")))
}

View File

@@ -4,9 +4,11 @@
package base package base
import ( import (
"crypto/sha256"
"github.com/pb33f/libopenapi/datamodel/low" "github.com/pb33f/libopenapi/datamodel/low"
"github.com/pb33f/libopenapi/index" "github.com/pb33f/libopenapi/index"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"strings"
) )
// ExternalDoc represents a low-level External Documentation object as defined by OpenAPI 2 and 3 // 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 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, "|")))
}

View File

@@ -1,12 +1,15 @@
package base package base
import ( import (
"crypto/sha256"
"fmt" "fmt"
"github.com/pb33f/libopenapi/datamodel/low" "github.com/pb33f/libopenapi/datamodel/low"
"github.com/pb33f/libopenapi/index" "github.com/pb33f/libopenapi/index"
"github.com/pb33f/libopenapi/utils" "github.com/pb33f/libopenapi/utils"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"sort"
"strconv" "strconv"
"strings"
) )
// SchemaDynamicValue is used to hold multiple possible values for a schema property. There are two values, a left // 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] 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 // FindProperty will return a ValueReference pointer containing a SchemaProxy pointer
// from a property key name. if found // from a property key name. if found
func (s *Schema) FindProperty(name string) *low.ValueReference[*SchemaProxy] { 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]) syncChan := make(chan *low.ValueReference[*SchemaProxy])
// build out a SchemaProxy for every sub-schema. // 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 // 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 // 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 // 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.kn = kn
sp.vn = vn sp.vn = vn
sp.idx = idx sp.idx = idx
if isRef {
sp.referenceLookup = refLocation
sp.isReference = true
}
res := &low.ValueReference[*SchemaProxy]{ res := &low.ValueReference[*SchemaProxy]{
Value: sp, Value: sp,
ValueNode: vn, ValueNode: vn,
@@ -484,8 +647,12 @@ func buildSchema(schemas chan schemaProxyBuildResult, labelNode, valueNode *yaml
c <- res c <- res
} }
isRef := false
refLocation := ""
if utils.IsNodeMap(valueNode) { 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) ref, _ := low.LocateRefNode(valueNode, idx)
if ref != nil { if ref != nil {
valueNode = ref 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 // this only runs once, however to keep things consistent, it makes sense to use the same async method
// that arrays will use. // that arrays will use.
go build(labelNode, valueNode, syncChan) go build(labelNode, valueNode, syncChan, isRef, refLocation)
select { select {
case r := <-syncChan: case r := <-syncChan:
schemas <- schemaProxyBuildResult{ schemas <- schemaProxyBuildResult{
@@ -512,7 +679,10 @@ func buildSchema(schemas chan schemaProxyBuildResult, labelNode, valueNode *yaml
if utils.IsNodeArray(valueNode) { if utils.IsNodeArray(valueNode) {
refBuilds := 0 refBuilds := 0
for _, vn := range valueNode.Content { 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) ref, _ := low.LocateRefNode(vn, idx)
if ref != nil { if ref != nil {
vn = ref vn = ref
@@ -524,7 +694,7 @@ func buildSchema(schemas chan schemaProxyBuildResult, labelNode, valueNode *yaml
} }
} }
refBuilds++ refBuilds++
go build(vn, vn, syncChan) go build(vn, vn, syncChan, isRef, refLocation)
} }
completedBuilds := 0 completedBuilds := 0
for completedBuilds < refBuilds { 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) { func ExtractSchema(root *yaml.Node, idx *index.SpecIndex) (*low.NodeReference[*SchemaProxy], error) {
var schLabel, schNode *yaml.Node var schLabel, schNode *yaml.Node
errStr := "schema build failed: reference '%s' cannot be found at line %d, col %d" errStr := "schema build failed: reference '%s' cannot be found at line %d, col %d"
isRef := false
refLocation := ""
if rf, rl, _ := utils.IsNodeRefValue(root); rf { if rf, rl, _ := utils.IsNodeRefValue(root); rf {
// locate reference in index. // locate reference in index.
isRef = true
ref, _ := low.LocateRefNode(root, idx) ref, _ := low.LocateRefNode(root, idx)
if ref != nil { if ref != nil {
schNode = ref schNode = ref
@@ -564,7 +738,9 @@ func ExtractSchema(root *yaml.Node, idx *index.SpecIndex) (*low.NodeReference[*S
} else { } else {
_, schLabel, schNode = utils.FindKeyNodeFull(SchemaLabel, root.Content) _, schLabel, schNode = utils.FindKeyNodeFull(SchemaLabel, root.Content)
if schNode != nil { 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) ref, _ := low.LocateRefNode(schNode, idx)
if ref != nil { if ref != nil {
schNode = ref schNode = ref
@@ -578,7 +754,7 @@ func ExtractSchema(root *yaml.Node, idx *index.SpecIndex) (*low.NodeReference[*S
if schNode != nil { if schNode != nil {
// check if schema has already been built. // 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 &low.NodeReference[*SchemaProxy]{Value: schema, KeyNode: schLabel, ValueNode: schNode}, nil
} }
return nil, nil return nil, nil

View File

@@ -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 // 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. // when a schema is needed, so the rest of the document is parsed and ready to use.
type SchemaProxy struct { type SchemaProxy struct {
kn *yaml.Node kn *yaml.Node
vn *yaml.Node vn *yaml.Node
idx *index.SpecIndex idx *index.SpecIndex
rendered *Schema rendered *Schema
buildError error 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. // 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 { func (sp *SchemaProxy) GetBuildError() error {
return sp.buildError 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
}

View File

@@ -1208,3 +1208,100 @@ func TestExtractSchema_OneOfRef(t *testing.T) {
res.Value.Schema().OneOf.Value[0].Value.Schema().Description.Value) 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()))
}

View File

@@ -1,9 +1,12 @@
package base package base
import ( import (
"crypto/sha256"
"fmt"
"github.com/pb33f/libopenapi/datamodel/low" "github.com/pb33f/libopenapi/datamodel/low"
"github.com/pb33f/libopenapi/index" "github.com/pb33f/libopenapi/index"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"strings"
) )
// XML represents a low-level representation of an XML object defined by all versions of OpenAPI. // 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] { func (x *XML) GetExtensions() map[low.KeyReference[string]]low.ValueReference[any] {
return x.Extensions 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, "|")))
}

View File

@@ -544,3 +544,8 @@ func ExtractExtensions(root *yaml.Node) map[KeyReference[string]]ValueReference[
} }
return extensionMap return extensionMap
} }
// AreEqual returns true if two Hashable objects are equal or not.
func AreEqual(l, r Hashable) bool {
return l.Hash() == r.Hash()
}

View File

@@ -22,6 +22,12 @@ type HasValueNode[T any] interface {
*T *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 // HasExtensions is implemented by any object that exposes extensions
type HasExtensions[T any] interface { type HasExtensions[T any] interface {
GetExtensions() map[KeyReference[string]]ValueReference[any] GetExtensions() map[KeyReference[string]]ValueReference[any]

View File

@@ -5,54 +5,89 @@ package v3
// Label definitions used to look up vales in yaml.Node tree. // Label definitions used to look up vales in yaml.Node tree.
const ( const (
ComponentsLabel = "components" ComponentsLabel = "components"
SchemasLabel = "schemas" SchemasLabel = "schemas"
EncodingLabel = "encoding" EncodingLabel = "encoding"
HeadersLabel = "headers" HeadersLabel = "headers"
ParametersLabel = "parameters" ParametersLabel = "parameters"
RequestBodyLabel = "requestBody" RequestBodyLabel = "requestBody"
RequestBodiesLabel = "requestBodies" RequestBodiesLabel = "requestBodies"
ResponsesLabel = "responses" ResponsesLabel = "responses"
CallbacksLabel = "callbacks" CallbacksLabel = "callbacks"
ContentLabel = "content" ContentLabel = "content"
PathsLabel = "paths" PathsLabel = "paths"
WebhooksLabel = "webhooks" WebhooksLabel = "webhooks"
JSONSchemaDialectLabel = "jsonSchemaDialect" JSONSchemaDialectLabel = "jsonSchemaDialect"
GetLabel = "get" GetLabel = "get"
PostLabel = "post" PostLabel = "post"
PatchLabel = "patch" PatchLabel = "patch"
PutLabel = "put" PutLabel = "put"
DeleteLabel = "delete" DeleteLabel = "delete"
OptionsLabel = "options" OptionsLabel = "options"
HeadLabel = "head" HeadLabel = "head"
TraceLabel = "trace" TraceLabel = "trace"
LinksLabel = "links" LinksLabel = "links"
DefaultLabel = "default" DefaultLabel = "default"
SecurityLabel = "security" SecurityLabel = "security"
SecuritySchemesLabel = "securitySchemes" SecuritySchemesLabel = "securitySchemes"
OAuthFlowsLabel = "flows" OAuthFlowsLabel = "flows"
VariablesLabel = "variables" VariablesLabel = "variables"
ServersLabel = "servers" ServersLabel = "servers"
ServerLabel = "server" ServerLabel = "server"
ImplicitLabel = "implicit" ImplicitLabel = "implicit"
PasswordLabel = "password" PasswordLabel = "password"
ClientCredentialsLabel = "clientCredentials" ClientCredentialsLabel = "clientCredentials"
AuthorizationCodeLabel = "authorizationCode" AuthorizationCodeLabel = "authorizationCode"
DescriptionLabel = "description" DescriptionLabel = "description"
URLLabel = "url" URLLabel = "url"
NameLabel = "name" NameLabel = "name"
EmailLabel = "email" EmailLabel = "email"
TitleLabel = "title" TitleLabel = "title"
TermsOfServiceLabel = "termsOfService" TermsOfServiceLabel = "termsOfService"
VersionLabel = "version" VersionLabel = "version"
LicenseLabel = "license" LicenseLabel = "license"
ContactLabel = "contact" ContactLabel = "contact"
NamespaceLabel = "namespace" NamespaceLabel = "namespace"
PrefixLabel = "prefix" PrefixLabel = "prefix"
AttributeLabel = "attribute" AttributeLabel = "attribute"
WrappedLabel = "wrapped" WrappedLabel = "wrapped"
PropertyNameLabel = "propertyName" PropertyNameLabel = "propertyName"
SummaryLabel = "summary" SummaryLabel = "summary"
ValueLabel = "value" ValueLabel = "value"
ExternalValue = "externalValue" 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"
) )

View File

@@ -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. // 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) { 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) CreateChange[T](changes, Modified, label, l, r, breaking, orig, new)
} }
} }

635
what-changed/schema.go Normal file
View File

@@ -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
}

188
what-changed/schema_test.go Normal file
View File

@@ -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)
}