Skip to content

Commit 16f0424

Browse files
committed
path: Initial Path Expression Support
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 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.
1 parent 5a338a7 commit 16f0424

Some content is hidden

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

42 files changed

+3807
-23
lines changed

.changelog/pending.txt

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
```release-note:enhancement
2+
tfsdk: Added `AttributePathExpression` field to `ModifyAttributePlanRequest` and `ValidateAttributeRequest` types
3+
```

internal/fwserver/attribute_validation.go

+12-8
Original file line numberDiff line numberDiff line change
@@ -157,8 +157,9 @@ func AttributeValidateNestedAttributes(ctx context.Context, a tfsdk.Attribute, r
157157
for idx := range l.Elems {
158158
for nestedName, nestedAttr := range a.Attributes.GetAttributes() {
159159
nestedAttrReq := tfsdk.ValidateAttributeRequest{
160-
AttributePath: req.AttributePath.AtListIndex(idx).AtName(nestedName),
161-
Config: req.Config,
160+
AttributePath: req.AttributePath.AtListIndex(idx).AtName(nestedName),
161+
AttributePathExpression: req.AttributePathExpression.AtListIndex(idx).AtName(nestedName),
162+
Config: req.Config,
162163
}
163164
nestedAttrResp := &tfsdk.ValidateAttributeResponse{
164165
Diagnostics: resp.Diagnostics,
@@ -186,8 +187,9 @@ func AttributeValidateNestedAttributes(ctx context.Context, a tfsdk.Attribute, r
186187
for _, value := range s.Elems {
187188
for nestedName, nestedAttr := range a.Attributes.GetAttributes() {
188189
nestedAttrReq := tfsdk.ValidateAttributeRequest{
189-
AttributePath: req.AttributePath.AtSetValue(value).AtName(nestedName),
190-
Config: req.Config,
190+
AttributePath: req.AttributePath.AtSetValue(value).AtName(nestedName),
191+
AttributePathExpression: req.AttributePathExpression.AtSetValue(value).AtName(nestedName),
192+
Config: req.Config,
191193
}
192194
nestedAttrResp := &tfsdk.ValidateAttributeResponse{
193195
Diagnostics: resp.Diagnostics,
@@ -215,8 +217,9 @@ func AttributeValidateNestedAttributes(ctx context.Context, a tfsdk.Attribute, r
215217
for key := range m.Elems {
216218
for nestedName, nestedAttr := range a.Attributes.GetAttributes() {
217219
nestedAttrReq := tfsdk.ValidateAttributeRequest{
218-
AttributePath: req.AttributePath.AtMapKey(key).AtName(nestedName),
219-
Config: req.Config,
220+
AttributePath: req.AttributePath.AtMapKey(key).AtName(nestedName),
221+
AttributePathExpression: req.AttributePathExpression.AtMapKey(key).AtName(nestedName),
222+
Config: req.Config,
220223
}
221224
nestedAttrResp := &tfsdk.ValidateAttributeResponse{
222225
Diagnostics: resp.Diagnostics,
@@ -244,8 +247,9 @@ func AttributeValidateNestedAttributes(ctx context.Context, a tfsdk.Attribute, r
244247
if !o.Null && !o.Unknown {
245248
for nestedName, nestedAttr := range a.Attributes.GetAttributes() {
246249
nestedAttrReq := tfsdk.ValidateAttributeRequest{
247-
AttributePath: req.AttributePath.AtName(nestedName),
248-
Config: req.Config,
250+
AttributePath: req.AttributePath.AtName(nestedName),
251+
AttributePathExpression: req.AttributePathExpression.AtName(nestedName),
252+
Config: req.Config,
249253
}
250254
nestedAttrResp := &tfsdk.ValidateAttributeResponse{
251255
Diagnostics: resp.Diagnostics,

internal/fwserver/block_validation.go

+12-8
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,9 @@ func BlockValidate(ctx context.Context, b tfsdk.Block, req tfsdk.ValidateAttribu
4747
for idx := range l.Elems {
4848
for name, attr := range b.Attributes {
4949
nestedAttrReq := tfsdk.ValidateAttributeRequest{
50-
AttributePath: req.AttributePath.AtListIndex(idx).AtName(name),
51-
Config: req.Config,
50+
AttributePath: req.AttributePath.AtListIndex(idx).AtName(name),
51+
AttributePathExpression: req.AttributePathExpression.AtListIndex(idx).AtName(name),
52+
Config: req.Config,
5253
}
5354
nestedAttrResp := &tfsdk.ValidateAttributeResponse{
5455
Diagnostics: resp.Diagnostics,
@@ -61,8 +62,9 @@ func BlockValidate(ctx context.Context, b tfsdk.Block, req tfsdk.ValidateAttribu
6162

6263
for name, block := range b.Blocks {
6364
nestedAttrReq := tfsdk.ValidateAttributeRequest{
64-
AttributePath: req.AttributePath.AtListIndex(idx).AtName(name),
65-
Config: req.Config,
65+
AttributePath: req.AttributePath.AtListIndex(idx).AtName(name),
66+
AttributePathExpression: req.AttributePathExpression.AtListIndex(idx).AtName(name),
67+
Config: req.Config,
6668
}
6769
nestedAttrResp := &tfsdk.ValidateAttributeResponse{
6870
Diagnostics: resp.Diagnostics,
@@ -90,8 +92,9 @@ func BlockValidate(ctx context.Context, b tfsdk.Block, req tfsdk.ValidateAttribu
9092
for _, value := range s.Elems {
9193
for name, attr := range b.Attributes {
9294
nestedAttrReq := tfsdk.ValidateAttributeRequest{
93-
AttributePath: req.AttributePath.AtSetValue(value).AtName(name),
94-
Config: req.Config,
95+
AttributePath: req.AttributePath.AtSetValue(value).AtName(name),
96+
AttributePathExpression: req.AttributePathExpression.AtSetValue(value).AtName(name),
97+
Config: req.Config,
9598
}
9699
nestedAttrResp := &tfsdk.ValidateAttributeResponse{
97100
Diagnostics: resp.Diagnostics,
@@ -104,8 +107,9 @@ func BlockValidate(ctx context.Context, b tfsdk.Block, req tfsdk.ValidateAttribu
104107

105108
for name, block := range b.Blocks {
106109
nestedAttrReq := tfsdk.ValidateAttributeRequest{
107-
AttributePath: req.AttributePath.AtSetValue(value).AtName(name),
108-
Config: req.Config,
110+
AttributePath: req.AttributePath.AtSetValue(value).AtName(name),
111+
AttributePathExpression: req.AttributePathExpression.AtSetValue(value).AtName(name),
112+
Config: req.Config,
109113
}
110114
nestedAttrResp := &tfsdk.ValidateAttributeResponse{
111115
Diagnostics: resp.Diagnostics,

internal/fwserver/schema_validation.go

+6-4
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,9 @@ func SchemaValidate(ctx context.Context, s tfsdk.Schema, req ValidateSchemaReque
3636
for name, attribute := range s.Attributes {
3737

3838
attributeReq := tfsdk.ValidateAttributeRequest{
39-
AttributePath: path.Root(name),
40-
Config: req.Config,
39+
AttributePath: path.Root(name),
40+
AttributePathExpression: path.MatchRoot(name),
41+
Config: req.Config,
4142
}
4243
attributeResp := &tfsdk.ValidateAttributeResponse{
4344
Diagnostics: resp.Diagnostics,
@@ -50,8 +51,9 @@ func SchemaValidate(ctx context.Context, s tfsdk.Schema, req ValidateSchemaReque
5051

5152
for name, block := range s.Blocks {
5253
attributeReq := tfsdk.ValidateAttributeRequest{
53-
AttributePath: path.Root(name),
54-
Config: req.Config,
54+
AttributePath: path.Root(name),
55+
AttributePathExpression: path.MatchRoot(name),
56+
Config: req.Config,
5557
}
5658
attributeResp := &tfsdk.ValidateAttributeResponse{
5759
Diagnostics: resp.Diagnostics,

path/expression.go

+163
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
package path
2+
3+
import (
4+
"github.com/hashicorp/terraform-plugin-framework/attr"
5+
)
6+
7+
// Expression represents an attribute path with expression steps, which can
8+
// represent zero, one, or more actual Paths.
9+
type Expression struct {
10+
// steps is the transversals included with the expression. In general,
11+
// operations against the path should protect against modification of the
12+
// original.
13+
steps ExpressionSteps
14+
}
15+
16+
// AtAnyListIndex returns a copied expression with a new list index step at the
17+
// end. The returned path is safe to modify without affecting the original.
18+
func (e Expression) AtAnyListIndex() Expression {
19+
copiedPath := e.Copy()
20+
21+
copiedPath.steps.Append(ExpressionStepElementKeyIntAny{})
22+
23+
return copiedPath
24+
}
25+
26+
// AtAnyMapKey returns a copied expression with a new map key step at the end.
27+
// The returned path is safe to modify without affecting the original.
28+
func (e Expression) AtAnyMapKey() Expression {
29+
copiedPath := e.Copy()
30+
31+
copiedPath.steps.Append(ExpressionStepElementKeyStringAny{})
32+
33+
return copiedPath
34+
}
35+
36+
// AtAnySetValue returns a copied expression with a new set value step at the
37+
// end. The returned path is safe to modify without affecting the original.
38+
func (e Expression) AtAnySetValue() Expression {
39+
copiedPath := e.Copy()
40+
41+
copiedPath.steps.Append(ExpressionStepElementKeyValueAny{})
42+
43+
return copiedPath
44+
}
45+
46+
// AtListIndex returns a copied expression with a new list index step at the
47+
// end. The returned path is safe to modify without affecting the original.
48+
func (e Expression) AtListIndex(index int) Expression {
49+
copiedPath := e.Copy()
50+
51+
copiedPath.steps.Append(ExpressionStepElementKeyIntExact(index))
52+
53+
return copiedPath
54+
}
55+
56+
// AtMapKey returns a copied expression with a new map key step at the end.
57+
// The returned path is safe to modify without affecting the original.
58+
func (e Expression) AtMapKey(key string) Expression {
59+
copiedPath := e.Copy()
60+
61+
copiedPath.steps.Append(ExpressionStepElementKeyStringExact(key))
62+
63+
return copiedPath
64+
}
65+
66+
// AtName returns a copied expression with a new attribute or block name step
67+
// at the end. The returned path is safe to modify without affecting the
68+
// original.
69+
func (e Expression) AtName(name string) Expression {
70+
copiedPath := e.Copy()
71+
72+
copiedPath.steps.Append(ExpressionStepAttributeNameExact(name))
73+
74+
return copiedPath
75+
}
76+
77+
// AtParent returns a copied expression with a new parent step at the end.
78+
// The returned path is safe to modify without affecting the original.
79+
func (e Expression) AtParent() Expression {
80+
copiedPath := e.Copy()
81+
82+
copiedPath.steps.Append(ExpressionStepParent{})
83+
84+
return copiedPath
85+
}
86+
87+
// AtSetValue returns a copied expression with a new set value step at the end.
88+
// The returned path is safe to modify without affecting the original.
89+
func (e Expression) AtSetValue(value attr.Value) Expression {
90+
copiedPath := e.Copy()
91+
92+
copiedPath.steps.Append(ExpressionStepElementKeyValueExact{Value: value})
93+
94+
return copiedPath
95+
}
96+
97+
// Copy returns a duplicate of the expression that is safe to modify without
98+
// affecting the original.
99+
func (e Expression) Copy() Expression {
100+
return Expression{
101+
steps: e.Steps(),
102+
}
103+
}
104+
105+
// Equal returns true if the given expression is exactly equivalent.
106+
func (e Expression) Equal(o Expression) bool {
107+
if e.steps == nil && o.steps == nil {
108+
return true
109+
}
110+
111+
if e.steps == nil {
112+
return false
113+
}
114+
115+
if !e.steps.Equal(o.steps) {
116+
return false
117+
}
118+
119+
return true
120+
}
121+
122+
// Matches returns true if the given Path is valid for the Expression.
123+
func (e Expression) Matches(path Path) bool {
124+
return e.steps.Matches(path.Steps())
125+
}
126+
127+
// Steps returns a copy of the underlying expression steps. Returns an empty
128+
// collection of steps if expression is nil.
129+
func (e Expression) Steps() ExpressionSteps {
130+
if len(e.steps) == 0 {
131+
return ExpressionSteps{}
132+
}
133+
134+
return e.steps.Copy()
135+
}
136+
137+
// String returns the human-readable representation of the path.
138+
// It is intended for logging and error messages and is not protected by
139+
// compatibility guarantees.
140+
func (e Expression) String() string {
141+
return e.steps.String()
142+
}
143+
144+
// MatchParent creates an attribute path expression starting with
145+
// ExpressionStepParent. This allows creating a relative expression in
146+
// nested schemas.
147+
func MatchParent() Expression {
148+
return Expression{
149+
steps: ExpressionSteps{
150+
ExpressionStepParent{},
151+
},
152+
}
153+
}
154+
155+
// MatchRoot creates an attribute path expression starting with
156+
// ExpressionStepAttributeNameExact.
157+
func MatchRoot(rootAttributeName string) Expression {
158+
return Expression{
159+
steps: ExpressionSteps{
160+
ExpressionStepAttributeNameExact(rootAttributeName),
161+
},
162+
}
163+
}

path/expression_step.go

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package path
2+
3+
// ExpressionStep represents an expression of an attribute path step, which may
4+
// match zero, one, or more actual paths.
5+
type ExpressionStep interface {
6+
// Equal should return true if the given Step is exactly equivalent.
7+
Equal(ExpressionStep) bool
8+
9+
// Matches should return true if the given PathStep can be fulfilled by the
10+
// ExpressionStep.
11+
Matches(PathStep) bool
12+
13+
// String should return a human-readable representation of the step
14+
// intended for logging and error messages. There should not be usage
15+
// that needs to be protected by compatibility guarantees.
16+
String() string
17+
18+
// unexported prevents outside types from satisfying the interface.
19+
unexported()
20+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package path
2+
3+
// Ensure ExpressionStepAttributeNameExact satisfies the ExpressionStep
4+
// interface.
5+
var _ ExpressionStep = ExpressionStepAttributeNameExact("")
6+
7+
// ExpressionStepAttributeNameExact is an attribute path expression for an
8+
// exact attribute name match within an object.
9+
type ExpressionStepAttributeNameExact string
10+
11+
// Equal returns true if the given ExpressionStep is a
12+
// ExpressionStepAttributeNameExact and the attribute name is equivalent.
13+
func (s ExpressionStepAttributeNameExact) Equal(o ExpressionStep) bool {
14+
other, ok := o.(ExpressionStepAttributeNameExact)
15+
16+
if !ok {
17+
return false
18+
}
19+
20+
return string(s) == string(other)
21+
}
22+
23+
// Matches returns true if the given PathStep is fulfilled by the
24+
// ExpressionStepAttributeNameExact condition.
25+
func (s ExpressionStepAttributeNameExact) Matches(pathStep PathStep) bool {
26+
pathStepAttributeName, ok := pathStep.(PathStepAttributeName)
27+
28+
if !ok {
29+
return false
30+
}
31+
32+
return string(s) == string(pathStepAttributeName)
33+
}
34+
35+
// String returns the human-readable representation of the attribute name
36+
// expression. It is intended for logging and error messages and is not
37+
// protected by compatibility guarantees.
38+
func (s ExpressionStepAttributeNameExact) String() string {
39+
return string(s)
40+
}
41+
42+
// unexported satisfies the Step interface.
43+
func (s ExpressionStepAttributeNameExact) unexported() {}

0 commit comments

Comments
 (0)