Skip to content

Commit 34bd9b6

Browse files
authored
path: Initial Path Expression Support (#396)
Reference: #81 Reference: hashicorp/terraform-plugin-framework-validators#14 Reference: hashicorp/terraform-plugin-framework-validators#15 Reference: hashicorp/terraform-plugin-framework-validators#16 Reference: hashicorp/terraform-plugin-framework-validators#17 Reference: hashicorp/terraform-plugin-framework-validators#20 This introduces the concept of root and relative attribute path expressions, abstractions on top of an attribute path, which enables provider developers to declare logic which might match zero, one, or more paths. Paths are directly convertible into path expressions as exact expression steps. The builder-like syntax for exact expression steps matches the syntax for regular 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()` and starting an expression with `MatchRelative()` so it 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.MatchRelative().AtParent().AtName("another_same_level_attribute"), ), }, } ``` Then the logic within the validator can take the `ValidateAttributeRequest.AttributePathExpression` and use the `(path.Expression).Merge()` method to combine the current attribute expression with any incoming expressions. To find matching attribute paths based on a path expression within `tfsdk.Config`, `tfsdk.Plan`, and `tfsdk.State`, a `PathMatches(path.Expression)` method has been added to each type. The resulting paths can then be used to fetch data via existing functionality, such as the `GetAttribute()` method of each type.
1 parent 133b0a4 commit 34bd9b6

File tree

57 files changed

+5636
-23
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

57 files changed

+5636
-23
lines changed

.changelog/396.txt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
```release-note:feature
2+
path: Introduced attribute path expressions
3+
```
4+
5+
```release-note:enhancement
6+
tfsdk: Added `AttributePathExpression` field to `ModifyAttributePlanRequest` and `ValidateAttributeRequest` types
7+
```
8+
9+
```release-note:enhancement
10+
tfsdk: Added `PathMatches` method to `Config`, `Plan`, and `State` types
11+
```
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package fromtftypes
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/hashicorp/terraform-plugin-framework/attr"
8+
"github.com/hashicorp/terraform-plugin-framework/path"
9+
"github.com/hashicorp/terraform-plugin-go/tftypes"
10+
)
11+
12+
// AttributePathStep returns the path.PathStep equivalent of a
13+
// tftypes.AttributePathStep. An error is returned instead of diag.Diagnostics
14+
// so callers can include appropriate logical context about when the error
15+
// occurred.
16+
func AttributePathStep(ctx context.Context, tfType tftypes.AttributePathStep, attrType attr.Type) (path.PathStep, error) {
17+
switch tfType := tfType.(type) {
18+
case tftypes.AttributeName:
19+
return path.PathStepAttributeName(string(tfType)), nil
20+
case tftypes.ElementKeyInt:
21+
return path.PathStepElementKeyInt(int64(tfType)), nil
22+
case tftypes.ElementKeyString:
23+
return path.PathStepElementKeyString(string(tfType)), nil
24+
case tftypes.ElementKeyValue:
25+
attrValue, err := Value(ctx, tftypes.Value(tfType), attrType)
26+
27+
if err != nil {
28+
return nil, fmt.Errorf("unable to create PathStepElementKeyValue from tftypes.Value: %w", err)
29+
}
30+
31+
return path.PathStepElementKeyValue{Value: attrValue}, nil
32+
default:
33+
return nil, fmt.Errorf("unknown tftypes.AttributePathStep: %#v", tfType)
34+
}
35+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package fromtftypes_test
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"strings"
7+
"testing"
8+
9+
"github.com/google/go-cmp/cmp"
10+
"github.com/hashicorp/terraform-plugin-framework/attr"
11+
"github.com/hashicorp/terraform-plugin-framework/internal/fromtftypes"
12+
"github.com/hashicorp/terraform-plugin-framework/path"
13+
"github.com/hashicorp/terraform-plugin-framework/types"
14+
"github.com/hashicorp/terraform-plugin-go/tftypes"
15+
)
16+
17+
func TestAttributePathStep(t *testing.T) {
18+
t.Parallel()
19+
20+
testCases := map[string]struct {
21+
tfType tftypes.AttributePathStep
22+
attrType attr.Type
23+
expected path.PathStep
24+
expectedError error
25+
}{
26+
"nil": {
27+
tfType: nil,
28+
expected: nil,
29+
expectedError: fmt.Errorf("unknown tftypes.AttributePathStep: <nil>"),
30+
},
31+
"PathStepAttributeName": {
32+
tfType: tftypes.AttributeName("test"),
33+
expected: path.PathStepAttributeName("test"),
34+
},
35+
"PathStepElementKeyInt": {
36+
tfType: tftypes.ElementKeyInt(1),
37+
expected: path.PathStepElementKeyInt(1),
38+
},
39+
"PathStepElementKeyString": {
40+
tfType: tftypes.ElementKeyString("test"),
41+
expected: path.PathStepElementKeyString("test"),
42+
},
43+
"PathStepElementKeyValue": {
44+
tfType: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")),
45+
attrType: types.StringType,
46+
expected: path.PathStepElementKeyValue{Value: types.String{Value: "test"}},
47+
},
48+
"PathStepElementKeyValue-error": {
49+
tfType: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")),
50+
attrType: types.BoolType,
51+
expected: nil,
52+
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"),
53+
},
54+
}
55+
56+
for name, testCase := range testCases {
57+
name, testCase := name, testCase
58+
59+
t.Run(name, func(t *testing.T) {
60+
t.Parallel()
61+
62+
got, err := fromtftypes.AttributePathStep(context.Background(), testCase.tfType, testCase.attrType)
63+
64+
if err != nil {
65+
if testCase.expectedError == nil {
66+
t.Fatalf("expected no error, got: %s", err)
67+
}
68+
69+
if !strings.Contains(err.Error(), testCase.expectedError.Error()) {
70+
t.Fatalf("expected error %q, got: %s", testCase.expectedError, err)
71+
}
72+
}
73+
74+
if err == nil && testCase.expectedError != nil {
75+
t.Fatalf("got no error, tfType: %s", testCase.expectedError)
76+
}
77+
78+
if diff := cmp.Diff(got, testCase.expected); diff != "" {
79+
t.Errorf("unexpected difference: %s", diff)
80+
}
81+
})
82+
}
83+
}

internal/fromtftypes/doc.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
// Package fromtftypes contains functions to convert from terraform-plugin-go
2+
// tftypes types to framework types.
3+
package fromtftypes

internal/fromtftypes/value.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package fromtftypes
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/hashicorp/terraform-plugin-framework/attr"
8+
"github.com/hashicorp/terraform-plugin-go/tftypes"
9+
)
10+
11+
// Value returns the attr.Value equivalent to the tftypes.Value.
12+
func Value(ctx context.Context, tfType tftypes.Value, attrType attr.Type) (attr.Value, error) {
13+
if attrType == nil {
14+
return nil, fmt.Errorf("unable to convert tftypes.Value (%s) to attr.Value: missing attr.Type", tfType.String())
15+
}
16+
17+
attrValue, err := attrType.ValueFromTerraform(ctx, tfType)
18+
19+
if err != nil {
20+
return nil, fmt.Errorf("unable to convert tftypes.Value (%s) to attr.Value: %w", tfType.String(), err)
21+
}
22+
23+
return attrValue, nil
24+
}

0 commit comments

Comments
 (0)