From b42e35f2b76d03c31c770f36a592397d8a87c070 Mon Sep 17 00:00:00 2001 From: Dave Shanley Date: Thu, 20 Oct 2022 09:27:24 -0400 Subject: [PATCH] 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. --- datamodel/low/extraction_functions.go | 16 +- datamodel/low/model_interfaces.go | 25 +++- datamodel/low/reference.go | 11 ++ datamodel/low/v2/header.go | 97 +++++++++++++ datamodel/low/v2/items.go | 99 ++++++++++++- datamodel/low/v3/constants.go | 1 + datamodel/low/v3/header.go | 114 ++++++++++++++- what-changed/comparison_functions.go | 2 + what-changed/discriminator.go | 10 +- what-changed/encoding.go | 78 +++++++++- what-changed/media_type.go | 82 ++++++++++- what-changed/parameter.go | 201 +++++++++++++++++++------- what-changed/schema_test.go | 21 +++ 13 files changed, 678 insertions(+), 79 deletions(-) diff --git a/datamodel/low/extraction_functions.go b/datamodel/low/extraction_functions.go index bd662fd..3b88c86 100644 --- a/datamodel/low/extraction_functions.go +++ b/datamodel/low/extraction_functions.go @@ -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", - root.Value, root.Line, root.Column) + rv, root.Line, root.Column) } return nil, nil } @@ -122,7 +122,7 @@ func ExtractObjectRaw[T Buildable[N], N any](root *yaml.Node, idx *index.SpecInd } } else { 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 { 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 { @@ -172,7 +172,7 @@ func ExtractObject[T Buildable[N], N any](label string, root *yaml.Node, idx *in } } else { 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() } -// 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 { + 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)))) } diff --git a/datamodel/low/model_interfaces.go b/datamodel/low/model_interfaces.go index 5c734d0..5a636d9 100644 --- a/datamodel/low/model_interfaces.go +++ b/datamodel/low/model_interfaces.go @@ -3,14 +3,10 @@ package low -type IsParameter interface { - GetName() *NodeReference[string] - GetIn() *NodeReference[string] +type SharedParameters interface { GetType() *NodeReference[string] GetDescription() *NodeReference[string] - GetRequired() *NodeReference[bool] GetDeprecated() *NodeReference[bool] - GetAllowEmptyValue() *NodeReference[bool] GetFormat() *NodeReference[string] GetStyle() *NodeReference[string] GetCollectionFormat() *NodeReference[string] @@ -30,8 +26,25 @@ type IsParameter interface { GetEnum() *NodeReference[[]ValueReference[string]] GetMultipleOf() *NodeReference[int] GetExample() *NodeReference[any] - GetExamples() *NodeReference[any] // requires cast GetSchema() *NodeReference[any] // requires cast. + GetExamples() *NodeReference[any] // requires cast GetContent() *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 +} diff --git a/datamodel/low/reference.go b/datamodel/low/reference.go index d3fc2aa..f18512c 100644 --- a/datamodel/low/reference.go +++ b/datamodel/low/reference.go @@ -82,6 +82,17 @@ func (n NodeReference[T]) IsEmpty() bool { 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. func (n NodeReference[T]) GenerateMapKey() string { return fmt.Sprintf("%d:%d", n.ValueNode.Line, n.ValueNode.Column) diff --git a/datamodel/low/v2/header.go b/datamodel/low/v2/header.go index d00b471..7ed7c47 100644 --- a/datamodel/low/v2/header.go +++ b/datamodel/low/v2/header.go @@ -85,3 +85,100 @@ func (h *Header) Build(root *yaml.Node, idx *index.SpecIndex) error { } 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 +} diff --git a/datamodel/low/v2/items.go b/datamodel/low/v2/items.go index 52dbab2..b0b28a0 100644 --- a/datamodel/low/v2/items.go +++ b/datamodel/low/v2/items.go @@ -16,7 +16,7 @@ import ( // 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 -// 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 type Items struct { Type low.NodeReference[string] @@ -120,3 +120,100 @@ func (i *Items) Build(root *yaml.Node, idx *index.SpecIndex) error { } 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. +} diff --git a/datamodel/low/v3/constants.go b/datamodel/low/v3/constants.go index 80a2703..2981a4e 100644 --- a/datamodel/low/v3/constants.go +++ b/datamodel/low/v3/constants.go @@ -99,4 +99,5 @@ const ( CollectionFormatLabel = "collectionFormat" AllowReservedLabel = "allowReserved" ExplodeLabel = "explode" + ContentTypeLabel = "contentType" ) diff --git a/datamodel/low/v3/header.go b/datamodel/low/v3/header.go index cde5691..4bdbacc 100644 --- a/datamodel/low/v3/header.go +++ b/datamodel/low/v3/header.go @@ -52,19 +52,19 @@ func (h *Header) Hash() [32]byte { if 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(sha256.Sum256([]byte(fmt.Sprint(h.Deprecated.Value))))) - f = append(f, fmt.Sprint(sha256.Sum256([]byte(fmt.Sprint(h.AllowEmptyValue.Value))))) + f = append(f, fmt.Sprint(h.Required.Value)) + f = append(f, fmt.Sprint(h.Deprecated.Value)) + f = append(f, fmt.Sprint(h.AllowEmptyValue.Value)) if 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(sha256.Sum256([]byte(fmt.Sprint(h.AllowReserved.Value))))) + f = append(f, fmt.Sprint(h.Explode.Value)) + f = append(f, fmt.Sprint(h.AllowReserved.Value)) if h.Schema.Value != nil { f = append(f, fmt.Sprint(h.Schema.Value.Schema().Hash())) } 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 { for k := range h.Examples.Value { @@ -126,3 +126,105 @@ func (h *Header) Build(root *yaml.Node, idx *index.SpecIndex) error { } 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 +} diff --git a/what-changed/comparison_functions.go b/what-changed/comparison_functions.go index 5ae5a1e..5ca061b 100644 --- a/what-changed/comparison_functions.go +++ b/what-changed/comparison_functions.go @@ -117,6 +117,8 @@ func CheckSpecificObjectAdded[T any](l, r map[string]*T, label string) bool { // CheckPropertyAdditionOrRemoval // CheckForModification func CheckProperties(properties []*PropertyCheck) { + + // todo: make this async to really speed things up. for _, n := range properties { 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) diff --git a/what-changed/discriminator.go b/what-changed/discriminator.go index 30c51e6..9616dd8 100644 --- a/what-changed/discriminator.go +++ b/what-changed/discriminator.go @@ -37,7 +37,7 @@ func CompareDiscriminator(l, r *base.Discriminator) *DiscriminatorChanges { dc := new(DiscriminatorChanges) var changes []*Change var props []*PropertyCheck - var mapping []*Change + var mappingChanges []*Change // Name (breaking change) props = append(props, &PropertyCheck{ @@ -59,11 +59,11 @@ func CompareDiscriminator(l, r *base.Discriminator) *DiscriminatorChanges { // check for removals, modifications and moves 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 rMap[i] != nil { 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()) } } @@ -71,13 +71,13 @@ func CompareDiscriminator(l, r *base.Discriminator) *DiscriminatorChanges { for i := range rMap { if lMap[i] == nil { - CreateChange(&mapping, ObjectAdded, i, nil, + CreateChange(&mappingChanges, ObjectAdded, i, nil, rMap[i].GetValueNode(), false, nil, rMap[i].GetValue()) } } dc.Changes = changes - dc.MappingChanges = mapping + dc.MappingChanges = mappingChanges if dc.TotalChanges() <= 0 { return nil } diff --git a/what-changed/encoding.go b/what-changed/encoding.go index 9622cd0..05f7a2a 100644 --- a/what-changed/encoding.go +++ b/what-changed/encoding.go @@ -3,7 +3,81 @@ package what_changed +import v3 "github.com/pb33f/libopenapi/datamodel/low/v3" + type EncodingChanges struct { - ParameterChanges - HeaderChanges *HeaderChanges + ParameterChanges + 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 } diff --git a/what-changed/media_type.go b/what-changed/media_type.go index a5802fb..8266f45 100644 --- a/what-changed/media_type.go +++ b/what-changed/media_type.go @@ -3,10 +3,90 @@ package what_changed +import ( + "github.com/pb33f/libopenapi/datamodel/low/v3" +) + type MediaTypeChanges struct { PropertyChanges SchemaChanges *SchemaChanges ExtensionChanges *ExtensionChanges 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 } diff --git a/what-changed/parameter.go b/what-changed/parameter.go index 723ffbb..48b067b 100644 --- a/what-changed/parameter.go +++ b/what-changed/parameter.go @@ -11,6 +11,8 @@ import ( v3 "github.com/pb33f/libopenapi/datamodel/low/v3" "gopkg.in/yaml.v3" "reflect" + "sort" + "strings" ) type ParameterChanges struct { @@ -19,7 +21,7 @@ type ParameterChanges struct { ExtensionChanges *ExtensionChanges // V2 change types - // ItemsChanges + ItemsChanges *ItemsChanges // v3 change types ExampleChanges map[string]*ExampleChanges @@ -84,6 +86,10 @@ func addOpenAPIParameterProperties(left, right low.IsParameter, changes *[]*Chan addPropertyCheck(&props, left.GetDeprecated().ValueNode, right.GetDeprecated().ValueNode, 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 } @@ -206,10 +212,46 @@ func CompareParameters(l, r any) *ParameterChanges { rSchema = rParam.Schema.Value } - // todo: items - // todo: default - // todo: enums + // items + if !lParam.Items.IsEmpty() && !rParam.Items.IsEmpty() { + 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 @@ -235,28 +277,10 @@ func CompareParameters(l, r any) *ParameterChanges { } // example - if lParam.Example.Value != nil && rParam.Example.Value != nil { - 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) - - } + checkParameterExample(lParam.Example, rParam.Example, changes) // examples - checkParameterExamples(lParam, rParam, changes, pc) + CheckMapForChanges(lParam.Examples.Value, rParam.Examples.Value, &changes, v3.ExamplesLabel, CompareExamples) // todo: content @@ -287,54 +311,125 @@ func CompareParameters(l, r any) *ParameterChanges { return nil } -func checkParameterExamples(lParam *v3.Parameter, rParam *v3.Parameter, changes []*Change, pc *ParameterChanges) { - lExpHashes := make(map[string]string) - rExpHashes := make(map[string]string) - lExpValues := make(map[string]low.ValueReference[*base.Example]) - rExpValues := make(map[string]low.ValueReference[*base.Example]) - if lParam != nil && lParam.Examples.Value != nil { - for k := range lParam.Examples.Value { - lExpHashes[k.Value] = fmt.Sprintf("%x", lParam.Examples.Value[k].Value.Hash()) - lExpValues[k.Value] = lParam.Examples.Value[k] +func ExtractStringValueSliceChanges(lParam, rParam []low.ValueReference[string], changes *[]*Change, label string) { + lKeys := make([]string, len(lParam)) + rKeys := make([]string, len(rParam)) + lValues := make(map[string]low.ValueReference[string]) + rValues := make(map[string]low.ValueReference[string]) + for i := range lParam { + lKeys[i] = strings.ToLower(lParam[i].Value) + lValues[lKeys[i]] = lParam[i] + } + 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 k := range rParam.Examples.Value { - rExpHashes[k.Value] = fmt.Sprintf("%x", rParam.Examples.Value[k].Value.Hash()) - rExpValues[k.Value] = rParam.Examples.Value[k] + for i := range rKeys { + if i >= len(lKeys) { + CreateChange(changes, PropertyAdded, label, + 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 - for k := range lExpHashes { - rhash := rExpHashes[k] + for k := range lHashes { + rhash := rHashes[k] if rhash == "" { - CreateChange(&changes, ObjectRemoved, v3.ExamplesLabel, - lExpValues[k].GetValueNode(), nil, false, - lExpValues[k].GetValue(), nil) + CreateChange(changes, ObjectRemoved, label, + lValues[k].GetValueNode(), nil, false, + lValues[k].GetValue(), nil) continue } - if lExpHashes[k] == rExpHashes[k] { + if lHashes[k] == rHashes[k] { 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 - for k := range rExpHashes { - lhash := lExpHashes[k] + for k := range rHashes { + lhash := lHashes[k] if lhash == "" { - CreateChange(&changes, ObjectAdded, v3.ExamplesLabel, - nil, lExpValues[k].GetValueNode(), false, - nil, lExpValues[k].GetValue()) + CreateChange(changes, ObjectAdded, v3.ExamplesLabel, + nil, lValues[k].GetValueNode(), false, + nil, lValues[k].GetValue()) continue } } - if len(expChanges) > 0 { - pc.ExampleChanges = expChanges - } + return expChanges } func checkParameterContent(lParam *v3.Parameter, rParam *v3.Parameter, changes []*Change, pc *ParameterChanges) { diff --git a/what-changed/schema_test.go b/what-changed/schema_test.go index 4948971..24f8c48 100644 --- a/what-changed/schema_test.go +++ b/what-changed/schema_test.go @@ -4,9 +4,11 @@ package what_changed import ( + "fmt" "github.com/pb33f/libopenapi/datamodel" "github.com/pb33f/libopenapi/datamodel/low" "github.com/pb33f/libopenapi/datamodel/low/base" + v2 "github.com/pb33f/libopenapi/datamodel/low/v2" v3 "github.com/pb33f/libopenapi/datamodel/low/v3" "github.com/stretchr/testify/assert" "testing" @@ -150,6 +152,25 @@ func test_BuildDoc(l, r string) (*v3.Document, *v3.Document) { 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) { left := `components: schemas: