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.
This commit is contained in:
Dave Shanley
2022-11-11 10:31:40 -05:00
parent 1cd492ae37
commit 61f99b8fd6
30 changed files with 782 additions and 99 deletions

View File

@@ -28,7 +28,7 @@ type Header struct {
MaxItems int MaxItems int
MinItems int MinItems int
UniqueItems bool UniqueItems bool
Enum []string Enum []any
MultipleOf int MultipleOf int
Extensions map[string]any Extensions map[string]any
low *low.Header low *low.Header
@@ -88,7 +88,7 @@ func NewHeader(header *low.Header) *Header {
h.UniqueItems = header.UniqueItems.IsEmpty() h.UniqueItems = header.UniqueItems.IsEmpty()
} }
if !header.Enum.IsEmpty() { if !header.Enum.IsEmpty() {
var enums []string var enums []any
for e := range header.Enum.Value { for e := range header.Enum.Value {
enums = append(enums, header.Enum.Value[e].Value) enums = append(enums, header.Enum.Value[e].Value)
} }

View File

@@ -3,7 +3,9 @@
package v2 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 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 // 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 MaxItems int
MinItems int MinItems int
UniqueItems bool UniqueItems bool
Enum []string Enum []any
MultipleOf int MultipleOf int
low *low.Items low *low.Items
} }
@@ -80,7 +82,7 @@ func NewItems(items *low.Items) *Items {
i.UniqueItems = items.UniqueItems.Value i.UniqueItems = items.UniqueItems.Value
} }
if !items.Enum.IsEmpty() { if !items.Enum.IsEmpty() {
var enums []string var enums []any
for e := range items.Enum.Value { for e := range items.Enum.Value {
enums = append(enums, items.Enum.Value[e].Value) enums = append(enums, items.Enum.Value[e].Value)
} }

View File

@@ -46,24 +46,24 @@ type Parameter struct {
Type string Type string
Format string Format string
Description string Description string
Required bool Required *bool
AllowEmptyValue bool AllowEmptyValue *bool
Schema *base.SchemaProxy Schema *base.SchemaProxy
Items *Items Items *Items
CollectionFormat string CollectionFormat string
Default any Default any
Maximum int Maximum *int
ExclusiveMaximum bool ExclusiveMaximum *bool
Minimum int Minimum *int
ExclusiveMinimum bool ExclusiveMinimum *bool
MaxLength int MaxLength *int
MinLength int MinLength *int
Pattern string Pattern string
MaxItems int MaxItems *int
MinItems int MinItems *int
UniqueItems bool UniqueItems *bool
Enum []string Enum []any
MultipleOf int MultipleOf *int
Extensions map[string]any Extensions map[string]any
low *low.Parameter low *low.Parameter
} }
@@ -89,10 +89,10 @@ func NewParameter(parameter *low.Parameter) *Parameter {
p.Description = parameter.Description.Value p.Description = parameter.Description.Value
} }
if !parameter.Required.IsEmpty() { if !parameter.Required.IsEmpty() {
p.Required = parameter.Required.Value p.Required = &parameter.Required.Value
} }
if !parameter.AllowEmptyValue.IsEmpty() { if !parameter.AllowEmptyValue.IsEmpty() {
p.AllowEmptyValue = parameter.AllowEmptyValue.Value p.AllowEmptyValue = &parameter.AllowEmptyValue.Value
} }
if !parameter.Schema.IsEmpty() { if !parameter.Schema.IsEmpty() {
p.Schema = base.NewSchemaProxy(&parameter.Schema) p.Schema = base.NewSchemaProxy(&parameter.Schema)
@@ -107,44 +107,44 @@ func NewParameter(parameter *low.Parameter) *Parameter {
p.Default = parameter.Default.Value p.Default = parameter.Default.Value
} }
if !parameter.Maximum.IsEmpty() { if !parameter.Maximum.IsEmpty() {
p.Maximum = parameter.Maximum.Value p.Maximum = &parameter.Maximum.Value
} }
if !parameter.ExclusiveMaximum.IsEmpty() { if !parameter.ExclusiveMaximum.IsEmpty() {
p.ExclusiveMaximum = parameter.ExclusiveMaximum.Value p.ExclusiveMaximum = &parameter.ExclusiveMaximum.Value
} }
if !parameter.Minimum.IsEmpty() { if !parameter.Minimum.IsEmpty() {
p.Minimum = parameter.Minimum.Value p.Minimum = &parameter.Minimum.Value
} }
if !parameter.ExclusiveMinimum.IsEmpty() { if !parameter.ExclusiveMinimum.IsEmpty() {
p.ExclusiveMinimum = parameter.ExclusiveMinimum.Value p.ExclusiveMinimum = &parameter.ExclusiveMinimum.Value
} }
if !parameter.MaxLength.IsEmpty() { if !parameter.MaxLength.IsEmpty() {
p.MaxLength = parameter.MaxLength.Value p.MaxLength = &parameter.MaxLength.Value
} }
if !parameter.MinLength.IsEmpty() { if !parameter.MinLength.IsEmpty() {
p.MinLength = parameter.MinLength.Value p.MinLength = &parameter.MinLength.Value
} }
if !parameter.Pattern.IsEmpty() { if !parameter.Pattern.IsEmpty() {
p.Pattern = parameter.Pattern.Value p.Pattern = parameter.Pattern.Value
} }
if !parameter.MinItems.IsEmpty() { if !parameter.MinItems.IsEmpty() {
p.MinItems = parameter.MinItems.Value p.MinItems = &parameter.MinItems.Value
} }
if !parameter.MaxItems.IsEmpty() { if !parameter.MaxItems.IsEmpty() {
p.MaxItems = parameter.MaxItems.Value p.MaxItems = &parameter.MaxItems.Value
} }
if !parameter.UniqueItems.IsEmpty() { if !parameter.UniqueItems.IsEmpty() {
p.UniqueItems = parameter.UniqueItems.Value p.UniqueItems = &parameter.UniqueItems.Value
} }
if !parameter.Enum.IsEmpty() { if !parameter.Enum.IsEmpty() {
var enums []string var enums []any
for e := range parameter.Enum.Value { for e := range parameter.Enum.Value {
enums = append(enums, parameter.Enum.Value[e].Value) enums = append(enums, parameter.Enum.Value[e].Value)
} }
p.Enum = enums p.Enum = enums
} }
if !parameter.MultipleOf.IsEmpty() { if !parameter.MultipleOf.IsEmpty() {
p.MultipleOf = parameter.MultipleOf.Value p.MultipleOf = &parameter.MultipleOf.Value
} }
return p return p
} }

View File

@@ -222,19 +222,19 @@ func TestNewSwaggerDocument_Paths(t *testing.T) {
assert.Equal(t, "petId", upload.Parameters[0].Name) assert.Equal(t, "petId", upload.Parameters[0].Name)
assert.Equal(t, "path", upload.Parameters[0].In) assert.Equal(t, "path", upload.Parameters[0].In)
assert.Equal(t, "ID of pet to update", upload.Parameters[0].Description) 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, "integer", upload.Parameters[0].Type)
assert.Equal(t, "int64", upload.Parameters[0].Format) assert.Equal(t, "int64", upload.Parameters[0].Format)
assert.True(t, upload.Parameters[0].ExclusiveMaximum) assert.True(t, *upload.Parameters[0].ExclusiveMaximum)
assert.True(t, upload.Parameters[0].ExclusiveMinimum) assert.True(t, *upload.Parameters[0].ExclusiveMinimum)
assert.Equal(t, 2, upload.Parameters[0].MaxLength) assert.Equal(t, 2, *upload.Parameters[0].MaxLength)
assert.Equal(t, 1, upload.Parameters[0].MinLength) assert.Equal(t, 1, *upload.Parameters[0].MinLength)
assert.Equal(t, 1, upload.Parameters[0].Minimum) assert.Equal(t, 1, *upload.Parameters[0].Minimum)
assert.Equal(t, 5, upload.Parameters[0].Maximum) assert.Equal(t, 5, *upload.Parameters[0].Maximum)
assert.Equal(t, "hi!", upload.Parameters[0].Pattern) assert.Equal(t, "hi!", upload.Parameters[0].Pattern)
assert.Equal(t, 1, upload.Parameters[0].MinItems) assert.Equal(t, 1, *upload.Parameters[0].MinItems)
assert.Equal(t, 20, upload.Parameters[0].MaxItems) assert.Equal(t, 20, *upload.Parameters[0].MaxItems)
assert.True(t, upload.Parameters[0].UniqueItems) assert.True(t, *upload.Parameters[0].UniqueItems)
assert.Len(t, upload.Parameters[0].Enum, 2) assert.Len(t, upload.Parameters[0].Enum, 2)
assert.Equal(t, "hello", upload.Parameters[0].Enum[0]) assert.Equal(t, "hello", upload.Parameters[0].Enum[0])
def := upload.Parameters[0].Default.(map[string]interface{}) def := upload.Parameters[0].Default.(map[string]interface{})

View File

@@ -4,9 +4,11 @@
package base package base
import ( import (
"crypto/sha256"
"github.com/pb33f/libopenapi/datamodel/low" "github.com/pb33f/libopenapi/datamodel/low"
"github.com/pb33f/libopenapi/index" "github.com/pb33f/libopenapi/index"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"strings"
) )
// Contact represents a low-level representation of the Contact definitions found at // 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. // not implemented.
return nil 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, "|")))
}

View File

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

View File

@@ -10,6 +10,7 @@ import (
"github.com/pb33f/libopenapi/index" "github.com/pb33f/libopenapi/index"
"github.com/pb33f/libopenapi/utils" "github.com/pb33f/libopenapi/utils"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"sort"
"strconv" "strconv"
"strings" "strings"
) )
@@ -45,9 +46,14 @@ func (ex *Example) Hash() [32]byte {
if ex.ExternalValue.Value != "" { if ex.ExternalValue.Value != "" {
f = append(f, ex.ExternalValue.Value) f = append(f, ex.ExternalValue.Value)
} }
keys := make([]string, len(ex.Extensions))
z := 0
for k := range ex.Extensions { 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, "|"))) return sha256.Sum256([]byte(strings.Join(f, "|")))
} }

View File

@@ -5,9 +5,11 @@ package base
import ( import (
"crypto/sha256" "crypto/sha256"
"fmt"
"github.com/pb33f/libopenapi/datamodel/low" "github.com/pb33f/libopenapi/datamodel/low"
"github.com/pb33f/libopenapi/index" "github.com/pb33f/libopenapi/index"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"sort"
"strings" "strings"
) )
@@ -40,9 +42,17 @@ func (ex *ExternalDoc) GetExtensions() map[low.KeyReference[string]]low.ValueRef
func (ex *ExternalDoc) Hash() [32]byte { func (ex *ExternalDoc) Hash() [32]byte {
// calculate a hash from every property. // calculate a hash from every property.
d := []string{ f := []string{
ex.Description.Value, ex.Description.Value,
ex.URL.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, "|")))
} }

View File

@@ -4,9 +4,13 @@
package base package base
import ( import (
"crypto/sha256"
"fmt"
"github.com/pb33f/libopenapi/datamodel/low" "github.com/pb33f/libopenapi/datamodel/low"
"github.com/pb33f/libopenapi/index" "github.com/pb33f/libopenapi/index"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"sort"
"strings"
) )
// Info represents a low-level Info object as defined by both OpenAPI 2 and OpenAPI 3. // 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 i.License = lic
return nil 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, "|")))
}

View File

@@ -68,3 +68,45 @@ func TestLicense_Build(t *testing.T) {
k := n.Build(nil, nil) k := n.Build(nil, nil)
assert.Nil(t, k) 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())
}

View File

@@ -4,9 +4,11 @@
package base package base
import ( import (
"crypto/sha256"
"github.com/pb33f/libopenapi/datamodel/low" "github.com/pb33f/libopenapi/datamodel/low"
"github.com/pb33f/libopenapi/index" "github.com/pb33f/libopenapi/index"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"strings"
) )
// License is a low-level representation of a License object as defined by OpenAPI 2 and OpenAPI 3 // 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 { func (l *License) Build(root *yaml.Node, idx *index.SpecIndex) error {
return nil 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, "|")))
}

View File

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

View File

@@ -153,7 +153,7 @@ func (s *Schema) Hash() [32]byte {
d = append(d, fmt.Sprint(s.MinProperties.Value)) d = append(d, fmt.Sprint(s.MinProperties.Value))
} }
if !s.AdditionalProperties.IsEmpty() { if !s.AdditionalProperties.IsEmpty() {
d = append(d, fmt.Sprint(s.AdditionalProperties.Value)) d = append(d, low.GenerateHashString(s.AdditionalProperties.Value))
} }
if !s.Description.IsEmpty() { if !s.Description.IsEmpty() {
d = append(d, fmt.Sprint(s.Description.Value)) 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() { if !s.ExclusiveMaximum.IsEmpty() && s.ExclusiveMaximum.Value.IsB() {
d = append(d, fmt.Sprint(s.ExclusiveMaximum.Value.B)) d = append(d, fmt.Sprint(s.ExclusiveMaximum.Value.B))
} }
if !s.ExclusiveMinimum.IsEmpty() && s.ExclusiveMinimum.Value.IsA() { if !s.ExclusiveMinimum.IsEmpty() && s.ExclusiveMinimum.Value.IsA() {
d = append(d, fmt.Sprint(s.ExclusiveMinimum.Value.A)) 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, "|")) d = append(d, strings.Join(j, "|"))
} }
keys := make([]string, len(s.Required.Value))
for i := range 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 { for i := range s.Enum.Value {
d = append(d, fmt.Sprint(s.Enum.Value[i].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 { for i := range s.Properties.Value {
propertyKeys = append(propertyKeys, i.Value) propertyKeys[z] = i.Value
z++
} }
sort.Strings(propertyKeys) sort.Strings(propertyKeys)
for k := range propertyKeys { for k := range propertyKeys {
@@ -233,15 +245,17 @@ func (s *Schema) Hash() [32]byte {
// hash polymorphic data // hash polymorphic data
if len(s.OneOf.Value) > 0 { 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) oneOfEntities := make(map[string]*Schema)
z = 0
for i := range s.OneOf.Value { for i := range s.OneOf.Value {
g := s.OneOf.Value[i].Value g := s.OneOf.Value[i].Value
if !g.IsSchemaReference() { if !g.IsSchemaReference() {
k := g.Schema() k := g.Schema()
r := low.GenerateHashString(k) r := low.GenerateHashString(k)
oneOfEntities[r] = k oneOfEntities[r] = k
oneOfKeys = append(oneOfKeys, r) oneOfKeys[z] = r
z++
} }
} }
sort.Strings(oneOfKeys) sort.Strings(oneOfKeys)
@@ -251,15 +265,17 @@ func (s *Schema) Hash() [32]byte {
} }
if len(s.AllOf.Value) > 0 { 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) allOfEntities := make(map[string]*Schema)
z = 0
for i := range s.AllOf.Value { for i := range s.AllOf.Value {
g := s.AllOf.Value[i].Value g := s.AllOf.Value[i].Value
if !g.IsSchemaReference() { if !g.IsSchemaReference() {
k := g.Schema() k := g.Schema()
r := low.GenerateHashString(k) r := low.GenerateHashString(k)
allOfEntities[r] = k allOfEntities[r] = k
allOfKeys = append(allOfKeys, r) allOfKeys[z] = r
z++
} }
} }
sort.Strings(allOfKeys) sort.Strings(allOfKeys)
@@ -269,15 +285,17 @@ func (s *Schema) Hash() [32]byte {
} }
if len(s.AnyOf.Value) > 0 { 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) anyOfEntities := make(map[string]*Schema)
z = 0
for i := range s.AnyOf.Value { for i := range s.AnyOf.Value {
g := s.AnyOf.Value[i].Value g := s.AnyOf.Value[i].Value
if !g.IsSchemaReference() { if !g.IsSchemaReference() {
k := g.Schema() k := g.Schema()
r := low.GenerateHashString(k) r := low.GenerateHashString(k)
anyOfEntities[r] = k anyOfEntities[r] = k
anyOfKeys = append(anyOfKeys, r) anyOfKeys[z] = r
z++
} }
} }
sort.Strings(anyOfKeys) sort.Strings(anyOfKeys)
@@ -287,15 +305,17 @@ func (s *Schema) Hash() [32]byte {
} }
if len(s.Not.Value) > 0 { 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) notEntities := make(map[string]*Schema)
z = 0
for i := range s.Not.Value { for i := range s.Not.Value {
g := s.Not.Value[i].Value g := s.Not.Value[i].Value
if !g.IsSchemaReference() { if !g.IsSchemaReference() {
k := g.Schema() k := g.Schema()
r := low.GenerateHashString(k) r := low.GenerateHashString(k)
notEntities[r] = k notEntities[r] = k
notKeys = append(notKeys, r) notKeys[z] = r
z++
} }
} }
sort.Strings(notKeys) sort.Strings(notKeys)
@@ -305,15 +325,17 @@ func (s *Schema) Hash() [32]byte {
} }
if len(s.Items.Value) > 0 { 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) itemsEntities := make(map[string]*Schema)
z = 0
for i := range s.Items.Value { for i := range s.Items.Value {
g := s.Items.Value[i].Value g := s.Items.Value[i].Value
if !g.IsSchemaReference() { if !g.IsSchemaReference() {
k := g.Schema() k := g.Schema()
r := low.GenerateHashString(k) r := low.GenerateHashString(k)
itemsEntities[r] = k itemsEntities[r] = k
itemsKeys = append(itemsKeys, r) itemsKeys[z] = r
z++
} }
} }
sort.Strings(itemsKeys) sort.Strings(itemsKeys)
@@ -322,9 +344,14 @@ func (s *Schema) Hash() [32]byte {
} }
} }
// add extensions to hash // add extensions to hash
keys = make([]string, len(s.Extensions))
z = 0
for k := range s.Extensions { 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 { if s.Example.Value != nil {
d = append(d, low.GenerateHashString(s.Example.Value)) d = append(d, low.GenerateHashString(s.Example.Value))
} }

View File

@@ -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) { func Test_Schema_31(t *testing.T) {
testSpec := `$schema: https://something testSpec := `$schema: https://something
type: type:
@@ -1246,26 +1268,86 @@ func TestExtractSchema_OneOfRef(t *testing.T) {
func TestSchema_Hash_Equal(t *testing.T) { func TestSchema_Hash_Equal(t *testing.T) {
left := `schema: 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 title: an OK message
required: required:
- propA - propA
enum:
- one
properties: properties:
propA: propA:
title: a proxy property title: a proxy property
type: string` type: string`
right := `schema: 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: enum:
- one - one
- two
x-toast: burned
title: an OK message title: an OK message
required:
- propA
properties: properties:
propA: propA:
title: a proxy property title: a proxy property
type: string type: string`
required:
- propA`
var lNode, rNode yaml.Node var lNode, rNode yaml.Node
_ = yaml.Unmarshal([]byte(left), &lNode) _ = yaml.Unmarshal([]byte(left), &lNode)

View File

@@ -4,9 +4,13 @@
package base package base
import ( import (
"crypto/sha256"
"fmt"
"github.com/pb33f/libopenapi/datamodel/low" "github.com/pb33f/libopenapi/datamodel/low"
"github.com/pb33f/libopenapi/index" "github.com/pb33f/libopenapi/index"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"sort"
"strings"
) )
// Tag represents a low-level Tag instance that is backed by a low-level one. // 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 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. // TODO: future mutation API experiment code is here. this snippet is to re-marshal the object.
//func (t *Tag) MarshalYAML() (interface{}, error) { //func (t *Tag) MarshalYAML() (interface{}, error) {
// m := make(map[string]interface{}) // m := make(map[string]interface{})

View File

@@ -55,3 +55,33 @@ externalDocs:
err = n.Build(idxNode.Content[0], idx) err = n.Build(idxNode.Content[0], idx)
assert.Error(t, err) 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())
}

View File

@@ -6,6 +6,7 @@ import (
"github.com/pb33f/libopenapi/datamodel/low" "github.com/pb33f/libopenapi/datamodel/low"
"github.com/pb33f/libopenapi/index" "github.com/pb33f/libopenapi/index"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"sort"
"strings" "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 // Hash generates a SHA256 hash of the XML object using properties
func (x *XML) Hash() [32]byte { func (x *XML) Hash() [32]byte {
// calculate a hash from every property. var f []string
d := []string{ if !x.Name.IsEmpty() {
x.Name.Value, f = append(f, x.Name.Value)
x.Namespace.Value,
x.Prefix.Value,
fmt.Sprint(x.Attribute.Value),
fmt.Sprint(x.Wrapped.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 { 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, "|")))
} }

View File

@@ -33,7 +33,7 @@ type SwaggerParameter interface {
GetMaxItems() *NodeReference[int] GetMaxItems() *NodeReference[int]
GetMinItems() *NodeReference[int] GetMinItems() *NodeReference[int]
GetUniqueItems() *NodeReference[bool] GetUniqueItems() *NodeReference[bool]
GetEnum() *NodeReference[[]ValueReference[string]] GetEnum() *NodeReference[[]ValueReference[any]]
GetMultipleOf() *NodeReference[int] GetMultipleOf() *NodeReference[int]
} }
@@ -54,7 +54,7 @@ type SwaggerHeader interface {
GetMaxItems() *NodeReference[int] GetMaxItems() *NodeReference[int]
GetMinItems() *NodeReference[int] GetMinItems() *NodeReference[int]
GetUniqueItems() *NodeReference[bool] GetUniqueItems() *NodeReference[bool]
GetEnum() *NodeReference[[]ValueReference[string]] GetEnum() *NodeReference[[]ValueReference[any]]
GetMultipleOf() *NodeReference[int] GetMultipleOf() *NodeReference[int]
GetItems() *NodeReference[any] // requires cast. GetItems() *NodeReference[any] // requires cast.
} }

View File

@@ -10,6 +10,7 @@ import (
"github.com/pb33f/libopenapi/index" "github.com/pb33f/libopenapi/index"
"github.com/pb33f/libopenapi/utils" "github.com/pb33f/libopenapi/utils"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"sort"
"strings" "strings"
) )
@@ -34,7 +35,7 @@ type Header struct {
MaxItems low.NodeReference[int] MaxItems low.NodeReference[int]
MinItems low.NodeReference[int] MinItems low.NodeReference[int]
UniqueItems low.NodeReference[bool] UniqueItems low.NodeReference[bool]
Enum low.NodeReference[[]low.ValueReference[string]] Enum low.NodeReference[[]low.ValueReference[any]]
MultipleOf low.NodeReference[int] MultipleOf low.NodeReference[int]
Extensions map[low.KeyReference[string]]low.ValueReference[any] Extensions map[low.KeyReference[string]]low.ValueReference[any]
} }
@@ -120,16 +121,26 @@ func (h *Header) Hash() [32]byte {
if h.Pattern.Value != "" { if h.Pattern.Value != "" {
f = append(f, fmt.Sprintf("%x", sha256.Sum256([]byte(fmt.Sprint(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 { keys := make([]string, len(h.Extensions))
f = append(f, fmt.Sprint(h.Enum.Value[k].Value)) z := 0
}
}
for k := range h.Extensions { 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 { 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, "|"))) 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] { func (h *Header) GetUniqueItems() *low.NodeReference[bool] {
return &h.UniqueItems return &h.UniqueItems
} }
func (h *Header) GetEnum() *low.NodeReference[[]low.ValueReference[string]] { func (h *Header) GetEnum() *low.NodeReference[[]low.ValueReference[any]] {
return &h.Enum return &h.Enum
} }
func (h *Header) GetMultipleOf() *low.NodeReference[int] { func (h *Header) GetMultipleOf() *low.NodeReference[int] {

View File

@@ -34,7 +34,7 @@ type Items struct {
MaxItems low.NodeReference[int] MaxItems low.NodeReference[int]
MinItems low.NodeReference[int] MinItems low.NodeReference[int]
UniqueItems low.NodeReference[bool] UniqueItems low.NodeReference[bool]
Enum low.NodeReference[[]low.ValueReference[string]] Enum low.NodeReference[[]low.ValueReference[any]]
MultipleOf low.NodeReference[int] MultipleOf low.NodeReference[int]
Extensions map[low.KeyReference[string]]low.ValueReference[any] 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] { func (i *Items) GetUniqueItems() *low.NodeReference[bool] {
return &i.UniqueItems return &i.UniqueItems
} }
func (i *Items) GetEnum() *low.NodeReference[[]low.ValueReference[string]] { func (i *Items) GetEnum() *low.NodeReference[[]low.ValueReference[any]] {
return &i.Enum return &i.Enum
} }
func (i *Items) GetMultipleOf() *low.NodeReference[int] { func (i *Items) GetMultipleOf() *low.NodeReference[int] {

View File

@@ -146,10 +146,14 @@ func (o *Operation) Hash() [32]byte {
} }
sort.Strings(keys) sort.Strings(keys)
f = append(f, keys...) f = append(f, keys...)
keys = make([]string, len(o.Extensions))
z := 0
for k := range o.Extensions { for k := range o.Extensions {
f = append(f, fmt.Sprintf("%s-%x", k.Value, keys[z] = fmt.Sprintf("%s-%x", k.Value, sha256.Sum256([]byte(fmt.Sprint(o.Extensions[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, "|"))) return sha256.Sum256([]byte(strings.Join(f, "|")))
} }

View File

@@ -67,7 +67,7 @@ type Parameter struct {
MaxItems low.NodeReference[int] MaxItems low.NodeReference[int]
MinItems low.NodeReference[int] MinItems low.NodeReference[int]
UniqueItems low.NodeReference[bool] UniqueItems low.NodeReference[bool]
Enum low.NodeReference[[]low.ValueReference[string]] Enum low.NodeReference[[]low.ValueReference[any]]
MultipleOf low.NodeReference[int] MultipleOf low.NodeReference[int]
Extensions map[low.KeyReference[string]]low.ValueReference[any] 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] { func (p *Parameter) GetUniqueItems() *low.NodeReference[bool] {
return &p.UniqueItems return &p.UniqueItems
} }
func (p *Parameter) GetEnum() *low.NodeReference[[]low.ValueReference[string]] { func (p *Parameter) GetEnum() *low.NodeReference[[]low.ValueReference[any]] {
return &p.Enum return &p.Enum
} }
func (p *Parameter) GetMultipleOf() *low.NodeReference[int] { func (p *Parameter) GetMultipleOf() *low.NodeReference[int] {

View File

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

View File

@@ -428,3 +428,49 @@ func TestPaths_Build_BrokenOp(t *testing.T) {
err = n.Build(idxNode.Content[0], idx) err = n.Build(idxNode.Content[0], idx)
assert.Error(t, err) 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)
}

View File

@@ -53,3 +53,49 @@ func TestRequestBody_Fail(t *testing.T) {
err = n.Build(idxNode.Content[0], idx) err = n.Build(idxNode.Content[0], idx)
assert.Error(t, err) 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())
}

View File

@@ -9,6 +9,7 @@ import (
"github.com/pb33f/libopenapi/datamodel/low" "github.com/pb33f/libopenapi/datamodel/low"
"github.com/pb33f/libopenapi/index" "github.com/pb33f/libopenapi/index"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"sort"
"strings" "strings"
) )
@@ -95,18 +96,38 @@ func (r *Response) Hash() [32]byte {
if r.Description.Value != "" { if r.Description.Value != "" {
f = append(f, r.Description.Value) f = append(f, r.Description.Value)
} }
keys := make([]string, len(r.Headers.Value))
z := 0
for k := range r.Headers.Value { 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 { 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 { 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 { for k := range r.Extensions {
f = append(f, fmt.Sprintf("%s-%x", k.Value, keys[z] = fmt.Sprintf("%s-%x", k.Value, sha256.Sum256([]byte(fmt.Sprint(r.Extensions[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, "|"))) return sha256.Sum256([]byte(strings.Join(f, "|")))
} }

View File

@@ -173,3 +173,64 @@ func TestResponses_Build_FailBadLinks(t *testing.T) {
assert.Error(t, err) 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())
}

View File

@@ -4,6 +4,7 @@
package model package model
import ( import (
"fmt"
"github.com/pb33f/libopenapi/datamodel/low" "github.com/pb33f/libopenapi/datamodel/low"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"strings" "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)
}
}
}

View File

@@ -190,7 +190,7 @@ func CompareHeaders(l, r any) *HeaderChanges {
// enum // enum
if len(lHeader.Enum.Value) > 0 || len(rHeader.Enum.Value) > 0 { 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 // items

View File

@@ -242,7 +242,7 @@ func CompareParameters(l, r any) *ParameterChanges {
// enum // enum
if len(lParam.Enum.Value) > 0 || len(rParam.Enum.Value) > 0 { 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)
} }
} }