From 03d69488cd8bd063f4bd9a712280f395f1414052 Mon Sep 17 00:00:00 2001 From: Brian Flad Date: Thu, 2 Sep 2021 12:05:40 -0400 Subject: [PATCH 1/8] types: Initial copy of List and ListType to Set and SetType This initial copy does not include a real set implementation or handling for duplicate elements, however this sets the stage for expanding testing to cover the use case. --- .changelog/pending.txt | 3 + internal/reflect/struct_test.go | 198 ++++++++- tfsdk/attribute.go | 45 +- tfsdk/attribute_test.go | 135 +++++- tfsdk/nested_attributes.go | 5 +- tfsdk/schema.go | 33 +- tfsdk/schema_test.go | 10 +- tfsdk/serve_provider_test.go | 102 ++++- tfsdk/serve_test.go | 132 ++++++ tfsdk/state_test.go | 709 ++++++++++++++++++++++++++++++-- types/object_test.go | 62 +++ types/set.go | 217 ++++++++++ types/set_test.go | 646 +++++++++++++++++++++++++++++ 13 files changed, 2242 insertions(+), 55 deletions(-) create mode 100644 .changelog/pending.txt create mode 100644 types/set.go create mode 100644 types/set_test.go diff --git a/.changelog/pending.txt b/.changelog/pending.txt new file mode 100644 index 000000000..663a0d6e3 --- /dev/null +++ b/.changelog/pending.txt @@ -0,0 +1,3 @@ +```release-note:feature +types: Support `Set` and `SetType` +``` diff --git a/internal/reflect/struct_test.go b/internal/reflect/struct_test.go index b28cf3037..2785ce482 100644 --- a/internal/reflect/struct_test.go +++ b/internal/reflect/struct_test.go @@ -205,11 +205,16 @@ func TestNewStruct_complex(t *testing.T) { t.Parallel() type myStruct struct { - Slice []string `tfsdk:"slice"` - SliceOfStructs []struct { + ListSlice []string `tfsdk:"list_slice"` + ListSliceOfStructs []struct { A string `tfsdk:"a"` B int `tfsdk:"b"` - } `tfsdk:"slice_of_structs"` + } `tfsdk:"list_slice_of_structs"` + SetSlice []string `tfsdk:"set_slice"` + SetSliceOfStructs []struct { + A string `tfsdk:"a"` + B int `tfsdk:"b"` + } `tfsdk:"set_slice_of_structs"` Struct struct { A bool `tfsdk:"a"` Slice []float64 `tfsdk:"slice"` @@ -226,10 +231,21 @@ func TestNewStruct_complex(t *testing.T) { var s myStruct result, diags := refl.Struct(context.Background(), types.ObjectType{ AttrTypes: map[string]attr.Type{ - "slice": types.ListType{ + "list_slice": types.ListType{ ElemType: types.StringType, }, - "slice_of_structs": types.ListType{ + "list_slice_of_structs": types.ListType{ + ElemType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "a": types.StringType, + "b": types.NumberType, + }, + }, + }, + "set_slice": types.SetType{ + ElemType: types.StringType, + }, + "set_slice_of_structs": types.SetType{ ElemType: types.ObjectType{ AttrTypes: map[string]attr.Type{ "a": types.StringType, @@ -260,10 +276,21 @@ func TestNewStruct_complex(t *testing.T) { }, }, tftypes.NewValue(tftypes.Object{ AttributeTypes: map[string]tftypes.Type{ - "slice": tftypes.List{ + "list_slice": tftypes.List{ ElementType: tftypes.String, }, - "slice_of_structs": tftypes.List{ + "list_slice_of_structs": tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "a": tftypes.String, + "b": tftypes.Number, + }, + }, + }, + "set_slice": tftypes.Set{ + ElementType: tftypes.String, + }, + "set_slice_of_structs": tftypes.Set{ ElementType: tftypes.Object{ AttributeTypes: map[string]tftypes.Type{ "a": tftypes.String, @@ -293,14 +320,48 @@ func TestNewStruct_complex(t *testing.T) { "unhandled_unknown": tftypes.String, }, }, map[string]tftypes.Value{ - "slice": tftypes.NewValue(tftypes.List{ + "list_slice": tftypes.NewValue(tftypes.List{ + ElementType: tftypes.String, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "red"), + tftypes.NewValue(tftypes.String, "blue"), + tftypes.NewValue(tftypes.String, "green"), + }), + "list_slice_of_structs": tftypes.NewValue(tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "a": tftypes.String, + "b": tftypes.Number, + }, + }, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "a": tftypes.String, + "b": tftypes.Number, + }, + }, map[string]tftypes.Value{ + "a": tftypes.NewValue(tftypes.String, "hello, world"), + "b": tftypes.NewValue(tftypes.Number, 123), + }), + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "a": tftypes.String, + "b": tftypes.Number, + }, + }, map[string]tftypes.Value{ + "a": tftypes.NewValue(tftypes.String, "goodnight, moon"), + "b": tftypes.NewValue(tftypes.Number, 456), + }), + }), + "set_slice": tftypes.NewValue(tftypes.Set{ ElementType: tftypes.String, }, []tftypes.Value{ tftypes.NewValue(tftypes.String, "red"), tftypes.NewValue(tftypes.String, "blue"), tftypes.NewValue(tftypes.String, "green"), }), - "slice_of_structs": tftypes.NewValue(tftypes.List{ + "set_slice_of_structs": tftypes.NewValue(tftypes.Set{ ElementType: tftypes.Object{ AttributeTypes: map[string]tftypes.Type{ "a": tftypes.String, @@ -380,8 +441,22 @@ func TestNewStruct_complex(t *testing.T) { } str := "pointed" expected := myStruct{ - Slice: []string{"red", "blue", "green"}, - SliceOfStructs: []struct { + ListSlice: []string{"red", "blue", "green"}, + ListSliceOfStructs: []struct { + A string `tfsdk:"a"` + B int `tfsdk:"b"` + }{ + { + A: "hello, world", + B: 123, + }, + { + A: "goodnight, moon", + B: 456, + }, + }, + SetSlice: []string{"red", "blue", "green"}, + SetSliceOfStructs: []struct { A string `tfsdk:"a"` B int `tfsdk:"b"` }{ @@ -471,11 +546,16 @@ func TestFromStruct_complex(t *testing.T) { t.Parallel() type myStruct struct { - Slice []string `tfsdk:"slice"` - SliceOfStructs []struct { + ListSlice []string `tfsdk:"list_slice"` + ListSliceOfStructs []struct { + A string `tfsdk:"a"` + B int `tfsdk:"b"` + } `tfsdk:"list_slice_of_structs"` + SetSlice []string `tfsdk:"set_slice"` + SetSliceOfStructs []struct { A string `tfsdk:"a"` B int `tfsdk:"b"` - } `tfsdk:"slice_of_structs"` + } `tfsdk:"set_slice_of_structs"` Struct struct { A bool `tfsdk:"a"` Slice []float64 `tfsdk:"slice"` @@ -492,8 +572,22 @@ func TestFromStruct_complex(t *testing.T) { } str := "pointed" s := myStruct{ - Slice: []string{"red", "blue", "green"}, - SliceOfStructs: []struct { + ListSlice: []string{"red", "blue", "green"}, + ListSliceOfStructs: []struct { + A string `tfsdk:"a"` + B int `tfsdk:"b"` + }{ + { + A: "hello, world", + B: 123, + }, + { + A: "goodnight, moon", + B: 456, + }, + }, + SetSlice: []string{"red", "blue", "green"}, + SetSliceOfStructs: []struct { A string `tfsdk:"a"` B int `tfsdk:"b"` }{ @@ -536,10 +630,21 @@ func TestFromStruct_complex(t *testing.T) { } result, diags := refl.FromStruct(context.Background(), types.ObjectType{ AttrTypes: map[string]attr.Type{ - "slice": types.ListType{ + "list_slice": types.ListType{ ElemType: types.StringType, }, - "slice_of_structs": types.ListType{ + "list_slice_of_structs": types.ListType{ + ElemType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "a": types.StringType, + "b": types.NumberType, + }, + }, + }, + "set_slice": types.SetType{ + ElemType: types.StringType, + }, + "set_slice_of_structs": types.SetType{ ElemType: types.ObjectType{ AttrTypes: map[string]attr.Type{ "a": types.StringType, @@ -575,10 +680,21 @@ func TestFromStruct_complex(t *testing.T) { } expected := types.Object{ AttrTypes: map[string]attr.Type{ - "slice": types.ListType{ + "list_slice": types.ListType{ + ElemType: types.StringType, + }, + "list_slice_of_structs": types.ListType{ + ElemType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "a": types.StringType, + "b": types.NumberType, + }, + }, + }, + "set_slice": types.SetType{ ElemType: types.StringType, }, - "slice_of_structs": types.ListType{ + "set_slice_of_structs": types.SetType{ ElemType: types.ObjectType{ AttrTypes: map[string]attr.Type{ "a": types.StringType, @@ -609,7 +725,45 @@ func TestFromStruct_complex(t *testing.T) { "uint": types.NumberType, }, Attrs: map[string]attr.Value{ - "slice": types.List{ + "list_slice": types.List{ + ElemType: types.StringType, + Elems: []attr.Value{ + types.String{Value: "red"}, + types.String{Value: "blue"}, + types.String{Value: "green"}, + }, + }, + "list_slice_of_structs": types.List{ + ElemType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "a": types.StringType, + "b": types.NumberType, + }, + }, + Elems: []attr.Value{ + types.Object{ + AttrTypes: map[string]attr.Type{ + "a": types.StringType, + "b": types.NumberType, + }, + Attrs: map[string]attr.Value{ + "a": types.String{Value: "hello, world"}, + "b": types.Number{Value: big.NewFloat(123)}, + }, + }, + types.Object{ + AttrTypes: map[string]attr.Type{ + "a": types.StringType, + "b": types.NumberType, + }, + Attrs: map[string]attr.Value{ + "a": types.String{Value: "goodnight, moon"}, + "b": types.Number{Value: big.NewFloat(456)}, + }, + }, + }, + }, + "set_slice": types.Set{ ElemType: types.StringType, Elems: []attr.Value{ types.String{Value: "red"}, @@ -617,7 +771,7 @@ func TestFromStruct_complex(t *testing.T) { types.String{Value: "green"}, }, }, - "slice_of_structs": types.List{ + "set_slice_of_structs": types.Set{ ElemType: types.ObjectType{ AttrTypes: map[string]attr.Type{ "a": types.StringType, diff --git a/tfsdk/attribute.go b/tfsdk/attribute.go index 55c1770c4..93eb0629e 100644 --- a/tfsdk/attribute.go +++ b/tfsdk/attribute.go @@ -309,8 +309,49 @@ func (a Attribute) validate(ctx context.Context, req ValidateAttributeRequest, r } } case NestingModeSet: - // TODO: Set implementation - // Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/53 + s, ok := req.AttributeConfig.(types.Set) + + if !ok { + err := fmt.Errorf("unknown attribute value type (%T) for nesting mode (%T) at path: %s", req.AttributeConfig, nm, req.AttributePath) + resp.Diagnostics.AddAttributeError( + req.AttributePath, + "Attribute Validation Error", + "Attribute validation cannot walk schema. Report this to the provider developer:\n\n"+err.Error(), + ) + + return + } + + for _, value := range s.Elems { + tfValueRaw, err := value.ToTerraformValue(ctx) + + if err != nil { + err := fmt.Errorf("error running ToTerraformValue on element value: %v", value) + resp.Diagnostics.AddAttributeError( + req.AttributePath, + "Attribute Validation Error", + "Attribute validation cannot convert element into a Terraform value. Report this to the provider developer:\n\n"+err.Error(), + ) + + return + } + + tfValue := tftypes.NewValue(s.ElemType.TerraformType(ctx), tfValueRaw) + + for nestedName, nestedAttr := range a.Attributes.GetAttributes() { + nestedAttrReq := ValidateAttributeRequest{ + AttributePath: req.AttributePath.WithElementKeyValue(tfValue).WithAttributeName(nestedName), + Config: req.Config, + } + nestedAttrResp := &ValidateAttributeResponse{ + Diagnostics: resp.Diagnostics, + } + + nestedAttr.validate(ctx, nestedAttrReq, nestedAttrResp) + + resp.Diagnostics = nestedAttrResp.Diagnostics + } + } case NestingModeMap: m, ok := req.AttributeConfig.(types.Map) diff --git a/tfsdk/attribute_test.go b/tfsdk/attribute_test.go index 3c8c79d0d..1abadd296 100644 --- a/tfsdk/attribute_test.go +++ b/tfsdk/attribute_test.go @@ -175,7 +175,19 @@ func TestAttributeTfprotov6SchemaAttribute(t *testing.T) { Optional: true, }, }, - // TODO: add set attribute when we support it + "attr-set": { + name: "set", + attr: Attribute{ + Type: types.SetType{ElemType: types.NumberType}, + Optional: true, + }, + path: tftypes.NewAttributePath(), + expected: &tfprotov6.SchemaAttribute{ + Name: "set", + Type: tftypes.Set{ElementType: tftypes.Number}, + Optional: true, + }, + }, // TODO: add tuple attribute when we support it "required": { name: "string", @@ -2635,6 +2647,127 @@ func TestAttributeValidate(t *testing.T) { }, }, }, + "nested-attr-set-no-validation": { + req: ValidateAttributeRequest{ + AttributePath: tftypes.NewAttributePath().WithAttributeName("test"), + Config: Config{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + }, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + }, + []tftypes.Value{ + tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "nested_attr": tftypes.NewValue(tftypes.String, "testvalue"), + }, + ), + }, + ), + }, + ), + Schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Attributes: SetNestedAttributes(map[string]Attribute{ + "nested_attr": { + Type: types.StringType, + Required: true, + }, + }, SetNestedAttributesOptions{}), + Required: true, + }, + }, + }, + }, + }, + resp: ValidateAttributeResponse{}, + }, + "nested-attr-set-validation": { + req: ValidateAttributeRequest{ + AttributePath: tftypes.NewAttributePath().WithAttributeName("test"), + Config: Config{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + }, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + }, + []tftypes.Value{ + tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "nested_attr": tftypes.NewValue(tftypes.String, "testvalue"), + }, + ), + }, + ), + }, + ), + Schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Attributes: SetNestedAttributes(map[string]Attribute{ + "nested_attr": { + Type: types.StringType, + Required: true, + Validators: []AttributeValidator{ + testErrorAttributeValidator{}, + }, + }, + }, SetNestedAttributesOptions{}), + Required: true, + }, + }, + }, + }, + }, + resp: ValidateAttributeResponse{ + Diagnostics: diag.Diagnostics{ + testErrorDiagnostic1, + }, + }, + }, "nested-attr-single-no-validation": { req: ValidateAttributeRequest{ AttributePath: tftypes.NewAttributePath().WithAttributeName("test"), diff --git a/tfsdk/nested_attributes.go b/tfsdk/nested_attributes.go index bac707f73..403cbbfb1 100644 --- a/tfsdk/nested_attributes.go +++ b/tfsdk/nested_attributes.go @@ -271,8 +271,9 @@ func (s setNestedAttributes) GetMaxItems() int64 { // AttributeType returns an attr.Type corresponding to the nested attributes. func (s setNestedAttributes) AttributeType() attr.Type { - // TODO fill in implementation when types.SetType is available - return nil + return types.SetType{ + ElemType: s.nestedAttributes.AttributeType(), + } } func (s setNestedAttributes) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { diff --git a/tfsdk/schema.go b/tfsdk/schema.go index b1f37ddab..a499a7622 100644 --- a/tfsdk/schema.go +++ b/tfsdk/schema.go @@ -267,8 +267,37 @@ func modifyAttributesPlans(ctx context.Context, attrs map[string]Attribute, path modifyAttributesPlans(ctx, nestedAttr.Attributes.GetAttributes(), attrPath.WithElementKeyInt(int64(idx)), req, resp) } case NestingModeSet: - // TODO: Set implementation - // Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/53 + s, ok := attrPlan.(types.Set) + + if !ok { + err := fmt.Errorf("unknown attribute value type (%T) for nesting mode (%T) at path: %s", attrPlan, nm, attrPath) + resp.Diagnostics.AddAttributeError( + attrPath, + "Attribute Plan Modification Error", + "Attribute plan modifier cannot walk schema. Report this to the provider developer:\n\n"+err.Error(), + ) + + return + } + + for _, value := range s.Elems { + tfValueRaw, err := value.ToTerraformValue(ctx) + + if err != nil { + err := fmt.Errorf("error running ToTerraformValue on element value: %v", value) + resp.Diagnostics.AddAttributeError( + attrPath, + "Attribute Plan Modification Error", + "Attribute plan modification cannot convert element into a Terraform value. Report this to the provider developer:\n\n"+err.Error(), + ) + + return + } + + tfValue := tftypes.NewValue(s.ElemType.TerraformType(ctx), tfValueRaw) + + modifyAttributesPlans(ctx, nestedAttr.Attributes.GetAttributes(), attrPath.WithElementKeyValue(tfValue), req, resp) + } case NestingModeMap: m, ok := attrPlan.(types.Map) diff --git a/tfsdk/schema_test.go b/tfsdk/schema_test.go index 981a29d25..b1741a0ff 100644 --- a/tfsdk/schema_test.go +++ b/tfsdk/schema_test.go @@ -158,8 +158,11 @@ func TestSchemaTfprotov6Schema(t *testing.T) { Type: types.MapType{ElemType: types.NumberType}, Computed: true, }, + "set": { + Type: types.SetType{ElemType: types.StringType}, + Required: true, + }, // TODO: add tuple support when it lands - // TODO: add set support when it lands }, }, expected: &tfprotov6.Schema{ @@ -185,6 +188,11 @@ func TestSchemaTfprotov6Schema(t *testing.T) { }}, Optional: true, }, + { + Name: "set", + Type: tftypes.Set{ElementType: tftypes.String}, + Required: true, + }, }, }, }, diff --git a/tfsdk/serve_provider_test.go b/tfsdk/serve_provider_test.go index e18a506bc..163ed4c35 100644 --- a/tfsdk/serve_provider_test.go +++ b/tfsdk/serve_provider_test.go @@ -163,7 +163,32 @@ func (t *testServeProvider) GetSchema(_ context.Context) (Schema, diag.Diagnosti Type: types.MapType{ElemType: types.NumberType}, Optional: true, }, - // TODO: add sets when we support them + "set-string": { + Type: types.SetType{ + ElemType: types.StringType, + }, + Optional: true, + }, + "set-set-string": { + Type: types.SetType{ + ElemType: types.SetType{ + ElemType: types.StringType, + }, + }, + Optional: true, + }, + "set-object": { + Type: types.SetType{ + ElemType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "foo": types.StringType, + "bar": types.BoolType, + "baz": types.NumberType, + }, + }, + }, + Optional: true, + }, // TODO: add tuples when we support them "single-nested-attributes": { Attributes: SingleNestedAttributes(map[string]Attribute{ @@ -207,6 +232,20 @@ func (t *testServeProvider) GetSchema(_ context.Context) (Schema, diag.Diagnosti }, MapNestedAttributesOptions{}), Optional: true, }, + "set-nested-attributes": { + Attributes: SetNestedAttributes(map[string]Attribute{ + "foo": { + Type: types.StringType, + Optional: true, + Computed: true, + }, + "bar": { + Type: types.NumberType, + Required: true, + }, + }, SetNestedAttributesOptions{}), + Optional: true, + }, }, }, nil } @@ -356,6 +395,55 @@ var testServeProviderProviderSchema = &tfprotov6.Schema{ Optional: true, Sensitive: true, }, + { + Name: "set-nested-attributes", + NestedType: &tfprotov6.SchemaObject{ + Nesting: tfprotov6.SchemaObjectNestingModeSet, + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "bar", + Type: tftypes.Number, + Required: true, + }, + { + Name: "foo", + Type: tftypes.String, + Optional: true, + Computed: true, + }, + }, + }, + Optional: true, + }, + { + Name: "set-object", + Type: tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "foo": tftypes.String, + "bar": tftypes.Bool, + "baz": tftypes.Number, + }, + }, + }, + Optional: true, + }, + { + Name: "set-set-string", + Type: tftypes.Set{ + ElementType: tftypes.Set{ + ElementType: tftypes.String, + }, + }, + Optional: true, + }, + { + Name: "set-string", + Type: tftypes.Set{ + ElementType: tftypes.String, + }, + Optional: true, + }, { Name: "single-nested-attributes", NestedType: &tfprotov6.SchemaObject{ @@ -381,7 +469,6 @@ var testServeProviderProviderSchema = &tfprotov6.Schema{ Type: tftypes.String, Optional: true, }, - // TODO: add sets when we support them // TODO: add tuples when we support them }, }, @@ -412,6 +499,13 @@ var testServeProviderProviderType = tftypes.Object{ "baz": tftypes.Number, "quux": tftypes.List{ElementType: tftypes.String}, }}, + "set-string": tftypes.Set{ElementType: tftypes.String}, + "set-set-string": tftypes.Set{ElementType: tftypes.Set{ElementType: tftypes.String}}, + "set-object": tftypes.Set{ElementType: tftypes.Object{AttributeTypes: map[string]tftypes.Type{ + "foo": tftypes.String, + "bar": tftypes.Bool, + "baz": tftypes.Number, + }}}, "empty-object": tftypes.Object{AttributeTypes: map[string]tftypes.Type{}}, "single-nested-attributes": tftypes.Object{AttributeTypes: map[string]tftypes.Type{ "foo": tftypes.String, @@ -425,6 +519,10 @@ var testServeProviderProviderType = tftypes.Object{ "foo": tftypes.String, "bar": tftypes.Number, }}}, + "set-nested-attributes": tftypes.Set{ElementType: tftypes.Object{AttributeTypes: map[string]tftypes.Type{ + "foo": tftypes.String, + "bar": tftypes.Number, + }}}, }, } diff --git a/tfsdk/serve_test.go b/tfsdk/serve_test.go index b7cc0b67d..58abf9904 100644 --- a/tfsdk/serve_test.go +++ b/tfsdk/serve_test.go @@ -419,6 +419,46 @@ func TestServerValidateProviderConfig(t *testing.T) { tftypes.NewValue(tftypes.String, "green"), }), }), + "set-string": tftypes.NewValue(tftypes.Set{ElementType: tftypes.String}, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "hello"), + tftypes.NewValue(tftypes.String, "world"), + }), + "set-set-string": tftypes.NewValue(tftypes.Set{ElementType: tftypes.Set{ElementType: tftypes.String}}, []tftypes.Value{ + tftypes.NewValue(tftypes.Set{ElementType: tftypes.String}, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "red"), + tftypes.NewValue(tftypes.String, "blue"), + tftypes.NewValue(tftypes.String, "green"), + }), + tftypes.NewValue(tftypes.Set{ElementType: tftypes.String}, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "rojo"), + tftypes.NewValue(tftypes.String, "azul"), + tftypes.NewValue(tftypes.String, "verde"), + }), + }), + "set-object": tftypes.NewValue(tftypes.Set{ElementType: tftypes.Object{AttributeTypes: map[string]tftypes.Type{ + "foo": tftypes.String, + "bar": tftypes.Bool, + "baz": tftypes.Number, + }}}, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{AttributeTypes: map[string]tftypes.Type{ + "foo": tftypes.String, + "bar": tftypes.Bool, + "baz": tftypes.Number, + }}, map[string]tftypes.Value{ + "foo": tftypes.NewValue(tftypes.String, "hello, world"), + "bar": tftypes.NewValue(tftypes.Bool, true), + "baz": tftypes.NewValue(tftypes.Number, 4567), + }), + tftypes.NewValue(tftypes.Object{AttributeTypes: map[string]tftypes.Type{ + "foo": tftypes.String, + "bar": tftypes.Bool, + "baz": tftypes.Number, + }}, map[string]tftypes.Value{ + "foo": tftypes.NewValue(tftypes.String, "goodnight, moon"), + "bar": tftypes.NewValue(tftypes.Bool, false), + "baz": tftypes.NewValue(tftypes.Number, 8675309), + }), + }), "empty-object": tftypes.NewValue(tftypes.Object{AttributeTypes: map[string]tftypes.Type{}}, map[string]tftypes.Value{}), "single-nested-attributes": tftypes.NewValue(tftypes.Object{AttributeTypes: map[string]tftypes.Type{ "foo": tftypes.String, @@ -470,6 +510,25 @@ func TestServerValidateProviderConfig(t *testing.T) { "foo": tftypes.NewValue(tftypes.String, "moon"), }), }), + "set-nested-attributes": tftypes.NewValue(tftypes.Set{ElementType: tftypes.Object{AttributeTypes: map[string]tftypes.Type{ + "foo": tftypes.String, + "bar": tftypes.Number, + }}}, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{AttributeTypes: map[string]tftypes.Type{ + "foo": tftypes.String, + "bar": tftypes.Number, + }}, map[string]tftypes.Value{ + "foo": tftypes.NewValue(tftypes.String, "let's do the math"), + "bar": tftypes.NewValue(tftypes.Number, 18973), + }), + tftypes.NewValue(tftypes.Object{AttributeTypes: map[string]tftypes.Type{ + "foo": tftypes.String, + "bar": tftypes.Number, + }}, map[string]tftypes.Value{ + "foo": tftypes.NewValue(tftypes.String, "this is why we can't have nice things"), + "bar": tftypes.NewValue(tftypes.Number, 14554216), + }), + }), }), provider: &testServeProvider{}, providerType: testServeProviderProviderType, @@ -798,6 +857,46 @@ func TestServerConfigureProvider(t *testing.T) { tftypes.NewValue(tftypes.String, "green"), }), }), + "set-string": tftypes.NewValue(tftypes.Set{ElementType: tftypes.String}, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "hello"), + tftypes.NewValue(tftypes.String, "world"), + }), + "set-set-string": tftypes.NewValue(tftypes.Set{ElementType: tftypes.Set{ElementType: tftypes.String}}, []tftypes.Value{ + tftypes.NewValue(tftypes.Set{ElementType: tftypes.String}, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "red"), + tftypes.NewValue(tftypes.String, "blue"), + tftypes.NewValue(tftypes.String, "green"), + }), + tftypes.NewValue(tftypes.Set{ElementType: tftypes.String}, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "rojo"), + tftypes.NewValue(tftypes.String, "azul"), + tftypes.NewValue(tftypes.String, "verde"), + }), + }), + "set-object": tftypes.NewValue(tftypes.Set{ElementType: tftypes.Object{AttributeTypes: map[string]tftypes.Type{ + "foo": tftypes.String, + "bar": tftypes.Bool, + "baz": tftypes.Number, + }}}, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{AttributeTypes: map[string]tftypes.Type{ + "foo": tftypes.String, + "bar": tftypes.Bool, + "baz": tftypes.Number, + }}, map[string]tftypes.Value{ + "foo": tftypes.NewValue(tftypes.String, "hello, world"), + "bar": tftypes.NewValue(tftypes.Bool, true), + "baz": tftypes.NewValue(tftypes.Number, 4567), + }), + tftypes.NewValue(tftypes.Object{AttributeTypes: map[string]tftypes.Type{ + "foo": tftypes.String, + "bar": tftypes.Bool, + "baz": tftypes.Number, + }}, map[string]tftypes.Value{ + "foo": tftypes.NewValue(tftypes.String, "goodnight, moon"), + "bar": tftypes.NewValue(tftypes.Bool, false), + "baz": tftypes.NewValue(tftypes.Number, 8675309), + }), + }), "empty-object": tftypes.NewValue(tftypes.Object{AttributeTypes: map[string]tftypes.Type{}}, map[string]tftypes.Value{}), "single-nested-attributes": tftypes.NewValue(tftypes.Object{AttributeTypes: map[string]tftypes.Type{ "foo": tftypes.String, @@ -825,6 +924,25 @@ func TestServerConfigureProvider(t *testing.T) { "bar": tftypes.NewValue(tftypes.Number, 14554216), }), }), + "set-nested-attributes": tftypes.NewValue(tftypes.Set{ElementType: tftypes.Object{AttributeTypes: map[string]tftypes.Type{ + "foo": tftypes.String, + "bar": tftypes.Number, + }}}, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{AttributeTypes: map[string]tftypes.Type{ + "foo": tftypes.String, + "bar": tftypes.Number, + }}, map[string]tftypes.Value{ + "foo": tftypes.NewValue(tftypes.String, "let's do the math"), + "bar": tftypes.NewValue(tftypes.Number, 18973), + }), + tftypes.NewValue(tftypes.Object{AttributeTypes: map[string]tftypes.Type{ + "foo": tftypes.String, + "bar": tftypes.Number, + }}, map[string]tftypes.Value{ + "foo": tftypes.NewValue(tftypes.String, "this is why we can't have nice things"), + "bar": tftypes.NewValue(tftypes.Number, 14554216), + }), + }), }), }, "config-unknown-value": { @@ -860,6 +978,16 @@ func TestServerConfigureProvider(t *testing.T) { "baz": tftypes.NewValue(tftypes.Number, 123), "quux": tftypes.NewValue(tftypes.List{ElementType: tftypes.String}, tftypes.UnknownValue), }), + "set-string": tftypes.NewValue(tftypes.Set{ElementType: tftypes.String}, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "hello"), + tftypes.NewValue(tftypes.String, "world"), + }), + "set-set-string": tftypes.NewValue(tftypes.Set{ElementType: tftypes.Set{ElementType: tftypes.String}}, tftypes.UnknownValue), + "set-object": tftypes.NewValue(tftypes.Set{ElementType: tftypes.Object{AttributeTypes: map[string]tftypes.Type{ + "foo": tftypes.String, + "bar": tftypes.Bool, + "baz": tftypes.Number, + }}}, tftypes.UnknownValue), "empty-object": tftypes.NewValue(tftypes.Object{AttributeTypes: map[string]tftypes.Type{}}, map[string]tftypes.Value{}), "single-nested-attributes": tftypes.NewValue(tftypes.Object{AttributeTypes: map[string]tftypes.Type{ "foo": tftypes.String, @@ -896,6 +1024,10 @@ func TestServerConfigureProvider(t *testing.T) { "foo": tftypes.NewValue(tftypes.String, "moon"), }), }), + "set-nested-attributes": tftypes.NewValue(tftypes.Set{ElementType: tftypes.Object{AttributeTypes: map[string]tftypes.Type{ + "foo": tftypes.String, + "bar": tftypes.Number, + }}}, tftypes.UnknownValue), }), }, } diff --git a/tfsdk/state_test.go b/tfsdk/state_test.go index 6aa48293f..7a14b73fd 100644 --- a/tfsdk/state_test.go +++ b/tfsdk/state_test.go @@ -19,10 +19,15 @@ func TestStateGet(t *testing.T) { Name types.String `tfsdk:"name"` MachineType string `tfsdk:"machine_type"` Tags types.List `tfsdk:"tags"` + TagsSet types.Set `tfsdk:"tags_set"` Disks []struct { ID string `tfsdk:"id"` DeleteWithInstance bool `tfsdk:"delete_with_instance"` } `tfsdk:"disks"` + DisksSet []struct { + ID string `tfsdk:"id"` + DeleteWithInstance bool `tfsdk:"delete_with_instance"` + } `tfsdk:"disks_set"` BootDisk struct { ID string `tfsdk:"id"` DeleteWithInstance bool `tfsdk:"delete_with_instance"` @@ -46,6 +51,7 @@ func TestStateGet(t *testing.T) { "name": tftypes.String, "machine_type": tftypes.String, "tags": tftypes.List{ElementType: tftypes.String}, + "tags_set": tftypes.Set{ElementType: tftypes.String}, "disks": tftypes.List{ ElementType: tftypes.Object{ AttributeTypes: map[string]tftypes.Type{ @@ -54,6 +60,14 @@ func TestStateGet(t *testing.T) { }, }, }, + "disks_set": tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, + }, "boot_disk": tftypes.Object{ AttributeTypes: map[string]tftypes.Type{ "id": tftypes.String, @@ -76,6 +90,13 @@ func TestStateGet(t *testing.T) { tftypes.NewValue(tftypes.String, "blue"), tftypes.NewValue(tftypes.String, "green"), }), + "tags_set": tftypes.NewValue(tftypes.Set{ + ElementType: tftypes.String, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "red"), + tftypes.NewValue(tftypes.String, "blue"), + tftypes.NewValue(tftypes.String, "green"), + }), "disks": tftypes.NewValue(tftypes.List{ ElementType: tftypes.Object{ AttributeTypes: map[string]tftypes.Type{ @@ -103,6 +124,33 @@ func TestStateGet(t *testing.T) { "delete_with_instance": tftypes.NewValue(tftypes.Bool, false), }), }), + "disks_set": tftypes.NewValue(tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "disk0"), + "delete_with_instance": tftypes.NewValue(tftypes.Bool, true), + }), + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "disk1"), + "delete_with_instance": tftypes.NewValue(tftypes.Bool, false), + }), + }), "boot_disk": tftypes.NewValue(tftypes.Object{ AttributeTypes: map[string]tftypes.Type{ "id": tftypes.String, @@ -135,6 +183,12 @@ func TestStateGet(t *testing.T) { }, Required: true, }, + "tags_set": { + Type: types.SetType{ + ElemType: types.StringType, + }, + Required: true, + }, "disks": { Attributes: ListNestedAttributes(map[string]Attribute{ "id": { @@ -149,6 +203,20 @@ func TestStateGet(t *testing.T) { Optional: true, Computed: true, }, + "disks_set": { + Attributes: SetNestedAttributes(map[string]Attribute{ + "id": { + Type: types.StringType, + Required: true, + }, + "delete_with_instance": { + Type: types.BoolType, + Optional: true, + }, + }, SetNestedAttributesOptions{}), + Optional: true, + Computed: true, + }, "boot_disk": { Attributes: SingleNestedAttributes(map[string]Attribute{ "id": { @@ -182,6 +250,14 @@ func TestStateGet(t *testing.T) { types.String{Value: "green"}, }, }, + TagsSet: types.Set{ + ElemType: types.StringType, + Elems: []attr.Value{ + types.String{Value: "red"}, + types.String{Value: "blue"}, + types.String{Value: "green"}, + }, + }, Disks: []struct { ID string `tfsdk:"id"` DeleteWithInstance bool `tfsdk:"delete_with_instance"` @@ -195,6 +271,19 @@ func TestStateGet(t *testing.T) { DeleteWithInstance: false, }, }, + DisksSet: []struct { + ID string `tfsdk:"id"` + DeleteWithInstance bool `tfsdk:"delete_with_instance"` + }{ + { + ID: "disk0", + DeleteWithInstance: true, + }, + { + ID: "disk1", + DeleteWithInstance: false, + }, + }, BootDisk: struct { ID string `tfsdk:"id"` DeleteWithInstance bool `tfsdk:"delete_with_instance"` @@ -238,10 +327,15 @@ func TestStateGet_testTypes(t *testing.T) { Name testtypes.String `tfsdk:"name"` MachineType string `tfsdk:"machine_type"` Tags types.List `tfsdk:"tags"` + TagsSet types.Set `tfsdk:"tags_set"` Disks []struct { ID string `tfsdk:"id"` DeleteWithInstance bool `tfsdk:"delete_with_instance"` } `tfsdk:"disks"` + DisksSet []struct { + ID string `tfsdk:"id"` + DeleteWithInstance bool `tfsdk:"delete_with_instance"` + } `tfsdk:"disks_set"` BootDisk struct { ID string `tfsdk:"id"` DeleteWithInstance bool `tfsdk:"delete_with_instance"` @@ -265,6 +359,7 @@ func TestStateGet_testTypes(t *testing.T) { "name": tftypes.String, "machine_type": tftypes.String, "tags": tftypes.List{ElementType: tftypes.String}, + "tags_set": tftypes.Set{ElementType: tftypes.String}, "disks": tftypes.List{ ElementType: tftypes.Object{ AttributeTypes: map[string]tftypes.Type{ @@ -273,6 +368,14 @@ func TestStateGet_testTypes(t *testing.T) { }, }, }, + "disks_set": tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, + }, "boot_disk": tftypes.Object{ AttributeTypes: map[string]tftypes.Type{ "id": tftypes.String, @@ -295,6 +398,13 @@ func TestStateGet_testTypes(t *testing.T) { tftypes.NewValue(tftypes.String, "blue"), tftypes.NewValue(tftypes.String, "green"), }), + "tags_set": tftypes.NewValue(tftypes.Set{ + ElementType: tftypes.String, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "red"), + tftypes.NewValue(tftypes.String, "blue"), + tftypes.NewValue(tftypes.String, "green"), + }), "disks": tftypes.NewValue(tftypes.List{ ElementType: tftypes.Object{ AttributeTypes: map[string]tftypes.Type{ @@ -322,6 +432,33 @@ func TestStateGet_testTypes(t *testing.T) { "delete_with_instance": tftypes.NewValue(tftypes.Bool, false), }), }), + "disks_set": tftypes.NewValue(tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "disk0"), + "delete_with_instance": tftypes.NewValue(tftypes.Bool, true), + }), + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "disk1"), + "delete_with_instance": tftypes.NewValue(tftypes.Bool, false), + }), + }), "boot_disk": tftypes.NewValue(tftypes.Object{ AttributeTypes: map[string]tftypes.Type{ "id": tftypes.String, @@ -354,6 +491,12 @@ func TestStateGet_testTypes(t *testing.T) { }, Required: true, }, + "tags_set": { + Type: types.SetType{ + ElemType: types.StringType, + }, + Required: true, + }, "disks": { Attributes: ListNestedAttributes(map[string]Attribute{ "id": { @@ -368,6 +511,20 @@ func TestStateGet_testTypes(t *testing.T) { Optional: true, Computed: true, }, + "disks_set": { + Attributes: SetNestedAttributes(map[string]Attribute{ + "id": { + Type: types.StringType, + Required: true, + }, + "delete_with_instance": { + Type: types.BoolType, + Optional: true, + }, + }, SetNestedAttributesOptions{}), + Optional: true, + Computed: true, + }, "boot_disk": { Attributes: SingleNestedAttributes(map[string]Attribute{ "id": { @@ -394,7 +551,9 @@ func TestStateGet_testTypes(t *testing.T) { Name: testtypes.String{String: types.String{Value: ""}, CreatedBy: testtypes.StringTypeWithValidateError{}}, MachineType: "", Tags: types.List{}, + TagsSet: types.Set{}, Disks: nil, + DisksSet: nil, BootDisk: struct { ID string `tfsdk:"id"` DeleteWithInstance bool `tfsdk:"delete_with_instance"` @@ -417,6 +576,7 @@ func TestStateGet_testTypes(t *testing.T) { "name": tftypes.String, "machine_type": tftypes.String, "tags": tftypes.List{ElementType: tftypes.String}, + "tags_set": tftypes.Set{ElementType: tftypes.String}, "disks": tftypes.List{ ElementType: tftypes.Object{ AttributeTypes: map[string]tftypes.Type{ @@ -425,6 +585,14 @@ func TestStateGet_testTypes(t *testing.T) { }, }, }, + "disks_set": tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, + }, "boot_disk": tftypes.Object{ AttributeTypes: map[string]tftypes.Type{ "id": tftypes.String, @@ -447,6 +615,13 @@ func TestStateGet_testTypes(t *testing.T) { tftypes.NewValue(tftypes.String, "blue"), tftypes.NewValue(tftypes.String, "green"), }), + "tags_set": tftypes.NewValue(tftypes.Set{ + ElementType: tftypes.String, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "red"), + tftypes.NewValue(tftypes.String, "blue"), + tftypes.NewValue(tftypes.String, "green"), + }), "disks": tftypes.NewValue(tftypes.List{ ElementType: tftypes.Object{ AttributeTypes: map[string]tftypes.Type{ @@ -474,6 +649,33 @@ func TestStateGet_testTypes(t *testing.T) { "delete_with_instance": tftypes.NewValue(tftypes.Bool, false), }), }), + "disks_set": tftypes.NewValue(tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "disk0"), + "delete_with_instance": tftypes.NewValue(tftypes.Bool, true), + }), + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "disk1"), + "delete_with_instance": tftypes.NewValue(tftypes.Bool, false), + }), + }), "boot_disk": tftypes.NewValue(tftypes.Object{ AttributeTypes: map[string]tftypes.Type{ "id": tftypes.String, @@ -506,6 +708,12 @@ func TestStateGet_testTypes(t *testing.T) { }, Required: true, }, + "tags_set": { + Type: types.SetType{ + ElemType: types.StringType, + }, + Required: true, + }, "disks": { Attributes: ListNestedAttributes(map[string]Attribute{ "id": { @@ -520,6 +728,20 @@ func TestStateGet_testTypes(t *testing.T) { Optional: true, Computed: true, }, + "disks_set": { + Attributes: SetNestedAttributes(map[string]Attribute{ + "id": { + Type: types.StringType, + Required: true, + }, + "delete_with_instance": { + Type: types.BoolType, + Optional: true, + }, + }, SetNestedAttributesOptions{}), + Optional: true, + Computed: true, + }, "boot_disk": { Attributes: SingleNestedAttributes(map[string]Attribute{ "id": { @@ -553,6 +775,14 @@ func TestStateGet_testTypes(t *testing.T) { types.String{Value: "green"}, }, }, + TagsSet: types.Set{ + ElemType: types.StringType, + Elems: []attr.Value{ + types.String{Value: "red"}, + types.String{Value: "blue"}, + types.String{Value: "green"}, + }, + }, Disks: []struct { ID string `tfsdk:"id"` DeleteWithInstance bool `tfsdk:"delete_with_instance"` @@ -566,6 +796,19 @@ func TestStateGet_testTypes(t *testing.T) { DeleteWithInstance: false, }, }, + DisksSet: []struct { + ID string `tfsdk:"id"` + DeleteWithInstance bool `tfsdk:"delete_with_instance"` + }{ + { + ID: "disk0", + DeleteWithInstance: true, + }, + { + ID: "disk1", + DeleteWithInstance: false, + }, + }, BootDisk: struct { ID string `tfsdk:"id"` DeleteWithInstance bool `tfsdk:"delete_with_instance"` @@ -859,49 +1102,178 @@ func TestStateGetAttribute(t *testing.T) { }, }, }, - "AttrTypeWithValidateError": { + "set": { state: State{ Raw: tftypes.NewValue(tftypes.Object{ AttributeTypes: map[string]tftypes.Type{ - "name": tftypes.String, + "tags": tftypes.Set{ElementType: tftypes.String}, }, }, map[string]tftypes.Value{ - "name": tftypes.NewValue(tftypes.String, "namevalue"), + "tags": tftypes.NewValue(tftypes.Set{ + ElementType: tftypes.String, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "red"), + tftypes.NewValue(tftypes.String, "blue"), + tftypes.NewValue(tftypes.String, "green"), + }), }), Schema: Schema{ Attributes: map[string]Attribute{ - "name": { - Type: testtypes.StringTypeWithValidateError{}, + "tags": { + Type: types.SetType{ + ElemType: types.StringType, + }, Required: true, }, }, }, }, - path: tftypes.NewAttributePath().WithAttributeName("name"), - expected: nil, - expectedDiags: diag.Diagnostics{testtypes.TestErrorDiagnostic(tftypes.NewAttributePath().WithAttributeName("name"))}, + path: tftypes.NewAttributePath().WithAttributeName("tags"), + expected: types.Set{ + Elems: []attr.Value{ + types.String{Value: "red"}, + types.String{Value: "blue"}, + types.String{Value: "green"}, + }, + ElemType: types.StringType, + }, }, - "AttrTypeWithValidateWarning": { + "nested-set": { state: State{ Raw: tftypes.NewValue(tftypes.Object{ AttributeTypes: map[string]tftypes.Type{ - "name": tftypes.String, + "disks": tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, + }, }, }, map[string]tftypes.Value{ - "name": tftypes.NewValue(tftypes.String, "namevalue"), - }), - Schema: Schema{ - Attributes: map[string]Attribute{ - "name": { - Type: testtypes.StringTypeWithValidateWarning{}, - Required: true, + "disks": tftypes.NewValue(tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, }, - }, - }, - }, - path: tftypes.NewAttributePath().WithAttributeName("name"), - expected: testtypes.String{String: types.String{Value: "namevalue"}, CreatedBy: testtypes.StringTypeWithValidateWarning{}}, - expectedDiags: diag.Diagnostics{testtypes.TestWarningDiagnostic(tftypes.NewAttributePath().WithAttributeName("name"))}, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "disk0"), + "delete_with_instance": tftypes.NewValue(tftypes.Bool, true), + }), + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "disk1"), + "delete_with_instance": tftypes.NewValue(tftypes.Bool, false), + }), + }), + }), + Schema: Schema{ + Attributes: map[string]Attribute{ + "disks": { + Attributes: SetNestedAttributes(map[string]Attribute{ + "id": { + Type: types.StringType, + Required: true, + }, + "delete_with_instance": { + Type: types.BoolType, + Optional: true, + }, + }, SetNestedAttributesOptions{}), + Optional: true, + Computed: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("disks"), + expected: types.Set{ + Elems: []attr.Value{ + types.Object{ + Attrs: map[string]attr.Value{ + "delete_with_instance": types.Bool{Value: true}, + "id": types.String{Value: "disk0"}, + }, + AttrTypes: map[string]attr.Type{ + "delete_with_instance": types.BoolType, + "id": types.StringType, + }, + }, + types.Object{ + Attrs: map[string]attr.Value{ + "delete_with_instance": types.Bool{Value: false}, + "id": types.String{Value: "disk1"}, + }, + AttrTypes: map[string]attr.Type{ + "delete_with_instance": types.BoolType, + "id": types.StringType, + }, + }, + }, + ElemType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "delete_with_instance": types.BoolType, + "id": types.StringType, + }, + }, + }, + }, + "AttrTypeWithValidateError": { + state: State{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "name": tftypes.String, + }, + }, map[string]tftypes.Value{ + "name": tftypes.NewValue(tftypes.String, "namevalue"), + }), + Schema: Schema{ + Attributes: map[string]Attribute{ + "name": { + Type: testtypes.StringTypeWithValidateError{}, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("name"), + expected: nil, + expectedDiags: diag.Diagnostics{testtypes.TestErrorDiagnostic(tftypes.NewAttributePath().WithAttributeName("name"))}, + }, + "AttrTypeWithValidateWarning": { + state: State{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "name": tftypes.String, + }, + }, map[string]tftypes.Value{ + "name": tftypes.NewValue(tftypes.String, "namevalue"), + }), + Schema: Schema{ + Attributes: map[string]Attribute{ + "name": { + Type: testtypes.StringTypeWithValidateWarning{}, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("name"), + expected: testtypes.String{String: types.String{Value: "namevalue"}, CreatedBy: testtypes.StringTypeWithValidateWarning{}}, + expectedDiags: diag.Diagnostics{testtypes.TestWarningDiagnostic(tftypes.NewAttributePath().WithAttributeName("name"))}, }, } @@ -1223,6 +1595,122 @@ func TestStateSet(t *testing.T) { }), }), }, + "set": { + state: State{ + Raw: tftypes.Value{}, + Schema: Schema{ + Attributes: map[string]Attribute{ + "tags": { + Type: types.SetType{ + ElemType: types.StringType, + }, + Required: true, + }, + }, + }, + }, + val: struct { + Tags []string `tfsdk:"tags"` + }{ + Tags: []string{"red", "blue", "green"}, + }, + expected: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "tags": tftypes.Set{ElementType: tftypes.String}, + }, + }, map[string]tftypes.Value{ + "tags": tftypes.NewValue(tftypes.Set{ + ElementType: tftypes.String, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "red"), + tftypes.NewValue(tftypes.String, "blue"), + tftypes.NewValue(tftypes.String, "green"), + }), + }), + }, + "nested-set": { + state: State{ + Raw: tftypes.Value{}, + Schema: Schema{ + Attributes: map[string]Attribute{ + "disks": { + Attributes: SetNestedAttributes(map[string]Attribute{ + "id": { + Type: types.StringType, + Required: true, + }, + "delete_with_instance": { + Type: types.BoolType, + Optional: true, + }, + }, SetNestedAttributesOptions{}), + Optional: true, + Computed: true, + }, + }, + }, + }, + val: struct { + Disks []struct { + ID string `tfsdk:"id"` + DeleteWithInstance bool `tfsdk:"delete_with_instance"` + } `tfsdk:"disks"` + }{ + Disks: []struct { + ID string `tfsdk:"id"` + DeleteWithInstance bool `tfsdk:"delete_with_instance"` + }{ + { + ID: "disk0", + DeleteWithInstance: true, + }, + { + ID: "disk1", + DeleteWithInstance: false, + }, + }, + }, + expected: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "disks": tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, + }, + }, + }, map[string]tftypes.Value{ + "disks": tftypes.NewValue(tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "disk0"), + "delete_with_instance": tftypes.NewValue(tftypes.Bool, true), + }), + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "disk1"), + "delete_with_instance": tftypes.NewValue(tftypes.Bool, false), + }), + }), + }), + }, "AttrTypeWithValidateError": { state: State{ Raw: tftypes.Value{}, @@ -1564,6 +2052,181 @@ func TestStateSetAttribute(t *testing.T) { "other": tftypes.NewValue(tftypes.String, "should be untouched"), }), }, + "set": { + state: State{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "tags": tftypes.Set{ElementType: tftypes.String}, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "tags": tftypes.NewValue(tftypes.Set{ + ElementType: tftypes.String, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "red"), + tftypes.NewValue(tftypes.String, "blue"), + tftypes.NewValue(tftypes.String, "green"), + }), + "other": tftypes.NewValue(tftypes.String, "should be untouched"), + }), + Schema: Schema{ + Attributes: map[string]Attribute{ + "tags": { + Type: types.SetType{ + ElemType: types.StringType, + }, + Required: true, + }, + "other": { + Type: types.StringType, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("tags"), + val: []string{"one", "two"}, + expected: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "tags": tftypes.Set{ElementType: tftypes.String}, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "tags": tftypes.NewValue(tftypes.Set{ + ElementType: tftypes.String, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "one"), + tftypes.NewValue(tftypes.String, "two"), + }), + "other": tftypes.NewValue(tftypes.String, "should be untouched"), + }), + }, + "set-element": { + state: State{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "disks": tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, + }, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "disks": tftypes.NewValue(tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "disk0"), + "delete_with_instance": tftypes.NewValue(tftypes.Bool, true), + }), + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "disk1"), + "delete_with_instance": tftypes.NewValue(tftypes.Bool, false), + }), + }), + "other": tftypes.NewValue(tftypes.String, "should be untouched"), + }), + Schema: Schema{ + Attributes: map[string]Attribute{ + "disks": { + Attributes: SetNestedAttributes(map[string]Attribute{ + "id": { + Type: types.StringType, + Required: true, + }, + "delete_with_instance": { + Type: types.BoolType, + Optional: true, + }, + }, SetNestedAttributesOptions{}), + Optional: true, + Computed: true, + }, + "other": { + Type: types.StringType, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("disks").WithElementKeyValue(tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "disk1"), + "delete_with_instance": tftypes.NewValue(tftypes.Bool, false), + })), + val: struct { + ID string `tfsdk:"id"` + DeleteWithInstance bool `tfsdk:"delete_with_instance"` + }{ + ID: "mynewdisk", + DeleteWithInstance: true, + }, + expected: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "disks": tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, + }, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "disks": tftypes.NewValue(tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "disk0"), + "delete_with_instance": tftypes.NewValue(tftypes.Bool, true), + }), + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "mynewdisk"), + "delete_with_instance": tftypes.NewValue(tftypes.Bool, true), + }), + }), + "other": tftypes.NewValue(tftypes.String, "should be untouched"), + }), + }, "AttrTypeWithValidateError": { state: State{ Raw: tftypes.NewValue(tftypes.Object{ diff --git a/types/object_test.go b/types/object_test.go index 11a32a0fd..ea5ea2a6b 100644 --- a/types/object_test.go +++ b/types/object_test.go @@ -628,6 +628,7 @@ func TestObjectToTerraformValue(t *testing.T) { "name": StringType, }, }, + "f": SetType{ElemType: StringType}, }, Attrs: map[string]attr.Value{ "a": List{ @@ -648,6 +649,13 @@ func TestObjectToTerraformValue(t *testing.T) { "name": String{Value: "testing123"}, }, }, + "f": Set{ + ElemType: StringType, + Elems: []attr.Value{ + String{Value: "hello"}, + String{Value: "world"}, + }, + }, }, }, expected: map[string]tftypes.Value{ @@ -665,6 +673,10 @@ func TestObjectToTerraformValue(t *testing.T) { }, map[string]tftypes.Value{ "name": tftypes.NewValue(tftypes.String, "testing123"), }), + "f": tftypes.NewValue(tftypes.Set{ElementType: tftypes.String}, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "hello"), + tftypes.NewValue(tftypes.String, "world"), + }), }, }, "unknown": { @@ -679,6 +691,7 @@ func TestObjectToTerraformValue(t *testing.T) { "name": StringType, }, }, + "f": SetType{ElemType: StringType}, }, Unknown: true, }, @@ -696,6 +709,7 @@ func TestObjectToTerraformValue(t *testing.T) { "name": StringType, }, }, + "f": SetType{ElemType: StringType}, }, Null: true, }, @@ -713,6 +727,7 @@ func TestObjectToTerraformValue(t *testing.T) { "name": StringType, }, }, + "f": SetType{ElemType: StringType}, }, Attrs: map[string]attr.Value{ "a": List{ @@ -733,6 +748,13 @@ func TestObjectToTerraformValue(t *testing.T) { "name": String{Value: "testing123"}, }, }, + "f": Set{ + ElemType: StringType, + Elems: []attr.Value{ + String{Value: "hello"}, + String{Value: "world"}, + }, + }, }, }, expected: map[string]tftypes.Value{ @@ -750,6 +772,10 @@ func TestObjectToTerraformValue(t *testing.T) { }, map[string]tftypes.Value{ "name": tftypes.NewValue(tftypes.String, "testing123"), }), + "f": tftypes.NewValue(tftypes.Set{ElementType: tftypes.String}, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "hello"), + tftypes.NewValue(tftypes.String, "world"), + }), }, }, "partial-null": { @@ -764,6 +790,7 @@ func TestObjectToTerraformValue(t *testing.T) { "name": StringType, }, }, + "f": SetType{ElemType: StringType}, }, Attrs: map[string]attr.Value{ "a": List{ @@ -784,6 +811,13 @@ func TestObjectToTerraformValue(t *testing.T) { "name": String{Value: "testing123"}, }, }, + "f": Set{ + ElemType: StringType, + Elems: []attr.Value{ + String{Value: "hello"}, + String{Value: "world"}, + }, + }, }, }, expected: map[string]tftypes.Value{ @@ -801,6 +835,10 @@ func TestObjectToTerraformValue(t *testing.T) { }, map[string]tftypes.Value{ "name": tftypes.NewValue(tftypes.String, "testing123"), }), + "f": tftypes.NewValue(tftypes.Set{ElementType: tftypes.String}, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "hello"), + tftypes.NewValue(tftypes.String, "world"), + }), }, }, "deep-partial-unknown": { @@ -815,6 +853,7 @@ func TestObjectToTerraformValue(t *testing.T) { "name": StringType, }, }, + "f": SetType{ElemType: StringType}, }, Attrs: map[string]attr.Value{ "a": List{ @@ -835,6 +874,13 @@ func TestObjectToTerraformValue(t *testing.T) { "name": String{Unknown: true}, }, }, + "f": List{ + ElemType: StringType, + Elems: []attr.Value{ + String{Value: "hello"}, + String{Value: "world"}, + }, + }, }, }, expected: map[string]tftypes.Value{ @@ -852,6 +898,10 @@ func TestObjectToTerraformValue(t *testing.T) { }, map[string]tftypes.Value{ "name": tftypes.NewValue(tftypes.String, tftypes.UnknownValue), }), + "f": tftypes.NewValue(tftypes.Set{ElementType: tftypes.String}, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "hello"), + tftypes.NewValue(tftypes.String, "world"), + }), }, }, "deep-partial-null": { @@ -866,6 +916,7 @@ func TestObjectToTerraformValue(t *testing.T) { "name": StringType, }, }, + "f": SetType{ElemType: StringType}, }, Attrs: map[string]attr.Value{ "a": List{ @@ -886,6 +937,13 @@ func TestObjectToTerraformValue(t *testing.T) { "name": String{Null: true}, }, }, + "f": Set{ + ElemType: StringType, + Elems: []attr.Value{ + String{Value: "hello"}, + String{Value: "world"}, + }, + }, }, }, expected: map[string]tftypes.Value{ @@ -903,6 +961,10 @@ func TestObjectToTerraformValue(t *testing.T) { }, map[string]tftypes.Value{ "name": tftypes.NewValue(tftypes.String, nil), }), + "f": tftypes.NewValue(tftypes.Set{ElementType: tftypes.String}, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "hello"), + tftypes.NewValue(tftypes.String, "world"), + }), }, }, } diff --git a/types/set.go b/types/set.go new file mode 100644 index 000000000..93fdbf50d --- /dev/null +++ b/types/set.go @@ -0,0 +1,217 @@ +package types + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/reflect" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +var ( + _ attr.Type = SetType{} + _ attr.Value = &Set{} +) + +// SetType is an AttributeType representing a set of values. All values must +// be of the same type, which the provider must specify as the ElemType +// property. +type SetType struct { + ElemType attr.Type +} + +// ElementType returns the attr.Type elements will be created from. +func (t SetType) ElementType() attr.Type { + return t.ElemType +} + +// WithElementType returns a SetType that is identical to `l`, but with the +// element type set to `typ`. +func (t SetType) WithElementType(typ attr.Type) attr.TypeWithElementType { + return SetType{ElemType: typ} +} + +// TerraformType returns the tftypes.Type that should be used to +// represent this type. This constrains what user input will be +// accepted and what kind of data can be set in state. The framework +// will use this to translate the AttributeType to something Terraform +// can understand. +func (t SetType) TerraformType(ctx context.Context) tftypes.Type { + return tftypes.Set{ + ElementType: t.ElemType.TerraformType(ctx), + } +} + +// ValueFromTerraform returns an AttributeValue given a tftypes.Value. +// This is meant to convert the tftypes.Value into a more convenient Go +// type for the provider to consume the data with. +func (t SetType) ValueFromTerraform(ctx context.Context, in tftypes.Value) (attr.Value, error) { + if !in.Type().Is(t.TerraformType(ctx)) { + return nil, fmt.Errorf("can't use %s as value of Set with ElementType %T, can only use %s values", in.String(), t.ElemType, t.ElemType.TerraformType(ctx).String()) + } + set := Set{ + ElemType: t.ElemType, + } + if !in.IsKnown() { + set.Unknown = true + return set, nil + } + if in.IsNull() { + set.Null = true + return set, nil + } + val := []tftypes.Value{} + err := in.As(&val) + if err != nil { + return nil, err + } + elems := make([]attr.Value, 0, len(val)) + for _, elem := range val { + av, err := t.ElemType.ValueFromTerraform(ctx, elem) + if err != nil { + return nil, err + } + elems = append(elems, av) + } + set.Elems = elems + return set, nil +} + +// Equal returns true if `o` is also a SetType and has the same ElemType. +func (t SetType) Equal(o attr.Type) bool { + if t.ElemType == nil { + return false + } + other, ok := o.(SetType) + if !ok { + return false + } + return t.ElemType.Equal(other.ElemType) +} + +// ApplyTerraform5AttributePathStep applies the given AttributePathStep to the +// set. +func (t SetType) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { + if _, ok := step.(tftypes.ElementKeyValue); !ok { + return nil, fmt.Errorf("cannot apply step %T to SetType", step) + } + + return t.ElemType, nil +} + +// Set represents a set of AttributeValues, all of the same type, indicated +// by ElemType. +type Set struct { + // Unknown will be set to true if the entire set is an unknown value. + // If only some of the elements in the set are unknown, their known or + // unknown status will be represented however that AttributeValue + // surfaces that information. The Set's Unknown property only tracks + // if the number of elements in a Set is known, not whether the + // elements that are in the set are known. + Unknown bool + + // Null will be set to true if the set is null, either because it was + // omitted from the configuration, state, or plan, or because it was + // explicitly set to null. + Null bool + + // Elems are the elements in the set. + Elems []attr.Value + + // ElemType is the tftypes.Type of the elements in the set. All + // elements in the set must be of this type. + ElemType attr.Type +} + +// ElementsAs populates `target` with the elements of the Set, throwing an +// error if the elements cannot be stored in `target`. +func (s Set) ElementsAs(ctx context.Context, target interface{}, allowUnhandled bool) diag.Diagnostics { + // we need a tftypes.Value for this Set to be able to use it with our + // reflection code + values, err := s.ToTerraformValue(ctx) + if err != nil { + return diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Set Element Conversion Error", + "An unexpected error was encountered trying to convert set elements. This is always an error in the provider. Please report the following to the provider developer:\n\n"+err.Error(), + ), + } + } + return reflect.Into(ctx, SetType{ElemType: s.ElemType}, tftypes.NewValue(tftypes.Set{ + ElementType: s.ElemType.TerraformType(ctx), + }, values), target, reflect.Options{ + UnhandledNullAsEmpty: allowUnhandled, + UnhandledUnknownAsEmpty: allowUnhandled, + }) +} + +// Type returns a SetType with the same element type as `s`. +func (s Set) Type(ctx context.Context) attr.Type { + return SetType{ElemType: s.ElemType} +} + +// ToTerraformValue returns the data contained in the AttributeValue as +// a Go type that tftypes.NewValue will accept. +func (s Set) ToTerraformValue(ctx context.Context) (interface{}, error) { + if s.Unknown { + return tftypes.UnknownValue, nil + } + if s.Null { + return nil, nil + } + vals := make([]tftypes.Value, 0, len(s.Elems)) + for _, elem := range s.Elems { + val, err := elem.ToTerraformValue(ctx) + if err != nil { + return nil, err + } + err = tftypes.ValidateValue(s.ElemType.TerraformType(ctx), val) + if err != nil { + return nil, fmt.Errorf("error validating terraform type: %w", err) + } + vals = append(vals, tftypes.NewValue(s.ElemType.TerraformType(ctx), val)) + } + return vals, nil +} + +// Equal must return true if the AttributeValue is considered +// semantically equal to the AttributeValue passed as an argument. +func (s Set) Equal(o attr.Value) bool { + other, ok := o.(Set) + if !ok { + return false + } + if s.Unknown != other.Unknown { + return false + } + if s.Null != other.Null { + return false + } + if s.ElemType == nil && other.ElemType != nil { + return false + } + if s.ElemType != nil && !s.ElemType.Equal(other.ElemType) { + return false + } + if len(s.Elems) != len(other.Elems) { + return false + } + for _, elem := range s.Elems { + if !other.contains(elem) { + return false + } + } + return true +} + +func (s Set) contains(v attr.Value) bool { + for _, elem := range s.Elems { + if elem.Equal(v) { + return true + } + } + + return false +} diff --git a/types/set_test.go b/types/set_test.go new file mode 100644 index 000000000..a05bf0603 --- /dev/null +++ b/types/set_test.go @@ -0,0 +1,646 @@ +package types + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestSetTypeTerraformType(t *testing.T) { + t.Parallel() + + type testCase struct { + input SetType + expected tftypes.Type + } + tests := map[string]testCase{ + "set-of-strings": { + input: SetType{ + ElemType: StringType, + }, + expected: tftypes.Set{ + ElementType: tftypes.String, + }, + }, + "set-of-set-of-strings": { + input: SetType{ + ElemType: SetType{ + ElemType: StringType, + }, + }, + expected: tftypes.Set{ + ElementType: tftypes.Set{ + ElementType: tftypes.String, + }, + }, + }, + "set-of-set-of-set-of-strings": { + input: SetType{ + ElemType: SetType{ + ElemType: SetType{ + ElemType: StringType, + }, + }, + }, + expected: tftypes.Set{ + ElementType: tftypes.Set{ + ElementType: tftypes.Set{ + ElementType: tftypes.String, + }, + }, + }, + }, + } + for name, test := range tests { + name, test := name, test + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := test.input.TerraformType(context.Background()) + if !got.Is(test.expected) { + t.Errorf("Expected %s, got %s", test.expected, got) + } + }) + } +} + +func TestSetTypeValueFromTerraform(t *testing.T) { + t.Parallel() + + type testCase struct { + receiver SetType + input tftypes.Value + expected attr.Value + expectedErr string + } + tests := map[string]testCase{ + "set-of-strings": { + receiver: SetType{ + ElemType: StringType, + }, + input: tftypes.NewValue(tftypes.Set{ + ElementType: tftypes.String, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "hello"), + tftypes.NewValue(tftypes.String, "world"), + }), + expected: Set{ + ElemType: StringType, + Elems: []attr.Value{ + String{Value: "hello"}, + String{Value: "world"}, + }, + }, + }, + "unknown-set": { + receiver: SetType{ + ElemType: StringType, + }, + input: tftypes.NewValue(tftypes.Set{ + ElementType: tftypes.String, + }, tftypes.UnknownValue), + expected: Set{ + ElemType: StringType, + Unknown: true, + }, + }, + "partially-unknown-set": { + receiver: SetType{ + ElemType: StringType, + }, + input: tftypes.NewValue(tftypes.Set{ + ElementType: tftypes.String, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "hello"), + tftypes.NewValue(tftypes.String, tftypes.UnknownValue), + }), + expected: Set{ + ElemType: StringType, + Elems: []attr.Value{ + String{Value: "hello"}, + String{Unknown: true}, + }, + }, + }, + "null-set": { + receiver: SetType{ + ElemType: StringType, + }, + input: tftypes.NewValue(tftypes.Set{ + ElementType: tftypes.String, + }, nil), + expected: Set{ + ElemType: StringType, + Null: true, + }, + }, + "partially-null-set": { + receiver: SetType{ + ElemType: StringType, + }, + input: tftypes.NewValue(tftypes.Set{ + ElementType: tftypes.String, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "hello"), + tftypes.NewValue(tftypes.String, nil), + }), + expected: Set{ + ElemType: StringType, + Elems: []attr.Value{ + String{Value: "hello"}, + String{Null: true}, + }, + }, + }, + } + for name, test := range tests { + name, test := name, test + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, gotErr := test.receiver.ValueFromTerraform(context.Background(), test.input) + if gotErr != nil { + if test.expectedErr != "" { + if gotErr.Error() != test.expectedErr { + t.Errorf("Expected error to be %q, got %q", test.expectedErr, gotErr.Error()) + return + } + } + t.Errorf("Unexpected error: %s", gotErr.Error()) + return + } + if gotErr == nil && test.expectedErr != "" { + t.Errorf("Expected error to be %q, got nil", test.expectedErr) + return + } + if diff := cmp.Diff(got, test.expected); diff != "" { + t.Errorf("Unexpected diff (-expected, +got): %s", diff) + } + }) + } +} + +func TestSetTypeEqual(t *testing.T) { + t.Parallel() + + type testCase struct { + receiver SetType + input attr.Type + expected bool + } + tests := map[string]testCase{ + "equal": { + receiver: SetType{ElemType: StringType}, + input: SetType{ElemType: StringType}, + expected: true, + }, + "diff": { + receiver: SetType{ElemType: StringType}, + input: SetType{ElemType: NumberType}, + expected: false, + }, + "wrongType": { + receiver: SetType{ElemType: StringType}, + input: NumberType, + expected: false, + }, + "nil": { + receiver: SetType{ElemType: StringType}, + input: nil, + expected: false, + }, + "nil-elem": { + receiver: SetType{}, + input: SetType{}, + // SetTypes with nil ElemTypes are invalid, and + // aren't equal to anything + expected: false, + }, + } + for name, test := range tests { + name, test := name, test + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := test.receiver.Equal(test.input) + if test.expected != got { + t.Errorf("Expected %v, got %v", test.expected, got) + } + }) + } +} + +func TestSetElementsAs_stringSlice(t *testing.T) { + t.Parallel() + + var stringSlice []string + expected := []string{"hello", "world"} + + diags := (Set{ + ElemType: StringType, + Elems: []attr.Value{ + String{Value: "hello"}, + String{Value: "world"}, + }}).ElementsAs(context.Background(), &stringSlice, false) + if diags.HasError() { + t.Errorf("Unexpected error: %s", diags) + } + if diff := cmp.Diff(stringSlice, expected); diff != "" { + t.Errorf("Unexpected diff (-expected, +got): %s", diff) + } +} + +func TestSetElementsAs_attributeValueSlice(t *testing.T) { + t.Parallel() + + var stringSlice []String + expected := []String{ + {Value: "hello"}, + {Value: "world"}, + } + + diags := (Set{ + ElemType: StringType, + Elems: []attr.Value{ + String{Value: "hello"}, + String{Value: "world"}, + }}).ElementsAs(context.Background(), &stringSlice, false) + if diags.HasError() { + t.Errorf("Unexpected error: %s", diags) + } + if diff := cmp.Diff(stringSlice, expected); diff != "" { + t.Errorf("Unexpected diff (-expected, +got): %s", diff) + } +} + +func TestSetToTerraformValue(t *testing.T) { + t.Parallel() + + type testCase struct { + input Set + expectation interface{} + } + tests := map[string]testCase{ + "value": { + input: Set{ + ElemType: StringType, + Elems: []attr.Value{ + String{Value: "hello"}, + String{Value: "world"}, + }, + }, + expectation: []tftypes.Value{ + tftypes.NewValue(tftypes.String, "hello"), + tftypes.NewValue(tftypes.String, "world"), + }, + }, + "unknown": { + input: Set{Unknown: true}, + expectation: tftypes.UnknownValue, + }, + "null": { + input: Set{Null: true}, + expectation: nil, + }, + "partial-unknown": { + input: Set{ + ElemType: StringType, + Elems: []attr.Value{ + String{Unknown: true}, + String{Value: "hello, world"}, + }, + }, + expectation: []tftypes.Value{ + tftypes.NewValue(tftypes.String, tftypes.UnknownValue), + tftypes.NewValue(tftypes.String, "hello, world"), + }, + }, + "partial-null": { + input: Set{ + ElemType: StringType, + Elems: []attr.Value{ + String{Null: true}, + String{Value: "hello, world"}, + }, + }, + expectation: []tftypes.Value{ + tftypes.NewValue(tftypes.String, nil), + tftypes.NewValue(tftypes.String, "hello, world"), + }, + }, + } + for name, test := range tests { + name, test := name, test + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := test.input.ToTerraformValue(context.Background()) + if err != nil { + t.Errorf("Unexpected error: %s", err) + return + } + if diff := cmp.Diff(got, test.expectation); diff != "" { + t.Errorf("Unexpected result (+got, -expected): %s", diff) + } + }) + } +} + +func TestSetEqual(t *testing.T) { + t.Parallel() + + type testCase struct { + receiver Set + input attr.Value + expected bool + } + tests := map[string]testCase{ + "set-value-set-value": { + receiver: Set{ + ElemType: StringType, + Elems: []attr.Value{ + String{Value: "hello"}, + String{Value: "world"}, + }, + }, + input: Set{ + ElemType: StringType, + Elems: []attr.Value{ + String{Value: "hello"}, + String{Value: "world"}, + }, + }, + expected: true, + }, + "set-value-diff": { + receiver: Set{ + ElemType: StringType, + Elems: []attr.Value{ + String{Value: "hello"}, + String{Value: "world"}, + }, + }, + input: Set{ + ElemType: StringType, + Elems: []attr.Value{ + String{Value: "goodnight"}, + String{Value: "moon"}, + }, + }, + expected: false, + }, + "set-value-count-diff": { + receiver: Set{ + ElemType: StringType, + Elems: []attr.Value{ + String{Value: "hello"}, + String{Value: "world"}, + }, + }, + input: Set{ + ElemType: StringType, + Elems: []attr.Value{ + String{Value: "hello"}, + String{Value: "world"}, + String{Value: "test"}, + }, + }, + expected: false, + }, + "set-value-type-diff": { + receiver: Set{ + ElemType: StringType, + Elems: []attr.Value{ + String{Value: "hello"}, + String{Value: "world"}, + }, + }, + input: Set{ + ElemType: BoolType, + Elems: []attr.Value{ + Bool{Value: false}, + Bool{Value: true}, + }, + }, + expected: false, + }, + "set-value-unknown": { + receiver: Set{ + ElemType: StringType, + Elems: []attr.Value{ + String{Value: "hello"}, + String{Value: "world"}, + }, + }, + input: Set{Unknown: true}, + expected: false, + }, + "set-value-null": { + receiver: Set{ + ElemType: StringType, + Elems: []attr.Value{ + String{Value: "hello"}, + String{Value: "world"}, + }, + }, + input: Set{Null: true}, + expected: false, + }, + "set-value-wrongType": { + receiver: Set{ + ElemType: StringType, + Elems: []attr.Value{ + String{Value: "hello"}, + String{Value: "world"}, + }, + }, + input: String{Value: "hello, world"}, + expected: false, + }, + "set-value-nil": { + receiver: Set{ + ElemType: StringType, + Elems: []attr.Value{ + String{Value: "hello"}, + String{Value: "world"}, + }, + }, + input: nil, + expected: false, + }, + "partially-known-set-value-set-value": { + receiver: Set{ + ElemType: StringType, + Elems: []attr.Value{ + String{Value: "hello"}, + String{Unknown: true}, + }, + }, + input: Set{ + ElemType: StringType, + Elems: []attr.Value{ + String{Value: "hello"}, + String{Unknown: true}, + }, + }, + expected: true, + }, + "partially-known-set-value-diff": { + receiver: Set{ + ElemType: StringType, + Elems: []attr.Value{ + String{Value: "hello"}, + String{Unknown: true}, + }, + }, + input: Set{ + ElemType: StringType, + Elems: []attr.Value{ + String{Value: "hello"}, + String{Value: "world"}, + }, + }, + expected: false, + }, + "partially-known-set-value-unknown": { + receiver: Set{ + ElemType: StringType, + Elems: []attr.Value{ + String{Value: "hello"}, + String{Unknown: true}, + }, + }, + input: Set{Unknown: true}, + expected: false, + }, + "partially-known-set-value-null": { + receiver: Set{ + ElemType: StringType, + Elems: []attr.Value{ + String{Value: "hello"}, + String{Unknown: true}, + }, + }, + input: Set{Null: true}, + expected: false, + }, + "partially-known-set-value-wrongType": { + receiver: Set{ + ElemType: StringType, + Elems: []attr.Value{ + String{Value: "hello"}, + String{Unknown: true}, + }, + }, + input: String{Value: "hello, world"}, + expected: false, + }, + "partially-known-set-value-nil": { + receiver: Set{ + ElemType: StringType, + Elems: []attr.Value{ + String{Value: "hello"}, + String{Unknown: true}, + }, + }, + input: nil, + expected: false, + }, + "partially-null-set-value-set-value": { + receiver: Set{ + ElemType: StringType, + Elems: []attr.Value{ + String{Value: "hello"}, + String{Null: true}, + }, + }, + input: Set{ + ElemType: StringType, + Elems: []attr.Value{ + String{Value: "hello"}, + String{Null: true}, + }, + }, + expected: true, + }, + "partially-null-set-value-diff": { + receiver: Set{ + ElemType: StringType, + Elems: []attr.Value{ + String{Value: "hello"}, + String{Null: true}, + }, + }, + input: Set{ + ElemType: StringType, + Elems: []attr.Value{ + String{Value: "hello"}, + String{Value: "world"}, + }, + }, + expected: false, + }, + "partially-null-set-value-unknown": { + receiver: Set{ + ElemType: StringType, + Elems: []attr.Value{ + String{Value: "hello"}, + String{Null: true}, + }, + }, + input: Set{ + Unknown: true, + }, + expected: false, + }, + "partially-null-set-value-null": { + receiver: Set{ + ElemType: StringType, + Elems: []attr.Value{ + String{Value: "hello"}, + String{Null: true}, + }, + }, + input: Set{ + Null: true, + }, + expected: false, + }, + "partially-null-set-value-wrongType": { + receiver: Set{ + ElemType: StringType, + Elems: []attr.Value{ + String{Value: "hello"}, + String{Null: true}, + }, + }, + input: String{Value: "hello, world"}, + expected: false, + }, + "partially-null-set-value-nil": { + receiver: Set{ + ElemType: StringType, + Elems: []attr.Value{ + String{Value: "hello"}, + String{Null: true}, + }, + }, + input: nil, + expected: false, + }, + } + for name, test := range tests { + name, test := name, test + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := test.receiver.Equal(test.input) + if got != test.expected { + t.Errorf("Expected %v, got %v", test.expected, got) + } + }) + } +} From 7a235c4adff374ae77c9b1fcba0189a4e8bdc989 Mon Sep 17 00:00:00 2001 From: Brian Flad Date: Fri, 3 Sep 2021 08:34:27 -0400 Subject: [PATCH 2/8] Update CHANGELOG for #126 --- .changelog/{pending.txt => 126.txt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .changelog/{pending.txt => 126.txt} (100%) diff --git a/.changelog/pending.txt b/.changelog/126.txt similarity index 100% rename from .changelog/pending.txt rename to .changelog/126.txt From df30eb767b7b159945e92d75c31c1116cc00038a Mon Sep 17 00:00:00 2001 From: Brian Flad Date: Thu, 9 Sep 2021 16:48:00 -0400 Subject: [PATCH 3/8] types: Add SetType Validate method Provides basic element duplicate detection. --- types/set.go | 63 ++++++++++++++- types/set_test.go | 197 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 258 insertions(+), 2 deletions(-) diff --git a/types/set.go b/types/set.go index 93fdbf50d..bdc96fe30 100644 --- a/types/set.go +++ b/types/set.go @@ -11,8 +11,8 @@ import ( ) var ( - _ attr.Type = SetType{} - _ attr.Value = &Set{} + _ attr.TypeWithValidate = SetType{} + _ attr.Value = &Set{} ) // SetType is an AttributeType representing a set of values. All values must @@ -101,6 +101,65 @@ func (t SetType) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep return t.ElemType, nil } +// Validate implements type validation. This type requires all elements to be +// unique. +func (s SetType) Validate(ctx context.Context, in tftypes.Value, path *tftypes.AttributePath) diag.Diagnostics { + var diags diag.Diagnostics + + if !in.Type().Is(tftypes.Set{}) { + err := fmt.Errorf("expected Set value, received %T with value: %v", in, in) + diags.AddAttributeError( + path, + "Set Type Validation Error", + "An unexpected error was encountered trying to validate an attribute value. This is always an error in the provider. Please report the following to the provider developer:\n\n"+err.Error(), + ) + return diags + } + + if !in.IsKnown() { + return diags + } + + var elems []tftypes.Value + + if err := in.As(&elems); err != nil { + diags.AddAttributeError( + path, + "Set Type Validation Error", + "An unexpected error was encountered trying to validate an attribute value. This is always an error in the provider. Please report the following to the provider developer:\n\n"+err.Error(), + ) + return diags + } + + // Attempting to use map[tftypes.Value]struct{} for duplicate detection yields: + // panic: runtime error: hash of unhashable type tftypes.primitive + // Instead, use for loops. + for indexOuter, elemOuter := range elems { + // Only evaluate fully known values for duplicates. + if !elemOuter.IsFullyKnown() { + continue + } + + for indexInner, elemInner := range elems { + if indexInner == indexOuter { + continue + } + + if !elemInner.Equal(elemOuter) { + continue + } + + diags.AddAttributeError( + path.WithElementKeyValue(elemInner), + "Duplicate Set Element", + fmt.Sprintf("This attribute contains duplicate values of: %s", elemInner), + ) + } + } + + return diags +} + // Set represents a set of AttributeValues, all of the same type, indicated // by ElemType. type Set struct { diff --git a/types/set_test.go b/types/set_test.go index a05bf0603..128d932d8 100644 --- a/types/set_test.go +++ b/types/set_test.go @@ -6,6 +6,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-go/tftypes" ) @@ -95,6 +96,26 @@ func TestSetTypeValueFromTerraform(t *testing.T) { }, }, }, + "set-of-duplicate-strings": { + receiver: SetType{ + ElemType: StringType, + }, + input: tftypes.NewValue(tftypes.Set{ + ElementType: tftypes.String, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "hello"), + tftypes.NewValue(tftypes.String, "hello"), + }), + // Duplicate validation does not occur during this method. + // This is okay, as tftypes allows duplicates. + expected: Set{ + ElemType: StringType, + Elems: []attr.Value{ + String{Value: "hello"}, + String{Value: "hello"}, + }, + }, + }, "unknown-set": { receiver: SetType{ ElemType: StringType, @@ -276,6 +297,167 @@ func TestSetElementsAs_attributeValueSlice(t *testing.T) { } } +func TestSetTypeValidate(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + in tftypes.Value + expectedDiags diag.Diagnostics + }{ + "null": { + in: tftypes.NewValue( + tftypes.Set{ + ElementType: tftypes.String, + }, + nil, + ), + }, + "null-element": { + in: tftypes.NewValue( + tftypes.Set{ + ElementType: tftypes.String, + }, + []tftypes.Value{ + tftypes.NewValue(tftypes.String, tftypes.UnknownValue), + }, + ), + }, + "null-elements": { + in: tftypes.NewValue( + tftypes.Set{ + ElementType: tftypes.String, + }, + []tftypes.Value{ + tftypes.NewValue(tftypes.String, tftypes.UnknownValue), + tftypes.NewValue(tftypes.String, tftypes.UnknownValue), + }, + ), + }, + "unknown": { + in: tftypes.NewValue( + tftypes.Set{ + ElementType: tftypes.String, + }, + tftypes.UnknownValue, + ), + }, + "unknown-element": { + in: tftypes.NewValue( + tftypes.Set{ + ElementType: tftypes.String, + }, + []tftypes.Value{ + tftypes.NewValue(tftypes.String, tftypes.UnknownValue), + }, + ), + }, + "unknown-elements": { + in: tftypes.NewValue( + tftypes.Set{ + ElementType: tftypes.String, + }, + []tftypes.Value{ + tftypes.NewValue(tftypes.String, tftypes.UnknownValue), + tftypes.NewValue(tftypes.String, tftypes.UnknownValue), + }, + ), + }, + "value": { + in: tftypes.NewValue( + tftypes.Set{ + ElementType: tftypes.String, + }, + []tftypes.Value{ + tftypes.NewValue(tftypes.String, "hello"), + }, + ), + }, + "value-and-null": { + in: tftypes.NewValue( + tftypes.Set{ + ElementType: tftypes.String, + }, + []tftypes.Value{ + tftypes.NewValue(tftypes.String, "hello"), + tftypes.NewValue(tftypes.String, nil), + }, + ), + }, + "value-and-unknown": { + in: tftypes.NewValue( + tftypes.Set{ + ElementType: tftypes.String, + }, + []tftypes.Value{ + tftypes.NewValue(tftypes.String, "hello"), + tftypes.NewValue(tftypes.String, tftypes.UnknownValue), + }, + ), + }, + "values": { + in: tftypes.NewValue( + tftypes.Set{ + ElementType: tftypes.String, + }, + []tftypes.Value{ + tftypes.NewValue(tftypes.String, "hello"), + tftypes.NewValue(tftypes.String, "world"), + }, + ), + }, + "values-duplicates": { + in: tftypes.NewValue( + tftypes.Set{ + ElementType: tftypes.String, + }, + []tftypes.Value{ + tftypes.NewValue(tftypes.String, "hello"), + tftypes.NewValue(tftypes.String, "hello"), + }, + ), + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + tftypes.NewAttributePath().WithAttributeName("test").WithElementKeyValue(tftypes.NewValue(tftypes.String, "hello")), + "Duplicate Set Element", + "This attribute contains duplicate values of: tftypes.String<\"hello\">", + ), + }, + }, + "values-duplicates-and-unknowns": { + in: tftypes.NewValue( + tftypes.Set{ + ElementType: tftypes.String, + }, + []tftypes.Value{ + tftypes.NewValue(tftypes.String, tftypes.UnknownValue), + tftypes.NewValue(tftypes.String, "hello"), + tftypes.NewValue(tftypes.String, tftypes.UnknownValue), + tftypes.NewValue(tftypes.String, "hello"), + }, + ), + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + tftypes.NewAttributePath().WithAttributeName("test").WithElementKeyValue(tftypes.NewValue(tftypes.String, "hello")), + "Duplicate Set Element", + "This attribute contains duplicate values of: tftypes.String<\"hello\">", + ), + }, + }, + } + for name, testCase := range testCases { + name, testCase := name, testCase + t.Run(name, func(t *testing.T) { + t.Parallel() + + diags := SetType{}.Validate(context.Background(), testCase.in, tftypes.NewAttributePath().WithAttributeName("test")) + + if diff := cmp.Diff(diags, testCase.expectedDiags); diff != "" { + t.Errorf("Unexpected diagnostics (+got, -expected): %s", diff) + } + }) + } +} + func TestSetToTerraformValue(t *testing.T) { t.Parallel() @@ -297,6 +479,21 @@ func TestSetToTerraformValue(t *testing.T) { tftypes.NewValue(tftypes.String, "world"), }, }, + "value-duplicates": { + input: Set{ + ElemType: StringType, + Elems: []attr.Value{ + String{Value: "hello"}, + String{Value: "hello"}, + }, + }, + // Duplicate validation does not occur during this method. + // This is okay, as tftypes allows duplicates. + expectation: []tftypes.Value{ + tftypes.NewValue(tftypes.String, "hello"), + tftypes.NewValue(tftypes.String, "hello"), + }, + }, "unknown": { input: Set{Unknown: true}, expectation: tftypes.UnknownValue, From 2763352677c6984031d1910748e31916f47a7ec6 Mon Sep 17 00:00:00 2001 From: Brian Flad Date: Thu, 9 Sep 2021 19:45:12 -0400 Subject: [PATCH 4/8] internal/reflect: Fix ElementType validation and paths with slices --- .changelog/126.txt | 4 ++++ internal/reflect/slice.go | 29 ++++++++++++++++++++++++----- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/.changelog/126.txt b/.changelog/126.txt index 663a0d6e3..4baaadae7 100644 --- a/.changelog/126.txt +++ b/.changelog/126.txt @@ -1,3 +1,7 @@ ```release-note:feature types: Support `Set` and `SetType` ``` + +```release-note:bug +attr: Ensure `List` types implementing `attr.TypeWithValidate` call `ElementType` validation only if that type implements `attr.TypeWithValidate` +``` diff --git a/internal/reflect/slice.go b/internal/reflect/slice.go index 097e6c46d..314b896ad 100644 --- a/internal/reflect/slice.go +++ b/internal/reflect/slice.go @@ -65,10 +65,14 @@ func reflectSlice(ctx context.Context, typ attr.Type, val tftypes.Value, target targetValue := reflect.Zero(elemType) // update our path so we can have nice errors - path := path.WithElementKeyInt(int64(pos)) + valPath := path.WithElementKeyInt(int64(pos)) + + if typ.TerraformType(ctx).Is(tftypes.Set{}) { + valPath = path.WithElementKeyValue(value) + } // reflect the value into our new target - val, valDiags := BuildValue(ctx, elemAttrType, value, targetValue, opts, path) + val, valDiags := BuildValue(ctx, elemAttrType, value, targetValue, opts, valPath) diags.Append(valDiags...) if diags.HasError() { @@ -135,25 +139,40 @@ func FromSlice(ctx context.Context, typ attr.Type, val reflect.Value, path *tfty elemType := t.ElementType() tfElems := make([]tftypes.Value, 0, val.Len()) for i := 0; i < val.Len(); i++ { - val, valDiags := FromValue(ctx, elemType, val.Index(i).Interface(), path.WithElementKeyInt(int64(i))) + // The underlying reflect.Slice is fetched by Index(). For set types, + // the path is value-based instead of index-based. Since there is only + // the index until the value is retrieved, this will pass the + // technically incorrect index-based path at first for framework + // debugging purposes, then correct the path afterwards. + valPath := path.WithElementKeyInt(int64(i)) + + val, valDiags := FromValue(ctx, elemType, val.Index(i).Interface(), valPath) diags.Append(valDiags...) if diags.HasError() { return nil, diags } + tfVal, err := val.ToTerraformValue(ctx) + if err != nil { return nil, append(diags, toTerraformValueErrorDiag(err, path)) } + err = tftypes.ValidateValue(elemType.TerraformType(ctx), tfVal) + if err != nil { return nil, append(diags, validateValueErrorDiag(err, path)) } tfElemVal := tftypes.NewValue(elemType.TerraformType(ctx), tfVal) - if typeWithValidate, ok := typ.(attr.TypeWithValidate); ok { - diags.Append(typeWithValidate.Validate(ctx, tfElemVal, path.WithElementKeyInt(int64(i)))...) + if tfType.Is(tftypes.Set{}) { + valPath = path.WithElementKeyValue(tfElemVal) + } + + if typeWithValidate, ok := elemType.(attr.TypeWithValidate); ok { + diags.Append(typeWithValidate.Validate(ctx, tfElemVal, valPath)...) if diags.HasError() { return nil, diags From 744697e0e39dcce0622362a6b039ca803c361429 Mon Sep 17 00:00:00 2001 From: Brian Flad Date: Mon, 13 Sep 2021 10:01:19 -0400 Subject: [PATCH 5/8] types: Apply suggestions from code review --- types/set.go | 5 ++++- types/set_test.go | 13 ++++++++++--- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/types/set.go b/types/set.go index bdc96fe30..3fd2a913a 100644 --- a/types/set.go +++ b/types/set.go @@ -116,7 +116,7 @@ func (s SetType) Validate(ctx context.Context, in tftypes.Value, path *tftypes.A return diags } - if !in.IsKnown() { + if !in.IsKnown() || in.IsNull() { return diags } @@ -251,6 +251,9 @@ func (s Set) Equal(o attr.Value) bool { if s.ElemType == nil && other.ElemType != nil { return false } + if other.ElemType == nil { + return false + } if s.ElemType != nil && !s.ElemType.Equal(other.ElemType) { return false } diff --git a/types/set_test.go b/types/set_test.go index 128d932d8..f0f8cec21 100644 --- a/types/set_test.go +++ b/types/set_test.go @@ -318,7 +318,7 @@ func TestSetTypeValidate(t *testing.T) { ElementType: tftypes.String, }, []tftypes.Value{ - tftypes.NewValue(tftypes.String, tftypes.UnknownValue), + tftypes.NewValue(tftypes.String, nil), }, ), }, @@ -328,10 +328,17 @@ func TestSetTypeValidate(t *testing.T) { ElementType: tftypes.String, }, []tftypes.Value{ - tftypes.NewValue(tftypes.String, tftypes.UnknownValue), - tftypes.NewValue(tftypes.String, tftypes.UnknownValue), + tftypes.NewValue(tftypes.String, nil), + tftypes.NewValue(tftypes.String, nil), }, ), + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + tftypes.NewAttributePath().WithAttributeName("test").WithElementKeyValue(tftypes.NewValue(tftypes.String, nil)), + "Duplicate Set Element", + "This attribute contains duplicate values of: tftypes.String", + ), + }, }, "unknown": { in: tftypes.NewValue( From bb01b8cdf07260b7982925ef6ce2153e5deae53e Mon Sep 17 00:00:00 2001 From: Brian Flad Date: Mon, 13 Sep 2021 10:41:38 -0400 Subject: [PATCH 6/8] types: Revert (Set).Equal() change, benchmark (Set).Validate(), adjust (Set).Validate() to be less O(n)^2 --- types/set.go | 9 ++------- types/set_test.go | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 7 deletions(-) diff --git a/types/set.go b/types/set.go index 3fd2a913a..aea474312 100644 --- a/types/set.go +++ b/types/set.go @@ -140,10 +140,8 @@ func (s SetType) Validate(ctx context.Context, in tftypes.Value, path *tftypes.A continue } - for indexInner, elemInner := range elems { - if indexInner == indexOuter { - continue - } + for indexInner := indexOuter + 1; indexInner < len(elems); indexInner++ { + elemInner := elems[indexInner] if !elemInner.Equal(elemOuter) { continue @@ -251,9 +249,6 @@ func (s Set) Equal(o attr.Value) bool { if s.ElemType == nil && other.ElemType != nil { return false } - if other.ElemType == nil { - return false - } if s.ElemType != nil && !s.ElemType.Equal(other.ElemType) { return false } diff --git a/types/set_test.go b/types/set_test.go index f0f8cec21..b3b44668b 100644 --- a/types/set_test.go +++ b/types/set_test.go @@ -2,6 +2,7 @@ package types import ( "context" + "strconv" "testing" "github.com/google/go-cmp/cmp" @@ -297,6 +298,52 @@ func TestSetElementsAs_attributeValueSlice(t *testing.T) { } } +func benchmarkSetTypeValidate(b *testing.B, elementCount int) { + elements := make([]tftypes.Value, 0, elementCount) + + for idx := range elements { + elements[idx] = tftypes.NewValue(tftypes.String, strconv.Itoa(idx)) + } + + ctx := context.Background() + in := tftypes.NewValue( + tftypes.Set{ + ElementType: tftypes.String, + }, + elements, + ) + path := tftypes.NewAttributePath().WithAttributeName("test") + set := SetType{} + + for n := 0; n < b.N; n++ { + set.Validate(ctx, in, path) + } +} + +func BenchmarkSetTypeValidate10(b *testing.B) { + benchmarkSetTypeValidate(b, 10) +} + +func BenchmarkSetTypeValidate100(b *testing.B) { + benchmarkSetTypeValidate(b, 100) +} + +func BenchmarkSetTypeValidate1000(b *testing.B) { + benchmarkSetTypeValidate(b, 1000) +} + +func BenchmarkSetTypeValidate10000(b *testing.B) { + benchmarkSetTypeValidate(b, 10000) +} + +func BenchmarkSetTypeValidate100000(b *testing.B) { + benchmarkSetTypeValidate(b, 100000) +} + +func BenchmarkSetTypeValidate1000000(b *testing.B) { + benchmarkSetTypeValidate(b, 1000000) +} + func TestSetTypeValidate(t *testing.T) { t.Parallel() From e20ed14fc7cd56cdb3e5e0228aaddfa12705e684 Mon Sep 17 00:00:00 2001 From: Brian Flad Date: Mon, 13 Sep 2021 10:56:38 -0400 Subject: [PATCH 7/8] types: Ensure (Set).Validate() benchmarking won't receive compiler optimizations --- types/set_test.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/types/set_test.go b/types/set_test.go index b3b44668b..a24ae8f3b 100644 --- a/types/set_test.go +++ b/types/set_test.go @@ -298,6 +298,8 @@ func TestSetElementsAs_attributeValueSlice(t *testing.T) { } } +var benchDiags diag.Diagnostics // Prevent compiler optimization + func benchmarkSetTypeValidate(b *testing.B, elementCount int) { elements := make([]tftypes.Value, 0, elementCount) @@ -305,6 +307,7 @@ func benchmarkSetTypeValidate(b *testing.B, elementCount int) { elements[idx] = tftypes.NewValue(tftypes.String, strconv.Itoa(idx)) } + var diags diag.Diagnostics // Prevent compiler optimization ctx := context.Background() in := tftypes.NewValue( tftypes.Set{ @@ -316,8 +319,10 @@ func benchmarkSetTypeValidate(b *testing.B, elementCount int) { set := SetType{} for n := 0; n < b.N; n++ { - set.Validate(ctx, in, path) + diags = set.Validate(ctx, in, path) } + + benchDiags = diags } func BenchmarkSetTypeValidate10(b *testing.B) { From a82241b4cca348d61df39cfdce49fe38f2d090ed Mon Sep 17 00:00:00 2001 From: Brian Flad Date: Tue, 14 Sep 2021 08:58:48 -0400 Subject: [PATCH 8/8] types: Implement (SetType).String() to satisfy updated attr.Type interface --- types/set.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/types/set.go b/types/set.go index aea474312..2ad60fb68 100644 --- a/types/set.go +++ b/types/set.go @@ -101,6 +101,11 @@ func (t SetType) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep return t.ElemType, nil } +// String returns a human-friendly description of the SetType. +func (t SetType) String() string { + return "types.SetType[" + t.ElemType.String() + "]" +} + // Validate implements type validation. This type requires all elements to be // unique. func (s SetType) Validate(ctx context.Context, in tftypes.Value, path *tftypes.AttributePath) diag.Diagnostics {