mirror of
https://github.com/LukeHagar/libopenapi.git
synced 2025-12-08 20:47:43 +00:00
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:
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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, "|")))
|
||||||
|
}
|
||||||
|
|||||||
@@ -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, "|")))
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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()))
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
@@ -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, "|")))
|
||||||
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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
635
what-changed/schema.go
Normal 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
188
what-changed/schema_test.go
Normal 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)
|
||||||
|
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user