Skip to content

Commit

Permalink
path: Initial Path Expression Support (hashicorp#396)
Browse files Browse the repository at this point in the history
Reference: hashicorp#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.
  • Loading branch information
bflad authored Jun 29, 2022
1 parent 133b0a4 commit 34bd9b6
Show file tree
Hide file tree
Showing 57 changed files with 5,636 additions and 23 deletions.
11 changes: 11 additions & 0 deletions .changelog/396.txt
Original file line number Diff line number Diff line change
@@ -0,0 +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
```
35 changes: 35 additions & 0 deletions internal/fromtftypes/attribute_path_step.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
83 changes: 83 additions & 0 deletions internal/fromtftypes/attribute_path_step_test.go
Original file line number Diff line number Diff line change
@@ -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: <nil>"),
},
"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)
}
})
}
}
3 changes: 3 additions & 0 deletions internal/fromtftypes/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// Package fromtftypes contains functions to convert from terraform-plugin-go
// tftypes types to framework types.
package fromtftypes
24 changes: 24 additions & 0 deletions internal/fromtftypes/value.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading

0 comments on commit 34bd9b6

Please sign in to comment.