From 61f99b8fd66ade6ecc00077a9ef4a550bc8a2db3 Mon Sep 17 00:00:00 2001 From: Dave Shanley Date: Fri, 11 Nov 2022 10:31:40 -0500 Subject: [PATCH] Replacing extensions hash code **breaking change** This is a large update, I realized that extensions are not being hashed correctly, and because I have the same code everywhere, it means running back through the stack and cleaning up the invalid code that will break if multiple extensions are used in different positions in the raw spec. At the same time, I realized that the v2 model has the same primitive/enum issues that are part cleaned up in v3. This is a breaking changhe because enums are now []any and not []string, as well as primitives for bool, int etc are all pointers now instead of the copied values. This will break any consumers. --- datamodel/high/v2/header.go | 4 +- datamodel/high/v2/items.go | 8 +- datamodel/high/v2/parameter.go | 52 ++++++------ datamodel/high/v2/swagger_test.go | 20 ++--- datamodel/low/base/contact.go | 17 ++++ datamodel/low/base/contact_test.go | 34 ++++++++ datamodel/low/base/example.go | 8 +- datamodel/low/base/external_doc.go | 14 +++- datamodel/low/base/info.go | 37 +++++++++ datamodel/low/base/info_test.go | 42 ++++++++++ datamodel/low/base/license.go | 14 ++++ datamodel/low/base/license_test.go | 33 ++++++++ datamodel/low/base/schema.go | 59 ++++++++++---- datamodel/low/base/schema_test.go | 92 ++++++++++++++++++++-- datamodel/low/base/tag.go | 27 +++++++ datamodel/low/base/tag_test.go | 30 +++++++ datamodel/low/base/xml.go | 33 +++++--- datamodel/low/model_interfaces.go | 4 +- datamodel/low/v2/header.go | 29 ++++--- datamodel/low/v2/items.go | 4 +- datamodel/low/v2/operation.go | 8 +- datamodel/low/v2/parameter.go | 4 +- datamodel/low/v3/path_item_test.go | 82 +++++++++++++++++++ datamodel/low/v3/paths_test.go | 46 +++++++++++ datamodel/low/v3/request_body_test.go | 46 +++++++++++ datamodel/low/v3/response.go | 31 ++++++-- datamodel/low/v3/response_test.go | 61 ++++++++++++++ what-changed/model/comparison_functions.go | 38 +++++++++ what-changed/model/header.go | 2 +- what-changed/model/parameter.go | 2 +- 30 files changed, 782 insertions(+), 99 deletions(-) create mode 100644 datamodel/low/base/contact_test.go create mode 100644 datamodel/low/base/license_test.go create mode 100644 datamodel/low/v3/path_item_test.go diff --git a/datamodel/high/v2/header.go b/datamodel/high/v2/header.go index 415b938..157f834 100644 --- a/datamodel/high/v2/header.go +++ b/datamodel/high/v2/header.go @@ -28,7 +28,7 @@ type Header struct { MaxItems int MinItems int UniqueItems bool - Enum []string + Enum []any MultipleOf int Extensions map[string]any low *low.Header @@ -88,7 +88,7 @@ func NewHeader(header *low.Header) *Header { h.UniqueItems = header.UniqueItems.IsEmpty() } if !header.Enum.IsEmpty() { - var enums []string + var enums []any for e := range header.Enum.Value { enums = append(enums, header.Enum.Value[e].Value) } diff --git a/datamodel/high/v2/items.go b/datamodel/high/v2/items.go index 88a73c2..cb07b53 100644 --- a/datamodel/high/v2/items.go +++ b/datamodel/high/v2/items.go @@ -3,7 +3,9 @@ package v2 -import low "github.com/pb33f/libopenapi/datamodel/low/v2" +import ( + low "github.com/pb33f/libopenapi/datamodel/low/v2" +) // Items is a high-level representation of a Swagger / OpenAPI 2 Items object, backed by a low level one. // Items is a limited subset of JSON-Schema's items object. It is used by parameter definitions that are not @@ -25,7 +27,7 @@ type Items struct { MaxItems int MinItems int UniqueItems bool - Enum []string + Enum []any MultipleOf int low *low.Items } @@ -80,7 +82,7 @@ func NewItems(items *low.Items) *Items { i.UniqueItems = items.UniqueItems.Value } if !items.Enum.IsEmpty() { - var enums []string + var enums []any for e := range items.Enum.Value { enums = append(enums, items.Enum.Value[e].Value) } diff --git a/datamodel/high/v2/parameter.go b/datamodel/high/v2/parameter.go index ee40476..e233274 100644 --- a/datamodel/high/v2/parameter.go +++ b/datamodel/high/v2/parameter.go @@ -46,24 +46,24 @@ type Parameter struct { Type string Format string Description string - Required bool - AllowEmptyValue bool + Required *bool + AllowEmptyValue *bool Schema *base.SchemaProxy Items *Items CollectionFormat string Default any - Maximum int - ExclusiveMaximum bool - Minimum int - ExclusiveMinimum bool - MaxLength int - MinLength int + Maximum *int + ExclusiveMaximum *bool + Minimum *int + ExclusiveMinimum *bool + MaxLength *int + MinLength *int Pattern string - MaxItems int - MinItems int - UniqueItems bool - Enum []string - MultipleOf int + MaxItems *int + MinItems *int + UniqueItems *bool + Enum []any + MultipleOf *int Extensions map[string]any low *low.Parameter } @@ -89,10 +89,10 @@ func NewParameter(parameter *low.Parameter) *Parameter { p.Description = parameter.Description.Value } if !parameter.Required.IsEmpty() { - p.Required = parameter.Required.Value + p.Required = ¶meter.Required.Value } if !parameter.AllowEmptyValue.IsEmpty() { - p.AllowEmptyValue = parameter.AllowEmptyValue.Value + p.AllowEmptyValue = ¶meter.AllowEmptyValue.Value } if !parameter.Schema.IsEmpty() { p.Schema = base.NewSchemaProxy(¶meter.Schema) @@ -107,44 +107,44 @@ func NewParameter(parameter *low.Parameter) *Parameter { p.Default = parameter.Default.Value } if !parameter.Maximum.IsEmpty() { - p.Maximum = parameter.Maximum.Value + p.Maximum = ¶meter.Maximum.Value } if !parameter.ExclusiveMaximum.IsEmpty() { - p.ExclusiveMaximum = parameter.ExclusiveMaximum.Value + p.ExclusiveMaximum = ¶meter.ExclusiveMaximum.Value } if !parameter.Minimum.IsEmpty() { - p.Minimum = parameter.Minimum.Value + p.Minimum = ¶meter.Minimum.Value } if !parameter.ExclusiveMinimum.IsEmpty() { - p.ExclusiveMinimum = parameter.ExclusiveMinimum.Value + p.ExclusiveMinimum = ¶meter.ExclusiveMinimum.Value } if !parameter.MaxLength.IsEmpty() { - p.MaxLength = parameter.MaxLength.Value + p.MaxLength = ¶meter.MaxLength.Value } if !parameter.MinLength.IsEmpty() { - p.MinLength = parameter.MinLength.Value + p.MinLength = ¶meter.MinLength.Value } if !parameter.Pattern.IsEmpty() { p.Pattern = parameter.Pattern.Value } if !parameter.MinItems.IsEmpty() { - p.MinItems = parameter.MinItems.Value + p.MinItems = ¶meter.MinItems.Value } if !parameter.MaxItems.IsEmpty() { - p.MaxItems = parameter.MaxItems.Value + p.MaxItems = ¶meter.MaxItems.Value } if !parameter.UniqueItems.IsEmpty() { - p.UniqueItems = parameter.UniqueItems.Value + p.UniqueItems = ¶meter.UniqueItems.Value } if !parameter.Enum.IsEmpty() { - var enums []string + var enums []any for e := range parameter.Enum.Value { enums = append(enums, parameter.Enum.Value[e].Value) } p.Enum = enums } if !parameter.MultipleOf.IsEmpty() { - p.MultipleOf = parameter.MultipleOf.Value + p.MultipleOf = ¶meter.MultipleOf.Value } return p } diff --git a/datamodel/high/v2/swagger_test.go b/datamodel/high/v2/swagger_test.go index 472faee..6a3cab3 100644 --- a/datamodel/high/v2/swagger_test.go +++ b/datamodel/high/v2/swagger_test.go @@ -222,19 +222,19 @@ func TestNewSwaggerDocument_Paths(t *testing.T) { assert.Equal(t, "petId", upload.Parameters[0].Name) assert.Equal(t, "path", upload.Parameters[0].In) assert.Equal(t, "ID of pet to update", upload.Parameters[0].Description) - assert.True(t, upload.Parameters[0].Required) + assert.True(t, *upload.Parameters[0].Required) assert.Equal(t, "integer", upload.Parameters[0].Type) assert.Equal(t, "int64", upload.Parameters[0].Format) - assert.True(t, upload.Parameters[0].ExclusiveMaximum) - assert.True(t, upload.Parameters[0].ExclusiveMinimum) - assert.Equal(t, 2, upload.Parameters[0].MaxLength) - assert.Equal(t, 1, upload.Parameters[0].MinLength) - assert.Equal(t, 1, upload.Parameters[0].Minimum) - assert.Equal(t, 5, upload.Parameters[0].Maximum) + assert.True(t, *upload.Parameters[0].ExclusiveMaximum) + assert.True(t, *upload.Parameters[0].ExclusiveMinimum) + assert.Equal(t, 2, *upload.Parameters[0].MaxLength) + assert.Equal(t, 1, *upload.Parameters[0].MinLength) + assert.Equal(t, 1, *upload.Parameters[0].Minimum) + assert.Equal(t, 5, *upload.Parameters[0].Maximum) assert.Equal(t, "hi!", upload.Parameters[0].Pattern) - assert.Equal(t, 1, upload.Parameters[0].MinItems) - assert.Equal(t, 20, upload.Parameters[0].MaxItems) - assert.True(t, upload.Parameters[0].UniqueItems) + assert.Equal(t, 1, *upload.Parameters[0].MinItems) + assert.Equal(t, 20, *upload.Parameters[0].MaxItems) + assert.True(t, *upload.Parameters[0].UniqueItems) assert.Len(t, upload.Parameters[0].Enum, 2) assert.Equal(t, "hello", upload.Parameters[0].Enum[0]) def := upload.Parameters[0].Default.(map[string]interface{}) diff --git a/datamodel/low/base/contact.go b/datamodel/low/base/contact.go index 5e33c6a..3717270 100644 --- a/datamodel/low/base/contact.go +++ b/datamodel/low/base/contact.go @@ -4,9 +4,11 @@ package base import ( + "crypto/sha256" "github.com/pb33f/libopenapi/datamodel/low" "github.com/pb33f/libopenapi/index" "gopkg.in/yaml.v3" + "strings" ) // Contact represents a low-level representation of the Contact definitions found at @@ -23,3 +25,18 @@ func (c *Contact) Build(root *yaml.Node, idx *index.SpecIndex) error { // not implemented. return nil } + +// Hash will return a consistent SHA256 Hash of the Contact object +func (c *Contact) Hash() [32]byte { + var f []string + if !c.Name.IsEmpty() { + f = append(f, c.Name.Value) + } + if !c.URL.IsEmpty() { + f = append(f, c.URL.Value) + } + if !c.Email.IsEmpty() { + f = append(f, c.Email.Value) + } + return sha256.Sum256([]byte(strings.Join(f, "|"))) +} diff --git a/datamodel/low/base/contact_test.go b/datamodel/low/base/contact_test.go new file mode 100644 index 0000000..2236398 --- /dev/null +++ b/datamodel/low/base/contact_test.go @@ -0,0 +1,34 @@ +// Copyright 2022 Princess B33f Heavy Industries / Dave Shanley +// SPDX-License-Identifier: MIT + +package base + +import ( + "github.com/pb33f/libopenapi/datamodel/low" + "github.com/stretchr/testify/assert" + "gopkg.in/yaml.v3" + "testing" +) + +func TestContact_Hash(t *testing.T) { + + left := `url: https://pb33f.io +description: the ranch +email: buckaroo@pb33f.io` + + right := `url: https://pb33f.io +description: the ranch +email: buckaroo@pb33f.io` + + var lNode, rNode yaml.Node + _ = yaml.Unmarshal([]byte(left), &lNode) + _ = yaml.Unmarshal([]byte(right), &rNode) + + // create low level objects + var lDoc Contact + var rDoc Contact + _ = low.BuildModel(lNode.Content[0], &lDoc) + _ = low.BuildModel(rNode.Content[0], &rDoc) + + assert.Equal(t, lDoc.Hash(), rDoc.Hash()) +} diff --git a/datamodel/low/base/example.go b/datamodel/low/base/example.go index 88d7b9c..cc5ce47 100644 --- a/datamodel/low/base/example.go +++ b/datamodel/low/base/example.go @@ -10,6 +10,7 @@ import ( "github.com/pb33f/libopenapi/index" "github.com/pb33f/libopenapi/utils" "gopkg.in/yaml.v3" + "sort" "strconv" "strings" ) @@ -45,9 +46,14 @@ func (ex *Example) Hash() [32]byte { if ex.ExternalValue.Value != "" { f = append(f, ex.ExternalValue.Value) } + keys := make([]string, len(ex.Extensions)) + z := 0 for k := range ex.Extensions { - f = append(f, fmt.Sprintf("%s-%x", k.Value, sha256.Sum256([]byte(fmt.Sprint(ex.Extensions[k].Value))))) + keys[z] = fmt.Sprintf("%s-%x", k.Value, sha256.Sum256([]byte(fmt.Sprint(ex.Extensions[k].Value)))) + z++ } + sort.Strings(keys) + f = append(f, keys...) return sha256.Sum256([]byte(strings.Join(f, "|"))) } diff --git a/datamodel/low/base/external_doc.go b/datamodel/low/base/external_doc.go index 5f20fda..a977dbb 100644 --- a/datamodel/low/base/external_doc.go +++ b/datamodel/low/base/external_doc.go @@ -5,9 +5,11 @@ package base import ( "crypto/sha256" + "fmt" "github.com/pb33f/libopenapi/datamodel/low" "github.com/pb33f/libopenapi/index" "gopkg.in/yaml.v3" + "sort" "strings" ) @@ -40,9 +42,17 @@ func (ex *ExternalDoc) GetExtensions() map[low.KeyReference[string]]low.ValueRef func (ex *ExternalDoc) Hash() [32]byte { // calculate a hash from every property. - d := []string{ + f := []string{ ex.Description.Value, ex.URL.Value, } - return sha256.Sum256([]byte(strings.Join(d, "|"))) + keys := make([]string, len(ex.Extensions)) + z := 0 + for k := range ex.Extensions { + keys[z] = fmt.Sprintf("%s-%x", k.Value, sha256.Sum256([]byte(fmt.Sprint(ex.Extensions[k].Value)))) + z++ + } + sort.Strings(keys) + f = append(f, keys...) + return sha256.Sum256([]byte(strings.Join(f, "|"))) } diff --git a/datamodel/low/base/info.go b/datamodel/low/base/info.go index 7151a16..faee7bd 100644 --- a/datamodel/low/base/info.go +++ b/datamodel/low/base/info.go @@ -4,9 +4,13 @@ package base import ( + "crypto/sha256" + "fmt" "github.com/pb33f/libopenapi/datamodel/low" "github.com/pb33f/libopenapi/index" "gopkg.in/yaml.v3" + "sort" + "strings" ) // Info represents a low-level Info object as defined by both OpenAPI 2 and OpenAPI 3. @@ -44,3 +48,36 @@ func (i *Info) Build(root *yaml.Node, idx *index.SpecIndex) error { i.License = lic return nil } + +// Hash will return a consistent SHA256 Hash of the Info object +func (i *Info) Hash() [32]byte { + var f []string + + if !i.Title.IsEmpty() { + f = append(f, i.Title.Value) + } + if !i.Description.IsEmpty() { + f = append(f, i.Description.Value) + } + if !i.TermsOfService.IsEmpty() { + f = append(f, i.TermsOfService.Value) + } + if !i.Contact.IsEmpty() { + f = append(f, low.GenerateHashString(i.Contact.Value)) + } + if !i.License.IsEmpty() { + f = append(f, low.GenerateHashString(i.License.Value)) + } + if !i.Version.IsEmpty() { + f = append(f, i.Version.Value) + } + keys := make([]string, len(i.Extensions)) + z := 0 + for k := range i.Extensions { + keys[z] = fmt.Sprintf("%s-%x", k.Value, sha256.Sum256([]byte(fmt.Sprint(i.Extensions[k].Value)))) + z++ + } + sort.Strings(keys) + f = append(f, keys...) + return sha256.Sum256([]byte(strings.Join(f, "|"))) +} diff --git a/datamodel/low/base/info_test.go b/datamodel/low/base/info_test.go index 1184373..92a9881 100644 --- a/datamodel/low/base/info_test.go +++ b/datamodel/low/base/info_test.go @@ -68,3 +68,45 @@ func TestLicense_Build(t *testing.T) { k := n.Build(nil, nil) assert.Nil(t, k) } + +func TestInfo_Hash(t *testing.T) { + + left := `title: princess b33f +description: a thing +termsOfService: https://pb33f.io +x-princess: b33f +contact: + name: buckaroo + url: https://pb33f.io +license: + name: magic beans +version: 1.2.3 +x-b33f: princess` + + right := `title: princess b33f +description: a thing +termsOfService: https://pb33f.io +x-princess: b33f +contact: + name: buckaroo + url: https://pb33f.io +license: + name: magic beans +version: 1.2.3 +x-b33f: princess` + + var lNode, rNode yaml.Node + _ = yaml.Unmarshal([]byte(left), &lNode) + _ = yaml.Unmarshal([]byte(right), &rNode) + + // create low level objects + var lDoc Info + var rDoc Info + _ = low.BuildModel(lNode.Content[0], &lDoc) + _ = low.BuildModel(rNode.Content[0], &rDoc) + _ = lDoc.Build(lNode.Content[0], nil) + _ = rDoc.Build(rNode.Content[0], nil) + + assert.Equal(t, lDoc.Hash(), rDoc.Hash()) + +} diff --git a/datamodel/low/base/license.go b/datamodel/low/base/license.go index 44c7ece..d957afd 100644 --- a/datamodel/low/base/license.go +++ b/datamodel/low/base/license.go @@ -4,9 +4,11 @@ package base import ( + "crypto/sha256" "github.com/pb33f/libopenapi/datamodel/low" "github.com/pb33f/libopenapi/index" "gopkg.in/yaml.v3" + "strings" ) // License is a low-level representation of a License object as defined by OpenAPI 2 and OpenAPI 3 @@ -21,3 +23,15 @@ type License struct { func (l *License) Build(root *yaml.Node, idx *index.SpecIndex) error { return nil } + +// Hash will return a consistent SHA256 Hash of the License object +func (l *License) Hash() [32]byte { + var f []string + if !l.Name.IsEmpty() { + f = append(f, l.Name.Value) + } + if !l.URL.IsEmpty() { + f = append(f, l.URL.Value) + } + return sha256.Sum256([]byte(strings.Join(f, "|"))) +} diff --git a/datamodel/low/base/license_test.go b/datamodel/low/base/license_test.go new file mode 100644 index 0000000..5a5b4d8 --- /dev/null +++ b/datamodel/low/base/license_test.go @@ -0,0 +1,33 @@ +// Copyright 2022 Princess B33f Heavy Industries / Dave Shanley +// SPDX-License-Identifier: MIT + +package base + +import ( + "github.com/pb33f/libopenapi/datamodel/low" + "github.com/stretchr/testify/assert" + "gopkg.in/yaml.v3" + "testing" +) + +func TestLicense_Hash(t *testing.T) { + + left := `url: https://pb33f.io +description: the ranch` + + right := `url: https://pb33f.io +description: the ranch` + + var lNode, rNode yaml.Node + _ = yaml.Unmarshal([]byte(left), &lNode) + _ = yaml.Unmarshal([]byte(right), &rNode) + + // create low level objects + var lDoc License + var rDoc License + _ = low.BuildModel(lNode.Content[0], &lDoc) + _ = low.BuildModel(rNode.Content[0], &rDoc) + + assert.Equal(t, lDoc.Hash(), rDoc.Hash()) + +} diff --git a/datamodel/low/base/schema.go b/datamodel/low/base/schema.go index ddcc1fb..4460a0b 100644 --- a/datamodel/low/base/schema.go +++ b/datamodel/low/base/schema.go @@ -153,7 +153,7 @@ func (s *Schema) Hash() [32]byte { d = append(d, fmt.Sprint(s.MinProperties.Value)) } if !s.AdditionalProperties.IsEmpty() { - d = append(d, fmt.Sprint(s.AdditionalProperties.Value)) + d = append(d, low.GenerateHashString(s.AdditionalProperties.Value)) } if !s.Description.IsEmpty() { d = append(d, fmt.Sprint(s.Description.Value)) @@ -185,7 +185,6 @@ func (s *Schema) Hash() [32]byte { if !s.ExclusiveMaximum.IsEmpty() && s.ExclusiveMaximum.Value.IsB() { d = append(d, fmt.Sprint(s.ExclusiveMaximum.Value.B)) } - if !s.ExclusiveMinimum.IsEmpty() && s.ExclusiveMinimum.Value.IsA() { d = append(d, fmt.Sprint(s.ExclusiveMinimum.Value.A)) } @@ -204,15 +203,28 @@ func (s *Schema) Hash() [32]byte { d = append(d, strings.Join(j, "|")) } + keys := make([]string, len(s.Required.Value)) for i := range s.Required.Value { - d = append(d, s.Required.Value[i].Value) + keys[i] = s.Required.Value[i].Value } + sort.Strings(keys) + d = append(d, keys...) + + keys = make([]string, len(s.Enum.Value)) + for i := range s.Enum.Value { + keys[i] = fmt.Sprint(s.Enum.Value[i].Value) + } + sort.Strings(keys) + d = append(d, keys...) + for i := range s.Enum.Value { d = append(d, fmt.Sprint(s.Enum.Value[i].Value)) } - propertyKeys := make([]string, 0, len(s.Properties.Value)) + propertyKeys := make([]string, len(s.Properties.Value)) + z := 0 for i := range s.Properties.Value { - propertyKeys = append(propertyKeys, i.Value) + propertyKeys[z] = i.Value + z++ } sort.Strings(propertyKeys) for k := range propertyKeys { @@ -233,15 +245,17 @@ func (s *Schema) Hash() [32]byte { // hash polymorphic data if len(s.OneOf.Value) > 0 { - oneOfKeys := make([]string, 0, len(s.OneOf.Value)) + oneOfKeys := make([]string, len(s.OneOf.Value)) oneOfEntities := make(map[string]*Schema) + z = 0 for i := range s.OneOf.Value { g := s.OneOf.Value[i].Value if !g.IsSchemaReference() { k := g.Schema() r := low.GenerateHashString(k) oneOfEntities[r] = k - oneOfKeys = append(oneOfKeys, r) + oneOfKeys[z] = r + z++ } } sort.Strings(oneOfKeys) @@ -251,15 +265,17 @@ func (s *Schema) Hash() [32]byte { } if len(s.AllOf.Value) > 0 { - allOfKeys := make([]string, 0, len(s.AllOf.Value)) + allOfKeys := make([]string, len(s.AllOf.Value)) allOfEntities := make(map[string]*Schema) + z = 0 for i := range s.AllOf.Value { g := s.AllOf.Value[i].Value if !g.IsSchemaReference() { k := g.Schema() r := low.GenerateHashString(k) allOfEntities[r] = k - allOfKeys = append(allOfKeys, r) + allOfKeys[z] = r + z++ } } sort.Strings(allOfKeys) @@ -269,15 +285,17 @@ func (s *Schema) Hash() [32]byte { } if len(s.AnyOf.Value) > 0 { - anyOfKeys := make([]string, 0, len(s.AnyOf.Value)) + anyOfKeys := make([]string, len(s.AnyOf.Value)) anyOfEntities := make(map[string]*Schema) + z = 0 for i := range s.AnyOf.Value { g := s.AnyOf.Value[i].Value if !g.IsSchemaReference() { k := g.Schema() r := low.GenerateHashString(k) anyOfEntities[r] = k - anyOfKeys = append(anyOfKeys, r) + anyOfKeys[z] = r + z++ } } sort.Strings(anyOfKeys) @@ -287,15 +305,17 @@ func (s *Schema) Hash() [32]byte { } if len(s.Not.Value) > 0 { - notKeys := make([]string, 0, len(s.Not.Value)) + notKeys := make([]string, len(s.Not.Value)) notEntities := make(map[string]*Schema) + z = 0 for i := range s.Not.Value { g := s.Not.Value[i].Value if !g.IsSchemaReference() { k := g.Schema() r := low.GenerateHashString(k) notEntities[r] = k - notKeys = append(notKeys, r) + notKeys[z] = r + z++ } } sort.Strings(notKeys) @@ -305,15 +325,17 @@ func (s *Schema) Hash() [32]byte { } if len(s.Items.Value) > 0 { - itemsKeys := make([]string, 0, len(s.Items.Value)) + itemsKeys := make([]string, len(s.Items.Value)) itemsEntities := make(map[string]*Schema) + z = 0 for i := range s.Items.Value { g := s.Items.Value[i].Value if !g.IsSchemaReference() { k := g.Schema() r := low.GenerateHashString(k) itemsEntities[r] = k - itemsKeys = append(itemsKeys, r) + itemsKeys[z] = r + z++ } } sort.Strings(itemsKeys) @@ -322,9 +344,14 @@ func (s *Schema) Hash() [32]byte { } } // add extensions to hash + keys = make([]string, len(s.Extensions)) + z = 0 for k := range s.Extensions { - d = append(d, fmt.Sprintf("%v-%x", k.Value, s.Extensions[k].Value)) + keys[z] = fmt.Sprintf("%s-%x", k.Value, sha256.Sum256([]byte(fmt.Sprint(s.Extensions[k].Value)))) + z++ } + sort.Strings(keys) + d = append(d, keys...) if s.Example.Value != nil { d = append(d, low.GenerateHashString(s.Example.Value)) } diff --git a/datamodel/low/base/schema_test.go b/datamodel/low/base/schema_test.go index 6564084..ab41786 100644 --- a/datamodel/low/base/schema_test.go +++ b/datamodel/low/base/schema_test.go @@ -263,6 +263,28 @@ func TestSchema_Hash(t *testing.T) { } +func BenchmarkSchema_Hash(b *testing.B) { + + //create two versions + testSpec := test_get_schema_blob() + var sc1n yaml.Node + _ = yaml.Unmarshal([]byte(testSpec), &sc1n) + sch1 := Schema{} + _ = low.BuildModel(&sc1n, &sch1) + _ = sch1.Build(sc1n.Content[0], nil) + + var sc2n yaml.Node + _ = yaml.Unmarshal([]byte(testSpec), &sc2n) + sch2 := Schema{} + _ = low.BuildModel(&sc2n, &sch2) + _ = sch2.Build(sc2n.Content[0], nil) + + for i := 0; i < b.N; i++ { + assert.Equal(b, sch1.Hash(), sch2.Hash()) + } + +} + func Test_Schema_31(t *testing.T) { testSpec := `$schema: https://something type: @@ -1246,26 +1268,86 @@ func TestExtractSchema_OneOfRef(t *testing.T) { func TestSchema_Hash_Equal(t *testing.T) { left := `schema: + $schema: https://athing.com + multipleOf: 1 + maximum: 10 + minimum: 1 + maxLength: 10 + minLength: 1 + pattern: something + format: another + maxItems: 10 + minItems: 1 + uniqueItems: 1 + maxProperties: 10 + minProperties: 1 + additionalProperties: anything + description: milky + contentEncoding: rubber shoes + contentMediaType: paper tiger + default: + type: jazz + nullable: true + readOnly: true + writeOnly: true + deprecated: true + exclusiveMaximum: 23 + exclusiveMinimum: 10 + type: + - int + x-coffee: black + enum: + - one + - two + x-toast: burned title: an OK message required: - propA - enum: - - one properties: propA: title: a proxy property type: string` right := `schema: + $schema: https://athing.com + multipleOf: 1 + maximum: 10 + x-coffee: black + minimum: 1 + maxLength: 10 + minLength: 1 + pattern: something + format: another + maxItems: 10 + minItems: 1 + uniqueItems: 1 + maxProperties: 10 + minProperties: 1 + additionalProperties: anything + description: milky + contentEncoding: rubber shoes + contentMediaType: paper tiger + default: + type: jazz + nullable: true + readOnly: true + writeOnly: true + deprecated: true + exclusiveMaximum: 23 + exclusiveMinimum: 10 + type: + - int enum: - one + - two + x-toast: burned title: an OK message + required: + - propA properties: propA: title: a proxy property - type: string - required: - - propA` + type: string` var lNode, rNode yaml.Node _ = yaml.Unmarshal([]byte(left), &lNode) diff --git a/datamodel/low/base/tag.go b/datamodel/low/base/tag.go index 175b913..31ba383 100644 --- a/datamodel/low/base/tag.go +++ b/datamodel/low/base/tag.go @@ -4,9 +4,13 @@ package base import ( + "crypto/sha256" + "fmt" "github.com/pb33f/libopenapi/datamodel/low" "github.com/pb33f/libopenapi/index" "gopkg.in/yaml.v3" + "sort" + "strings" ) // Tag represents a low-level Tag instance that is backed by a low-level one. @@ -42,6 +46,29 @@ func (t *Tag) GetExtensions() map[low.KeyReference[string]]low.ValueReference[an return t.Extensions } +// Hash will return a consistent SHA256 Hash of the Info object +func (t *Tag) Hash() [32]byte { + var f []string + if !t.Name.IsEmpty() { + f = append(f, t.Name.Value) + } + if !t.Description.IsEmpty() { + f = append(f, t.Description.Value) + } + if !t.ExternalDocs.IsEmpty() { + f = append(f, low.GenerateHashString(t.ExternalDocs.Value)) + } + keys := make([]string, len(t.Extensions)) + z := 0 + for k := range t.Extensions { + keys[z] = fmt.Sprintf("%s-%x", k.Value, sha256.Sum256([]byte(fmt.Sprint(t.Extensions[k].Value)))) + z++ + } + sort.Strings(keys) + f = append(f, keys...) + return sha256.Sum256([]byte(strings.Join(f, "|"))) +} + // TODO: future mutation API experiment code is here. this snippet is to re-marshal the object. //func (t *Tag) MarshalYAML() (interface{}, error) { // m := make(map[string]interface{}) diff --git a/datamodel/low/base/tag_test.go b/datamodel/low/base/tag_test.go index 30c99d5..3169719 100644 --- a/datamodel/low/base/tag_test.go +++ b/datamodel/low/base/tag_test.go @@ -55,3 +55,33 @@ externalDocs: err = n.Build(idxNode.Content[0], idx) assert.Error(t, err) } + +func TestTag_Hash(t *testing.T) { + + left := `name: melody +description: my princess +externalDocs: + url: https://pb33f.io +x-b33f: princess` + + right := `name: melody +description: my princess +externalDocs: + url: https://pb33f.io +x-b33f: princess` + + var lNode, rNode yaml.Node + _ = yaml.Unmarshal([]byte(left), &lNode) + _ = yaml.Unmarshal([]byte(right), &rNode) + + // create low level objects + var lDoc Tag + var rDoc Tag + _ = low.BuildModel(lNode.Content[0], &lDoc) + _ = low.BuildModel(rNode.Content[0], &rDoc) + _ = lDoc.Build(lNode.Content[0], nil) + _ = rDoc.Build(rNode.Content[0], nil) + + assert.Equal(t, lDoc.Hash(), rDoc.Hash()) + +} \ No newline at end of file diff --git a/datamodel/low/base/xml.go b/datamodel/low/base/xml.go index d73a75a..37c250b 100644 --- a/datamodel/low/base/xml.go +++ b/datamodel/low/base/xml.go @@ -6,6 +6,7 @@ import ( "github.com/pb33f/libopenapi/datamodel/low" "github.com/pb33f/libopenapi/index" "gopkg.in/yaml.v3" + "sort" "strings" ) @@ -38,17 +39,29 @@ func (x *XML) GetExtensions() map[low.KeyReference[string]]low.ValueReference[an // Hash generates a SHA256 hash of the XML object using properties func (x *XML) Hash() [32]byte { - // calculate a hash from every property. - d := []string{ - x.Name.Value, - x.Namespace.Value, - x.Prefix.Value, - fmt.Sprint(x.Attribute.Value), - fmt.Sprint(x.Wrapped.Value), + var f []string + if !x.Name.IsEmpty() { + f = append(f, x.Name.Value) } - // add extensions to hash + if !x.Namespace.IsEmpty() { + f = append(f, x.Namespace.Value) + } + if !x.Prefix.IsEmpty() { + f = append(f, x.Prefix.Value) + } + if !x.Attribute.IsEmpty() { + f = append(f, fmt.Sprint(x.Attribute.Value)) + } + if !x.Wrapped.IsEmpty() { + f = append(f, fmt.Sprint(x.Wrapped.Value)) + } + keys := make([]string, len(x.Extensions)) + z := 0 for k := range x.Extensions { - d = append(d, fmt.Sprintf("%v-%x", k.Value, x.Extensions[k].Value)) + keys[z] = fmt.Sprintf("%s-%x", k.Value, sha256.Sum256([]byte(fmt.Sprint(x.Extensions[k].Value)))) + z++ } - return sha256.Sum256([]byte(strings.Join(d, "|"))) + sort.Strings(keys) + f = append(f, keys...) + return sha256.Sum256([]byte(strings.Join(f, "|"))) } diff --git a/datamodel/low/model_interfaces.go b/datamodel/low/model_interfaces.go index 782acb6..66c294c 100644 --- a/datamodel/low/model_interfaces.go +++ b/datamodel/low/model_interfaces.go @@ -33,7 +33,7 @@ type SwaggerParameter interface { GetMaxItems() *NodeReference[int] GetMinItems() *NodeReference[int] GetUniqueItems() *NodeReference[bool] - GetEnum() *NodeReference[[]ValueReference[string]] + GetEnum() *NodeReference[[]ValueReference[any]] GetMultipleOf() *NodeReference[int] } @@ -54,7 +54,7 @@ type SwaggerHeader interface { GetMaxItems() *NodeReference[int] GetMinItems() *NodeReference[int] GetUniqueItems() *NodeReference[bool] - GetEnum() *NodeReference[[]ValueReference[string]] + GetEnum() *NodeReference[[]ValueReference[any]] GetMultipleOf() *NodeReference[int] GetItems() *NodeReference[any] // requires cast. } diff --git a/datamodel/low/v2/header.go b/datamodel/low/v2/header.go index 610cd7a..168ff58 100644 --- a/datamodel/low/v2/header.go +++ b/datamodel/low/v2/header.go @@ -10,6 +10,7 @@ import ( "github.com/pb33f/libopenapi/index" "github.com/pb33f/libopenapi/utils" "gopkg.in/yaml.v3" + "sort" "strings" ) @@ -34,7 +35,7 @@ type Header struct { MaxItems low.NodeReference[int] MinItems low.NodeReference[int] UniqueItems low.NodeReference[bool] - Enum low.NodeReference[[]low.ValueReference[string]] + Enum low.NodeReference[[]low.ValueReference[any]] MultipleOf low.NodeReference[int] Extensions map[low.KeyReference[string]]low.ValueReference[any] } @@ -120,16 +121,26 @@ func (h *Header) Hash() [32]byte { if h.Pattern.Value != "" { f = append(f, fmt.Sprintf("%x", sha256.Sum256([]byte(fmt.Sprint(h.Pattern.Value))))) } - if len(h.Enum.Value) > 0 { - for k := range h.Enum.Value { - f = append(f, fmt.Sprint(h.Enum.Value[k].Value)) - } - } + + keys := make([]string, len(h.Extensions)) + z := 0 for k := range h.Extensions { - f = append(f, fmt.Sprintf("%s-%v", k.Value, h.Extensions[k].Value)) + keys[z] = fmt.Sprintf("%s-%x", k.Value, sha256.Sum256([]byte(fmt.Sprint(h.Extensions[k].Value)))) + z++ } + sort.Strings(keys) + f = append(f, keys...) + + keys = make([]string, len(h.Enum.Value)) + z = 0 + for k := range h.Enum.Value { + keys[z] = fmt.Sprint(h.Enum.Value[k].Value) + z++ + } + sort.Strings(keys) + f = append(f, keys...) if h.Items.Value != nil { - f = append(f, fmt.Sprintf("%x", h.Items.Value.Hash())) + f = append(f, low.GenerateHashString(h.Items.Value)) } return sha256.Sum256([]byte(strings.Join(f, "|"))) } @@ -189,7 +200,7 @@ func (h *Header) GetMinItems() *low.NodeReference[int] { func (h *Header) GetUniqueItems() *low.NodeReference[bool] { return &h.UniqueItems } -func (h *Header) GetEnum() *low.NodeReference[[]low.ValueReference[string]] { +func (h *Header) GetEnum() *low.NodeReference[[]low.ValueReference[any]] { return &h.Enum } func (h *Header) GetMultipleOf() *low.NodeReference[int] { diff --git a/datamodel/low/v2/items.go b/datamodel/low/v2/items.go index 1778785..1b69046 100644 --- a/datamodel/low/v2/items.go +++ b/datamodel/low/v2/items.go @@ -34,7 +34,7 @@ type Items struct { MaxItems low.NodeReference[int] MinItems low.NodeReference[int] UniqueItems low.NodeReference[bool] - Enum low.NodeReference[[]low.ValueReference[string]] + Enum low.NodeReference[[]low.ValueReference[any]] MultipleOf low.NodeReference[int] Extensions map[low.KeyReference[string]]low.ValueReference[any] } @@ -186,7 +186,7 @@ func (i *Items) GetMinItems() *low.NodeReference[int] { func (i *Items) GetUniqueItems() *low.NodeReference[bool] { return &i.UniqueItems } -func (i *Items) GetEnum() *low.NodeReference[[]low.ValueReference[string]] { +func (i *Items) GetEnum() *low.NodeReference[[]low.ValueReference[any]] { return &i.Enum } func (i *Items) GetMultipleOf() *low.NodeReference[int] { diff --git a/datamodel/low/v2/operation.go b/datamodel/low/v2/operation.go index 21edbd1..2783bc9 100644 --- a/datamodel/low/v2/operation.go +++ b/datamodel/low/v2/operation.go @@ -146,10 +146,14 @@ func (o *Operation) Hash() [32]byte { } sort.Strings(keys) f = append(f, keys...) + keys = make([]string, len(o.Extensions)) + z := 0 for k := range o.Extensions { - f = append(f, fmt.Sprintf("%s-%x", k.Value, - sha256.Sum256([]byte(fmt.Sprint(o.Extensions[k].Value))))) + keys[z] = fmt.Sprintf("%s-%x", k.Value, sha256.Sum256([]byte(fmt.Sprint(o.Extensions[k].Value)))) + z++ } + sort.Strings(keys) + f = append(f, keys...) return sha256.Sum256([]byte(strings.Join(f, "|"))) } diff --git a/datamodel/low/v2/parameter.go b/datamodel/low/v2/parameter.go index 36d9de0..e565e4b 100644 --- a/datamodel/low/v2/parameter.go +++ b/datamodel/low/v2/parameter.go @@ -67,7 +67,7 @@ type Parameter struct { MaxItems low.NodeReference[int] MinItems low.NodeReference[int] UniqueItems low.NodeReference[bool] - Enum low.NodeReference[[]low.ValueReference[string]] + Enum low.NodeReference[[]low.ValueReference[any]] MultipleOf low.NodeReference[int] Extensions map[low.KeyReference[string]]low.ValueReference[any] } @@ -258,7 +258,7 @@ func (p *Parameter) GetMinItems() *low.NodeReference[int] { func (p *Parameter) GetUniqueItems() *low.NodeReference[bool] { return &p.UniqueItems } -func (p *Parameter) GetEnum() *low.NodeReference[[]low.ValueReference[string]] { +func (p *Parameter) GetEnum() *low.NodeReference[[]low.ValueReference[any]] { return &p.Enum } func (p *Parameter) GetMultipleOf() *low.NodeReference[int] { diff --git a/datamodel/low/v3/path_item_test.go b/datamodel/low/v3/path_item_test.go new file mode 100644 index 0000000..9040df1 --- /dev/null +++ b/datamodel/low/v3/path_item_test.go @@ -0,0 +1,82 @@ +// Copyright 2022 Princess B33f Heavy Industries / Dave Shanley +// SPDX-License-Identifier: MIT + +package v3 + +import ( + "github.com/pb33f/libopenapi/datamodel/low" + "github.com/pb33f/libopenapi/index" + "github.com/stretchr/testify/assert" + "gopkg.in/yaml.v3" + "testing" +) + +func TestPathItem_Hash(t *testing.T) { + + yml := `description: a path item +summary: it's another path item +servers: + - url: https://pb33f.io +parameters: + - in: head +get: + description: get me +post: + description: post me +put: + description: put me +patch: + description: patch me +delete: + description: delete me +head: + description: top +options: + description: choices +trace: + description: find me +x-byebye: boebert` + + var idxNode yaml.Node + _ = yaml.Unmarshal([]byte(yml), &idxNode) + idx := index.NewSpecIndex(&idxNode) + + var n PathItem + _ = low.BuildModel(idxNode.Content[0], &n) + _ = n.Build(idxNode.Content[0], idx) + + yml2 := `get: + description: get me +post: + description: post me +servers: + - url: https://pb33f.io +parameters: + - in: head +put: + description: put me +patch: + description: patch me +delete: + description: delete me +head: + description: top +options: + description: choices +trace: + description: find me +x-byebye: boebert +description: a path item +summary: it's another path item` + + var idxNode2 yaml.Node + _ = yaml.Unmarshal([]byte(yml2), &idxNode2) + idx2 := index.NewSpecIndex(&idxNode2) + + var n2 PathItem + _ = low.BuildModel(idxNode2.Content[0], &n2) + _ = n2.Build(idxNode2.Content[0], idx2) + + // hash + assert.Equal(t, n.Hash(), n2.Hash()) +} diff --git a/datamodel/low/v3/paths_test.go b/datamodel/low/v3/paths_test.go index 55ab840..4dcb329 100644 --- a/datamodel/low/v3/paths_test.go +++ b/datamodel/low/v3/paths_test.go @@ -428,3 +428,49 @@ func TestPaths_Build_BrokenOp(t *testing.T) { err = n.Build(idxNode.Content[0], idx) assert.Error(t, err) } + +func TestPaths_Hash(t *testing.T) { + + yml := `/french/toast: + description: toast +/french/hen: + description: chicken +/french/food: + description: the worst. +x-france: french` + + var idxNode yaml.Node + _ = yaml.Unmarshal([]byte(yml), &idxNode) + idx := index.NewSpecIndex(&idxNode) + + var n Paths + _ = low.BuildModel(idxNode.Content[0], &n) + _ = n.Build(idxNode.Content[0], idx) + + yml2 := `/french/toast: + description: toast +/french/hen: + description: chicken +/french/food: + description: the worst. +x-france: french` + + var idxNode2 yaml.Node + _ = yaml.Unmarshal([]byte(yml2), &idxNode2) + idx2 := index.NewSpecIndex(&idxNode2) + + var n2 Paths + _ = low.BuildModel(idxNode2.Content[0], &n2) + _ = n2.Build(idxNode2.Content[0], idx2) + + // hash + assert.Equal(t, n.Hash(), n2.Hash()) + a, b := n.FindPathAndKey("/french/toast") + assert.NotNil(t, a) + assert.NotNil(t, b) + + a, b = n.FindPathAndKey("I do not exist") + assert.Nil(t, a) + assert.Nil(t, b) + +} diff --git a/datamodel/low/v3/request_body_test.go b/datamodel/low/v3/request_body_test.go index 8cebd91..340c798 100644 --- a/datamodel/low/v3/request_body_test.go +++ b/datamodel/low/v3/request_body_test.go @@ -53,3 +53,49 @@ func TestRequestBody_Fail(t *testing.T) { err = n.Build(idxNode.Content[0], idx) assert.Error(t, err) } + +func TestRequestBody_Hash(t *testing.T) { + + yml := `description: nice toast +content: + jammy/toast: + schema: + type: int + honey/toast: + schema: + type: int +required: true +x-toast: nice +` + + var idxNode yaml.Node + _ = yaml.Unmarshal([]byte(yml), &idxNode) + idx := index.NewSpecIndex(&idxNode) + + var n RequestBody + _ = low.BuildModel(idxNode.Content[0], &n) + _ = n.Build(idxNode.Content[0], idx) + + yml2 := `description: nice toast +content: + jammy/toast: + schema: + type: int + honey/toast: + schema: + type: int +required: true +x-toast: nice` + + var idxNode2 yaml.Node + _ = yaml.Unmarshal([]byte(yml2), &idxNode2) + idx2 := index.NewSpecIndex(&idxNode2) + + var n2 RequestBody + _ = low.BuildModel(idxNode2.Content[0], &n2) + _ = n2.Build(idxNode2.Content[0], idx2) + + // hash + assert.Equal(t, n.Hash(), n2.Hash()) + +} diff --git a/datamodel/low/v3/response.go b/datamodel/low/v3/response.go index 21b2048..b48c4e0 100644 --- a/datamodel/low/v3/response.go +++ b/datamodel/low/v3/response.go @@ -9,6 +9,7 @@ import ( "github.com/pb33f/libopenapi/datamodel/low" "github.com/pb33f/libopenapi/index" "gopkg.in/yaml.v3" + "sort" "strings" ) @@ -95,18 +96,38 @@ func (r *Response) Hash() [32]byte { if r.Description.Value != "" { f = append(f, r.Description.Value) } + + keys := make([]string, len(r.Headers.Value)) + z := 0 for k := range r.Headers.Value { - f = append(f, low.GenerateHashString(r.Headers.Value[k].Value)) + keys[z] = low.GenerateHashString(r.Headers.Value[k].Value) + z++ } + sort.Strings(keys) + f = append(f, keys...) + keys = make([]string, len(r.Content.Value)) + z = 0 for k := range r.Content.Value { - f = append(f, low.GenerateHashString(r.Content.Value[k].Value)) + keys[z] = low.GenerateHashString(r.Content.Value[k].Value) + z++ } + sort.Strings(keys) + f = append(f, keys...) + keys = make([]string, len(r.Links.Value)) + z = 0 for k := range r.Links.Value { - f = append(f, low.GenerateHashString(r.Links.Value[k].Value)) + keys[z] = low.GenerateHashString(r.Links.Value[k].Value) + z++ } + sort.Strings(keys) + f = append(f, keys...) + keys = make([]string, len(r.Extensions)) + z = 0 for k := range r.Extensions { - f = append(f, fmt.Sprintf("%s-%x", k.Value, - sha256.Sum256([]byte(fmt.Sprint(r.Extensions[k].Value))))) + keys[z] = fmt.Sprintf("%s-%x", k.Value, sha256.Sum256([]byte(fmt.Sprint(r.Extensions[k].Value)))) + z++ } + sort.Strings(keys) + f = append(f, keys...) return sha256.Sum256([]byte(strings.Join(f, "|"))) } diff --git a/datamodel/low/v3/response_test.go b/datamodel/low/v3/response_test.go index 53718c0..3969348 100644 --- a/datamodel/low/v3/response_test.go +++ b/datamodel/low/v3/response_test.go @@ -173,3 +173,64 @@ func TestResponses_Build_FailBadLinks(t *testing.T) { assert.Error(t, err) } + +func TestResponse_Hash(t *testing.T) { + + yml := `description: nice toast +headers: + heady: + description: a header + handy: + description: a handy +content: + nice/toast: + schema: + type: int + nice/roast: + schema: + type: int +x-jam: toast +x-ham: jam +links: + linky: + operationId: one two toast` + + var idxNode yaml.Node + _ = yaml.Unmarshal([]byte(yml), &idxNode) + idx := index.NewSpecIndex(&idxNode) + + var n Response + _ = low.BuildModel(idxNode.Content[0], &n) + _ = n.Build(idxNode.Content[0], idx) + + yml2 := `description: nice toast +x-ham: jam +headers: + heady: + description: a header + handy: + description: a handy +content: + nice/toast: + schema: + type: int + nice/roast: + schema: + type: int +x-jam: toast +links: + linky: + operationId: one two toast` + + var idxNode2 yaml.Node + _ = yaml.Unmarshal([]byte(yml2), &idxNode2) + idx2 := index.NewSpecIndex(&idxNode2) + + var n2 Response + _ = low.BuildModel(idxNode2.Content[0], &n2) + _ = n2.Build(idxNode2.Content[0], idx2) + + // hash + assert.Equal(t, n.Hash(), n2.Hash()) + +} diff --git a/what-changed/model/comparison_functions.go b/what-changed/model/comparison_functions.go index aa318d8..c106b58 100644 --- a/what-changed/model/comparison_functions.go +++ b/what-changed/model/comparison_functions.go @@ -4,6 +4,7 @@ package model import ( + "fmt" "github.com/pb33f/libopenapi/datamodel/low" "gopkg.in/yaml.v3" "strings" @@ -279,3 +280,40 @@ func ExtractStringValueSliceChanges(lParam, rParam []low.ValueReference[string], } } } + +// ExtractRawValueSliceChanges will compare two low level interface{} slices for changes. +func ExtractRawValueSliceChanges(lParam, rParam []low.ValueReference[any], + changes *[]*Change, label string, breaking bool) { + lKeys := make([]string, len(lParam)) + rKeys := make([]string, len(rParam)) + lValues := make(map[string]low.ValueReference[any]) + rValues := make(map[string]low.ValueReference[any]) + for i := range lParam { + lKeys[i] = strings.ToLower(fmt.Sprint(lParam[i].Value)) + lValues[lKeys[i]] = lParam[i] + } + for i := range rParam { + rKeys[i] = strings.ToLower(fmt.Sprint(rParam[i].Value)) + rValues[rKeys[i]] = rParam[i] + } + for i := range lValues { + if _, ok := rValues[i]; !ok { + CreateChange(changes, PropertyRemoved, label, + lValues[i].ValueNode, + nil, + breaking, + lValues[i].Value, + nil) + } + } + for i := range rValues { + if _, ok := lValues[i]; !ok { + CreateChange(changes, PropertyAdded, label, + nil, + rValues[i].ValueNode, + false, + nil, + rValues[i].Value) + } + } +} diff --git a/what-changed/model/header.go b/what-changed/model/header.go index 8b0ba61..92e0013 100644 --- a/what-changed/model/header.go +++ b/what-changed/model/header.go @@ -190,7 +190,7 @@ func CompareHeaders(l, r any) *HeaderChanges { // enum if len(lHeader.Enum.Value) > 0 || len(rHeader.Enum.Value) > 0 { - ExtractStringValueSliceChanges(lHeader.Enum.Value, rHeader.Enum.Value, &changes, v3.EnumLabel, true) + ExtractRawValueSliceChanges(lHeader.Enum.Value, rHeader.Enum.Value, &changes, v3.EnumLabel, true) } // items diff --git a/what-changed/model/parameter.go b/what-changed/model/parameter.go index 8f17df3..b1c6247 100644 --- a/what-changed/model/parameter.go +++ b/what-changed/model/parameter.go @@ -242,7 +242,7 @@ func CompareParameters(l, r any) *ParameterChanges { // enum if len(lParam.Enum.Value) > 0 || len(rParam.Enum.Value) > 0 { - ExtractStringValueSliceChanges(lParam.Enum.Value, rParam.Enum.Value, &changes, v3.EnumLabel, true) + ExtractRawValueSliceChanges(lParam.Enum.Value, rParam.Enum.Value, &changes, v3.EnumLabel, true) } }