diff --git a/datamodel/low/v3/constants.go b/datamodel/low/v3/constants.go index f963532..19cd219 100644 --- a/datamodel/low/v3/constants.go +++ b/datamodel/low/v3/constants.go @@ -38,4 +38,6 @@ const ( PasswordLabel = "password" ClientCredentialsLabel = "clientCredentials" AuthorizationCodeLabel = "authorizationCode" + DescriptionLabel = "description" + URLLabel = "url" ) diff --git a/what-changed/extensions.go b/what-changed/extensions.go new file mode 100644 index 0000000..c84289e --- /dev/null +++ b/what-changed/extensions.go @@ -0,0 +1,89 @@ +// Copyright 2022 Princess B33f Heavy Industries / Dave Shanley +// SPDX-License-Identifier: MIT + +package what_changed + +import ( + "fmt" + "github.com/pb33f/libopenapi/datamodel/low" + "strings" +) + +type ExtensionChanges struct { + PropertyChanges +} + +func CompareExtensions(l, r map[low.KeyReference[string]]low.ValueReference[any]) *ExtensionChanges { + + // look at the original and then look through the new. + seenLeft := make(map[string]*low.ValueReference[any]) + seenRight := make(map[string]*low.ValueReference[any]) + for i := range l { + h := l[i] + seenLeft[strings.ToLower(i.Value)] = &h + } + for i := range r { + h := r[i] + seenRight[strings.ToLower(i.Value)] = &h + } + + var changes []*Change + var changeType int + for i := range seenLeft { + changeType = 0 + if seenRight[i] == nil { + // deleted + changeType = PropertyRemoved + ctx := CreateContext(seenLeft[i].ValueNode, nil) + changes = append(changes, &Change{ + Context: ctx, + ChangeType: changeType, + Property: i, + Original: fmt.Sprintf("%v", seenLeft[i].Value), + }) + + } + if seenRight[i] != nil { + // potentially modified and or moved + ctx := CreateContext(seenLeft[i].ValueNode, seenRight[i].ValueNode) + if seenLeft[i].Value != seenRight[i].Value { + changeType = Modified + } + if ctx.HasChanged() { + if changeType == Modified { + changeType = ModifiedAndMoved + } else { + changeType = Moved + } + } + if changeType != 0 { + changes = append(changes, &Change{ + Context: ctx, + ChangeType: changeType, + Property: i, + Original: fmt.Sprintf("%v", seenLeft[i].Value), + New: fmt.Sprintf("%v", seenRight[i].Value), + }) + } + } + } + for i := range seenRight { + if seenLeft[i] == nil { + // added + ctx := CreateContext(nil, seenRight[i].ValueNode) + changes = append(changes, &Change{ + Context: ctx, + ChangeType: PropertyAdded, + Property: i, + New: fmt.Sprintf("%v", seenRight[i].Value), + }) + } + } + + if len(changes) <= 0 { + return nil + } + ex := new(ExtensionChanges) + ex.Changes = changes + return ex +} diff --git a/what-changed/extensions_test.go b/what-changed/extensions_test.go new file mode 100644 index 0000000..df6afab --- /dev/null +++ b/what-changed/extensions_test.go @@ -0,0 +1,146 @@ +// Copyright 2022 Princess B33f Heavy Industries / Dave Shanley +// SPDX-License-Identifier: MIT + +package what_changed + +import ( + "github.com/pb33f/libopenapi/datamodel/low" + "github.com/stretchr/testify/assert" + "gopkg.in/yaml.v3" + "testing" +) + +func TestCompareExtensions(t *testing.T) { + + left := `x-test: 1` + right := `x-test: 2` + + var lNode, rNode yaml.Node + _ = yaml.Unmarshal([]byte(left), &lNode) + _ = yaml.Unmarshal([]byte(right), &rNode) + + lExt := low.ExtractExtensions(lNode.Content[0]) + rExt := low.ExtractExtensions(rNode.Content[0]) + + extChanges := CompareExtensions(lExt, rExt) + + assert.Len(t, extChanges.Changes, 1) + assert.Equal(t, Modified, extChanges.Changes[0].ChangeType) + assert.Equal(t, "1", extChanges.Changes[0].Original) + assert.Equal(t, "2", extChanges.Changes[0].New) + assert.False(t, extChanges.Changes[0].Context.HasChanged()) +} + +func TestCompareExtensions_Moved(t *testing.T) { + + left := `pizza: pie +x-test: 1` + + right := `x-test: 1` + + var lNode, rNode yaml.Node + _ = yaml.Unmarshal([]byte(left), &lNode) + _ = yaml.Unmarshal([]byte(right), &rNode) + + lExt := low.ExtractExtensions(lNode.Content[0]) + rExt := low.ExtractExtensions(rNode.Content[0]) + + extChanges := CompareExtensions(lExt, rExt) + + assert.Len(t, extChanges.Changes, 1) + assert.Equal(t, Moved, extChanges.Changes[0].ChangeType) + assert.Equal(t, 2, extChanges.Changes[0].Context.OrigLine) + assert.Equal(t, 1, extChanges.Changes[0].Context.NewLine) + assert.True(t, extChanges.Changes[0].Context.HasChanged()) +} + +func TestCompareExtensions_ModifiedAndMoved(t *testing.T) { + + left := `pizza: pie +x-test: 1` + + right := `x-test: 2` + + var lNode, rNode yaml.Node + _ = yaml.Unmarshal([]byte(left), &lNode) + _ = yaml.Unmarshal([]byte(right), &rNode) + + lExt := low.ExtractExtensions(lNode.Content[0]) + rExt := low.ExtractExtensions(rNode.Content[0]) + + extChanges := CompareExtensions(lExt, rExt) + + assert.Len(t, extChanges.Changes, 1) + assert.Equal(t, ModifiedAndMoved, extChanges.Changes[0].ChangeType) + assert.Equal(t, 2, extChanges.Changes[0].Context.OrigLine) + assert.Equal(t, 1, extChanges.Changes[0].Context.NewLine) + assert.Equal(t, "1", extChanges.Changes[0].Original) + assert.Equal(t, "2", extChanges.Changes[0].New) + assert.True(t, extChanges.Changes[0].Context.HasChanged()) +} + +func TestCompareExtensions_Removed(t *testing.T) { + + left := `pizza: pie +x-test: 1` + + right := `pizza: pie` + + var lNode, rNode yaml.Node + _ = yaml.Unmarshal([]byte(left), &lNode) + _ = yaml.Unmarshal([]byte(right), &rNode) + + lExt := low.ExtractExtensions(lNode.Content[0]) + rExt := low.ExtractExtensions(rNode.Content[0]) + + extChanges := CompareExtensions(lExt, rExt) + + assert.Len(t, extChanges.Changes, 1) + assert.Equal(t, PropertyRemoved, extChanges.Changes[0].ChangeType) + assert.Equal(t, 2, extChanges.Changes[0].Context.OrigLine) + assert.Equal(t, -1, extChanges.Changes[0].Context.NewLine) + assert.Equal(t, "1", extChanges.Changes[0].Original) + assert.True(t, extChanges.Changes[0].Context.HasChanged()) +} + +func TestCompareExtensions_Added(t *testing.T) { + + left := `pizza: pie` + + right := `pizza: pie +x-test: 1` + + var lNode, rNode yaml.Node + _ = yaml.Unmarshal([]byte(left), &lNode) + _ = yaml.Unmarshal([]byte(right), &rNode) + + lExt := low.ExtractExtensions(lNode.Content[0]) + rExt := low.ExtractExtensions(rNode.Content[0]) + + extChanges := CompareExtensions(lExt, rExt) + + assert.Len(t, extChanges.Changes, 1) + assert.Equal(t, PropertyAdded, extChanges.Changes[0].ChangeType) + assert.Equal(t, -1, extChanges.Changes[0].Context.OrigLine) + assert.Equal(t, 2, extChanges.Changes[0].Context.NewLine) + assert.Equal(t, "1", extChanges.Changes[0].New) + assert.True(t, extChanges.Changes[0].Context.HasChanged()) +} + +func TestCompareExtensions_Identical(t *testing.T) { + + left := `x-test: 1` + + right := `x-test: 1` + + var lNode, rNode yaml.Node + _ = yaml.Unmarshal([]byte(left), &lNode) + _ = yaml.Unmarshal([]byte(right), &rNode) + + lExt := low.ExtractExtensions(lNode.Content[0]) + rExt := low.ExtractExtensions(rNode.Content[0]) + + extChanges := CompareExtensions(lExt, rExt) + + assert.Nil(t, extChanges) +} diff --git a/what-changed/external_docs.go b/what-changed/external_docs.go new file mode 100644 index 0000000..144ee24 --- /dev/null +++ b/what-changed/external_docs.go @@ -0,0 +1,63 @@ +// Copyright 2022 Princess B33f Heavy Industries / Dave Shanley +// SPDX-License-Identifier: MIT + +package what_changed + +import ( + "github.com/pb33f/libopenapi/datamodel/low" + lowbase "github.com/pb33f/libopenapi/datamodel/low/base" + lowv3 "github.com/pb33f/libopenapi/datamodel/low/v3" +) + +type ExternalDocChanges struct { + PropertyChanges + ExtensionChanges *ExtensionChanges +} + +func CompareExternalDocs(l, r *lowbase.ExternalDoc) *ExternalDocChanges { + var changes []*Change + changeType := 0 + if l != nil && r != nil && l.URL.Value != r.URL.Value { + changeType = Modified + ctx := CreateContext(l.URL.ValueNode, r.URL.ValueNode) + if ctx.HasChanged() { + changeType = ModifiedAndMoved + } + changes = append(changes, &Change{ + Context: ctx, + ChangeType: changeType, + Property: lowv3.URLLabel, + Original: l.URL.Value, + New: r.URL.Value, + }) + } + if l != nil && r != nil && l.Description.Value != r.Description.Value { + changeType = Modified + ctx := CreateContext(l.Description.ValueNode, r.Description.ValueNode) + if ctx.HasChanged() { + changeType = ModifiedAndMoved + } + changes = append(changes, &Change{ + Context: ctx, + ChangeType: changeType, + Property: lowv3.DescriptionLabel, + Original: l.Description.Value, + New: r.Description.Value, + }) + } + if changeType == 0 { + // no change, return nothing. + return nil + } + dc := new(ExternalDocChanges) + dc.Changes = changes + var lExt, rExt map[low.KeyReference[string]]low.ValueReference[any] + if l != nil && len(l.Extensions) > 0 { + lExt = l.Extensions + } + if r != nil && len(r.Extensions) > 0 { + rExt = r.Extensions + } + dc.ExtensionChanges = CompareExtensions(lExt, rExt) + return dc +} diff --git a/what-changed/external_docs_test.go b/what-changed/external_docs_test.go new file mode 100644 index 0000000..a9ba48b --- /dev/null +++ b/what-changed/external_docs_test.go @@ -0,0 +1,154 @@ +// Copyright 2022 Princess B33f Heavy Industries / Dave Shanley +// SPDX-License-Identifier: MIT + +package what_changed + +import ( + "github.com/pb33f/libopenapi/datamodel/low" + lowbase "github.com/pb33f/libopenapi/datamodel/low/base" + lowv3 "github.com/pb33f/libopenapi/datamodel/low/v3" + "github.com/stretchr/testify/assert" + "gopkg.in/yaml.v3" + "testing" +) + +func TestCompareExternalDocs(t *testing.T) { + + left := `url: https://pb33f.io +description: this is a test +x-testing: hello` + + right := `url: https://quobix.com +description: this is another test +x-testing: hiya!` + + var lNode, rNode yaml.Node + _ = yaml.Unmarshal([]byte(left), &lNode) + _ = yaml.Unmarshal([]byte(right), &rNode) + + // create low level objects + var lDoc lowbase.ExternalDoc + var rDoc lowbase.ExternalDoc + _ = low.BuildModel(&lNode, &lDoc) + _ = low.BuildModel(&rNode, &rDoc) + _ = lDoc.Build(lNode.Content[0], nil) + _ = rDoc.Build(rNode.Content[0], nil) + + // compare. + extChanges := CompareExternalDocs(&lDoc, &rDoc) + assert.Len(t, extChanges.ExtensionChanges.Changes, 1) + assert.Len(t, extChanges.Changes, 2) + + // validate property changes + urlChange := extChanges.Changes[0] + assert.Equal(t, Modified, urlChange.ChangeType) + assert.False(t, urlChange.Context.HasChanged()) + assert.Equal(t, "https://pb33f.io", urlChange.Original) + assert.Equal(t, "https://quobix.com", urlChange.New) + assert.Equal(t, 1, urlChange.Context.OrigLine) + assert.Equal(t, lowv3.URLLabel, urlChange.Property) + + descChange := extChanges.Changes[1] + assert.Equal(t, Modified, descChange.ChangeType) + assert.False(t, descChange.Context.HasChanged()) + assert.Equal(t, "this is another test", descChange.New) + assert.Equal(t, "this is a test", descChange.Original) + assert.Equal(t, 2, descChange.Context.OrigLine) + assert.Equal(t, 14, descChange.Context.OrigCol) + + // validate extensions + extChange := extChanges.ExtensionChanges.Changes[0] + assert.Equal(t, Modified, extChange.ChangeType) + assert.False(t, extChange.Context.HasChanged()) + assert.Equal(t, "hiya!", extChange.New) + assert.Equal(t, "hello", extChange.Original) + assert.Equal(t, 3, extChange.Context.OrigLine) + assert.Equal(t, 12, extChange.Context.OrigCol) + +} + +func TestCompareExternalDocs_Moved(t *testing.T) { + + left := `url: https://pb33f.io +description: this is a test +x-testing: hello` + + right := `description: this is another test +x-testing: hiya! +url: https://quobix.com` + + var lNode, rNode yaml.Node + _ = yaml.Unmarshal([]byte(left), &lNode) + _ = yaml.Unmarshal([]byte(right), &rNode) + + // create low level objects + var lDoc lowbase.ExternalDoc + var rDoc lowbase.ExternalDoc + _ = low.BuildModel(&lNode, &lDoc) + _ = low.BuildModel(&rNode, &rDoc) + _ = lDoc.Build(lNode.Content[0], nil) + _ = rDoc.Build(rNode.Content[0], nil) + + // compare. + extChanges := CompareExternalDocs(&lDoc, &rDoc) + assert.Len(t, extChanges.ExtensionChanges.Changes, 1) + assert.Len(t, extChanges.Changes, 2) + + // validate property changes + urlChange := extChanges.Changes[0] + assert.Equal(t, ModifiedAndMoved, urlChange.ChangeType) + assert.True(t, urlChange.Context.HasChanged()) + assert.Equal(t, "https://pb33f.io", urlChange.Original) + assert.Equal(t, "https://quobix.com", urlChange.New) + assert.Equal(t, 1, urlChange.Context.OrigLine) + assert.Equal(t, 3, urlChange.Context.NewLine) + assert.Equal(t, lowv3.URLLabel, urlChange.Property) + + descChange := extChanges.Changes[1] + assert.Equal(t, ModifiedAndMoved, descChange.ChangeType) + assert.True(t, descChange.Context.HasChanged()) + assert.Equal(t, "this is another test", descChange.New) + assert.Equal(t, "this is a test", descChange.Original) + assert.Equal(t, 2, descChange.Context.OrigLine) + assert.Equal(t, 14, descChange.Context.OrigCol) + assert.Equal(t, 1, descChange.Context.NewLine) + assert.Equal(t, 14, descChange.Context.NewCol) + + // validate extensions + extChange := extChanges.ExtensionChanges.Changes[0] + assert.Equal(t, ModifiedAndMoved, extChange.ChangeType) + assert.True(t, extChange.Context.HasChanged()) + assert.Equal(t, "hiya!", extChange.New) + assert.Equal(t, "hello", extChange.Original) + assert.Equal(t, 3, extChange.Context.OrigLine) + assert.Equal(t, 12, extChange.Context.OrigCol) + assert.Equal(t, 2, extChange.Context.NewLine) + assert.Equal(t, 12, extChange.Context.NewCol) +} + +func TestCompareExternalDocs_Identical(t *testing.T) { + + left := `url: https://pb33f.io +description: this is a test +x-testing: hello` + + right := `url: https://pb33f.io +description: this is a test +x-testing: hello` + + var lNode, rNode yaml.Node + _ = yaml.Unmarshal([]byte(left), &lNode) + _ = yaml.Unmarshal([]byte(right), &rNode) + + // create low level objects + var lDoc lowbase.ExternalDoc + var rDoc lowbase.ExternalDoc + _ = low.BuildModel(&lNode, &lDoc) + _ = low.BuildModel(&rNode, &rDoc) + _ = lDoc.Build(lNode.Content[0], nil) + _ = rDoc.Build(rNode.Content[0], nil) + + // compare. + extChanges := CompareExternalDocs(&lDoc, &rDoc) + assert.Nil(t, extChanges) +} diff --git a/what-changed/what_changed.go b/what-changed/what_changed.go index 0a95109..5237ee9 100644 --- a/what-changed/what_changed.go +++ b/what-changed/what_changed.go @@ -2,3 +2,176 @@ // SPDX-License-Identifier: MIT package what_changed + +import ( + "gopkg.in/yaml.v3" +) + +const ( + Modified = iota + 1 + PropertyAdded + ObjectAdded + ObjectRemoved + PropertyRemoved + Moved + ModifiedAndMoved +) + +type WhatChanged struct { + Added int + Removed int + Modified int + Moved int + TotalChanges int + Changes *Changes +} + +type ChangeContext struct { + OrigLine int + OrigCol int + NewLine int + NewCol int +} + +func (c *ChangeContext) HasChanged() bool { + return c.NewLine != c.OrigLine || c.NewCol != c.OrigCol +} + +type Change struct { + Context *ChangeContext + ChangeType int + Property string + Original string + New string +} + +type PropertyChanges struct { + Changes []*Change +} + +type TagChanges struct { + PropertyChanges + ExternalDocs *ExternalDocChanges +} + +type Changes struct { + TagChanges *TagChanges +} + +//func WhatChangedBetweenDocuments(leftDocument, rightDocument *lowv3.Document) *WhatChanged { +// +// // compare tags +// //leftTags := leftDocument.Tags.Value +// //rightTags := rightDocument.Tags.Value +// +// return nil +//} + +func CreateContext(l, r *yaml.Node) *ChangeContext { + ctx := new(ChangeContext) + if l != nil { + ctx.OrigLine = l.Line + ctx.OrigCol = l.Column + } else { + ctx.OrigLine = -1 + ctx.OrigCol = -1 + } + if r != nil { + ctx.NewLine = r.Line + ctx.NewCol = r.Column + } else { + ctx.NewLine = -1 + ctx.NewCol = -1 + } + return ctx +} + +// +//func compareTags(l, r []low.ValueReference[*lowbase.Tag]) *TagChanges { +// +// tc := new(TagChanges) +// +// // look at the original and then look through the new. +// seenLeft := make(map[string]*low.ValueReference[*lowbase.Tag]) +// seenRight := make(map[string]*low.ValueReference[*lowbase.Tag]) +// for i := range l { +// seenLeft[strings.ToLower(l[i].Value.Name.Value)] = &l[i] +// } +// for i := range r { +// seenRight[strings.ToLower(l[i].Value.Name.Value)] = &l[i] +// } +// +// for i := range seenLeft { +// if seenRight[i] == nil { +// // deleted +// //ctx := CreateContext(seenLeft[i].ValueNode, nil) +// //tc.Changes = +// +// } +// if seenRight[i] != nil { +// +// // potentially modified and or moved +// } +// } +// +// for i := range seenRight { +// if seenLeft[i] == nil { +// // added +// } +// } +// +// for i := range r { +// // if we find a match +// t := r[i] +// name := r[i].Value.Name.Value +// found := seenLeft[strings.ToLower(name)] +// if found.Value != nil { +// +// // check values +// if found.Value.Description.Value != t.Value.Description.Value { +// ctx := CreateContext(found.ValueNode, t.ValueNode) +// changeType := Modified +// if ctx.HasChanged() { +// changeType = ModifiedAndMoved +// } +// tc.Changes = append(tc.Changes, &Change{ +// Context: ctx, +// ChangeType: changeType, +// Property: lowv3.DescriptionLabel, +// Original: found.Value.Description.Value, +// New: t.Value.Description.Value, +// }) +// } +// +// } else { +// +// // new stuff +// +// } +// +// } +// +// // more tags in right hand-side +// if len(r) > len(l) { +// +// } +// +// // less tags in right hand-side +// if len(r) < len(l) { +// +// } +// +// //for i := range a { +// // eq, l, c := comparePositions(a) +// // +// //} +// +// return nil +//} +// +//func comparePositions(left, right *yaml.Node) (bool, int, int) { +// if left.Line == right.Line && left.Column == right.Column { +// return true, 0, 0 +// } +// return false, right.Line, right.Column +//} diff --git a/what-changed/what_changed_test.go b/what-changed/what_changed_test.go new file mode 100644 index 0000000..6e78fbd --- /dev/null +++ b/what-changed/what_changed_test.go @@ -0,0 +1,5 @@ +// Copyright 2022 Princess B33f Heavy Industries / Dave Shanley +// SPDX-License-Identifier: MIT + +package what_changed +