diff --git a/datamodel/low/base/schema_proxy.go b/datamodel/low/base/schema_proxy.go index 49a2ece..15b225d 100644 --- a/datamodel/low/base/schema_proxy.go +++ b/datamodel/low/base/schema_proxy.go @@ -114,3 +114,12 @@ func (sp *SchemaProxy) GetSchemaReference() string { func (sp *SchemaProxy) GetValueNode() *yaml.Node { return sp.vn } + +// Hash will return a consistent SHA256 Hash of the SchemaProxy object (it will resolve it) +func (sp *SchemaProxy) Hash() [32]byte { + if sp.rendered != nil { + return sp.rendered.Hash() + } else { + return sp.Schema().Hash() + } +} diff --git a/datamodel/low/v2/definitions.go b/datamodel/low/v2/definitions.go index 2d251d7..429ab61 100644 --- a/datamodel/low/v2/definitions.go +++ b/datamodel/low/v2/definitions.go @@ -4,10 +4,13 @@ package v2 import ( + "crypto/sha256" "github.com/pb33f/libopenapi/datamodel/low" "github.com/pb33f/libopenapi/datamodel/low/base" "github.com/pb33f/libopenapi/index" "gopkg.in/yaml.v3" + "sort" + "strings" ) // ParameterDefinitions is a low-level representation of a Swagger / OpenAPI 2 Parameters Definitions object. @@ -110,6 +113,22 @@ func (d *Definitions) Build(root *yaml.Node, idx *index.SpecIndex) error { return nil } +// Hash will return a consistent SHA256 Hash of the Definitions object +func (d *Definitions) Hash() [32]byte { + var f []string + keys := make([]string, len(d.Schemas)) + z := 0 + for k := range d.Schemas { + keys[z] = k.Value + z++ + } + sort.Strings(keys) + for k := range keys { + f = append(f, low.GenerateHashString(d.FindSchema(keys[k]).Value)) + } + return sha256.Sum256([]byte(strings.Join(f, "|"))) +} + // Build will extract all ParameterDefinitions into Parameter instances. func (pd *ParameterDefinitions) Build(root *yaml.Node, idx *index.SpecIndex) error { errorChan := make(chan error) diff --git a/what-changed/model/components.go b/what-changed/model/components.go index df7430a..960db69 100644 --- a/what-changed/model/components.go +++ b/what-changed/model/components.go @@ -3,17 +3,141 @@ package model +import ( + v2 "github.com/pb33f/libopenapi/datamodel/low/v2" + v3 "github.com/pb33f/libopenapi/datamodel/low/v3" + "reflect" +) + type ComponentsChanges struct { PropertyChanges SchemaChanges map[string]*SchemaChanges - ResponsesChanges map[string]*SchemaChanges + ResponsesChanges map[string]*ResponseChanges ParameterChanges map[string]*ParameterChanges ExamplesChanges map[string]*ExamplesChanges RequestBodyChanges map[string]*RequestBodyChanges HeaderChanges map[string]*HeaderChanges SecuritySchemeChanges map[string]*SecuritySchemeChanges LinkChanges map[string]*LinkChanges - // todo: - //CallbackChanges map[string]*CallbackChanges - ExtensionChanges *ExtensionChanges + CallbackChanges map[string]*CallbackChanges + ExtensionChanges *ExtensionChanges +} + +func CompareComponents(l, r any) *ComponentsChanges { + + var changes []*Change + + cc := new(ComponentsChanges) + + // Swagger Parameters + if reflect.TypeOf(&v2.ParameterDefinitions{}) == reflect.TypeOf(l) && + reflect.TypeOf(&v2.ParameterDefinitions{}) == reflect.TypeOf(r) { + lDef := l.(*v2.ParameterDefinitions) + rDef := r.(*v2.ParameterDefinitions) + cc.ParameterChanges = CheckMapForChangesUntyped(lDef.Definitions, rDef.Definitions, &changes, + v3.ParametersLabel, CompareParameters) + } + + // Swagger Responses + if reflect.TypeOf(&v2.ResponsesDefinitions{}) == reflect.TypeOf(l) && + reflect.TypeOf(&v2.ResponsesDefinitions{}) == reflect.TypeOf(r) { + lDef := l.(*v2.ResponsesDefinitions) + rDef := r.(*v2.ResponsesDefinitions) + cc.ResponsesChanges = CheckMapForChangesUntyped(lDef.Definitions, rDef.Definitions, &changes, + v3.ResponsesLabel, CompareResponse) + } + + // Swagger Schemas + if reflect.TypeOf(&v2.Definitions{}) == reflect.TypeOf(l) && + reflect.TypeOf(&v2.Definitions{}) == reflect.TypeOf(r) { + lDef := l.(*v2.Definitions) + rDef := r.(*v2.Definitions) + cc.SchemaChanges = CheckMapForChanges(lDef.Schemas, rDef.Schemas, &changes, + v2.DefinitionsLabel, CompareSchemas) + } + + // Swagger Security Definitions + if reflect.TypeOf(&v2.SecurityDefinitions{}) == reflect.TypeOf(l) && + reflect.TypeOf(&v2.SecurityDefinitions{}) == reflect.TypeOf(r) { + lDef := l.(*v2.SecurityDefinitions) + rDef := r.(*v2.SecurityDefinitions) + cc.SecuritySchemeChanges = CheckMapForChangesUntyped(lDef.Definitions, rDef.Definitions, &changes, + v3.SecurityDefinitionLabel, CompareSecuritySchemes) + } + + cc.Changes = changes + if cc.TotalChanges() <= 0 { + return nil + } + return cc +} + +func (c *ComponentsChanges) TotalChanges() int { + v := c.PropertyChanges.TotalChanges() + for k := range c.SchemaChanges { + v += c.SchemaChanges[k].TotalChanges() + } + for k := range c.ResponsesChanges { + v += c.ResponsesChanges[k].TotalChanges() + } + for k := range c.ParameterChanges { + v += c.ParameterChanges[k].TotalChanges() + } + for k := range c.ExamplesChanges { + v += c.ExamplesChanges[k].TotalChanges() + } + for k := range c.RequestBodyChanges { + v += c.RequestBodyChanges[k].TotalChanges() + } + for k := range c.HeaderChanges { + v += c.HeaderChanges[k].TotalChanges() + } + for k := range c.SecuritySchemeChanges { + v += c.SecuritySchemeChanges[k].TotalChanges() + } + for k := range c.LinkChanges { + v += c.LinkChanges[k].TotalChanges() + } + for k := range c.CallbackChanges { + v += c.CallbackChanges[k].TotalChanges() + } + if c.ExtensionChanges != nil { + v += c.ExtensionChanges.TotalChanges() + } + return v +} + +func (c *ComponentsChanges) TotalBreakingChanges() int { + v := c.PropertyChanges.TotalBreakingChanges() + for k := range c.SchemaChanges { + v += c.SchemaChanges[k].TotalBreakingChanges() + } + for k := range c.ResponsesChanges { + v += c.ResponsesChanges[k].TotalBreakingChanges() + } + for k := range c.ParameterChanges { + v += c.ParameterChanges[k].TotalBreakingChanges() + } + for k := range c.ExamplesChanges { + v += c.ExamplesChanges[k].TotalBreakingChanges() + } + for k := range c.RequestBodyChanges { + v += c.RequestBodyChanges[k].TotalBreakingChanges() + } + for k := range c.HeaderChanges { + v += c.HeaderChanges[k].TotalBreakingChanges() + } + for k := range c.SecuritySchemeChanges { + v += c.SecuritySchemeChanges[k].TotalBreakingChanges() + } + for k := range c.LinkChanges { + v += c.LinkChanges[k].TotalBreakingChanges() + } + for k := range c.CallbackChanges { + v += c.CallbackChanges[k].TotalBreakingChanges() + } + if c.ExtensionChanges != nil { + v += c.ExtensionChanges.TotalBreakingChanges() + } + return v } diff --git a/what-changed/model/components_test.go b/what-changed/model/components_test.go index e6c99af..7d74069 100644 --- a/what-changed/model/components_test.go +++ b/what-changed/model/components_test.go @@ -2,3 +2,502 @@ // SPDX-License-Identifier: MIT package model + +import ( + "github.com/pb33f/libopenapi/datamodel/low" + v2 "github.com/pb33f/libopenapi/datamodel/low/v2" + v3low "github.com/pb33f/libopenapi/datamodel/low/v3" + "github.com/stretchr/testify/assert" + "gopkg.in/yaml.v3" + "testing" +) + +func TestCompareComponents_Swagger_Definitions_Equal(t *testing.T) { + + left := `thing1: + type: int + description: a thing +thing2: + type: string + description: another thing.` + + right := `thing1: + type: int + description: a thing +thing2: + type: string + description: another thing.` + + var lNode, rNode yaml.Node + _ = yaml.Unmarshal([]byte(left), &lNode) + _ = yaml.Unmarshal([]byte(right), &rNode) + + // create low level objects + var lDoc v2.Definitions + var rDoc v2.Definitions + _ = low.BuildModel(lNode.Content[0], &lDoc) + _ = low.BuildModel(rNode.Content[0], &rDoc) + _ = lDoc.Build(lNode.Content[0], nil) + _ = rDoc.Build(rNode.Content[0], nil) + + // compare. + extChanges := CompareComponents(&lDoc, &rDoc) + assert.Nil(t, extChanges) +} + +func TestCompareComponents_Swagger_Definitions_Modified(t *testing.T) { + + left := `thing1: + type: int + description: a thing +thing2: + type: int + description: another thing.` + + right := `thing1: + type: int + description: a thing +thing2: + type: string + description: another thing.` + + var lNode, rNode yaml.Node + _ = yaml.Unmarshal([]byte(left), &lNode) + _ = yaml.Unmarshal([]byte(right), &rNode) + + // create low level objects + var lDoc v2.Definitions + var rDoc v2.Definitions + _ = low.BuildModel(lNode.Content[0], &lDoc) + _ = low.BuildModel(rNode.Content[0], &rDoc) + _ = lDoc.Build(lNode.Content[0], nil) + _ = rDoc.Build(rNode.Content[0], nil) + + // compare. + extChanges := CompareComponents(&lDoc, &rDoc) + assert.Equal(t, 1, extChanges.TotalChanges()) + assert.Equal(t, 1, extChanges.TotalBreakingChanges()) + assert.Equal(t, 1, extChanges.SchemaChanges["thing2"].TotalChanges()) + +} + +func TestCompareComponents_Swagger_Definitions_Added(t *testing.T) { + + left := `thing1: + type: int + description: a thing +thing2: + type: string + description: another thing.` + + right := `thing1: + type: int + description: a thing +thing2: + type: string + description: another thing. +thing3: + type: int + description: added a thing` + + var lNode, rNode yaml.Node + _ = yaml.Unmarshal([]byte(left), &lNode) + _ = yaml.Unmarshal([]byte(right), &rNode) + + // create low level objects + var lDoc v2.Definitions + var rDoc v2.Definitions + _ = low.BuildModel(lNode.Content[0], &lDoc) + _ = low.BuildModel(rNode.Content[0], &rDoc) + _ = lDoc.Build(lNode.Content[0], nil) + _ = rDoc.Build(rNode.Content[0], nil) + + // compare. + extChanges := CompareComponents(&lDoc, &rDoc) + assert.Equal(t, 1, extChanges.TotalChanges()) + assert.Equal(t, 0, extChanges.TotalBreakingChanges()) + assert.Equal(t, ObjectAdded, extChanges.Changes[0].ChangeType) + +} + +func TestCompareComponents_Swagger_Definitions_Removed(t *testing.T) { + + left := `thing1: + type: int + description: a thing +thing2: + type: string + description: another thing.` + + right := `thing1: + type: int + description: a thing +thing2: + type: string + description: another thing. +thing3: + type: int + description: added a thing` + + var lNode, rNode yaml.Node + _ = yaml.Unmarshal([]byte(left), &lNode) + _ = yaml.Unmarshal([]byte(right), &rNode) + + // create low level objects + var lDoc v2.Definitions + var rDoc v2.Definitions + _ = low.BuildModel(lNode.Content[0], &lDoc) + _ = low.BuildModel(rNode.Content[0], &rDoc) + _ = lDoc.Build(lNode.Content[0], nil) + _ = rDoc.Build(rNode.Content[0], nil) + + // compare. + extChanges := CompareComponents(&rDoc, &lDoc) + assert.Equal(t, 1, extChanges.TotalChanges()) + assert.Equal(t, 1, extChanges.TotalBreakingChanges()) + assert.Equal(t, ObjectRemoved, extChanges.Changes[0].ChangeType) + assert.Equal(t, "thing3", extChanges.Changes[0].Original) +} + +func TestCompareComponents_Swagger_Parameters_Equal(t *testing.T) { + + left := `param1: + name: nap +param2: + name: sleep +param3: + name: snooze +` + right := `param1: + name: nap +param2: + name: sleep +param3: + name: snooze` + + var lNode, rNode yaml.Node + _ = yaml.Unmarshal([]byte(left), &lNode) + _ = yaml.Unmarshal([]byte(right), &rNode) + + // create low level objects + var lDoc v2.ParameterDefinitions + var rDoc v2.ParameterDefinitions + _ = low.BuildModel(lNode.Content[0], &lDoc) + _ = low.BuildModel(rNode.Content[0], &rDoc) + _ = lDoc.Build(lNode.Content[0], nil) + _ = rDoc.Build(rNode.Content[0], nil) + + // compare. + extChanges := CompareComponents(&lDoc, &rDoc) + assert.Nil(t, extChanges) +} + +func TestCompareComponents_Swagger_Parameters_Modified(t *testing.T) { + + left := `param1: + name: nap +param2: + name: sleep +param3: + name: snooze +` + right := `param1: + name: WIDE AWAKE +param2: + name: sleep +param3: + name: KINDA SNOOZ` + + var lNode, rNode yaml.Node + _ = yaml.Unmarshal([]byte(left), &lNode) + _ = yaml.Unmarshal([]byte(right), &rNode) + + // create low level objects + var lDoc v2.ParameterDefinitions + var rDoc v2.ParameterDefinitions + _ = low.BuildModel(lNode.Content[0], &lDoc) + _ = low.BuildModel(rNode.Content[0], &rDoc) + _ = lDoc.Build(lNode.Content[0], nil) + _ = rDoc.Build(rNode.Content[0], nil) + + // compare. + extChanges := CompareComponents(&lDoc, &rDoc) + assert.Equal(t, 2, extChanges.TotalChanges()) + assert.Equal(t, 2, extChanges.TotalBreakingChanges()) + assert.Equal(t, Modified, extChanges.ParameterChanges["param1"].Changes[0].ChangeType) + assert.Equal(t, "WIDE AWAKE", extChanges.ParameterChanges["param1"].Changes[0].New) + assert.Equal(t, "KINDA SNOOZ", extChanges.ParameterChanges["param3"].Changes[0].New) + assert.Equal(t, v3low.NameLabel, extChanges.ParameterChanges["param1"].Changes[0].Property) + +} + +func TestCompareComponents_Swagger_Parameters_Added(t *testing.T) { + + left := `param1: + name: nap +param2: + name: sleep +param3: + name: snooze +` + right := `param1: + name: nap +param2: + name: sleep +param3: + name: snooze +param4: + name: I woke up!` + + var lNode, rNode yaml.Node + _ = yaml.Unmarshal([]byte(left), &lNode) + _ = yaml.Unmarshal([]byte(right), &rNode) + + // create low level objects + var lDoc v2.ParameterDefinitions + var rDoc v2.ParameterDefinitions + _ = low.BuildModel(lNode.Content[0], &lDoc) + _ = low.BuildModel(rNode.Content[0], &rDoc) + _ = lDoc.Build(lNode.Content[0], nil) + _ = rDoc.Build(rNode.Content[0], nil) + + // compare. + extChanges := CompareComponents(&lDoc, &rDoc) + assert.Equal(t, 1, extChanges.TotalChanges()) + assert.Equal(t, 0, extChanges.TotalBreakingChanges()) + assert.Equal(t, ObjectAdded, extChanges.Changes[0].ChangeType) + assert.Equal(t, "param4", extChanges.Changes[0].New) +} + +func TestCompareComponents_Swagger_Parameters_Removed(t *testing.T) { + + left := `param1: + name: nap +param2: + name: sleep +param3: + name: snooze +` + right := `param1: + name: nap +param2: + name: sleep +param3: + name: snooze +param4: + name: I woke up!` + + var lNode, rNode yaml.Node + _ = yaml.Unmarshal([]byte(left), &lNode) + _ = yaml.Unmarshal([]byte(right), &rNode) + + // create low level objects + var lDoc v2.ParameterDefinitions + var rDoc v2.ParameterDefinitions + _ = low.BuildModel(lNode.Content[0], &lDoc) + _ = low.BuildModel(rNode.Content[0], &rDoc) + _ = lDoc.Build(lNode.Content[0], nil) + _ = rDoc.Build(rNode.Content[0], nil) + + // compare. + extChanges := CompareComponents(&rDoc, &lDoc) + assert.Equal(t, 1, extChanges.TotalChanges()) + assert.Equal(t, 1, extChanges.TotalBreakingChanges()) + assert.Equal(t, ObjectRemoved, extChanges.Changes[0].ChangeType) + assert.Equal(t, "param4", extChanges.Changes[0].Original) +} + +func TestCompareComponents_Swagger_Responses_Equal(t *testing.T) { + + left := `resp1: + description: hi! +resp2: + description: bye! +` + right := `resp1: + description: hi! +resp2: + description: bye!` + + var lNode, rNode yaml.Node + _ = yaml.Unmarshal([]byte(left), &lNode) + _ = yaml.Unmarshal([]byte(right), &rNode) + + // create low level objects + var lDoc v2.ResponsesDefinitions + var rDoc v2.ResponsesDefinitions + _ = low.BuildModel(lNode.Content[0], &lDoc) + _ = low.BuildModel(rNode.Content[0], &rDoc) + _ = lDoc.Build(lNode.Content[0], nil) + _ = rDoc.Build(rNode.Content[0], nil) + + // compare. + extChanges := CompareComponents(&lDoc, &rDoc) + assert.Nil(t, extChanges) +} + +func TestCompareComponents_Swagger_Responses_Modified(t *testing.T) { + + left := `resp1: + description: hi! +resp2: + description: bye! +` + right := `resp1: + description: hi! +resp2: + description: oh, so you want to change huh?` + + var lNode, rNode yaml.Node + _ = yaml.Unmarshal([]byte(left), &lNode) + _ = yaml.Unmarshal([]byte(right), &rNode) + + // create low level objects + var lDoc v2.ResponsesDefinitions + var rDoc v2.ResponsesDefinitions + _ = low.BuildModel(lNode.Content[0], &lDoc) + _ = low.BuildModel(rNode.Content[0], &rDoc) + _ = lDoc.Build(lNode.Content[0], nil) + _ = rDoc.Build(rNode.Content[0], nil) + + // compare. + extChanges := CompareComponents(&lDoc, &rDoc) + + assert.Equal(t, 1, extChanges.TotalChanges()) + assert.Equal(t, 0, extChanges.TotalBreakingChanges()) + assert.Equal(t, 1, extChanges.ResponsesChanges["resp2"].TotalChanges()) + assert.Equal(t, v3low.DescriptionLabel, extChanges.ResponsesChanges["resp2"].Changes[0].Property) +} + +func TestCompareComponents_Swagger_Responses_Added(t *testing.T) { + + left := `resp1: + description: hi! +resp2: + description: bye! +` + right := `resp1: + description: hi! +resp2: + description: bye! +resp3: + description: another response!` + + var lNode, rNode yaml.Node + _ = yaml.Unmarshal([]byte(left), &lNode) + _ = yaml.Unmarshal([]byte(right), &rNode) + + // create low level objects + var lDoc v2.ResponsesDefinitions + var rDoc v2.ResponsesDefinitions + _ = low.BuildModel(lNode.Content[0], &lDoc) + _ = low.BuildModel(rNode.Content[0], &rDoc) + _ = lDoc.Build(lNode.Content[0], nil) + _ = rDoc.Build(rNode.Content[0], nil) + + // compare. + extChanges := CompareComponents(&lDoc, &rDoc) + + assert.Equal(t, 1, extChanges.TotalChanges()) + assert.Equal(t, 0, extChanges.TotalBreakingChanges()) + assert.Equal(t, "resp3", extChanges.Changes[0].New) + assert.Equal(t, ObjectAdded, extChanges.Changes[0].ChangeType) + assert.Equal(t, v2.ResponsesLabel, extChanges.Changes[0].Property) +} + +func TestCompareComponents_Swagger_Responses_Removed(t *testing.T) { + + left := `resp1: + description: hi! +resp2: + description: bye! +` + right := `resp1: + description: hi! +resp2: + description: bye! +resp3: + description: another response!` + + var lNode, rNode yaml.Node + _ = yaml.Unmarshal([]byte(left), &lNode) + _ = yaml.Unmarshal([]byte(right), &rNode) + + // create low level objects + var lDoc v2.ResponsesDefinitions + var rDoc v2.ResponsesDefinitions + _ = low.BuildModel(lNode.Content[0], &lDoc) + _ = low.BuildModel(rNode.Content[0], &rDoc) + _ = lDoc.Build(lNode.Content[0], nil) + _ = rDoc.Build(rNode.Content[0], nil) + + // compare. + extChanges := CompareComponents(&rDoc, &lDoc) + + assert.Equal(t, 1, extChanges.TotalChanges()) + assert.Equal(t, 1, extChanges.TotalBreakingChanges()) + assert.Equal(t, "resp3", extChanges.Changes[0].Original) + assert.Equal(t, ObjectRemoved, extChanges.Changes[0].ChangeType) + assert.Equal(t, v2.ResponsesLabel, extChanges.Changes[0].Property) +} + +func TestCompareComponents_Swagger_SecurityDefinitions_Equal(t *testing.T) { + + left := `scheme1: + description: hi! +scheme2: + description: bye! +` + right := `scheme1: + description: hi! +scheme2: + description: bye!` + + var lNode, rNode yaml.Node + _ = yaml.Unmarshal([]byte(left), &lNode) + _ = yaml.Unmarshal([]byte(right), &rNode) + + // create low level objects + var lDoc v2.SecurityDefinitions + var rDoc v2.SecurityDefinitions + _ = low.BuildModel(lNode.Content[0], &lDoc) + _ = low.BuildModel(rNode.Content[0], &rDoc) + _ = lDoc.Build(lNode.Content[0], nil) + _ = rDoc.Build(rNode.Content[0], nil) + + // compare. + extChanges := CompareComponents(&lDoc, &rDoc) + assert.Nil(t, extChanges) +} + +func TestCompareComponents_Swagger_SecurityDefinitions_Modified(t *testing.T) { + + left := `scheme1: + description: hi! +scheme2: + description: bye! +` + right := `scheme1: + description: hi! +scheme2: + description: bye! again!` + + var lNode, rNode yaml.Node + _ = yaml.Unmarshal([]byte(left), &lNode) + _ = yaml.Unmarshal([]byte(right), &rNode) + + // create low level objects + var lDoc v2.SecurityDefinitions + var rDoc v2.SecurityDefinitions + _ = low.BuildModel(lNode.Content[0], &lDoc) + _ = low.BuildModel(rNode.Content[0], &rDoc) + _ = lDoc.Build(lNode.Content[0], nil) + _ = rDoc.Build(rNode.Content[0], nil) + + // compare. + extChanges := CompareComponents(&lDoc, &rDoc) + assert.Equal(t, 1, extChanges.TotalChanges()) + assert.Equal(t, 0, extChanges.TotalBreakingChanges()) + assert.Equal(t, 1, extChanges.SecuritySchemeChanges["scheme2"].TotalChanges()) + assert.Equal(t, v3low.DescriptionLabel, extChanges.SecuritySchemeChanges["scheme2"].Changes[0].Property) +}