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

View File

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

View File

@@ -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 = &parameter.Required.Value
}
if !parameter.AllowEmptyValue.IsEmpty() {
p.AllowEmptyValue = parameter.AllowEmptyValue.Value
p.AllowEmptyValue = &parameter.AllowEmptyValue.Value
}
if !parameter.Schema.IsEmpty() {
p.Schema = base.NewSchemaProxy(&parameter.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 = &parameter.Maximum.Value
}
if !parameter.ExclusiveMaximum.IsEmpty() {
p.ExclusiveMaximum = parameter.ExclusiveMaximum.Value
p.ExclusiveMaximum = &parameter.ExclusiveMaximum.Value
}
if !parameter.Minimum.IsEmpty() {
p.Minimum = parameter.Minimum.Value
p.Minimum = &parameter.Minimum.Value
}
if !parameter.ExclusiveMinimum.IsEmpty() {
p.ExclusiveMinimum = parameter.ExclusiveMinimum.Value
p.ExclusiveMinimum = &parameter.ExclusiveMinimum.Value
}
if !parameter.MaxLength.IsEmpty() {
p.MaxLength = parameter.MaxLength.Value
p.MaxLength = &parameter.MaxLength.Value
}
if !parameter.MinLength.IsEmpty() {
p.MinLength = parameter.MinLength.Value
p.MinLength = &parameter.MinLength.Value
}
if !parameter.Pattern.IsEmpty() {
p.Pattern = parameter.Pattern.Value
}
if !parameter.MinItems.IsEmpty() {
p.MinItems = parameter.MinItems.Value
p.MinItems = &parameter.MinItems.Value
}
if !parameter.MaxItems.IsEmpty() {
p.MaxItems = parameter.MaxItems.Value
p.MaxItems = &parameter.MaxItems.Value
}
if !parameter.UniqueItems.IsEmpty() {
p.UniqueItems = parameter.UniqueItems.Value
p.UniqueItems = &parameter.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 = &parameter.MultipleOf.Value
}
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, "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{})

View File

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

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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