From d55e48d11a264e73668b1c8d2fdf8591aeeb73af Mon Sep 17 00:00:00 2001 From: Brian Flad Date: Wed, 9 Mar 2022 14:28:31 -0500 Subject: [PATCH 1/2] tftypes: Support AttributePathStepper interface in Type Reference: https://github.com/hashicorp/terraform-plugin-go/issues/110 This will allow implementations to step into `Type` via `AttributePath` similar to `Value`, which can simplify data handling logic. --- .changelog/pending.txt | 3 + tftypes/attribute_path.go | 6 +- tftypes/attribute_path_test.go | 186 +++++++++++++++++++++++++++++++-- tftypes/list.go | 17 +++ tftypes/list_test.go | 73 ++++++++++++- tftypes/map.go | 13 +++ tftypes/map_test.go | 67 +++++++++++- tftypes/object.go | 23 ++++ tftypes/object_test.go | 73 ++++++++++++- tftypes/primitive.go | 6 ++ tftypes/primitive_test.go | 129 +++++++++++++++++++++++ tftypes/set.go | 13 +++ tftypes/set_test.go | 67 +++++++++++- tftypes/tuple.go | 17 +++ tftypes/tuple_test.go | 79 +++++++++++++- tftypes/type.go | 6 ++ 16 files changed, 763 insertions(+), 15 deletions(-) create mode 100644 .changelog/pending.txt diff --git a/.changelog/pending.txt b/.changelog/pending.txt new file mode 100644 index 00000000..8011eb29 --- /dev/null +++ b/.changelog/pending.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +tftypes: Added `Type` support to `WalkAttributePath()` function +``` diff --git a/tftypes/attribute_path.go b/tftypes/attribute_path.go index fd91f96a..d6108d4a 100644 --- a/tftypes/attribute_path.go +++ b/tftypes/attribute_path.go @@ -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 diff --git a/tftypes/attribute_path_test.go b/tftypes/attribute_path_test.go index 623c2485..5e041ac0 100644 --- a/tftypes/attribute_path_test.go +++ b/tftypes/attribute_path_test.go @@ -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, @@ -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, @@ -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, @@ -119,7 +291,7 @@ func TestWalkAttributePath(t *testing.T) { }, }, "slice-interface-full": { - value: []interface{}{ + in: []interface{}{ map[string]interface{}{ "a": true, "b": 123, @@ -144,7 +316,7 @@ func TestWalkAttributePath(t *testing.T) { expected: "hello world", }, "attributepathstepper": { - value: []interface{}{ + in: []interface{}{ attributePathStepperTestStruct{ Name: "terraform", Colors: []string{ @@ -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) } diff --git a/tftypes/list.go b/tftypes/list.go index 820efbcd..a0bc2f37 100644 --- a/tftypes/list.go +++ b/tftypes/list.go @@ -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 { diff --git a/tftypes/list_test.go b/tftypes/list_test.go index db4a2ca0..d2e37bf9 100644 --- a/tftypes/list_test.go +++ b/tftypes/list_test.go @@ -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() diff --git a/tftypes/map.go b/tftypes/map.go index ecc6bed0..7c1e7d05 100644 --- a/tftypes/map.go +++ b/tftypes/map.go @@ -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 { diff --git a/tftypes/map_test.go b/tftypes/map_test.go index 7e44ef47..7e6981e6 100644 --- a/tftypes/map_test.go +++ b/tftypes/map_test.go @@ -1,6 +1,71 @@ package tftypes -import "testing" +import ( + "errors" + "testing" + + "github.com/google/go-cmp/cmp" +) + +func TestMapApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + m Map + step AttributePathStep + expectedType interface{} + expectedError error + }{ + "AttributeName": { + m: Map{}, + step: AttributeName("test"), + expectedType: nil, + expectedError: ErrInvalidStep, + }, + "ElementKeyInt": { + m: Map{}, + step: ElementKeyInt(123), + expectedType: nil, + expectedError: ErrInvalidStep, + }, + "ElementKeyString-no-ElementType": { + m: Map{}, + step: ElementKeyString("test"), + expectedType: nil, + expectedError: nil, + }, + "ElementKeyString-ElementType": { + m: Map{ElementType: String}, + step: ElementKeyString("test"), + expectedType: String, + expectedError: nil, + }, + "ElementKeyValue": { + m: Map{}, + 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.m.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 TestMapEqual(t *testing.T) { t.Parallel() diff --git a/tftypes/object.go b/tftypes/object.go index ef3a622b..ce2bf132 100644 --- a/tftypes/object.go +++ b/tftypes/object.go @@ -44,6 +44,29 @@ type Object struct { _ []struct{} } +// ApplyTerraform5AttributePathStep applies an AttributePathStep to an Object, +// returning the Type found at that AttributePath within the Object. If the +// AttributePathStep cannot be applied to the Object, an ErrInvalidStep error +// will be returned. +func (o Object) ApplyTerraform5AttributePathStep(step AttributePathStep) (interface{}, error) { + switch s := step.(type) { + case AttributeName: + if len(o.AttributeTypes) == 0 { + return nil, ErrInvalidStep + } + + attrType, ok := o.AttributeTypes[string(s)] + + if !ok { + return nil, ErrInvalidStep + } + + return attrType, nil + default: + return nil, ErrInvalidStep + } +} + // Equal returns true if the two Objects are exactly equal. Unlike Is, passing // in an Object with no AttributeTypes will always return false. func (o Object) Equal(other Type) bool { diff --git a/tftypes/object_test.go b/tftypes/object_test.go index 418664ac..0955bbc2 100644 --- a/tftypes/object_test.go +++ b/tftypes/object_test.go @@ -1,6 +1,77 @@ package tftypes -import "testing" +import ( + "errors" + "testing" + + "github.com/google/go-cmp/cmp" +) + +func TestObjectApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + object Object + step AttributePathStep + expectedType interface{} + expectedError error + }{ + "AttributeName-no-AttributeTypes": { + object: Object{}, + step: AttributeName("test"), + expectedType: nil, + expectedError: ErrInvalidStep, + }, + "AttributeName-AttributeTypes-found": { + object: Object{AttributeTypes: map[string]Type{"test": String}}, + step: AttributeName("test"), + expectedType: String, + expectedError: nil, + }, + "AttributeName-AttributeTypes-not-found": { + object: Object{AttributeTypes: map[string]Type{"other": String}}, + step: AttributeName("test"), + expectedType: nil, + expectedError: ErrInvalidStep, + }, + "ElementKeyInt": { + object: Object{}, + step: ElementKeyInt(123), + expectedType: nil, + expectedError: ErrInvalidStep, + }, + "ElementKeyString": { + object: Object{}, + step: ElementKeyString("test"), + expectedType: nil, + expectedError: ErrInvalidStep, + }, + "ElementKeyValue": { + object: Object{}, + 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.object.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 TestObjectEqual(t *testing.T) { t.Parallel() diff --git a/tftypes/primitive.go b/tftypes/primitive.go index 26cfe839..0349f414 100644 --- a/tftypes/primitive.go +++ b/tftypes/primitive.go @@ -37,6 +37,12 @@ type primitive struct { _ []struct{} } +// ApplyTerraform5AttributePathStep always returns an ErrInvalidStep error +// as it is invalid to step into a primitive. +func (p primitive) ApplyTerraform5AttributePathStep(step AttributePathStep) (interface{}, error) { + return nil, ErrInvalidStep +} + func (p primitive) Equal(o Type) bool { v, ok := o.(primitive) if !ok { diff --git a/tftypes/primitive_test.go b/tftypes/primitive_test.go index 5536e095..66686003 100644 --- a/tftypes/primitive_test.go +++ b/tftypes/primitive_test.go @@ -1,9 +1,138 @@ package tftypes import ( + "errors" "testing" + + "github.com/google/go-cmp/cmp" ) +func TestPrimitiveApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + primitive primitive + step AttributePathStep + expectedType interface{} + expectedError error + }{ + "Bool-AttributeName": { + primitive: Bool, + step: AttributeName("test"), + expectedType: nil, + expectedError: ErrInvalidStep, + }, + "Bool-ElementKeyInt": { + primitive: Bool, + step: ElementKeyInt(123), + expectedType: nil, + expectedError: ErrInvalidStep, + }, + "Bool-ElementKeyString": { + primitive: Bool, + step: ElementKeyString("test"), + expectedType: nil, + expectedError: ErrInvalidStep, + }, + "Bool-ElementKeyValue": { + primitive: Bool, + step: ElementKeyValue(NewValue(String, "test")), + expectedType: nil, + expectedError: ErrInvalidStep, + }, + "DynamicPseudoType-AttributeName": { + primitive: DynamicPseudoType, + step: AttributeName("test"), + expectedType: nil, + expectedError: ErrInvalidStep, + }, + "DynamicPseudoType-ElementKeyInt": { + primitive: DynamicPseudoType, + step: ElementKeyInt(123), + expectedType: nil, + expectedError: ErrInvalidStep, + }, + "DynamicPseudoType-ElementKeyString": { + primitive: DynamicPseudoType, + step: ElementKeyString("test"), + expectedType: nil, + expectedError: ErrInvalidStep, + }, + "DynamicPseudoType-ElementKeyValue": { + primitive: DynamicPseudoType, + step: ElementKeyValue(NewValue(String, "test")), + expectedType: nil, + expectedError: ErrInvalidStep, + }, + "Number-AttributeName": { + primitive: Number, + step: AttributeName("test"), + expectedType: nil, + expectedError: ErrInvalidStep, + }, + "Number-ElementKeyInt": { + primitive: Number, + step: ElementKeyInt(123), + expectedType: nil, + expectedError: ErrInvalidStep, + }, + "Number-ElementKeyString": { + primitive: Number, + step: ElementKeyString("test"), + expectedType: nil, + expectedError: ErrInvalidStep, + }, + "Number-ElementKeyValue": { + primitive: Number, + step: ElementKeyValue(NewValue(String, "test")), + expectedType: nil, + expectedError: ErrInvalidStep, + }, + "String-AttributeName": { + primitive: String, + step: AttributeName("test"), + expectedType: nil, + expectedError: ErrInvalidStep, + }, + "String-ElementKeyInt": { + primitive: String, + step: ElementKeyInt(123), + expectedType: nil, + expectedError: ErrInvalidStep, + }, + "String-ElementKeyString": { + primitive: String, + step: ElementKeyString("test"), + expectedType: nil, + expectedError: ErrInvalidStep, + }, + "String-ElementKeyValue": { + primitive: String, + 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.primitive.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 TestPrimitiveEqual(t *testing.T) { type testCase struct { p1 primitive diff --git a/tftypes/set.go b/tftypes/set.go index 28ab23b7..00163067 100644 --- a/tftypes/set.go +++ b/tftypes/set.go @@ -15,6 +15,19 @@ type Set struct { _ []struct{} } +// ApplyTerraform5AttributePathStep applies an AttributePathStep to a Set, +// returning the Type found at that AttributePath within the Set. If the +// AttributePathStep cannot be applied to the Set, an ErrInvalidStep error +// will be returned. +func (s Set) ApplyTerraform5AttributePathStep(step AttributePathStep) (interface{}, error) { + switch step.(type) { + case ElementKeyValue: + return s.ElementType, nil + default: + return nil, ErrInvalidStep + } +} + // Equal returns true if the two Sets are exactly equal. Unlike Is, passing in // a Set with no ElementType will always return false. func (s Set) Equal(o Type) bool { diff --git a/tftypes/set_test.go b/tftypes/set_test.go index 3c67403a..abe9d7d6 100644 --- a/tftypes/set_test.go +++ b/tftypes/set_test.go @@ -1,6 +1,71 @@ package tftypes -import "testing" +import ( + "errors" + "testing" + + "github.com/google/go-cmp/cmp" +) + +func TestSetApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + set Set + step AttributePathStep + expectedType interface{} + expectedError error + }{ + "AttributeName": { + set: Set{}, + step: AttributeName("test"), + expectedType: nil, + expectedError: ErrInvalidStep, + }, + "ElementKeyInt": { + set: Set{}, + step: ElementKeyInt(123), + expectedType: nil, + expectedError: ErrInvalidStep, + }, + "ElementKeyString": { + set: Set{}, + step: ElementKeyString("test"), + expectedType: nil, + expectedError: ErrInvalidStep, + }, + "ElementKeyValue-no-ElementType": { + set: Set{}, + step: ElementKeyValue(NewValue(String, "test")), + expectedType: nil, + expectedError: nil, + }, + "ElementKeyValue-ElementType": { + set: Set{ElementType: String}, + step: ElementKeyValue(NewValue(String, "test")), + expectedType: String, + expectedError: nil, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.set.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 TestSetEqual(t *testing.T) { t.Parallel() diff --git a/tftypes/tuple.go b/tftypes/tuple.go index df57d90d..5ca5709a 100644 --- a/tftypes/tuple.go +++ b/tftypes/tuple.go @@ -19,6 +19,23 @@ type Tuple struct { _ []struct{} } +// ApplyTerraform5AttributePathStep applies an AttributePathStep to a Tuple, +// returning the Type found at that AttributePath within the Tuple. If the +// AttributePathStep cannot be applied to the Tuple, an ErrInvalidStep error +// will be returned. +func (tu Tuple) ApplyTerraform5AttributePathStep(step AttributePathStep) (interface{}, error) { + switch s := step.(type) { + case ElementKeyInt: + if int64(s) < 0 || int64(s) >= int64(len(tu.ElementTypes)) { + return nil, ErrInvalidStep + } + + return tu.ElementTypes[int64(s)], nil + default: + return nil, ErrInvalidStep + } +} + // Equal returns true if the two Tuples are exactly equal. Unlike Is, passing // in a Tuple with no ElementTypes will always return false. func (tu Tuple) Equal(o Type) bool { diff --git a/tftypes/tuple_test.go b/tftypes/tuple_test.go index 4a1a288c..c21aafaf 100644 --- a/tftypes/tuple_test.go +++ b/tftypes/tuple_test.go @@ -1,6 +1,83 @@ package tftypes -import "testing" +import ( + "errors" + "testing" + + "github.com/google/go-cmp/cmp" +) + +func TestTupleApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + tuple Tuple + step AttributePathStep + expectedType interface{} + expectedError error + }{ + "AttributeName": { + tuple: Tuple{}, + step: AttributeName("test"), + expectedType: nil, + expectedError: ErrInvalidStep, + }, + "ElementKeyInt-no-ElementTypes": { + tuple: Tuple{}, + step: ElementKeyInt(123), + expectedType: nil, + expectedError: ErrInvalidStep, + }, + "ElementKeyInt-ElementTypes-found": { + tuple: Tuple{ElementTypes: []Type{String}}, + step: ElementKeyInt(0), + expectedType: String, + expectedError: nil, + }, + "ElementKeyInt-ElementTypes-negative": { + tuple: Tuple{ElementTypes: []Type{String}}, + step: ElementKeyInt(-1), + expectedType: nil, + expectedError: ErrInvalidStep, + }, + "ElementKeyInt-ElementTypes-overflow": { + tuple: Tuple{ElementTypes: []Type{String}}, + step: ElementKeyInt(1), + expectedType: nil, + expectedError: ErrInvalidStep, + }, + "ElementKeyString": { + tuple: Tuple{}, + step: ElementKeyString("test"), + expectedType: nil, + expectedError: ErrInvalidStep, + }, + "ElementKeyValue": { + tuple: Tuple{}, + 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.tuple.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 TestTupleEqual(t *testing.T) { t.Parallel() diff --git a/tftypes/type.go b/tftypes/type.go index 955e322f..4f0e4af7 100644 --- a/tftypes/type.go +++ b/tftypes/type.go @@ -12,6 +12,12 @@ import ( // implemented by the tftypes package. Types define the shape and // characteristics of data coming from or being sent to Terraform. type Type interface { + // AttributePathStepper requires each Type to implement the + // ApplyTerraform5AttributePathStep method, so Type is compatible with + // WalkAttributePath. The method should return the Type found at that + // AttributePath within the Type or ErrInvalidStep. + AttributePathStepper + // Is is used to determine what type a Type implementation is. It is // the recommended method for determining whether two types are // equivalent or not. From a5fff28fa2418917d40bdf3121da244787497c79 Mon Sep 17 00:00:00 2001 From: Brian Flad Date: Wed, 9 Mar 2022 14:31:03 -0500 Subject: [PATCH 2/2] Update CHANGELOG for #163 --- .changelog/{pending.txt => 163.txt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .changelog/{pending.txt => 163.txt} (100%) diff --git a/.changelog/pending.txt b/.changelog/163.txt similarity index 100% rename from .changelog/pending.txt rename to .changelog/163.txt