diff --git a/datamodel/low/v2/response.go b/datamodel/low/v2/response.go index 77e4af4..8f1fe1e 100644 --- a/datamodel/low/v2/response.go +++ b/datamodel/low/v2/response.go @@ -77,8 +77,10 @@ func (r *Response) Hash() [32]byte { if !r.Schema.IsEmpty() { f = append(f, low.GenerateHashString(r.Schema.Value.Schema())) } - for k := range r.Examples.Value.Values { - f = append(f, low.GenerateHashString(r.Examples.Value.Values[k].Value)) + if !r.Examples.IsEmpty() { + for k := range r.Examples.Value.Values { + f = append(f, low.GenerateHashString(r.Examples.Value.Values[k].Value)) + } } for k := range r.Extensions { f = append(f, fmt.Sprintf("%s-%x", k.Value, diff --git a/datamodel/low/v3/response.go b/datamodel/low/v3/response.go index 5c56a7f..893d592 100644 --- a/datamodel/low/v3/response.go +++ b/datamodel/low/v3/response.go @@ -146,13 +146,13 @@ func (r *Response) Hash() [32]byte { f = append(f, r.Description.Value) } for k := range r.Headers.Value { - f = append(f, low.GenerateHashString(r.Headers.Value[k])) + f = append(f, low.GenerateHashString(r.Headers.Value[k].Value)) } for k := range r.Content.Value { - f = append(f, low.GenerateHashString(r.Content.Value[k])) + f = append(f, low.GenerateHashString(r.Content.Value[k].Value)) } for k := range r.Links.Value { - f = append(f, low.GenerateHashString(r.Links.Value[k])) + f = append(f, low.GenerateHashString(r.Links.Value[k].Value)) } for k := range r.Extensions { f = append(f, fmt.Sprintf("%s-%x", k.Value, diff --git a/what-changed/model/examples.go b/what-changed/model/examples.go new file mode 100644 index 0000000..2fb0992 --- /dev/null +++ b/what-changed/model/examples.go @@ -0,0 +1,77 @@ +// Copyright 2022 Princess B33f Heavy Industries / Dave Shanley +// SPDX-License-Identifier: MIT + +package model + +import ( + "github.com/pb33f/libopenapi/datamodel/low" + v2 "github.com/pb33f/libopenapi/datamodel/low/v2" +) + +// v2 Examples object. +type ExamplesChanges struct { + PropertyChanges +} + +func (a *ExamplesChanges) TotalChanges() int { + return a.PropertyChanges.TotalChanges() +} + +func (a *ExamplesChanges) TotalBreakingChanges() int { + return 0 // not supported. +} + +func CompareExamplesV2(l, r *v2.Examples) *ExamplesChanges { + + lHashes := make(map[string]string) + rHashes := make(map[string]string) + lValues := make(map[string]low.ValueReference[any]) + rValues := make(map[string]low.ValueReference[any]) + + for k := range l.Values { + lHashes[k.Value] = low.GenerateHashString(l.Values[k].Value) + lValues[k.Value] = l.Values[k] + } + + for k := range r.Values { + rHashes[k.Value] = low.GenerateHashString(r.Values[k].Value) + rValues[k.Value] = r.Values[k] + } + var changes []*Change + + // check left example hashes + for k := range lHashes { + rhash := rHashes[k] + if rhash == "" { + CreateChange(&changes, ObjectRemoved, k, + lValues[k].GetValueNode(), nil, false, + lValues[k].GetValue(), nil) + continue + } + if lHashes[k] == rHashes[k] { + continue + } + CreateChange(&changes, Modified, k, + lValues[k].GetValueNode(), rValues[k].GetValueNode(), false, + lValues[k].GetValue(), lValues[k].GetValue()) + + } + + //check right example hashes + for k := range rHashes { + lhash := lHashes[k] + if lhash == "" { + CreateChange(&changes, ObjectAdded, k, + nil, lValues[k].GetValueNode(), false, + nil, lValues[k].GetValue()) + continue + } + } + + ex := new(ExamplesChanges) + ex.Changes = changes + if ex.TotalChanges() <= 0 { + return nil + } + return ex +} diff --git a/what-changed/model/examples_test.go b/what-changed/model/examples_test.go new file mode 100644 index 0000000..9576555 --- /dev/null +++ b/what-changed/model/examples_test.go @@ -0,0 +1,108 @@ +// Copyright 2022 Princess B33f Heavy Industries / Dave Shanley +// SPDX-License-Identifier: MIT + +package model + +import ( + "github.com/pb33f/libopenapi/datamodel/low" + "github.com/pb33f/libopenapi/datamodel/low/v2" + "github.com/pb33f/libopenapi/datamodel/low/v3" + "github.com/stretchr/testify/assert" + "gopkg.in/yaml.v3" + "testing" +) + +func TestCompareExamplesV2(t *testing.T) { + + left := `summary: magic herbs` + right := `summary: cure all` + + var lNode, rNode yaml.Node + _ = yaml.Unmarshal([]byte(left), &lNode) + _ = yaml.Unmarshal([]byte(right), &rNode) + + // create low level objects + var lDoc v2.Examples + var rDoc v2.Examples + _ = low.BuildModel(&lNode, &lDoc) + _ = low.BuildModel(&rNode, &rDoc) + _ = lDoc.Build(lNode.Content[0], nil) + _ = rDoc.Build(rNode.Content[0], nil) + + extChanges := CompareExamplesV2(&lDoc, &rDoc) + assert.Equal(t, extChanges.TotalChanges(), 1) + assert.Equal(t, 0, extChanges.TotalBreakingChanges()) + assert.Equal(t, Modified, extChanges.Changes[0].ChangeType) + assert.Equal(t, v3.SummaryLabel, extChanges.Changes[0].Property) + assert.Equal(t, "magic herbs", extChanges.Changes[0].Original) + assert.Equal(t, "cure all", extChanges.Changes[0].New) +} + +func TestCompareExamplesV2_Add(t *testing.T) { + + left := `summary: magic herbs` + right := `summary: magic herbs +yummy: coffee` + + var lNode, rNode yaml.Node + _ = yaml.Unmarshal([]byte(left), &lNode) + _ = yaml.Unmarshal([]byte(right), &rNode) + + // create low level objects + var lDoc v2.Examples + var rDoc v2.Examples + _ = low.BuildModel(&lNode, &lDoc) + _ = low.BuildModel(&rNode, &rDoc) + _ = lDoc.Build(lNode.Content[0], nil) + _ = rDoc.Build(rNode.Content[0], nil) + + extChanges := CompareExamplesV2(&lDoc, &rDoc) + assert.Equal(t, extChanges.TotalChanges(), 1) + assert.Equal(t, 0, extChanges.TotalBreakingChanges()) + assert.Equal(t, ObjectAdded, extChanges.Changes[0].ChangeType) +} + +func TestCompareExamplesV2_Remove(t *testing.T) { + + left := `summary: magic herbs` + right := `summary: magic herbs +yummy: coffee` + + var lNode, rNode yaml.Node + _ = yaml.Unmarshal([]byte(left), &lNode) + _ = yaml.Unmarshal([]byte(right), &rNode) + + // create low level objects + var lDoc v2.Examples + var rDoc v2.Examples + _ = low.BuildModel(&lNode, &lDoc) + _ = low.BuildModel(&rNode, &rDoc) + _ = lDoc.Build(lNode.Content[0], nil) + _ = rDoc.Build(rNode.Content[0], nil) + + extChanges := CompareExamplesV2(&rDoc, &lDoc) + assert.Equal(t, extChanges.TotalChanges(), 1) + assert.Equal(t, 0, extChanges.TotalBreakingChanges()) + assert.Equal(t, ObjectRemoved, extChanges.Changes[0].ChangeType) +} + +func TestCompareExamplesV2_Identical(t *testing.T) { + + left := `summary: magic herbs` + right := left + + var lNode, rNode yaml.Node + _ = yaml.Unmarshal([]byte(left), &lNode) + _ = yaml.Unmarshal([]byte(right), &rNode) + + // create low level objects + var lDoc v2.Examples + var rDoc v2.Examples + _ = low.BuildModel(&lNode, &lDoc) + _ = low.BuildModel(&rNode, &rDoc) + _ = lDoc.Build(lNode.Content[0], nil) + _ = rDoc.Build(rNode.Content[0], nil) + + extChanges := CompareExamplesV2(&rDoc, &lDoc) + assert.Nil(t, extChanges) +} diff --git a/what-changed/model/response.go b/what-changed/model/response.go index e6c99af..1a7dcff 100644 --- a/what-changed/model/response.go +++ b/what-changed/model/response.go @@ -2,3 +2,156 @@ // SPDX-License-Identifier: MIT package model + +import ( + "github.com/pb33f/libopenapi/datamodel/low" + "github.com/pb33f/libopenapi/datamodel/low/v2" + "github.com/pb33f/libopenapi/datamodel/low/v3" + "reflect" +) + +type ResponseChanges struct { + PropertyChanges + ExtensionChanges *ExtensionChanges + HeadersChanges map[string]*HeaderChanges + + // v2 + SchemaChanges *SchemaChanges + ExamplesChanges *ExamplesChanges + + // v3 + ContentChanges map[string]*MediaTypeChanges + LinkChanges map[string]*LinkChanges + ServerChanges *ServerChanges +} + +func (r *ResponseChanges) TotalChanges() int { + c := r.PropertyChanges.TotalChanges() + if r.ExtensionChanges != nil { + c += r.ExtensionChanges.TotalChanges() + } + if r.SchemaChanges != nil { + c += r.SchemaChanges.TotalChanges() + } + if r.ExamplesChanges != nil { + c += r.ExamplesChanges.TotalChanges() + } + for k := range r.HeadersChanges { + c += r.HeadersChanges[k].TotalChanges() + } + for k := range r.ContentChanges { + c += r.ContentChanges[k].TotalChanges() + } + for k := range r.LinkChanges { + c += r.LinkChanges[k].TotalChanges() + } + return c +} + +func (r *ResponseChanges) TotalBreakingChanges() int { + c := r.PropertyChanges.TotalBreakingChanges() + if r.SchemaChanges != nil { + c += r.SchemaChanges.TotalBreakingChanges() + } + for k := range r.HeadersChanges { + c += r.HeadersChanges[k].TotalBreakingChanges() + } + for k := range r.ContentChanges { + c += r.ContentChanges[k].TotalBreakingChanges() + } + for k := range r.LinkChanges { + c += r.LinkChanges[k].TotalBreakingChanges() + } + return c +} + +func CompareResponse(l, r any) *ResponseChanges { + + var changes []*Change + var props []*PropertyCheck + + rc := new(ResponseChanges) + + if reflect.TypeOf(&v2.Response{}) == reflect.TypeOf(l) && reflect.TypeOf(&v2.Response{}) == reflect.TypeOf(r) { + + lResponse := l.(*v2.Response) + rResponse := r.(*v2.Response) + + // perform hash check to avoid further processing + if low.AreEqual(lResponse, rResponse) { + return nil + } + + // description + addPropertyCheck(&props, lResponse.Description.ValueNode, rResponse.Description.ValueNode, + lResponse.Description.Value, lResponse.Description.Value, &changes, v3.DescriptionLabel, false) + + if !lResponse.Schema.IsEmpty() && !rResponse.Schema.IsEmpty() { + rc.SchemaChanges = CompareSchemas(lResponse.Schema.Value, rResponse.Schema.Value) + } + if !lResponse.Schema.IsEmpty() && rResponse.Schema.IsEmpty() { + CreateChange(&changes, ObjectRemoved, v3.SchemaLabel, + lResponse.Schema.ValueNode, nil, true, + lResponse.Schema.Value, nil) + } + if lResponse.Schema.IsEmpty() && !rResponse.Schema.IsEmpty() { + CreateChange(&changes, ObjectAdded, v3.SchemaLabel, + nil, rResponse.Schema.ValueNode, true, + nil, lResponse.Schema.Value) + } + + rc.HeadersChanges = + CheckMapForChanges(lResponse.Headers.Value, rResponse.Headers.Value, + &changes, v3.HeadersLabel, CompareHeadersV2) + + if !lResponse.Examples.IsEmpty() && !rResponse.Examples.IsEmpty() { + rc.ExamplesChanges = CompareExamplesV2(lResponse.Examples.Value, rResponse.Examples.Value) + } + if !lResponse.Examples.IsEmpty() && rResponse.Examples.IsEmpty() { + CreateChange(&changes, PropertyRemoved, v3.ExamplesLabel, + lResponse.Schema.ValueNode, nil, false, + lResponse.Schema.Value, nil) + } + if lResponse.Examples.IsEmpty() && !rResponse.Examples.IsEmpty() { + CreateChange(&changes, ObjectAdded, v3.ExamplesLabel, + nil, rResponse.Schema.ValueNode, false, + nil, lResponse.Schema.Value) + } + + rc.ExtensionChanges = CompareExtensions(lResponse.Extensions, rResponse.Extensions) + } + + if reflect.TypeOf(&v3.Response{}) == reflect.TypeOf(l) && reflect.TypeOf(&v3.Response{}) == reflect.TypeOf(r) { + + lResponse := l.(*v3.Response) + rResponse := r.(*v3.Response) + + // perform hash check to avoid further processing + if low.AreEqual(lResponse, rResponse) { + return nil + } + + // description + addPropertyCheck(&props, lResponse.Description.ValueNode, rResponse.Description.ValueNode, + lResponse.Description.Value, lResponse.Description.Value, &changes, v3.DescriptionLabel, false) + + rc.HeadersChanges = + CheckMapForChanges(lResponse.Headers.Value, rResponse.Headers.Value, + &changes, v3.HeadersLabel, CompareHeadersV3) + + rc.ContentChanges = + CheckMapForChanges(lResponse.Content.Value, rResponse.Content.Value, + &changes, v3.ContentLabel, CompareMediaTypes) + + rc.LinkChanges = + CheckMapForChanges(lResponse.Links.Value, rResponse.Links.Value, + &changes, v3.LinksLabel, CompareLinks) + + rc.ExtensionChanges = CompareExtensions(lResponse.Extensions, rResponse.Extensions) + } + + CheckProperties(props) + rc.Changes = changes + return rc + +} diff --git a/what-changed/model/response_test.go b/what-changed/model/response_test.go new file mode 100644 index 0000000..db4a4da --- /dev/null +++ b/what-changed/model/response_test.go @@ -0,0 +1,227 @@ +// Copyright 2022 Princess B33f Heavy Industries / Dave Shanley +// SPDX-License-Identifier: MIT + +package model + +import ( + "github.com/pb33f/libopenapi/datamodel/low" + "github.com/pb33f/libopenapi/datamodel/low/v2" + "github.com/pb33f/libopenapi/datamodel/low/v3" + "github.com/stretchr/testify/assert" + "gopkg.in/yaml.v3" + "testing" +) + +func TestCompareResponse_V2(t *testing.T) { + + left := `description: response +schema: + type: string +headers: + thing: + description: a header +examples: + bam: alam +x-toot: poot` + right := left + + var lNode, rNode yaml.Node + _ = yaml.Unmarshal([]byte(left), &lNode) + _ = yaml.Unmarshal([]byte(right), &rNode) + + // create low level objects + var lDoc v2.Response + var rDoc v2.Response + _ = low.BuildModel(&lNode, &lDoc) + _ = low.BuildModel(&rNode, &rDoc) + _ = lDoc.Build(lNode.Content[0], nil) + _ = rDoc.Build(rNode.Content[0], nil) + + extChanges := CompareResponse(&lDoc, &rDoc) + assert.Nil(t, extChanges) + +} + +func TestCompareResponse_V2_Modify(t *testing.T) { + + left := `description: response +schema: + type: string +headers: + thing: + description: a header +examples: + bam: alam` + + right := `description: response changed +schema: + type: int +headers: + thing: + description: a header changed +examples: + bam: alabama +x-toot: poot` + + var lNode, rNode yaml.Node + _ = yaml.Unmarshal([]byte(left), &lNode) + _ = yaml.Unmarshal([]byte(right), &rNode) + + // create low level objects + var lDoc v2.Response + var rDoc v2.Response + _ = low.BuildModel(&lNode, &lDoc) + _ = low.BuildModel(&rNode, &rDoc) + _ = lDoc.Build(lNode.Content[0], nil) + _ = rDoc.Build(rNode.Content[0], nil) + + extChanges := CompareResponse(&lDoc, &rDoc) + assert.Equal(t, 5, extChanges.TotalChanges()) + assert.Equal(t, 1, extChanges.TotalBreakingChanges()) +} + +func TestCompareResponse_V2_Add(t *testing.T) { + + left := `description: response +headers: + thing: + description: a header` + + right := `description: response +schema: + type: int +headers: + thing: + description: a header +examples: + bam: alam` + + var lNode, rNode yaml.Node + _ = yaml.Unmarshal([]byte(left), &lNode) + _ = yaml.Unmarshal([]byte(right), &rNode) + + // create low level objects + var lDoc v2.Response + var rDoc v2.Response + _ = low.BuildModel(&lNode, &lDoc) + _ = low.BuildModel(&rNode, &rDoc) + _ = lDoc.Build(lNode.Content[0], nil) + _ = rDoc.Build(rNode.Content[0], nil) + + extChanges := CompareResponse(&lDoc, &rDoc) + assert.Equal(t, 2, extChanges.TotalChanges()) + assert.Equal(t, 1, extChanges.TotalBreakingChanges()) +} + +func TestCompareResponse_V2_Remove(t *testing.T) { + + left := `description: response +headers: + thing: + description: a header` + + right := `description: response +schema: + type: int +headers: + thing: + description: a header +examples: + bam: alabama` + + var lNode, rNode yaml.Node + _ = yaml.Unmarshal([]byte(left), &lNode) + _ = yaml.Unmarshal([]byte(right), &rNode) + + // create low level objects + var lDoc v2.Response + var rDoc v2.Response + _ = low.BuildModel(&lNode, &lDoc) + _ = low.BuildModel(&rNode, &rDoc) + _ = lDoc.Build(lNode.Content[0], nil) + _ = rDoc.Build(rNode.Content[0], nil) + + extChanges := CompareResponse(&rDoc, &lDoc) + assert.Equal(t, 2, extChanges.TotalChanges()) + assert.Equal(t, 1, extChanges.TotalBreakingChanges()) +} + +func TestCompareResponse_V3(t *testing.T) { + + left := `description: response +content: + application/json: + schema: + type: string +headers: + thing: + description: a header +links: + aLink: + operationId: oneTwoThree +x-toot: poot` + right := left + + var lNode, rNode yaml.Node + _ = yaml.Unmarshal([]byte(left), &lNode) + _ = yaml.Unmarshal([]byte(right), &rNode) + + // create low level objects + var lDoc v3.Response + var rDoc v3.Response + _ = low.BuildModel(&lNode, &lDoc) + _ = low.BuildModel(&rNode, &rDoc) + _ = lDoc.Build(lNode.Content[0], nil) + _ = rDoc.Build(rNode.Content[0], nil) + + extChanges := CompareResponse(&lDoc, &rDoc) + assert.Nil(t, extChanges) +} + +func TestCompareResponse_V3_Modify(t *testing.T) { + + left := `description: response +content: + application/json: + schema: + type: string +headers: + thing: + description: a header +links: + aLink: + operationId: oneTwoThree +server: + url: https://pb33f.io +x-toot: poot` + + right := `links: + aLink: + operationId: oneTwoThreeFour +content: + application/json: + schema: + type: int +description: response change +headers: + thing: + description: a header changed +x-toot: pooty` + + var lNode, rNode yaml.Node + _ = yaml.Unmarshal([]byte(left), &lNode) + _ = yaml.Unmarshal([]byte(right), &rNode) + + // create low level objects + var lDoc v3.Response + var rDoc v3.Response + _ = low.BuildModel(&lNode, &lDoc) + _ = low.BuildModel(&rNode, &rDoc) + _ = lDoc.Build(lNode.Content[0], nil) + _ = rDoc.Build(rNode.Content[0], nil) + + extChanges := CompareResponse(&lDoc, &rDoc) + + assert.Equal(t, 5, extChanges.TotalChanges()) + assert.Equal(t, 2, extChanges.TotalBreakingChanges()) +}