diff --git a/path/expressions.go b/path/expressions.go index 6aacbb6ec..197212179 100644 --- a/path/expressions.go +++ b/path/expressions.go @@ -5,6 +5,36 @@ import "strings" // Expressions is a collection of attribute path expressions. type Expressions []Expression +// Append adds the given Expressions to the collection without duplication and +// returns the combined result. +func (e *Expressions) Append(expressions ...Expression) Expressions { + if e == nil { + return expressions + } + + for _, newExpression := range expressions { + if e.Contains(newExpression) { + continue + } + + *e = append(*e, newExpression) + } + + return *e +} + +// Contains returns true if the collection of expressions includes the given +// expression. +func (e Expressions) Contains(checkExpression Expression) bool { + for _, expression := range e { + if expression.Equal(checkExpression) { + return true + } + } + + return false +} + // String returns the human-readable representation of the expression // collection. It is intended for logging and error messages and is not // protected by compatibility guarantees. diff --git a/path/expressions_test.go b/path/expressions_test.go index 896c2bc76..ed4786313 100644 --- a/path/expressions_test.go +++ b/path/expressions_test.go @@ -5,8 +5,222 @@ import ( "github.com/google/go-cmp/cmp" "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/types" ) +func TestExpressionsAppend(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + expressions path.Expressions + add path.Expressions + expected path.Expressions + }{ + "nil-nil": { + expressions: nil, + add: nil, + expected: nil, + }, + "nil-nonempty": { + expressions: nil, + add: path.Expressions{path.MatchRoot("test")}, + expected: path.Expressions{path.MatchRoot("test")}, + }, + "nonempty-nil": { + expressions: path.Expressions{path.MatchRoot("test")}, + add: nil, + expected: path.Expressions{path.MatchRoot("test")}, + }, + "empty-empty": { + expressions: path.Expressions{}, + add: path.Expressions{}, + expected: path.Expressions{}, + }, + "empty-nonempty": { + expressions: path.Expressions{}, + add: path.Expressions{path.MatchRoot("test")}, + expected: path.Expressions{path.MatchRoot("test")}, + }, + "nonempty-empty": { + expressions: path.Expressions{path.MatchRoot("test")}, + add: path.Expressions{}, + expected: path.Expressions{path.MatchRoot("test")}, + }, + "nonempty-nonempty": { + expressions: path.Expressions{ + path.MatchRoot("test1"), + path.MatchRoot("test2"), + }, + add: path.Expressions{ + path.MatchRoot("test3"), + path.MatchRoot("test4"), + }, + expected: path.Expressions{ + path.MatchRoot("test1"), + path.MatchRoot("test2"), + path.MatchRoot("test3"), + path.MatchRoot("test4"), + }, + }, + "deduplication": { + expressions: path.Expressions{ + path.MatchRoot("test1"), + path.MatchRoot("test2"), + }, + add: path.Expressions{ + path.MatchRoot("test1"), + path.MatchRoot("test3"), + }, + expected: path.Expressions{ + path.MatchRoot("test1"), + path.MatchRoot("test2"), + path.MatchRoot("test3"), + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.expressions.Append(testCase.add...) + + if diff := cmp.Diff(testCase.expressions, testCase.expected); diff != "" { + t.Errorf("unexpected original difference: %s", diff) + } + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected result difference: %s", diff) + } + }) + } +} + +func TestExpressionsContains(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + expressions path.Expressions + contains path.Expression + expected bool + }{ + "paths-nil": { + expressions: nil, + contains: path.MatchRoot("test"), + expected: false, + }, + "paths-empty": { + expressions: path.Expressions{}, + contains: path.MatchRoot("test"), + expected: false, + }, + "contains-empty": { + expressions: path.Expressions{ + path.MatchRoot("test"), + }, + contains: path.MatchRelative(), + expected: false, + }, + "contains-middle": { + expressions: path.Expressions{ + path.MatchRoot("test1").AtName("test1_attr"), + path.MatchRoot("test2").AtName("test2_attr"), + path.MatchRoot("test3").AtName("test3_attr"), + }, + contains: path.MatchRoot("test2").AtName("test2_attr"), + expected: true, + }, + "contains-end": { + expressions: path.Expressions{ + path.MatchRoot("test1").AtName("test1_attr"), + path.MatchRoot("test2").AtName("test2_attr"), + path.MatchRoot("test3").AtName("test3_attr"), + }, + contains: path.MatchRoot("test3").AtName("test3_attr"), + expected: true, + }, + "relative-paths-different": { + expressions: path.Expressions{ + path.MatchRoot("test_parent").AtName("test_child"), + }, + contains: path.MatchRoot("test_parent").AtName("test_child").AtParent().AtName("test_child"), + expected: false, // Contains intentionally does not Resolve() + }, + "AttributeName-different": { + expressions: path.Expressions{ + path.MatchRoot("test"), + }, + contains: path.MatchRoot("not-test"), + expected: false, + }, + "AttributeName-equal": { + expressions: path.Expressions{ + path.MatchRoot("test"), + }, + contains: path.MatchRoot("test"), + expected: true, + }, + "ElementKeyInt-different": { + expressions: path.Expressions{ + path.MatchRelative().AtListIndex(0), + }, + contains: path.MatchRelative().AtListIndex(1), + expected: false, + }, + "ElementKeyInt-equal": { + expressions: path.Expressions{ + path.MatchRelative().AtListIndex(0), + }, + contains: path.MatchRelative().AtListIndex(0), + expected: true, + }, + "ElementKeyString-different": { + expressions: path.Expressions{ + path.MatchRelative().AtMapKey("test"), + }, + contains: path.MatchRelative().AtMapKey("not-test"), + expected: false, + }, + "ElementKeyString-equal": { + expressions: path.Expressions{ + path.MatchRelative().AtMapKey("test"), + }, + contains: path.MatchRelative().AtMapKey("test"), + expected: true, + }, + "ElementKeyValue-different": { + expressions: path.Expressions{ + path.MatchRelative().AtSetValue(types.String{Value: "test"}), + }, + contains: path.MatchRelative().AtSetValue(types.String{Value: "not-test"}), + expected: false, + }, + "ElementKeyValue-equal": { + expressions: path.Expressions{ + path.MatchRelative().AtSetValue(types.String{Value: "test"}), + }, + contains: path.MatchRelative().AtSetValue(types.String{Value: "test"}), + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.expressions.Contains(testCase.contains) + + if got != testCase.expected { + t.Errorf("expected %t, got %t", testCase.expected, got) + } + }) + } +} + func TestExpressionsString(t *testing.T) { t.Parallel() diff --git a/path/paths.go b/path/paths.go index 5518f0613..579dea9f7 100644 --- a/path/paths.go +++ b/path/paths.go @@ -5,6 +5,24 @@ import "strings" // Paths is a collection of exact attribute paths. type Paths []Path +// Append adds the given Paths to the collection without duplication and +// returns the combined result. +func (p *Paths) Append(paths ...Path) Paths { + if p == nil { + return paths + } + + for _, newPath := range paths { + if p.Contains(newPath) { + continue + } + + *p = append(*p, newPath) + } + + return *p +} + // Contains returns true if the collection of paths includes the given path. func (p Paths) Contains(checkPath Path) bool { for _, path := range p { diff --git a/path/paths_test.go b/path/paths_test.go index e3b611568..e8d5acbe6 100644 --- a/path/paths_test.go +++ b/path/paths_test.go @@ -8,6 +8,96 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types" ) +func TestPathsAppend(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + paths path.Paths + add path.Paths + expected path.Paths + }{ + "nil-nil": { + paths: nil, + add: nil, + expected: nil, + }, + "nil-nonempty": { + paths: nil, + add: path.Paths{path.Root("test")}, + expected: path.Paths{path.Root("test")}, + }, + "nonempty-nil": { + paths: path.Paths{path.Root("test")}, + add: nil, + expected: path.Paths{path.Root("test")}, + }, + "empty-empty": { + paths: path.Paths{}, + add: path.Paths{}, + expected: path.Paths{}, + }, + "empty-nonempty": { + paths: path.Paths{}, + add: path.Paths{path.Root("test")}, + expected: path.Paths{path.Root("test")}, + }, + "nonempty-empty": { + paths: path.Paths{path.Root("test")}, + add: path.Paths{}, + expected: path.Paths{path.Root("test")}, + }, + "nonempty-nonempty": { + paths: path.Paths{ + path.Root("test1"), + path.Root("test2"), + }, + add: path.Paths{ + path.Root("test3"), + path.Root("test4"), + }, + expected: path.Paths{ + path.Root("test1"), + path.Root("test2"), + path.Root("test3"), + path.Root("test4"), + }, + }, + "deduplication": { + paths: path.Paths{ + path.Root("test1"), + path.Root("test2"), + }, + add: path.Paths{ + path.Root("test1"), + path.Root("test3"), + }, + expected: path.Paths{ + path.Root("test1"), + path.Root("test2"), + path.Root("test3"), + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.paths.Append(testCase.add...) + + if diff := cmp.Diff(testCase.paths, testCase.expected); diff != "" { + t.Errorf("unexpected original difference: %s", diff) + } + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected result difference: %s", diff) + } + }) + } +} + func TestPathsContains(t *testing.T) { t.Parallel()