Skip to content

Commit

Permalink
path: Initial Path Expression Support
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
bflad committed Jun 28, 2022
1 parent 5a338a7 commit 16f0424
Show file tree
Hide file tree
Showing 42 changed files with 3,807 additions and 23 deletions.
3 changes: 3 additions & 0 deletions .changelog/pending.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:enhancement
tfsdk: Added `AttributePathExpression` field to `ModifyAttributePlanRequest` and `ValidateAttributeRequest` types
```
20 changes: 12 additions & 8 deletions internal/fwserver/attribute_validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
20 changes: 12 additions & 8 deletions internal/fwserver/block_validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down
10 changes: 6 additions & 4 deletions internal/fwserver/schema_validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down
163 changes: 163 additions & 0 deletions path/expression.go
Original file line number Diff line number Diff line change
@@ -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),
},
}
}
20 changes: 20 additions & 0 deletions path/expression_step.go
Original file line number Diff line number Diff line change
@@ -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()
}
43 changes: 43 additions & 0 deletions path/expression_step_attribute_name_exact.go
Original file line number Diff line number Diff line change
@@ -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() {}
Loading

0 comments on commit 16f0424

Please sign in to comment.