From e8f62d70a0bf710efb6d3c447f63b19feb0e636e Mon Sep 17 00:00:00 2001 From: Brian Flad Date: Tue, 28 Sep 2021 10:15:47 -0400 Subject: [PATCH] Revert Schema.AttributeAtPath implicit parent Attribute handling and add unit testing --- tfsdk/schema.go | 12 +- tfsdk/schema_test.go | 771 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 773 insertions(+), 10 deletions(-) diff --git a/tfsdk/schema.go b/tfsdk/schema.go index 0d04ae8e8..0ecccdf89 100644 --- a/tfsdk/schema.go +++ b/tfsdk/schema.go @@ -122,16 +122,8 @@ func (s Schema) AttributeAtPath(path *tftypes.AttributePath) (Attribute, error) return Attribute{}, ErrPathInsideAtomicAttribute } - // list, set, and map nested attributes all return a - // map[string]Attribute when the path points to an element in the list, - // set, or path. We need it to point to an attribute specifically, so - // we're choosing to use the parent attribute in this case, which is - // usually what we want. - if len(path.Steps()) > 1 { - lastStep := path.Steps()[len(path.Steps())-1] - if _, ok := lastStep.(tftypes.AttributeName); !ok { - return s.AttributeAtPath(path.WithoutLastStep()) - } + if _, ok := res.(nestedAttributes); ok { + return Attribute{}, ErrPathInsideAtomicAttribute } a, ok := res.(Attribute) diff --git a/tfsdk/schema_test.go b/tfsdk/schema_test.go index b1741a0ff..245da709c 100644 --- a/tfsdk/schema_test.go +++ b/tfsdk/schema_test.go @@ -12,6 +12,777 @@ import ( "github.com/hashicorp/terraform-plugin-go/tftypes" ) +func TestSchemaAttributeAtPath(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + schema Schema + path *tftypes.AttributePath + expected Attribute + expectedErr string + }{ + "empty-root": { + schema: Schema{}, + path: tftypes.NewAttributePath(), + expected: Attribute{}, + expectedErr: "got unexpected type tfsdk.Schema", + }, + "empty-nil": { + schema: Schema{}, + path: nil, + expected: Attribute{}, + expectedErr: "got unexpected type tfsdk.Schema", + }, + "root": { + schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Type: types.StringType, + Required: true, + }, + }, + }, + path: tftypes.NewAttributePath(), + expected: Attribute{}, + expectedErr: "got unexpected type tfsdk.Schema", + }, + "nil": { + schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Type: types.StringType, + Required: true, + }, + }, + }, + path: nil, + expected: Attribute{}, + expectedErr: "got unexpected type tfsdk.Schema", + }, + "WithAttributeName": { + schema: Schema{ + Attributes: map[string]Attribute{ + "other": { + Type: types.BoolType, + Optional: true, + }, + "test": { + Type: types.StringType, + Required: true, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test"), + expected: Attribute{ + Type: types.StringType, + Required: true, + }, + }, + "WithAttributeName-ListNestedAttributes-WithAttributeName": { + schema: Schema{ + Attributes: map[string]Attribute{ + "other": { + Type: types.BoolType, + Optional: true, + }, + "test": { + Attributes: ListNestedAttributes(map[string]Attribute{ + "other": { + Type: types.BoolType, + Optional: true, + }, + "sub_test": { + Type: types.StringType, + Required: true, + }, + }, ListNestedAttributesOptions{}), + Required: true, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test").WithAttributeName("sub_test"), + expected: Attribute{}, + expectedErr: "AttributeName(\"sub_test\") still remains in the path: can't apply tftypes.AttributeName to ListNestedAttributes", + }, + "WithAttributeName-ListNestedAttributes-WithElementKeyInt": { + schema: Schema{ + Attributes: map[string]Attribute{ + "other": { + Type: types.BoolType, + Optional: true, + }, + "test": { + Attributes: ListNestedAttributes(map[string]Attribute{ + "other": { + Type: types.BoolType, + Optional: true, + }, + "sub_test": { + Type: types.StringType, + Required: true, + }, + }, ListNestedAttributesOptions{}), + Required: true, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test").WithElementKeyInt(0), + expected: Attribute{}, + expectedErr: ErrPathInsideAtomicAttribute.Error(), + }, + "WithAttributeName-ListNestedAttributes-WithElementKeyInt-WithAttributeName": { + schema: Schema{ + Attributes: map[string]Attribute{ + "other": { + Type: types.BoolType, + Optional: true, + }, + "test": { + Attributes: ListNestedAttributes(map[string]Attribute{ + "other": { + Type: types.BoolType, + Optional: true, + }, + "sub_test": { + Type: types.StringType, + Required: true, + }, + }, ListNestedAttributesOptions{}), + Required: true, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test").WithElementKeyInt(0).WithAttributeName("sub_test"), + expected: Attribute{ + Type: types.StringType, + Required: true, + }, + }, + "WithAttributeName-ListNestedAttributes-WithElementKeyString": { + schema: Schema{ + Attributes: map[string]Attribute{ + "other": { + Type: types.BoolType, + Optional: true, + }, + "test": { + Attributes: ListNestedAttributes(map[string]Attribute{ + "other": { + Type: types.BoolType, + Optional: true, + }, + "sub_test": { + Type: types.StringType, + Required: true, + }, + }, ListNestedAttributesOptions{}), + Required: true, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test").WithElementKeyString("sub_test"), + expected: Attribute{}, + expectedErr: "ElementKeyString(\"sub_test\") still remains in the path: can't apply tftypes.ElementKeyString to ListNestedAttributes", + }, + "WithAttributeName-ListNestedAttributes-WithElementKeyValue": { + schema: Schema{ + Attributes: map[string]Attribute{ + "other": { + Type: types.BoolType, + Optional: true, + }, + "test": { + Attributes: ListNestedAttributes(map[string]Attribute{ + "other": { + Type: types.BoolType, + Optional: true, + }, + "sub_test": { + Type: types.StringType, + Required: true, + }, + }, ListNestedAttributesOptions{}), + Required: true, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test").WithElementKeyValue(tftypes.NewValue(tftypes.String, "sub_test")), + expected: Attribute{}, + expectedErr: "ElementKeyValue(tftypes.String<\"sub_test\">) still remains in the path: can't apply tftypes.ElementKeyValue to ListNestedAttributes", + }, + "WithAttributeName-MapNestedAttributes-WithAttributeName": { + schema: Schema{ + Attributes: map[string]Attribute{ + "other": { + Type: types.BoolType, + Optional: true, + }, + "test": { + Attributes: MapNestedAttributes(map[string]Attribute{ + "other": { + Type: types.BoolType, + Optional: true, + }, + "sub_test": { + Type: types.StringType, + Required: true, + }, + }, MapNestedAttributesOptions{}), + Required: true, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test").WithAttributeName("sub_test"), + expected: Attribute{}, + expectedErr: "AttributeName(\"sub_test\") still remains in the path: can't use tftypes.AttributeName on maps", + }, + "WithAttributeName-MapNestedAttributes-WithElementKeyInt": { + schema: Schema{ + Attributes: map[string]Attribute{ + "other": { + Type: types.BoolType, + Optional: true, + }, + "test": { + Attributes: MapNestedAttributes(map[string]Attribute{ + "other": { + Type: types.BoolType, + Optional: true, + }, + "sub_test": { + Type: types.StringType, + Required: true, + }, + }, MapNestedAttributesOptions{}), + Required: true, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test").WithElementKeyInt(0), + expected: Attribute{}, + expectedErr: "ElementKeyInt(0) still remains in the path: can't use tftypes.ElementKeyInt on maps", + }, + "WithAttributeName-MapNestedAttributes-WithElementKeyString": { + schema: Schema{ + Attributes: map[string]Attribute{ + "other": { + Type: types.BoolType, + Optional: true, + }, + "test": { + Attributes: MapNestedAttributes(map[string]Attribute{ + "other": { + Type: types.BoolType, + Optional: true, + }, + "sub_test": { + Type: types.StringType, + Required: true, + }, + }, MapNestedAttributesOptions{}), + Required: true, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test").WithElementKeyString("sub_test"), + expected: Attribute{}, + expectedErr: ErrPathInsideAtomicAttribute.Error(), + }, + "WithAttributeName-MapNestedAttributes-WithElementKeyString-WithAttributeName": { + schema: Schema{ + Attributes: map[string]Attribute{ + "other": { + Type: types.BoolType, + Optional: true, + }, + "test": { + Attributes: MapNestedAttributes(map[string]Attribute{ + "other": { + Type: types.BoolType, + Optional: true, + }, + "sub_test": { + Type: types.StringType, + Required: true, + }, + }, MapNestedAttributesOptions{}), + Required: true, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test").WithElementKeyString("element").WithAttributeName("sub_test"), + expected: Attribute{ + Type: types.StringType, + Required: true, + }, + }, + "WithAttributeName-MapNestedAttributes-WithElementKeyValue": { + schema: Schema{ + Attributes: map[string]Attribute{ + "other": { + Type: types.BoolType, + Optional: true, + }, + "test": { + Attributes: MapNestedAttributes(map[string]Attribute{ + "other": { + Type: types.BoolType, + Optional: true, + }, + "sub_test": { + Type: types.StringType, + Required: true, + }, + }, MapNestedAttributesOptions{}), + Required: true, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test").WithElementKeyValue(tftypes.NewValue(tftypes.String, "sub_test")), + expected: Attribute{}, + expectedErr: "ElementKeyValue(tftypes.String<\"sub_test\">) still remains in the path: can't use tftypes.ElementKeyValue on maps", + }, + "WithAttributeName-SetNestedAttributes-WithAttributeName": { + schema: Schema{ + Attributes: map[string]Attribute{ + "other": { + Type: types.BoolType, + Optional: true, + }, + "test": { + Attributes: SetNestedAttributes(map[string]Attribute{ + "other": { + Type: types.BoolType, + Optional: true, + }, + "sub_test": { + Type: types.StringType, + Required: true, + }, + }, SetNestedAttributesOptions{}), + Required: true, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test").WithAttributeName("sub_test"), + expected: Attribute{}, + expectedErr: "AttributeName(\"sub_test\") still remains in the path: can't use tftypes.AttributeName on sets", + }, + "WithAttributeName-SetNestedAttributes-WithElementKeyInt": { + schema: Schema{ + Attributes: map[string]Attribute{ + "other": { + Type: types.BoolType, + Optional: true, + }, + "test": { + Attributes: SetNestedAttributes(map[string]Attribute{ + "other": { + Type: types.BoolType, + Optional: true, + }, + "sub_test": { + Type: types.StringType, + Required: true, + }, + }, SetNestedAttributesOptions{}), + Required: true, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test").WithElementKeyInt(0), + expected: Attribute{}, + expectedErr: "ElementKeyInt(0) still remains in the path: can't use tftypes.ElementKeyInt on sets", + }, + "WithAttributeName-SetNestedAttributes-WithElementKeyString": { + schema: Schema{ + Attributes: map[string]Attribute{ + "other": { + Type: types.BoolType, + Optional: true, + }, + "test": { + Attributes: SetNestedAttributes(map[string]Attribute{ + "other": { + Type: types.BoolType, + Optional: true, + }, + "sub_test": { + Type: types.StringType, + Required: true, + }, + }, SetNestedAttributesOptions{}), + Required: true, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test").WithElementKeyString("sub_test"), + expected: Attribute{}, + expectedErr: "ElementKeyString(\"sub_test\") still remains in the path: can't use tftypes.ElementKeyString on sets", + }, + "WithAttributeName-SetNestedAttributes-WithElementKeyValue": { + schema: Schema{ + Attributes: map[string]Attribute{ + "other": { + Type: types.BoolType, + Optional: true, + }, + "test": { + Attributes: SetNestedAttributes(map[string]Attribute{ + "other": { + Type: types.BoolType, + Optional: true, + }, + "sub_test": { + Type: types.StringType, + Required: true, + }, + }, SetNestedAttributesOptions{}), + Required: true, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test").WithElementKeyValue(tftypes.NewValue(tftypes.String, "sub_test")), + expected: Attribute{}, + expectedErr: ErrPathInsideAtomicAttribute.Error(), + }, + "WithAttributeName-SetNestedAttributes-WithElementKeyValue-WithAttributeName": { + schema: Schema{ + Attributes: map[string]Attribute{ + "other": { + Type: types.BoolType, + Optional: true, + }, + "test": { + Attributes: SetNestedAttributes(map[string]Attribute{ + "other": { + Type: types.BoolType, + Optional: true, + }, + "sub_test": { + Type: types.StringType, + Required: true, + }, + }, SetNestedAttributesOptions{}), + Required: true, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test").WithElementKeyValue(tftypes.NewValue(tftypes.String, "element")).WithAttributeName("sub_test"), + expected: Attribute{ + Type: types.StringType, + Required: true, + }, + }, + "WithAttributeName-SingleNestedAttributes-WithAttributeName": { + schema: Schema{ + Attributes: map[string]Attribute{ + "other": { + Type: types.BoolType, + Optional: true, + }, + "test": { + Attributes: SingleNestedAttributes(map[string]Attribute{ + "other": { + Type: types.BoolType, + Optional: true, + }, + "sub_test": { + Type: types.StringType, + Required: true, + }, + }), + Required: true, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test").WithAttributeName("sub_test"), + expected: Attribute{ + Type: types.StringType, + Required: true, + }, + }, + "WithAttributeName-SingleNestedAttributes-WithElementKeyInt": { + schema: Schema{ + Attributes: map[string]Attribute{ + "other": { + Type: types.BoolType, + Optional: true, + }, + "test": { + Attributes: SingleNestedAttributes(map[string]Attribute{ + "other": { + Type: types.BoolType, + Optional: true, + }, + "sub_test": { + Type: types.StringType, + Required: true, + }, + }), + Required: true, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test").WithElementKeyInt(0), + expected: Attribute{}, + expectedErr: "ElementKeyInt(0) still remains in the path: can't apply tftypes.ElementKeyInt to Attributes", + }, + "WithAttributeName-SingleNestedAttributes-WithElementKeyString": { + schema: Schema{ + Attributes: map[string]Attribute{ + "other": { + Type: types.BoolType, + Optional: true, + }, + "test": { + Attributes: SingleNestedAttributes(map[string]Attribute{ + "other": { + Type: types.BoolType, + Optional: true, + }, + "sub_test": { + Type: types.StringType, + Required: true, + }, + }), + Required: true, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test").WithElementKeyString("sub_test"), + expected: Attribute{}, + expectedErr: "ElementKeyString(\"sub_test\") still remains in the path: can't apply tftypes.ElementKeyString to Attributes", + }, + "WithAttributeName-SingleNestedAttributes-WithElementKeyValue": { + schema: Schema{ + Attributes: map[string]Attribute{ + "other": { + Type: types.BoolType, + Optional: true, + }, + "test": { + Attributes: SingleNestedAttributes(map[string]Attribute{ + "other": { + Type: types.BoolType, + Optional: true, + }, + "sub_test": { + Type: types.StringType, + Required: true, + }, + }), + Required: true, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test").WithElementKeyValue(tftypes.NewValue(tftypes.String, "sub_test")), + expected: Attribute{}, + expectedErr: "ElementKeyValue(tftypes.String<\"sub_test\">) still remains in the path: can't apply tftypes.ElementKeyValue to Attributes", + }, + "WithAttributeName-Object-WithAttributeName": { + schema: Schema{ + Attributes: map[string]Attribute{ + "other": { + Type: types.BoolType, + Optional: true, + }, + "test": { + Type: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "sub_test": types.StringType, + }, + }, + Required: true, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test").WithAttributeName("sub_test"), + expected: Attribute{}, + expectedErr: ErrPathInsideAtomicAttribute.Error(), + }, + "WithAttributeName-WithElementKeyInt-invalid-parent": { + schema: Schema{ + Attributes: map[string]Attribute{ + "other": { + Type: types.BoolType, + Optional: true, + }, + "test": { + Type: types.StringType, + Required: true, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test").WithElementKeyInt(0), + expected: Attribute{}, + expectedErr: "ElementKeyInt(0) still remains in the path: cannot apply AttributePathStep tftypes.ElementKeyInt to types.StringType", + }, + "WithAttributeName-WithElementKeyInt-valid-parent": { + schema: Schema{ + Attributes: map[string]Attribute{ + "other": { + Type: types.BoolType, + Optional: true, + }, + "test": { + Type: types.ListType{ + ElemType: types.StringType, + }, + Required: true, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test").WithElementKeyInt(0), + expected: Attribute{}, + expectedErr: ErrPathInsideAtomicAttribute.Error(), + }, + "WithAttributeName-WithElementKeyString-invalid-parent": { + schema: Schema{ + Attributes: map[string]Attribute{ + "other": { + Type: types.BoolType, + Optional: true, + }, + "test": { + Type: types.StringType, + Required: true, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test").WithElementKeyString("element"), + expected: Attribute{}, + expectedErr: "ElementKeyString(\"element\") still remains in the path: cannot apply AttributePathStep tftypes.ElementKeyString to types.StringType", + }, + "WithAttributeName-WithElementKeyString-valid-parent": { + schema: Schema{ + Attributes: map[string]Attribute{ + "other": { + Type: types.BoolType, + Optional: true, + }, + "test": { + Type: types.MapType{ + ElemType: types.StringType, + }, + Required: true, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test").WithElementKeyString("element"), + expected: Attribute{}, + expectedErr: ErrPathInsideAtomicAttribute.Error(), + }, + "WithAttributeName-WithElementKeyValue-invalid-parent": { + schema: Schema{ + Attributes: map[string]Attribute{ + "other": { + Type: types.BoolType, + Optional: true, + }, + "test": { + Type: types.StringType, + Required: true, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test").WithElementKeyValue(tftypes.NewValue(tftypes.String, "element")), + expected: Attribute{}, + expectedErr: "ElementKeyValue(tftypes.String<\"element\">) still remains in the path: cannot apply AttributePathStep tftypes.ElementKeyValue to types.StringType", + }, + "WithAttributeName-WithElementKeyValue-valid-parent": { + schema: Schema{ + Attributes: map[string]Attribute{ + "other": { + Type: types.BoolType, + Optional: true, + }, + "test": { + Type: types.SetType{ + ElemType: types.StringType, + }, + Required: true, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test").WithElementKeyValue(tftypes.NewValue(tftypes.String, "element")), + expected: Attribute{}, + expectedErr: ErrPathInsideAtomicAttribute.Error(), + }, + "WithElementKeyInt": { + schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Type: types.StringType, + Required: true, + }, + }, + }, + path: tftypes.NewAttributePath().WithElementKeyInt(0), + expected: Attribute{}, + expectedErr: "ElementKeyInt(0) still remains in the path: cannot apply AttributePathStep tftypes.ElementKeyInt to schema", + }, + "WithElementKeyString": { + schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Type: types.StringType, + Required: true, + }, + }, + }, + path: tftypes.NewAttributePath().WithElementKeyString("test"), + expected: Attribute{}, + expectedErr: "ElementKeyString(\"test\") still remains in the path: cannot apply AttributePathStep tftypes.ElementKeyString to schema", + }, + "WithElementKeyValue": { + schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Type: types.StringType, + Required: true, + }, + }, + }, + path: tftypes.NewAttributePath().WithElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: Attribute{}, + expectedErr: "ElementKeyValue(tftypes.String<\"test\">) still remains in the path: cannot apply AttributePathStep tftypes.ElementKeyValue to schema", + }, + } + + for name, tc := range testCases { + name, tc := name, tc + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := tc.schema.AttributeAtPath(tc.path) + + if err != nil { + if tc.expectedErr == "" { + t.Errorf("Unexpected error: %s", err) + return + } + if err.Error() != tc.expectedErr { + t.Errorf("Expected error to be %q, got %q", tc.expectedErr, err.Error()) + return + } + // got expected error + return + } + + if err == nil && tc.expectedErr != "" { + t.Errorf("Expected error to be %q, got nil", tc.expectedErr) + return + } + + if diff := cmp.Diff(got, tc.expected); diff != "" { + t.Errorf("Unexpected result (+wanted, -got): %s", diff) + } + }) + } +} + func TestSchemaAttributeType(t *testing.T) { testSchema := Schema{ Attributes: map[string]Attribute{