diff --git a/.changelog/126.txt b/.changelog/126.txt new file mode 100644 index 000000000..4baaadae7 --- /dev/null +++ b/.changelog/126.txt @@ -0,0 +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 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..2ad60fb68 --- /dev/null +++ b/types/set.go @@ -0,0 +1,279 @@ +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.TypeWithValidate = 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 +} + +// 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 { + 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() || in.IsNull() { + 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 := indexOuter + 1; indexInner < len(elems); indexInner++ { + elemInner := elems[indexInner] + + 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 { + // 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..a24ae8f3b --- /dev/null +++ b/types/set_test.go @@ -0,0 +1,902 @@ +package types + +import ( + "context" + "strconv" + "testing" + + "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" +) + +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"}, + }, + }, + }, + "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, + }, + 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) + } +} + +var benchDiags diag.Diagnostics // Prevent compiler optimization + +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)) + } + + var diags diag.Diagnostics // Prevent compiler optimization + 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++ { + diags = set.Validate(ctx, in, path) + } + + benchDiags = diags +} + +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() + + 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, nil), + }, + ), + }, + "null-elements": { + in: tftypes.NewValue( + tftypes.Set{ + ElementType: tftypes.String, + }, + []tftypes.Value{ + 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( + 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() + + 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"), + }, + }, + "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, + }, + "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) + } + }) + } +}