Wired up multi-version handling patterns

Designs for handling multiple versions of objects have been set, seems clean and scalable. Generic functions for handling maps has been added also, which will cut down time moving forward.
This commit is contained in:
Dave Shanley
2022-10-20 09:27:24 -04:00
parent 0aecd98ab6
commit b42e35f2b7
13 changed files with 678 additions and 79 deletions

View File

@@ -104,7 +104,7 @@ func LocateRefNode(root *yaml.Node, idx *index.SpecIndex) (*yaml.Node, error) {
} }
} }
return nil, fmt.Errorf("reference '%s' at line %d, column %d was not found", return nil, fmt.Errorf("reference '%s' at line %d, column %d was not found",
root.Value, root.Line, root.Column) rv, root.Line, root.Column)
} }
return nil, nil return nil, nil
} }
@@ -122,7 +122,7 @@ func ExtractObjectRaw[T Buildable[N], N any](root *yaml.Node, idx *index.SpecInd
} }
} else { } else {
if err != nil { if err != nil {
return nil, fmt.Errorf("object extraciton failed: %s", err.Error()) return nil, fmt.Errorf("object extraction failed: %s", err.Error())
} }
} }
} }
@@ -157,7 +157,7 @@ func ExtractObject[T Buildable[N], N any](label string, root *yaml.Node, idx *in
} }
} else { } else {
if err != nil { if err != nil {
return NodeReference[T]{}, fmt.Errorf("object extraciton failed: %s", err.Error()) return NodeReference[T]{}, fmt.Errorf("object extraction failed: %s", err.Error())
} }
} }
} else { } else {
@@ -172,7 +172,7 @@ func ExtractObject[T Buildable[N], N any](label string, root *yaml.Node, idx *in
} }
} else { } else {
if lerr != nil { if lerr != nil {
return NodeReference[T]{}, fmt.Errorf("object extraciton failed: %s", lerr.Error()) return NodeReference[T]{}, fmt.Errorf("object extraction failed: %s", lerr.Error())
} }
} }
} }
@@ -554,7 +554,13 @@ func AreEqual(l, r Hashable) bool {
return l.Hash() == r.Hash() return l.Hash() == r.Hash()
} }
// GenerateHashString will generate a SHA36 hash of any object passed in. // GenerateHashString will generate a SHA36 hash of any object passed in. If the object is Hashable
// then the underlying Hash() method will be called.
func GenerateHashString(v any) string { func GenerateHashString(v any) string {
if h, ok := v.(Hashable); ok {
if h != nil {
return fmt.Sprintf("%x", h.Hash())
}
}
return fmt.Sprintf("%x", sha256.Sum256([]byte(fmt.Sprint(v)))) return fmt.Sprintf("%x", sha256.Sum256([]byte(fmt.Sprint(v))))
} }

View File

@@ -3,14 +3,10 @@
package low package low
type IsParameter interface { type SharedParameters interface {
GetName() *NodeReference[string]
GetIn() *NodeReference[string]
GetType() *NodeReference[string] GetType() *NodeReference[string]
GetDescription() *NodeReference[string] GetDescription() *NodeReference[string]
GetRequired() *NodeReference[bool]
GetDeprecated() *NodeReference[bool] GetDeprecated() *NodeReference[bool]
GetAllowEmptyValue() *NodeReference[bool]
GetFormat() *NodeReference[string] GetFormat() *NodeReference[string]
GetStyle() *NodeReference[string] GetStyle() *NodeReference[string]
GetCollectionFormat() *NodeReference[string] GetCollectionFormat() *NodeReference[string]
@@ -30,8 +26,25 @@ type IsParameter interface {
GetEnum() *NodeReference[[]ValueReference[string]] GetEnum() *NodeReference[[]ValueReference[string]]
GetMultipleOf() *NodeReference[int] GetMultipleOf() *NodeReference[int]
GetExample() *NodeReference[any] GetExample() *NodeReference[any]
GetExamples() *NodeReference[any] // requires cast
GetSchema() *NodeReference[any] // requires cast. GetSchema() *NodeReference[any] // requires cast.
GetExamples() *NodeReference[any] // requires cast
GetContent() *NodeReference[any] // requires cast. GetContent() *NodeReference[any] // requires cast.
GetItems() *NodeReference[any] // requires cast. GetItems() *NodeReference[any] // requires cast.
} }
type IsParameter interface {
GetName() *NodeReference[string]
GetIn() *NodeReference[string]
SharedParameterHeader
SharedParameters
}
type SharedParameterHeader interface {
GetRequired() *NodeReference[bool]
GetAllowEmptyValue() *NodeReference[bool]
}
type IsHeader interface {
SharedParameters
SharedParameterHeader
}

View File

@@ -82,6 +82,17 @@ func (n NodeReference[T]) IsEmpty() bool {
return n.KeyNode == nil && n.ValueNode == nil return n.KeyNode == nil && n.ValueNode == nil
} }
func (n NodeReference[T]) IsReferenceNode() bool {
for k := range n.KeyNode.Content {
if k%2 == 0 {
if n.KeyNode.Content[k].Value == "$ref" {
return true
}
}
}
return false
}
// GenerateMapKey will return a string based on the line and column number of the node, e.g. 33:56 for line 33, col 56. // GenerateMapKey will return a string based on the line and column number of the node, e.g. 33:56 for line 33, col 56.
func (n NodeReference[T]) GenerateMapKey() string { func (n NodeReference[T]) GenerateMapKey() string {
return fmt.Sprintf("%d:%d", n.ValueNode.Line, n.ValueNode.Column) return fmt.Sprintf("%d:%d", n.ValueNode.Line, n.ValueNode.Column)

View File

@@ -85,3 +85,100 @@ func (h *Header) Build(root *yaml.Node, idx *index.SpecIndex) error {
} }
return nil return nil
} }
// IsHeader compliance methods
func (h *Header) GetType() *low.NodeReference[string] {
return &h.Type
}
func (h *Header) GetDescription() *low.NodeReference[string] {
return &h.Description
}
func (h *Header) GetDeprecated() *low.NodeReference[bool] {
// not implemented.
return nil
}
func (h *Header) GetSchema() *low.NodeReference[any] {
// not implemented.
return &low.NodeReference[any]{}
}
func (h *Header) GetFormat() *low.NodeReference[string] {
return &h.Format
}
func (h *Header) GetItems() *low.NodeReference[any] {
i := low.NodeReference[any]{
KeyNode: h.Items.KeyNode,
ValueNode: h.Items.ValueNode,
Value: h.Items.KeyNode,
}
return &i
}
func (h *Header) GetStyle() *low.NodeReference[string] {
// not implemented.
return nil
}
func (h *Header) GetCollectionFormat() *low.NodeReference[string] {
return &h.CollectionFormat
}
func (h *Header) GetDefault() *low.NodeReference[any] {
return &h.Default
}
func (h *Header) GetAllowReserved() *low.NodeReference[bool] {
return nil // not implemented
}
func (h *Header) GetExplode() *low.NodeReference[bool] {
return nil // not implemented
}
func (h *Header) GetMaximum() *low.NodeReference[int] {
return &h.Maximum
}
func (h *Header) GetExclusiveMaximum() *low.NodeReference[bool] {
return &h.ExclusiveMaximum
}
func (h *Header) GetMinimum() *low.NodeReference[int] {
return &h.Minimum
}
func (h *Header) GetExclusiveMinimum() *low.NodeReference[bool] {
return &h.ExclusiveMinimum
}
func (h *Header) GetMaxLength() *low.NodeReference[int] {
return &h.MaxLength
}
func (h *Header) GetMinLength() *low.NodeReference[int] {
return &h.MinLength
}
func (h *Header) GetPattern() *low.NodeReference[string] {
return &h.Pattern
}
func (h *Header) GetMaxItems() *low.NodeReference[int] {
return &h.MaxItems
}
func (h *Header) GetMinItems() *low.NodeReference[int] {
return &h.MaxItems
}
func (h *Header) GetUniqueItems() *low.NodeReference[bool] {
return &h.UniqueItems
}
func (h *Header) GetEnum() *low.NodeReference[[]low.ValueReference[string]] {
return &h.Enum
}
func (h *Header) GetMultipleOf() *low.NodeReference[int] {
return &h.MultipleOf
}
func (h *Header) GetExample() *low.NodeReference[any] {
return nil // not implemented
}
func (h *Header) GetExamples() *low.NodeReference[any] {
return nil // not implemented
}
func (h *Header) GetContent() *low.NodeReference[any] {
return nil // not implemented
}
func (h *Header) GetRequired() *low.NodeReference[bool] {
return nil // not implemented
}
func (h *Header) GetAllowEmptyValue() *low.NodeReference[bool] {
return nil // not implemented
}

View File

@@ -16,7 +16,7 @@ import (
// Items is a low-level representation of a Swagger / OpenAPI 2 Items object. // Items is a low-level representation of a Swagger / OpenAPI 2 Items object.
// //
// Items is a limited subset of JSON-Schema's items object. It is used by parameter definitions that are not // Items is a limited subset of JSON-Schema's items object. It is used by parameter definitions that are not
// located in "body" // located in "body". Items, is actually identical to a Header, except it does not have description.
// - https://swagger.io/specification/v2/#itemsObject // - https://swagger.io/specification/v2/#itemsObject
type Items struct { type Items struct {
Type low.NodeReference[string] Type low.NodeReference[string]
@@ -120,3 +120,100 @@ func (i *Items) Build(root *yaml.Node, idx *index.SpecIndex) error {
} }
return nil return nil
} }
// IsHeader compliance methods
func (i *Items) GetType() *low.NodeReference[string] {
return &i.Type
}
func (i *Items) GetDescription() *low.NodeReference[string] {
// not implemented
return nil
}
func (i *Items) GetDeprecated() *low.NodeReference[bool] {
// not implemented.
return nil
}
func (i *Items) GetSchema() *low.NodeReference[any] {
// not implemented.
return &low.NodeReference[any]{}
}
func (i *Items) GetFormat() *low.NodeReference[string] {
return &i.Format
}
func (i *Items) GetItems() *low.NodeReference[any] {
k := low.NodeReference[any]{
KeyNode: i.Items.KeyNode,
ValueNode: i.Items.ValueNode,
Value: i.Items.KeyNode,
}
return &k
}
func (i *Items) GetStyle() *low.NodeReference[string] {
// not implemented.
return nil
}
func (i *Items) GetCollectionFormat() *low.NodeReference[string] {
return &i.CollectionFormat
}
func (i *Items) GetDefault() *low.NodeReference[any] {
return &i.Default
}
func (i *Items) GetAllowReserved() *low.NodeReference[bool] {
return nil // not implemented
}
func (i *Items) GetExplode() *low.NodeReference[bool] {
return nil // not implemented
}
func (i *Items) GetMaximum() *low.NodeReference[int] {
return &i.Maximum
}
func (i *Items) GetExclusiveMaximum() *low.NodeReference[bool] {
return &i.ExclusiveMaximum
}
func (i *Items) GetMinimum() *low.NodeReference[int] {
return &i.Minimum
}
func (i *Items) GetExclusiveMinimum() *low.NodeReference[bool] {
return &i.ExclusiveMinimum
}
func (i *Items) GetMaxLength() *low.NodeReference[int] {
return &i.MaxLength
}
func (i *Items) GetMinLength() *low.NodeReference[int] {
return &i.MinLength
}
func (i *Items) GetPattern() *low.NodeReference[string] {
return &i.Pattern
}
func (i *Items) GetMaxItems() *low.NodeReference[int] {
return &i.MaxItems
}
func (i *Items) GetMinItems() *low.NodeReference[int] {
return &i.MaxItems
}
func (i *Items) GetUniqueItems() *low.NodeReference[bool] {
return &i.UniqueItems
}
func (i *Items) GetEnum() *low.NodeReference[[]low.ValueReference[string]] {
return &i.Enum
}
func (i *Items) GetMultipleOf() *low.NodeReference[int] {
return &i.MultipleOf
}
func (i *Items) GetExample() *low.NodeReference[any] {
return nil // not implemented
}
func (i *Items) GetExamples() *low.NodeReference[any] {
return nil // not implemented
}
func (i *Items) GetContent() *low.NodeReference[any] {
return nil // not implemented
}
func (i *Items) GetAllowEmptyValue() *low.NodeReference[bool] {
return nil // not implemented, not even a property... damn you swagger.
}
func (i *Items) GetRequired() *low.NodeReference[bool] {
return nil // not implemented, not even a property... damn you swagger.
}

View File

@@ -99,4 +99,5 @@ const (
CollectionFormatLabel = "collectionFormat" CollectionFormatLabel = "collectionFormat"
AllowReservedLabel = "allowReserved" AllowReservedLabel = "allowReserved"
ExplodeLabel = "explode" ExplodeLabel = "explode"
ContentTypeLabel = "contentType"
) )

View File

@@ -52,19 +52,19 @@ func (h *Header) Hash() [32]byte {
if h.Description.Value != "" { if h.Description.Value != "" {
f = append(f, h.Description.Value) f = append(f, h.Description.Value)
} }
f = append(f, fmt.Sprint(sha256.Sum256([]byte(fmt.Sprint(h.Required.Value))))) f = append(f, fmt.Sprint(h.Required.Value))
f = append(f, fmt.Sprint(sha256.Sum256([]byte(fmt.Sprint(h.Deprecated.Value))))) f = append(f, fmt.Sprint(h.Deprecated.Value))
f = append(f, fmt.Sprint(sha256.Sum256([]byte(fmt.Sprint(h.AllowEmptyValue.Value))))) f = append(f, fmt.Sprint(h.AllowEmptyValue.Value))
if h.Style.Value != "" { if h.Style.Value != "" {
f = append(f, h.Style.Value) f = append(f, h.Style.Value)
} }
f = append(f, fmt.Sprint(sha256.Sum256([]byte(fmt.Sprint(h.Explode.Value))))) f = append(f, fmt.Sprint(h.Explode.Value))
f = append(f, fmt.Sprint(sha256.Sum256([]byte(fmt.Sprint(h.AllowReserved.Value))))) f = append(f, fmt.Sprint(h.AllowReserved.Value))
if h.Schema.Value != nil { if h.Schema.Value != nil {
f = append(f, fmt.Sprint(h.Schema.Value.Schema().Hash())) f = append(f, fmt.Sprint(h.Schema.Value.Schema().Hash()))
} }
if h.Example.Value != nil { if h.Example.Value != nil {
f = append(f, fmt.Sprint(sha256.Sum256([]byte(fmt.Sprint(h.Example.Value))))) f = append(f, fmt.Sprint(h.Example.Value))
} }
if len(h.Examples.Value) > 0 { if len(h.Examples.Value) > 0 {
for k := range h.Examples.Value { for k := range h.Examples.Value {
@@ -126,3 +126,105 @@ func (h *Header) Build(root *yaml.Node, idx *index.SpecIndex) error {
} }
return nil return nil
} }
// IsHeader compliance methods.
func (h *Header) GetType() *low.NodeReference[string] {
return nil // not implemented
}
func (h *Header) GetDescription() *low.NodeReference[string] {
return &h.Description
}
func (h *Header) GetRequired() *low.NodeReference[bool] {
return &h.Required
}
func (h *Header) GetDeprecated() *low.NodeReference[bool] {
return &h.Deprecated
}
func (h *Header) GetAllowEmptyValue() *low.NodeReference[bool] {
return &h.AllowEmptyValue
}
func (h *Header) GetSchema() *low.NodeReference[any] {
i := low.NodeReference[any]{
KeyNode: h.Schema.KeyNode,
ValueNode: h.Schema.ValueNode,
Value: h.Schema.KeyNode,
}
return &i
}
func (h *Header) GetFormat() *low.NodeReference[string] {
return nil
}
func (h *Header) GetItems() *low.NodeReference[any] {
return nil
}
func (h *Header) GetStyle() *low.NodeReference[string] {
return &h.Style
}
func (h *Header) GetCollectionFormat() *low.NodeReference[string] {
return nil
}
func (h *Header) GetDefault() *low.NodeReference[any] {
return nil
}
func (h *Header) GetAllowReserved() *low.NodeReference[bool] {
return &h.AllowReserved
}
func (h *Header) GetExplode() *low.NodeReference[bool] {
return &h.Explode
}
func (h *Header) GetMaximum() *low.NodeReference[int] {
return nil
}
func (h *Header) GetExclusiveMaximum() *low.NodeReference[bool] {
return nil
}
func (h *Header) GetMinimum() *low.NodeReference[int] {
return nil
}
func (h *Header) GetExclusiveMinimum() *low.NodeReference[bool] {
return nil
}
func (h *Header) GetMaxLength() *low.NodeReference[int] {
return nil
}
func (h *Header) GetMinLength() *low.NodeReference[int] {
return nil
}
func (h *Header) GetPattern() *low.NodeReference[string] {
return nil
}
func (h *Header) GetMaxItems() *low.NodeReference[int] {
return nil
}
func (h *Header) GetMinItems() *low.NodeReference[int] {
return nil
}
func (h *Header) GetUniqueItems() *low.NodeReference[bool] {
return nil
}
func (h *Header) GetEnum() *low.NodeReference[[]low.ValueReference[string]] {
return nil
}
func (h *Header) GetMultipleOf() *low.NodeReference[int] {
return nil
}
func (h *Header) GetExample() *low.NodeReference[any] {
return &h.Example
}
func (h *Header) GetExamples() *low.NodeReference[any] {
i := low.NodeReference[any]{
KeyNode: h.Examples.KeyNode,
ValueNode: h.Examples.ValueNode,
Value: h.Examples.KeyNode,
}
return &i
}
func (h *Header) GetContent() *low.NodeReference[any] {
c := low.NodeReference[any]{
KeyNode: h.Content.KeyNode,
ValueNode: h.Content.ValueNode,
Value: h.Content.Value,
}
return &c
}

View File

@@ -117,6 +117,8 @@ func CheckSpecificObjectAdded[T any](l, r map[string]*T, label string) bool {
// CheckPropertyAdditionOrRemoval // CheckPropertyAdditionOrRemoval
// CheckForModification // CheckForModification
func CheckProperties(properties []*PropertyCheck) { func CheckProperties(properties []*PropertyCheck) {
// todo: make this async to really speed things up.
for _, n := range properties { for _, n := range properties {
CheckPropertyAdditionOrRemoval(n.LeftNode, n.RightNode, n.Label, n.Changes, n.Breaking, n.Original, n.New) CheckPropertyAdditionOrRemoval(n.LeftNode, n.RightNode, n.Label, n.Changes, n.Breaking, n.Original, n.New)
CheckForModification(n.LeftNode, n.RightNode, n.Label, n.Changes, n.Breaking, n.Original, n.New) CheckForModification(n.LeftNode, n.RightNode, n.Label, n.Changes, n.Breaking, n.Original, n.New)

View File

@@ -37,7 +37,7 @@ func CompareDiscriminator(l, r *base.Discriminator) *DiscriminatorChanges {
dc := new(DiscriminatorChanges) dc := new(DiscriminatorChanges)
var changes []*Change var changes []*Change
var props []*PropertyCheck var props []*PropertyCheck
var mapping []*Change var mappingChanges []*Change
// Name (breaking change) // Name (breaking change)
props = append(props, &PropertyCheck{ props = append(props, &PropertyCheck{
@@ -59,11 +59,11 @@ func CompareDiscriminator(l, r *base.Discriminator) *DiscriminatorChanges {
// check for removals, modifications and moves // check for removals, modifications and moves
for i := range lMap { for i := range lMap {
CheckForObjectAdditionOrRemoval[string](lMap, rMap, i, &mapping, false, true) CheckForObjectAdditionOrRemoval[string](lMap, rMap, i, &mappingChanges, false, true)
// if the existing tag exists, let's check it. // if the existing tag exists, let's check it.
if rMap[i] != nil { if rMap[i] != nil {
if lMap[i].Value != rMap[i].Value { if lMap[i].Value != rMap[i].Value {
CreateChange(&mapping, Modified, i, lMap[i].GetValueNode(), CreateChange(&mappingChanges, Modified, i, lMap[i].GetValueNode(),
rMap[i].GetValueNode(), true, lMap[i].GetValue(), rMap[i].GetValue()) rMap[i].GetValueNode(), true, lMap[i].GetValue(), rMap[i].GetValue())
} }
} }
@@ -71,13 +71,13 @@ func CompareDiscriminator(l, r *base.Discriminator) *DiscriminatorChanges {
for i := range rMap { for i := range rMap {
if lMap[i] == nil { if lMap[i] == nil {
CreateChange(&mapping, ObjectAdded, i, nil, CreateChange(&mappingChanges, ObjectAdded, i, nil,
rMap[i].GetValueNode(), false, nil, rMap[i].GetValue()) rMap[i].GetValueNode(), false, nil, rMap[i].GetValue())
} }
} }
dc.Changes = changes dc.Changes = changes
dc.MappingChanges = mapping dc.MappingChanges = mappingChanges
if dc.TotalChanges() <= 0 { if dc.TotalChanges() <= 0 {
return nil return nil
} }

View File

@@ -3,7 +3,81 @@
package what_changed package what_changed
import v3 "github.com/pb33f/libopenapi/datamodel/low/v3"
type EncodingChanges struct { type EncodingChanges struct {
ParameterChanges ParameterChanges
HeaderChanges *HeaderChanges HeaderChanges map[string]*HeaderChanges
}
func (e *EncodingChanges) TotalChanges() int {
c := e.PropertyChanges.TotalChanges()
if e.HeaderChanges != nil {
for i := range e.HeaderChanges {
c += e.HeaderChanges[i].TotalChanges()
}
}
return c
}
func (e *EncodingChanges) TotalBreakingChanges() int {
c := e.PropertyChanges.TotalBreakingChanges()
if e.HeaderChanges != nil {
for i := range e.HeaderChanges {
c += e.HeaderChanges[i].TotalBreakingChanges()
}
}
return c
}
func CompareEncoding(l, r *v3.Encoding) *EncodingChanges {
var changes []*Change
var props []*PropertyCheck
// ContentType
props = append(props, &PropertyCheck{
LeftNode: l.ContentType.ValueNode,
RightNode: r.ContentType.ValueNode,
Label: v3.ContentTypeLabel,
Changes: &changes,
Breaking: true,
Original: l,
New: r,
})
// Explode
props = append(props, &PropertyCheck{
LeftNode: l.Explode.ValueNode,
RightNode: r.Explode.ValueNode,
Label: v3.ExplodeLabel,
Changes: &changes,
Breaking: true,
Original: l,
New: r,
})
// AllowReserved
props = append(props, &PropertyCheck{
LeftNode: l.AllowReserved.ValueNode,
RightNode: r.AllowReserved.ValueNode,
Label: v3.AllowReservedLabel,
Changes: &changes,
Breaking: false,
Original: l,
New: r,
})
// check everything.
CheckProperties(props)
ec := new(EncodingChanges)
ec.Changes = changes
// headers
ec.HeaderChanges = CheckMapForChanges(l.Headers.Value, r.Headers.Value, &changes, v3.HeadersLabel, CompareHeadersV3)
if ec.TotalChanges() <= 0 {
return nil
}
return ec
} }

View File

@@ -3,10 +3,90 @@
package what_changed package what_changed
import (
"github.com/pb33f/libopenapi/datamodel/low/v3"
)
type MediaTypeChanges struct { type MediaTypeChanges struct {
PropertyChanges PropertyChanges
SchemaChanges *SchemaChanges SchemaChanges *SchemaChanges
ExtensionChanges *ExtensionChanges ExtensionChanges *ExtensionChanges
ExampleChanges map[string]*ExampleChanges ExampleChanges map[string]*ExampleChanges
EncodingChanges *EncodingChanges EncodingChanges map[string]*EncodingChanges
}
func (m *MediaTypeChanges) TotalChanges() int {
c := m.PropertyChanges.TotalChanges()
for k := range m.ExampleChanges {
c += m.ExampleChanges[k].TotalChanges()
}
if m.SchemaChanges != nil {
c += m.SchemaChanges.TotalChanges()
}
if len(m.EncodingChanges) > 0 {
for i := range m.EncodingChanges {
c += m.EncodingChanges[i].TotalChanges()
}
}
if m.ExtensionChanges != nil {
c += m.ExtensionChanges.TotalChanges()
}
return c
}
func (m *MediaTypeChanges) TotalBreakingChanges() int {
c := m.PropertyChanges.TotalBreakingChanges()
for k := range m.ExampleChanges {
c += m.ExampleChanges[k].TotalBreakingChanges()
}
if m.SchemaChanges != nil {
c += m.SchemaChanges.TotalBreakingChanges()
}
if len(m.EncodingChanges) > 0 {
for i := range m.EncodingChanges {
c += m.EncodingChanges[i].TotalBreakingChanges()
}
}
return c
}
func CompareMediaTypes(l, r *v3.MediaType) *MediaTypeChanges {
var props []*PropertyCheck
var changes []*Change
mc := new(MediaTypeChanges)
// Example
addPropertyCheck(&props, l.Example.ValueNode, r.Example.ValueNode,
l.Example.Value, r.Example.Value, &changes, v3.ExampleLabel, false)
CheckProperties(props)
mc.Changes = changes
// schema
if !l.Schema.IsEmpty() && !r.Schema.IsEmpty() {
mc.SchemaChanges = CompareSchemas(l.Schema.Value, r.Schema.Value)
}
if !l.Schema.IsEmpty() && r.Schema.IsEmpty() {
CreateChange(&changes, ObjectRemoved, v3.SchemaLabel, l.Schema.ValueNode,
nil, true, l.Schema.Value, nil)
}
if l.Schema.IsEmpty() && !r.Schema.IsEmpty() {
CreateChange(&changes, ObjectAdded, v3.SchemaLabel, nil,
r.Schema.ValueNode, true, nil, r.Schema.Value)
}
// examples
mc.ExampleChanges = CheckMapForChanges(l.Examples.Value, r.Examples.Value,
&changes, v3.ExamplesLabel, CompareExamples)
// encoding
mc.EncodingChanges = CheckMapForChanges(l.Encoding.Value, r.Encoding.Value,
&changes, v3.EncodingLabel, CompareEncoding)
if mc.TotalChanges() <= 0 {
return nil
}
return mc
} }

View File

@@ -11,6 +11,8 @@ import (
v3 "github.com/pb33f/libopenapi/datamodel/low/v3" v3 "github.com/pb33f/libopenapi/datamodel/low/v3"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"reflect" "reflect"
"sort"
"strings"
) )
type ParameterChanges struct { type ParameterChanges struct {
@@ -19,7 +21,7 @@ type ParameterChanges struct {
ExtensionChanges *ExtensionChanges ExtensionChanges *ExtensionChanges
// V2 change types // V2 change types
// ItemsChanges ItemsChanges *ItemsChanges
// v3 change types // v3 change types
ExampleChanges map[string]*ExampleChanges ExampleChanges map[string]*ExampleChanges
@@ -84,6 +86,10 @@ func addOpenAPIParameterProperties(left, right low.IsParameter, changes *[]*Chan
addPropertyCheck(&props, left.GetDeprecated().ValueNode, right.GetDeprecated().ValueNode, addPropertyCheck(&props, left.GetDeprecated().ValueNode, right.GetDeprecated().ValueNode,
left.GetDeprecated(), right.GetDeprecated(), changes, v3.DeprecatedLabel, false) left.GetDeprecated(), right.GetDeprecated(), changes, v3.DeprecatedLabel, false)
// example
addPropertyCheck(&props, left.GetExample().ValueNode, right.GetExample().ValueNode,
left.GetExample(), right.GetExample(), changes, v3.ExampleLabel, false)
return props return props
} }
@@ -206,10 +212,46 @@ func CompareParameters(l, r any) *ParameterChanges {
rSchema = rParam.Schema.Value rSchema = rParam.Schema.Value
} }
// todo: items // items
// todo: default if !lParam.Items.IsEmpty() && !rParam.Items.IsEmpty() {
// todo: enums if lParam.Items.Value.Hash() != rParam.Items.Value.Hash() {
pc.ItemsChanges = CompareItems(lParam.Items.Value, rParam.Items.Value)
}
}
if lParam.Items.IsEmpty() && !rParam.Items.IsEmpty() {
CreateChange(&changes, ObjectAdded, v3.ItemsLabel,
nil, rParam.Items.ValueNode, true, nil,
rParam.Items.Value)
}
if !lParam.Items.IsEmpty() && rParam.Items.IsEmpty() {
CreateChange(&changes, ObjectRemoved, v3.ItemsLabel,
lParam.Items.ValueNode, nil, true, lParam.Items.Value,
nil)
}
// default
if !lParam.Default.IsEmpty() && !rParam.Default.IsEmpty() {
if low.GenerateHashString(lParam.Default.Value) != low.GenerateHashString(lParam.Default.Value) {
CreateChange(&changes, Modified, v3.DefaultLabel,
lParam.Items.ValueNode, rParam.Items.ValueNode, true, lParam.Items.Value,
rParam.Items.ValueNode)
}
}
if lParam.Default.IsEmpty() && !rParam.Default.IsEmpty() {
CreateChange(&changes, ObjectAdded, v3.DefaultLabel,
nil, rParam.Default.ValueNode, true, nil,
rParam.Default.Value)
}
if !lParam.Default.IsEmpty() && rParam.Items.IsEmpty() {
CreateChange(&changes, ObjectRemoved, v3.ItemsLabel,
lParam.Items.ValueNode, nil, true, lParam.Items.Value,
nil)
}
// enum
if len(lParam.Enum.Value) > 0 || len(rParam.Enum.Value) > 0 {
ExtractStringValueSliceChanges(lParam.Enum.Value, rParam.Enum.Value, &changes, v3.EnumLabel)
}
} }
// OpenAPI // OpenAPI
@@ -235,28 +277,10 @@ func CompareParameters(l, r any) *ParameterChanges {
} }
// example // example
if lParam.Example.Value != nil && rParam.Example.Value != nil { checkParameterExample(lParam.Example, rParam.Example, changes)
if low.GenerateHashString(lParam.Example.Value) != low.GenerateHashString(rParam.Example.Value) {
CreateChange(&changes, Modified, v3.ExampleLabel,
lParam.Example.GetValueNode(), rParam.Example.GetValueNode(), false,
lParam.Example.GetValue(), rParam.Example.GetValue())
}
}
if lParam.Example.Value == nil && rParam.Example.Value != nil {
CreateChange(&changes, PropertyAdded, v3.ExampleLabel,
nil, rParam.Example.GetValueNode(), false,
nil, rParam.Example.GetValue())
}
if lParam.Example.Value != nil && rParam.Example.Value == nil {
CreateChange(&changes, PropertyRemoved, v3.ExampleLabel,
lParam.Example.GetValueNode(), nil, false,
lParam.Example.GetValue(), nil)
}
// examples // examples
checkParameterExamples(lParam, rParam, changes, pc) CheckMapForChanges(lParam.Examples.Value, rParam.Examples.Value, &changes, v3.ExamplesLabel, CompareExamples)
// todo: content // todo: content
@@ -287,54 +311,125 @@ func CompareParameters(l, r any) *ParameterChanges {
return nil return nil
} }
func checkParameterExamples(lParam *v3.Parameter, rParam *v3.Parameter, changes []*Change, pc *ParameterChanges) { func ExtractStringValueSliceChanges(lParam, rParam []low.ValueReference[string], changes *[]*Change, label string) {
lExpHashes := make(map[string]string) lKeys := make([]string, len(lParam))
rExpHashes := make(map[string]string) rKeys := make([]string, len(rParam))
lExpValues := make(map[string]low.ValueReference[*base.Example]) lValues := make(map[string]low.ValueReference[string])
rExpValues := make(map[string]low.ValueReference[*base.Example]) rValues := make(map[string]low.ValueReference[string])
if lParam != nil && lParam.Examples.Value != nil { for i := range lParam {
for k := range lParam.Examples.Value { lKeys[i] = strings.ToLower(lParam[i].Value)
lExpHashes[k.Value] = fmt.Sprintf("%x", lParam.Examples.Value[k].Value.Hash()) lValues[lKeys[i]] = lParam[i]
lExpValues[k.Value] = lParam.Examples.Value[k] }
for i := range rParam {
rKeys[i] = strings.ToLower(rParam[i].Value)
rValues[lKeys[i]] = rParam[i]
}
sort.Strings(lKeys)
sort.Strings(rKeys)
for i := range lKeys {
if i < len(rKeys) {
if lKeys[i] != rKeys[i] {
CreateChange(changes, Modified, label,
lValues[lKeys[i]].ValueNode,
rValues[rKeys[i]].ValueNode,
true,
lValues[lKeys[i]].Value,
rValues[rKeys[i]].ValueNode)
}
continue
}
if i >= len(rKeys) {
CreateChange(changes, PropertyRemoved, label,
lValues[lKeys[i]].ValueNode,
nil,
true,
lValues[lKeys[i]].Value,
nil)
} }
} }
if rParam != nil && rParam.Examples.Value != nil { for i := range rKeys {
for k := range rParam.Examples.Value { if i >= len(lKeys) {
rExpHashes[k.Value] = fmt.Sprintf("%x", rParam.Examples.Value[k].Value.Hash()) CreateChange(changes, PropertyAdded, label,
rExpValues[k.Value] = rParam.Examples.Value[k] nil,
rValues[rKeys[i]].ValueNode,
false,
nil,
rValues[rKeys[i]].ValueNode)
} }
} }
expChanges := make(map[string]*ExampleChanges) }
func checkParameterExample(expLeft, expRight low.NodeReference[any], changes []*Change) {
if !expLeft.IsEmpty() && !expRight.IsEmpty() {
if low.GenerateHashString(expLeft.GetValue()) != low.GenerateHashString(expRight.GetValue()) {
CreateChange(&changes, Modified, v3.ExampleLabel,
expLeft.GetValueNode(), expRight.GetValueNode(), false,
expLeft.GetValue(), expRight.GetValue())
}
}
if expLeft.Value == nil && expRight.Value != nil {
CreateChange(&changes, PropertyAdded, v3.ExampleLabel,
nil, expRight.GetValueNode(), false,
nil, expRight.GetValue())
}
if expLeft.Value != nil && expRight.Value == nil {
CreateChange(&changes, PropertyRemoved, v3.ExampleLabel,
expLeft.GetValueNode(), nil, false,
expLeft.GetValue(), nil)
}
}
func CheckMapForChanges[T any, R any](expLeft, expRight map[low.KeyReference[string]]low.ValueReference[T],
changes *[]*Change, label string, compareFunc func(l, r T) R) map[string]R {
lHashes := make(map[string]string)
rHashes := make(map[string]string)
lValues := make(map[string]low.ValueReference[T])
rValues := make(map[string]low.ValueReference[T])
for k := range expLeft {
lHashes[k.Value] = fmt.Sprintf("%x", low.GenerateHashString(expLeft[k].Value))
lValues[k.Value] = expLeft[k]
}
for k := range expRight {
rHashes[k.Value] = fmt.Sprintf("%x", low.GenerateHashString(expRight[k].Value))
rValues[k.Value] = expRight[k]
}
expChanges := make(map[string]R)
// check left example hashes // check left example hashes
for k := range lExpHashes { for k := range lHashes {
rhash := rExpHashes[k] rhash := rHashes[k]
if rhash == "" { if rhash == "" {
CreateChange(&changes, ObjectRemoved, v3.ExamplesLabel, CreateChange(changes, ObjectRemoved, label,
lExpValues[k].GetValueNode(), nil, false, lValues[k].GetValueNode(), nil, false,
lExpValues[k].GetValue(), nil) lValues[k].GetValue(), nil)
continue continue
} }
if lExpHashes[k] == rExpHashes[k] { if lHashes[k] == rHashes[k] {
continue continue
} }
expChanges[k] = CompareExamples(lExpValues[k].Value, rExpValues[k].Value) // run comparison.
expChanges[k] = compareFunc(lValues[k].Value, rValues[k].Value)
} }
//check right example hashes //check right example hashes
for k := range rExpHashes { for k := range rHashes {
lhash := lExpHashes[k] lhash := lHashes[k]
if lhash == "" { if lhash == "" {
CreateChange(&changes, ObjectAdded, v3.ExamplesLabel, CreateChange(changes, ObjectAdded, v3.ExamplesLabel,
nil, lExpValues[k].GetValueNode(), false, nil, lValues[k].GetValueNode(), false,
nil, lExpValues[k].GetValue()) nil, lValues[k].GetValue())
continue continue
} }
} }
if len(expChanges) > 0 { return expChanges
pc.ExampleChanges = expChanges
}
} }
func checkParameterContent(lParam *v3.Parameter, rParam *v3.Parameter, changes []*Change, pc *ParameterChanges) { func checkParameterContent(lParam *v3.Parameter, rParam *v3.Parameter, changes []*Change, pc *ParameterChanges) {

View File

@@ -4,9 +4,11 @@
package what_changed package what_changed
import ( import (
"fmt"
"github.com/pb33f/libopenapi/datamodel" "github.com/pb33f/libopenapi/datamodel"
"github.com/pb33f/libopenapi/datamodel/low" "github.com/pb33f/libopenapi/datamodel/low"
"github.com/pb33f/libopenapi/datamodel/low/base" "github.com/pb33f/libopenapi/datamodel/low/base"
v2 "github.com/pb33f/libopenapi/datamodel/low/v2"
v3 "github.com/pb33f/libopenapi/datamodel/low/v3" v3 "github.com/pb33f/libopenapi/datamodel/low/v3"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"testing" "testing"
@@ -150,6 +152,25 @@ func test_BuildDoc(l, r string) (*v3.Document, *v3.Document) {
return leftDoc, rightDoc return leftDoc, rightDoc
} }
func test_BuildDocv2(l, r string) (*v2.Swagger, *v2.Swagger) {
leftInfo, _ := datamodel.ExtractSpecInfo([]byte(l))
rightInfo, _ := datamodel.ExtractSpecInfo([]byte(r))
var err []error
var leftDoc, rightDoc *v2.Swagger
leftDoc, err = v2.CreateDocument(leftInfo)
rightDoc, err = v2.CreateDocument(rightInfo)
if len(err) > 0 {
for i := range err {
fmt.Printf("error: %v\n", err[i])
}
panic("failed to create doc")
}
return leftDoc, rightDoc
}
func TestCompareSchemas_RefIgnore(t *testing.T) { func TestCompareSchemas_RefIgnore(t *testing.T) {
left := `components: left := `components:
schemas: schemas: