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: