From 16f0424c87824084d1d0ac03d58a5b3513ebe89b Mon Sep 17 00:00:00 2001 From: Brian Flad Date: Thu, 23 Jun 2022 10:42:30 -0400 Subject: [PATCH 1/4] path: Initial Path Expression Support Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/81 Reference: https://github.com/hashicorp/terraform-plugin-framework-validators/issues/14 Reference: https://github.com/hashicorp/terraform-plugin-framework-validators/issues/15 Reference: https://github.com/hashicorp/terraform-plugin-framework-validators/issues/16 Reference: https://github.com/hashicorp/terraform-plugin-framework-validators/issues/17 Reference: https://github.com/hashicorp/terraform-plugin-framework-validators/issues/20 This introduces the concept of an attribute path expression, an abstraction on top of an attribute path, which enables provider developers to declare logic which might match zero, one, or more paths. Paths are directly convertable into path expressions as exact expression steps. The builder-like syntax for exact expression steps matches the syntax for path steps, such as `AtName()` in both cases always represents an exact transversal into the attribute name of an object. Additional expression steps enable matching any list, map, or set element, such as `AtAnyListIndex()`. It also supports relative attribute path expressions, by supporting a parent expression step `AtParent()` or starting an expression with `MatchParent()` which can be combined with a prior path expression. The framework will automatically expose path expressions to attribute plan modifiers and validators, so they can more intuitively support relative paths as inputs to their logic. For example, the `terraform-plugin-framework-validators` Go module will implement support for `terraform-plugin-sdk` multiple attribute schema behaviors such as `ConflictsWith`. It is expected that the downstream implementation can allow provider developers to declare the validator with expressions such as: ```go tfsdk.Attribute{ // ... other fields ... Validators: []AttributeValidators{ schemavalidator.ConflictsWith( // Example absolute path from root path.MatchRoot("root_attribute"), // Example relative path from current attribute // e.g. another attribute at the same list index of ListNestedAttributes path.MatchParent().AtName("another_same_level_attribute"), ), }, } ``` Then the logic within the validator can take the `ValidateAttributeRequest.AttributePathExpression` and use the `(path.Expression).Append()` method to combine the current attribute expression with any incoming expressions. While this introduction will expose the expression types and make them available to attribute plan modifiers and validators, there is not yet a simple methodology for getting valid paths within data stored in `tfsdk.Config`, `tfsdk.Plan`, and `tfsdk.State` that match the expression. This will be added after this initial expression API is reviewed and approved. --- .changelog/pending.txt | 3 + internal/fwserver/attribute_validation.go | 20 +- internal/fwserver/block_validation.go | 20 +- internal/fwserver/schema_validation.go | 10 +- path/expression.go | 163 +++ path/expression_step.go | 20 + path/expression_step_attribute_name_exact.go | 43 + ...pression_step_attribute_name_exact_test.go | 137 +++ path/expression_step_element_key_int_any.go | 35 + ...xpression_step_element_key_int_any_test.go | 132 +++ path/expression_step_element_key_int_exact.go | 47 + ...ression_step_element_key_int_exact_test.go | 142 +++ .../expression_step_element_key_string_any.go | 35 + ...ession_step_element_key_string_any_test.go | 132 +++ ...xpression_step_element_key_string_exact.go | 47 + ...sion_step_element_key_string_exact_test.go | 141 +++ path/expression_step_element_key_value_any.go | 35 + ...ression_step_element_key_value_any_test.go | 132 +++ ...expression_step_element_key_value_exact.go | 51 + ...ssion_step_element_key_value_exact_test.go | 191 ++++ path/expression_step_parent.go | 35 + path/expression_step_parent_test.go | 132 +++ path/expression_steps.go | 150 +++ path/expression_steps_test.go | 980 ++++++++++++++++++ path/expression_test.go | 645 ++++++++++++ path/expressions.go | 33 + path/expressions_test.go | 66 ++ path/path.go | 7 + path/path_step.go | 4 + path/path_step_attribute_name.go | 5 + path/path_step_attribute_name_test.go | 28 + path/path_step_element_key_int.go | 5 + path/path_step_element_key_int_test.go | 28 + path/path_step_element_key_string.go | 5 + path/path_step_element_key_string_test.go | 28 + path/path_step_element_key_value.go | 5 + path/path_step_element_key_value_test.go | 28 + path/path_steps.go | 12 + path/path_steps_test.go | 50 + path/path_test.go | 32 + tfsdk/attribute_plan_modification.go | 7 +- tfsdk/attribute_validation.go | 9 +- 42 files changed, 3807 insertions(+), 23 deletions(-) create mode 100644 .changelog/pending.txt create mode 100644 path/expression.go create mode 100644 path/expression_step.go create mode 100644 path/expression_step_attribute_name_exact.go create mode 100644 path/expression_step_attribute_name_exact_test.go create mode 100644 path/expression_step_element_key_int_any.go create mode 100644 path/expression_step_element_key_int_any_test.go create mode 100644 path/expression_step_element_key_int_exact.go create mode 100644 path/expression_step_element_key_int_exact_test.go create mode 100644 path/expression_step_element_key_string_any.go create mode 100644 path/expression_step_element_key_string_any_test.go create mode 100644 path/expression_step_element_key_string_exact.go create mode 100644 path/expression_step_element_key_string_exact_test.go create mode 100644 path/expression_step_element_key_value_any.go create mode 100644 path/expression_step_element_key_value_any_test.go create mode 100644 path/expression_step_element_key_value_exact.go create mode 100644 path/expression_step_element_key_value_exact_test.go create mode 100644 path/expression_step_parent.go create mode 100644 path/expression_step_parent_test.go create mode 100644 path/expression_steps.go create mode 100644 path/expression_steps_test.go create mode 100644 path/expression_test.go create mode 100644 path/expressions.go create mode 100644 path/expressions_test.go diff --git a/.changelog/pending.txt b/.changelog/pending.txt new file mode 100644 index 000000000..515542ed1 --- /dev/null +++ b/.changelog/pending.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +tfsdk: Added `AttributePathExpression` field to `ModifyAttributePlanRequest` and `ValidateAttributeRequest` types +``` diff --git a/internal/fwserver/attribute_validation.go b/internal/fwserver/attribute_validation.go index 91aa8f739..0d3ebb126 100644 --- a/internal/fwserver/attribute_validation.go +++ b/internal/fwserver/attribute_validation.go @@ -157,8 +157,9 @@ func AttributeValidateNestedAttributes(ctx context.Context, a tfsdk.Attribute, r for idx := range l.Elems { for nestedName, nestedAttr := range a.Attributes.GetAttributes() { nestedAttrReq := tfsdk.ValidateAttributeRequest{ - AttributePath: req.AttributePath.AtListIndex(idx).AtName(nestedName), - Config: req.Config, + AttributePath: req.AttributePath.AtListIndex(idx).AtName(nestedName), + AttributePathExpression: req.AttributePathExpression.AtListIndex(idx).AtName(nestedName), + Config: req.Config, } nestedAttrResp := &tfsdk.ValidateAttributeResponse{ Diagnostics: resp.Diagnostics, @@ -186,8 +187,9 @@ func AttributeValidateNestedAttributes(ctx context.Context, a tfsdk.Attribute, r for _, value := range s.Elems { for nestedName, nestedAttr := range a.Attributes.GetAttributes() { nestedAttrReq := tfsdk.ValidateAttributeRequest{ - AttributePath: req.AttributePath.AtSetValue(value).AtName(nestedName), - Config: req.Config, + AttributePath: req.AttributePath.AtSetValue(value).AtName(nestedName), + AttributePathExpression: req.AttributePathExpression.AtSetValue(value).AtName(nestedName), + Config: req.Config, } nestedAttrResp := &tfsdk.ValidateAttributeResponse{ Diagnostics: resp.Diagnostics, @@ -215,8 +217,9 @@ func AttributeValidateNestedAttributes(ctx context.Context, a tfsdk.Attribute, r for key := range m.Elems { for nestedName, nestedAttr := range a.Attributes.GetAttributes() { nestedAttrReq := tfsdk.ValidateAttributeRequest{ - AttributePath: req.AttributePath.AtMapKey(key).AtName(nestedName), - Config: req.Config, + AttributePath: req.AttributePath.AtMapKey(key).AtName(nestedName), + AttributePathExpression: req.AttributePathExpression.AtMapKey(key).AtName(nestedName), + Config: req.Config, } nestedAttrResp := &tfsdk.ValidateAttributeResponse{ Diagnostics: resp.Diagnostics, @@ -244,8 +247,9 @@ func AttributeValidateNestedAttributes(ctx context.Context, a tfsdk.Attribute, r if !o.Null && !o.Unknown { for nestedName, nestedAttr := range a.Attributes.GetAttributes() { nestedAttrReq := tfsdk.ValidateAttributeRequest{ - AttributePath: req.AttributePath.AtName(nestedName), - Config: req.Config, + AttributePath: req.AttributePath.AtName(nestedName), + AttributePathExpression: req.AttributePathExpression.AtName(nestedName), + Config: req.Config, } nestedAttrResp := &tfsdk.ValidateAttributeResponse{ Diagnostics: resp.Diagnostics, diff --git a/internal/fwserver/block_validation.go b/internal/fwserver/block_validation.go index 7daa2526e..d504f2663 100644 --- a/internal/fwserver/block_validation.go +++ b/internal/fwserver/block_validation.go @@ -47,8 +47,9 @@ func BlockValidate(ctx context.Context, b tfsdk.Block, req tfsdk.ValidateAttribu for idx := range l.Elems { for name, attr := range b.Attributes { nestedAttrReq := tfsdk.ValidateAttributeRequest{ - AttributePath: req.AttributePath.AtListIndex(idx).AtName(name), - Config: req.Config, + AttributePath: req.AttributePath.AtListIndex(idx).AtName(name), + AttributePathExpression: req.AttributePathExpression.AtListIndex(idx).AtName(name), + Config: req.Config, } nestedAttrResp := &tfsdk.ValidateAttributeResponse{ Diagnostics: resp.Diagnostics, @@ -61,8 +62,9 @@ func BlockValidate(ctx context.Context, b tfsdk.Block, req tfsdk.ValidateAttribu for name, block := range b.Blocks { nestedAttrReq := tfsdk.ValidateAttributeRequest{ - AttributePath: req.AttributePath.AtListIndex(idx).AtName(name), - Config: req.Config, + AttributePath: req.AttributePath.AtListIndex(idx).AtName(name), + AttributePathExpression: req.AttributePathExpression.AtListIndex(idx).AtName(name), + Config: req.Config, } nestedAttrResp := &tfsdk.ValidateAttributeResponse{ Diagnostics: resp.Diagnostics, @@ -90,8 +92,9 @@ func BlockValidate(ctx context.Context, b tfsdk.Block, req tfsdk.ValidateAttribu for _, value := range s.Elems { for name, attr := range b.Attributes { nestedAttrReq := tfsdk.ValidateAttributeRequest{ - AttributePath: req.AttributePath.AtSetValue(value).AtName(name), - Config: req.Config, + AttributePath: req.AttributePath.AtSetValue(value).AtName(name), + AttributePathExpression: req.AttributePathExpression.AtSetValue(value).AtName(name), + Config: req.Config, } nestedAttrResp := &tfsdk.ValidateAttributeResponse{ Diagnostics: resp.Diagnostics, @@ -104,8 +107,9 @@ func BlockValidate(ctx context.Context, b tfsdk.Block, req tfsdk.ValidateAttribu for name, block := range b.Blocks { nestedAttrReq := tfsdk.ValidateAttributeRequest{ - AttributePath: req.AttributePath.AtSetValue(value).AtName(name), - Config: req.Config, + AttributePath: req.AttributePath.AtSetValue(value).AtName(name), + AttributePathExpression: req.AttributePathExpression.AtSetValue(value).AtName(name), + Config: req.Config, } nestedAttrResp := &tfsdk.ValidateAttributeResponse{ Diagnostics: resp.Diagnostics, diff --git a/internal/fwserver/schema_validation.go b/internal/fwserver/schema_validation.go index f46f119b4..adc581687 100644 --- a/internal/fwserver/schema_validation.go +++ b/internal/fwserver/schema_validation.go @@ -36,8 +36,9 @@ func SchemaValidate(ctx context.Context, s tfsdk.Schema, req ValidateSchemaReque for name, attribute := range s.Attributes { attributeReq := tfsdk.ValidateAttributeRequest{ - AttributePath: path.Root(name), - Config: req.Config, + AttributePath: path.Root(name), + AttributePathExpression: path.MatchRoot(name), + Config: req.Config, } attributeResp := &tfsdk.ValidateAttributeResponse{ Diagnostics: resp.Diagnostics, @@ -50,8 +51,9 @@ func SchemaValidate(ctx context.Context, s tfsdk.Schema, req ValidateSchemaReque for name, block := range s.Blocks { attributeReq := tfsdk.ValidateAttributeRequest{ - AttributePath: path.Root(name), - Config: req.Config, + AttributePath: path.Root(name), + AttributePathExpression: path.MatchRoot(name), + Config: req.Config, } attributeResp := &tfsdk.ValidateAttributeResponse{ Diagnostics: resp.Diagnostics, diff --git a/path/expression.go b/path/expression.go new file mode 100644 index 000000000..2dc3e4f46 --- /dev/null +++ b/path/expression.go @@ -0,0 +1,163 @@ +package path + +import ( + "github.com/hashicorp/terraform-plugin-framework/attr" +) + +// Expression represents an attribute path with expression steps, which can +// represent zero, one, or more actual Paths. +type Expression struct { + // steps is the transversals included with the expression. In general, + // operations against the path should protect against modification of the + // original. + steps ExpressionSteps +} + +// AtAnyListIndex returns a copied expression with a new list index step at the +// end. The returned path is safe to modify without affecting the original. +func (e Expression) AtAnyListIndex() Expression { + copiedPath := e.Copy() + + copiedPath.steps.Append(ExpressionStepElementKeyIntAny{}) + + return copiedPath +} + +// AtAnyMapKey returns a copied expression with a new map key step at the end. +// The returned path is safe to modify without affecting the original. +func (e Expression) AtAnyMapKey() Expression { + copiedPath := e.Copy() + + copiedPath.steps.Append(ExpressionStepElementKeyStringAny{}) + + return copiedPath +} + +// AtAnySetValue returns a copied expression with a new set value step at the +// end. The returned path is safe to modify without affecting the original. +func (e Expression) AtAnySetValue() Expression { + copiedPath := e.Copy() + + copiedPath.steps.Append(ExpressionStepElementKeyValueAny{}) + + return copiedPath +} + +// AtListIndex returns a copied expression with a new list index step at the +// end. The returned path is safe to modify without affecting the original. +func (e Expression) AtListIndex(index int) Expression { + copiedPath := e.Copy() + + copiedPath.steps.Append(ExpressionStepElementKeyIntExact(index)) + + return copiedPath +} + +// AtMapKey returns a copied expression with a new map key step at the end. +// The returned path is safe to modify without affecting the original. +func (e Expression) AtMapKey(key string) Expression { + copiedPath := e.Copy() + + copiedPath.steps.Append(ExpressionStepElementKeyStringExact(key)) + + return copiedPath +} + +// AtName returns a copied expression with a new attribute or block name step +// at the end. The returned path is safe to modify without affecting the +// original. +func (e Expression) AtName(name string) Expression { + copiedPath := e.Copy() + + copiedPath.steps.Append(ExpressionStepAttributeNameExact(name)) + + return copiedPath +} + +// AtParent returns a copied expression with a new parent step at the end. +// The returned path is safe to modify without affecting the original. +func (e Expression) AtParent() Expression { + copiedPath := e.Copy() + + copiedPath.steps.Append(ExpressionStepParent{}) + + return copiedPath +} + +// AtSetValue returns a copied expression with a new set value step at the end. +// The returned path is safe to modify without affecting the original. +func (e Expression) AtSetValue(value attr.Value) Expression { + copiedPath := e.Copy() + + copiedPath.steps.Append(ExpressionStepElementKeyValueExact{Value: value}) + + return copiedPath +} + +// Copy returns a duplicate of the expression that is safe to modify without +// affecting the original. +func (e Expression) Copy() Expression { + return Expression{ + steps: e.Steps(), + } +} + +// Equal returns true if the given expression is exactly equivalent. +func (e Expression) Equal(o Expression) bool { + if e.steps == nil && o.steps == nil { + return true + } + + if e.steps == nil { + return false + } + + if !e.steps.Equal(o.steps) { + return false + } + + return true +} + +// Matches returns true if the given Path is valid for the Expression. +func (e Expression) Matches(path Path) bool { + return e.steps.Matches(path.Steps()) +} + +// Steps returns a copy of the underlying expression steps. Returns an empty +// collection of steps if expression is nil. +func (e Expression) Steps() ExpressionSteps { + if len(e.steps) == 0 { + return ExpressionSteps{} + } + + return e.steps.Copy() +} + +// String returns the human-readable representation of the path. +// It is intended for logging and error messages and is not protected by +// compatibility guarantees. +func (e Expression) String() string { + return e.steps.String() +} + +// MatchParent creates an attribute path expression starting with +// ExpressionStepParent. This allows creating a relative expression in +// nested schemas. +func MatchParent() Expression { + return Expression{ + steps: ExpressionSteps{ + ExpressionStepParent{}, + }, + } +} + +// MatchRoot creates an attribute path expression starting with +// ExpressionStepAttributeNameExact. +func MatchRoot(rootAttributeName string) Expression { + return Expression{ + steps: ExpressionSteps{ + ExpressionStepAttributeNameExact(rootAttributeName), + }, + } +} diff --git a/path/expression_step.go b/path/expression_step.go new file mode 100644 index 000000000..7e2b676db --- /dev/null +++ b/path/expression_step.go @@ -0,0 +1,20 @@ +package path + +// ExpressionStep represents an expression of an attribute path step, which may +// match zero, one, or more actual paths. +type ExpressionStep interface { + // Equal should return true if the given Step is exactly equivalent. + Equal(ExpressionStep) bool + + // Matches should return true if the given PathStep can be fulfilled by the + // ExpressionStep. + Matches(PathStep) bool + + // String should return a human-readable representation of the step + // intended for logging and error messages. There should not be usage + // that needs to be protected by compatibility guarantees. + String() string + + // unexported prevents outside types from satisfying the interface. + unexported() +} diff --git a/path/expression_step_attribute_name_exact.go b/path/expression_step_attribute_name_exact.go new file mode 100644 index 000000000..fa70fc549 --- /dev/null +++ b/path/expression_step_attribute_name_exact.go @@ -0,0 +1,43 @@ +package path + +// Ensure ExpressionStepAttributeNameExact satisfies the ExpressionStep +// interface. +var _ ExpressionStep = ExpressionStepAttributeNameExact("") + +// ExpressionStepAttributeNameExact is an attribute path expression for an +// exact attribute name match within an object. +type ExpressionStepAttributeNameExact string + +// Equal returns true if the given ExpressionStep is a +// ExpressionStepAttributeNameExact and the attribute name is equivalent. +func (s ExpressionStepAttributeNameExact) Equal(o ExpressionStep) bool { + other, ok := o.(ExpressionStepAttributeNameExact) + + if !ok { + return false + } + + return string(s) == string(other) +} + +// Matches returns true if the given PathStep is fulfilled by the +// ExpressionStepAttributeNameExact condition. +func (s ExpressionStepAttributeNameExact) Matches(pathStep PathStep) bool { + pathStepAttributeName, ok := pathStep.(PathStepAttributeName) + + if !ok { + return false + } + + return string(s) == string(pathStepAttributeName) +} + +// String returns the human-readable representation of the attribute name +// expression. It is intended for logging and error messages and is not +// protected by compatibility guarantees. +func (s ExpressionStepAttributeNameExact) String() string { + return string(s) +} + +// unexported satisfies the Step interface. +func (s ExpressionStepAttributeNameExact) unexported() {} diff --git a/path/expression_step_attribute_name_exact_test.go b/path/expression_step_attribute_name_exact_test.go new file mode 100644 index 000000000..140ba377d --- /dev/null +++ b/path/expression_step_attribute_name_exact_test.go @@ -0,0 +1,137 @@ +package path_test + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func TestExpressionStepAttributeNameExactEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + step path.ExpressionStepAttributeNameExact + other path.ExpressionStep + expected bool + }{ + "ExpressionStepAttributeNameExact-different": { + step: path.ExpressionStepAttributeNameExact("test"), + other: path.ExpressionStepAttributeNameExact("not-test"), + expected: false, + }, + "ExpressionStepAttributeNameExact-equal": { + step: path.ExpressionStepAttributeNameExact("test"), + other: path.ExpressionStepAttributeNameExact("test"), + expected: true, + }, + "ExpressionStepElementKeyIntExact": { + step: path.ExpressionStepAttributeNameExact("test"), + other: path.ExpressionStepElementKeyIntExact(0), + expected: false, + }, + "ExpressionStepElementKeyStringExact": { + step: path.ExpressionStepAttributeNameExact("test"), + other: path.ExpressionStepElementKeyStringExact("test"), + expected: false, + }, + "ExpressionStepElementKeyValueExact": { + step: path.ExpressionStepAttributeNameExact("test"), + other: path.ExpressionStepElementKeyValueExact{Value: types.String{Value: "test"}}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.step.Equal(testCase.other) + + if got != testCase.expected { + t.Errorf("expected %t, got %t", testCase.expected, got) + } + }) + } +} + +func TestExpressionStepAttributeNameExactMatches(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + step path.ExpressionStepAttributeNameExact + pathStep path.PathStep + expected bool + }{ + "StepAttributeName-different": { + step: path.ExpressionStepAttributeNameExact("test"), + pathStep: path.PathStepAttributeName("not-test"), + expected: false, + }, + "StepAttributeName-equal": { + step: path.ExpressionStepAttributeNameExact("test"), + pathStep: path.PathStepAttributeName("test"), + expected: true, + }, + "StepElementKeyInt": { + step: path.ExpressionStepAttributeNameExact("test"), + pathStep: path.PathStepElementKeyInt(0), + expected: false, + }, + "StepElementKeyString": { + step: path.ExpressionStepAttributeNameExact("test"), + pathStep: path.PathStepElementKeyString("test"), + expected: false, + }, + "StepElementKeyValue": { + step: path.ExpressionStepAttributeNameExact("test"), + pathStep: path.PathStepElementKeyValue{Value: types.String{Value: "test"}}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.step.Matches(testCase.pathStep) + + if got != testCase.expected { + t.Errorf("expected %t, got %t", testCase.expected, got) + } + }) + } +} + +func TestExpressionStepAttributeNameExactString(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + step path.ExpressionStepAttributeNameExact + expected string + }{ + "basic": { + step: path.ExpressionStepAttributeNameExact("test"), + expected: "test", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.step.String() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/path/expression_step_element_key_int_any.go b/path/expression_step_element_key_int_any.go new file mode 100644 index 000000000..a76704c0b --- /dev/null +++ b/path/expression_step_element_key_int_any.go @@ -0,0 +1,35 @@ +package path + +// Ensure ExpressionStepElementKeyIntAny satisfies the ExpressionStep +// interface. +var _ ExpressionStep = ExpressionStepElementKeyIntAny{} + +// ExpressionStepElementKeyIntAny is an attribute path expression for a matching any +// integer element key within a list. +type ExpressionStepElementKeyIntAny struct{} + +// Equal returns true if the given ExpressionStep is a +// ExpressionStepElementKeyIntAny. +func (s ExpressionStepElementKeyIntAny) Equal(o ExpressionStep) bool { + _, ok := o.(ExpressionStepElementKeyIntAny) + + return ok +} + +// Matches returns true if the given PathStep is fulfilled by the +// ExpressionStepElementKeyIntAny condition. +func (s ExpressionStepElementKeyIntAny) Matches(pathStep PathStep) bool { + _, ok := pathStep.(PathStepElementKeyInt) + + return ok +} + +// String returns the human-readable representation of the element key +// expression. It is intended for logging and error messages and is not +// protected by compatibility guarantees. +func (s ExpressionStepElementKeyIntAny) String() string { + return "[*]" +} + +// unexported satisfies the Step interface. +func (s ExpressionStepElementKeyIntAny) unexported() {} diff --git a/path/expression_step_element_key_int_any_test.go b/path/expression_step_element_key_int_any_test.go new file mode 100644 index 000000000..f3c4759ad --- /dev/null +++ b/path/expression_step_element_key_int_any_test.go @@ -0,0 +1,132 @@ +package path_test + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func TestExpressionStepElementKeyIntAnyEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + step path.ExpressionStepElementKeyIntAny + other path.ExpressionStep + expected bool + }{ + "ExpressionStepAttributeNameExact": { + step: path.ExpressionStepElementKeyIntAny{}, + other: path.ExpressionStepAttributeNameExact("test"), + expected: false, + }, + "ExpressionStepElementKeyIntAny": { + step: path.ExpressionStepElementKeyIntAny{}, + other: path.ExpressionStepElementKeyIntAny{}, + expected: true, + }, + "ExpressionStepElementKeyIntExact": { + step: path.ExpressionStepElementKeyIntAny{}, + other: path.ExpressionStepElementKeyIntExact(0), + expected: false, + }, + "ExpressionStepElementKeyStringExact": { + step: path.ExpressionStepElementKeyIntAny{}, + other: path.ExpressionStepElementKeyStringExact("test"), + expected: false, + }, + "ExpressionStepElementKeyValueExact": { + step: path.ExpressionStepElementKeyIntAny{}, + other: path.ExpressionStepElementKeyValueExact{Value: types.String{Value: "test"}}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.step.Equal(testCase.other) + + if got != testCase.expected { + t.Errorf("expected %t, got %t", testCase.expected, got) + } + }) + } +} + +func TestExpressionStepElementKeyIntAnyMatches(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + step path.ExpressionStepElementKeyIntAny + pathStep path.PathStep + expected bool + }{ + "StepAttributeName": { + step: path.ExpressionStepElementKeyIntAny{}, + pathStep: path.PathStepAttributeName("test"), + expected: false, + }, + "StepElementKeyInt": { + step: path.ExpressionStepElementKeyIntAny{}, + pathStep: path.PathStepElementKeyInt(0), + expected: true, + }, + "StepElementKeyString": { + step: path.ExpressionStepElementKeyIntAny{}, + pathStep: path.PathStepElementKeyString("test"), + expected: false, + }, + "StepElementKeyValue": { + step: path.ExpressionStepElementKeyIntAny{}, + pathStep: path.PathStepElementKeyValue{Value: types.String{Value: "test"}}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.step.Matches(testCase.pathStep) + + if got != testCase.expected { + t.Errorf("expected %t, got %t", testCase.expected, got) + } + }) + } +} + +func TestExpressionStepElementKeyIntAnyString(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + step path.ExpressionStepElementKeyIntAny + expected string + }{ + "basic": { + step: path.ExpressionStepElementKeyIntAny{}, + expected: "[*]", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.step.String() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/path/expression_step_element_key_int_exact.go b/path/expression_step_element_key_int_exact.go new file mode 100644 index 000000000..874eec37c --- /dev/null +++ b/path/expression_step_element_key_int_exact.go @@ -0,0 +1,47 @@ +package path + +import ( + "fmt" +) + +// Ensure ExpressionStepElementKeyIntExact satisfies the ExpressionStep +// interface. +var _ ExpressionStep = ExpressionStepElementKeyIntExact(0) + +// ExpressionStepElementKeyIntExact is an attribute path expression for an exact integer +// element key match within a list. List indexing starts at 0. +type ExpressionStepElementKeyIntExact int64 + +// Equal returns true if the given ExpressionStep is a +// ExpressionStepElementKeyIntExact and the integer element key is equivalent. +func (s ExpressionStepElementKeyIntExact) Equal(o ExpressionStep) bool { + other, ok := o.(ExpressionStepElementKeyIntExact) + + if !ok { + return false + } + + return int64(s) == int64(other) +} + +// Matches returns true if the given PathStep is fulfilled by the +// ExpressionStepElementKeyIntExact condition. +func (s ExpressionStepElementKeyIntExact) Matches(pathStep PathStep) bool { + pathStepElementKeyInt, ok := pathStep.(PathStepElementKeyInt) + + if !ok { + return false + } + + return int64(s) == int64(pathStepElementKeyInt) +} + +// String returns the human-readable representation of the element key +// expression. It is intended for logging and error messages and is not +// protected by compatibility guarantees. +func (s ExpressionStepElementKeyIntExact) String() string { + return fmt.Sprintf("[%d]", s) +} + +// unexported satisfies the Step interface. +func (s ExpressionStepElementKeyIntExact) unexported() {} diff --git a/path/expression_step_element_key_int_exact_test.go b/path/expression_step_element_key_int_exact_test.go new file mode 100644 index 000000000..9ef627b15 --- /dev/null +++ b/path/expression_step_element_key_int_exact_test.go @@ -0,0 +1,142 @@ +package path_test + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func TestExpressionStepElementKeyIntExactEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + step path.ExpressionStepElementKeyIntExact + other path.ExpressionStep + expected bool + }{ + "ExpressionStepAttributeNameExact": { + step: path.ExpressionStepElementKeyIntExact(0), + other: path.ExpressionStepAttributeNameExact("test"), + expected: false, + }, + "ExpressionStepElementKeyIntAny": { + step: path.ExpressionStepElementKeyIntExact(0), + other: path.ExpressionStepElementKeyIntAny{}, + expected: false, + }, + "ExpressionStepElementKeyIntExact-different": { + step: path.ExpressionStepElementKeyIntExact(0), + other: path.ExpressionStepElementKeyIntExact(1), + expected: false, + }, + "ExpressionStepElementKeyIntExact-equal": { + step: path.ExpressionStepElementKeyIntExact(0), + other: path.ExpressionStepElementKeyIntExact(0), + expected: true, + }, + "ExpressionStepElementKeyStringExact": { + step: path.ExpressionStepElementKeyIntExact(0), + other: path.ExpressionStepElementKeyStringExact("test"), + expected: false, + }, + "ExpressionStepElementKeyValueExact": { + step: path.ExpressionStepElementKeyIntExact(0), + other: path.ExpressionStepElementKeyValueExact{Value: types.String{Value: "test"}}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.step.Equal(testCase.other) + + if got != testCase.expected { + t.Errorf("expected %t, got %t", testCase.expected, got) + } + }) + } +} + +func TestExpressionStepElementKeyIntExactMatches(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + step path.ExpressionStepElementKeyIntExact + pathStep path.PathStep + expected bool + }{ + "StepAttributeName": { + step: path.ExpressionStepElementKeyIntExact(0), + pathStep: path.PathStepAttributeName("test"), + expected: false, + }, + "StepElementKeyInt-different": { + step: path.ExpressionStepElementKeyIntExact(0), + pathStep: path.PathStepElementKeyInt(1), + expected: false, + }, + "StepElementKeyInt-equal": { + step: path.ExpressionStepElementKeyIntExact(0), + pathStep: path.PathStepElementKeyInt(0), + expected: true, + }, + "StepElementKeyString": { + step: path.ExpressionStepElementKeyIntExact(0), + pathStep: path.PathStepElementKeyString("test"), + expected: false, + }, + "StepElementKeyValue": { + step: path.ExpressionStepElementKeyIntExact(0), + pathStep: path.PathStepElementKeyValue{Value: types.String{Value: "test"}}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.step.Matches(testCase.pathStep) + + if got != testCase.expected { + t.Errorf("expected %t, got %t", testCase.expected, got) + } + }) + } +} + +func TestExpressionStepElementKeyIntExactString(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + step path.ExpressionStepElementKeyIntExact + expected string + }{ + "basic": { + step: path.ExpressionStepElementKeyIntExact(0), + expected: "[0]", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.step.String() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/path/expression_step_element_key_string_any.go b/path/expression_step_element_key_string_any.go new file mode 100644 index 000000000..6a96201c5 --- /dev/null +++ b/path/expression_step_element_key_string_any.go @@ -0,0 +1,35 @@ +package path + +// Ensure ExpressionStepElementKeyStringAny satisfies the ExpressionStep +// interface. +var _ ExpressionStep = ExpressionStepElementKeyStringAny{} + +// ExpressionStepElementKeyStringAny is an attribute path expression for a matching any +// string key within a map. +type ExpressionStepElementKeyStringAny struct{} + +// Equal returns true if the given ExpressionStep is a +// ExpressionStepElementKeyStringAny. +func (s ExpressionStepElementKeyStringAny) Equal(o ExpressionStep) bool { + _, ok := o.(ExpressionStepElementKeyStringAny) + + return ok +} + +// Matches returns true if the given PathStep is fulfilled by the +// ExpressionStepElementKeyStringAny condition. +func (s ExpressionStepElementKeyStringAny) Matches(pathStep PathStep) bool { + _, ok := pathStep.(PathStepElementKeyString) + + return ok +} + +// String returns the human-readable representation of the element key +// expression. It is intended for logging and error messages and is not +// protected by compatibility guarantees. +func (s ExpressionStepElementKeyStringAny) String() string { + return `["*"]` +} + +// unexported satisfies the Step interface. +func (s ExpressionStepElementKeyStringAny) unexported() {} diff --git a/path/expression_step_element_key_string_any_test.go b/path/expression_step_element_key_string_any_test.go new file mode 100644 index 000000000..933469ea8 --- /dev/null +++ b/path/expression_step_element_key_string_any_test.go @@ -0,0 +1,132 @@ +package path_test + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func TestExpressionStepElementKeyStringAnyEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + step path.ExpressionStepElementKeyStringAny + other path.ExpressionStep + expected bool + }{ + "ExpressionStepAttributeNameExact": { + step: path.ExpressionStepElementKeyStringAny{}, + other: path.ExpressionStepAttributeNameExact("test"), + expected: false, + }, + "ExpressionStepElementKeyIntExact": { + step: path.ExpressionStepElementKeyStringAny{}, + other: path.ExpressionStepElementKeyIntExact(0), + expected: false, + }, + "ExpressionStepElementKeyStringAny": { + step: path.ExpressionStepElementKeyStringAny{}, + other: path.ExpressionStepElementKeyStringAny{}, + expected: true, + }, + "ExpressionStepElementKeyStringExact": { + step: path.ExpressionStepElementKeyStringAny{}, + other: path.ExpressionStepElementKeyStringExact("test"), + expected: false, + }, + "ExpressionStepElementKeyValueExact": { + step: path.ExpressionStepElementKeyStringAny{}, + other: path.ExpressionStepElementKeyValueExact{Value: types.String{Value: "test"}}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.step.Equal(testCase.other) + + if got != testCase.expected { + t.Errorf("expected %t, got %t", testCase.expected, got) + } + }) + } +} + +func TestExpressionStepElementKeyStringAnyMatches(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + step path.ExpressionStepElementKeyStringAny + pathStep path.PathStep + expected bool + }{ + "StepAttributeName": { + step: path.ExpressionStepElementKeyStringAny{}, + pathStep: path.PathStepAttributeName("test"), + expected: false, + }, + "StepElementKeyInt": { + step: path.ExpressionStepElementKeyStringAny{}, + pathStep: path.PathStepElementKeyInt(0), + expected: false, + }, + "StepElementKeyString": { + step: path.ExpressionStepElementKeyStringAny{}, + pathStep: path.PathStepElementKeyString("test"), + expected: true, + }, + "StepElementKeyValue": { + step: path.ExpressionStepElementKeyStringAny{}, + pathStep: path.PathStepElementKeyValue{Value: types.String{Value: "test"}}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.step.Matches(testCase.pathStep) + + if got != testCase.expected { + t.Errorf("expected %t, got %t", testCase.expected, got) + } + }) + } +} + +func TestExpressionStepElementKeyStringAnyString(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + step path.ExpressionStepElementKeyStringAny + expected string + }{ + "basic": { + step: path.ExpressionStepElementKeyStringAny{}, + expected: `["*"]`, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.step.String() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/path/expression_step_element_key_string_exact.go b/path/expression_step_element_key_string_exact.go new file mode 100644 index 000000000..cf8120816 --- /dev/null +++ b/path/expression_step_element_key_string_exact.go @@ -0,0 +1,47 @@ +package path + +import ( + "fmt" +) + +// Ensure ExpressionStepElementKeyStringExact satisfies the ExpressionStep +// interface. +var _ ExpressionStep = ExpressionStepElementKeyStringExact("") + +// ExpressionStepElementKeyStringExact is an attribute path expression for an exact string +// key within a map. Map keys are always strings. +type ExpressionStepElementKeyStringExact string + +// Equal returns true if the given ExpressionStep is a +// ExpressionStepElementKeyStringExact and the string element key is equivalent. +func (s ExpressionStepElementKeyStringExact) Equal(o ExpressionStep) bool { + other, ok := o.(ExpressionStepElementKeyStringExact) + + if !ok { + return false + } + + return string(s) == string(other) +} + +// Matches returns true if the given PathStep is fulfilled by the +// ExpressionStepElementKeyStringExact condition. +func (s ExpressionStepElementKeyStringExact) Matches(pathStep PathStep) bool { + pathStepElementKeyString, ok := pathStep.(PathStepElementKeyString) + + if !ok { + return false + } + + return string(s) == string(pathStepElementKeyString) +} + +// String returns the human-readable representation of the element key +// expression. It is intended for logging and error messages and is not +// protected by compatibility guarantees. +func (s ExpressionStepElementKeyStringExact) String() string { + return fmt.Sprintf("[%q]", string(s)) +} + +// unexported satisfies the Step interface. +func (s ExpressionStepElementKeyStringExact) unexported() {} diff --git a/path/expression_step_element_key_string_exact_test.go b/path/expression_step_element_key_string_exact_test.go new file mode 100644 index 000000000..13c8cebd0 --- /dev/null +++ b/path/expression_step_element_key_string_exact_test.go @@ -0,0 +1,141 @@ +package path_test + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func TestExpressionStepElementKeyStringExactEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + step path.ExpressionStepElementKeyStringExact + other path.ExpressionStep + expected bool + }{ + "ExpressionStepAttributeNameExact": { + step: path.ExpressionStepElementKeyStringExact("test"), + other: path.ExpressionStepAttributeNameExact("test"), + expected: false, + }, + "ExpressionStepElementKeyIntExact": { + step: path.ExpressionStepElementKeyStringExact("test"), + other: path.ExpressionStepElementKeyIntExact(1), + expected: false, + }, + "ExpressionStepElementKeyStringExact-different": { + step: path.ExpressionStepElementKeyStringExact("test"), + other: path.ExpressionStepElementKeyStringExact("not-test"), + expected: false, + }, + "ExpressionStepElementKeyStringExact-equal": { + step: path.ExpressionStepElementKeyStringExact("test"), + other: path.ExpressionStepElementKeyStringExact("test"), + expected: true, + }, + "ExpressionStepElementKeyValueExact": { + step: path.ExpressionStepElementKeyStringExact("test"), + other: path.ExpressionStepElementKeyValueExact{Value: types.String{Value: "test"}}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.step.Equal(testCase.other) + + if got != testCase.expected { + t.Errorf("expected %t, got %t", testCase.expected, got) + } + }) + } +} + +func TestExpressionStepElementKeyStringExactMatches(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + step path.ExpressionStepElementKeyStringExact + pathStep path.PathStep + expected bool + }{ + "StepAttributeName": { + step: path.ExpressionStepElementKeyStringExact("test"), + pathStep: path.PathStepAttributeName("test"), + expected: false, + }, + "StepElementKeyInt": { + step: path.ExpressionStepElementKeyStringExact("test"), + pathStep: path.PathStepElementKeyInt(0), + expected: false, + }, + "StepElementKeyString-different": { + step: path.ExpressionStepElementKeyStringExact("test"), + pathStep: path.PathStepElementKeyString("not-test"), + expected: false, + }, + "StepElementKeyString-equal": { + step: path.ExpressionStepElementKeyStringExact("test"), + pathStep: path.PathStepElementKeyString("test"), + expected: true, + }, + "StepElementKeyValue": { + step: path.ExpressionStepElementKeyStringExact("test"), + pathStep: path.PathStepElementKeyValue{Value: types.String{Value: "test"}}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.step.Matches(testCase.pathStep) + + if got != testCase.expected { + t.Errorf("expected %t, got %t", testCase.expected, got) + } + }) + } +} + +func TestExpressionStepElementKeyStringExactString(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + step path.ExpressionStepElementKeyStringExact + expected string + }{ + "basic": { + step: path.ExpressionStepElementKeyStringExact("test"), + expected: `["test"]`, + }, + "quotes": { + step: path.ExpressionStepElementKeyStringExact(`testing is "fun"`), + expected: `["testing is \"fun\""]`, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.step.String() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/path/expression_step_element_key_value_any.go b/path/expression_step_element_key_value_any.go new file mode 100644 index 000000000..26e93ae26 --- /dev/null +++ b/path/expression_step_element_key_value_any.go @@ -0,0 +1,35 @@ +package path + +// Ensure ExpressionStepElementKeyValueAny satisfies the ExpressionStep +// interface. +var _ ExpressionStep = ExpressionStepElementKeyValueAny{} + +// ExpressionStepElementKeyValueAny is an attribute path expression for a matching any +// Value element within a set. +type ExpressionStepElementKeyValueAny struct{} + +// Equal returns true if the given ExpressionStep is a +// ExpressionStepElementKeyValueAny. +func (s ExpressionStepElementKeyValueAny) Equal(o ExpressionStep) bool { + _, ok := o.(ExpressionStepElementKeyValueAny) + + return ok +} + +// Matches returns true if the given PathStep is fulfilled by the +// ExpressionStepElementKeyValueAny condition. +func (s ExpressionStepElementKeyValueAny) Matches(pathStep PathStep) bool { + _, ok := pathStep.(PathStepElementKeyValue) + + return ok +} + +// String returns the human-readable representation of the element key +// expression. It is intended for logging and error messages and is not +// protected by compatibility guarantees. +func (s ExpressionStepElementKeyValueAny) String() string { + return "[Value(*)]" +} + +// unexported satisfies the Step interface. +func (s ExpressionStepElementKeyValueAny) unexported() {} diff --git a/path/expression_step_element_key_value_any_test.go b/path/expression_step_element_key_value_any_test.go new file mode 100644 index 000000000..2502bbef3 --- /dev/null +++ b/path/expression_step_element_key_value_any_test.go @@ -0,0 +1,132 @@ +package path_test + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func TestExpressionStepElementKeyValueAnyEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + step path.ExpressionStepElementKeyValueAny + other path.ExpressionStep + expected bool + }{ + "ExpressionStepAttributeNameExact": { + step: path.ExpressionStepElementKeyValueAny{}, + other: path.ExpressionStepAttributeNameExact("test"), + expected: false, + }, + "ExpressionStepElementKeyIntExact": { + step: path.ExpressionStepElementKeyValueAny{}, + other: path.ExpressionStepElementKeyIntExact(0), + expected: false, + }, + "ExpressionStepElementKeyStringExact": { + step: path.ExpressionStepElementKeyValueAny{}, + other: path.ExpressionStepElementKeyStringExact("test"), + expected: false, + }, + "ExpressionStepElementKeyValueAny": { + step: path.ExpressionStepElementKeyValueAny{}, + other: path.ExpressionStepElementKeyValueAny{}, + expected: true, + }, + "ExpressionStepElementKeyValueExact": { + step: path.ExpressionStepElementKeyValueAny{}, + other: path.ExpressionStepElementKeyValueExact{Value: types.String{Value: "test"}}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.step.Equal(testCase.other) + + if got != testCase.expected { + t.Errorf("expected %t, got %t", testCase.expected, got) + } + }) + } +} + +func TestExpressionStepElementKeyValueAnyMatches(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + step path.ExpressionStepElementKeyValueAny + pathStep path.PathStep + expected bool + }{ + "StepAttributeName": { + step: path.ExpressionStepElementKeyValueAny{}, + pathStep: path.PathStepAttributeName("test"), + expected: false, + }, + "StepElementKeyInt": { + step: path.ExpressionStepElementKeyValueAny{}, + pathStep: path.PathStepElementKeyInt(0), + expected: false, + }, + "StepElementKeyString": { + step: path.ExpressionStepElementKeyValueAny{}, + pathStep: path.PathStepElementKeyString("test"), + expected: false, + }, + "StepElementKeyValue": { + step: path.ExpressionStepElementKeyValueAny{}, + pathStep: path.PathStepElementKeyValue{Value: 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.step.Matches(testCase.pathStep) + + if got != testCase.expected { + t.Errorf("expected %t, got %t", testCase.expected, got) + } + }) + } +} + +func TestExpressionStepElementKeyValueAnyString(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + step path.ExpressionStepElementKeyValueAny + expected string + }{ + "basic": { + step: path.ExpressionStepElementKeyValueAny{}, + expected: "[Value(*)]", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.step.String() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/path/expression_step_element_key_value_exact.go b/path/expression_step_element_key_value_exact.go new file mode 100644 index 000000000..d60842cd9 --- /dev/null +++ b/path/expression_step_element_key_value_exact.go @@ -0,0 +1,51 @@ +package path + +import ( + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/attr" +) + +// Ensure ExpressionStepElementKeyValueExact satisfies the Step interface. +// var _ Step = ExpressionStepElementKeyValueExact(/* ... */) + +// ExpressionStepElementKeyValueExact is an attribute path expression for an exact Value +// element within a set. Sets do not use integer-based indexing. +type ExpressionStepElementKeyValueExact struct { + // Value is an interface, so it cannot be type aliased with methods. + attr.Value +} + +// Equal returns true if the given ExpressionStep is a +// ExpressionStepElementKeyValueExact and the Value element key is equivalent. +func (s ExpressionStepElementKeyValueExact) Equal(o ExpressionStep) bool { + other, ok := o.(ExpressionStepElementKeyValueExact) + + if !ok { + return false + } + + return s.Value.Equal(other.Value) +} + +// Matches returns true if the given PathStep is fulfilled by the +// ExpressionStepElementKeyValueExact condition. +func (s ExpressionStepElementKeyValueExact) Matches(pathStep PathStep) bool { + pathStepElementKeyValue, ok := pathStep.(PathStepElementKeyValue) + + if !ok { + return false + } + + return s.Value.Equal(pathStepElementKeyValue.Value) +} + +// String returns the human-readable representation of the element key +// expression. It is intended for logging and error messages and is not +// protected by compatibility guarantees. +func (s ExpressionStepElementKeyValueExact) String() string { + return fmt.Sprintf("[Value(%s)]", s.Value.String()) +} + +// unexported satisfies the Step interface. +func (s ExpressionStepElementKeyValueExact) unexported() {} diff --git a/path/expression_step_element_key_value_exact_test.go b/path/expression_step_element_key_value_exact_test.go new file mode 100644 index 000000000..78e041416 --- /dev/null +++ b/path/expression_step_element_key_value_exact_test.go @@ -0,0 +1,191 @@ +package path_test + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func TestExpressionStepElementKeyValueExactEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + step path.ExpressionStepElementKeyValueExact + other path.ExpressionStep + expected bool + }{ + "ExpressionStepAttributeNameExact": { + step: path.ExpressionStepElementKeyValueExact{Value: types.String{Value: "test"}}, + other: path.ExpressionStepAttributeNameExact("test"), + expected: false, + }, + "ExpressionStepElementKeyIntExact": { + step: path.ExpressionStepElementKeyValueExact{Value: types.String{Value: "test"}}, + other: path.ExpressionStepElementKeyIntExact(0), + expected: false, + }, + "ExpressionStepElementKeyStringExact": { + step: path.ExpressionStepElementKeyValueExact{Value: types.String{Value: "test"}}, + other: path.ExpressionStepElementKeyStringExact("test"), + expected: false, + }, + "ExpressionStepElementKeyValueExact-different": { + step: path.ExpressionStepElementKeyValueExact{Value: types.String{Value: "test"}}, + other: path.ExpressionStepElementKeyValueExact{Value: types.String{Value: "not-test"}}, + expected: false, + }, + "ExpressionStepElementKeyValueExact-equal": { + step: path.ExpressionStepElementKeyValueExact{Value: types.String{Value: "test"}}, + other: path.ExpressionStepElementKeyValueExact{Value: 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.step.Equal(testCase.other) + + if got != testCase.expected { + t.Errorf("expected %t, got %t", testCase.expected, got) + } + }) + } +} + +func TestExpressionStepElementKeyValueExactMatches(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + step path.ExpressionStepElementKeyValueExact + pathStep path.PathStep + expected bool + }{ + "StepAttributeName": { + step: path.ExpressionStepElementKeyValueExact{Value: types.String{Value: "test"}}, + pathStep: path.PathStepAttributeName("test"), + expected: false, + }, + "StepElementKeyInt": { + step: path.ExpressionStepElementKeyValueExact{Value: types.String{Value: "test"}}, + pathStep: path.PathStepElementKeyInt(0), + expected: false, + }, + "StepElementKeyString": { + step: path.ExpressionStepElementKeyValueExact{Value: types.String{Value: "test"}}, + pathStep: path.PathStepElementKeyString("test"), + expected: false, + }, + "StepElementKeyValue-different": { + step: path.ExpressionStepElementKeyValueExact{Value: types.String{Value: "test"}}, + pathStep: path.PathStepElementKeyValue{Value: types.String{Value: "not-test"}}, + expected: false, + }, + "StepElementKeyValue-equal": { + step: path.ExpressionStepElementKeyValueExact{Value: types.String{Value: "test"}}, + pathStep: path.PathStepElementKeyValue{Value: 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.step.Matches(testCase.pathStep) + + if got != testCase.expected { + t.Errorf("expected %t, got %t", testCase.expected, got) + } + }) + } +} + +func TestExpressionStepElementKeyValueExactString(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + step path.ExpressionStepElementKeyValueExact + expected string + }{ + "bool-value": { + step: path.ExpressionStepElementKeyValueExact{Value: types.Bool{Value: true}}, + expected: `[Value(true)]`, + }, + "float64-value": { + step: path.ExpressionStepElementKeyValueExact{Value: types.Float64{Value: 1.2}}, + expected: `[Value(1.200000)]`, + }, + "int64-value": { + step: path.ExpressionStepElementKeyValueExact{Value: types.Int64{Value: 123}}, + expected: `[Value(123)]`, + }, + "list-value": { + step: path.ExpressionStepElementKeyValueExact{Value: types.List{ + Elems: []attr.Value{ + types.String{Value: "test-element-1"}, + types.String{Value: "test-element-2"}, + }, + ElemType: types.StringType, + }}, + expected: `[Value(["test-element-1","test-element-2"])]`, + }, + "map-value": { + step: path.ExpressionStepElementKeyValueExact{Value: types.Map{ + Elems: map[string]attr.Value{ + "test-key-1": types.String{Value: "test-value-1"}, + "test-key-2": types.String{Value: "test-value-2"}, + }, + ElemType: types.StringType, + }}, + expected: `[Value({"test-key-1":"test-value-1","test-key-2":"test-value-2"})]`, + }, + "object-value": { + step: path.ExpressionStepElementKeyValueExact{Value: types.Object{ + Attrs: map[string]attr.Value{ + "test_attr_1": types.Bool{Value: true}, + "test_attr_2": types.String{Value: "test-value"}, + }, + AttrTypes: map[string]attr.Type{ + "test_attr_1": types.BoolType, + "test_attr_2": types.StringType, + }, + }}, + expected: `[Value({"test_attr_1":true,"test_attr_2":"test-value"})]`, + }, + "string-null": { + step: path.ExpressionStepElementKeyValueExact{Value: types.String{Null: true}}, + expected: `[Value()]`, + }, + "string-unknown": { + step: path.ExpressionStepElementKeyValueExact{Value: types.String{Unknown: true}}, + expected: `[Value()]`, + }, + "string-value": { + step: path.ExpressionStepElementKeyValueExact{Value: types.String{Value: "test"}}, + expected: `[Value("test")]`, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.step.String() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/path/expression_step_parent.go b/path/expression_step_parent.go new file mode 100644 index 000000000..9590ad0bd --- /dev/null +++ b/path/expression_step_parent.go @@ -0,0 +1,35 @@ +package path + +// Ensure StepParent satisfies the ExpressionStep interface. +var _ ExpressionStep = ExpressionStepParent{} + +// StepParent is an attribute path expression for a traversing to the parent +// attribute path relative to the current one. This is intended only for the +// start of attribute-level expressions which will be combined with the current +// attribute path being called. +type ExpressionStepParent struct{} + +// Equal returns true if the given ExpressionStep is a ExpressionStepParent. +func (s ExpressionStepParent) Equal(o ExpressionStep) bool { + _, ok := o.(ExpressionStepParent) + + return ok +} + +// Matches returns true if the given PathStep is fulfilled by the +// ExpressionStepParent condition. +func (s ExpressionStepParent) Matches(_ PathStep) bool { + // This return value should have no effect, as this Step is a + // sentinel type, rather than one that should be used in matching. + return false +} + +// String returns the human-readable representation of the element key +// expression. It is intended for logging and error messages and is not +// protected by compatibility guarantees. +func (s ExpressionStepParent) String() string { + return "<" +} + +// unexported satisfies the Step interface. +func (s ExpressionStepParent) unexported() {} diff --git a/path/expression_step_parent_test.go b/path/expression_step_parent_test.go new file mode 100644 index 000000000..87701c1a7 --- /dev/null +++ b/path/expression_step_parent_test.go @@ -0,0 +1,132 @@ +package path_test + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func TestExpressionStepParentEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + step path.ExpressionStepParent + other path.ExpressionStep + expected bool + }{ + "ExpressionStepAttributeNameExact": { + step: path.ExpressionStepParent{}, + other: path.ExpressionStepAttributeNameExact("test"), + expected: false, + }, + "ExpressionStepElementKeyIntExact": { + step: path.ExpressionStepParent{}, + other: path.ExpressionStepElementKeyIntExact(0), + expected: false, + }, + "ExpressionStepElementKeyStringExact": { + step: path.ExpressionStepParent{}, + other: path.ExpressionStepElementKeyStringExact("test"), + expected: false, + }, + "ExpressionStepElementKeyValueExact": { + step: path.ExpressionStepParent{}, + other: path.ExpressionStepElementKeyValueExact{Value: types.String{Value: "test"}}, + expected: false, + }, + "StepParent": { + step: path.ExpressionStepParent{}, + other: path.ExpressionStepParent{}, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.step.Equal(testCase.other) + + if got != testCase.expected { + t.Errorf("expected %t, got %t", testCase.expected, got) + } + }) + } +} + +func TestExpressionStepParentMatches(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + step path.ExpressionStepParent + pathStep path.PathStep + expected bool + }{ + "StepAttributeName": { + step: path.ExpressionStepParent{}, + pathStep: path.PathStepAttributeName("test"), + expected: false, + }, + "StepElementKeyInt": { + step: path.ExpressionStepParent{}, + pathStep: path.PathStepElementKeyInt(0), + expected: false, + }, + "StepElementKeyString": { + step: path.ExpressionStepParent{}, + pathStep: path.PathStepElementKeyString("test"), + expected: false, + }, + "StepElementKeyValue": { + step: path.ExpressionStepParent{}, + pathStep: path.PathStepElementKeyValue{Value: types.String{Value: "test"}}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.step.Matches(testCase.pathStep) + + if got != testCase.expected { + t.Errorf("expected %t, got %t", testCase.expected, got) + } + }) + } +} + +func TestExpressionStepParentString(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + step path.ExpressionStepParent + expected string + }{ + "basic": { + step: path.ExpressionStepParent{}, + expected: "<", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.step.String() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/path/expression_steps.go b/path/expression_steps.go new file mode 100644 index 000000000..97890dfd4 --- /dev/null +++ b/path/expression_steps.go @@ -0,0 +1,150 @@ +package path + +import "strings" + +// ExpressionSteps represents an ordered collection of attribute path +// expressions. +type ExpressionSteps []ExpressionStep + +// Append adds the given ExpressionSteps to the end of the previous ExpressionSteps and +// returns the combined result. +func (s *ExpressionSteps) Append(steps ...ExpressionStep) ExpressionSteps { + if s == nil { + return steps + } + + *s = append(*s, steps...) + + return *s +} + +// Copy returns a duplicate of the steps that is safe to modify without +// affecting the original. Returns nil if the original steps is nil. +func (s ExpressionSteps) Copy() ExpressionSteps { + if s == nil { + return nil + } + + copiedExpressionSteps := make(ExpressionSteps, len(s)) + + copy(copiedExpressionSteps, s) + + return copiedExpressionSteps +} + +// Equal returns true if the given ExpressionSteps are equivalent. +func (s ExpressionSteps) Equal(o ExpressionSteps) bool { + if len(s) != len(o) { + return false + } + + for stepIndex, step := range s { + if !step.Equal(o[stepIndex]) { + return false + } + } + + return true +} + +// LastStep returns the final ExpressionStep and the remaining ExpressionSteps. +func (s ExpressionSteps) LastStep() (ExpressionStep, ExpressionSteps) { + if len(s) == 0 { + return nil, ExpressionSteps{} + } + + if len(s) == 1 { + return s[0], ExpressionSteps{} + } + + return s[len(s)-1], s[:len(s)-1] +} + +// Matches returns true if the given PathSteps match each ExpressionStep. +// +// Any ExpressionStepParent will automatically be resolved. +func (s ExpressionSteps) Matches(pathSteps PathSteps) bool { + resolvedExpressionSteps := s.Resolve() + + if len(resolvedExpressionSteps) != len(pathSteps) { + return false + } + + for stepIndex, expressionStep := range resolvedExpressionSteps { + if !expressionStep.Matches(pathSteps[stepIndex]) { + return false + } + } + + return true +} + +// NextStep returns the first ExpressionStep and the remaining ExpressionSteps. +func (s ExpressionSteps) NextStep() (ExpressionStep, ExpressionSteps) { + if len(s) == 0 { + return nil, s + } + + return s[0], s[1:] +} + +// Resolve returns a copy of ExpressionSteps without any ExpressionStepParent. +// +// Returns empty ExpressionSteps if any ExpressionStepParent attempt to go +// beyond the first element. Returns nil if the original steps is nil. +func (s ExpressionSteps) Resolve() ExpressionSteps { + if s == nil { + return nil + } + + result := make(ExpressionSteps, 0, len(s)) + + // This might not be the most efficient or prettiest algorithm, but it + // works for now. + for _, step := range s { + _, ok := step.(ExpressionStepParent) + + if !ok { + result.Append(step) + + continue + } + + // Allow parent traversal up to the root, but not beyond. + if len(result) == 0 { + return ExpressionSteps{} + } + + _, remaining := result.LastStep() + + if len(remaining) == 0 { + result = ExpressionSteps{} + + continue + } + + result = remaining + } + + return result +} + +// String returns the human-readable representation of the ExpressionSteps. +// It is intended for logging and error messages and is not protected by +// compatibility guarantees. +func (s ExpressionSteps) String() string { + var result strings.Builder + + for stepIndex, step := range s { + if stepIndex != 0 { + switch step.(type) { + case ExpressionStepAttributeNameExact, ExpressionStepParent: + result.WriteString(".") + } + } + + result.WriteString(step.String()) + } + + return result.String() +} diff --git a/path/expression_steps_test.go b/path/expression_steps_test.go new file mode 100644 index 000000000..2b69acbf7 --- /dev/null +++ b/path/expression_steps_test.go @@ -0,0 +1,980 @@ +package path_test + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func TestExpressionStepsAppend(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + steps path.ExpressionSteps + add path.ExpressionSteps + expected path.ExpressionSteps + }{ + "empty-empty": { + steps: path.ExpressionSteps{}, + add: path.ExpressionSteps{}, + expected: path.ExpressionSteps{}, + }, + "empty-nonempty": { + steps: path.ExpressionSteps{}, + add: path.ExpressionSteps{path.ExpressionStepAttributeNameExact("test")}, + expected: path.ExpressionSteps{path.ExpressionStepAttributeNameExact("test")}, + }, + "nonempty-empty": { + steps: path.ExpressionSteps{path.ExpressionStepAttributeNameExact("test")}, + add: path.ExpressionSteps{}, + expected: path.ExpressionSteps{path.ExpressionStepAttributeNameExact("test")}, + }, + "nonempty-nonempty": { + steps: path.ExpressionSteps{ + path.ExpressionStepAttributeNameExact("test"), + path.ExpressionStepElementKeyIntExact(0), + }, + add: path.ExpressionSteps{ + path.ExpressionStepAttributeNameExact("add-test"), + path.ExpressionStepElementKeyStringExact("add-test-key"), + }, + expected: path.ExpressionSteps{ + path.ExpressionStepAttributeNameExact("test"), + path.ExpressionStepElementKeyIntExact(0), + path.ExpressionStepAttributeNameExact("add-test"), + path.ExpressionStepElementKeyStringExact("add-test-key"), + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.steps.Append(testCase.add...) + + if diff := cmp.Diff(testCase.steps, 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 TestExpressionStepsCopy(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + steps path.ExpressionSteps + expected path.ExpressionSteps + }{ + "nil": { + steps: nil, + expected: nil, + }, + "empty": { + steps: path.ExpressionSteps{}, + expected: path.ExpressionSteps{}, + }, + "shallow": { + steps: path.ExpressionSteps{ + path.ExpressionStepAttributeNameExact("test"), + }, + expected: path.ExpressionSteps{ + path.ExpressionStepAttributeNameExact("test"), + }, + }, + "deep": { + steps: path.ExpressionSteps{ + path.ExpressionStepAttributeNameExact("test1"), + path.ExpressionStepElementKeyIntExact(0), + path.ExpressionStepAttributeNameExact("test2"), + }, + expected: path.ExpressionSteps{ + path.ExpressionStepAttributeNameExact("test1"), + path.ExpressionStepElementKeyIntExact(0), + path.ExpressionStepAttributeNameExact("test2"), + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.steps.Copy() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + + // Ensure original is not modified + got.Append(path.ExpressionStepAttributeNameExact("modify-test")) + + if diff := cmp.Diff(got, testCase.expected); diff == "" { + t.Error("unexpected modification") + } + }) + } +} + +func TestExpressionStepsEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + steps path.ExpressionSteps + other path.ExpressionSteps + expected bool + }{ + "nil-nil": { + steps: nil, + other: nil, + expected: true, + }, + "empty-empty": { + steps: path.ExpressionSteps{}, + other: path.ExpressionSteps{}, + expected: true, + }, + "different-length": { + steps: path.ExpressionSteps{ + path.ExpressionStepAttributeNameExact("test1"), + path.ExpressionStepAttributeNameExact("test2"), + }, + other: path.ExpressionSteps{ + path.ExpressionStepAttributeNameExact("test1"), + }, + expected: false, + }, + "StepAttributeName-different": { + steps: path.ExpressionSteps{ + path.ExpressionStepAttributeNameExact("test"), + }, + other: path.ExpressionSteps{ + path.ExpressionStepAttributeNameExact("not-test"), + }, + expected: false, + }, + "StepAttributeName-equal": { + steps: path.ExpressionSteps{ + path.ExpressionStepAttributeNameExact("test"), + }, + other: path.ExpressionSteps{ + path.ExpressionStepAttributeNameExact("test"), + }, + expected: true, + }, + "StepAttributeName-StepElementKeyInt-different": { + steps: path.ExpressionSteps{ + path.ExpressionStepAttributeNameExact("test"), + path.ExpressionStepElementKeyIntExact(0), + }, + other: path.ExpressionSteps{ + path.ExpressionStepAttributeNameExact("test"), + path.ExpressionStepElementKeyIntExact(1), + }, + expected: false, + }, + "StepAttributeName-StepElementKeyInt-equal": { + steps: path.ExpressionSteps{ + path.ExpressionStepAttributeNameExact("test"), + path.ExpressionStepElementKeyIntExact(0), + }, + other: path.ExpressionSteps{ + path.ExpressionStepAttributeNameExact("test"), + path.ExpressionStepElementKeyIntExact(0), + }, + expected: true, + }, + "StepAttributeName-StepElementKeyString-different": { + steps: path.ExpressionSteps{ + path.ExpressionStepAttributeNameExact("test"), + path.ExpressionStepElementKeyStringExact("test-key"), + }, + other: path.ExpressionSteps{ + path.ExpressionStepAttributeNameExact("test"), + path.ExpressionStepElementKeyStringExact("not-test-key"), + }, + expected: false, + }, + "StepAttributeName-StepElementKeyString-equal": { + steps: path.ExpressionSteps{ + path.ExpressionStepAttributeNameExact("test"), + path.ExpressionStepElementKeyStringExact("test-key"), + }, + other: path.ExpressionSteps{ + path.ExpressionStepAttributeNameExact("test"), + path.ExpressionStepElementKeyStringExact("test-key"), + }, + expected: true, + }, + "StepAttributeName-StepElementKeyValue-different": { + steps: path.ExpressionSteps{ + path.ExpressionStepAttributeNameExact("test"), + path.ExpressionStepElementKeyValueExact{Value: types.String{Value: "test-value"}}, + }, + other: path.ExpressionSteps{ + path.ExpressionStepAttributeNameExact("test"), + path.ExpressionStepElementKeyValueExact{Value: types.String{Value: "not-test-value"}}, + }, + expected: false, + }, + "StepAttributeName-StepElementKeyValue-equal": { + steps: path.ExpressionSteps{ + path.ExpressionStepAttributeNameExact("test"), + path.ExpressionStepElementKeyValueExact{Value: types.String{Value: "test-value"}}, + }, + other: path.ExpressionSteps{ + path.ExpressionStepAttributeNameExact("test"), + path.ExpressionStepElementKeyValueExact{Value: types.String{Value: "test-value"}}, + }, + expected: true, + }, + "StepElementKeyInt-different": { + steps: path.ExpressionSteps{ + path.ExpressionStepElementKeyIntExact(0), + }, + other: path.ExpressionSteps{ + path.ExpressionStepElementKeyIntExact(1), + }, + expected: false, + }, + "StepElementKeyInt-equal": { + steps: path.ExpressionSteps{ + path.ExpressionStepElementKeyIntExact(0), + }, + other: path.ExpressionSteps{ + path.ExpressionStepElementKeyIntExact(0), + }, + expected: true, + }, + "StepElementKeyString-different": { + steps: path.ExpressionSteps{ + path.ExpressionStepElementKeyStringExact("test"), + }, + other: path.ExpressionSteps{ + path.ExpressionStepElementKeyStringExact("not-test"), + }, + expected: false, + }, + "StepElementKeyString-equal": { + steps: path.ExpressionSteps{ + path.ExpressionStepElementKeyStringExact("test"), + }, + other: path.ExpressionSteps{ + path.ExpressionStepElementKeyStringExact("test"), + }, + expected: true, + }, + "StepElementKeyValue-different": { + steps: path.ExpressionSteps{ + path.ExpressionStepElementKeyValueExact{Value: types.String{Value: "test-value"}}, + }, + other: path.ExpressionSteps{ + path.ExpressionStepElementKeyValueExact{Value: types.String{Value: "not-test-value"}}, + }, + expected: false, + }, + "StepElementKeyValue-equal": { + steps: path.ExpressionSteps{ + path.ExpressionStepElementKeyValueExact{Value: types.String{Value: "test-value"}}, + }, + other: path.ExpressionSteps{ + path.ExpressionStepElementKeyValueExact{Value: types.String{Value: "test-value"}}, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.steps.Equal(testCase.other) + + if got != testCase.expected { + t.Errorf("expected %t, got %t", testCase.expected, got) + } + }) + } +} + +func TestExpressionStepsLastStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + steps path.ExpressionSteps + expectedLastStep path.ExpressionStep + expectedRemaining path.ExpressionSteps + }{ + "nil": { + steps: nil, + expectedLastStep: nil, + expectedRemaining: nil, + }, + "empty": { + steps: path.ExpressionSteps{}, + expectedLastStep: nil, + expectedRemaining: nil, + }, + "one": { + steps: path.ExpressionSteps{ + path.ExpressionStepAttributeNameExact("test"), + }, + expectedLastStep: path.ExpressionStepAttributeNameExact("test"), + expectedRemaining: nil, + }, + "two": { + steps: path.ExpressionSteps{ + path.ExpressionStepAttributeNameExact("test"), + path.ExpressionStepElementKeyIntExact(0), + }, + expectedLastStep: path.ExpressionStepElementKeyIntExact(0), + expectedRemaining: path.ExpressionSteps{ + path.ExpressionStepAttributeNameExact("test"), + }, + }, + "three": { + steps: path.ExpressionSteps{ + path.ExpressionStepAttributeNameExact("test"), + path.ExpressionStepElementKeyIntExact(0), + path.ExpressionStepAttributeNameExact("nested-test"), + }, + expectedLastStep: path.ExpressionStepAttributeNameExact("nested-test"), + expectedRemaining: path.ExpressionSteps{ + path.ExpressionStepAttributeNameExact("test"), + path.ExpressionStepElementKeyIntExact(0), + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + gotLastStep, gotRemaining := testCase.steps.LastStep() + + if diff := cmp.Diff(gotLastStep, testCase.expectedLastStep); diff != "" { + t.Errorf("unexpected last step difference: %s", diff) + } + + if diff := cmp.Diff(gotRemaining, testCase.expectedRemaining); diff != "" { + t.Errorf("unexpected remaining difference: %s", diff) + } + }) + } +} + +func TestExpressionStepsMatches(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + steps path.ExpressionSteps + pathSteps path.PathSteps + expected bool + }{ + "empty-empty": { + steps: path.ExpressionSteps{}, + pathSteps: path.PathSteps{}, + expected: true, + }, + "empty-nonempty": { + steps: path.ExpressionSteps{}, + pathSteps: path.PathSteps{ + path.PathStepAttributeName("test"), + }, + expected: false, + }, + "nonempty-empty": { + steps: path.ExpressionSteps{ + path.ExpressionStepAttributeNameExact("test"), + }, + pathSteps: path.PathSteps{}, + expected: false, + }, + "AttributeNameExact-different": { + steps: path.ExpressionSteps{ + path.ExpressionStepAttributeNameExact("test"), + }, + pathSteps: path.PathSteps{ + path.PathStepAttributeName("not-test"), + }, + expected: false, + }, + "AttributeNameExact-equal": { + steps: path.ExpressionSteps{ + path.ExpressionStepAttributeNameExact("test"), + }, + pathSteps: path.PathSteps{ + path.PathStepAttributeName("test"), + }, + expected: true, + }, + "AttributeNameExact-AttributeNameExact-different-firststep": { + steps: path.ExpressionSteps{ + path.ExpressionStepAttributeNameExact("test1"), + path.ExpressionStepAttributeNameExact("test2"), + }, + pathSteps: path.PathSteps{ + path.PathStepAttributeName("test2"), + path.PathStepAttributeName("test2"), + }, + expected: false, + }, + "AttributeNameExact-AttributeNameExact-different-laststep": { + steps: path.ExpressionSteps{ + path.ExpressionStepAttributeNameExact("test1"), + path.ExpressionStepAttributeNameExact("test2"), + }, + pathSteps: path.PathSteps{ + path.PathStepAttributeName("test1"), + path.PathStepAttributeName("test3"), + }, + expected: false, + }, + "AttributeNameExact-AttributeNameExact-equal": { + steps: path.ExpressionSteps{ + path.ExpressionStepAttributeNameExact("test1"), + path.ExpressionStepAttributeNameExact("test2"), + }, + pathSteps: path.PathSteps{ + path.PathStepAttributeName("test1"), + path.PathStepAttributeName("test2"), + }, + expected: true, + }, + "AttributeNameExact-AttributeNameExact-Parent-different": { + steps: path.ExpressionSteps{ + path.ExpressionStepAttributeNameExact("test1"), + path.ExpressionStepAttributeNameExact("test2"), + path.ExpressionStepParent{}, + }, + pathSteps: path.PathSteps{ + path.PathStepAttributeName("test2"), + }, + expected: false, + }, + "AttributeNameExact-AttributeNameExact-Parent-equal": { + steps: path.ExpressionSteps{ + path.ExpressionStepAttributeNameExact("test1"), + path.ExpressionStepAttributeNameExact("test2"), + path.ExpressionStepParent{}, + }, + pathSteps: path.PathSteps{ + path.PathStepAttributeName("test1"), + }, + expected: true, + }, + "AttributeNameExact-AttributeNameExact-Parent-AttributeNameExact-different": { + steps: path.ExpressionSteps{ + path.ExpressionStepAttributeNameExact("test1"), + path.ExpressionStepAttributeNameExact("test2"), + path.ExpressionStepParent{}, + path.ExpressionStepAttributeNameExact("test3"), + }, + pathSteps: path.PathSteps{ + path.PathStepAttributeName("test1"), + path.PathStepAttributeName("test2"), + }, + expected: false, + }, + "AttributeNameExact-AttributeNameExact-Parent-AttributeNameExact-equal": { + steps: path.ExpressionSteps{ + path.ExpressionStepAttributeNameExact("test1"), + path.ExpressionStepAttributeNameExact("test2"), + path.ExpressionStepParent{}, + path.ExpressionStepAttributeNameExact("test3"), + }, + pathSteps: path.PathSteps{ + path.PathStepAttributeName("test1"), + path.PathStepAttributeName("test3"), + }, + expected: true, + }, + "AttributeNameExact-ElementKeyIntAny": { + steps: path.ExpressionSteps{ + path.ExpressionStepAttributeNameExact("test"), + path.ExpressionStepElementKeyIntAny{}, + }, + pathSteps: path.PathSteps{ + path.PathStepAttributeName("test"), + path.PathStepElementKeyInt(0), + }, + expected: true, + }, + "AttributeNameExact-ElementKeyIntExact-different": { + steps: path.ExpressionSteps{ + path.ExpressionStepAttributeNameExact("test"), + path.ExpressionStepElementKeyIntExact(0), + }, + pathSteps: path.PathSteps{ + path.PathStepAttributeName("test"), + path.PathStepElementKeyInt(1), + }, + expected: false, + }, + "AttributeNameExact-ElementKeyIntExact-equal": { + steps: path.ExpressionSteps{ + path.ExpressionStepAttributeNameExact("test"), + path.ExpressionStepElementKeyIntExact(0), + }, + pathSteps: path.PathSteps{ + path.PathStepAttributeName("test"), + path.PathStepElementKeyInt(0), + }, + expected: true, + }, + "AttributeNameExact-ElementKeyStringAny": { + steps: path.ExpressionSteps{ + path.ExpressionStepAttributeNameExact("test"), + path.ExpressionStepElementKeyStringAny{}, + }, + pathSteps: path.PathSteps{ + path.PathStepAttributeName("test"), + path.PathStepElementKeyString("test-key"), + }, + expected: true, + }, + "AttributeNameExact-ElementKeyStringExact-different": { + steps: path.ExpressionSteps{ + path.ExpressionStepAttributeNameExact("test"), + path.ExpressionStepElementKeyStringExact("test-key"), + }, + pathSteps: path.PathSteps{ + path.PathStepAttributeName("test"), + path.PathStepElementKeyString("not-test-key"), + }, + expected: false, + }, + "AttributeNameExact-ElementKeyStringExact-equal": { + steps: path.ExpressionSteps{ + path.ExpressionStepAttributeNameExact("test"), + path.ExpressionStepElementKeyStringExact("test-key"), + }, + pathSteps: path.PathSteps{ + path.PathStepAttributeName("test"), + path.PathStepElementKeyString("test-key"), + }, + expected: true, + }, + "AttributeNameExact-ElementKeyValueAny": { + steps: path.ExpressionSteps{ + path.ExpressionStepAttributeNameExact("test"), + path.ExpressionStepElementKeyValueAny{}, + }, + pathSteps: path.PathSteps{ + path.PathStepAttributeName("test"), + path.PathStepElementKeyValue{Value: types.String{Value: "test-value"}}, + }, + expected: true, + }, + "AttributeNameExact-ElementKeyValueExact-different": { + steps: path.ExpressionSteps{ + path.ExpressionStepAttributeNameExact("test"), + path.ExpressionStepElementKeyValueExact{Value: types.String{Value: "test-value"}}, + }, + pathSteps: path.PathSteps{ + path.PathStepAttributeName("test"), + path.PathStepElementKeyValue{Value: types.String{Value: "not-test-value"}}, + }, + expected: false, + }, + "AttributeNameExact-ElementKeyValueExact-equal": { + steps: path.ExpressionSteps{ + path.ExpressionStepAttributeNameExact("test"), + path.ExpressionStepElementKeyValueExact{Value: types.String{Value: "test-value"}}, + }, + pathSteps: path.PathSteps{ + path.PathStepAttributeName("test"), + path.PathStepElementKeyValue{Value: types.String{Value: "test-value"}}, + }, + expected: true, + }, + "AttributeNameExact-Parent-AttributeNameExact-different": { + steps: path.ExpressionSteps{ + path.ExpressionStepAttributeNameExact("test1"), + path.ExpressionStepParent{}, + path.ExpressionStepAttributeNameExact("test2"), + }, + pathSteps: path.PathSteps{ + path.PathStepAttributeName("test1"), + }, + expected: false, + }, + "AttributeNameExact-Parent-AttributeNameExact-equal": { + steps: path.ExpressionSteps{ + path.ExpressionStepAttributeNameExact("test1"), + path.ExpressionStepParent{}, + path.ExpressionStepAttributeNameExact("test2"), + }, + pathSteps: path.PathSteps{ + path.PathStepAttributeName("test2"), + }, + expected: true, + }, + "Parent-AttributeNameExact": { + steps: path.ExpressionSteps{ + path.ExpressionStepParent{}, + path.ExpressionStepAttributeNameExact("test"), + }, + pathSteps: path.PathSteps{ + path.PathStepAttributeName("test"), + }, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.steps.Matches(testCase.pathSteps) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestExpressionStepsNextStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + steps path.ExpressionSteps + expectedNextStep path.ExpressionStep + expectedRemaining path.ExpressionSteps + }{ + "nil": { + steps: nil, + expectedNextStep: nil, + expectedRemaining: nil, + }, + "empty": { + steps: path.ExpressionSteps{}, + expectedNextStep: nil, + expectedRemaining: nil, + }, + "one": { + steps: path.ExpressionSteps{ + path.ExpressionStepAttributeNameExact("test"), + }, + expectedNextStep: path.ExpressionStepAttributeNameExact("test"), + expectedRemaining: nil, + }, + "two": { + steps: path.ExpressionSteps{ + path.ExpressionStepAttributeNameExact("test"), + path.ExpressionStepElementKeyIntExact(0), + }, + expectedNextStep: path.ExpressionStepAttributeNameExact("test"), + expectedRemaining: path.ExpressionSteps{ + path.ExpressionStepElementKeyIntExact(0), + }, + }, + "three": { + steps: path.ExpressionSteps{ + path.ExpressionStepAttributeNameExact("test"), + path.ExpressionStepElementKeyIntExact(0), + path.ExpressionStepAttributeNameExact("nested-test"), + }, + expectedNextStep: path.ExpressionStepAttributeNameExact("test"), + expectedRemaining: path.ExpressionSteps{ + path.ExpressionStepElementKeyIntExact(0), + path.ExpressionStepAttributeNameExact("nested-test"), + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + gotNextStep, gotRemaining := testCase.steps.NextStep() + + if diff := cmp.Diff(gotNextStep, testCase.expectedNextStep); diff != "" { + t.Errorf("unexpected next step difference: %s", diff) + } + + if diff := cmp.Diff(gotRemaining, testCase.expectedRemaining); diff != "" { + t.Errorf("unexpected remaining difference: %s", diff) + } + }) + } +} + +func TestExpressionStepsResolve(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + steps path.ExpressionSteps + expected path.ExpressionSteps + }{ + "nil": { + steps: nil, + expected: nil, + }, + "empty": { + steps: path.ExpressionSteps{}, + expected: nil, + }, + "AttributeNameExact": { + steps: path.ExpressionSteps{ + path.ExpressionStepAttributeNameExact("test"), + }, + expected: path.ExpressionSteps{ + path.ExpressionStepAttributeNameExact("test"), + }, + }, + "AttributeNameExact-AttributeNameExact": { + steps: path.ExpressionSteps{ + path.ExpressionStepAttributeNameExact("test1"), + path.ExpressionStepAttributeNameExact("test2"), + }, + expected: path.ExpressionSteps{ + path.ExpressionStepAttributeNameExact("test1"), + path.ExpressionStepAttributeNameExact("test2"), + }, + }, + "AttributeNameExact-AttributeNameExact-Parent": { + steps: path.ExpressionSteps{ + path.ExpressionStepAttributeNameExact("test1"), + path.ExpressionStepAttributeNameExact("test2"), + path.ExpressionStepParent{}, + }, + expected: path.ExpressionSteps{ + path.ExpressionStepAttributeNameExact("test1"), + }, + }, + "AttributeNameExact-AttributeNameExact-Parent-AttributeNameExact": { + steps: path.ExpressionSteps{ + path.ExpressionStepAttributeNameExact("test1"), + path.ExpressionStepAttributeNameExact("test2"), + path.ExpressionStepParent{}, + path.ExpressionStepAttributeNameExact("test3"), + }, + expected: path.ExpressionSteps{ + path.ExpressionStepAttributeNameExact("test1"), + path.ExpressionStepAttributeNameExact("test3"), + }, + }, + "AttributeNameExact-Parent": { + steps: path.ExpressionSteps{ + path.ExpressionStepAttributeNameExact("test1"), + path.ExpressionStepParent{}, + }, + expected: path.ExpressionSteps{}, + }, + "AttributeNameExact-Parent-AttributeNameExact": { + steps: path.ExpressionSteps{ + path.ExpressionStepAttributeNameExact("test1"), + path.ExpressionStepParent{}, + path.ExpressionStepAttributeNameExact("test2"), + }, + expected: path.ExpressionSteps{ + path.ExpressionStepAttributeNameExact("test2"), + }, + }, + "Parent": { + steps: path.ExpressionSteps{ + path.ExpressionStepParent{}, + }, + expected: nil, + }, + "Parent-AttributeNameExact": { + steps: path.ExpressionSteps{ + path.ExpressionStepParent{}, + path.ExpressionStepAttributeNameExact("test"), + }, + expected: nil, + }, + "Parent-Parent": { + steps: path.ExpressionSteps{ + path.ExpressionStepParent{}, + path.ExpressionStepParent{}, + }, + expected: nil, + }, + "Parent-Parent-AttributeNameExact": { + steps: path.ExpressionSteps{ + path.ExpressionStepParent{}, + path.ExpressionStepParent{}, + path.ExpressionStepAttributeNameExact("test"), + }, + expected: nil, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.steps.Resolve() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestExpressionStepsString(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + steps path.ExpressionSteps + expected string + }{ + "nil": { + steps: nil, + expected: ``, + }, + "empty": { + steps: path.ExpressionSteps{}, + expected: ``, + }, + "AttributeName": { + steps: path.ExpressionSteps{ + path.ExpressionStepAttributeNameExact("test"), + }, + expected: `test`, + }, + "AttributeName-AttributeName": { + steps: path.ExpressionSteps{ + path.ExpressionStepAttributeNameExact("test1"), + path.ExpressionStepAttributeNameExact("test2"), + }, + expected: `test1.test2`, + }, + "AttributeName-AttributeName-AttributeName": { + steps: path.ExpressionSteps{ + path.ExpressionStepAttributeNameExact("test1"), + path.ExpressionStepAttributeNameExact("test2"), + path.ExpressionStepAttributeNameExact("test3"), + }, + expected: `test1.test2.test3`, + }, + "AttributeName-ElementKeyInt": { + steps: path.ExpressionSteps{ + path.ExpressionStepAttributeNameExact("test"), + path.ExpressionStepElementKeyIntExact(0), + }, + expected: `test[0]`, + }, + "AttributeName-ElementKeyInt-AttributeName": { + steps: path.ExpressionSteps{ + path.ExpressionStepAttributeNameExact("test1"), + path.ExpressionStepElementKeyIntExact(0), + path.ExpressionStepAttributeNameExact("test2"), + }, + expected: `test1[0].test2`, + }, + "AttributeName-ElementKeyInt-ElementKeyInt": { + steps: path.ExpressionSteps{ + path.ExpressionStepAttributeNameExact("test"), + path.ExpressionStepElementKeyIntExact(0), + path.ExpressionStepElementKeyIntExact(1), + }, + expected: `test[0][1]`, + }, + "AttributeName-ElementKeyString": { + steps: path.ExpressionSteps{ + path.ExpressionStepAttributeNameExact("test"), + path.ExpressionStepElementKeyStringExact("test-key"), + }, + expected: `test["test-key"]`, + }, + "AttributeName-ElementKeyString-AttributeName": { + steps: path.ExpressionSteps{ + path.ExpressionStepAttributeNameExact("test1"), + path.ExpressionStepElementKeyStringExact("test-key"), + path.ExpressionStepAttributeNameExact("test2"), + }, + expected: `test1["test-key"].test2`, + }, + "AttributeName-ElementKeyString-ElementKeyString": { + steps: path.ExpressionSteps{ + path.ExpressionStepAttributeNameExact("test"), + path.ExpressionStepElementKeyStringExact("test-key1"), + path.ExpressionStepElementKeyStringExact("test-key2"), + }, + expected: `test["test-key1"]["test-key2"]`, + }, + "AttributeName-ElementKeyValue": { + steps: path.ExpressionSteps{ + path.ExpressionStepAttributeNameExact("test"), + path.ExpressionStepElementKeyValueExact{Value: types.String{Value: "test-value"}}, + }, + expected: `test[Value("test-value")]`, + }, + "AttributeName-ElementKeyValue-AttributeName": { + steps: path.ExpressionSteps{ + path.ExpressionStepAttributeNameExact("test"), + path.ExpressionStepElementKeyValueExact{Value: types.Object{ + Attrs: map[string]attr.Value{ + "test_attr_1": types.Bool{Value: true}, + "test_attr_2": types.String{Value: "test-value"}, + }, + AttrTypes: map[string]attr.Type{ + "test_attr_1": types.BoolType, + "test_attr_2": types.StringType, + }, + }}, + path.ExpressionStepAttributeNameExact("test_attr_1"), + }, + expected: `test[Value({"test_attr_1":true,"test_attr_2":"test-value"})].test_attr_1`, + }, + "ElementKeyInt": { + steps: path.ExpressionSteps{ + path.ExpressionStepElementKeyIntExact(0), + }, + expected: `[0]`, + }, + "ElementKeyString": { + steps: path.ExpressionSteps{ + path.ExpressionStepElementKeyStringExact("test"), + }, + expected: `["test"]`, + }, + "ElementKeyValue": { + steps: path.ExpressionSteps{ + path.ExpressionStepElementKeyValueExact{Value: types.String{Value: "test"}}, + }, + expected: `[Value("test")]`, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.steps.String() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/path/expression_test.go b/path/expression_test.go new file mode 100644 index 000000000..677e16f84 --- /dev/null +++ b/path/expression_test.go @@ -0,0 +1,645 @@ +package path_test + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func TestExpressionAtAnyListIndex(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + expression path.Expression + expected path.Expression + }{ + "shallow": { + expression: path.MatchRoot("test"), + expected: path.MatchRoot("test").AtAnyListIndex(), + }, + "deep": { + expression: path.MatchRoot("test1").AtListIndex(0).AtName("test2"), + expected: path.MatchRoot("test1").AtListIndex(0).AtName("test2").AtAnyListIndex(), + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.expression.AtAnyListIndex() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestExpressionAtAnyMapKey(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + expression path.Expression + expected path.Expression + }{ + "shallow": { + expression: path.MatchRoot("test"), + expected: path.MatchRoot("test").AtAnyMapKey(), + }, + "deep": { + expression: path.MatchRoot("test1").AtListIndex(0).AtName("test2"), + expected: path.MatchRoot("test1").AtListIndex(0).AtName("test2").AtAnyMapKey(), + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.expression.AtAnyMapKey() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestExpressionAtAnySetValue(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + expression path.Expression + expected path.Expression + }{ + "shallow": { + expression: path.MatchRoot("test"), + expected: path.MatchRoot("test").AtAnySetValue(), + }, + "deep": { + expression: path.MatchRoot("test1").AtListIndex(0).AtName("test2"), + expected: path.MatchRoot("test1").AtListIndex(0).AtName("test2").AtAnySetValue(), + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.expression.AtAnySetValue() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestExpressionAtListIndex(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + expression path.Expression + index int + expected path.Expression + }{ + "shallow": { + expression: path.MatchRoot("test"), + index: 1, + expected: path.MatchRoot("test").AtListIndex(1), + }, + "deep": { + expression: path.MatchRoot("test1").AtListIndex(0).AtName("test2"), + index: 1, + expected: path.MatchRoot("test1").AtListIndex(0).AtName("test2").AtListIndex(1), + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.expression.AtListIndex(testCase.index) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestExpressionAtMapKey(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + expression path.Expression + key string + expected path.Expression + }{ + "shallow": { + expression: path.MatchRoot("test"), + key: "test-key", + expected: path.MatchRoot("test").AtMapKey("test-key"), + }, + "deep": { + expression: path.MatchRoot("test1").AtListIndex(0).AtName("test2"), + key: "test-key", + expected: path.MatchRoot("test1").AtListIndex(0).AtName("test2").AtMapKey("test-key"), + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.expression.AtMapKey(testCase.key) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestExpressionAtName(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + expression path.Expression + name string + expected path.Expression + }{ + "shallow": { + expression: path.MatchRoot("test1"), + name: "test2", + expected: path.MatchRoot("test1").AtName("test2"), + }, + "deep": { + expression: path.MatchRoot("test1").AtListIndex(0), + name: "test2", + expected: path.MatchRoot("test1").AtListIndex(0).AtName("test2"), + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.expression.AtName(testCase.name) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestExpressionAtParent(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + expression path.Expression + expected path.Expression + }{ + "shallow": { + expression: path.MatchRoot("test"), + expected: path.MatchRoot("test").AtParent(), + }, + "deep": { + expression: path.MatchRoot("test1").AtListIndex(0).AtName("test2"), + expected: path.MatchRoot("test1").AtListIndex(0).AtName("test2").AtParent(), + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.expression.AtParent() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestExpressionAtSetValue(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + expression path.Expression + value attr.Value + expected path.Expression + }{ + "shallow": { + expression: path.MatchRoot("test"), + value: types.String{Value: "test"}, + expected: path.MatchRoot("test").AtSetValue(types.String{Value: "test"}), + }, + "deep": { + expression: path.MatchRoot("test1").AtListIndex(0).AtName("test2"), + value: types.String{Value: "test"}, + expected: path.MatchRoot("test1").AtListIndex(0).AtName("test2").AtSetValue(types.String{Value: "test"}), + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.expression.AtSetValue(testCase.value) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestExpressionCopy(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + expression path.Expression + expected path.Expression + }{ + "shallow": { + expression: path.MatchRoot("test"), + expected: path.MatchRoot("test"), + }, + "deep": { + expression: path.MatchRoot("test1").AtListIndex(0).AtName("test2"), + expected: path.MatchRoot("test1").AtListIndex(0).AtName("test2"), + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.expression.Copy() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestExpressionEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + expression path.Expression + other path.Expression + expected bool + }{ + "different-length": { + expression: path.MatchRoot("test1").AtName("test2"), + other: path.MatchRoot("test1"), + expected: false, + }, + "different-step-shallow": { + expression: path.MatchRoot("test"), + other: path.MatchRoot("not-test"), + expected: false, + }, + "different-step-deep": { + expression: path.MatchRoot("test1").AtListIndex(0).AtName("test2"), + other: path.MatchRoot("test2").AtListIndex(0).AtName("not-test2"), + expected: false, + }, + "equal-shallow": { + expression: path.MatchRoot("test"), + other: path.MatchRoot("test"), + expected: true, + }, + "equal-deep": { + expression: path.MatchRoot("test1").AtListIndex(0).AtName("test2"), + other: path.MatchRoot("test1").AtListIndex(0).AtName("test2"), + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.expression.Equal(testCase.other) + + if got != testCase.expected { + t.Errorf("expected %t, got %t", testCase.expected, got) + } + }) + } +} + +func TestExpressionMatches(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + expression path.Expression + path path.Path + expected bool + }{ + "empty-empty": { + expression: path.Expression{}, + path: path.Empty(), + expected: true, + }, + "empty-nonempty": { + expression: path.Expression{}, + path: path.Root("test"), + expected: false, + }, + "nonempty-empty": { + expression: path.MatchRoot("test"), + path: path.Empty(), + expected: false, + }, + "AttributeNameExact-different": { + expression: path.MatchRoot("test"), + path: path.Root("not-test"), + expected: false, + }, + "AttributeNameExact-equal": { + expression: path.MatchRoot("test"), + path: path.Root("test"), + expected: true, + }, + "AttributeNameExact-AttributeNameExact-different-firststep": { + expression: path.MatchRoot("test1").AtName("test2"), + path: path.Root("test2").AtName("test2"), + expected: false, + }, + "AttributeNameExact-AttributeNameExact-different-laststep": { + expression: path.MatchRoot("test1").AtName("test2"), + path: path.Root("test1").AtName("test3"), + expected: false, + }, + "AttributeNameExact-AttributeNameExact-equal": { + expression: path.MatchRoot("test1").AtName("test2"), + path: path.Root("test1").AtName("test2"), + expected: true, + }, + "AttributeNameExact-AttributeNameExact-Parent-different": { + expression: path.MatchRoot("test1").AtName("test2").AtParent(), + path: path.Root("test2"), + expected: false, + }, + "AttributeNameExact-AttributeNameExact-Parent-equal": { + expression: path.MatchRoot("test1").AtName("test2").AtParent(), + path: path.Root("test1"), + expected: true, + }, + "AttributeNameExact-AttributeNameExact-Parent-AttributeNameExact-different": { + expression: path.MatchRoot("test1").AtName("test2").AtParent().AtName("test3"), + path: path.Root("test1").AtName("test2"), + expected: false, + }, + "AttributeNameExact-AttributeNameExact-Parent-AttributeNameExact-equal": { + expression: path.MatchRoot("test1").AtName("test2").AtParent().AtName("test3"), + path: path.Root("test1").AtName("test3"), + expected: true, + }, + "AttributeNameExact-ElementKeyIntAny": { + expression: path.MatchRoot("test").AtAnyListIndex(), + path: path.Root("test").AtListIndex(0), + expected: true, + }, + "AttributeNameExact-ElementKeyIntExact-different": { + expression: path.MatchRoot("test").AtListIndex(0), + path: path.Root("test").AtListIndex(1), + expected: false, + }, + "AttributeNameExact-ElementKeyIntExact-equal": { + expression: path.MatchRoot("test").AtListIndex(0), + path: path.Root("test").AtListIndex(0), + expected: true, + }, + "AttributeNameExact-ElementKeyStringAny": { + expression: path.MatchRoot("test").AtAnyMapKey(), + path: path.Root("test").AtMapKey("test-key"), + expected: true, + }, + "AttributeNameExact-ElementKeyStringExact-different": { + expression: path.MatchRoot("test").AtMapKey("test-key"), + path: path.Root("test").AtMapKey("not-test-key"), + expected: false, + }, + "AttributeNameExact-ElementKeyStringExact-equal": { + expression: path.MatchRoot("test").AtMapKey("test-key"), + path: path.Root("test").AtMapKey("test-key"), + expected: true, + }, + "AttributeNameExact-ElementKeyValueAny": { + expression: path.MatchRoot("test").AtAnySetValue(), + path: path.Root("test").AtSetValue(types.String{Value: "test-value"}), + expected: true, + }, + "AttributeNameExact-ElementKeyValueExact-different": { + expression: path.MatchRoot("test").AtSetValue(types.String{Value: "test-value"}), + path: path.Root("test").AtSetValue(types.String{Value: "not-test-value"}), + expected: false, + }, + "AttributeNameExact-ElementKeyValueExact-equal": { + expression: path.MatchRoot("test").AtSetValue(types.String{Value: "test-value"}), + path: path.Root("test").AtSetValue(types.String{Value: "test-value"}), + expected: true, + }, + "AttributeNameExact-Parent-AttributeNameExact-different": { + expression: path.MatchRoot("test1").AtParent().AtName("test2"), + path: path.Root("test1"), + expected: false, + }, + "AttributeNameExact-Parent-AttributeNameExact-equal": { + expression: path.MatchRoot("test1").AtParent().AtName("test2"), + path: path.Root("test2"), + expected: true, + }, + "Parent-AttributeNameExact": { + expression: path.MatchParent().AtName("test"), + path: path.Root("test"), + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.expression.Matches(testCase.path) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestExpressionSteps(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + expression path.Expression + expected path.ExpressionSteps + }{ + "one": { + expression: path.MatchRoot("test"), + expected: path.ExpressionSteps{ + path.ExpressionStepAttributeNameExact("test"), + }, + }, + "two": { + expression: path.MatchRoot("test").AtListIndex(1), + expected: path.ExpressionSteps{ + path.ExpressionStepAttributeNameExact("test"), + path.ExpressionStepElementKeyIntExact(1), + }, + }, + "any": { + expression: path.MatchRoot("test").AtAnyListIndex(), + expected: path.ExpressionSteps{ + path.ExpressionStepAttributeNameExact("test"), + path.ExpressionStepElementKeyIntAny{}, + }, + }, + "parent": { + expression: path.MatchRoot("test").AtParent(), + expected: path.ExpressionSteps{ + path.ExpressionStepAttributeNameExact("test"), + path.ExpressionStepParent{}, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.expression.Steps() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestExpressionString(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + expression path.Expression + expected string + }{ + "AttributeNameExact": { + expression: path.MatchRoot("test"), + expected: `test`, + }, + "AttributeNameExact-AttributeNameExact": { + expression: path.MatchRoot("test1").AtName("test2"), + expected: `test1.test2`, + }, + "AttributeNameExact-AttributeNameExact-AttributeNameExact": { + expression: path.MatchRoot("test1").AtName("test2").AtName("test3"), + expected: `test1.test2.test3`, + }, + "AttributeNameExact-ElementKeyIntAny": { + expression: path.MatchRoot("test").AtAnyListIndex(), + expected: `test[*]`, + }, + "AttributeNameExact-ElementKeyIntExact": { + expression: path.MatchRoot("test").AtListIndex(0), + expected: `test[0]`, + }, + "AttributeNameExact-ElementKeyIntExact-AttributeNameExact": { + expression: path.MatchRoot("test1").AtListIndex(0).AtName("test2"), + expected: `test1[0].test2`, + }, + "AttributeNameExact-ElementKeyIntExact-ElementKeyIntExact": { + expression: path.MatchRoot("test").AtListIndex(0).AtListIndex(1), + expected: `test[0][1]`, + }, + "AttributeNameExact-ElementKeyStringAny": { + expression: path.MatchRoot("test").AtAnyMapKey(), + expected: `test["*"]`, + }, + "AttributeNameExact-ElementKeyStringExact": { + expression: path.MatchRoot("test").AtMapKey("test-key"), + expected: `test["test-key"]`, + }, + "AttributeNameExact-ElementKeyStringExact-AttributeNameExact": { + expression: path.MatchRoot("test1").AtMapKey("test-key").AtName("test2"), + expected: `test1["test-key"].test2`, + }, + "AttributeNameExact-ElementKeyStringExact-ElementKeyStringExact": { + expression: path.MatchRoot("test").AtMapKey("test-key1").AtMapKey("test-key2"), + expected: `test["test-key1"]["test-key2"]`, + }, + "AttributeNameExact-ElementKeyValueAny": { + expression: path.MatchRoot("test").AtAnySetValue(), + expected: `test[Value(*)]`, + }, + "AttributeNameExact-ElementKeyValueExact": { + expression: path.MatchRoot("test").AtSetValue(types.String{Value: "test-value"}), + expected: `test[Value("test-value")]`, + }, + "AttributeNameExact-ElementKeyValue-AttributeNameExact": { + expression: path.MatchRoot("test").AtSetValue(types.Object{ + Attrs: map[string]attr.Value{ + "test_attr_1": types.Bool{Value: true}, + "test_attr_2": types.String{Value: "test-value"}, + }, + AttrTypes: map[string]attr.Type{ + "test_attr_1": types.BoolType, + "test_attr_2": types.StringType, + }, + }).AtName("test_attr_1"), + expected: `test[Value({"test_attr_1":true,"test_attr_2":"test-value"})].test_attr_1`, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.expression.String() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/path/expressions.go b/path/expressions.go new file mode 100644 index 000000000..6aacbb6ec --- /dev/null +++ b/path/expressions.go @@ -0,0 +1,33 @@ +package path + +import "strings" + +// Expressions is a collection of attribute path expressions. +type Expressions []Expression + +// 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. +// +// Empty expressions are skipped. +func (p Expressions) String() string { + var result strings.Builder + + result.WriteString("[") + + for pathIndex, path := range p { + if path.Equal(Expression{}) { + continue + } + + if pathIndex != 0 { + result.WriteString(",") + } + + result.WriteString(path.String()) + } + + result.WriteString("]") + + return result.String() +} diff --git a/path/expressions_test.go b/path/expressions_test.go new file mode 100644 index 000000000..896c2bc76 --- /dev/null +++ b/path/expressions_test.go @@ -0,0 +1,66 @@ +package path_test + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/path" +) + +func TestExpressionsString(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + expressions path.Expressions + expected string + }{ + "nil": { + expressions: nil, + expected: "[]", + }, + "empty": { + expressions: path.Expressions{}, + expected: "[]", + }, + "one": { + expressions: path.Expressions{ + path.MatchRoot("test"), + }, + expected: "[test]", + }, + "one-empty": { + expressions: path.Expressions{ + path.Expression{}, + }, + expected: "[]", + }, + "two": { + expressions: path.Expressions{ + path.MatchRoot("test1"), + path.MatchRoot("test2"), + }, + expected: "[test1,test2]", + }, + "two-empty": { + expressions: path.Expressions{ + path.MatchRoot("test"), + path.Expression{}, + }, + expected: "[test]", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.expressions.String() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/path/path.go b/path/path.go index d648fbd2d..c9dc1b0bd 100644 --- a/path/path.go +++ b/path/path.go @@ -78,6 +78,13 @@ func (p Path) Equal(o Path) bool { return true } +// Expression returns an Expression which exactly matches the Path. +func (p Path) Expression() Expression { + return Expression{ + steps: p.steps.ExpressionSteps(), + } +} + // ParentPath returns a copy of the path with the last step removed. // // If the current path is empty, an empty path is returned. diff --git a/path/path_step.go b/path/path_step.go index 27a40e558..77d115615 100644 --- a/path/path_step.go +++ b/path/path_step.go @@ -7,6 +7,10 @@ type PathStep interface { // Equal should return true if the given PathStep is exactly equivalent. Equal(PathStep) bool + // ExpressionStep should return an ExpressionStep which exactly + // matches the PathStep. + ExpressionStep() ExpressionStep + // String should return a human-readable representation of the step // intended for logging and error messages. There should not be usage // that needs to be protected by compatibility guarantees. diff --git a/path/path_step_attribute_name.go b/path/path_step_attribute_name.go index dd4a16cb6..110ed0808 100644 --- a/path/path_step_attribute_name.go +++ b/path/path_step_attribute_name.go @@ -23,6 +23,11 @@ func (s PathStepAttributeName) Equal(o PathStep) bool { return string(s) == string(other) } +// ExpressionStep returns the ExpressionStep for the PathStep. +func (s PathStepAttributeName) ExpressionStep() ExpressionStep { + return ExpressionStepAttributeNameExact(s) +} + // String returns the human-readable representation of the attribute name. // It is intended for logging and error messages and is not protected by // compatibility guarantees. diff --git a/path/path_step_attribute_name_test.go b/path/path_step_attribute_name_test.go index 7498140a3..15ce073ff 100644 --- a/path/path_step_attribute_name_test.go +++ b/path/path_step_attribute_name_test.go @@ -58,6 +58,34 @@ func TestPathStepAttributeNameEqual(t *testing.T) { } } +func TestPathStepAttributeNameExpressionStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + step path.PathStepAttributeName + expected path.ExpressionStep + }{ + "basic": { + step: path.PathStepAttributeName("test"), + expected: path.ExpressionStepAttributeNameExact("test"), + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.step.ExpressionStep() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestPathStepAttributeNameString(t *testing.T) { t.Parallel() diff --git a/path/path_step_element_key_int.go b/path/path_step_element_key_int.go index c4ae32fb2..3d5d884a0 100644 --- a/path/path_step_element_key_int.go +++ b/path/path_step_element_key_int.go @@ -25,6 +25,11 @@ func (s PathStepElementKeyInt) Equal(o PathStep) bool { return int64(s) == int64(other) } +// ExpressionStep returns the ExpressionStep for the PathStep. +func (s PathStepElementKeyInt) ExpressionStep() ExpressionStep { + return ExpressionStepElementKeyIntExact(s) +} + // String returns the human-readable representation of the element key. // It is intended for logging and error messages and is not protected by // compatibility guarantees. diff --git a/path/path_step_element_key_int_test.go b/path/path_step_element_key_int_test.go index 87d8aaf08..f03a74db2 100644 --- a/path/path_step_element_key_int_test.go +++ b/path/path_step_element_key_int_test.go @@ -58,6 +58,34 @@ func TestPathStepElementKeyIntEqual(t *testing.T) { } } +func TestPathStepElementKeyIntExpressionStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + step path.PathStepElementKeyInt + expected path.ExpressionStep + }{ + "basic": { + step: path.PathStepElementKeyInt(1), + expected: path.ExpressionStepElementKeyIntExact(1), + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.step.ExpressionStep() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestPathStepElementKeyIntString(t *testing.T) { t.Parallel() diff --git a/path/path_step_element_key_string.go b/path/path_step_element_key_string.go index 083a1bca1..b3ec8f135 100644 --- a/path/path_step_element_key_string.go +++ b/path/path_step_element_key_string.go @@ -25,6 +25,11 @@ func (s PathStepElementKeyString) Equal(o PathStep) bool { return string(s) == string(other) } +// ExpressionStep returns the ExpressionStep for the PathStep. +func (s PathStepElementKeyString) ExpressionStep() ExpressionStep { + return ExpressionStepElementKeyStringExact(s) +} + // String returns the human-readable representation of the element key. // It is intended for logging and error messages and is not protected by // compatibility guarantees. diff --git a/path/path_step_element_key_string_test.go b/path/path_step_element_key_string_test.go index fe425b6c9..a98f4de1c 100644 --- a/path/path_step_element_key_string_test.go +++ b/path/path_step_element_key_string_test.go @@ -58,6 +58,34 @@ func TestPathStepElementKeyStringEqual(t *testing.T) { } } +func TestPathStepElementKeyStringExpressionStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + step path.PathStepElementKeyString + expected path.ExpressionStep + }{ + "basic": { + step: path.PathStepElementKeyString("test"), + expected: path.ExpressionStepElementKeyStringExact("test"), + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.step.ExpressionStep() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestPathStepElementKeyStringString(t *testing.T) { t.Parallel() diff --git a/path/path_step_element_key_value.go b/path/path_step_element_key_value.go index 9d3fc3815..535fddbd2 100644 --- a/path/path_step_element_key_value.go +++ b/path/path_step_element_key_value.go @@ -32,6 +32,11 @@ func (s PathStepElementKeyValue) Equal(o PathStep) bool { return s.Value.Equal(other.Value) } +// ExpressionStep returns the ExpressionStep for the PathStep. +func (s PathStepElementKeyValue) ExpressionStep() ExpressionStep { + return ExpressionStepElementKeyValueExact(s) +} + // String returns the human-readable representation of the element key. // It is intended for logging and error messages and is not protected by // compatibility guarantees. diff --git a/path/path_step_element_key_value_test.go b/path/path_step_element_key_value_test.go index 035b44734..6be90b128 100644 --- a/path/path_step_element_key_value_test.go +++ b/path/path_step_element_key_value_test.go @@ -64,6 +64,34 @@ func TestPathStepElementKeyValueEqual(t *testing.T) { } } +func TestPathStepElementKeyValueExpressionStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + step path.PathStepElementKeyValue + expected path.ExpressionStep + }{ + "basic": { + step: path.PathStepElementKeyValue{Value: types.String{Value: "test"}}, + expected: path.ExpressionStepElementKeyValueExact{Value: types.String{Value: "test"}}, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.step.ExpressionStep() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestPathStepElementKeyValueString(t *testing.T) { t.Parallel() diff --git a/path/path_steps.go b/path/path_steps.go index 1f6a6efe5..43edf30d2 100644 --- a/path/path_steps.go +++ b/path/path_steps.go @@ -84,3 +84,15 @@ func (s PathSteps) String() string { return result.String() } + +// ExpressionSteps returns the ordered collection of expression steps which +// exactly matches the PathSteps. +func (s PathSteps) ExpressionSteps() ExpressionSteps { + result := make(ExpressionSteps, len(s)) + + for stepIndex, pathStep := range s { + result[stepIndex] = pathStep.ExpressionStep() + } + + return result +} diff --git a/path/path_steps_test.go b/path/path_steps_test.go index 8052bde58..dbff0f134 100644 --- a/path/path_steps_test.go +++ b/path/path_steps_test.go @@ -311,6 +311,56 @@ func TestPathStepsEqual(t *testing.T) { } } +func TestPathStepsExpressionSteps(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + steps path.PathSteps + expected path.ExpressionSteps + }{ + "nil": { + steps: nil, + expected: path.ExpressionSteps{}, + }, + "empty": { + steps: path.PathSteps{}, + expected: path.ExpressionSteps{}, + }, + "one": { + steps: path.PathSteps{ + path.PathStepAttributeName("test"), + }, + expected: path.ExpressionSteps{ + path.ExpressionStepAttributeNameExact("test"), + }, + }, + "two": { + steps: path.PathSteps{ + path.PathStepAttributeName("test"), + path.PathStepElementKeyInt(1), + }, + expected: path.ExpressionSteps{ + path.ExpressionStepAttributeNameExact("test"), + path.ExpressionStepElementKeyIntExact(1), + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.steps.ExpressionSteps() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestPathStepsLastStep(t *testing.T) { t.Parallel() diff --git a/path/path_test.go b/path/path_test.go index 334249290..711fb23d0 100644 --- a/path/path_test.go +++ b/path/path_test.go @@ -260,6 +260,38 @@ func TestPathEqual(t *testing.T) { } } +func TestPathExpression(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + path path.Path + expected path.Expression + }{ + "one": { + path: path.Root("test"), + expected: path.MatchRoot("test"), + }, + "two": { + path: path.Root("test").AtListIndex(1), + expected: path.MatchRoot("test").AtListIndex(1), + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.path.Expression() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestPathParentPath(t *testing.T) { t.Parallel() diff --git a/tfsdk/attribute_plan_modification.go b/tfsdk/attribute_plan_modification.go index 33173de6a..d4f0fd8ec 100644 --- a/tfsdk/attribute_plan_modification.go +++ b/tfsdk/attribute_plan_modification.go @@ -389,9 +389,14 @@ func (r UseStateForUnknownModifier) MarkdownDescription(ctx context.Context) str // instance of this request struct is supplied as an argument to the Modify // function of an attribute's plan modifier(s). type ModifyAttributePlanRequest struct { - // AttributePath is the path of the attribute. + // AttributePath is the path of the attribute. Use this path for any + // response diagnostics. AttributePath path.Path + // AttributePathExpression is the expression matching the exact path of the + // attribute. + AttributePathExpression path.Expression + // Config is the configuration the user supplied for the resource. Config Config diff --git a/tfsdk/attribute_validation.go b/tfsdk/attribute_validation.go index 3d73a71a1..c9a6a6167 100644 --- a/tfsdk/attribute_validation.go +++ b/tfsdk/attribute_validation.go @@ -26,11 +26,16 @@ type AttributeValidator interface { Validate(context.Context, ValidateAttributeRequest, *ValidateAttributeResponse) } -// ValidateAttributeRequest repesents a request for +// ValidateAttributeRequest repesents a request for attribute validation. type ValidateAttributeRequest struct { - // AttributePath contains the path of the attribute. + // AttributePath contains the path of the attribute. Use this path for any + // response diagnostics. AttributePath path.Path + // AttributePathExpression contains the expression matching the exact path + // of the attribute. + AttributePathExpression path.Expression + // AttributeConfig contains the value of the attribute in the configuration. AttributeConfig attr.Value From 86dd1a6a7a20cb424659561e08204ae9400be083 Mon Sep 17 00:00:00 2001 From: Brian Flad Date: Tue, 28 Jun 2022 10:39:43 -0400 Subject: [PATCH 2/4] Update CHANGELOG for #396 --- .changelog/{pending.txt => 396.txt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .changelog/{pending.txt => 396.txt} (100%) diff --git a/.changelog/pending.txt b/.changelog/396.txt similarity index 100% rename from .changelog/pending.txt rename to .changelog/396.txt From 8aec9135fccfe6f622c88befe20a10cb1fd60fca Mon Sep 17 00:00:00 2001 From: Brian Flad Date: Wed, 29 Jun 2022 13:07:13 -0400 Subject: [PATCH 3/4] Updates to path expression handling --- .changelog/396.txt | 8 + internal/fromtftypes/attribute_path_step.go | 35 + .../fromtftypes/attribute_path_step_test.go | 83 +++ internal/fromtftypes/doc.go | 3 + internal/fromtftypes/value.go | 24 + internal/fromtftypes/value_test.go | 313 +++++++++ path/expression.go | 61 +- path/expression_test.go | 89 ++- tfsdk/config.go | 5 + tfsdk/config_test.go | 81 +++ tfsdk/path_matches.go | 40 ++ tfsdk/path_matches_test.go | 606 ++++++++++++++++++ tfsdk/plan.go | 5 + tfsdk/plan_test.go | 81 +++ tfsdk/state.go | 5 + tfsdk/state_test.go | 81 +++ tfsdk/tftypes_attribute_path.go | 95 +++ tfsdk/tftypes_attribute_path_test.go | 198 ++++++ 18 files changed, 1803 insertions(+), 10 deletions(-) create mode 100644 internal/fromtftypes/attribute_path_step.go create mode 100644 internal/fromtftypes/attribute_path_step_test.go create mode 100644 internal/fromtftypes/doc.go create mode 100644 internal/fromtftypes/value.go create mode 100644 internal/fromtftypes/value_test.go create mode 100644 tfsdk/path_matches.go create mode 100644 tfsdk/path_matches_test.go create mode 100644 tfsdk/tftypes_attribute_path.go create mode 100644 tfsdk/tftypes_attribute_path_test.go diff --git a/.changelog/396.txt b/.changelog/396.txt index 515542ed1..a76abcac2 100644 --- a/.changelog/396.txt +++ b/.changelog/396.txt @@ -1,3 +1,11 @@ +```release-note:feature +path: Introduced attribute path expressions +``` + ```release-note:enhancement tfsdk: Added `AttributePathExpression` field to `ModifyAttributePlanRequest` and `ValidateAttributeRequest` types ``` + +```release-note:enhancement +tfsdk: Added `PathMatches` method to `Config`, `Plan`, and `State` types +``` diff --git a/internal/fromtftypes/attribute_path_step.go b/internal/fromtftypes/attribute_path_step.go new file mode 100644 index 000000000..aef594bd4 --- /dev/null +++ b/internal/fromtftypes/attribute_path_step.go @@ -0,0 +1,35 @@ +package fromtftypes + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +// AttributePathStep returns the path.PathStep equivalent of a +// tftypes.AttributePathStep. An error is returned instead of diag.Diagnostics +// so callers can include appropriate logical context about when the error +// occurred. +func AttributePathStep(ctx context.Context, tfType tftypes.AttributePathStep, attrType attr.Type) (path.PathStep, error) { + switch tfType := tfType.(type) { + case tftypes.AttributeName: + return path.PathStepAttributeName(string(tfType)), nil + case tftypes.ElementKeyInt: + return path.PathStepElementKeyInt(int64(tfType)), nil + case tftypes.ElementKeyString: + return path.PathStepElementKeyString(string(tfType)), nil + case tftypes.ElementKeyValue: + attrValue, err := Value(ctx, tftypes.Value(tfType), attrType) + + if err != nil { + return nil, fmt.Errorf("unable to create PathStepElementKeyValue from tftypes.Value: %w", err) + } + + return path.PathStepElementKeyValue{Value: attrValue}, nil + default: + return nil, fmt.Errorf("unknown tftypes.AttributePathStep: %#v", tfType) + } +} diff --git a/internal/fromtftypes/attribute_path_step_test.go b/internal/fromtftypes/attribute_path_step_test.go new file mode 100644 index 000000000..39b5494be --- /dev/null +++ b/internal/fromtftypes/attribute_path_step_test.go @@ -0,0 +1,83 @@ +package fromtftypes_test + +import ( + "context" + "fmt" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/internal/fromtftypes" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestAttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + tfType tftypes.AttributePathStep + attrType attr.Type + expected path.PathStep + expectedError error + }{ + "nil": { + tfType: nil, + expected: nil, + expectedError: fmt.Errorf("unknown tftypes.AttributePathStep: "), + }, + "PathStepAttributeName": { + tfType: tftypes.AttributeName("test"), + expected: path.PathStepAttributeName("test"), + }, + "PathStepElementKeyInt": { + tfType: tftypes.ElementKeyInt(1), + expected: path.PathStepElementKeyInt(1), + }, + "PathStepElementKeyString": { + tfType: tftypes.ElementKeyString("test"), + expected: path.PathStepElementKeyString("test"), + }, + "PathStepElementKeyValue": { + tfType: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + attrType: types.StringType, + expected: path.PathStepElementKeyValue{Value: types.String{Value: "test"}}, + }, + "PathStepElementKeyValue-error": { + tfType: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + attrType: types.BoolType, + expected: nil, + expectedError: fmt.Errorf("unable to create PathStepElementKeyValue from tftypes.Value: unable to convert tftypes.Value (tftypes.String<\"test\">) to attr.Value: can't unmarshal tftypes.String into *bool, expected boolean"), + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := fromtftypes.AttributePathStep(context.Background(), testCase.tfType, testCase.attrType) + + if err != nil { + if testCase.expectedError == nil { + t.Fatalf("expected no error, got: %s", err) + } + + if !strings.Contains(err.Error(), testCase.expectedError.Error()) { + t.Fatalf("expected error %q, got: %s", testCase.expectedError, err) + } + } + + if err == nil && testCase.expectedError != nil { + t.Fatalf("got no error, tfType: %s", testCase.expectedError) + } + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/internal/fromtftypes/doc.go b/internal/fromtftypes/doc.go new file mode 100644 index 000000000..c97dd1149 --- /dev/null +++ b/internal/fromtftypes/doc.go @@ -0,0 +1,3 @@ +// Package fromtftypes contains functions to convert from terraform-plugin-go +// tftypes types to framework types. +package fromtftypes diff --git a/internal/fromtftypes/value.go b/internal/fromtftypes/value.go new file mode 100644 index 000000000..4fc20875d --- /dev/null +++ b/internal/fromtftypes/value.go @@ -0,0 +1,24 @@ +package fromtftypes + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +// Value returns the attr.Value equivalent to the tftypes.Value. +func Value(ctx context.Context, tfType tftypes.Value, attrType attr.Type) (attr.Value, error) { + if attrType == nil { + return nil, fmt.Errorf("unable to convert tftypes.Value (%s) to attr.Value: missing attr.Type", tfType.String()) + } + + attrValue, err := attrType.ValueFromTerraform(ctx, tfType) + + if err != nil { + return nil, fmt.Errorf("unable to convert tftypes.Value (%s) to attr.Value: %w", tfType.String(), err) + } + + return attrValue, nil +} diff --git a/internal/fromtftypes/value_test.go b/internal/fromtftypes/value_test.go new file mode 100644 index 000000000..2ea227f76 --- /dev/null +++ b/internal/fromtftypes/value_test.go @@ -0,0 +1,313 @@ +package fromtftypes_test + +import ( + "context" + "fmt" + "math/big" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/internal/fromtftypes" + testtypes "github.com/hashicorp/terraform-plugin-framework/internal/testing/types" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestValue(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + tfType tftypes.Value + attrType attr.Type + expected attr.Value + expectedError error + }{ + "empty-tftype": { + tfType: tftypes.Value{}, + attrType: types.BoolType, + expected: types.Bool{Null: true}, + }, + "nil-attr-type": { + tfType: tftypes.Value{}, + attrType: nil, + expected: nil, + expectedError: fmt.Errorf("unable to convert tftypes.Value (invalid typeless tftypes.Value<>) to attr.Value: missing attr.Type"), + }, + "invalid-attr-type": { + tfType: tftypes.NewValue(tftypes.Bool, true), + attrType: testtypes.InvalidType{}, + expected: nil, + expectedError: fmt.Errorf("unable to convert tftypes.Value (tftypes.Bool<\"true\">) to attr.Value: intentional ValueFromTerraform error"), + }, + "bool-null": { + tfType: tftypes.NewValue(tftypes.Bool, nil), + attrType: types.BoolType, + expected: types.Bool{Null: true}, + }, + "bool-unknown": { + tfType: tftypes.NewValue(tftypes.Bool, tftypes.UnknownValue), + attrType: types.BoolType, + expected: types.Bool{Unknown: true}, + }, + "bool-value": { + tfType: tftypes.NewValue(tftypes.Bool, true), + attrType: types.BoolType, + expected: types.Bool{Value: true}, + }, + "float64-null": { + tfType: tftypes.NewValue(tftypes.Number, nil), + attrType: types.Float64Type, + expected: types.Float64{Null: true}, + }, + "float64-unknown": { + tfType: tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), + attrType: types.Float64Type, + expected: types.Float64{Unknown: true}, + }, + "float64-value": { + tfType: tftypes.NewValue(tftypes.Number, big.NewFloat(1.2)), + attrType: types.Float64Type, + expected: types.Float64{Value: 1.2}, + }, + "int64-null": { + tfType: tftypes.NewValue(tftypes.Number, nil), + attrType: types.Int64Type, + expected: types.Int64{Null: true}, + }, + "int64-unknown": { + tfType: tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), + attrType: types.Int64Type, + expected: types.Int64{Unknown: true}, + }, + "int64-value": { + tfType: tftypes.NewValue(tftypes.Number, 123), + attrType: types.Int64Type, + expected: types.Int64{Value: 123}, + }, + "list-null": { + tfType: tftypes.NewValue( + tftypes.List{ + ElementType: tftypes.String, + }, + nil, + ), + attrType: types.ListType{ + ElemType: types.StringType, + }, + expected: types.List{ + ElemType: types.StringType, + Null: true, + }, + }, + "list-unknown": { + tfType: tftypes.NewValue( + tftypes.List{ + ElementType: tftypes.String, + }, + tftypes.UnknownValue, + ), + attrType: types.ListType{ + ElemType: types.StringType, + }, + expected: types.List{ + ElemType: types.StringType, + Unknown: true, + }, + }, + "list-value": { + tfType: tftypes.NewValue( + tftypes.List{ + ElementType: tftypes.String, + }, + []tftypes.Value{ + tftypes.NewValue(tftypes.String, "test-value"), + }, + ), + attrType: types.ListType{ + ElemType: types.StringType, + }, + expected: types.List{ + ElemType: types.StringType, + Elems: []attr.Value{ + types.String{Value: "test-value"}, + }, + }, + }, + "number-null": { + tfType: tftypes.NewValue(tftypes.Number, nil), + attrType: types.NumberType, + expected: types.Number{Null: true}, + }, + "number-unknown": { + tfType: tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), + attrType: types.NumberType, + expected: types.Number{Unknown: true}, + }, + "number-value": { + tfType: tftypes.NewValue(tftypes.Number, big.NewFloat(1.2)), + attrType: types.NumberType, + expected: types.Number{Value: big.NewFloat(1.2)}, + }, + "object-null": { + tfType: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_attr": tftypes.String, + }, + }, + nil, + ), + attrType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "test_attr": types.StringType, + }, + }, + expected: types.Object{ + AttrTypes: map[string]attr.Type{ + "test_attr": types.StringType, + }, + Null: true, + }, + }, + "object-unknown": { + tfType: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_attr": tftypes.String, + }, + }, + tftypes.UnknownValue, + ), + attrType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "test_attr": types.StringType, + }, + }, + expected: types.Object{ + AttrTypes: map[string]attr.Type{ + "test_attr": types.StringType, + }, + Unknown: true, + }, + }, + "object-value": { + tfType: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_attr": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "test_attr": tftypes.NewValue(tftypes.String, "test-value"), + }, + ), + attrType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "test_attr": types.StringType, + }, + }, + expected: types.Object{ + AttrTypes: map[string]attr.Type{ + "test_attr": types.StringType, + }, + Attrs: map[string]attr.Value{ + "test_attr": types.String{Value: "test-value"}, + }, + }, + }, + "set-null": { + tfType: tftypes.NewValue( + tftypes.Set{ + ElementType: tftypes.String, + }, + nil, + ), + attrType: types.SetType{ + ElemType: types.StringType, + }, + expected: types.Set{ + ElemType: types.StringType, + Null: true, + }, + }, + "set-unknown": { + tfType: tftypes.NewValue( + tftypes.Set{ + ElementType: tftypes.String, + }, + tftypes.UnknownValue, + ), + attrType: types.SetType{ + ElemType: types.StringType, + }, + expected: types.Set{ + ElemType: types.StringType, + Unknown: true, + }, + }, + "set-value": { + tfType: tftypes.NewValue( + tftypes.Set{ + ElementType: tftypes.String, + }, + []tftypes.Value{ + tftypes.NewValue(tftypes.String, "test-value"), + }, + ), + attrType: types.SetType{ + ElemType: types.StringType, + }, + expected: types.Set{ + ElemType: types.StringType, + Elems: []attr.Value{ + types.String{Value: "test-value"}, + }, + }, + }, + "string-null": { + tfType: tftypes.NewValue(tftypes.String, nil), + attrType: types.StringType, + expected: types.String{Null: true}, + }, + "string-unknown": { + tfType: tftypes.NewValue(tftypes.String, tftypes.UnknownValue), + attrType: types.StringType, + expected: types.String{Unknown: true}, + }, + "string-value": { + tfType: tftypes.NewValue(tftypes.String, "test-value"), + attrType: types.StringType, + expected: types.String{Value: "test-value"}, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := fromtftypes.Value(context.Background(), testCase.tfType, testCase.attrType) + + if err != nil { + if testCase.expectedError == nil { + t.Fatalf("expected no error, got: %s", err) + } + + if !strings.Contains(err.Error(), testCase.expectedError.Error()) { + t.Fatalf("expected error %q, got: %s", testCase.expectedError, err) + } + } + + if err == nil && testCase.expectedError != nil { + t.Fatalf("got no error, tfType: %s", testCase.expectedError) + } + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/path/expression.go b/path/expression.go index 2dc3e4f46..b5af162ea 100644 --- a/path/expression.go +++ b/path/expression.go @@ -7,6 +7,11 @@ import ( // Expression represents an attribute path with expression steps, which can // represent zero, one, or more actual Paths. type Expression struct { + // root stores whether an expression was intentionally created to start + // from the root of the data. This is used with Merge to overwrite steps + // instead of appending steps. + root bool + // steps is the transversals included with the expression. In general, // operations against the path should protect against modification of the // original. @@ -98,7 +103,7 @@ func (e Expression) AtSetValue(value attr.Value) Expression { // affecting the original. func (e Expression) Copy() Expression { return Expression{ - steps: e.Steps(), + steps: e.Steps().Copy(), } } @@ -119,11 +124,49 @@ func (e Expression) Equal(o Expression) bool { return true } -// Matches returns true if the given Path is valid for the Expression. +// Matches returns true if the given Path is valid for the Expression. Any +// relative expression steps, such as ExpressionStepParent, are automatically +// resolved before matching. func (e Expression) Matches(path Path) bool { return e.steps.Matches(path.Steps()) } +// Merge returns a copied expression either with the steps of the given +// expression added to the end of the existing steps, or overwriting the +// steps if the given expression was a root expression. +// +// Any merged expressions will preserve all expressions steps, such as +// ExpressionStepParent, for troubleshooting. Methods such as Matches() will +// automatically resolve the expression when using it. Call the Resolve() +// method explicitly if a resolved expression without any ExpressionStepParent +// is desired. +func (e Expression) Merge(other Expression) Expression { + if other.root { + return other.Copy() + } + + copiedExpression := e.Copy() + + copiedExpression.steps.Append(other.steps...) + + return copiedExpression +} + +// Resolve returns a copied expression with any relative steps, such as +// ExpressionStepParent, resolved. This is not necessary before calling methods +// such as Matches(), however it can be useful before returning the String() +// method so the path information is simplified. +// +// Returns an empty expression if any ExpressionStepParent attempt to go +// beyond the first element. +func (e Expression) Resolve() Expression { + copiedExpression := e.Copy() + + copiedExpression.steps = copiedExpression.steps.Resolve() + + return copiedExpression +} + // Steps returns a copy of the underlying expression steps. Returns an empty // collection of steps if expression is nil. func (e Expression) Steps() ExpressionSteps { @@ -141,14 +184,13 @@ func (e Expression) String() string { return e.steps.String() } -// MatchParent creates an attribute path expression starting with -// ExpressionStepParent. This allows creating a relative expression in -// nested schemas. -func MatchParent() Expression { +// MatchRelative creates an empty attribute path expression that is intended +// to be combined with an existing attribute path expression. This allows +// creating a relative expression in nested schemas, using AtParent() to +// traverse up the path or other At methods to traverse further down. +func MatchRelative() Expression { return Expression{ - steps: ExpressionSteps{ - ExpressionStepParent{}, - }, + steps: ExpressionSteps{}, } } @@ -156,6 +198,7 @@ func MatchParent() Expression { // ExpressionStepAttributeNameExact. func MatchRoot(rootAttributeName string) Expression { return Expression{ + root: true, steps: ExpressionSteps{ ExpressionStepAttributeNameExact(rootAttributeName), }, diff --git a/path/expression_test.go b/path/expression_test.go index 677e16f84..9f1602e64 100644 --- a/path/expression_test.go +++ b/path/expression_test.go @@ -483,7 +483,7 @@ func TestExpressionMatches(t *testing.T) { expected: true, }, "Parent-AttributeNameExact": { - expression: path.MatchParent().AtName("test"), + expression: path.MatchRelative().AtParent().AtName("test"), path: path.Root("test"), expected: false, }, @@ -504,6 +504,93 @@ func TestExpressionMatches(t *testing.T) { } } +func TestExpressionMerge(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + expression path.Expression + other path.Expression + expected path.Expression + }{ + "Relative-further": { + expression: path.MatchRoot("test1"), + other: path.MatchRelative().AtName("test2"), + expected: path.MatchRoot("test1").AtName("test2"), + }, + "Relative-Parent-root-level": { + expression: path.MatchRoot("test1"), + other: path.MatchRelative().AtParent().AtName("test2"), + expected: path.MatchRoot("test1").AtParent().AtName("test2"), + }, + "Relative-Parent-nested-level": { + expression: path.MatchRoot("test_parent").AtListIndex(1).AtName("test_child1"), + other: path.MatchRelative().AtParent().AtName("test_child2"), + expected: path.MatchRoot("test_parent").AtListIndex(1).AtName("test_child1").AtParent().AtName("test_child2"), + }, + "Root": { + expression: path.MatchRoot("test1"), + other: path.MatchRoot("test2"), + expected: path.MatchRoot("test2"), + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.expression.Merge(testCase.other) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected result difference: %s", diff) + } + }) + } +} + +func TestExpressionResolve(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + expression path.Expression + expected path.Expression + }{ + // Refer to TestExpressionStepsResolve for more exhaustive unit + // testing of the underlying step resolving functionality. + "AttributeNameExact": { + expression: path.MatchRoot("test1"), + expected: path.MatchRoot("test1"), + }, + "AttributeNameExact-AttributeNameExact": { + expression: path.MatchRoot("test1").AtName("test2"), + expected: path.MatchRoot("test1").AtName("test2"), + }, + "AttributeNameExact-Parent-AttributeNameExact": { + expression: path.MatchRoot("test1").AtParent().AtName("test2"), + expected: path.MatchRoot("test2"), + }, + "AttributeNameExact-ElementKeyIntExact-AttributeNameExact-Parent-AttributeNameExact": { + expression: path.MatchRoot("test_parent").AtListIndex(1).AtName("test_child1").AtParent().AtName("test_child2"), + expected: path.MatchRoot("test_parent").AtListIndex(1).AtName("test_child2"), + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.expression.Resolve() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestExpressionSteps(t *testing.T) { t.Parallel() diff --git a/tfsdk/config.go b/tfsdk/config.go index 354e72a61..76127e37e 100644 --- a/tfsdk/config.go +++ b/tfsdk/config.go @@ -59,6 +59,11 @@ func (c Config) GetAttribute(ctx context.Context, path path.Path, target interfa return diags } +// PathMatches returns all matching path.Paths from the given path.Expression. +func (c Config) PathMatches(ctx context.Context, pathExpr path.Expression) (path.Paths, diag.Diagnostics) { + return pathMatches(ctx, c.Schema, c.Raw, pathExpr) +} + // getAttributeValue retrieves the attribute found at `path` and returns it as an // attr.Value. Consumers should assert the type of the returned value with the // desired attr.Type. diff --git a/tfsdk/config_test.go b/tfsdk/config_test.go index 330568e97..1be8e48c2 100644 --- a/tfsdk/config_test.go +++ b/tfsdk/config_test.go @@ -1735,3 +1735,84 @@ func TestConfigGetAttributeValue(t *testing.T) { }) } } + +func TestConfigPathMatches(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + config Config + expression path.Expression + expected path.Paths + expectedDiags diag.Diagnostics + }{ + // Refer to TestPathMatches for more exhaustive unit testing. + // These test cases are to ensure Config schema and data values are + // passed appropriately to the shared implementation. + "AttributeNameExact-match": { + config: Config{ + Schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Type: types.StringType, + }, + }, + }, + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.String, "test-value"), + }, + ), + }, + expression: path.MatchRoot("test"), + expected: path.Paths{ + path.Root("test"), + }, + }, + "AttributeNameExact-mismatch": { + config: Config{ + Schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Type: types.StringType, + }, + }, + }, + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.String, "test-value"), + }, + ), + }, + expression: path.MatchRoot("not-test"), + expected: nil, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, diags := testCase.config.PathMatches(context.Background(), testCase.expression) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + + if diff := cmp.Diff(diags, testCase.expectedDiags); diff != "" { + t.Errorf("unexpected diagnostics difference: %s", diff) + } + }) + } +} diff --git a/tfsdk/path_matches.go b/tfsdk/path_matches.go new file mode 100644 index 000000000..1aead5c73 --- /dev/null +++ b/tfsdk/path_matches.go @@ -0,0 +1,40 @@ +package tfsdk + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +// pathMatches returns all matching path.Paths from the given path.Expression. +// +// TODO: This function should be part of a internal/schemadata package +// except that doing so would currently introduce an import cycle due to the +// Schema parameter here and Config/Plan/State.PathMatches needing to +// call this function until the schema data is migrated to attr.Value. +// Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/172 +// Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/365 +func pathMatches(ctx context.Context, schema Schema, tfTypeValue tftypes.Value, pathExpr path.Expression) (path.Paths, diag.Diagnostics) { + var diags diag.Diagnostics + var paths path.Paths + + _ = tftypes.Walk(tfTypeValue, func(tfTypePath *tftypes.AttributePath, tfTypeValue tftypes.Value) (bool, error) { + fwPath, fwPathDiags := attributePath(ctx, tfTypePath, schema) + + diags.Append(fwPathDiags...) + + if diags.HasError() { + return false, nil + } + + if pathExpr.Matches(fwPath) { + paths = append(paths, fwPath) + } + + return true, nil + }) + + return paths, diags +} diff --git a/tfsdk/path_matches_test.go b/tfsdk/path_matches_test.go new file mode 100644 index 000000000..59160dc7a --- /dev/null +++ b/tfsdk/path_matches_test.go @@ -0,0 +1,606 @@ +package tfsdk + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestPathMatches(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + schema Schema + tfTypeValue tftypes.Value + expression path.Expression + expected path.Paths + expectedDiags diag.Diagnostics + }{ + "ohno": { + schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Type: types.StringType, + }, + }, + }, + tfTypeValue: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.String, "test-value"), + }, + ), + expression: path.MatchRoot("test").AtParent().AtParent(), + expected: nil, + }, + "AttributeNameExact-match": { + schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Type: types.StringType, + }, + }, + }, + tfTypeValue: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.String, "test-value"), + }, + ), + expression: path.MatchRoot("test"), + expected: path.Paths{ + path.Root("test"), + }, + }, + "AttributeNameExact-mismatch": { + schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Type: types.StringType, + }, + }, + }, + tfTypeValue: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.String, "test-value"), + }, + ), + expression: path.MatchRoot("not-test"), + expected: nil, + }, + "AttributeNameExact-ElementKeyIntAny-match": { + schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Type: types.ListType{ + ElemType: types.StringType, + }, + }, + }, + }, + tfTypeValue: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.List{ + ElementType: tftypes.String, + }, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.List{ + ElementType: tftypes.String, + }, + []tftypes.Value{ + tftypes.NewValue(tftypes.String, "test-value1"), + tftypes.NewValue(tftypes.String, "test-value2"), + tftypes.NewValue(tftypes.String, "test-value3"), + }, + ), + }, + ), + expression: path.MatchRoot("test").AtAnyListIndex(), + expected: path.Paths{ + path.Root("test").AtListIndex(0), + path.Root("test").AtListIndex(1), + path.Root("test").AtListIndex(2), + }, + }, + "AttributeNameExact-ElementKeyIntAny-mismatch": { + schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Type: types.SetType{ + ElemType: types.StringType, + }, + }, + }, + }, + tfTypeValue: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Set{ + ElementType: tftypes.String, + }, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.Set{ + ElementType: tftypes.String, + }, + []tftypes.Value{ + tftypes.NewValue(tftypes.String, "test-value1"), + tftypes.NewValue(tftypes.String, "test-value2"), + tftypes.NewValue(tftypes.String, "test-value3"), + }, + ), + }, + ), + expression: path.MatchRoot("test").AtAnyListIndex(), + expected: nil, + }, + "AttributeNameExact-ElementKeyIntExact-match": { + schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Type: types.ListType{ + ElemType: types.StringType, + }, + }, + }, + }, + tfTypeValue: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.List{ + ElementType: tftypes.String, + }, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.List{ + ElementType: tftypes.String, + }, + []tftypes.Value{ + tftypes.NewValue(tftypes.String, "test-value1"), + tftypes.NewValue(tftypes.String, "test-value2"), + tftypes.NewValue(tftypes.String, "test-value3"), + }, + ), + }, + ), + expression: path.MatchRoot("test").AtListIndex(1), + expected: path.Paths{ + path.Root("test").AtListIndex(1), + }, + }, + "AttributeNameExact-ElementKeyIntExact-AttributeNameExact-Parent-AttributeNameExact-match": { + schema: Schema{ + Attributes: map[string]Attribute{ + "test_parent": { + Attributes: ListNestedAttributes(map[string]Attribute{ + "test_child1": { + Type: types.StringType, + }, + "test_child2": { + Type: types.StringType, + }, + }), + }, + }, + }, + tfTypeValue: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_parent": tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_child1": tftypes.String, + "test_child2": tftypes.String, + }, + }, + }, + }, + }, + map[string]tftypes.Value{ + "test_parent": tftypes.NewValue( + tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_child1": tftypes.String, + "test_child2": tftypes.String, + }, + }, + }, + []tftypes.Value{ + tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_child1": tftypes.String, + "test_child2": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "test_child1": tftypes.NewValue(tftypes.String, "test-value-list-0-child-1"), + "test_child2": tftypes.NewValue(tftypes.String, "test-value-list-0-child-2"), + }, + ), + tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_child1": tftypes.String, + "test_child2": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "test_child1": tftypes.NewValue(tftypes.String, "test-value-list-1-child-1"), + "test_child2": tftypes.NewValue(tftypes.String, "test-value-list-1-child-2"), + }, + ), + }, + ), + }, + ), + // e.g. Something that would be created in an attribute plan modifier or validator + expression: path.MatchRoot("test_parent").AtListIndex(1).AtName("test_child1").AtParent().AtName("test_child2"), + expected: path.Paths{ + path.Root("test_parent").AtListIndex(1).AtName("test_child2"), + }, + }, + "AttributeNameExact-ElementKeyIntExact-mismatch": { + schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Type: types.ListType{ + ElemType: types.StringType, + }, + }, + }, + }, + tfTypeValue: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.List{ + ElementType: tftypes.String, + }, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.List{ + ElementType: tftypes.String, + }, + []tftypes.Value{ + tftypes.NewValue(tftypes.String, "test-value1"), + tftypes.NewValue(tftypes.String, "test-value2"), + tftypes.NewValue(tftypes.String, "test-value3"), + }, + ), + }, + ), + expression: path.MatchRoot("test").AtListIndex(4), + expected: nil, + }, + "AttributeNameExact-ElementKeyStringAny-match": { + schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Type: types.MapType{ + ElemType: types.StringType, + }, + }, + }, + }, + tfTypeValue: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Map{ + ElementType: tftypes.String, + }, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.Map{ + ElementType: tftypes.String, + }, + map[string]tftypes.Value{ + // Map access is non-deterministic, so test with + // a single key to prevent ordering issues in + // the expected path.Paths + "test-key1": tftypes.NewValue(tftypes.String, "test-value1"), + }, + ), + }, + ), + expression: path.MatchRoot("test").AtAnyMapKey(), + expected: path.Paths{ + path.Root("test").AtMapKey("test-key1"), + }, + }, + "AttributeNameExact-ElementKeyStringAny-mismatch": { + schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Type: types.ListType{ + ElemType: types.StringType, + }, + }, + }, + }, + tfTypeValue: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.List{ + ElementType: tftypes.String, + }, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.List{ + ElementType: tftypes.String, + }, + []tftypes.Value{ + tftypes.NewValue(tftypes.String, "test-value1"), + tftypes.NewValue(tftypes.String, "test-value2"), + tftypes.NewValue(tftypes.String, "test-value3"), + }, + ), + }, + ), + expression: path.MatchRoot("test").AtAnyMapKey(), + expected: nil, + }, + "AttributeNameExact-ElementKeyStringExact-match": { + schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Type: types.MapType{ + ElemType: types.StringType, + }, + }, + }, + }, + tfTypeValue: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Map{ + ElementType: tftypes.String, + }, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.Map{ + ElementType: tftypes.String, + }, + map[string]tftypes.Value{ + "test-key1": tftypes.NewValue(tftypes.String, "test-value1"), + "test-key2": tftypes.NewValue(tftypes.String, "test-value2"), + "test-key3": tftypes.NewValue(tftypes.String, "test-value3"), + }, + ), + }, + ), + expression: path.MatchRoot("test").AtMapKey("test-key2"), + expected: path.Paths{ + path.Root("test").AtMapKey("test-key2"), + }, + }, + "AttributeNameExact-ElementKeyStringExact-mismatch": { + schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Type: types.MapType{ + ElemType: types.StringType, + }, + }, + }, + }, + tfTypeValue: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Map{ + ElementType: tftypes.String, + }, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.Map{ + ElementType: tftypes.String, + }, + map[string]tftypes.Value{ + "test-key1": tftypes.NewValue(tftypes.String, "test-value1"), + "test-key2": tftypes.NewValue(tftypes.String, "test-value2"), + "test-key3": tftypes.NewValue(tftypes.String, "test-value3"), + }, + ), + }, + ), + expression: path.MatchRoot("test").AtMapKey("test-key4"), + expected: nil, + }, + "AttributeNameExact-ElementKeyValueAny-match": { + schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Type: types.SetType{ + ElemType: types.StringType, + }, + }, + }, + }, + tfTypeValue: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Set{ + ElementType: tftypes.String, + }, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.Set{ + ElementType: tftypes.String, + }, + []tftypes.Value{ + tftypes.NewValue(tftypes.String, "test-value1"), + tftypes.NewValue(tftypes.String, "test-value2"), + tftypes.NewValue(tftypes.String, "test-value3"), + }, + ), + }, + ), + expression: path.MatchRoot("test").AtAnySetValue(), + expected: path.Paths{ + path.Root("test").AtSetValue(types.String{Value: "test-value1"}), + path.Root("test").AtSetValue(types.String{Value: "test-value2"}), + path.Root("test").AtSetValue(types.String{Value: "test-value3"}), + }, + }, + "AttributeNameExact-ElementKeyValueAny-mismatch": { + schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Type: types.ListType{ + ElemType: types.StringType, + }, + }, + }, + }, + tfTypeValue: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.List{ + ElementType: tftypes.String, + }, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.List{ + ElementType: tftypes.String, + }, + []tftypes.Value{ + tftypes.NewValue(tftypes.String, "test-value1"), + tftypes.NewValue(tftypes.String, "test-value2"), + tftypes.NewValue(tftypes.String, "test-value3"), + }, + ), + }, + ), + expression: path.MatchRoot("test").AtAnySetValue(), + expected: nil, + }, + "AttributeNameExact-ElementKeyValueExact-match": { + schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Type: types.SetType{ + ElemType: types.StringType, + }, + }, + }, + }, + tfTypeValue: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Set{ + ElementType: tftypes.String, + }, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.Set{ + ElementType: tftypes.String, + }, + []tftypes.Value{ + tftypes.NewValue(tftypes.String, "test-value1"), + tftypes.NewValue(tftypes.String, "test-value2"), + tftypes.NewValue(tftypes.String, "test-value3"), + }, + ), + }, + ), + expression: path.MatchRoot("test").AtSetValue(types.String{Value: "test-value2"}), + expected: path.Paths{ + path.Root("test").AtSetValue(types.String{Value: "test-value2"}), + }, + }, + "AttributeNameExact-ElementKeyValueExact-mismatch": { + schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Type: types.SetType{ + ElemType: types.StringType, + }, + }, + }, + }, + tfTypeValue: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Set{ + ElementType: tftypes.String, + }, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.Set{ + ElementType: tftypes.String, + }, + []tftypes.Value{ + tftypes.NewValue(tftypes.String, "test-value1"), + tftypes.NewValue(tftypes.String, "test-value2"), + tftypes.NewValue(tftypes.String, "test-value3"), + }, + ), + }, + ), + expression: path.MatchRoot("test").AtSetValue(types.String{Value: "test-value4"}), + expected: nil, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, diags := pathMatches(context.Background(), testCase.schema, testCase.tfTypeValue, testCase.expression) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + + if diff := cmp.Diff(diags, testCase.expectedDiags); diff != "" { + t.Errorf("unexpected diagnostics difference: %s", diff) + } + }) + } +} diff --git a/tfsdk/plan.go b/tfsdk/plan.go index 1b6f5e930..ab64b9130 100644 --- a/tfsdk/plan.go +++ b/tfsdk/plan.go @@ -130,6 +130,11 @@ func (p Plan) getAttributeValue(ctx context.Context, path path.Path) (attr.Value return attrValue, diags } +// PathMatches returns all matching path.Paths from the given path.Expression. +func (p Plan) PathMatches(ctx context.Context, pathExpr path.Expression) (path.Paths, diag.Diagnostics) { + return pathMatches(ctx, p.Schema, p.Raw, pathExpr) +} + // Set populates the entire plan using the supplied Go value. The value `val` // should be a struct whose values have one of the attr.Value types. Each field // must be tagged with the corresponding schema field. diff --git a/tfsdk/plan_test.go b/tfsdk/plan_test.go index 481fd44f6..4a9304d6b 100644 --- a/tfsdk/plan_test.go +++ b/tfsdk/plan_test.go @@ -2167,6 +2167,87 @@ func TestPlanPathExists(t *testing.T) { } } +func TestPlanPathMatches(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + plan Plan + expression path.Expression + expected path.Paths + expectedDiags diag.Diagnostics + }{ + // Refer to TestPathMatches for more exhaustive unit testing. + // These test cases are to ensure Plan schema and data values are + // passed appropriately to the shared implementation. + "AttributeNameExact-match": { + plan: Plan{ + Schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Type: types.StringType, + }, + }, + }, + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.String, "test-value"), + }, + ), + }, + expression: path.MatchRoot("test"), + expected: path.Paths{ + path.Root("test"), + }, + }, + "AttributeNameExact-mismatch": { + plan: Plan{ + Schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Type: types.StringType, + }, + }, + }, + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.String, "test-value"), + }, + ), + }, + expression: path.MatchRoot("not-test"), + expected: nil, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, diags := testCase.plan.PathMatches(context.Background(), testCase.expression) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + + if diff := cmp.Diff(diags, testCase.expectedDiags); diff != "" { + t.Errorf("unexpected diagnostics difference: %s", diff) + } + }) + } +} + func TestPlanSet(t *testing.T) { t.Parallel() diff --git a/tfsdk/state.go b/tfsdk/state.go index 773e0a872..da20c1231 100644 --- a/tfsdk/state.go +++ b/tfsdk/state.go @@ -130,6 +130,11 @@ func (s State) getAttributeValue(ctx context.Context, path path.Path) (attr.Valu return attrValue, diags } +// PathMatches returns all matching path.Paths from the given path.Expression. +func (s State) PathMatches(ctx context.Context, pathExpr path.Expression) (path.Paths, diag.Diagnostics) { + return pathMatches(ctx, s.Schema, s.Raw, pathExpr) +} + // Set populates the entire state using the supplied Go value. The value `val` // should be a struct whose values have one of the attr.Value types. Each field // must be tagged with the corresponding schema field. diff --git a/tfsdk/state_test.go b/tfsdk/state_test.go index 72ae041d2..f417cac84 100644 --- a/tfsdk/state_test.go +++ b/tfsdk/state_test.go @@ -2864,6 +2864,87 @@ func TestStatePathExists(t *testing.T) { } } +func TestStatePathMatches(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + state State + expression path.Expression + expected path.Paths + expectedDiags diag.Diagnostics + }{ + // Refer to TestPathMatches for more exhaustive unit testing. + // These test cases are to ensure State schema and data values are + // passed appropriately to the shared implementation. + "AttributeNameExact-match": { + state: State{ + Schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Type: types.StringType, + }, + }, + }, + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.String, "test-value"), + }, + ), + }, + expression: path.MatchRoot("test"), + expected: path.Paths{ + path.Root("test"), + }, + }, + "AttributeNameExact-mismatch": { + state: State{ + Schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Type: types.StringType, + }, + }, + }, + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.String, "test-value"), + }, + ), + }, + expression: path.MatchRoot("not-test"), + expected: nil, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, diags := testCase.state.PathMatches(context.Background(), testCase.expression) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + + if diff := cmp.Diff(diags, testCase.expectedDiags); diff != "" { + t.Errorf("unexpected diagnostics difference: %s", diff) + } + }) + } +} + func TestStateSet(t *testing.T) { t.Parallel() diff --git a/tfsdk/tftypes_attribute_path.go b/tfsdk/tftypes_attribute_path.go new file mode 100644 index 000000000..70aac6a0a --- /dev/null +++ b/tfsdk/tftypes_attribute_path.go @@ -0,0 +1,95 @@ +package tfsdk + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fromtftypes" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +// attributePath returns the path.Path equivalent of a *tftypes.AttributePath. +// +// TODO: This function should be exported as internal/fromtftypes.AttributePath +// except that doing so would currently introduce an import cycle due to the +// tfsdk.Schema parameter here and Config/Plan/State.PathMatches needing to +// call this function until the schema data is migrated to attr.Value. +// Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/172 +// Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/365 +func attributePath(ctx context.Context, tfType *tftypes.AttributePath, schema Schema) (path.Path, diag.Diagnostics) { + fwPath := path.Empty() + + for tfTypeStepIndex, tfTypeStep := range tfType.Steps() { + currentTfTypeSteps := tfType.Steps()[:tfTypeStepIndex+1] + currentTfTypePath := tftypes.NewAttributePathWithSteps(currentTfTypeSteps) + attrType, err := schema.AttributeTypeAtPath(currentTfTypePath) + + if err != nil { + return path.Empty(), diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Unable to Convert Attribute Path", + "An unexpected error occurred while trying to convert an attribute path. "+ + "This is an error in terraform-plugin-framework used by the provider. "+ + "Please report the following to the provider developers.\n\n"+ + // Since this is an error with the attribute path + // conversion, we cannot return a protocol path-based + // diagnostic. Returning a framework human-readable + // representation seems like the next best thing to do. + fmt.Sprintf("Attribute Path: %s\n", currentTfTypePath.String())+ + fmt.Sprintf("Original Error: %s", err), + ), + } + } + + fwStep, err := fromtftypes.AttributePathStep(ctx, tfTypeStep, attrType) + + if err != nil { + return path.Empty(), diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Unable to Convert Attribute Path", + "An unexpected error occurred while trying to convert an attribute path. "+ + "This is either an error in terraform-plugin-framework or a custom attribute type used by the provider. "+ + "Please report the following to the provider developers.\n\n"+ + // Since this is an error with the attribute path + // conversion, we cannot return a protocol path-based + // diagnostic. Returning a framework human-readable + // representation seems like the next best thing to do. + fmt.Sprintf("Attribute Path: %s\n", currentTfTypePath.String())+ + fmt.Sprintf("Original Error: %s", err), + ), + } + } + + // In lieu of creating a path.NewPathFromSteps function, this path + // building logic is inlined to not expand the path package API. + switch fwStep := fwStep.(type) { + case path.PathStepAttributeName: + fwPath = fwPath.AtName(string(fwStep)) + case path.PathStepElementKeyInt: + fwPath = fwPath.AtListIndex(int(fwStep)) + case path.PathStepElementKeyString: + fwPath = fwPath.AtMapKey(string(fwStep)) + case path.PathStepElementKeyValue: + fwPath = fwPath.AtSetValue(fwStep.Value) + default: + return fwPath, diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Unable to Convert Attribute Path", + "An unexpected error occurred while trying to convert an attribute path. "+ + "This is an error in terraform-plugin-framework used by the provider. "+ + "Please report the following to the provider developers.\n\n"+ + // Since this is an error with the attribute path + // conversion, we cannot return a protocol path-based + // diagnostic. Returning a framework human-readable + // representation seems like the next best thing to do. + fmt.Sprintf("Attribute Path: %s\n", currentTfTypePath.String())+ + fmt.Sprintf("Original Error: unknown path.PathStep type: %#v", fwStep), + ), + } + } + } + + return fwPath, nil +} diff --git a/tfsdk/tftypes_attribute_path_test.go b/tfsdk/tftypes_attribute_path_test.go new file mode 100644 index 000000000..bb6eb826c --- /dev/null +++ b/tfsdk/tftypes_attribute_path_test.go @@ -0,0 +1,198 @@ +package tfsdk + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/diag" + testtypes "github.com/hashicorp/terraform-plugin-framework/internal/testing/types" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestAttributePath(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + tfType *tftypes.AttributePath + schema Schema + expected path.Path + expectedDiags diag.Diagnostics + }{ + "nil": { + tfType: nil, + expected: path.Empty(), + }, + "empty": { + tfType: tftypes.NewAttributePath(), + expected: path.Empty(), + }, + "AttributeName": { + tfType: tftypes.NewAttributePath().WithAttributeName("test"), + schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Type: types.StringType, + }, + }, + }, + expected: path.Root("test"), + }, + "AttributeName-nonexistent-attribute": { + tfType: tftypes.NewAttributePath().WithAttributeName("test"), + schema: Schema{ + Attributes: map[string]Attribute{ + "not-test": { + Type: testtypes.StringType{}, + }, + }, + }, + expected: path.Empty(), + expectedDiags: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Unable to Convert Attribute Path", + "An unexpected error occurred while trying to convert an attribute path. "+ + "This is an error in terraform-plugin-framework used by the provider. "+ + "Please report the following to the provider developers.\n\n"+ + "Attribute Path: AttributeName(\"test\")\n"+ + "Original Error: AttributeName(\"test\") still remains in the path: could not find attribute or block \"test\" in schema", + ), + }, + }, + "AttributeName-ElementKeyInt": { + tfType: tftypes.NewAttributePath().WithAttributeName("test").WithElementKeyInt(1), + schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Type: types.ListType{ + ElemType: types.StringType, + }, + }, + }, + }, + expected: path.Root("test").AtListIndex(1), + }, + "AttributeName-ElementKeyValue": { + tfType: tftypes.NewAttributePath().WithAttributeName("test").WithElementKeyValue(tftypes.NewValue(tftypes.String, "test-value")), + schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Type: types.SetType{ + ElemType: types.StringType, + }, + }, + }, + }, + expected: path.Root("test").AtSetValue(types.String{Value: "test-value"}), + }, + "AttributeName-ElementKeyValue-value-conversion-error": { + tfType: tftypes.NewAttributePath().WithAttributeName("test").WithElementKeyValue(tftypes.NewValue(tftypes.String, "test-value")), + schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Type: types.SetType{ + ElemType: testtypes.InvalidType{}, + }, + }, + }, + }, + expected: path.Empty(), + expectedDiags: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Unable to Convert Attribute Path", + "An unexpected error occurred while trying to convert an attribute path. "+ + "This is either an error in terraform-plugin-framework or a custom attribute type used by the provider. "+ + "Please report the following to the provider developers.\n\n"+ + "Attribute Path: AttributeName(\"test\").ElementKeyValue(tftypes.String<\"test-value\">)\n"+ + "Original Error: unable to create PathStepElementKeyValue from tftypes.Value: unable to convert tftypes.Value (tftypes.String<\"test-value\">) to attr.Value: intentional ValueFromTerraform error", + ), + }, + }, + "ElementKeyInt": { + tfType: tftypes.NewAttributePath().WithElementKeyInt(1), + schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Type: testtypes.StringType{}, + }, + }, + }, + expected: path.Empty(), + expectedDiags: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Unable to Convert Attribute Path", + "An unexpected error occurred while trying to convert an attribute path. "+ + "This is an error in terraform-plugin-framework used by the provider. "+ + "Please report the following to the provider developers.\n\n"+ + "Attribute Path: ElementKeyInt(1)\n"+ + "Original Error: ElementKeyInt(1) still remains in the path: cannot apply AttributePathStep tftypes.ElementKeyInt to schema", + ), + }, + }, + "ElementKeyString": { + tfType: tftypes.NewAttributePath().WithElementKeyString("test"), + schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Type: testtypes.StringType{}, + }, + }, + }, + expected: path.Empty(), + expectedDiags: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Unable to Convert Attribute Path", + "An unexpected error occurred while trying to convert an attribute path. "+ + "This is an error in terraform-plugin-framework used by the provider. "+ + "Please report the following to the provider developers.\n\n"+ + "Attribute Path: ElementKeyString(\"test\")\n"+ + "Original Error: ElementKeyString(\"test\") still remains in the path: cannot apply AttributePathStep tftypes.ElementKeyString to schema", + ), + }, + }, + "ElementKeyValue": { + tfType: tftypes.NewAttributePath().WithElementKeyValue(tftypes.NewValue(tftypes.String, "test-value")), + schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Type: testtypes.StringType{}, + }, + }, + }, + expected: path.Empty(), + expectedDiags: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Unable to Convert Attribute Path", + "An unexpected error occurred while trying to convert an attribute path. "+ + "This is an error in terraform-plugin-framework used by the provider. "+ + "Please report the following to the provider developers.\n\n"+ + "Attribute Path: ElementKeyValue(tftypes.String<\"test-value\">)\n"+ + "Original Error: ElementKeyValue(tftypes.String<\"test-value\">) still remains in the path: cannot apply AttributePathStep tftypes.ElementKeyValue to schema", + ), + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, diags := attributePath(context.Background(), testCase.tfType, testCase.schema) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + + if diff := cmp.Diff(diags, testCase.expectedDiags); diff != "" { + for _, d := range diags { + t.Logf("diag: %s", d.Detail()) + } + t.Errorf("unexpected diagnostics difference: %s", diff) + } + }) + } +} From 9f3d84cd8cb69ef6e2c43d31c65c202b5940ed67 Mon Sep 17 00:00:00 2001 From: Brian Flad Date: Wed, 29 Jun 2022 13:14:49 -0400 Subject: [PATCH 4/4] Ensure empty resolved expressions do not match anything --- path/expression_steps.go | 5 +++ path/expression_steps_test.go | 12 ++++++- path/expression_test.go | 2 +- tfsdk/path_matches_test.go | 63 +++++++++++++++++++++++------------ 4 files changed, 59 insertions(+), 23 deletions(-) diff --git a/path/expression_steps.go b/path/expression_steps.go index 97890dfd4..7ee79b01d 100644 --- a/path/expression_steps.go +++ b/path/expression_steps.go @@ -66,6 +66,11 @@ func (s ExpressionSteps) LastStep() (ExpressionStep, ExpressionSteps) { func (s ExpressionSteps) Matches(pathSteps PathSteps) bool { resolvedExpressionSteps := s.Resolve() + // Empty expression should not match anything to prevent false positives. + if len(resolvedExpressionSteps) == 0 { + return false + } + if len(resolvedExpressionSteps) != len(pathSteps) { return false } diff --git a/path/expression_steps_test.go b/path/expression_steps_test.go index 2b69acbf7..751dc3455 100644 --- a/path/expression_steps_test.go +++ b/path/expression_steps_test.go @@ -390,7 +390,7 @@ func TestExpressionStepsMatches(t *testing.T) { "empty-empty": { steps: path.ExpressionSteps{}, pathSteps: path.PathSteps{}, - expected: true, + expected: false, }, "empty-nonempty": { steps: path.ExpressionSteps{}, @@ -604,6 +604,16 @@ func TestExpressionStepsMatches(t *testing.T) { }, expected: true, }, + "AttributeNameExact-Parent": { + steps: path.ExpressionSteps{ + path.ExpressionStepAttributeNameExact("test1"), + path.ExpressionStepParent{}, + }, + pathSteps: path.PathSteps{ + path.PathStepAttributeName("test1"), + }, + expected: false, + }, "AttributeNameExact-Parent-AttributeNameExact-different": { steps: path.ExpressionSteps{ path.ExpressionStepAttributeNameExact("test1"), diff --git a/path/expression_test.go b/path/expression_test.go index 9f1602e64..a9aa7011a 100644 --- a/path/expression_test.go +++ b/path/expression_test.go @@ -370,7 +370,7 @@ func TestExpressionMatches(t *testing.T) { "empty-empty": { expression: path.Expression{}, path: path.Empty(), - expected: true, + expected: false, }, "empty-nonempty": { expression: path.Expression{}, diff --git a/tfsdk/path_matches_test.go b/tfsdk/path_matches_test.go index 59160dc7a..27d416ae2 100644 --- a/tfsdk/path_matches_test.go +++ b/tfsdk/path_matches_test.go @@ -21,27 +21,6 @@ func TestPathMatches(t *testing.T) { expected path.Paths expectedDiags diag.Diagnostics }{ - "ohno": { - schema: Schema{ - Attributes: map[string]Attribute{ - "test": { - Type: types.StringType, - }, - }, - }, - tfTypeValue: tftypes.NewValue( - tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "test": tftypes.String, - }, - }, - map[string]tftypes.Value{ - "test": tftypes.NewValue(tftypes.String, "test-value"), - }, - ), - expression: path.MatchRoot("test").AtParent().AtParent(), - expected: nil, - }, "AttributeNameExact-match": { schema: Schema{ Attributes: map[string]Attribute{ @@ -584,6 +563,48 @@ func TestPathMatches(t *testing.T) { expression: path.MatchRoot("test").AtSetValue(types.String{Value: "test-value4"}), expected: nil, }, + "AttributeNameExact-Parent": { + schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Type: types.StringType, + }, + }, + }, + tfTypeValue: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.String, "test-value"), + }, + ), + expression: path.MatchRoot("test").AtParent(), + expected: nil, + }, + "AttributeNameExact-Parent-Parent": { + schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Type: types.StringType, + }, + }, + }, + tfTypeValue: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.String, "test-value"), + }, + ), + expression: path.MatchRoot("test").AtParent().AtParent(), + expected: nil, + }, } for name, testCase := range testCases {