diff --git a/helper/schema/resource_data.go b/helper/schema/resource_data.go index 096f449d1f..7569b00293 100644 --- a/helper/schema/resource_data.go +++ b/helper/schema/resource_data.go @@ -15,7 +15,11 @@ import ( "github.com/hashicorp/go-cty/cty" "github.com/hashicorp/go-cty/cty/gocty" + "github.com/hashicorp/terraform-plugin-go/tftypes" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/internal/configs/configschema" + "github.com/hashicorp/terraform-plugin-sdk/v2/internal/configs/hcl2shim" + "github.com/hashicorp/terraform-plugin-sdk/v2/internal/plugin/convert" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" ) @@ -59,6 +63,105 @@ type getResult struct { Schema *Schema } +// TfTypeIdentityState returns the identity data as a tftypes.Value. +func (d *ResourceData) TfTypeIdentityState() (*tftypes.Value, error) { + s := schemaMap(d.identitySchema).CoreConfigSchema() + + state := d.State() + + if state == nil { + return nil, fmt.Errorf("state is nil, call SetId() on ResourceData first") + } + + stateVal, err := hcl2shim.HCL2ValueFromFlatmap(state.Identity, s.ImpliedType()) + if err != nil { + return nil, fmt.Errorf("converting identity flatmap to cty value: %+v", err) + } + + return convert.ToTfValue(stateVal) +} + +// TfTypeResourceState returns the resource data as a tftypes.Value. +func (d *ResourceData) TfTypeResourceState() (*tftypes.Value, error) { + s := schemaMap(d.schema).CoreConfigSchema() + + // The CoreConfigSchema method on schemaMaps doesn't automatically handle adding the id + // attribute or timeouts like the method on Resource does + if _, ok := s.Attributes["id"]; !ok { + s.Attributes["id"] = &configschema.Attribute{ + Type: cty.String, + Optional: true, + Computed: true, + } + } + + _, timeoutsAttr := s.Attributes[TimeoutsConfigKey] + _, timeoutsBlock := s.BlockTypes[TimeoutsConfigKey] + + if d.timeouts != nil && !timeoutsAttr && !timeoutsBlock { + timeouts := configschema.Block{ + Attributes: map[string]*configschema.Attribute{}, + } + + if d.timeouts.Create != nil { + timeouts.Attributes[TimeoutCreate] = &configschema.Attribute{ + Type: cty.String, + Optional: true, + } + } + + if d.timeouts.Read != nil { + timeouts.Attributes[TimeoutRead] = &configschema.Attribute{ + Type: cty.String, + Optional: true, + } + } + + if d.timeouts.Update != nil { + timeouts.Attributes[TimeoutUpdate] = &configschema.Attribute{ + Type: cty.String, + Optional: true, + } + } + + if d.timeouts.Delete != nil { + timeouts.Attributes[TimeoutDelete] = &configschema.Attribute{ + Type: cty.String, + Optional: true, + } + } + + if d.timeouts.Default != nil { + timeouts.Attributes[TimeoutDefault] = &configschema.Attribute{ + Type: cty.String, + Optional: true, + } + } + + if len(timeouts.Attributes) != 0 { + s.BlockTypes[TimeoutsConfigKey] = &configschema.NestedBlock{ + Nesting: configschema.NestingSingle, + Block: timeouts, + } + } + } + + state := d.State() + if state == nil { + return nil, fmt.Errorf("state is nil, call SetId() on ResourceData first") + } + + // Although we handle adding/omitting timeouts to the schema depending on how it's been defined on the resource + // we don't process or convert the timeout values since they reside in Meta and aren't needed for the purposes + // of this function and in the context of a List. + stateVal, err := hcl2shim.HCL2ValueFromFlatmap(state.Attributes, s.ImpliedType()) + if err != nil { + return nil, fmt.Errorf("converting resource state flatmap to cty value: %+v", err) + } + + return convert.ToTfValue(stateVal) +} + // Get returns the data for the given key, or nil if the key doesn't exist // in the schema. // diff --git a/helper/schema/resource_data_test.go b/helper/schema/resource_data_test.go index e81ca198c8..b8b50a36fd 100644 --- a/helper/schema/resource_data_test.go +++ b/helper/schema/resource_data_test.go @@ -13,6 +13,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/hashicorp/go-cty/cty" + "github.com/hashicorp/terraform-plugin-go/tftypes" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" ) @@ -4314,6 +4315,183 @@ func TestResourceDataIdentity_no_schema(t *testing.T) { } } +func TestResourceData_TfTypeIdentityState(t *testing.T) { + d := &ResourceData{ + identitySchema: map[string]*Schema{ + "foo": { + Type: TypeString, + RequiredForImport: true, + }, + }, + } + + d.SetId("baz") // just required to be able to call .State() + + identity, err := d.Identity() + if err != nil { + t.Fatalf("err: %s", err) + } + + err = identity.Set("foo", "bar") + if err != nil { + t.Fatalf("err: %s", err) + } + + expectedIdentity := tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "foo": tftypes.String, + }}, map[string]tftypes.Value{ + "foo": tftypes.NewValue(tftypes.String, "bar"), + }) + + tfTypeIdentity, err := d.TfTypeIdentityState() + if err != nil { + t.Fatalf("err: %s", err) + } + + if !tfTypeIdentity.Equal(expectedIdentity) { + t.Fatalf("expected tftype value of identity to be %+v, got %+v", expectedIdentity, tfTypeIdentity) + } +} + +func TestResourceData_TfTypeResourceState(t *testing.T) { + cases := []struct { + d *ResourceData + expected tftypes.Value + }{ + { + d: &ResourceData{ + schema: map[string]*Schema{ + "location": { + Type: TypeString, + Optional: true, + }, + }, + timeouts: timeoutForValues(30, 5, 30, 5, 5), + }, + expected: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "location": tftypes.String, + "id": tftypes.String, + "timeouts": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "create": tftypes.String, + "delete": tftypes.String, + "read": tftypes.String, + "update": tftypes.String, + "default": tftypes.String, + }, + }, + }}, map[string]tftypes.Value{ + "location": tftypes.NewValue(tftypes.String, "westeurope"), + "id": tftypes.NewValue(tftypes.String, "baz"), + "timeouts": tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "create": tftypes.String, + "default": tftypes.String, + "delete": tftypes.String, + "read": tftypes.String, + "update": tftypes.String, + }, + }, map[string]tftypes.Value{ + "create": tftypes.NewValue(tftypes.String, nil), + "default": tftypes.NewValue(tftypes.String, nil), + "delete": tftypes.NewValue(tftypes.String, nil), + "read": tftypes.NewValue(tftypes.String, nil), + "update": tftypes.NewValue(tftypes.String, nil), + }), + }), + }, + { + d: &ResourceData{ + schema: map[string]*Schema{ + "location": { + Type: TypeString, + Optional: true, + }, + }, + }, + expected: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "location": tftypes.String, + "id": tftypes.String, + }}, map[string]tftypes.Value{ + "location": tftypes.NewValue(tftypes.String, "westeurope"), + "id": tftypes.NewValue(tftypes.String, "baz"), + }), + }, + { + d: &ResourceData{ + schema: map[string]*Schema{ + "location": { + Type: TypeString, + Optional: true, + }, + }, + timeouts: &ResourceTimeout{}, + }, + expected: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "location": tftypes.String, + "id": tftypes.String, + }}, map[string]tftypes.Value{ + "location": tftypes.NewValue(tftypes.String, "westeurope"), + "id": tftypes.NewValue(tftypes.String, "baz"), + }), + }, + { + d: &ResourceData{ + schema: map[string]*Schema{ + "location": { + Type: TypeString, + Optional: true, + }, + }, + timeouts: &ResourceTimeout{ + Create: DefaultTimeout(30 * time.Minute), + }, + }, + expected: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "location": tftypes.String, + "id": tftypes.String, + "timeouts": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "create": tftypes.String, + }, + }, + }}, map[string]tftypes.Value{ + "location": tftypes.NewValue(tftypes.String, "westeurope"), + "id": tftypes.NewValue(tftypes.String, "baz"), + "timeouts": tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "create": tftypes.String, + }, + }, map[string]tftypes.Value{ + "create": tftypes.NewValue(tftypes.String, nil), + }), + }), + }, + } + + for _, tc := range cases { + tc.d.SetId("baz") // just required to be able to call .State() + + if err := tc.d.Set("location", "westeurope"); err != nil { + t.Fatalf("err: %s", err) + } + + tfTypeIdentity, err := tc.d.TfTypeResourceState() + if err != nil { + t.Fatalf("err: %s", err) + } + + if !tfTypeIdentity.Equal(tc.expected) { + t.Fatalf("expected tftype value of identity to be %+v, got %+v", tc.expected, tfTypeIdentity) + } + } +} + func testPtrTo(raw interface{}) interface{} { return &raw } diff --git a/internal/plugin/convert/value.go b/internal/plugin/convert/value.go new file mode 100644 index 0000000000..84d710f960 --- /dev/null +++ b/internal/plugin/convert/value.go @@ -0,0 +1,213 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package convert + +import ( + "fmt" + + "github.com/hashicorp/go-cty/cty" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func primitiveTfValue(in cty.Value) (*tftypes.Value, error) { + primitiveType, err := tftypeFromCtyType(in.Type()) + if err != nil { + return nil, err + } + + if in.IsNull() { + return nullTfValue(primitiveType), nil + } + + if !in.IsKnown() { + return unknownTfValue(primitiveType), nil + } + + var val tftypes.Value + switch in.Type() { + case cty.String: + val = tftypes.NewValue(tftypes.String, in.AsString()) + case cty.Bool: + val = tftypes.NewValue(tftypes.Bool, in.True()) + case cty.Number: + val = tftypes.NewValue(tftypes.Number, in.AsBigFloat()) + } + + return &val, nil +} + +func listTfValue(in cty.Value) (*tftypes.Value, error) { + listType, err := tftypeFromCtyType(in.Type()) + if err != nil { + return nil, err + } + + if in.IsNull() { + return nullTfValue(listType), nil + } + + if !in.IsKnown() { + return unknownTfValue(listType), nil + } + + vals := make([]tftypes.Value, 0) + + for _, v := range in.AsValueSlice() { + tfVal, err := ToTfValue(v) + if err != nil { + return nil, err + } + vals = append(vals, *tfVal) + } + + out := tftypes.NewValue(listType, vals) + + return &out, nil +} + +func mapTfValue(in cty.Value) (*tftypes.Value, error) { + mapType, err := tftypeFromCtyType(in.Type()) + if err != nil { + return nil, err + } + + if in.IsNull() { + return nullTfValue(mapType), nil + } + + if !in.IsKnown() { + return unknownTfValue(mapType), nil + } + + vals := make(map[string]tftypes.Value) + + for k, v := range in.AsValueMap() { + tfVal, err := ToTfValue(v) + if err != nil { + return nil, err + } + vals[k] = *tfVal + } + + out := tftypes.NewValue(mapType, vals) + + return &out, nil +} + +func setTfValue(in cty.Value) (*tftypes.Value, error) { + setType, err := tftypeFromCtyType(in.Type()) + if err != nil { + return nil, err + } + + if in.IsNull() { + return nullTfValue(setType), nil + } + + if !in.IsKnown() { + return unknownTfValue(setType), nil + } + + vals := make([]tftypes.Value, 0) + + for _, v := range in.AsValueSlice() { + tfVal, err := ToTfValue(v) + if err != nil { + return nil, err + } + vals = append(vals, *tfVal) + } + + out := tftypes.NewValue(setType, vals) + + return &out, nil +} + +func objectTfValue(in cty.Value) (*tftypes.Value, error) { + objType, err := tftypeFromCtyType(in.Type()) + if err != nil { + return nil, err + } + + if in.IsNull() { + return nullTfValue(objType), nil + } + + if !in.IsKnown() { + return unknownTfValue(objType), nil + } + + vals := make(map[string]tftypes.Value) + + for k, v := range in.AsValueMap() { + tfVal, err := ToTfValue(v) + if err != nil { + return nil, err + } + vals[k] = *tfVal + } + + out := tftypes.NewValue(objType, vals) + + return &out, nil +} + +func tupleTfValue(in cty.Value) (*tftypes.Value, error) { + tupleType, err := tftypeFromCtyType(in.Type()) + if err != nil { + return nil, err + } + + if in.IsNull() { + return nullTfValue(tupleType), nil + } + + if !in.IsKnown() { + return unknownTfValue(tupleType), nil + } + + vals := make([]tftypes.Value, 0) + + for _, v := range in.AsValueSlice() { + tfVal, err := ToTfValue(v) + if err != nil { + return nil, err + } + vals = append(vals, *tfVal) + } + + out := tftypes.NewValue(tupleType, vals) + + return &out, nil +} + +func ToTfValue(in cty.Value) (*tftypes.Value, error) { + ty := in.Type() + switch { + case ty.IsPrimitiveType(): + return primitiveTfValue(in) + case ty.IsListType(): + return listTfValue(in) + case ty.IsObjectType(): + return objectTfValue(in) + case ty.IsMapType(): + return mapTfValue(in) + case ty.IsSetType(): + return setTfValue(in) + case ty.IsTupleType(): + return tupleTfValue(in) + default: + return nil, fmt.Errorf("unsupported type %s", ty) + } +} + +func nullTfValue(ty tftypes.Type) *tftypes.Value { + nullValue := tftypes.NewValue(ty, nil) + return &nullValue +} + +func unknownTfValue(ty tftypes.Type) *tftypes.Value { + unknownValue := tftypes.NewValue(ty, tftypes.UnknownValue) + return &unknownValue +} diff --git a/internal/plugin/convert/value_test.go b/internal/plugin/convert/value_test.go new file mode 100644 index 0000000000..fb69f78f2f --- /dev/null +++ b/internal/plugin/convert/value_test.go @@ -0,0 +1,613 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package convert + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/go-cty/cty" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestPrimitiveTfType(t *testing.T) { + t.Parallel() + + tests := []struct { + Value cty.Value + Want tftypes.Value + }{ + { + Value: cty.StringVal("test"), + Want: tftypes.NewValue(tftypes.String, "test"), + }, + { + Value: cty.BoolVal(true), + Want: tftypes.NewValue(tftypes.Bool, true), + }, + { + Value: cty.NumberIntVal(42), + Want: tftypes.NewValue(tftypes.Number, 42), + }, + { + Value: cty.NumberFloatVal(3.14), + Want: tftypes.NewValue(tftypes.Number, 3.14), + }, + { + Value: cty.NullVal(cty.String), + Want: tftypes.NewValue(tftypes.String, nil), + }, + { + Value: cty.UnknownVal(cty.String), + Want: tftypes.NewValue(tftypes.String, tftypes.UnknownValue), + }, + } + + for _, test := range tests { + t.Run(test.Value.GoString(), func(t *testing.T) { + t.Parallel() + + got, err := ToTfValue(test.Value) + if err != nil { + t.Error(err) + } + + if diff := cmp.Diff(test.Want, *got); diff != "" { + t.Errorf("unexpected differences: %s", diff) + } + }) + } +} + +func TestListTfType(t *testing.T) { + t.Parallel() + + tests := []struct { + Value cty.Value + Want tftypes.Value + }{ + { + Value: cty.ListVal([]cty.Value{ + cty.StringVal("apple"), + cty.StringVal("cherry"), + cty.StringVal("kangaroo"), + }), + Want: tftypes.NewValue(tftypes.List{ElementType: tftypes.String}, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "apple"), + tftypes.NewValue(tftypes.String, "cherry"), + tftypes.NewValue(tftypes.String, "kangaroo"), + }), + }, + { + Value: cty.ListVal([]cty.Value{ + cty.BoolVal(true), + cty.BoolVal(false), + }), + Want: tftypes.NewValue(tftypes.List{ElementType: tftypes.Bool}, []tftypes.Value{ + tftypes.NewValue(tftypes.Bool, true), + tftypes.NewValue(tftypes.Bool, false), + }), + }, + { + Value: cty.ListVal([]cty.Value{ + cty.NumberIntVal(100), + cty.NumberIntVal(200), + }), + Want: tftypes.NewValue(tftypes.List{ElementType: tftypes.Number}, []tftypes.Value{ + tftypes.NewValue(tftypes.Number, 100), + tftypes.NewValue(tftypes.Number, 200), + }), + }, + { + Value: cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("Alice"), + "breed": cty.StringVal("Beagle"), + "weight": cty.NumberIntVal(20), + "toys": cty.ListVal([]cty.Value{cty.StringVal("ball"), cty.StringVal("rope")}), + }), + cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("Bobby"), + "breed": cty.StringVal("Golden"), + "weight": cty.NumberIntVal(30), + "toys": cty.ListVal([]cty.Value{cty.StringVal("dummy"), cty.StringVal("frisbee")}), + }), + }), + Want: tftypes.NewValue(tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "name": tftypes.String, + "breed": tftypes.String, + "weight": tftypes.Number, + "toys": tftypes.List{ElementType: tftypes.String}, + }, + }, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "name": tftypes.String, + "breed": tftypes.String, + "weight": tftypes.Number, + "toys": tftypes.List{ElementType: tftypes.String}, + }, + }, map[string]tftypes.Value{ + "name": tftypes.NewValue(tftypes.String, "Alice"), + "breed": tftypes.NewValue(tftypes.String, "Beagle"), + "weight": tftypes.NewValue(tftypes.Number, 20), + "toys": tftypes.NewValue(tftypes.List{ElementType: tftypes.String}, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "ball"), + tftypes.NewValue(tftypes.String, "rope"), + }), + }), + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "name": tftypes.String, + "breed": tftypes.String, + "weight": tftypes.Number, + "toys": tftypes.List{ElementType: tftypes.String}, + }, + }, map[string]tftypes.Value{ + "name": tftypes.NewValue(tftypes.String, "Bobby"), + "breed": tftypes.NewValue(tftypes.String, "Golden"), + "weight": tftypes.NewValue(tftypes.Number, 30), + "toys": tftypes.NewValue(tftypes.List{ElementType: tftypes.String}, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "dummy"), + tftypes.NewValue(tftypes.String, "frisbee"), + }), + }), + }), + }, + { + Value: cty.NullVal(cty.List(cty.String)), + Want: tftypes.NewValue(tftypes.List{ElementType: tftypes.String}, nil), + }, + { + Value: cty.UnknownVal(cty.List(cty.String)), + Want: tftypes.NewValue(tftypes.List{ElementType: tftypes.String}, tftypes.UnknownValue), + }, + } + + for _, test := range tests { + t.Run(test.Value.GoString(), func(t *testing.T) { + t.Parallel() + + got, err := ToTfValue(test.Value) + if err != nil { + t.Error(err) + } + + if diff := cmp.Diff(test.Want, *got); diff != "" { + t.Errorf("unexpected differences: %s", diff) + } + }) + } +} + +func TestSetTfType(t *testing.T) { + t.Parallel() + + tests := []struct { + Value cty.Value + Want tftypes.Value + }{ + { + Value: cty.SetVal([]cty.Value{ + cty.StringVal("apple"), + cty.StringVal("cherry"), + cty.StringVal("kangaroo"), + }), + Want: tftypes.NewValue(tftypes.Set{ElementType: tftypes.String}, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "apple"), + tftypes.NewValue(tftypes.String, "cherry"), + tftypes.NewValue(tftypes.String, "kangaroo"), + }), + }, + { + Value: cty.SetVal([]cty.Value{ + cty.BoolVal(true), + cty.BoolVal(false), + }), + Want: tftypes.NewValue(tftypes.Set{ElementType: tftypes.Bool}, []tftypes.Value{ + tftypes.NewValue(tftypes.Bool, true), + tftypes.NewValue(tftypes.Bool, false), + }), + }, + { + Value: cty.SetVal([]cty.Value{ + cty.NumberIntVal(100), + cty.NumberIntVal(200), + }), + Want: tftypes.NewValue(tftypes.Set{ElementType: tftypes.Number}, []tftypes.Value{ + tftypes.NewValue(tftypes.Number, 100), + tftypes.NewValue(tftypes.Number, 200), + }), + }, + { + Value: cty.SetVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("Alice"), + "breed": cty.StringVal("Beagle"), + "weight": cty.NumberIntVal(20), + "toys": cty.SetVal([]cty.Value{cty.StringVal("ball"), cty.StringVal("rope")}), + }), + cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("Bobby"), + "breed": cty.StringVal("Golden"), + "weight": cty.NumberIntVal(30), + "toys": cty.SetVal([]cty.Value{cty.StringVal("dummy"), cty.StringVal("frisbee")}), + }), + }), + Want: tftypes.NewValue(tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "name": tftypes.String, + "breed": tftypes.String, + "weight": tftypes.Number, + "toys": tftypes.Set{ElementType: tftypes.String}, + }, + }, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "name": tftypes.String, + "breed": tftypes.String, + "weight": tftypes.Number, + "toys": tftypes.Set{ElementType: tftypes.String}, + }, + }, map[string]tftypes.Value{ + "name": tftypes.NewValue(tftypes.String, "Alice"), + "breed": tftypes.NewValue(tftypes.String, "Beagle"), + "weight": tftypes.NewValue(tftypes.Number, 20), + "toys": tftypes.NewValue(tftypes.Set{ElementType: tftypes.String}, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "ball"), + tftypes.NewValue(tftypes.String, "rope"), + }), + }), + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "name": tftypes.String, + "breed": tftypes.String, + "weight": tftypes.Number, + "toys": tftypes.Set{ElementType: tftypes.String}, + }, + }, map[string]tftypes.Value{ + "name": tftypes.NewValue(tftypes.String, "Bobby"), + "breed": tftypes.NewValue(tftypes.String, "Golden"), + "weight": tftypes.NewValue(tftypes.Number, 30), + "toys": tftypes.NewValue(tftypes.Set{ElementType: tftypes.String}, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "dummy"), + tftypes.NewValue(tftypes.String, "frisbee"), + }), + }), + }), + }, + { + Value: cty.NullVal(cty.Set(cty.String)), + Want: tftypes.NewValue(tftypes.Set{ElementType: tftypes.String}, nil), + }, + { + Value: cty.UnknownVal(cty.Set(cty.String)), + Want: tftypes.NewValue(tftypes.Set{ElementType: tftypes.String}, tftypes.UnknownValue), + }, + } + + for _, test := range tests { + t.Run(test.Value.GoString(), func(t *testing.T) { + t.Parallel() + + got, err := ToTfValue(test.Value) + if err != nil { + t.Error(err) + } + + if diff := cmp.Diff(test.Want, *got); diff != "" { + t.Errorf("unexpected differences: %s", diff) + } + }) + } +} + +func TestMapTfType(t *testing.T) { + t.Parallel() + + tests := []struct { + Value cty.Value + Want tftypes.Value + }{ + { + Value: cty.MapVal(map[string]cty.Value{ + "foo": cty.StringVal("bar"), + "baz": cty.StringVal("qux"), + }), + Want: tftypes.NewValue(tftypes.Map{ + ElementType: tftypes.String, + }, map[string]tftypes.Value{ + "foo": tftypes.NewValue(tftypes.String, "bar"), + "baz": tftypes.NewValue(tftypes.String, "qux"), + }), + }, + { + Value: cty.MapVal(map[string]cty.Value{ + "foo": cty.MapVal(map[string]cty.Value{ + "foo": cty.StringVal("bar"), + "baz": cty.StringVal("qux"), + }), + }), + Want: tftypes.NewValue(tftypes.Map{ + ElementType: tftypes.Map{ + ElementType: tftypes.String, + }}, map[string]tftypes.Value{ + "foo": tftypes.NewValue(tftypes.Map{ + ElementType: tftypes.String, + }, map[string]tftypes.Value{ + "foo": tftypes.NewValue(tftypes.String, "bar"), + "baz": tftypes.NewValue(tftypes.String, "qux"), + }), + }), + }, + { + Value: cty.MapVal(map[string]cty.Value{ + "foo": cty.ObjectVal(map[string]cty.Value{ + "fruits": cty.MapVal(map[string]cty.Value{ + "ananas": cty.StringVal("pineapple"), + "erdbeere": cty.StringVal("strawberry"), + }), + }), + }), + Want: tftypes.NewValue(tftypes.Map{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "fruits": tftypes.Map{ElementType: tftypes.String}, + }}}, map[string]tftypes.Value{ + "foo": tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "fruits": tftypes.Map{ElementType: tftypes.String}, + }}, map[string]tftypes.Value{ + "fruits": tftypes.NewValue(tftypes.Map{ElementType: tftypes.String}, map[string]tftypes.Value{ + "ananas": tftypes.NewValue(tftypes.String, "pineapple"), + "erdbeere": tftypes.NewValue(tftypes.String, "strawberry"), + }), + }), + }), + }, + { + Value: cty.NullVal(cty.Map(cty.String)), + Want: tftypes.NewValue(tftypes.Map{ElementType: tftypes.String}, nil), + }, + { + Value: cty.UnknownVal(cty.Map(cty.String)), + Want: tftypes.NewValue(tftypes.Map{ElementType: tftypes.String}, tftypes.UnknownValue), + }, + } + + for _, test := range tests { + t.Run(test.Value.GoString(), func(t *testing.T) { + t.Parallel() + + got, err := ToTfValue(test.Value) + if err != nil { + t.Error(err) + } + + if diff := cmp.Diff(test.Want, *got); diff != "" { + t.Errorf("unexpected differences: %s", diff) + } + }) + } +} + +func TestTupleTfType(t *testing.T) { + t.Parallel() + + tests := []struct { + Value cty.Value + Want tftypes.Value + }{ + { + Value: cty.TupleVal([]cty.Value{cty.StringVal("one")}), + Want: tftypes.NewValue(tftypes.Tuple{ElementTypes: []tftypes.Type{tftypes.String}}, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "one"), + }), + }, + { + Value: cty.TupleVal([]cty.Value{ + cty.StringVal("apple"), + cty.NumberIntVal(5), + cty.TupleVal([]cty.Value{cty.StringVal("banana"), cty.StringVal("pineapple")}), + }), + Want: tftypes.NewValue(tftypes.Tuple{ElementTypes: []tftypes.Type{tftypes.String, tftypes.Number, tftypes.Tuple{ElementTypes: []tftypes.Type{tftypes.String, tftypes.String}}}}, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "apple"), + tftypes.NewValue(tftypes.Number, 5), + tftypes.NewValue(tftypes.Tuple{ElementTypes: []tftypes.Type{tftypes.String, tftypes.String}}, []tftypes.Value{tftypes.NewValue(tftypes.String, "banana"), tftypes.NewValue(tftypes.String, "pineapple")}), + }), + }, + { + Value: cty.NullVal(cty.Tuple([]cty.Type{cty.String})), + Want: tftypes.NewValue(tftypes.Tuple{ElementTypes: []tftypes.Type{tftypes.String}}, nil), + }, + { + Value: cty.UnknownVal(cty.Tuple([]cty.Type{cty.String})), + Want: tftypes.NewValue(tftypes.Tuple{ElementTypes: []tftypes.Type{tftypes.String}}, tftypes.UnknownValue), + }, + } + + for _, test := range tests { + t.Run(test.Value.GoString(), func(t *testing.T) { + t.Parallel() + + got, err := ToTfValue(test.Value) + if err != nil { + t.Error(err) + } + + if diff := cmp.Diff(test.Want, *got); diff != "" { + t.Errorf("unexpected differences: %s", diff) + } + }) + } +} + +func TestObjectTfType(t *testing.T) { + t.Parallel() + + tests := []struct { + Value cty.Value + Want tftypes.Value + }{ + { + Value: cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("Alice"), + "breed": cty.StringVal("Beagle"), + "weight": cty.NumberIntVal(20), + }), + Want: tftypes.NewValue(tftypes.Object{AttributeTypes: map[string]tftypes.Type{ + "name": tftypes.String, + "breed": tftypes.String, + "weight": tftypes.Number, + }}, map[string]tftypes.Value{ + "name": tftypes.NewValue(tftypes.String, "Alice"), + "breed": tftypes.NewValue(tftypes.String, "Beagle"), + "weight": tftypes.NewValue(tftypes.Number, 20), + }), + }, + { + Value: cty.ObjectVal(map[string]cty.Value{ + "chonk": cty.ObjectVal(map[string]cty.Value{ + "size": cty.StringVal("large"), + "weight": cty.NumberIntVal(50), + }), + "blep": cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "color": cty.StringVal("brown"), + "pattern": cty.ObjectVal(map[string]cty.Value{ + "style": cty.ListVal([]cty.Value{cty.StringVal("striped"), cty.StringVal("spotted")}), + }), + }), + cty.ObjectVal(map[string]cty.Value{ + "color": cty.StringVal("black"), + "pattern": cty.ObjectVal(map[string]cty.Value{ + "style": cty.ListVal([]cty.Value{cty.StringVal("dotted"), cty.StringVal("plain")}), + }), + }), + }), + }), + Want: tftypes.NewValue(tftypes.Object{AttributeTypes: map[string]tftypes.Type{ + "chonk": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "size": tftypes.String, + "weight": tftypes.Number, + }, + }, + "blep": tftypes.List{ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "color": tftypes.String, + "pattern": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "style": tftypes.List{ElementType: tftypes.String}, + }, + }, + }, + }}, + }}, map[string]tftypes.Value{ + "chonk": tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "size": tftypes.String, + "weight": tftypes.Number, + }}, map[string]tftypes.Value{ + "size": tftypes.NewValue(tftypes.String, "large"), + "weight": tftypes.NewValue(tftypes.Number, 50), + }), + "blep": tftypes.NewValue(tftypes.List{ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "color": tftypes.String, + "pattern": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "style": tftypes.List{ElementType: tftypes.String}, + }, + }, + }}}, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "color": tftypes.String, + "pattern": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "style": tftypes.List{ElementType: tftypes.String}, + }, + }, + }, + }, map[string]tftypes.Value{ + "color": tftypes.NewValue(tftypes.String, "brown"), + "pattern": tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "style": tftypes.List{ElementType: tftypes.String}, + }, + }, map[string]tftypes.Value{ + "style": tftypes.NewValue(tftypes.List{ElementType: tftypes.String}, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "striped"), + tftypes.NewValue(tftypes.String, "spotted"), + }), + }), + }), + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "color": tftypes.String, + "pattern": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "style": tftypes.List{ElementType: tftypes.String}, + }, + }, + }, + }, map[string]tftypes.Value{ + "color": tftypes.NewValue(tftypes.String, "black"), + "pattern": tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "style": tftypes.List{ElementType: tftypes.String}, + }, + }, map[string]tftypes.Value{ + "style": tftypes.NewValue(tftypes.List{ElementType: tftypes.String}, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "dotted"), + tftypes.NewValue(tftypes.String, "plain"), + }), + }), + }), + }), + }), + }, + { + Value: cty.NullVal(cty.Object(map[string]cty.Type{ + "foo": cty.String, + "bar": cty.Number, + })), + Want: tftypes.NewValue(tftypes.Object{AttributeTypes: map[string]tftypes.Type{ + "foo": tftypes.String, + "bar": tftypes.Number, + }}, nil), + }, + { + Value: cty.UnknownVal(cty.Object(map[string]cty.Type{ + "foo": cty.String, + "bar": cty.Number, + })), + Want: tftypes.NewValue(tftypes.Object{AttributeTypes: map[string]tftypes.Type{ + "foo": tftypes.String, + "bar": tftypes.Number, + }}, tftypes.UnknownValue), + }, + } + + for _, test := range tests { + t.Run(test.Value.GoString(), func(t *testing.T) { + t.Parallel() + + got, err := ToTfValue(test.Value) + if err != nil { + t.Error(err) + } + + if diff := cmp.Diff(test.Want, *got); diff != "" { + t.Errorf("unexpected differences: %s", diff) + } + }) + } +}