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