diff --git a/datamodel/low/reference.go b/datamodel/low/reference.go index 54277dc..0c1d8df 100644 --- a/datamodel/low/reference.go +++ b/datamodel/low/reference.go @@ -16,6 +16,19 @@ type Buildable[T any] interface { *T } +// HasValueNode is implemented by NodeReference and ValueReference to return the yaml.Node backing the value. +type HasValueNode[T any] interface { + GetValueNode() *yaml.Node + *T +} + +// HasValue is implemented by NodeReference and ValueReference to return the yaml.Node backing the value. +type HasValue[T any] interface { + GetValue() T + GetValueNode() *yaml.Node + *T +} + // NodeReference is a low-level container for holding a Value of type T, as well as references to // a key yaml.Node that points to the key node that contains the value node, and the value node that contains // the actual value. @@ -71,6 +84,16 @@ func (n NodeReference[T]) Mutate(value T) NodeReference[T] { return n } +// GetValueNode will return the yaml.Node containing the reference value node +func (n NodeReference[T]) GetValueNode() *yaml.Node { + return n.ValueNode +} + +// GetValue will return the raw value of the node +func (n NodeReference[T]) GetValue() T { + return n.Value +} + // IsEmpty will return true if this reference has no key or value nodes assigned (it's been ignored) func (n ValueReference[T]) IsEmpty() bool { return n.ValueNode == nil @@ -81,6 +104,16 @@ func (n ValueReference[T]) GenerateMapKey() string { return fmt.Sprintf("%d:%d", n.ValueNode.Line, n.ValueNode.Column) } +// GetValueNode will return the yaml.Node containing the reference value node +func (n ValueReference[T]) GetValueNode() *yaml.Node { + return n.ValueNode +} + +// GetValue will return the raw value of the node +func (n ValueReference[T]) GetValue() T { + return n.Value +} + // IsEmpty will return true if this reference has no key or value nodes assigned (it's been ignored) func (n KeyReference[T]) IsEmpty() bool { return n.KeyNode == nil diff --git a/datamodel/low/v3/constants.go b/datamodel/low/v3/constants.go index 19cd219..8f75098 100644 --- a/datamodel/low/v3/constants.go +++ b/datamodel/low/v3/constants.go @@ -40,4 +40,5 @@ const ( AuthorizationCodeLabel = "authorizationCode" DescriptionLabel = "description" URLLabel = "url" + NameLabel = "name" ) diff --git a/what-changed/external_docs.go b/what-changed/external_docs.go index 144ee24..e01aeb2 100644 --- a/what-changed/external_docs.go +++ b/what-changed/external_docs.go @@ -14,6 +14,14 @@ type ExternalDocChanges struct { ExtensionChanges *ExtensionChanges } +func (e *ExternalDocChanges) TotalChanges() int { + c := len(e.Changes) + if e.ExtensionChanges != nil { + c += len(e.ExtensionChanges.Changes) + } + return c +} + func CompareExternalDocs(l, r *lowbase.ExternalDoc) *ExternalDocChanges { var changes []*Change changeType := 0 diff --git a/what-changed/external_docs_test.go b/what-changed/external_docs_test.go index a9ba48b..fb9048f 100644 --- a/what-changed/external_docs_test.go +++ b/what-changed/external_docs_test.go @@ -38,6 +38,7 @@ x-testing: hiya!` extChanges := CompareExternalDocs(&lDoc, &rDoc) assert.Len(t, extChanges.ExtensionChanges.Changes, 1) assert.Len(t, extChanges.Changes, 2) + assert.Equal(t, 3, extChanges.TotalChanges()) // validate property changes urlChange := extChanges.Changes[0] diff --git a/what-changed/tags.go b/what-changed/tags.go new file mode 100644 index 0000000..3503ae2 --- /dev/null +++ b/what-changed/tags.go @@ -0,0 +1,147 @@ +// Copyright 2022 Princess B33f Heavy Industries / Dave Shanley +// SPDX-License-Identifier: MIT + +package what_changed + +import ( + "fmt" + "github.com/pb33f/libopenapi/datamodel/low" + lowbase "github.com/pb33f/libopenapi/datamodel/low/base" + lowv3 "github.com/pb33f/libopenapi/datamodel/low/v3" + "strings" +) + +type TagChanges struct { + PropertyChanges + ExternalDocs *ExternalDocChanges + ExtensionChanges *ExtensionChanges +} + +func (t *TagChanges) TotalChanges() int { + c := len(t.Changes) + if t.ExternalDocs != nil { + c += t.ExternalDocs.TotalChanges() + } + if t.ExtensionChanges != nil { + c += len(t.ExtensionChanges.Changes) + } + return c +} + +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 { + h := l[i] + seenLeft[strings.ToLower(l[i].Value.Name.Value)] = &h + } + for i := range r { + h := r[i] + seenRight[strings.ToLower(r[i].Value.Name.Value)] = &h + } + + var changes []*Change + var changeType int + + // check for removals, modifications and moves + for i := range seenLeft { + changeType = 0 + if seenRight[i] == nil { + // deleted + changeType = ObjectRemoved + ctx := CreateContext(seenLeft[i].ValueNode, nil) + changes = append(changes, &Change{ + Context: ctx, + ChangeType: changeType, + Property: i, + Original: fmt.Sprintf("%v", seenLeft[i].Value), + }) + continue + } + + // if the existing tag exists, let's check it. + if seenRight[i] != nil { + + // check if name has moved + ctx := CreateContext(seenLeft[i].Value.Name.ValueNode, seenRight[i].Value.Name.ValueNode) + if ctx.HasChanged() { + changeType = Moved + changes = append(changes, &Change{ + Context: ctx, + ChangeType: changeType, + Property: lowv3.NameLabel, + Original: seenLeft[i].Value.Name.Value, + New: seenRight[i].Value.Name.Value, + }) + } + + // check if description has been modified + if seenLeft[i].Value.Description.Value != seenRight[i].Value.Description.Value { + changeType = Modified + ctx = CreateContext(seenLeft[i].Value.Description.ValueNode, seenRight[i].Value.Description.ValueNode) + if ctx.HasChanged() { + changeType = ModifiedAndMoved + } + changes = append(changes, &Change{ + Context: ctx, + ChangeType: changeType, + Property: lowv3.DescriptionLabel, + Original: seenLeft[i].Value.Description.Value, + New: seenRight[i].Value.Description.Value, + }) + + } + + // check if description has moved + if seenLeft[i].Value.Description.Value == seenRight[i].Value.Description.Value { + ctx = CreateContext(seenLeft[i].Value.Description.ValueNode, seenRight[i].Value.Description.ValueNode) + if ctx.HasChanged() { + changeType = Moved + changes = append(changes, &Change{ + Context: ctx, + ChangeType: changeType, + Property: lowv3.DescriptionLabel, + Original: seenLeft[i].Value.Description.Value, + New: seenRight[i].Value.Description.Value, + }) + } + } + + // compare extensions + var lExt, rExt map[low.KeyReference[string]]low.ValueReference[any] + if l != nil && len(seenLeft[i].Value.Extensions) > 0 { + lExt = seenLeft[i].Value.Extensions + } + if r != nil && len(seenRight[i].Value.Extensions) > 0 { + rExt = seenRight[i].Value.Extensions + } + tc.ExtensionChanges = CompareExtensions(lExt, rExt) + + // compare external docs + tc.ExternalDocs = CompareExternalDocs(seenLeft[i].Value.ExternalDocs.Value, + seenRight[i].Value.ExternalDocs.Value) + } + } + + // check for additions + for i := range seenRight { + if seenLeft[i] == nil { + // added + ctx := CreateContext(nil, seenRight[i].ValueNode) + changes = append(changes, &Change{ + Context: ctx, + ChangeType: ObjectAdded, + Property: i, + New: fmt.Sprintf("%v", seenRight[i].Value), + }) + } + } + if len(changes) <= 0 { + return nil + } + tc.Changes = changes + return tc +} diff --git a/what-changed/tags_test.go b/what-changed/tags_test.go new file mode 100644 index 0000000..c0d822a --- /dev/null +++ b/what-changed/tags_test.go @@ -0,0 +1,288 @@ +// Copyright 2022 Princess B33f Heavy Industries / Dave Shanley +// SPDX-License-Identifier: MIT + +package what_changed + +import ( + "github.com/pb33f/libopenapi/datamodel" + lowv3 "github.com/pb33f/libopenapi/datamodel/low/v3" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestCompareTags(t *testing.T) { + + left := `openapi: 3.0.1 +tags: + - name: a tag + description: a lovely tag + x-tag: something + externalDocs: + url: https://quobix.com + description: cool` + + right := `openapi: 3.0.1 +tags: + - name: a tag + description: a lovelier tag description + x-tag: something else + externalDocs: + url: https://pb33f.io + description: cooler` + + // create document (which will create our correct tags low level structures) + lInfo, _ := datamodel.ExtractSpecInfo([]byte(left)) + rInfo, _ := datamodel.ExtractSpecInfo([]byte(right)) + lDoc, _ := lowv3.CreateDocument(lInfo) + rDoc, _ := lowv3.CreateDocument(rInfo) + + // compare. + changes := CompareTags(lDoc.Tags.Value, rDoc.Tags.Value) + + // evaluate. + assert.Len(t, changes.Changes, 1) + assert.Len(t, changes.ExternalDocs.Changes, 2) + assert.Len(t, changes.ExtensionChanges.Changes, 1) + assert.Equal(t, 4, changes.TotalChanges()) + + descChange := changes.Changes[0] + assert.Equal(t, "a lovelier tag description", descChange.New) + assert.Equal(t, "a lovely tag", descChange.Original) + assert.Equal(t, Modified, descChange.ChangeType) + assert.False(t, descChange.Context.HasChanged()) +} + +func TestCompareTags_AddNewTag(t *testing.T) { + + left := `openapi: 3.0.1 +tags: + - name: a tag + description: a lovelier tag description + x-tag: something else + externalDocs: + url: https://pb33f.io + description: cooler` + + right := `openapi: 3.0.1 +tags: + - name: a tag + description: a lovelier tag description + x-tag: something else + externalDocs: + url: https://pb33f.io + description: cooler + - name: a new tag + description: a cool new tag` + + // create document (which will create our correct tags low level structures) + lInfo, _ := datamodel.ExtractSpecInfo([]byte(left)) + rInfo, _ := datamodel.ExtractSpecInfo([]byte(right)) + lDoc, _ := lowv3.CreateDocument(lInfo) + rDoc, _ := lowv3.CreateDocument(rInfo) + + // compare. + changes := CompareTags(lDoc.Tags.Value, rDoc.Tags.Value) + + // evaluate. + assert.Len(t, changes.Changes, 1) + assert.Equal(t, 1, changes.TotalChanges()) + + descChange := changes.Changes[0] + assert.Equal(t, ObjectAdded, descChange.ChangeType) +} + +func TestCompareTags_AddDeleteTag(t *testing.T) { + + left := `openapi: 3.0.1 +tags: + - name: a tag + description: a lovelier tag description + x-tag: something else + externalDocs: + url: https://pb33f.io + description: cooler` + + right := `openapi: 3.0.1 +tags: + - name: a new tag + description: a cool new tag` + + // create document (which will create our correct tags low level structures) + lInfo, _ := datamodel.ExtractSpecInfo([]byte(left)) + rInfo, _ := datamodel.ExtractSpecInfo([]byte(right)) + lDoc, _ := lowv3.CreateDocument(lInfo) + rDoc, _ := lowv3.CreateDocument(rInfo) + + // compare. + changes := CompareTags(lDoc.Tags.Value, rDoc.Tags.Value) + + // evaluate. + assert.Len(t, changes.Changes, 2) + assert.Equal(t, 2, changes.TotalChanges()) + + assert.Equal(t, ObjectRemoved, changes.Changes[0].ChangeType) + assert.Equal(t, ObjectAdded, changes.Changes[1].ChangeType) +} + +func TestCompareTags_DescriptionMoved(t *testing.T) { + + left := `openapi: 3.0.1 +tags: + - description: a lovelier tag description + name: a tag + x-tag: something else + externalDocs: + url: https://pb33f.io + description: cooler` + + right := `openapi: 3.0.1 +tags: + - name: a tag + x-tag: something else + description: a lovelier tag description + externalDocs: + url: https://pb33f.io + description: cooler` + + // create document (which will create our correct tags low level structures) + lInfo, _ := datamodel.ExtractSpecInfo([]byte(left)) + rInfo, _ := datamodel.ExtractSpecInfo([]byte(right)) + lDoc, _ := lowv3.CreateDocument(lInfo) + rDoc, _ := lowv3.CreateDocument(rInfo) + + // compare. + changes := CompareTags(lDoc.Tags.Value, rDoc.Tags.Value) + + // evaluate. + assert.Len(t, changes.Changes, 2) + assert.Equal(t, 3, changes.TotalChanges()) + + nameChange := changes.Changes[0] + assert.Equal(t, Moved, nameChange.ChangeType) + assert.Equal(t, 4, nameChange.Context.OrigLine) + assert.Equal(t, 3, nameChange.Context.NewLine) + assert.True(t, nameChange.Context.HasChanged()) + + descChange := changes.Changes[1] + assert.Equal(t, Moved, descChange.ChangeType) + assert.Equal(t, 3, descChange.Context.OrigLine) + assert.Equal(t, 5, descChange.Context.NewLine) + assert.True(t, descChange.Context.HasChanged()) + +} + +func TestCompareTags_NameMoved(t *testing.T) { + + left := `openapi: 3.0.1 +tags: + - description: a lovelier tag description + name: a tag + x-tag: something else + externalDocs: + url: https://pb33f.io + description: cooler` + + right := `openapi: 3.0.1 +tags: + - description: a lovelier tag description + x-tag: something else + externalDocs: + url: https://pb33f.io + description: cooler + name: a tag` + + // create document (which will create our correct tags low level structures) + lInfo, _ := datamodel.ExtractSpecInfo([]byte(left)) + rInfo, _ := datamodel.ExtractSpecInfo([]byte(right)) + lDoc, _ := lowv3.CreateDocument(lInfo) + rDoc, _ := lowv3.CreateDocument(rInfo) + + // compare. + changes := CompareTags(lDoc.Tags.Value, rDoc.Tags.Value) + + // evaluate. + assert.Len(t, changes.Changes, 1) + assert.Equal(t, 2, changes.TotalChanges()) + + nameChange := changes.Changes[0] + assert.Equal(t, Moved, nameChange.ChangeType) + assert.Equal(t, 4, nameChange.Context.OrigLine) + assert.Equal(t, 8, nameChange.Context.NewLine) + assert.True(t, nameChange.Context.HasChanged()) +} + +func TestCompareTags_ModifiedAndMoved(t *testing.T) { + + left := `openapi: 3.0.1 +tags: + - description: a lovelier tag description + name: a tag + x-tag: something else + externalDocs: + url: https://pb33f.io + description: cooler` + + right := `openapi: 3.0.1 +tags: + - name: a tag + x-tag: something else + description: a different tag description + externalDocs: + url: https://pb33f.io + description: cooler` + + // create document (which will create our correct tags low level structures) + lInfo, _ := datamodel.ExtractSpecInfo([]byte(left)) + rInfo, _ := datamodel.ExtractSpecInfo([]byte(right)) + lDoc, _ := lowv3.CreateDocument(lInfo) + rDoc, _ := lowv3.CreateDocument(rInfo) + + // compare. + changes := CompareTags(lDoc.Tags.Value, rDoc.Tags.Value) + + // evaluate. + assert.Len(t, changes.Changes, 2) + assert.Equal(t, 3, changes.TotalChanges()) + + descChange := changes.Changes[1] + assert.Equal(t, ModifiedAndMoved, descChange.ChangeType) + assert.Equal(t, 3, descChange.Context.OrigLine) + assert.Equal(t, 5, descChange.Context.NewLine) + assert.Equal(t, "a lovelier tag description", descChange.Original) + assert.Equal(t, "a different tag description", descChange.New) + assert.True(t, descChange.Context.HasChanged()) +} + +func TestCompareTags_Identical(t *testing.T) { + + left := `openapi: 3.0.1 +tags: + - description: a lovelier tag description + name: a tag + x-tag: something else + externalDocs: + url: https://pb33f.io + description: cooler` + + right := `openapi: 3.0.1 +tags: + - description: a lovelier tag description + name: a tag + x-tag: something else + externalDocs: + url: https://pb33f.io + description: cooler` + + // create document (which will create our correct tags low level structures) + lInfo, _ := datamodel.ExtractSpecInfo([]byte(left)) + rInfo, _ := datamodel.ExtractSpecInfo([]byte(right)) + lDoc, _ := lowv3.CreateDocument(lInfo) + rDoc, _ := lowv3.CreateDocument(rInfo) + + // compare. + changes := CompareTags(lDoc.Tags.Value, rDoc.Tags.Value) + + // evaluate. + assert.Nil(t, changes) + +} diff --git a/what-changed/what_changed.go b/what-changed/what_changed.go index 5237ee9..a8378ee 100644 --- a/what-changed/what_changed.go +++ b/what-changed/what_changed.go @@ -49,11 +49,6 @@ type PropertyChanges struct { Changes []*Change } -type TagChanges struct { - PropertyChanges - ExternalDocs *ExternalDocChanges -} - type Changes struct { TagChanges *TagChanges }