Skip to content

Commit

Permalink
tftypes: Support AttributePathStepper interface in Type
Browse files Browse the repository at this point in the history
Reference: #110

This will allow implementations to step into `Type` via `AttributePath` similar to `Value`, which can simplify data handling logic.
  • Loading branch information
bflad committed Mar 9, 2022
1 parent 8da41e2 commit d55e48d
Show file tree
Hide file tree
Showing 16 changed files with 763 additions and 15 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
tftypes: Added `Type` support to `WalkAttributePath()` function
```
6 changes: 3 additions & 3 deletions tftypes/attribute_path.go
Original file line number Diff line number Diff line change
Expand Up @@ -284,9 +284,9 @@ type AttributePathStepper interface {
ApplyTerraform5AttributePathStep(AttributePathStep) (interface{}, error)
}

// WalkAttributePath will return the value that `path` is pointing to, using
// `in` as the root. If an error is returned, the AttributePath returned will
// indicate the steps that remained to be applied when the error was
// WalkAttributePath will return the Type or Value that `path` is pointing to,
// using `in` as the root. If an error is returned, the AttributePath returned
// will indicate the steps that remained to be applied when the error was
// encountered.
//
// map[string]interface{} and []interface{} types have built-in support. Other
Expand Down
186 changes: 179 additions & 7 deletions tftypes/attribute_path_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,13 @@ func (a attributePathStepperTestSlice) ApplyTerraform5AttributePathStep(step Att
func TestWalkAttributePath(t *testing.T) {
t.Parallel()
type testCase struct {
value interface{}
in interface{}
path *AttributePath
expected interface{}
}
tests := map[string]testCase{
"msi-root": {
value: map[string]interface{}{
in: map[string]interface{}{
"a": map[string]interface{}{
"red": true,
"blue": 123,
Expand All @@ -70,7 +70,7 @@ func TestWalkAttributePath(t *testing.T) {
},
},
"msi-full": {
value: map[string]interface{}{
in: map[string]interface{}{
"a": map[string]interface{}{
"red": true,
"blue": 123,
Expand All @@ -88,8 +88,180 @@ func TestWalkAttributePath(t *testing.T) {
},
expected: true,
},
"Object-AttributeName-Bool": {
in: Object{
AttributeTypes: map[string]Type{
"other": String,
"test": Bool,
},
},
path: &AttributePath{
steps: []AttributePathStep{
AttributeName("test"),
},
},
expected: Bool,
},
"Object-AttributeName-DynamicPseudoType": {
in: Object{
AttributeTypes: map[string]Type{
"other": Bool,
"test": DynamicPseudoType,
},
},
path: &AttributePath{
steps: []AttributePathStep{
AttributeName("test"),
},
},
expected: DynamicPseudoType,
},
"Object-AttributeName-List": {
in: Object{
AttributeTypes: map[string]Type{
"other": Bool,
"test": List{ElementType: String},
},
},
path: &AttributePath{
steps: []AttributePathStep{
AttributeName("test"),
},
},
expected: List{ElementType: String},
},
"Object-AttributeName-List-ElementKeyInt": {
in: Object{
AttributeTypes: map[string]Type{
"other": Bool,
"test": List{ElementType: String},
},
},
path: &AttributePath{
steps: []AttributePathStep{
AttributeName("test"),
ElementKeyInt(0),
},
},
expected: String,
},
"Object-AttributeName-Map": {
in: Object{
AttributeTypes: map[string]Type{
"other": Bool,
"test": Map{ElementType: String},
},
},
path: &AttributePath{
steps: []AttributePathStep{
AttributeName("test"),
},
},
expected: Map{ElementType: String},
},
"Object-AttributeName-Map-ElementKeyString": {
in: Object{
AttributeTypes: map[string]Type{
"other": Bool,
"test": Map{ElementType: String},
},
},
path: &AttributePath{
steps: []AttributePathStep{
AttributeName("test"),
ElementKeyString("sub-test"),
},
},
expected: String,
},
"Object-AttributeName-Number": {
in: Object{
AttributeTypes: map[string]Type{
"other": Bool,
"test": Number,
},
},
path: &AttributePath{
steps: []AttributePathStep{
AttributeName("test"),
},
},
expected: Number,
},
"Object-AttributeName-Set": {
in: Object{
AttributeTypes: map[string]Type{
"other": Bool,
"test": Set{ElementType: String},
},
},
path: &AttributePath{
steps: []AttributePathStep{
AttributeName("test"),
},
},
expected: Set{ElementType: String},
},
"Object-AttributeName-Set-ElementKeyValue": {
in: Object{
AttributeTypes: map[string]Type{
"other": Bool,
"test": Set{ElementType: String},
},
},
path: &AttributePath{
steps: []AttributePathStep{
AttributeName("test"),
ElementKeyValue(NewValue(String, "sub-test")),
},
},
expected: String,
},
"Object-AttributeName-String": {
in: Object{
AttributeTypes: map[string]Type{
"other": Bool,
"test": String,
},
},
path: &AttributePath{
steps: []AttributePathStep{
AttributeName("test"),
},
},
expected: String,
},
"Object-AttributeName-Tuple": {
in: Object{
AttributeTypes: map[string]Type{
"other": Bool,
"test": Tuple{ElementTypes: []Type{Bool, String}},
},
},
path: &AttributePath{
steps: []AttributePathStep{
AttributeName("test"),
},
},
expected: Tuple{ElementTypes: []Type{Bool, String}},
},
"Object-AttributeName-Tuple-ElementKeyInt": {
in: Object{
AttributeTypes: map[string]Type{
"other": Bool,
"test": Tuple{ElementTypes: []Type{Bool, String}},
},
},
path: &AttributePath{
steps: []AttributePathStep{
AttributeName("test"),
ElementKeyInt(1),
},
},
expected: String,
},
"slice-interface-root": {
value: []interface{}{
in: []interface{}{
map[string]interface{}{
"a": true,
"b": 123,
Expand Down Expand Up @@ -119,7 +291,7 @@ func TestWalkAttributePath(t *testing.T) {
},
},
"slice-interface-full": {
value: []interface{}{
in: []interface{}{
map[string]interface{}{
"a": true,
"b": 123,
Expand All @@ -144,7 +316,7 @@ func TestWalkAttributePath(t *testing.T) {
expected: "hello world",
},
"attributepathstepper": {
value: []interface{}{
in: []interface{}{
attributePathStepperTestStruct{
Name: "terraform",
Colors: []string{
Expand Down Expand Up @@ -173,7 +345,7 @@ func TestWalkAttributePath(t *testing.T) {
name, test := name, test
t.Run(name, func(t *testing.T) {
t.Parallel()
result, remaining, err := WalkAttributePath(test.value, test.path)
result, remaining, err := WalkAttributePath(test.in, test.path)
if err != nil {
t.Fatalf("error walking attribute path, %v still remains in the path: %s", remaining, err)
}
Expand Down
17 changes: 17 additions & 0 deletions tftypes/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,23 @@ type List struct {
_ []struct{}
}

// ApplyTerraform5AttributePathStep applies an AttributePathStep to a List,
// returning the Type found at that AttributePath within the List. If the
// AttributePathStep cannot be applied to the List, an ErrInvalidStep error
// will be returned.
func (l List) ApplyTerraform5AttributePathStep(step AttributePathStep) (interface{}, error) {
switch s := step.(type) {
case ElementKeyInt:
if int64(s) < 0 {
return nil, ErrInvalidStep
}

return l.ElementType, nil
default:
return nil, ErrInvalidStep
}
}

// Equal returns true if the two Lists are exactly equal. Unlike Is, passing in
// a List with no ElementType will always return false.
func (l List) Equal(o Type) bool {
Expand Down
73 changes: 72 additions & 1 deletion tftypes/list_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,77 @@
package tftypes

import "testing"
import (
"errors"
"testing"

"github.com/google/go-cmp/cmp"
)

func TestListApplyTerraform5AttributePathStep(t *testing.T) {
t.Parallel()

testCases := map[string]struct {
list List
step AttributePathStep
expectedType interface{}
expectedError error
}{
"AttributeName": {
list: List{},
step: AttributeName("test"),
expectedType: nil,
expectedError: ErrInvalidStep,
},
"ElementKeyInt-no-ElementType": {
list: List{},
step: ElementKeyInt(123),
expectedType: nil,
expectedError: nil,
},
"ElementKeyInt-ElementType-found": {
list: List{ElementType: String},
step: ElementKeyInt(123),
expectedType: String,
expectedError: nil,
},
"ElementKeyInt-ElementType-negative": {
list: List{ElementType: String},
step: ElementKeyInt(-1),
expectedType: nil,
expectedError: ErrInvalidStep,
},
"ElementKeyString": {
list: List{},
step: ElementKeyString("test"),
expectedType: nil,
expectedError: ErrInvalidStep,
},
"ElementKeyValue": {
list: List{},
step: ElementKeyValue(NewValue(String, "test")),
expectedType: nil,
expectedError: ErrInvalidStep,
},
}

for name, testCase := range testCases {
name, testCase := name, testCase

t.Run(name, func(t *testing.T) {
t.Parallel()

got, err := testCase.list.ApplyTerraform5AttributePathStep(testCase.step)

if !errors.Is(err, testCase.expectedError) {
t.Errorf("expected error %q, got %s", testCase.expectedError, err)
}

if diff := cmp.Diff(got, testCase.expectedType); diff != "" {
t.Errorf("unexpected difference: %s", diff)
}
})
}
}

func TestListEqual(t *testing.T) {
t.Parallel()
Expand Down
13 changes: 13 additions & 0 deletions tftypes/map.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,19 @@ type Map struct {
_ []struct{}
}

// ApplyTerraform5AttributePathStep applies an AttributePathStep to a Map,
// returning the Type found at that AttributePath within the Map. If the
// AttributePathStep cannot be applied to the Map, an ErrInvalidStep error
// will be returned.
func (m Map) ApplyTerraform5AttributePathStep(step AttributePathStep) (interface{}, error) {
switch step.(type) {
case ElementKeyString:
return m.ElementType, nil
default:
return nil, ErrInvalidStep
}
}

// Equal returns true if the two Maps are exactly equal. Unlike Is, passing in
// a Map with no ElementType will always return false.
func (m Map) Equal(o Type) bool {
Expand Down
Loading

0 comments on commit d55e48d

Please sign in to comment.