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",
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))))
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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