diff --git a/.changelog/502.txt b/.changelog/502.txt new file mode 100644 index 000000000..9d41303dd --- /dev/null +++ b/.changelog/502.txt @@ -0,0 +1,95 @@ +```release-note:note +types: The `Bool` type `Null`, `Unknown`, and `Value` fields have been deprecated in preference of the `BoolNull()`, `BoolUnknown()`, and `BoolValue()` creation functions and `IsNull()`, `IsUnknown()`, and `ValueBool()` methods. The fields will be removed in a future release. +``` + +```release-note:note +types: The `Float64` type `Null`, `Unknown`, and `Value` fields have been deprecated in preference of the `Float64Null()`, `Float64Unknown()`, and `Float64Value()` creation functions and `IsNull()`, `IsUnknown()`, and `ValueFloat64()` methods. The fields will be removed in a future release. +``` + +```release-note:note +types: The `Int64` type `Null`, `Unknown`, and `Value` fields have been deprecated in preference of the `Int64Null()`, `Int64Unknown()`, and `Int64Value()` creation functions and `IsNull()`, `IsUnknown()`, and `ValueInt64()` methods. The fields will be removed in a future release. +``` + +```release-note:note +types: The `List` type `Null`, `Unknown`, and `Value` fields have been deprecated in preference of the `ListNull()`, `ListUnknown()`, and `ListValue()` creation functions and `Elements()`, `ElementsAs()`, `ElementType()`, `IsNull()`, and `IsUnknown()` methods. The fields will be removed in a future release. +``` + +```release-note:note +types: The `Map` type `Null`, `Unknown`, and `Value` fields have been deprecated in preference of the `MapNull()`, `MapUnknown()`, and `MapValue()` creation functions and `Elements()`, `ElementsAs()`, `ElementType()`, `IsNull()`, and `IsUnknown()` methods. The fields will be removed in a future release. +``` + +```release-note:note +types: The `Number` type `Null`, `Unknown`, and `Value` fields have been deprecated in preference of the `NumberNull()`, `NumberUnknown()`, and `NumberValue()` creation functions and `IsNull()`, `IsUnknown()`, and `ValueBigFloat()` methods. The fields will be removed in a future release. +``` + +```release-note:note +types: The `Set` type `Null`, `Unknown`, and `Value` fields have been deprecated in preference of the `SetNull()`, `SetUnknown()`, and `SetValue()` creation functions and `Elements()`, `ElementsAs()`, `ElementType()`, `IsNull()`, and `IsUnknown()` methods. The fields will be removed in a future release. +``` + +```release-note:note +types: The `String` type `Null`, `Unknown`, and `Value` fields have been deprecated in preference of the `StringNull()`, `StringUnknown()`, and `StringValue()` creation functions and `IsNull()`, `IsUnknown()`, and `ValueString()` methods. The fields will be removed in a future release. +``` + +```release-note:enhancement +types: Added `BoolNull()`, `BoolUnknown()`, `BoolValue()` functions, which create immutable `Bool` values +``` + +```release-note:enhancement +types: Added `Float64Null()`, `Float64Unknown()`, `Float64Value()` functions, which create immutable `Float64` values +``` + +```release-note:enhancement +types: Added `Int64Null()`, `Int64Unknown()`, `Int64Value()` functions, which create immutable `Int64` values +``` + +```release-note:enhancement +types: Added `ListNull()`, `ListUnknown()`, `ListValue()` functions, which create immutable `List` values +``` + +```release-note:enhancement +types: Added `MapNull()`, `MapUnknown()`, `MapValue()` functions, which create immutable `Map` values +``` + +```release-note:enhancement +types: Added `NumberNull()`, `NumberUnknown()`, `NumberValue()` functions, which create immutable `Number` values +``` + +```release-note:enhancement +types: Added `SetNull()`, `SetUnknown()`, `SetValue()` functions, which create immutable `Set` values +``` + +```release-note:enhancement +types: Added `StringNull()`, `StringUnknown()`, `StringValue()` functions, which create immutable `String` values +``` + +```release-note:enhancement +types: Added `Bool` type `ValueBool()` method, which returns the `bool` of the known value or `false` if null or unknown +``` + +```release-note:enhancement +types: Added `Float64` type `ValueFloat64()` method, which returns the `float64` of the known value or `0.0` if null or unknown +``` + +```release-note:enhancement +types: Added `Int64` type `ValueInt64()` method, which returns the `int64` of the known value or `0` if null or unknown +``` + +```release-note:enhancement +types: Added `List` type `Elements()` method, which returns the `[]attr.Value` of the known values or `nil` if null or unknown +``` + +```release-note:enhancement +types: Added `Map` type `Elements()` method, which returns the `map[string]attr.Value` of the known values or `nil` if null or unknown +``` + +```release-note:enhancement +types: Added `Number` type `ValueBigFloat()` method, which returns the `*big.Float` of the known value or `nil` if null or unknown +``` + +```release-note:enhancement +types: Added `Set` type `Elements()` method, which returns the `[]attr.Value` of the known values or `nil` if null or unknown +``` + +```release-note:enhancement +types: Added `String` type `ValueString()` method, which returns the `string` of the known value or `""` if null or unknown +``` diff --git a/internal/fwserver/attr_value.go b/internal/fwserver/attr_value.go index 1165982b4..70731345d 100644 --- a/internal/fwserver/attr_value.go +++ b/internal/fwserver/attr_value.go @@ -68,15 +68,16 @@ func listElemObject(ctx context.Context, schemaPath path.Path, list types.List, return listElemObjectFromTerraformValue(ctx, schemaPath, list, description, tftypes.UnknownValue) } - if index >= len(list.Elems) { + if index >= len(list.Elements()) { return listElemObjectFromTerraformValue(ctx, schemaPath, list, description, nil) } - return coerceObjectValue(schemaPath, list.Elems[index]) + return coerceObjectValue(schemaPath, list.Elements()[index]) } func listElemObjectFromTerraformValue(ctx context.Context, schemaPath path.Path, list types.List, description fwschemadata.DataDescription, tfValue any) (types.Object, diag.Diagnostics) { - elemValue, err := list.ElemType.ValueFromTerraform(ctx, tftypes.NewValue(list.ElemType.TerraformType(ctx), tfValue)) + elemType := list.ElementType(ctx) + elemValue, err := elemType.ValueFromTerraform(ctx, tftypes.NewValue(elemType.TerraformType(ctx), tfValue)) if err != nil { return types.Object{Null: true}, diag.Diagnostics{ @@ -96,7 +97,7 @@ func mapElemObject(ctx context.Context, schemaPath path.Path, m types.Map, key s return mapElemObjectFromTerraformValue(ctx, schemaPath, m, description, tftypes.UnknownValue) } - elemValue, ok := m.Elems[key] + elemValue, ok := m.Elements()[key] if !ok { return mapElemObjectFromTerraformValue(ctx, schemaPath, m, description, nil) @@ -106,7 +107,8 @@ func mapElemObject(ctx context.Context, schemaPath path.Path, m types.Map, key s } func mapElemObjectFromTerraformValue(ctx context.Context, schemaPath path.Path, m types.Map, description fwschemadata.DataDescription, tfValue any) (types.Object, diag.Diagnostics) { - elemValue, err := m.ElemType.ValueFromTerraform(ctx, tftypes.NewValue(m.ElemType.TerraformType(ctx), tfValue)) + elemType := m.ElementType(ctx) + elemValue, err := elemType.ValueFromTerraform(ctx, tftypes.NewValue(elemType.TerraformType(ctx), tfValue)) if err != nil { return types.Object{Null: true}, diag.Diagnostics{ @@ -128,13 +130,13 @@ func objectAttributeValue(ctx context.Context, object types.Object, attributeNam // A panic here indicates a bug somewhere else in the framework or an // invalid test case. - return object.Attrs[attributeName], nil + return object.Attributes()[attributeName], nil } func objectAttributeValueFromTerraformValue(ctx context.Context, object types.Object, attributeName string, description fwschemadata.DataDescription, tfValue any) (attr.Value, diag.Diagnostics) { // A panic here indicates a bug somewhere else in the framework or an // invalid test case. - attrType := object.AttrTypes[attributeName] + attrType := object.AttributeTypes(ctx)[attributeName] elemValue, err := attrType.ValueFromTerraform(ctx, tftypes.NewValue(attrType.TerraformType(ctx), tfValue)) @@ -156,15 +158,16 @@ func setElemObject(ctx context.Context, schemaPath path.Path, set types.Set, ind return setElemObjectFromTerraformValue(ctx, schemaPath, set, description, tftypes.UnknownValue) } - if index >= len(set.Elems) { + if index >= len(set.Elements()) { return setElemObjectFromTerraformValue(ctx, schemaPath, set, description, nil) } - return coerceObjectValue(schemaPath, set.Elems[index]) + return coerceObjectValue(schemaPath, set.Elements()[index]) } func setElemObjectFromTerraformValue(ctx context.Context, schemaPath path.Path, set types.Set, description fwschemadata.DataDescription, tfValue any) (types.Object, diag.Diagnostics) { - elemValue, err := set.ElemType.ValueFromTerraform(ctx, tftypes.NewValue(set.ElemType.TerraformType(ctx), tfValue)) + elemType := set.ElementType(ctx) + elemValue, err := elemType.ValueFromTerraform(ctx, tftypes.NewValue(elemType.TerraformType(ctx), tfValue)) if err != nil { return types.Object{Null: true}, diag.Diagnostics{ diff --git a/internal/fwserver/attribute_plan_modification.go b/internal/fwserver/attribute_plan_modification.go index 32281b42c..1f2369de2 100644 --- a/internal/fwserver/attribute_plan_modification.go +++ b/internal/fwserver/attribute_plan_modification.go @@ -13,6 +13,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" ) type ModifyAttributePlanResponse struct { @@ -85,6 +86,11 @@ func AttributeModifyPlan(ctx context.Context, a fwschema.Attribute, req tfsdk.Mo return } + // Null and unknown values should not have nested schema to modify. + if req.AttributePlan.IsNull() || req.AttributePlan.IsUnknown() { + return + } + if a.GetAttributes() == nil || len(a.GetAttributes().GetAttributes()) == 0 { return } @@ -116,7 +122,9 @@ func AttributeModifyPlan(ctx context.Context, a fwschema.Attribute, req tfsdk.Mo return } - for idx, planElem := range planList.Elems { + planElements := planList.Elements() + + for idx, planElem := range planElements { attrPath := req.AttributePath.AtListIndex(idx) configObject, diags := listElemObject(ctx, attrPath, configList, idx, fwschemadata.DataDescriptionConfiguration) @@ -143,6 +151,8 @@ func AttributeModifyPlan(ctx context.Context, a fwschema.Attribute, req tfsdk.Mo return } + planAttributes := planObject.Attributes() + for name, attr := range a.GetAttributes().GetAttributes() { attrConfig, diags := objectAttributeValue(ctx, configObject, name, fwschemadata.DataDescriptionConfiguration) @@ -187,16 +197,16 @@ func AttributeModifyPlan(ctx context.Context, a fwschema.Attribute, req tfsdk.Mo AttributeModifyPlan(ctx, attr, attrReq, &attrResp) - planObject.Attrs[name] = attrResp.AttributePlan + planAttributes[name] = attrResp.AttributePlan resp.Diagnostics.Append(attrResp.Diagnostics...) resp.RequiresReplace = attrResp.RequiresReplace resp.Private = attrResp.Private } - planList.Elems[idx] = planObject + planElements[idx] = types.ObjectValue(planObject.AttributeTypes(ctx), planAttributes) } - resp.AttributePlan = planList + resp.AttributePlan = types.ListValue(planList.ElementType(ctx), planElements) case fwschema.NestingModeSet: configSet, diags := coerceSetValue(req.AttributePath, req.AttributeConfig) @@ -222,7 +232,9 @@ func AttributeModifyPlan(ctx context.Context, a fwschema.Attribute, req tfsdk.Mo return } - for idx, planElem := range planSet.Elems { + planElements := planSet.Elements() + + for idx, planElem := range planElements { attrPath := req.AttributePath.AtSetValue(planElem) configObject, diags := setElemObject(ctx, attrPath, configSet, idx, fwschemadata.DataDescriptionConfiguration) @@ -249,6 +261,8 @@ func AttributeModifyPlan(ctx context.Context, a fwschema.Attribute, req tfsdk.Mo return } + planAttributes := planObject.Attributes() + for name, attr := range a.GetAttributes().GetAttributes() { attrConfig, diags := objectAttributeValue(ctx, configObject, name, fwschemadata.DataDescriptionConfiguration) @@ -293,16 +307,16 @@ func AttributeModifyPlan(ctx context.Context, a fwschema.Attribute, req tfsdk.Mo AttributeModifyPlan(ctx, attr, attrReq, &attrResp) - planObject.Attrs[name] = attrResp.AttributePlan + planAttributes[name] = attrResp.AttributePlan resp.Diagnostics.Append(attrResp.Diagnostics...) resp.RequiresReplace = attrResp.RequiresReplace resp.Private = attrResp.Private } - planSet.Elems[idx] = planObject + planElements[idx] = types.ObjectValue(planObject.AttributeTypes(ctx), planAttributes) } - resp.AttributePlan = planSet + resp.AttributePlan = types.SetValue(planSet.ElementType(ctx), planElements) case fwschema.NestingModeMap: configMap, diags := coerceMapValue(req.AttributePath, req.AttributeConfig) @@ -328,7 +342,9 @@ func AttributeModifyPlan(ctx context.Context, a fwschema.Attribute, req tfsdk.Mo return } - for key, planElem := range planMap.Elems { + planElements := planMap.Elements() + + for key, planElem := range planElements { attrPath := req.AttributePath.AtMapKey(key) configObject, diags := mapElemObject(ctx, attrPath, configMap, key, fwschemadata.DataDescriptionConfiguration) @@ -355,6 +371,8 @@ func AttributeModifyPlan(ctx context.Context, a fwschema.Attribute, req tfsdk.Mo return } + planAttributes := planObject.Attributes() + for name, attr := range a.GetAttributes().GetAttributes() { attrConfig, diags := objectAttributeValue(ctx, configObject, name, fwschemadata.DataDescriptionConfiguration) @@ -399,16 +417,16 @@ func AttributeModifyPlan(ctx context.Context, a fwschema.Attribute, req tfsdk.Mo AttributeModifyPlan(ctx, attr, attrReq, &attrResp) - planObject.Attrs[name] = attrResp.AttributePlan + planAttributes[name] = attrResp.AttributePlan resp.Diagnostics.Append(attrResp.Diagnostics...) resp.RequiresReplace = attrResp.RequiresReplace resp.Private = attrResp.Private } - planMap.Elems[key] = planObject + planElements[key] = types.ObjectValue(planObject.AttributeTypes(ctx), planAttributes) } - resp.AttributePlan = planMap + resp.AttributePlan = types.MapValue(planMap.ElementType(ctx), planElements) case fwschema.NestingModeSingle: configObject, diags := coerceObjectValue(req.AttributePath, req.AttributeConfig) @@ -434,10 +452,12 @@ func AttributeModifyPlan(ctx context.Context, a fwschema.Attribute, req tfsdk.Mo return } - if len(planObject.Attrs) == 0 { + if len(planObject.Attributes()) == 0 { return } + planAttributes := planObject.Attributes() + for name, attr := range a.GetAttributes().GetAttributes() { attrConfig, diags := objectAttributeValue(ctx, configObject, name, fwschemadata.DataDescriptionConfiguration) @@ -482,13 +502,13 @@ func AttributeModifyPlan(ctx context.Context, a fwschema.Attribute, req tfsdk.Mo AttributeModifyPlan(ctx, attr, attrReq, &attrResp) - planObject.Attrs[name] = attrResp.AttributePlan + planAttributes[name] = attrResp.AttributePlan resp.Diagnostics.Append(attrResp.Diagnostics...) resp.RequiresReplace = attrResp.RequiresReplace resp.Private = attrResp.Private } - resp.AttributePlan = planObject + resp.AttributePlan = types.ObjectValue(planObject.AttributeTypes(ctx), planAttributes) default: err := fmt.Errorf("unknown attribute nesting mode (%T: %v) at path: %s", nm, nm, req.AttributePath) resp.Diagnostics.AddAttributeError( diff --git a/internal/fwserver/attribute_plan_modification_test.go b/internal/fwserver/attribute_plan_modification_test.go index 12f0082f4..fce97bafc 100644 --- a/internal/fwserver/attribute_plan_modification_test.go +++ b/internal/fwserver/attribute_plan_modification_test.go @@ -65,7 +65,7 @@ func TestAttributeModifyPlan(t *testing.T) { AttributeState: types.String{Value: "TESTATTRONE"}, }, expectedResp: ModifyAttributePlanResponse{ - AttributePlan: types.String{Value: "MODIFIED_TWO"}, + AttributePlan: types.StringValue("MODIFIED_TWO"), Private: testEmptyProviderData, }, }, @@ -179,23 +179,23 @@ func TestAttributeModifyPlan(t *testing.T) { }, }, expectedResp: ModifyAttributePlanResponse{ - AttributePlan: types.List{ - ElemType: types.ObjectType{ + AttributePlan: types.ListValue( + types.ObjectType{ AttrTypes: map[string]attr.Type{ "nested_attr": types.StringType, }, }, - Elems: []attr.Value{ - types.Object{ - AttrTypes: map[string]attr.Type{ + []attr.Value{ + types.ObjectValue( + map[string]attr.Type{ "nested_attr": types.StringType, }, - Attrs: map[string]attr.Value{ + map[string]attr.Value{ "nested_attr": types.String{Value: "testvalue"}, }, - }, + ), }, - }, + ), Private: testProviderData, }, }, @@ -270,23 +270,23 @@ func TestAttributeModifyPlan(t *testing.T) { }, }, expectedResp: ModifyAttributePlanResponse{ - AttributePlan: types.Set{ - ElemType: types.ObjectType{ + AttributePlan: types.SetValue( + types.ObjectType{ AttrTypes: map[string]attr.Type{ "nested_attr": types.StringType, }, }, - Elems: []attr.Value{ - types.Object{ - AttrTypes: map[string]attr.Type{ + []attr.Value{ + types.ObjectValue( + map[string]attr.Type{ "nested_attr": types.StringType, }, - Attrs: map[string]attr.Value{ + map[string]attr.Value{ "nested_attr": types.String{Value: "testvalue"}, }, - }, + ), }, - }, + ), Private: testProviderData, }, }, @@ -401,36 +401,36 @@ func TestAttributeModifyPlan(t *testing.T) { }, }, expectedResp: ModifyAttributePlanResponse{ - AttributePlan: types.Set{ - ElemType: types.ObjectType{ + AttributePlan: types.SetValue( + types.ObjectType{ AttrTypes: map[string]attr.Type{ "nested_computed": types.StringType, "nested_required": types.StringType, }, }, - Elems: []attr.Value{ - types.Object{ - AttrTypes: map[string]attr.Type{ + []attr.Value{ + types.ObjectValue( + map[string]attr.Type{ "nested_computed": types.StringType, "nested_required": types.StringType, }, - Attrs: map[string]attr.Value{ + map[string]attr.Value{ "nested_computed": types.String{Value: "statevalue1"}, "nested_required": types.String{Value: "testvalue1"}, }, - }, - types.Object{ - AttrTypes: map[string]attr.Type{ + ), + types.ObjectValue( + map[string]attr.Type{ "nested_computed": types.StringType, "nested_required": types.StringType, }, - Attrs: map[string]attr.Value{ + map[string]attr.Value{ "nested_computed": types.String{Value: "statevalue2"}, "nested_required": types.String{Value: "testvalue2"}, }, - }, + ), }, - }, + ), Private: testEmptyProviderData, }, }, @@ -505,23 +505,23 @@ func TestAttributeModifyPlan(t *testing.T) { }, }, expectedResp: ModifyAttributePlanResponse{ - AttributePlan: types.Map{ - ElemType: types.ObjectType{ + AttributePlan: types.MapValue( + types.ObjectType{ AttrTypes: map[string]attr.Type{ "nested_attr": types.StringType, }, }, - Elems: map[string]attr.Value{ - "testkey": types.Object{ - AttrTypes: map[string]attr.Type{ + map[string]attr.Value{ + "testkey": types.ObjectValue( + map[string]attr.Type{ "nested_attr": types.StringType, }, - Attrs: map[string]attr.Value{ + map[string]attr.Value{ "nested_attr": types.String{Value: "testvalue"}, }, - }, + ), }, - }, + ), Private: testProviderData, }, }, @@ -569,14 +569,14 @@ func TestAttributeModifyPlan(t *testing.T) { }, }, expectedResp: ModifyAttributePlanResponse{ - AttributePlan: types.Object{ - AttrTypes: map[string]attr.Type{ + AttributePlan: types.ObjectValue( + map[string]attr.Type{ "testing": types.StringType, }, - Attrs: map[string]attr.Value{ + map[string]attr.Value{ "testing": types.String{Value: "testvalue"}, }, - }, + ), Private: testProviderData, }, }, @@ -704,7 +704,7 @@ func TestAttributeModifyPlan(t *testing.T) { }, }, expectedResp: ModifyAttributePlanResponse{ - AttributePlan: types.String{Value: "TESTATTRTWO"}, + AttributePlan: types.StringValue("TESTATTRTWO"), RequiresReplace: path.Paths{ path.Root("test"), }, diff --git a/internal/fwserver/attribute_validation.go b/internal/fwserver/attribute_validation.go index 55808d412..b781053b4 100644 --- a/internal/fwserver/attribute_validation.go +++ b/internal/fwserver/attribute_validation.go @@ -153,7 +153,7 @@ func AttributeValidateNestedAttributes(ctx context.Context, a fwschema.Attribute return } - for idx := range l.Elems { + for idx := range l.Elements() { for nestedName, nestedAttr := range a.GetAttributes().GetAttributes() { nestedAttrReq := tfsdk.ValidateAttributeRequest{ AttributePath: req.AttributePath.AtListIndex(idx).AtName(nestedName), @@ -183,7 +183,7 @@ func AttributeValidateNestedAttributes(ctx context.Context, a fwschema.Attribute return } - for _, value := range s.Elems { + for _, value := range s.Elements() { for nestedName, nestedAttr := range a.GetAttributes().GetAttributes() { nestedAttrReq := tfsdk.ValidateAttributeRequest{ AttributePath: req.AttributePath.AtSetValue(value).AtName(nestedName), @@ -213,7 +213,7 @@ func AttributeValidateNestedAttributes(ctx context.Context, a fwschema.Attribute return } - for key := range m.Elems { + for key := range m.Elements() { for nestedName, nestedAttr := range a.GetAttributes().GetAttributes() { nestedAttrReq := tfsdk.ValidateAttributeRequest{ AttributePath: req.AttributePath.AtMapKey(key).AtName(nestedName), @@ -243,21 +243,23 @@ func AttributeValidateNestedAttributes(ctx context.Context, a fwschema.Attribute return } - if !o.Null && !o.Unknown { - for nestedName, nestedAttr := range a.GetAttributes().GetAttributes() { - nestedAttrReq := tfsdk.ValidateAttributeRequest{ - AttributePath: req.AttributePath.AtName(nestedName), - AttributePathExpression: req.AttributePathExpression.AtName(nestedName), - Config: req.Config, - } - nestedAttrResp := &tfsdk.ValidateAttributeResponse{ - Diagnostics: resp.Diagnostics, - } - - AttributeValidate(ctx, nestedAttr, nestedAttrReq, nestedAttrResp) + if o.IsNull() || o.IsUnknown() { + return + } - resp.Diagnostics = nestedAttrResp.Diagnostics + for nestedName, nestedAttr := range a.GetAttributes().GetAttributes() { + nestedAttrReq := tfsdk.ValidateAttributeRequest{ + AttributePath: req.AttributePath.AtName(nestedName), + AttributePathExpression: req.AttributePathExpression.AtName(nestedName), + Config: req.Config, } + nestedAttrResp := &tfsdk.ValidateAttributeResponse{ + Diagnostics: resp.Diagnostics, + } + + AttributeValidate(ctx, nestedAttr, nestedAttrReq, nestedAttrResp) + + resp.Diagnostics = nestedAttrResp.Diagnostics } default: err := fmt.Errorf("unknown attribute validation nesting mode (%T: %v) at path: %s", nm, nm, req.AttributePath) diff --git a/internal/fwserver/block_plan_modification.go b/internal/fwserver/block_plan_modification.go index 747f8f39b..b6a383465 100644 --- a/internal/fwserver/block_plan_modification.go +++ b/internal/fwserver/block_plan_modification.go @@ -10,6 +10,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/fwschemadata" "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" ) // BlockModifyPlan performs all Block plan modification. @@ -55,6 +56,11 @@ func BlockModifyPlan(ctx context.Context, b fwschema.Block, req tfsdk.ModifyAttr resp.RequiresReplace = append(resp.RequiresReplace, req.AttributePath) } + // Null and unknown values should not have nested schema to modify. + if req.AttributePlan.IsNull() || req.AttributePlan.IsUnknown() { + return + } + nm := b.GetNestingMode() switch nm { case fwschema.BlockNestingModeList: @@ -82,7 +88,9 @@ func BlockModifyPlan(ctx context.Context, b fwschema.Block, req tfsdk.ModifyAttr return } - for idx, planElem := range planList.Elems { + planElements := planList.Elements() + + for idx, planElem := range planElements { attrPath := req.AttributePath.AtListIndex(idx) configObject, diags := listElemObject(ctx, attrPath, configList, idx, fwschemadata.DataDescriptionConfiguration) @@ -109,6 +117,8 @@ func BlockModifyPlan(ctx context.Context, b fwschema.Block, req tfsdk.ModifyAttr return } + planAttributes := planObject.Attributes() + for name, attr := range b.GetAttributes() { attrConfig, diags := objectAttributeValue(ctx, configObject, name, fwschemadata.DataDescriptionConfiguration) @@ -153,7 +163,7 @@ func BlockModifyPlan(ctx context.Context, b fwschema.Block, req tfsdk.ModifyAttr AttributeModifyPlan(ctx, attr, attrReq, &attrResp) - planObject.Attrs[name] = attrResp.AttributePlan + planAttributes[name] = attrResp.AttributePlan resp.Diagnostics.Append(attrResp.Diagnostics...) resp.RequiresReplace = attrResp.RequiresReplace resp.Private = attrResp.Private @@ -203,16 +213,16 @@ func BlockModifyPlan(ctx context.Context, b fwschema.Block, req tfsdk.ModifyAttr BlockModifyPlan(ctx, block, blockReq, &blockResp) - planObject.Attrs[name] = blockResp.AttributePlan + planAttributes[name] = blockResp.AttributePlan resp.Diagnostics.Append(blockResp.Diagnostics...) resp.RequiresReplace = blockResp.RequiresReplace resp.Private = blockResp.Private } - planList.Elems[idx] = planObject + planElements[idx] = types.ObjectValue(planObject.AttributeTypes(ctx), planAttributes) } - resp.AttributePlan = planList + resp.AttributePlan = types.ListValue(planList.ElementType(ctx), planElements) case fwschema.BlockNestingModeSet: configSet, diags := coerceSetValue(req.AttributePath, req.AttributeConfig) @@ -238,7 +248,9 @@ func BlockModifyPlan(ctx context.Context, b fwschema.Block, req tfsdk.ModifyAttr return } - for idx, planElem := range planSet.Elems { + planElements := planSet.Elements() + + for idx, planElem := range planElements { attrPath := req.AttributePath.AtSetValue(planElem) configObject, diags := setElemObject(ctx, attrPath, configSet, idx, fwschemadata.DataDescriptionConfiguration) @@ -265,6 +277,8 @@ func BlockModifyPlan(ctx context.Context, b fwschema.Block, req tfsdk.ModifyAttr return } + planAttributes := planObject.Attributes() + for name, attr := range b.GetAttributes() { attrConfig, diags := objectAttributeValue(ctx, configObject, name, fwschemadata.DataDescriptionConfiguration) @@ -309,7 +323,7 @@ func BlockModifyPlan(ctx context.Context, b fwschema.Block, req tfsdk.ModifyAttr AttributeModifyPlan(ctx, attr, attrReq, &attrResp) - planObject.Attrs[name] = attrResp.AttributePlan + planAttributes[name] = attrResp.AttributePlan resp.Diagnostics.Append(attrResp.Diagnostics...) resp.RequiresReplace = attrResp.RequiresReplace resp.Private = attrResp.Private @@ -359,16 +373,16 @@ func BlockModifyPlan(ctx context.Context, b fwschema.Block, req tfsdk.ModifyAttr BlockModifyPlan(ctx, block, blockReq, &blockResp) - planObject.Attrs[name] = blockResp.AttributePlan + planAttributes[name] = blockResp.AttributePlan resp.Diagnostics.Append(blockResp.Diagnostics...) resp.RequiresReplace = blockResp.RequiresReplace resp.Private = blockResp.Private } - planSet.Elems[idx] = planObject + planElements[idx] = types.ObjectValue(planObject.AttributeTypes(ctx), planAttributes) } - resp.AttributePlan = planSet + resp.AttributePlan = types.SetValue(planSet.ElementType(ctx), planElements) case fwschema.BlockNestingModeSingle: configObject, diags := coerceObjectValue(req.AttributePath, req.AttributeConfig) @@ -394,8 +408,10 @@ func BlockModifyPlan(ctx context.Context, b fwschema.Block, req tfsdk.ModifyAttr return } - if planObject.Attrs == nil { - planObject.Attrs = make(map[string]attr.Value) + planAttributes := planObject.Attributes() + + if planAttributes == nil { + planAttributes = make(map[string]attr.Value) } for name, attr := range b.GetAttributes() { @@ -442,7 +458,7 @@ func BlockModifyPlan(ctx context.Context, b fwschema.Block, req tfsdk.ModifyAttr AttributeModifyPlan(ctx, attr, attrReq, &attrResp) - planObject.Attrs[name] = attrResp.AttributePlan + planAttributes[name] = attrResp.AttributePlan resp.Diagnostics.Append(attrResp.Diagnostics...) resp.RequiresReplace = attrResp.RequiresReplace resp.Private = attrResp.Private @@ -492,13 +508,13 @@ func BlockModifyPlan(ctx context.Context, b fwschema.Block, req tfsdk.ModifyAttr BlockModifyPlan(ctx, block, blockReq, &blockResp) - planObject.Attrs[name] = blockResp.AttributePlan + planAttributes[name] = blockResp.AttributePlan resp.Diagnostics.Append(blockResp.Diagnostics...) resp.RequiresReplace = blockResp.RequiresReplace resp.Private = blockResp.Private } - resp.AttributePlan = planObject + resp.AttributePlan = types.ObjectValue(planObject.AttributeTypes(ctx), planAttributes) default: err := fmt.Errorf("unknown block plan modification nesting mode (%T: %v) at path: %s", nm, nm, req.AttributePath) resp.Diagnostics.AddAttributeError( diff --git a/internal/fwserver/block_plan_modification_test.go b/internal/fwserver/block_plan_modification_test.go index 8bd32a22d..7e5a664f1 100644 --- a/internal/fwserver/block_plan_modification_test.go +++ b/internal/fwserver/block_plan_modification_test.go @@ -195,23 +195,23 @@ func TestBlockModifyPlan(t *testing.T) { }, ), expectedResp: ModifyAttributePlanResponse{ - AttributePlan: types.List{ - ElemType: types.ObjectType{ + AttributePlan: types.ListValue( + types.ObjectType{ AttrTypes: map[string]attr.Type{ "nested_attr": types.StringType, }, }, - Elems: []attr.Value{ - types.Object{ - AttrTypes: map[string]attr.Type{ + []attr.Value{ + types.ObjectValue( + map[string]attr.Type{ "nested_attr": types.StringType, }, - Attrs: map[string]attr.Value{ + map[string]attr.Value{ "nested_attr": types.String{Value: "testvalue"}, }, - }, + ), }, - }, + ), }, }, "block-modified": { @@ -278,23 +278,23 @@ func TestBlockModifyPlan(t *testing.T) { testProviderData, ), expectedResp: ModifyAttributePlanResponse{ - AttributePlan: types.List{ - ElemType: types.ObjectType{ + AttributePlan: types.ListValue( + types.ObjectType{ AttrTypes: map[string]attr.Type{ "nested_attr": types.StringType, }, }, - Elems: []attr.Value{ - types.Object{ - AttrTypes: map[string]attr.Type{ + []attr.Value{ + types.ObjectValue( + map[string]attr.Type{ "nested_attr": types.StringType, }, - Attrs: map[string]attr.Value{ + map[string]attr.Value{ "nested_attr": types.String{Value: "TESTATTRONE"}, }, - }, + ), }, - }, + ), Private: testProviderData, }, }, @@ -324,23 +324,23 @@ func TestBlockModifyPlan(t *testing.T) { }, ), expectedResp: ModifyAttributePlanResponse{ - AttributePlan: types.List{ - ElemType: types.ObjectType{ + AttributePlan: types.ListValue( + types.ObjectType{ AttrTypes: map[string]attr.Type{ "nested_attr": types.StringType, }, }, - Elems: []attr.Value{ - types.Object{ - AttrTypes: map[string]attr.Type{ + []attr.Value{ + types.ObjectValue( + map[string]attr.Type{ "nested_attr": types.StringType, }, - Attrs: map[string]attr.Value{ + map[string]attr.Value{ "nested_attr": types.String{Value: "TESTATTRONE"}, }, - }, + ), }, - }, + ), Private: testProviderData, }, }, @@ -479,23 +479,23 @@ func TestBlockModifyPlan(t *testing.T) { }, }, expectedResp: ModifyAttributePlanResponse{ - AttributePlan: types.List{ - ElemType: types.ObjectType{ + AttributePlan: types.ListValue( + types.ObjectType{ AttrTypes: map[string]attr.Type{ "nested_attr": types.StringType, }, }, - Elems: []attr.Value{ - types.Object{ - AttrTypes: map[string]attr.Type{ + []attr.Value{ + types.ObjectValue( + map[string]attr.Type{ "nested_attr": types.StringType, }, - Attrs: map[string]attr.Value{ + map[string]attr.Value{ "nested_attr": types.String{Value: "testvalue"}, }, - }, + ), }, - }, + ), Private: testProviderData, }, }, @@ -570,23 +570,23 @@ func TestBlockModifyPlan(t *testing.T) { }, }, expectedResp: ModifyAttributePlanResponse{ - AttributePlan: types.List{ - ElemType: types.ObjectType{ + AttributePlan: types.ListValue( + types.ObjectType{ AttrTypes: map[string]attr.Type{ "nested_attr": types.StringType, }, }, - Elems: []attr.Value{ - types.Object{ - AttrTypes: map[string]attr.Type{ + []attr.Value{ + types.ObjectValue( + map[string]attr.Type{ "nested_attr": types.StringType, }, - Attrs: map[string]attr.Value{ + map[string]attr.Value{ "nested_attr": types.String{Value: "testvalue"}, }, - }, + ), }, - }, + ), Private: testProviderData, }, }, @@ -725,23 +725,23 @@ func TestBlockModifyPlan(t *testing.T) { }, }, expectedResp: ModifyAttributePlanResponse{ - AttributePlan: types.Set{ - ElemType: types.ObjectType{ + AttributePlan: types.SetValue( + types.ObjectType{ AttrTypes: map[string]attr.Type{ "nested_attr": types.StringType, }, }, - Elems: []attr.Value{ - types.Object{ - AttrTypes: map[string]attr.Type{ + []attr.Value{ + types.ObjectValue( + map[string]attr.Type{ "nested_attr": types.StringType, }, - Attrs: map[string]attr.Value{ + map[string]attr.Value{ "nested_attr": types.String{Value: "testvalue"}, }, - }, + ), }, - }, + ), Private: testProviderData, }, }, @@ -816,23 +816,23 @@ func TestBlockModifyPlan(t *testing.T) { }, }, expectedResp: ModifyAttributePlanResponse{ - AttributePlan: types.Set{ - ElemType: types.ObjectType{ + AttributePlan: types.SetValue( + types.ObjectType{ AttrTypes: map[string]attr.Type{ "nested_attr": types.StringType, }, }, - Elems: []attr.Value{ - types.Object{ - AttrTypes: map[string]attr.Type{ + []attr.Value{ + types.ObjectValue( + map[string]attr.Type{ "nested_attr": types.StringType, }, - Attrs: map[string]attr.Value{ + map[string]attr.Value{ "nested_attr": types.String{Value: "testvalue"}, }, - }, + ), }, - }, + ), Private: testProviderData, }, }, @@ -1031,8 +1031,8 @@ func TestBlockModifyPlan(t *testing.T) { }, }, expectedResp: ModifyAttributePlanResponse{ - AttributePlan: types.List{ - ElemType: types.ObjectType{ + AttributePlan: types.ListValue( + types.ObjectType{ AttrTypes: map[string]attr.Type{ "id": types.StringType, "list": types.ListType{ @@ -1045,9 +1045,9 @@ func TestBlockModifyPlan(t *testing.T) { }, }, }, - Elems: []attr.Value{ - types.Object{ - AttrTypes: map[string]attr.Type{ + []attr.Value{ + types.ObjectValue( + map[string]attr.Type{ "id": types.StringType, "list": types.ListType{ ElemType: types.ObjectType{ @@ -1058,32 +1058,32 @@ func TestBlockModifyPlan(t *testing.T) { }, }, }, - Attrs: map[string]attr.Value{ + map[string]attr.Value{ "id": types.String{Value: "one"}, - "list": types.List{ - ElemType: types.ObjectType{ + "list": types.ListValue( + types.ObjectType{ AttrTypes: map[string]attr.Type{ "nested_computed": types.StringType, "nested_required": types.StringType, }, }, - Elems: []attr.Value{ - types.Object{ - AttrTypes: map[string]attr.Type{ + []attr.Value{ + types.ObjectValue( + map[string]attr.Type{ "nested_computed": types.StringType, "nested_required": types.StringType, }, - Attrs: map[string]attr.Value{ + map[string]attr.Value{ "nested_computed": types.String{Value: "statevalue"}, "nested_required": types.String{Value: "configvalue"}, }, - }, + ), }, - }, + ), }, - }, + ), }, - }, + ), Private: testEmptyProviderData, }, }, @@ -1198,36 +1198,36 @@ func TestBlockModifyPlan(t *testing.T) { }, }, expectedResp: ModifyAttributePlanResponse{ - AttributePlan: types.Set{ - ElemType: types.ObjectType{ + AttributePlan: types.SetValue( + types.ObjectType{ AttrTypes: map[string]attr.Type{ "nested_computed": types.StringType, "nested_required": types.StringType, }, }, - Elems: []attr.Value{ - types.Object{ - AttrTypes: map[string]attr.Type{ + []attr.Value{ + types.ObjectValue( + map[string]attr.Type{ "nested_computed": types.StringType, "nested_required": types.StringType, }, - Attrs: map[string]attr.Value{ + map[string]attr.Value{ "nested_computed": types.String{Value: "statevalue1"}, "nested_required": types.String{Value: "testvalue1"}, }, - }, - types.Object{ - AttrTypes: map[string]attr.Type{ + ), + types.ObjectValue( + map[string]attr.Type{ "nested_computed": types.StringType, "nested_required": types.StringType, }, - Attrs: map[string]attr.Value{ + map[string]attr.Value{ "nested_computed": types.String{Value: "statevalue2"}, "nested_required": types.String{Value: "testvalue2"}, }, - }, + ), }, - }, + ), Private: testEmptyProviderData, }, }, @@ -1277,9 +1277,6 @@ func TestBlockModifyPlan(t *testing.T) { AttrTypes: map[string]attr.Type{ "nested_attr": types.StringType, }, - Attrs: map[string]attr.Value{ - "nested_attr": types.String{Null: true}, - }, Null: true, }, Private: testProviderData, @@ -1327,14 +1324,14 @@ func TestBlockModifyPlan(t *testing.T) { }, }, expectedResp: ModifyAttributePlanResponse{ - AttributePlan: types.Object{ - AttrTypes: map[string]attr.Type{ + AttributePlan: types.ObjectValue( + map[string]attr.Type{ "nested_attr": types.StringType, }, - Attrs: map[string]attr.Value{ + map[string]attr.Value{ "nested_attr": types.String{Value: "testvalue"}, }, - }, + ), Private: testProviderData, }, }, @@ -1382,14 +1379,14 @@ func TestBlockModifyPlan(t *testing.T) { }, }, expectedResp: ModifyAttributePlanResponse{ - AttributePlan: types.Object{ - AttrTypes: map[string]attr.Type{ + AttributePlan: types.ObjectValue( + map[string]attr.Type{ "nested_attr": types.StringType, }, - Attrs: map[string]attr.Value{ + map[string]attr.Value{ "nested_attr": types.String{Value: "testvalue"}, }, - }, + ), Private: testProviderData, }, }, @@ -1444,16 +1441,16 @@ func TestBlockModifyPlan(t *testing.T) { }, }, expectedResp: ModifyAttributePlanResponse{ - AttributePlan: types.Object{ - AttrTypes: map[string]attr.Type{ + AttributePlan: types.ObjectValue( + map[string]attr.Type{ "nested_computed": types.StringType, "nested_required": types.StringType, }, - Attrs: map[string]attr.Value{ + map[string]attr.Value{ "nested_computed": types.String{Value: "statevalue"}, "nested_required": types.String{Value: "testvalue"}, }, - }, + ), Private: testEmptyProviderData, }, }, @@ -1482,23 +1479,23 @@ func TestBlockModifyPlan(t *testing.T) { }, ), expectedResp: ModifyAttributePlanResponse{ - AttributePlan: types.List{ - ElemType: types.ObjectType{ + AttributePlan: types.ListValue( + types.ObjectType{ AttrTypes: map[string]attr.Type{ "nested_attr": types.StringType, }, }, - Elems: []attr.Value{ - types.Object{ - AttrTypes: map[string]attr.Type{ + []attr.Value{ + types.ObjectValue( + map[string]attr.Type{ "nested_attr": types.StringType, }, - Attrs: map[string]attr.Value{ + map[string]attr.Value{ "nested_attr": types.String{Value: "newtestvalue"}, }, - }, + ), }, - }, + ), RequiresReplace: path.Paths{ path.Root("test"), }, @@ -1573,23 +1570,23 @@ func TestBlockModifyPlan(t *testing.T) { }, ), expectedResp: ModifyAttributePlanResponse{ - AttributePlan: types.List{ - ElemType: types.ObjectType{ + AttributePlan: types.ListValue( + types.ObjectType{ AttrTypes: map[string]attr.Type{ "nested_attr": types.StringType, }, }, - Elems: []attr.Value{ - types.Object{ - AttrTypes: map[string]attr.Type{ + []attr.Value{ + types.ObjectValue( + map[string]attr.Type{ "nested_attr": types.StringType, }, - Attrs: map[string]attr.Value{ + map[string]attr.Value{ "nested_attr": types.String{Value: "newtestvalue"}, }, - }, + ), }, - }, + ), Private: testEmptyProviderData, }, }, @@ -1629,23 +1626,23 @@ func TestBlockModifyPlan(t *testing.T) { "This is a warning", ), }, - AttributePlan: types.List{ - ElemType: types.ObjectType{ + AttributePlan: types.ListValue( + types.ObjectType{ AttrTypes: map[string]attr.Type{ "nested_attr": types.StringType, }, }, - Elems: []attr.Value{ - types.Object{ - AttrTypes: map[string]attr.Type{ + []attr.Value{ + types.ObjectValue( + map[string]attr.Type{ "nested_attr": types.StringType, }, - Attrs: map[string]attr.Value{ + map[string]attr.Value{ "nested_attr": types.String{Value: "TESTDIAG"}, }, - }, + ), }, - }, + ), Private: testEmptyProviderData, }, }, @@ -1729,23 +1726,23 @@ func TestBlockModifyPlan(t *testing.T) { }, ), expectedResp: ModifyAttributePlanResponse{ - AttributePlan: types.List{ - ElemType: types.ObjectType{ + AttributePlan: types.ListValue( + types.ObjectType{ AttrTypes: map[string]attr.Type{ "nested_attr": types.StringType, }, }, - Elems: []attr.Value{ - types.Object{ - AttrTypes: map[string]attr.Type{ + []attr.Value{ + types.ObjectValue( + map[string]attr.Type{ "nested_attr": types.StringType, }, - Attrs: map[string]attr.Value{ - "nested_attr": types.String{Value: "MODIFIED_TWO"}, + map[string]attr.Value{ + "nested_attr": types.StringValue("MODIFIED_TWO"), }, - }, + ), }, - }, + ), Private: testEmptyProviderData, }, }, @@ -1774,23 +1771,23 @@ func TestBlockModifyPlan(t *testing.T) { }, ), expectedResp: ModifyAttributePlanResponse{ - AttributePlan: types.List{ - ElemType: types.ObjectType{ + AttributePlan: types.ListValue( + types.ObjectType{ AttrTypes: map[string]attr.Type{ "nested_attr": types.StringType, }, }, - Elems: []attr.Value{ - types.Object{ - AttrTypes: map[string]attr.Type{ + []attr.Value{ + types.ObjectValue( + map[string]attr.Type{ "nested_attr": types.StringType, }, - Attrs: map[string]attr.Value{ + map[string]attr.Value{ "nested_attr": types.String{Value: "newtestvalue"}, }, - }, + ), }, - }, + ), RequiresReplace: path.Paths{ path.Root("test").AtListIndex(0).AtName("nested_attr"), }, @@ -1824,23 +1821,23 @@ func TestBlockModifyPlan(t *testing.T) { }, ), expectedResp: ModifyAttributePlanResponse{ - AttributePlan: types.List{ - ElemType: types.ObjectType{ + AttributePlan: types.ListValue( + types.ObjectType{ AttrTypes: map[string]attr.Type{ "nested_attr": types.StringType, }, }, - Elems: []attr.Value{ - types.Object{ - AttrTypes: map[string]attr.Type{ + []attr.Value{ + types.ObjectValue( + map[string]attr.Type{ "nested_attr": types.StringType, }, - Attrs: map[string]attr.Value{ - "nested_attr": types.String{Value: "TESTATTRTWO"}, + map[string]attr.Value{ + "nested_attr": types.StringValue("TESTATTRTWO"), }, - }, + ), }, - }, + ), RequiresReplace: path.Paths{ path.Root("test").AtListIndex(0).AtName("nested_attr"), }, @@ -1874,23 +1871,23 @@ func TestBlockModifyPlan(t *testing.T) { }, ), expectedResp: ModifyAttributePlanResponse{ - AttributePlan: types.List{ - ElemType: types.ObjectType{ + AttributePlan: types.ListValue( + types.ObjectType{ AttrTypes: map[string]attr.Type{ "nested_attr": types.StringType, }, }, - Elems: []attr.Value{ - types.Object{ - AttrTypes: map[string]attr.Type{ + []attr.Value{ + types.ObjectValue( + map[string]attr.Type{ "nested_attr": types.StringType, }, - Attrs: map[string]attr.Value{ + map[string]attr.Value{ "nested_attr": types.String{Value: "newtestvalue"}, }, - }, + ), }, - }, + ), Private: testEmptyProviderData, }, }, @@ -1930,23 +1927,23 @@ func TestBlockModifyPlan(t *testing.T) { "This is a warning", ), }, - AttributePlan: types.List{ - ElemType: types.ObjectType{ + AttributePlan: types.ListValue( + types.ObjectType{ AttrTypes: map[string]attr.Type{ "nested_attr": types.StringType, }, }, - Elems: []attr.Value{ - types.Object{ - AttrTypes: map[string]attr.Type{ + []attr.Value{ + types.ObjectValue( + map[string]attr.Type{ "nested_attr": types.StringType, }, - Attrs: map[string]attr.Value{ + map[string]attr.Value{ "nested_attr": types.String{Value: "TESTDIAG"}, }, - }, + ), }, - }, + ), Private: testEmptyProviderData, }, }, @@ -1983,23 +1980,23 @@ func TestBlockModifyPlan(t *testing.T) { "This is an error", ), }, - AttributePlan: types.List{ - ElemType: types.ObjectType{ + AttributePlan: types.ListValue( + types.ObjectType{ AttrTypes: map[string]attr.Type{ "nested_attr": types.StringType, }, }, - Elems: []attr.Value{ - types.Object{ - AttrTypes: map[string]attr.Type{ + []attr.Value{ + types.ObjectValue( + map[string]attr.Type{ "nested_attr": types.StringType, }, - Attrs: map[string]attr.Value{ + map[string]attr.Value{ "nested_attr": types.String{Value: "TESTDIAG"}, }, - }, + ), }, - }, + ), Private: testEmptyProviderData, }, }, diff --git a/internal/fwserver/block_validation.go b/internal/fwserver/block_validation.go index 95c9c32ec..77d929a86 100644 --- a/internal/fwserver/block_validation.go +++ b/internal/fwserver/block_validation.go @@ -62,7 +62,7 @@ func BlockValidate(ctx context.Context, b fwschema.Block, req tfsdk.ValidateAttr return } - for idx := range l.Elems { + for idx := range l.Elements() { for name, attr := range b.GetAttributes() { nestedAttrReq := tfsdk.ValidateAttributeRequest{ AttributePath: req.AttributePath.AtListIndex(idx).AtName(name), @@ -100,8 +100,8 @@ func BlockValidate(ctx context.Context, b fwschema.Block, req tfsdk.ValidateAttr // Terraform 0.15.2 and later implements MaxItems validation during // configuration decoding, so if this framework drops Terraform support // for earlier versions, this validation can be removed. - if b.GetMaxItems() > 0 && int64(len(l.Elems)) > b.GetMaxItems() { - resp.Diagnostics.Append(blockMaxItemsDiagnostic(req.AttributePath, b.GetMaxItems(), len(l.Elems))) + if b.GetMaxItems() > 0 && int64(len(l.Elements())) > b.GetMaxItems() { + resp.Diagnostics.Append(blockMaxItemsDiagnostic(req.AttributePath, b.GetMaxItems(), len(l.Elements()))) } // Terraform 0.12 through 0.15.1 implement conservative block MinItems @@ -113,8 +113,8 @@ func BlockValidate(ctx context.Context, b fwschema.Block, req tfsdk.ValidateAttr // Terraform 0.15.2 and later implements proper MinItems validation // during configuration decoding, so if this framework drops Terraform // support for earlier versions, this validation can be removed. - if b.GetMinItems() > 0 && int64(len(l.Elems)) < b.GetMinItems() && !l.IsUnknown() { - resp.Diagnostics.Append(blockMinItemsDiagnostic(req.AttributePath, b.GetMinItems(), len(l.Elems))) + if b.GetMinItems() > 0 && int64(len(l.Elements())) < b.GetMinItems() && !l.IsUnknown() { + resp.Diagnostics.Append(blockMinItemsDiagnostic(req.AttributePath, b.GetMinItems(), len(l.Elements()))) } case fwschema.BlockNestingModeSet: s, ok := req.AttributeConfig.(types.Set) @@ -130,7 +130,7 @@ func BlockValidate(ctx context.Context, b fwschema.Block, req tfsdk.ValidateAttr return } - for _, value := range s.Elems { + for _, value := range s.Elements() { for name, attr := range b.GetAttributes() { nestedAttrReq := tfsdk.ValidateAttributeRequest{ AttributePath: req.AttributePath.AtSetValue(value).AtName(name), @@ -168,8 +168,8 @@ func BlockValidate(ctx context.Context, b fwschema.Block, req tfsdk.ValidateAttr // Terraform 0.15.2 and later implements MaxItems validation during // configuration decoding, so if this framework drops Terraform support // for earlier versions, this validation can be removed. - if b.GetMaxItems() > 0 && int64(len(s.Elems)) > b.GetMaxItems() { - resp.Diagnostics.Append(blockMaxItemsDiagnostic(req.AttributePath, b.GetMaxItems(), len(s.Elems))) + if b.GetMaxItems() > 0 && int64(len(s.Elements())) > b.GetMaxItems() { + resp.Diagnostics.Append(blockMaxItemsDiagnostic(req.AttributePath, b.GetMaxItems(), len(s.Elements()))) } // Terraform 0.12 through 0.15.1 implement conservative block MinItems @@ -181,8 +181,8 @@ func BlockValidate(ctx context.Context, b fwschema.Block, req tfsdk.ValidateAttr // Terraform 0.15.2 and later implements proper MinItems validation // during configuration decoding, so if this framework drops Terraform // support for earlier versions, this validation can be removed. - if b.GetMinItems() > 0 && int64(len(s.Elems)) < b.GetMinItems() && !s.IsUnknown() { - resp.Diagnostics.Append(blockMinItemsDiagnostic(req.AttributePath, b.GetMinItems(), len(s.Elems))) + if b.GetMinItems() > 0 && int64(len(s.Elements())) < b.GetMinItems() && !s.IsUnknown() { + resp.Diagnostics.Append(blockMinItemsDiagnostic(req.AttributePath, b.GetMinItems(), len(s.Elements()))) } case fwschema.BlockNestingModeSingle: s, ok := req.AttributeConfig.(types.Object) diff --git a/internal/fwserver/server_applyresourcechange_test.go b/internal/fwserver/server_applyresourcechange_test.go index 195962a5e..b10e5ba15 100644 --- a/internal/fwserver/server_applyresourcechange_test.go +++ b/internal/fwserver/server_applyresourcechange_test.go @@ -138,8 +138,8 @@ func TestServerApplyResourceChange(t *testing.T) { resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) - if data.TestRequired.Value != "test-config-value" { - resp.Diagnostics.AddError("unexpected req.Config value: %s", data.TestRequired.Value) + if data.TestRequired.ValueString() != "test-config-value" { + resp.Diagnostics.AddError("unexpected req.Config value: %s", data.TestRequired.ValueString()) } // Prevent missing resource state error diagnostic @@ -184,8 +184,8 @@ func TestServerApplyResourceChange(t *testing.T) { resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) - if data.TestRequired.Value != "test-plannedstate-value" { - resp.Diagnostics.AddError("unexpected req.Plan value: %s", data.TestRequired.Value) + if data.TestRequired.ValueString() != "test-plannedstate-value" { + resp.Diagnostics.AddError("unexpected req.Plan value: %s", data.TestRequired.ValueString()) } // Prevent missing resource state error diagnostic @@ -229,8 +229,8 @@ func TestServerApplyResourceChange(t *testing.T) { resp.Diagnostics.Append(req.ProviderMeta.Get(ctx, &metadata)...) - if metadata.TestProviderMetaAttribute.Value != "test-provider-meta-value" { - resp.Diagnostics.AddError("Unexpected req.ProviderMeta Value", "Got: "+metadata.TestProviderMetaAttribute.Value) + if metadata.TestProviderMetaAttribute.ValueString() != "test-provider-meta-value" { + resp.Diagnostics.AddError("Unexpected req.ProviderMeta Value", "Got: "+metadata.TestProviderMetaAttribute.ValueString()) } // Prevent missing resource state error diagnostic @@ -451,8 +451,8 @@ func TestServerApplyResourceChange(t *testing.T) { resp.Diagnostics.Append(req.State.Get(ctx, &data)...) - if data.TestRequired.Value != "test-priorstate-value" { - resp.Diagnostics.AddError("unexpected req.State value: %s", data.TestRequired.Value) + if data.TestRequired.ValueString() != "test-priorstate-value" { + resp.Diagnostics.AddError("unexpected req.State value: %s", data.TestRequired.ValueString()) } }, UpdateMethod: func(_ context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { @@ -487,8 +487,8 @@ func TestServerApplyResourceChange(t *testing.T) { resp.Diagnostics.Append(req.ProviderMeta.Get(ctx, &data)...) - if data.TestProviderMetaAttribute.Value != "test-provider-meta-value" { - resp.Diagnostics.AddError("unexpected req.ProviderMeta value: %s", data.TestProviderMetaAttribute.Value) + if data.TestProviderMetaAttribute.ValueString() != "test-provider-meta-value" { + resp.Diagnostics.AddError("unexpected req.ProviderMeta value: %s", data.TestProviderMetaAttribute.ValueString()) } }, UpdateMethod: func(_ context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { @@ -699,8 +699,8 @@ func TestServerApplyResourceChange(t *testing.T) { resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) - if data.TestRequired.Value != "test-new-value" { - resp.Diagnostics.AddError("Unexpected req.Config Value", "Got: "+data.TestRequired.Value) + if data.TestRequired.ValueString() != "test-new-value" { + resp.Diagnostics.AddError("Unexpected req.Config Value", "Got: "+data.TestRequired.ValueString()) } }, }, @@ -756,8 +756,8 @@ func TestServerApplyResourceChange(t *testing.T) { resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) - if data.TestComputed.Value != "test-plannedstate-value" { - resp.Diagnostics.AddError("Unexpected req.Plan Value", "Got: "+data.TestComputed.Value) + if data.TestComputed.ValueString() != "test-plannedstate-value" { + resp.Diagnostics.AddError("Unexpected req.Plan Value", "Got: "+data.TestComputed.ValueString()) } }, }, @@ -813,8 +813,8 @@ func TestServerApplyResourceChange(t *testing.T) { resp.Diagnostics.Append(req.State.Get(ctx, &data)...) - if data.TestRequired.Value != "test-old-value" { - resp.Diagnostics.AddError("Unexpected req.State Value", "Got: "+data.TestRequired.Value) + if data.TestRequired.ValueString() != "test-old-value" { + resp.Diagnostics.AddError("Unexpected req.State Value", "Got: "+data.TestRequired.ValueString()) } }, }, @@ -871,8 +871,8 @@ func TestServerApplyResourceChange(t *testing.T) { resp.Diagnostics.Append(req.ProviderMeta.Get(ctx, &data)...) - if data.TestProviderMetaAttribute.Value != "test-provider-meta-value" { - resp.Diagnostics.AddError("Unexpected req.ProviderMeta Value", "Got: "+data.TestProviderMetaAttribute.Value) + if data.TestProviderMetaAttribute.ValueString() != "test-provider-meta-value" { + resp.Diagnostics.AddError("Unexpected req.ProviderMeta Value", "Got: "+data.TestProviderMetaAttribute.ValueString()) } }, }, diff --git a/internal/fwserver/server_configureprovider_test.go b/internal/fwserver/server_configureprovider_test.go index 43b0ac4af..28ba5dd4f 100644 --- a/internal/fwserver/server_configureprovider_test.go +++ b/internal/fwserver/server_configureprovider_test.go @@ -68,8 +68,8 @@ func TestServerConfigureProvider(t *testing.T) { return } - if got.Value != "test-value" { - resp.Diagnostics.AddError("Incorrect req.Config", "expected test-value, got "+got.Value) + if got.ValueString() != "test-value" { + resp.Diagnostics.AddError("Incorrect req.Config", "expected test-value, got "+got.ValueString()) } }, }, diff --git a/internal/fwserver/server_createresource_test.go b/internal/fwserver/server_createresource_test.go index c74087572..6e13d494c 100644 --- a/internal/fwserver/server_createresource_test.go +++ b/internal/fwserver/server_createresource_test.go @@ -114,8 +114,8 @@ func TestServerCreateResource(t *testing.T) { resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) - if data.TestRequired.Value != "test-config-value" { - resp.Diagnostics.AddError("Unexpected req.Config Value", "Got: "+data.TestRequired.Value) + if data.TestRequired.ValueString() != "test-config-value" { + resp.Diagnostics.AddError("Unexpected req.Config Value", "Got: "+data.TestRequired.ValueString()) } // Prevent missing resource state error diagnostic @@ -153,8 +153,8 @@ func TestServerCreateResource(t *testing.T) { resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) - if data.TestRequired.Value != "test-plannedstate-value" { - resp.Diagnostics.AddError("Unexpected req.Plan Value", "Got: "+data.TestRequired.Value) + if data.TestRequired.ValueString() != "test-plannedstate-value" { + resp.Diagnostics.AddError("Unexpected req.Plan Value", "Got: "+data.TestRequired.ValueString()) } // Prevent missing resource state error diagnostic @@ -192,8 +192,8 @@ func TestServerCreateResource(t *testing.T) { resp.Diagnostics.Append(req.ProviderMeta.Get(ctx, &metadata)...) - if metadata.TestProviderMetaAttribute.Value != "test-provider-meta-value" { - resp.Diagnostics.AddError("Unexpected req.ProviderMeta Value", "Got: "+metadata.TestProviderMetaAttribute.Value) + if metadata.TestProviderMetaAttribute.ValueString() != "test-provider-meta-value" { + resp.Diagnostics.AddError("Unexpected req.ProviderMeta Value", "Got: "+metadata.TestProviderMetaAttribute.ValueString()) } // Prevent missing resource state error diagnostic diff --git a/internal/fwserver/server_deleteresource_test.go b/internal/fwserver/server_deleteresource_test.go index 9bf57ff95..0f177d85f 100644 --- a/internal/fwserver/server_deleteresource_test.go +++ b/internal/fwserver/server_deleteresource_test.go @@ -109,8 +109,8 @@ func TestServerDeleteResource(t *testing.T) { resp.Diagnostics.Append(req.State.Get(ctx, &data)...) - if data.TestRequired.Value != "test-priorstate-value" { - resp.Diagnostics.AddError("unexpected req.State value: %s", data.TestRequired.Value) + if data.TestRequired.ValueString() != "test-priorstate-value" { + resp.Diagnostics.AddError("unexpected req.State value: %s", data.TestRequired.ValueString()) } }, }, @@ -138,8 +138,8 @@ func TestServerDeleteResource(t *testing.T) { resp.Diagnostics.Append(req.ProviderMeta.Get(ctx, &data)...) - if data.TestProviderMetaAttribute.Value != "test-provider-meta-value" { - resp.Diagnostics.AddError("unexpected req.ProviderMeta value: %s", data.TestProviderMetaAttribute.Value) + if data.TestProviderMetaAttribute.ValueString() != "test-provider-meta-value" { + resp.Diagnostics.AddError("unexpected req.ProviderMeta value: %s", data.TestProviderMetaAttribute.ValueString()) } }, }, diff --git a/internal/fwserver/server_planresourcechange_test.go b/internal/fwserver/server_planresourcechange_test.go index 3d0db6fda..ee0e14c48 100644 --- a/internal/fwserver/server_planresourcechange_test.go +++ b/internal/fwserver/server_planresourcechange_test.go @@ -810,8 +810,8 @@ func TestServerPlanResourceChange(t *testing.T) { resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) - if data.TestRequired.Value != "test-config-value" { - resp.Diagnostics.AddError("Unexpected req.Config Value", "Got: "+data.TestRequired.Value) + if data.TestRequired.ValueString() != "test-config-value" { + resp.Diagnostics.AddError("Unexpected req.Config Value", "Got: "+data.TestRequired.ValueString()) } }, }, @@ -902,8 +902,8 @@ func TestServerPlanResourceChange(t *testing.T) { resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) - if !data.TestComputed.Unknown { - resp.Diagnostics.AddError("Unexpected req.Plan Value", "Got: "+data.TestComputed.Value) + if !data.TestComputed.IsUnknown() { + resp.Diagnostics.AddError("Unexpected req.Plan Value", "Got: "+data.TestComputed.ValueString()) } }, }, @@ -947,8 +947,8 @@ func TestServerPlanResourceChange(t *testing.T) { resp.Diagnostics.Append(req.ProviderMeta.Get(ctx, &data)...) - if data.TestProviderMetaAttribute.Value != "test-provider-meta-value" { - resp.Diagnostics.AddError("Unexpected req.ProviderMeta Value", "Got: "+data.TestProviderMetaAttribute.Value) + if data.TestProviderMetaAttribute.ValueString() != "test-provider-meta-value" { + resp.Diagnostics.AddError("Unexpected req.ProviderMeta Value", "Got: "+data.TestProviderMetaAttribute.ValueString()) } }, }, @@ -1211,8 +1211,8 @@ func TestServerPlanResourceChange(t *testing.T) { resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) - if data.TestRequired.Value != "test-config-value" { - resp.Diagnostics.AddError("Unexpected req.Config Value", "Got: "+data.TestRequired.Value) + if data.TestRequired.ValueString() != "test-config-value" { + resp.Diagnostics.AddError("Unexpected req.Config Value", "Got: "+data.TestRequired.ValueString()) } }, }, @@ -1291,8 +1291,8 @@ func TestServerPlanResourceChange(t *testing.T) { resp.Diagnostics.Append(req.State.Get(ctx, &data)...) - if data.TestRequired.Value != "test-state-value" { - resp.Diagnostics.AddError("Unexpected req.State Value", "Got: "+data.TestRequired.Value) + if data.TestRequired.ValueString() != "test-state-value" { + resp.Diagnostics.AddError("Unexpected req.State Value", "Got: "+data.TestRequired.ValueString()) } }, }, @@ -1330,8 +1330,8 @@ func TestServerPlanResourceChange(t *testing.T) { resp.Diagnostics.Append(req.ProviderMeta.Get(ctx, &data)...) - if data.TestProviderMetaAttribute.Value != "test-provider-meta-value" { - resp.Diagnostics.AddError("Unexpected req.ProviderMeta Value", "Got: "+data.TestProviderMetaAttribute.Value) + if data.TestProviderMetaAttribute.ValueString() != "test-provider-meta-value" { + resp.Diagnostics.AddError("Unexpected req.ProviderMeta Value", "Got: "+data.TestProviderMetaAttribute.ValueString()) } }, }, @@ -1918,8 +1918,8 @@ func TestServerPlanResourceChange(t *testing.T) { resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) - if data.TestRequired.Value != "test-new-value" { - resp.Diagnostics.AddError("Unexpected req.Config Value", "Got: "+data.TestRequired.Value) + if data.TestRequired.ValueString() != "test-new-value" { + resp.Diagnostics.AddError("Unexpected req.Config Value", "Got: "+data.TestRequired.ValueString()) } }, }, @@ -1968,8 +1968,8 @@ func TestServerPlanResourceChange(t *testing.T) { resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) - if !data.TestComputed.Unknown { - resp.Diagnostics.AddError("Unexpected req.Plan Value", "Got: "+data.TestComputed.Value) + if !data.TestComputed.IsUnknown() { + resp.Diagnostics.AddError("Unexpected req.Plan Value", "Got: "+data.TestComputed.ValueString()) } }, }, @@ -2019,8 +2019,8 @@ func TestServerPlanResourceChange(t *testing.T) { resp.Diagnostics.Append(req.ProviderMeta.Get(ctx, &data)...) - if data.TestProviderMetaAttribute.Value != "test-provider-meta-value" { - resp.Diagnostics.AddError("Unexpected req.ProviderMeta Value", "Got: "+data.TestProviderMetaAttribute.Value) + if data.TestProviderMetaAttribute.ValueString() != "test-provider-meta-value" { + resp.Diagnostics.AddError("Unexpected req.ProviderMeta Value", "Got: "+data.TestProviderMetaAttribute.ValueString()) } }, }, diff --git a/internal/fwserver/server_readdatasource_test.go b/internal/fwserver/server_readdatasource_test.go index 71db54bab..7c0f95149 100644 --- a/internal/fwserver/server_readdatasource_test.go +++ b/internal/fwserver/server_readdatasource_test.go @@ -90,8 +90,8 @@ func TestServerReadDataSource(t *testing.T) { resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) - if config.TestRequired.Value != "test-config-value" { - resp.Diagnostics.AddError("unexpected req.Config value: %s", config.TestRequired.Value) + if config.TestRequired.ValueString() != "test-config-value" { + resp.Diagnostics.AddError("unexpected req.Config value: %s", config.TestRequired.ValueString()) } }, }, @@ -116,8 +116,8 @@ func TestServerReadDataSource(t *testing.T) { resp.Diagnostics.Append(req.ProviderMeta.Get(ctx, &config)...) - if config.TestRequired.Value != "test-config-value" { - resp.Diagnostics.AddError("unexpected req.ProviderMeta value: %s", config.TestRequired.Value) + if config.TestRequired.ValueString() != "test-config-value" { + resp.Diagnostics.AddError("unexpected req.ProviderMeta value: %s", config.TestRequired.ValueString()) } }, }, diff --git a/internal/fwserver/server_readresource_test.go b/internal/fwserver/server_readresource_test.go index 6bdd9a3a0..5ca6c60aa 100644 --- a/internal/fwserver/server_readresource_test.go +++ b/internal/fwserver/server_readresource_test.go @@ -141,8 +141,8 @@ func TestServerReadResource(t *testing.T) { resp.Diagnostics.Append(req.State.Get(ctx, &data)...) - if data.TestRequired.Value != "test-currentstate-value" { - resp.Diagnostics.AddError("unexpected req.State value: %s", data.TestRequired.Value) + if data.TestRequired.ValueString() != "test-currentstate-value" { + resp.Diagnostics.AddError("unexpected req.State value: %s", data.TestRequired.ValueString()) } }, }, @@ -167,8 +167,8 @@ func TestServerReadResource(t *testing.T) { resp.Diagnostics.Append(req.ProviderMeta.Get(ctx, &config)...) - if config.TestRequired.Value != "test-currentstate-value" { - resp.Diagnostics.AddError("unexpected req.ProviderMeta value: %s", config.TestRequired.Value) + if config.TestRequired.ValueString() != "test-currentstate-value" { + resp.Diagnostics.AddError("unexpected req.ProviderMeta value: %s", config.TestRequired.ValueString()) } }, }, diff --git a/internal/fwserver/server_updateresource_test.go b/internal/fwserver/server_updateresource_test.go index 5c8e9dbfd..9baae760f 100644 --- a/internal/fwserver/server_updateresource_test.go +++ b/internal/fwserver/server_updateresource_test.go @@ -141,8 +141,8 @@ func TestServerUpdateResource(t *testing.T) { resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) - if data.TestRequired.Value != "test-new-value" { - resp.Diagnostics.AddError("Unexpected req.Config Value", "Got: "+data.TestRequired.Value) + if data.TestRequired.ValueString() != "test-new-value" { + resp.Diagnostics.AddError("Unexpected req.Config Value", "Got: "+data.TestRequired.ValueString()) } }, }, @@ -192,8 +192,8 @@ func TestServerUpdateResource(t *testing.T) { resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) - if data.TestComputed.Value != "test-plannedstate-value" { - resp.Diagnostics.AddError("Unexpected req.Plan Value", "Got: "+data.TestComputed.Value) + if data.TestComputed.ValueString() != "test-plannedstate-value" { + resp.Diagnostics.AddError("Unexpected req.Plan Value", "Got: "+data.TestComputed.ValueString()) } }, }, @@ -243,8 +243,8 @@ func TestServerUpdateResource(t *testing.T) { resp.Diagnostics.Append(req.State.Get(ctx, &data)...) - if data.TestRequired.Value != "test-old-value" { - resp.Diagnostics.AddError("Unexpected req.State Value", "Got: "+data.TestRequired.Value) + if data.TestRequired.ValueString() != "test-old-value" { + resp.Diagnostics.AddError("Unexpected req.State Value", "Got: "+data.TestRequired.ValueString()) } }, }, @@ -295,8 +295,8 @@ func TestServerUpdateResource(t *testing.T) { resp.Diagnostics.Append(req.ProviderMeta.Get(ctx, &data)...) - if data.TestProviderMetaAttribute.Value != "test-provider-meta-value" { - resp.Diagnostics.AddError("Unexpected req.ProviderMeta Value", "Got: "+data.TestProviderMetaAttribute.Value) + if data.TestProviderMetaAttribute.ValueString() != "test-provider-meta-value" { + resp.Diagnostics.AddError("Unexpected req.ProviderMeta Value", "Got: "+data.TestProviderMetaAttribute.ValueString()) } }, }, diff --git a/internal/fwserver/server_validatedatasourceconfig_test.go b/internal/fwserver/server_validatedatasourceconfig_test.go index af33e9f88..358a26da4 100644 --- a/internal/fwserver/server_validatedatasourceconfig_test.go +++ b/internal/fwserver/server_validatedatasourceconfig_test.go @@ -58,8 +58,8 @@ func TestServerValidateDataSourceConfig(t *testing.T) { return } - if got.Value != "test-value" { - resp.Diagnostics.AddError("Incorrect req.AttributeConfig", "expected test-value, got "+got.Value) + if got.ValueString() != "test-value" { + resp.Diagnostics.AddError("Incorrect req.AttributeConfig", "expected test-value, got "+got.ValueString()) } }, }, @@ -179,8 +179,8 @@ func TestServerValidateDataSourceConfig(t *testing.T) { return } - if got.Value != "test-value" { - resp.Diagnostics.AddError("Incorrect req.Config", "expected test-value, got "+got.Value) + if got.ValueString() != "test-value" { + resp.Diagnostics.AddError("Incorrect req.Config", "expected test-value, got "+got.ValueString()) } }, }, @@ -242,8 +242,8 @@ func TestServerValidateDataSourceConfig(t *testing.T) { return } - if got.Value != "test-value" { - resp.Diagnostics.AddError("Incorrect req.Config", "expected test-value, got "+got.Value) + if got.ValueString() != "test-value" { + resp.Diagnostics.AddError("Incorrect req.Config", "expected test-value, got "+got.ValueString()) } }, }, diff --git a/internal/fwserver/server_validateproviderconfig_test.go b/internal/fwserver/server_validateproviderconfig_test.go index c2607de62..68438b68a 100644 --- a/internal/fwserver/server_validateproviderconfig_test.go +++ b/internal/fwserver/server_validateproviderconfig_test.go @@ -58,8 +58,8 @@ func TestServerValidateProviderConfig(t *testing.T) { return } - if got.Value != "test-value" { - resp.Diagnostics.AddError("Incorrect req.AttributeConfig", "expected test-value, got "+got.Value) + if got.ValueString() != "test-value" { + resp.Diagnostics.AddError("Incorrect req.AttributeConfig", "expected test-value, got "+got.ValueString()) } }, }, @@ -177,8 +177,8 @@ func TestServerValidateProviderConfig(t *testing.T) { return } - if got.Value != "test-value" { - resp.Diagnostics.AddError("Incorrect req.Config", "expected test-value, got "+got.Value) + if got.ValueString() != "test-value" { + resp.Diagnostics.AddError("Incorrect req.Config", "expected test-value, got "+got.ValueString()) } }, }, @@ -242,8 +242,8 @@ func TestServerValidateProviderConfig(t *testing.T) { return } - if got.Value != "test-value" { - resp.Diagnostics.AddError("Incorrect req.Config", "expected test-value, got "+got.Value) + if got.ValueString() != "test-value" { + resp.Diagnostics.AddError("Incorrect req.Config", "expected test-value, got "+got.ValueString()) } }, }, diff --git a/internal/fwserver/server_validateresourceconfig_test.go b/internal/fwserver/server_validateresourceconfig_test.go index fd4d32bb9..2552b9a03 100644 --- a/internal/fwserver/server_validateresourceconfig_test.go +++ b/internal/fwserver/server_validateresourceconfig_test.go @@ -58,8 +58,8 @@ func TestServerValidateResourceConfig(t *testing.T) { return } - if got.Value != "test-value" { - resp.Diagnostics.AddError("Incorrect req.AttributeConfig", "expected test-value, got "+got.Value) + if got.ValueString() != "test-value" { + resp.Diagnostics.AddError("Incorrect req.AttributeConfig", "expected test-value, got "+got.ValueString()) } }, }, @@ -179,8 +179,8 @@ func TestServerValidateResourceConfig(t *testing.T) { return } - if got.Value != "test-value" { - resp.Diagnostics.AddError("Incorrect req.Config", "expected test-value, got "+got.Value) + if got.ValueString() != "test-value" { + resp.Diagnostics.AddError("Incorrect req.Config", "expected test-value, got "+got.ValueString()) } }, }, @@ -242,8 +242,8 @@ func TestServerValidateResourceConfig(t *testing.T) { return } - if got.Value != "test-value" { - resp.Diagnostics.AddError("Incorrect req.Config", "expected test-value, got "+got.Value) + if got.ValueString() != "test-value" { + resp.Diagnostics.AddError("Incorrect req.Config", "expected test-value, got "+got.ValueString()) } }, }, diff --git a/internal/proto5server/server_applyresourcechange_test.go b/internal/proto5server/server_applyresourcechange_test.go index 7450d4025..582160d0e 100644 --- a/internal/proto5server/server_applyresourcechange_test.go +++ b/internal/proto5server/server_applyresourcechange_test.go @@ -96,8 +96,8 @@ func TestServerApplyResourceChange(t *testing.T) { resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) - if data.TestRequired.Value != "test-config-value" { - resp.Diagnostics.AddError("Unexpected req.Config Value", "Got: "+data.TestRequired.Value) + if data.TestRequired.ValueString() != "test-config-value" { + resp.Diagnostics.AddError("Unexpected req.Config Value", "Got: "+data.TestRequired.ValueString()) } // Prevent missing resource state error diagnostic @@ -154,8 +154,8 @@ func TestServerApplyResourceChange(t *testing.T) { resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) - if data.TestComputed.Value != "test-plannedstate-value" { - resp.Diagnostics.AddError("Unexpected req.Plan Value", "Got: "+data.TestComputed.Value) + if data.TestComputed.ValueString() != "test-plannedstate-value" { + resp.Diagnostics.AddError("Unexpected req.Plan Value", "Got: "+data.TestComputed.ValueString()) } // Prevent missing resource state error diagnostic @@ -213,8 +213,8 @@ func TestServerApplyResourceChange(t *testing.T) { resp.Diagnostics.Append(req.ProviderMeta.Get(ctx, &metadata)...) - if metadata.TestProviderMetaAttribute.Value != "test-provider-meta-value" { - resp.Diagnostics.AddError("Unexpected req.ProviderMeta Value", "Got: "+metadata.TestProviderMetaAttribute.Value) + if metadata.TestProviderMetaAttribute.ValueString() != "test-provider-meta-value" { + resp.Diagnostics.AddError("Unexpected req.ProviderMeta Value", "Got: "+metadata.TestProviderMetaAttribute.ValueString()) } // Prevent missing resource state error diagnostic @@ -499,8 +499,8 @@ func TestServerApplyResourceChange(t *testing.T) { resp.Diagnostics.Append(req.State.Get(ctx, &data)...) - if data.TestRequired.Value != "test-priorstate-value" { - resp.Diagnostics.AddError("Unexpected req.State Value", "Got: "+data.TestRequired.Value) + if data.TestRequired.ValueString() != "test-priorstate-value" { + resp.Diagnostics.AddError("Unexpected req.State Value", "Got: "+data.TestRequired.ValueString()) } }, UpdateMethod: func(_ context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { @@ -548,8 +548,8 @@ func TestServerApplyResourceChange(t *testing.T) { resp.Diagnostics.Append(req.ProviderMeta.Get(ctx, &data)...) - if data.TestProviderMetaAttribute.Value != "test-provider-meta-value" { - resp.Diagnostics.AddError("Unexpected req.ProviderMeta Value", "Got: "+data.TestProviderMetaAttribute.Value) + if data.TestProviderMetaAttribute.ValueString() != "test-provider-meta-value" { + resp.Diagnostics.AddError("Unexpected req.ProviderMeta Value", "Got: "+data.TestProviderMetaAttribute.ValueString()) } }, UpdateMethod: func(_ context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { @@ -759,8 +759,8 @@ func TestServerApplyResourceChange(t *testing.T) { resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) - if data.TestRequired.Value != "test-new-value" { - resp.Diagnostics.AddError("Unexpected req.Config Value", "Got: "+data.TestRequired.Value) + if data.TestRequired.ValueString() != "test-new-value" { + resp.Diagnostics.AddError("Unexpected req.Config Value", "Got: "+data.TestRequired.ValueString()) } }, } @@ -819,8 +819,8 @@ func TestServerApplyResourceChange(t *testing.T) { resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) - if data.TestComputed.Value != "test-plannedstate-value" { - resp.Diagnostics.AddError("Unexpected req.Plan Value", "Got: "+data.TestComputed.Value) + if data.TestComputed.ValueString() != "test-plannedstate-value" { + resp.Diagnostics.AddError("Unexpected req.Plan Value", "Got: "+data.TestComputed.ValueString()) } }, } @@ -878,8 +878,8 @@ func TestServerApplyResourceChange(t *testing.T) { resp.Diagnostics.Append(req.State.Get(ctx, &data)...) - if data.TestRequired.Value != "test-old-value" { - resp.Diagnostics.AddError("Unexpected req.State Value", "Got: "+data.TestRequired.Value) + if data.TestRequired.ValueString() != "test-old-value" { + resp.Diagnostics.AddError("Unexpected req.State Value", "Got: "+data.TestRequired.ValueString()) } }, } @@ -938,8 +938,8 @@ func TestServerApplyResourceChange(t *testing.T) { resp.Diagnostics.Append(req.ProviderMeta.Get(ctx, &data)...) - if data.TestProviderMetaAttribute.Value != "test-provider-meta-value" { - resp.Diagnostics.AddError("Unexpected req.ProviderMeta Value", "Got: "+data.TestProviderMetaAttribute.Value) + if data.TestProviderMetaAttribute.ValueString() != "test-provider-meta-value" { + resp.Diagnostics.AddError("Unexpected req.ProviderMeta Value", "Got: "+data.TestProviderMetaAttribute.ValueString()) } }, } diff --git a/internal/proto5server/server_configureprovider_test.go b/internal/proto5server/server_configureprovider_test.go index ec53a0dcd..6b143945d 100644 --- a/internal/proto5server/server_configureprovider_test.go +++ b/internal/proto5server/server_configureprovider_test.go @@ -93,8 +93,8 @@ func TestServerConfigureProvider(t *testing.T) { return } - if got.Value != "test-value" { - resp.Diagnostics.AddError("Incorrect req.Config", "expected test-value, got "+got.Value) + if got.ValueString() != "test-value" { + resp.Diagnostics.AddError("Incorrect req.Config", "expected test-value, got "+got.ValueString()) } }, }, diff --git a/internal/proto5server/server_planresourcechange_test.go b/internal/proto5server/server_planresourcechange_test.go index d206871c0..44dd35b15 100644 --- a/internal/proto5server/server_planresourcechange_test.go +++ b/internal/proto5server/server_planresourcechange_test.go @@ -96,8 +96,8 @@ func TestServerPlanResourceChange(t *testing.T) { resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) - if data.TestRequired.Value != "test-config-value" { - resp.Diagnostics.AddError("Unexpected req.Config Value", "Got: "+data.TestRequired.Value) + if data.TestRequired.ValueString() != "test-config-value" { + resp.Diagnostics.AddError("Unexpected req.Config Value", "Got: "+data.TestRequired.ValueString()) } }, } @@ -147,8 +147,8 @@ func TestServerPlanResourceChange(t *testing.T) { resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) - if !data.TestComputed.Unknown { - resp.Diagnostics.AddError("Unexpected req.Plan Value", "Got: "+data.TestComputed.Value) + if !data.TestComputed.IsUnknown() { + resp.Diagnostics.AddError("Unexpected req.Plan Value", "Got: "+data.TestComputed.ValueString()) } }, } @@ -199,8 +199,8 @@ func TestServerPlanResourceChange(t *testing.T) { resp.Diagnostics.Append(req.ProviderMeta.Get(ctx, &data)...) - if data.TestProviderMetaAttribute.Value != "test-provider-meta-value" { - resp.Diagnostics.AddError("Unexpected req.ProviderMeta Value", "Got: "+data.TestProviderMetaAttribute.Value) + if data.TestProviderMetaAttribute.ValueString() != "test-provider-meta-value" { + resp.Diagnostics.AddError("Unexpected req.ProviderMeta Value", "Got: "+data.TestProviderMetaAttribute.ValueString()) } }, } @@ -420,8 +420,8 @@ func TestServerPlanResourceChange(t *testing.T) { resp.Diagnostics.Append(req.State.Get(ctx, &data)...) - if data.TestRequired.Value != "test-priorstate-value" { - resp.Diagnostics.AddError("Unexpected req.State Value", "Got: "+data.TestRequired.Value) + if data.TestRequired.ValueString() != "test-priorstate-value" { + resp.Diagnostics.AddError("Unexpected req.State Value", "Got: "+data.TestRequired.ValueString()) } }, } @@ -465,8 +465,8 @@ func TestServerPlanResourceChange(t *testing.T) { resp.Diagnostics.Append(req.ProviderMeta.Get(ctx, &data)...) - if data.TestProviderMetaAttribute.Value != "test-provider-meta-value" { - resp.Diagnostics.AddError("Unexpected req.ProviderMeta Value", "Got: "+data.TestProviderMetaAttribute.Value) + if data.TestProviderMetaAttribute.ValueString() != "test-provider-meta-value" { + resp.Diagnostics.AddError("Unexpected req.ProviderMeta Value", "Got: "+data.TestProviderMetaAttribute.ValueString()) } }, } @@ -665,8 +665,8 @@ func TestServerPlanResourceChange(t *testing.T) { resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) - if data.TestRequired.Value != "test-new-value" { - resp.Diagnostics.AddError("Unexpected req.Config Value", "Got: "+data.TestRequired.Value) + if data.TestRequired.ValueString() != "test-new-value" { + resp.Diagnostics.AddError("Unexpected req.Config Value", "Got: "+data.TestRequired.ValueString()) } }, } @@ -719,8 +719,8 @@ func TestServerPlanResourceChange(t *testing.T) { resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) - if !data.TestComputed.Unknown { - resp.Diagnostics.AddError("Unexpected req.Plan Value", "Got: "+data.TestComputed.Value) + if !data.TestComputed.IsUnknown() { + resp.Diagnostics.AddError("Unexpected req.Plan Value", "Got: "+data.TestComputed.ValueString()) } }, } @@ -773,8 +773,8 @@ func TestServerPlanResourceChange(t *testing.T) { resp.Diagnostics.Append(req.State.Get(ctx, &data)...) - if data.TestRequired.Value != "test-old-value" { - resp.Diagnostics.AddError("Unexpected req.State Value", "Got: "+data.TestRequired.Value) + if data.TestRequired.ValueString() != "test-old-value" { + resp.Diagnostics.AddError("Unexpected req.State Value", "Got: "+data.TestRequired.ValueString()) } }, } @@ -828,8 +828,8 @@ func TestServerPlanResourceChange(t *testing.T) { resp.Diagnostics.Append(req.ProviderMeta.Get(ctx, &data)...) - if data.TestProviderMetaAttribute.Value != "test-provider-meta-value" { - resp.Diagnostics.AddError("Unexpected req.ProviderMeta Value", "Got: "+data.TestProviderMetaAttribute.Value) + if data.TestProviderMetaAttribute.ValueString() != "test-provider-meta-value" { + resp.Diagnostics.AddError("Unexpected req.ProviderMeta Value", "Got: "+data.TestProviderMetaAttribute.ValueString()) } }, } diff --git a/internal/proto5server/server_readdatasource_test.go b/internal/proto5server/server_readdatasource_test.go index d8a0b1148..8f00dd9ad 100644 --- a/internal/proto5server/server_readdatasource_test.go +++ b/internal/proto5server/server_readdatasource_test.go @@ -107,8 +107,8 @@ func TestServerReadDataSource(t *testing.T) { resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) - if config.TestRequired.Value != "test-config-value" { - resp.Diagnostics.AddError("unexpected req.Config value: %s", config.TestRequired.Value) + if config.TestRequired.ValueString() != "test-config-value" { + resp.Diagnostics.AddError("unexpected req.Config value: %s", config.TestRequired.ValueString()) } }, } @@ -149,8 +149,8 @@ func TestServerReadDataSource(t *testing.T) { resp.Diagnostics.Append(req.ProviderMeta.Get(ctx, &config)...) - if config.TestRequired.Value != "test-config-value" { - resp.Diagnostics.AddError("unexpected req.ProviderMeta value: %s", config.TestRequired.Value) + if config.TestRequired.ValueString() != "test-config-value" { + resp.Diagnostics.AddError("unexpected req.ProviderMeta value: %s", config.TestRequired.ValueString()) } }, } diff --git a/internal/proto5server/server_readresource_test.go b/internal/proto5server/server_readresource_test.go index a36d3c274..daaf37177 100644 --- a/internal/proto5server/server_readresource_test.go +++ b/internal/proto5server/server_readresource_test.go @@ -112,8 +112,8 @@ func TestServerReadResource(t *testing.T) { resp.Diagnostics.Append(req.State.Get(ctx, &data)...) - if data.TestRequired.Value != "test-currentstate-value" { - resp.Diagnostics.AddError("unexpected req.State value: %s", data.TestRequired.Value) + if data.TestRequired.ValueString() != "test-currentstate-value" { + resp.Diagnostics.AddError("unexpected req.State value: %s", data.TestRequired.ValueString()) } }, } @@ -154,8 +154,8 @@ func TestServerReadResource(t *testing.T) { resp.Diagnostics.Append(req.ProviderMeta.Get(ctx, &data)...) - if data.TestRequired.Value != "test-currentstate-value" { - resp.Diagnostics.AddError("unexpected req.ProviderMeta value: %s", data.TestRequired.Value) + if data.TestRequired.ValueString() != "test-currentstate-value" { + resp.Diagnostics.AddError("unexpected req.ProviderMeta value: %s", data.TestRequired.ValueString()) } }, } diff --git a/internal/proto6server/server_applyresourcechange_test.go b/internal/proto6server/server_applyresourcechange_test.go index d99bb524a..7075f879a 100644 --- a/internal/proto6server/server_applyresourcechange_test.go +++ b/internal/proto6server/server_applyresourcechange_test.go @@ -96,8 +96,8 @@ func TestServerApplyResourceChange(t *testing.T) { resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) - if data.TestRequired.Value != "test-config-value" { - resp.Diagnostics.AddError("Unexpected req.Config Value", "Got: "+data.TestRequired.Value) + if data.TestRequired.ValueString() != "test-config-value" { + resp.Diagnostics.AddError("Unexpected req.Config Value", "Got: "+data.TestRequired.ValueString()) } // Prevent missing resource state error diagnostic @@ -154,8 +154,8 @@ func TestServerApplyResourceChange(t *testing.T) { resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) - if data.TestComputed.Value != "test-plannedstate-value" { - resp.Diagnostics.AddError("Unexpected req.Plan Value", "Got: "+data.TestComputed.Value) + if data.TestComputed.ValueString() != "test-plannedstate-value" { + resp.Diagnostics.AddError("Unexpected req.Plan Value", "Got: "+data.TestComputed.ValueString()) } // Prevent missing resource state error diagnostic @@ -213,8 +213,8 @@ func TestServerApplyResourceChange(t *testing.T) { resp.Diagnostics.Append(req.ProviderMeta.Get(ctx, &metadata)...) - if metadata.TestProviderMetaAttribute.Value != "test-provider-meta-value" { - resp.Diagnostics.AddError("Unexpected req.ProviderMeta Value", "Got: "+metadata.TestProviderMetaAttribute.Value) + if metadata.TestProviderMetaAttribute.ValueString() != "test-provider-meta-value" { + resp.Diagnostics.AddError("Unexpected req.ProviderMeta Value", "Got: "+metadata.TestProviderMetaAttribute.ValueString()) } // Prevent missing resource state error diagnostic @@ -499,8 +499,8 @@ func TestServerApplyResourceChange(t *testing.T) { resp.Diagnostics.Append(req.State.Get(ctx, &data)...) - if data.TestRequired.Value != "test-priorstate-value" { - resp.Diagnostics.AddError("Unexpected req.State Value", "Got: "+data.TestRequired.Value) + if data.TestRequired.ValueString() != "test-priorstate-value" { + resp.Diagnostics.AddError("Unexpected req.State Value", "Got: "+data.TestRequired.ValueString()) } }, UpdateMethod: func(_ context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { @@ -548,8 +548,8 @@ func TestServerApplyResourceChange(t *testing.T) { resp.Diagnostics.Append(req.ProviderMeta.Get(ctx, &data)...) - if data.TestProviderMetaAttribute.Value != "test-provider-meta-value" { - resp.Diagnostics.AddError("Unexpected req.ProviderMeta Value", "Got: "+data.TestProviderMetaAttribute.Value) + if data.TestProviderMetaAttribute.ValueString() != "test-provider-meta-value" { + resp.Diagnostics.AddError("Unexpected req.ProviderMeta Value", "Got: "+data.TestProviderMetaAttribute.ValueString()) } }, UpdateMethod: func(_ context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { @@ -759,8 +759,8 @@ func TestServerApplyResourceChange(t *testing.T) { resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) - if data.TestRequired.Value != "test-new-value" { - resp.Diagnostics.AddError("Unexpected req.Config Value", "Got: "+data.TestRequired.Value) + if data.TestRequired.ValueString() != "test-new-value" { + resp.Diagnostics.AddError("Unexpected req.Config Value", "Got: "+data.TestRequired.ValueString()) } }, } @@ -819,8 +819,8 @@ func TestServerApplyResourceChange(t *testing.T) { resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) - if data.TestComputed.Value != "test-plannedstate-value" { - resp.Diagnostics.AddError("Unexpected req.Plan Value", "Got: "+data.TestComputed.Value) + if data.TestComputed.ValueString() != "test-plannedstate-value" { + resp.Diagnostics.AddError("Unexpected req.Plan Value", "Got: "+data.TestComputed.ValueString()) } }, } @@ -878,8 +878,8 @@ func TestServerApplyResourceChange(t *testing.T) { resp.Diagnostics.Append(req.State.Get(ctx, &data)...) - if data.TestRequired.Value != "test-old-value" { - resp.Diagnostics.AddError("Unexpected req.State Value", "Got: "+data.TestRequired.Value) + if data.TestRequired.ValueString() != "test-old-value" { + resp.Diagnostics.AddError("Unexpected req.State Value", "Got: "+data.TestRequired.ValueString()) } }, } @@ -938,8 +938,8 @@ func TestServerApplyResourceChange(t *testing.T) { resp.Diagnostics.Append(req.ProviderMeta.Get(ctx, &data)...) - if data.TestProviderMetaAttribute.Value != "test-provider-meta-value" { - resp.Diagnostics.AddError("Unexpected req.ProviderMeta Value", "Got: "+data.TestProviderMetaAttribute.Value) + if data.TestProviderMetaAttribute.ValueString() != "test-provider-meta-value" { + resp.Diagnostics.AddError("Unexpected req.ProviderMeta Value", "Got: "+data.TestProviderMetaAttribute.ValueString()) } }, } diff --git a/internal/proto6server/server_configureprovider_test.go b/internal/proto6server/server_configureprovider_test.go index ae3d1b538..005f2a2b5 100644 --- a/internal/proto6server/server_configureprovider_test.go +++ b/internal/proto6server/server_configureprovider_test.go @@ -93,8 +93,8 @@ func TestServerConfigureProvider(t *testing.T) { return } - if got.Value != "test-value" { - resp.Diagnostics.AddError("Incorrect req.Config", "expected test-value, got "+got.Value) + if got.ValueString() != "test-value" { + resp.Diagnostics.AddError("Incorrect req.Config", "expected test-value, got "+got.ValueString()) } }, }, diff --git a/internal/proto6server/server_planresourcechange_test.go b/internal/proto6server/server_planresourcechange_test.go index 36ba4d5e5..83be2b9cf 100644 --- a/internal/proto6server/server_planresourcechange_test.go +++ b/internal/proto6server/server_planresourcechange_test.go @@ -96,8 +96,8 @@ func TestServerPlanResourceChange(t *testing.T) { resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) - if data.TestRequired.Value != "test-config-value" { - resp.Diagnostics.AddError("Unexpected req.Config Value", "Got: "+data.TestRequired.Value) + if data.TestRequired.ValueString() != "test-config-value" { + resp.Diagnostics.AddError("Unexpected req.Config Value", "Got: "+data.TestRequired.ValueString()) } }, } @@ -147,8 +147,8 @@ func TestServerPlanResourceChange(t *testing.T) { resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) - if !data.TestComputed.Unknown { - resp.Diagnostics.AddError("Unexpected req.Plan Value", "Got: "+data.TestComputed.Value) + if !data.TestComputed.IsUnknown() { + resp.Diagnostics.AddError("Unexpected req.Plan Value", "Got: "+data.TestComputed.ValueString()) } }, } @@ -199,8 +199,8 @@ func TestServerPlanResourceChange(t *testing.T) { resp.Diagnostics.Append(req.ProviderMeta.Get(ctx, &data)...) - if data.TestProviderMetaAttribute.Value != "test-provider-meta-value" { - resp.Diagnostics.AddError("Unexpected req.ProviderMeta Value", "Got: "+data.TestProviderMetaAttribute.Value) + if data.TestProviderMetaAttribute.ValueString() != "test-provider-meta-value" { + resp.Diagnostics.AddError("Unexpected req.ProviderMeta Value", "Got: "+data.TestProviderMetaAttribute.ValueString()) } }, } @@ -420,8 +420,8 @@ func TestServerPlanResourceChange(t *testing.T) { resp.Diagnostics.Append(req.State.Get(ctx, &data)...) - if data.TestRequired.Value != "test-priorstate-value" { - resp.Diagnostics.AddError("Unexpected req.State Value", "Got: "+data.TestRequired.Value) + if data.TestRequired.ValueString() != "test-priorstate-value" { + resp.Diagnostics.AddError("Unexpected req.State Value", "Got: "+data.TestRequired.ValueString()) } }, } @@ -465,8 +465,8 @@ func TestServerPlanResourceChange(t *testing.T) { resp.Diagnostics.Append(req.ProviderMeta.Get(ctx, &data)...) - if data.TestProviderMetaAttribute.Value != "test-provider-meta-value" { - resp.Diagnostics.AddError("Unexpected req.ProviderMeta Value", "Got: "+data.TestProviderMetaAttribute.Value) + if data.TestProviderMetaAttribute.ValueString() != "test-provider-meta-value" { + resp.Diagnostics.AddError("Unexpected req.ProviderMeta Value", "Got: "+data.TestProviderMetaAttribute.ValueString()) } }, } @@ -665,8 +665,8 @@ func TestServerPlanResourceChange(t *testing.T) { resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) - if data.TestRequired.Value != "test-new-value" { - resp.Diagnostics.AddError("Unexpected req.Config Value", "Got: "+data.TestRequired.Value) + if data.TestRequired.ValueString() != "test-new-value" { + resp.Diagnostics.AddError("Unexpected req.Config Value", "Got: "+data.TestRequired.ValueString()) } }, } @@ -719,8 +719,8 @@ func TestServerPlanResourceChange(t *testing.T) { resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) - if !data.TestComputed.Unknown { - resp.Diagnostics.AddError("Unexpected req.Plan Value", "Got: "+data.TestComputed.Value) + if !data.TestComputed.IsUnknown() { + resp.Diagnostics.AddError("Unexpected req.Plan Value", "Got: "+data.TestComputed.ValueString()) } }, } @@ -773,8 +773,8 @@ func TestServerPlanResourceChange(t *testing.T) { resp.Diagnostics.Append(req.State.Get(ctx, &data)...) - if data.TestRequired.Value != "test-old-value" { - resp.Diagnostics.AddError("Unexpected req.State Value", "Got: "+data.TestRequired.Value) + if data.TestRequired.ValueString() != "test-old-value" { + resp.Diagnostics.AddError("Unexpected req.State Value", "Got: "+data.TestRequired.ValueString()) } }, } @@ -828,8 +828,8 @@ func TestServerPlanResourceChange(t *testing.T) { resp.Diagnostics.Append(req.ProviderMeta.Get(ctx, &data)...) - if data.TestProviderMetaAttribute.Value != "test-provider-meta-value" { - resp.Diagnostics.AddError("Unexpected req.ProviderMeta Value", "Got: "+data.TestProviderMetaAttribute.Value) + if data.TestProviderMetaAttribute.ValueString() != "test-provider-meta-value" { + resp.Diagnostics.AddError("Unexpected req.ProviderMeta Value", "Got: "+data.TestProviderMetaAttribute.ValueString()) } }, } diff --git a/internal/proto6server/server_readdatasource_test.go b/internal/proto6server/server_readdatasource_test.go index 89b0cc07f..42e614dd7 100644 --- a/internal/proto6server/server_readdatasource_test.go +++ b/internal/proto6server/server_readdatasource_test.go @@ -107,8 +107,8 @@ func TestServerReadDataSource(t *testing.T) { resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) - if config.TestRequired.Value != "test-config-value" { - resp.Diagnostics.AddError("unexpected req.Config value: %s", config.TestRequired.Value) + if config.TestRequired.ValueString() != "test-config-value" { + resp.Diagnostics.AddError("unexpected req.Config value: %s", config.TestRequired.ValueString()) } }, } @@ -149,8 +149,8 @@ func TestServerReadDataSource(t *testing.T) { resp.Diagnostics.Append(req.ProviderMeta.Get(ctx, &config)...) - if config.TestRequired.Value != "test-config-value" { - resp.Diagnostics.AddError("unexpected req.ProviderMeta value: %s", config.TestRequired.Value) + if config.TestRequired.ValueString() != "test-config-value" { + resp.Diagnostics.AddError("unexpected req.ProviderMeta value: %s", config.TestRequired.ValueString()) } }, } diff --git a/internal/proto6server/server_readresource_test.go b/internal/proto6server/server_readresource_test.go index 0ff4b63b8..28110d4d8 100644 --- a/internal/proto6server/server_readresource_test.go +++ b/internal/proto6server/server_readresource_test.go @@ -112,8 +112,8 @@ func TestServerReadResource(t *testing.T) { resp.Diagnostics.Append(req.State.Get(ctx, &data)...) - if data.TestRequired.Value != "test-currentstate-value" { - resp.Diagnostics.AddError("unexpected req.State value: %s", data.TestRequired.Value) + if data.TestRequired.ValueString() != "test-currentstate-value" { + resp.Diagnostics.AddError("unexpected req.State value: %s", data.TestRequired.ValueString()) } }, } @@ -154,8 +154,8 @@ func TestServerReadResource(t *testing.T) { resp.Diagnostics.Append(req.ProviderMeta.Get(ctx, &data)...) - if data.TestRequired.Value != "test-currentstate-value" { - resp.Diagnostics.AddError("unexpected req.ProviderMeta value: %s", data.TestRequired.Value) + if data.TestRequired.ValueString() != "test-currentstate-value" { + resp.Diagnostics.AddError("unexpected req.ProviderMeta value: %s", data.TestRequired.ValueString()) } }, } diff --git a/internal/reflect/primitive_test.go b/internal/reflect/primitive_test.go index 5301ea969..46cbb91d5 100644 --- a/internal/reflect/primitive_test.go +++ b/internal/reflect/primitive_test.go @@ -160,9 +160,7 @@ func TestFromBool(t *testing.T) { val: true, typ: testtypes.BoolTypeWithValidateWarning{}, expected: testtypes.Bool{ - Bool: types.Bool{ - Value: true, - }, + Bool: types.BoolValue(true), CreatedBy: testtypes.BoolTypeWithValidateWarning{}, }, expectedDiags: diag.Diagnostics{ diff --git a/internal/testing/planmodifiers/planmodifiers.go b/internal/testing/planmodifiers/planmodifiers.go index 827df2c9a..9bc3972e4 100644 --- a/internal/testing/planmodifiers/planmodifiers.go +++ b/internal/testing/planmodifiers/planmodifiers.go @@ -49,10 +49,8 @@ func (t TestAttrPlanValueModifierOne) Modify(ctx context.Context, req tfsdk.Modi return } - if attrVal.Value == "TESTATTRONE" { - resp.AttributePlan = types.String{ - Value: "TESTATTRTWO", - } + if attrVal.ValueString() == "TESTATTRONE" { + resp.AttributePlan = types.StringValue("TESTATTRTWO") } } @@ -72,10 +70,8 @@ func (t TestAttrPlanValueModifierTwo) Modify(ctx context.Context, req tfsdk.Modi return } - if attrVal.Value == "TESTATTRTWO" { - resp.AttributePlan = types.String{ - Value: "MODIFIED_TWO", - } + if attrVal.ValueString() == "TESTATTRTWO" { + resp.AttributePlan = types.StringValue("MODIFIED_TWO") } } @@ -96,7 +92,7 @@ func (t TestAttrDefaultValueModifier) Modify(ctx context.Context, req tfsdk.Modi configVal := req.AttributeConfig.(types.String) - if configVal.Null { + if configVal.IsNull() { resp.AttributePlan = types.String{Value: "DEFAULTVALUE"} } } diff --git a/internal/testing/types/bool.go b/internal/testing/types/bool.go index 2ae8247d8..16f94ec6a 100644 --- a/internal/testing/types/bool.go +++ b/internal/testing/types/bool.go @@ -41,13 +41,13 @@ func (t BoolType) TerraformType(_ context.Context) tftypes.Type { func (t BoolType) ValueFromTerraform(ctx context.Context, in tftypes.Value) (attr.Value, error) { if in.IsNull() { return Bool{ - Bool: types.Bool{Null: true}, + Bool: types.BoolNull(), CreatedBy: t, }, nil } if !in.IsKnown() { return Bool{ - Bool: types.Bool{Unknown: true}, + Bool: types.BoolUnknown(), CreatedBy: t, }, nil } @@ -56,7 +56,7 @@ func (t BoolType) ValueFromTerraform(ctx context.Context, in tftypes.Value) (att if err != nil { return nil, err } - return Bool{Bool: types.Bool{Value: b}, CreatedBy: t}, nil + return Bool{Bool: types.BoolValue(b), CreatedBy: t}, nil } // ValueType returns the Value type. @@ -91,13 +91,5 @@ func (b Bool) IsUnknown() bool { } func (b Bool) String() string { - if b.Bool.IsUnknown() { - return attr.UnknownValueString - } - - if b.Bool.IsNull() { - return attr.NullValueString - } - - return fmt.Sprintf("%t", b.Value) + return b.Bool.String() } diff --git a/types/bool.go b/types/bool.go index 3566c1c19..3b6a13e13 100644 --- a/types/bool.go +++ b/types/bool.go @@ -12,15 +12,51 @@ var ( _ attr.Value = Bool{} ) +// BoolNull creates a Bool with a null value. Determine whether the value is +// null via the Bool type IsNull method. +// +// Setting the deprecated Bool type Null, Unknown, or Value fields after +// creating a Bool with this function has no effect. +func BoolNull() Bool { + return Bool{ + state: valueStateNull, + } +} + +// BoolUnknown creates a Bool with an unknown value. Determine whether the +// value is unknown via the Bool type IsUnknown method. +// +// Setting the deprecated Bool type Null, Unknown, or Value fields after +// creating a Bool with this function has no effect. +func BoolUnknown() Bool { + return Bool{ + state: valueStateUnknown, + } +} + +// BoolValue creates a Bool with a known value. Access the value via the Bool +// type ValueBool method. +// +// Setting the deprecated Bool type Null, Unknown, or Value fields after +// creating a Bool with this function has no effect. +func BoolValue(value bool) Bool { + return Bool{ + state: valueStateKnown, + value: value, + } +} + func boolValueFromTerraform(ctx context.Context, in tftypes.Value) (attr.Value, error) { if in.IsNull() { return Bool{ - Null: true, + Null: true, + state: valueStateDeprecated, }, nil } if !in.IsKnown() { return Bool{ Unknown: true, + state: valueStateDeprecated, }, nil } var b bool @@ -28,21 +64,50 @@ func boolValueFromTerraform(ctx context.Context, in tftypes.Value) (attr.Value, if err != nil { return nil, err } - return Bool{Value: b}, nil + return Bool{ + Value: b, + state: valueStateDeprecated, + }, nil } // Bool represents a boolean value. type Bool struct { // Unknown will be true if the value is not yet known. + // + // If the Bool was created with the BoolValue, BoolNull, or BoolUnknown + // functions, changing this field has no effect. + // + // Deprecated: Use the BoolUnknown function to create an unknown Bool + // value or use the IsUnknown method to determine whether the Bool value + // is unknown instead. Unknown bool // Null will be true if the value was not set, or was explicitly set to // null. + // + // If the Bool was created with the BoolValue, BoolNull, or BoolUnknown + // functions, changing this field has no effect. + // + // Deprecated: Use the BoolNull function to create a null Bool value or + // use the IsNull method to determine whether the Bool value is null + // instead. Null bool // Value contains the set value, as long as Unknown and Null are both // false. + // + // If the Bool was created with the BoolValue, BoolNull, or BoolUnknown + // functions, changing this field has no effect. + // + // Deprecated: Use the BoolValue function to create a known Bool value or + // use the ValueBool method to retrieve the Bool value instead. Value bool + + // state represents whether the Bool is null, unknown, or known. + state valueState + + // value contains the known value, if not null or unknown. + value bool } // Type returns a BoolType. @@ -52,16 +117,31 @@ func (b Bool) Type(_ context.Context) attr.Type { // ToTerraformValue returns the data contained in the Bool as a tftypes.Value. func (b Bool) ToTerraformValue(_ context.Context) (tftypes.Value, error) { - if b.Null { + switch b.state { + case valueStateDeprecated: + if b.Null { + return tftypes.NewValue(tftypes.Bool, nil), nil + } + if b.Unknown { + return tftypes.NewValue(tftypes.Bool, tftypes.UnknownValue), nil + } + if err := tftypes.ValidateValue(tftypes.Bool, b.Value); err != nil { + return tftypes.NewValue(tftypes.Bool, tftypes.UnknownValue), err + } + return tftypes.NewValue(tftypes.Bool, b.Value), nil + case valueStateKnown: + if err := tftypes.ValidateValue(tftypes.Bool, b.value); err != nil { + return tftypes.NewValue(tftypes.Bool, tftypes.UnknownValue), err + } + + return tftypes.NewValue(tftypes.Bool, b.value), nil + case valueStateNull: return tftypes.NewValue(tftypes.Bool, nil), nil - } - if b.Unknown { + case valueStateUnknown: return tftypes.NewValue(tftypes.Bool, tftypes.UnknownValue), nil + default: + panic(fmt.Sprintf("unhandled Bool state in ToTerraformValue: %s", b.state)) } - if err := tftypes.ValidateValue(tftypes.Bool, b.Value); err != nil { - return tftypes.NewValue(tftypes.Bool, tftypes.UnknownValue), err - } - return tftypes.NewValue(tftypes.Bool, b.Value), nil } // Equal returns true if `other` is a *Bool and has the same value as `b`. @@ -70,6 +150,12 @@ func (b Bool) Equal(other attr.Value) bool { if !ok { return false } + if b.state != o.state { + return false + } + if b.state == valueStateKnown { + return b.value == o.value + } if b.Unknown != o.Unknown { return false } @@ -81,25 +167,47 @@ func (b Bool) Equal(other attr.Value) bool { // IsNull returns true if the Bool represents a null value. func (b Bool) IsNull() bool { - return b.Null + if b.state == valueStateNull { + return true + } + + return b.state == valueStateDeprecated && b.Null } // IsUnknown returns true if the Bool represents a currently unknown value. func (b Bool) IsUnknown() bool { - return b.Unknown + if b.state == valueStateUnknown { + return true + } + + return b.state == valueStateDeprecated && b.Unknown } // String returns a human-readable representation of the Bool value. // The string returned here is not protected by any compatibility guarantees, // and is intended for logging and error reporting. func (b Bool) String() string { - if b.Unknown { + if b.IsUnknown() { return attr.UnknownValueString } - if b.Null { + if b.IsNull() { return attr.NullValueString } + if b.state == valueStateKnown { + return fmt.Sprintf("%t", b.value) + } + return fmt.Sprintf("%t", b.Value) } + +// ValueBool returns the known bool value. If Bool is null or unknown, returns +// false. +func (b Bool) ValueBool() bool { + if b.state == valueStateDeprecated { + return b.Value + } + + return b.value +} diff --git a/types/bool_test.go b/types/bool_test.go index 2b0428944..dd3132753 100644 --- a/types/bool_test.go +++ b/types/bool_test.go @@ -9,6 +9,84 @@ import ( "github.com/hashicorp/terraform-plugin-go/tftypes" ) +// This test verifies the assumptions that creating the Value via function then +// setting the fields directly has no effects. +func TestBoolValueDeprecatedFieldSetting(t *testing.T) { + t.Parallel() + + knownBool := BoolValue(false) + + knownBool.Null = true + + if knownBool.IsNull() { + t.Error("unexpected null update after Null field setting") + } + + knownBool.Unknown = true + + if knownBool.IsUnknown() { + t.Error("unexpected unknown update after Unknown field setting") + } + + knownBool.Value = true + + if knownBool.ValueBool() { + t.Error("unexpected value update after Value field setting") + } +} + +// This test verifies the assumptions that creating the Value via function then +// setting the fields directly has no effects. +func TestBoolNullDeprecatedFieldSetting(t *testing.T) { + t.Parallel() + + nullBool := BoolNull() + + nullBool.Null = false + + if !nullBool.IsNull() { + t.Error("unexpected null update after Null field setting") + } + + nullBool.Unknown = true + + if nullBool.IsUnknown() { + t.Error("unexpected unknown update after Unknown field setting") + } + + nullBool.Value = true + + if nullBool.ValueBool() { + t.Error("unexpected value update after Value field setting") + } +} + +// This test verifies the assumptions that creating the Value via function then +// setting the fields directly has no effects. +func TestBoolUnknownDeprecatedFieldSetting(t *testing.T) { + t.Parallel() + + unknownBool := BoolUnknown() + + unknownBool.Null = true + + if unknownBool.IsNull() { + t.Error("unexpected null update after Null field setting") + } + + unknownBool.Unknown = false + + if !unknownBool.IsUnknown() { + t.Error("unexpected unknown update after Unknown field setting") + } + + unknownBool.Value = true + + if unknownBool.ValueBool() { + t.Error("unexpected value update after Value field setting") + } +} + func TestBoolValueFromTerraform(t *testing.T) { t.Parallel() @@ -92,19 +170,35 @@ func TestBoolToTerraformValue(t *testing.T) { expectation interface{} } tests := map[string]testCase{ - "true": { + "known-true": { + input: BoolValue(true), + expectation: tftypes.NewValue(tftypes.Bool, true), + }, + "known-false": { + input: BoolValue(false), + expectation: tftypes.NewValue(tftypes.Bool, false), + }, + "unknown": { + input: BoolUnknown(), + expectation: tftypes.NewValue(tftypes.Bool, tftypes.UnknownValue), + }, + "null": { + input: BoolNull(), + expectation: tftypes.NewValue(tftypes.Bool, nil), + }, + "deprecated-true": { input: Bool{Value: true}, expectation: tftypes.NewValue(tftypes.Bool, true), }, - "false": { + "deprecated-false": { input: Bool{Value: false}, expectation: tftypes.NewValue(tftypes.Bool, false), }, - "unknown": { + "deprecated-unknown": { input: Bool{Unknown: true}, expectation: tftypes.NewValue(tftypes.Bool, tftypes.UnknownValue), }, - "null": { + "deprecated-null": { input: Bool{Null: true}, expectation: tftypes.NewValue(tftypes.Bool, nil), }, @@ -136,122 +230,342 @@ func TestBoolEqual(t *testing.T) { expectation bool } tests := map[string]testCase{ - "true-true": { + "known-true-nil": { + input: BoolValue(true), + candidate: nil, + expectation: false, + }, + "known-true-wrongtype": { + input: BoolValue(true), + candidate: String{Value: "true"}, + expectation: false, + }, + "known-true-known-false": { + input: BoolValue(true), + candidate: BoolValue(false), + expectation: false, + }, + "known-true-known-true": { + input: BoolValue(true), + candidate: BoolValue(true), + expectation: true, + }, + "known-true-deprecated-false": { + input: BoolValue(true), + candidate: Bool{Value: false}, + expectation: false, + }, + "known-true-deprecated-true": { + input: BoolValue(true), + candidate: Bool{Value: true}, + expectation: false, // intentional + }, + "known-true-null": { + input: BoolValue(true), + candidate: BoolNull(), + expectation: false, + }, + "known-true-deprecated-null": { + input: BoolValue(true), + candidate: Bool{Null: true}, + expectation: false, + }, + "known-true-unknown": { + input: BoolValue(true), + candidate: BoolUnknown(), + expectation: false, + }, + "known-true-deprecated-unknown": { + input: BoolValue(true), + candidate: Bool{Unknown: true}, + expectation: false, + }, + "known-false-nil": { + input: BoolValue(false), + candidate: nil, + expectation: false, + }, + "known-false-wrongtype": { + input: BoolValue(false), + candidate: String{Value: "false"}, + expectation: false, + }, + "known-false-known-false": { + input: BoolValue(false), + candidate: BoolValue(false), + expectation: true, + }, + "known-false-known-true": { + input: BoolValue(false), + candidate: BoolValue(true), + expectation: false, + }, + "known-false-deprecated-false": { + input: BoolValue(false), + candidate: Bool{Value: false}, + expectation: false, // intentional + }, + "known-false-deprecated-true": { + input: BoolValue(false), + candidate: Bool{Value: true}, + expectation: false, + }, + "known-false-null": { + input: BoolValue(false), + candidate: BoolNull(), + expectation: false, + }, + "known-false-deprecated-null": { + input: BoolValue(false), + candidate: Bool{Null: true}, + expectation: false, + }, + "known-false-unknown": { + input: BoolValue(false), + candidate: BoolUnknown(), + expectation: false, + }, + "known-false-deprecated-unknown": { + input: BoolValue(false), + candidate: Bool{Unknown: true}, + expectation: false, + }, + "null-nil": { + input: BoolNull(), + candidate: nil, + expectation: false, + }, + "null-wrongtype": { + input: BoolNull(), + candidate: String{Value: "true"}, + expectation: false, + }, + "null-known-false": { + input: BoolNull(), + candidate: BoolValue(false), + expectation: false, + }, + "null-known-true": { + input: BoolNull(), + candidate: BoolValue(true), + expectation: false, + }, + "null-deprecated-true": { + input: BoolNull(), + candidate: Bool{Value: true}, + expectation: false, + }, + "null-deprecated-false": { + input: BoolNull(), + candidate: Bool{Value: false}, + expectation: false, + }, + "null-null": { + input: BoolNull(), + candidate: BoolNull(), + expectation: true, + }, + "null-deprecated-null": { + input: BoolNull(), + candidate: Bool{Null: true}, + expectation: false, // intentional + }, + "null-unknown": { + input: BoolNull(), + candidate: BoolUnknown(), + expectation: false, + }, + "null-deprecated-unknown": { + input: BoolNull(), + candidate: Bool{Unknown: true}, + expectation: false, + }, + "deprecated-true-known-false": { + input: Bool{Value: true}, + candidate: BoolValue(false), + expectation: false, + }, + "deprecated-true-known-true": { + input: Bool{Value: true}, + candidate: BoolValue(true), + expectation: false, // intentional + }, + "deprecated-true-deprecated-true": { input: Bool{Value: true}, candidate: Bool{Value: true}, expectation: true, }, - "true-false": { + "deprecated-true-deprecated-false": { input: Bool{Value: true}, candidate: Bool{Value: false}, expectation: false, }, - "true-unknown": { + "deprecated-true-unknown": { + input: Bool{Value: true}, + candidate: BoolUnknown(), + expectation: false, + }, + "deprecated-true-deprecated-unknown": { input: Bool{Value: true}, candidate: Bool{Unknown: true}, expectation: false, }, - "true-null": { + "deprecated-true-null": { + input: Bool{Value: true}, + candidate: BoolNull(), + expectation: false, + }, + "deprecated-true-deprecated-null": { input: Bool{Value: true}, candidate: Bool{Null: true}, expectation: false, }, - "true-wrongType": { + "deprecated-true-wrongType": { input: Bool{Value: true}, candidate: &String{Value: "oops"}, expectation: false, }, - "true-nil": { + "deprecated-true-nil": { input: Bool{Value: true}, candidate: nil, expectation: false, }, - "false-true": { + "deprecated-false-known-false": { + input: Bool{Value: false}, + candidate: BoolValue(false), + expectation: false, // intentional + }, + "deprecated-false-known-true": { + input: Bool{Value: false}, + candidate: BoolValue(true), + expectation: false, + }, + "deprecated-false-deprecated-true": { input: Bool{Value: false}, candidate: Bool{Value: true}, expectation: false, }, - "false-false": { + "deprecated-false-deprecated-false": { input: Bool{Value: false}, candidate: Bool{Value: false}, expectation: true, }, - "false-unknown": { + "deprecated-false-unknown": { + input: Bool{Value: false}, + candidate: BoolUnknown(), + expectation: false, + }, + "deprecated-false-deprecated-unknown": { input: Bool{Value: false}, candidate: Bool{Unknown: true}, expectation: false, }, - "false-null": { + "deprecated-false-null": { + input: Bool{Value: false}, + candidate: BoolNull(), + expectation: false, + }, + "deprecated-false-deprecated-null": { input: Bool{Value: false}, candidate: Bool{Null: true}, expectation: false, }, - "false-wrongType": { + "deprecated-false-wrongType": { input: Bool{Value: false}, candidate: &String{Value: "oops"}, expectation: false, }, - "false-nil": { + "deprecated-false-nil": { input: Bool{Value: false}, candidate: nil, expectation: false, }, - "unknown-true": { + "deprecated-unknown-known-false": { + input: Bool{Unknown: true}, + candidate: BoolValue(false), + expectation: false, + }, + "deprecated-unknown-known-true": { + input: Bool{Unknown: true}, + candidate: BoolValue(true), + expectation: false, + }, + "deprecated-unknown-deprecated-true": { input: Bool{Unknown: true}, candidate: Bool{Value: true}, expectation: false, }, - "unknown-false": { + "deprecated-unknown-deprecated-false": { input: Bool{Unknown: true}, candidate: Bool{Value: false}, expectation: false, }, - "unknown-unknown": { + "deprecated-unknown-deprecated-unknown": { input: Bool{Unknown: true}, candidate: Bool{Unknown: true}, expectation: true, }, - "unknown-null": { + "deprecated-unknown-deprecated-null": { input: Bool{Unknown: true}, candidate: Bool{Null: true}, expectation: false, }, - "unknown-wrongType": { + "deprecated-unknown-wrongType": { input: Bool{Unknown: true}, candidate: &String{Value: "oops"}, expectation: false, }, - "unknown-nil": { + "deprecated-unknown-nil": { input: Bool{Unknown: true}, candidate: nil, expectation: false, }, - "null-true": { + "deprecated-null-deprecated-true": { input: Bool{Null: true}, candidate: Bool{Value: true}, expectation: false, }, - "null-false": { + "deprecated-null-deprecated-false": { input: Bool{Null: true}, candidate: Bool{Value: false}, expectation: false, }, - "null-unknown": { + "deprecated-null-known-false": { + input: Bool{Null: true}, + candidate: BoolValue(false), + expectation: false, + }, + "deprecated-null-known-true": { + input: Bool{Null: true}, + candidate: BoolValue(true), + expectation: false, + }, + "deprecated-null-unknown": { + input: Bool{Null: true}, + candidate: BoolUnknown(), + expectation: false, + }, + "deprecated-null-deprecated-unknown": { input: Bool{Null: true}, candidate: Bool{Unknown: true}, expectation: false, }, - "null-null": { + "deprecated-null-null": { + input: Bool{Null: true}, + candidate: BoolNull(), + expectation: false, // intentional + }, + "deprecated-null-deprecated-null": { input: Bool{Null: true}, candidate: Bool{Null: true}, expectation: true, }, - "null-wrongType": { + "deprecated-null-wrongType": { input: Bool{Null: true}, candidate: &String{Value: "oops"}, expectation: false, }, - "null-nil": { + "deprecated-null-nil": { input: Bool{Null: true}, candidate: nil, expectation: false, @@ -270,6 +584,110 @@ func TestBoolEqual(t *testing.T) { } } +func TestBoolIsNull(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + input Bool + expected bool + }{ + "known": { + input: BoolValue(true), + expected: false, + }, + "deprecated-known": { + input: Bool{Value: true}, + expected: false, + }, + "null": { + input: BoolNull(), + expected: true, + }, + "deprecated-null": { + input: Bool{Null: true}, + expected: true, + }, + "unknown": { + input: BoolUnknown(), + expected: false, + }, + "deprecated-unknown": { + input: Bool{Unknown: true}, + expected: false, + }, + "deprecated-invalid": { + input: Bool{Null: true, Unknown: true}, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.input.IsNull() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestBoolIsUnknown(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + input Bool + expected bool + }{ + "known": { + input: BoolValue(true), + expected: false, + }, + "deprecated-known": { + input: Bool{Value: true}, + expected: false, + }, + "null": { + input: BoolNull(), + expected: false, + }, + "deprecated-null": { + input: Bool{Null: true}, + expected: false, + }, + "unknown": { + input: BoolUnknown(), + expected: true, + }, + "deprecated-unknown": { + input: Bool{Unknown: true}, + expected: true, + }, + "deprecated-invalid": { + input: Bool{Null: true, Unknown: true}, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.input.IsUnknown() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestBoolString(t *testing.T) { t.Parallel() @@ -278,19 +696,35 @@ func TestBoolString(t *testing.T) { expectation string } tests := map[string]testCase{ - "true": { + "known-true": { + input: BoolValue(true), + expectation: "true", + }, + "known-false": { + input: BoolValue(false), + expectation: "false", + }, + "null": { + input: BoolNull(), + expectation: "", + }, + "unknown": { + input: BoolUnknown(), + expectation: "", + }, + "deprecated-true": { input: Bool{Value: true}, expectation: "true", }, - "false": { + "deprecated-false": { input: Bool{Value: false}, expectation: "false", }, - "unknown": { + "deprecated-unknown": { input: Bool{Unknown: true}, expectation: "", }, - "null": { + "deprecated-null": { input: Bool{Null: true}, expectation: "", }, @@ -312,3 +746,63 @@ func TestBoolString(t *testing.T) { }) } } + +func TestBoolValueBool(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + input Bool + expected bool + }{ + "known-false": { + input: BoolValue(false), + expected: false, + }, + "known-true": { + input: BoolValue(true), + expected: true, + }, + "deprecated-known-false": { + input: Bool{Value: false}, + expected: false, + }, + "deprecated-known-true": { + input: Bool{Value: true}, + expected: true, + }, + "null": { + input: BoolNull(), + expected: false, + }, + "deprecated-null": { + input: Bool{Null: true}, + expected: false, + }, + "unknown": { + input: BoolUnknown(), + expected: false, + }, + "deprecated-unknown": { + input: Bool{Unknown: true}, + expected: false, + }, + "deprecated-invalid": { + input: Bool{Null: true, Unknown: true}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.input.ValueBool() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/types/float64.go b/types/float64.go index 9db18f6e5..3f4818aa2 100644 --- a/types/float64.go +++ b/types/float64.go @@ -15,6 +15,40 @@ var ( _ attr.Value = Float64{} ) +// Float64Null creates a Float64 with a null value. Determine whether the value is +// null via the Float64 type IsNull method. +// +// Setting the deprecated Float64 type Null, Unknown, or Value fields after +// creating a Float64 with this function has no effect. +func Float64Null() Float64 { + return Float64{ + state: valueStateNull, + } +} + +// Float64Unknown creates a Float64 with an unknown value. Determine whether the +// value is unknown via the Float64 type IsUnknown method. +// +// Setting the deprecated Float64 type Null, Unknown, or Value fields after +// creating a Float64 with this function has no effect. +func Float64Unknown() Float64 { + return Float64{ + state: valueStateUnknown, + } +} + +// Float64Value creates a Float64 with a known value. Access the value via the Float64 +// type ValueFloat64 method. +// +// Setting the deprecated Float64 type Null, Unknown, or Value fields after +// creating a Float64 with this function has no effect. +func Float64Value(value float64) Float64 { + return Float64{ + state: valueStateKnown, + value: value, + } +} + func float64Validate(_ context.Context, in tftypes.Value, path path.Path) diag.Diagnostics { var diags diag.Diagnostics @@ -65,11 +99,17 @@ func float64Validate(_ context.Context, in tftypes.Value, path path.Path) diag.D func float64ValueFromTerraform(ctx context.Context, in tftypes.Value) (attr.Value, error) { if !in.IsKnown() { - return Float64{Unknown: true}, nil + return Float64{ + Unknown: true, + state: valueStateDeprecated, + }, nil } if in.IsNull() { - return Float64{Null: true}, nil + return Float64{ + Null: true, + state: valueStateDeprecated, + }, nil } var bigF *big.Float @@ -85,21 +125,50 @@ func float64ValueFromTerraform(ctx context.Context, in tftypes.Value) (attr.Valu return nil, fmt.Errorf("Value %s cannot be represented as a 64-bit floating point.", bigF) } - return Float64{Value: f}, nil + return Float64{ + Value: f, + state: valueStateDeprecated, + }, nil } // Float64 represents a 64-bit floating point value, exposed as a float64. type Float64 struct { // Unknown will be true if the value is not yet known. + // + // If the Float64 was created with the Float64Value, Float64Null, or Float64Unknown + // functions, changing this field has no effect. + // + // Deprecated: Use the Float64Unknown function to create an unknown Float64 + // value or use the IsUnknown method to determine whether the Float64 value + // is unknown instead. Unknown bool // Null will be true if the value was not set, or was explicitly set to // null. + // + // If the Float64 was created with the Float64Value, Float64Null, or Float64Unknown + // functions, changing this field has no effect. + // + // Deprecated: Use the Float64Null function to create a null Float64 value or + // use the IsNull method to determine whether the Float64 value is null + // instead. Null bool // Value contains the set value, as long as Unknown and Null are both // false. + // + // If the Float64 was created with the Float64Value, Float64Null, or Float64Unknown + // functions, changing this field has no effect. + // + // Deprecated: Use the Float64Value function to create a known Float64 value or + // use the ValueFloat64 method to retrieve the Float64 value instead. Value float64 + + // state represents whether the Float64 is null, unknown, or known. + state valueState + + // value contains the known value, if not null or unknown. + value float64 } // Equal returns true if `other` is a Float64 and has the same value as `f`. @@ -110,6 +179,14 @@ func (f Float64) Equal(other attr.Value) bool { return false } + if f.state != o.state { + return false + } + + if f.state == valueStateKnown { + return f.value == o.value + } + if f.Unknown != o.Unknown { return false } @@ -123,19 +200,31 @@ func (f Float64) Equal(other attr.Value) bool { // ToTerraformValue returns the data contained in the Float64 as a tftypes.Value. func (f Float64) ToTerraformValue(ctx context.Context) (tftypes.Value, error) { - if f.Null { + switch f.state { + case valueStateDeprecated: + if f.Null { + return tftypes.NewValue(tftypes.Number, nil), nil + } + if f.Unknown { + return tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), nil + } + if err := tftypes.ValidateValue(tftypes.Number, f.Value); err != nil { + return tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), err + } + return tftypes.NewValue(tftypes.Number, f.Value), nil + case valueStateKnown: + if err := tftypes.ValidateValue(tftypes.Number, f.value); err != nil { + return tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), err + } + + return tftypes.NewValue(tftypes.Number, f.value), nil + case valueStateNull: return tftypes.NewValue(tftypes.Number, nil), nil - } - - if f.Unknown { + case valueStateUnknown: return tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), nil + default: + panic(fmt.Sprintf("unhandled Float64 state in ToTerraformValue: %s", f.state)) } - - bf := big.NewFloat(f.Value) - if err := tftypes.ValidateValue(tftypes.Number, bf); err != nil { - return tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), err - } - return tftypes.NewValue(tftypes.Number, bf), nil } // Type returns a Float64Type. @@ -145,25 +234,47 @@ func (f Float64) Type(ctx context.Context) attr.Type { // IsNull returns true if the Float64 represents a null value. func (f Float64) IsNull() bool { - return f.Null + if f.state == valueStateNull { + return true + } + + return f.state == valueStateDeprecated && f.Null } // IsUnknown returns true if the Float64 represents a currently unknown value. func (f Float64) IsUnknown() bool { - return f.Unknown + if f.state == valueStateUnknown { + return true + } + + return f.state == valueStateDeprecated && f.Unknown } // String returns a human-readable representation of the Float64 value. // The string returned here is not protected by any compatibility guarantees, // and is intended for logging and error reporting. func (f Float64) String() string { - if f.Unknown { + if f.IsUnknown() { return attr.UnknownValueString } - if f.Null { + if f.IsNull() { return attr.NullValueString } + if f.state == valueStateKnown { + return fmt.Sprintf("%f", f.value) + } + return fmt.Sprintf("%f", f.Value) } + +// ValueFloat64 returns the known float64 value. If Float64 is null or unknown, returns +// 0.0. +func (f Float64) ValueFloat64() float64 { + if f.state == valueStateDeprecated { + return f.Value + } + + return f.value +} diff --git a/types/float64_test.go b/types/float64_test.go index e0b98783d..6a46edd39 100644 --- a/types/float64_test.go +++ b/types/float64_test.go @@ -11,6 +11,84 @@ import ( "github.com/hashicorp/terraform-plugin-go/tftypes" ) +// This test verifies the assumptions that creating the Value via function then +// setting the fields directly has no effects. +func TestFloat64ValueDeprecatedFieldSetting(t *testing.T) { + t.Parallel() + + knownFloat64 := Float64Value(2.4) + + knownFloat64.Null = true + + if knownFloat64.IsNull() { + t.Error("unexpected null update after Null field setting") + } + + knownFloat64.Unknown = true + + if knownFloat64.IsUnknown() { + t.Error("unexpected unknown update after Unknown field setting") + } + + knownFloat64.Value = 4.8 + + if knownFloat64.ValueFloat64() == 4.8 { + t.Error("unexpected value update after Value field setting") + } +} + +// This test verifies the assumptions that creating the Value via function then +// setting the fields directly has no effects. +func TestFloat64NullDeprecatedFieldSetting(t *testing.T) { + t.Parallel() + + nullFloat64 := Float64Null() + + nullFloat64.Null = false + + if !nullFloat64.IsNull() { + t.Error("unexpected null update after Null field setting") + } + + nullFloat64.Unknown = true + + if nullFloat64.IsUnknown() { + t.Error("unexpected unknown update after Unknown field setting") + } + + nullFloat64.Value = 4.8 + + if nullFloat64.ValueFloat64() == 4.8 { + t.Error("unexpected value update after Value field setting") + } +} + +// This test verifies the assumptions that creating the Value via function then +// setting the fields directly has no effects. +func TestFloat64UnknownDeprecatedFieldSetting(t *testing.T) { + t.Parallel() + + unknownFloat64 := Float64Unknown() + + unknownFloat64.Null = true + + if unknownFloat64.IsNull() { + t.Error("unexpected null update after Null field setting") + } + + unknownFloat64.Unknown = false + + if !unknownFloat64.IsUnknown() { + t.Error("unexpected unknown update after Unknown field setting") + } + + unknownFloat64.Value = 4.8 + + if unknownFloat64.ValueFloat64() == 4.8 { + t.Error("unexpected value update after Value field setting") + } +} + func TestFloat64ValueFromTerraform(t *testing.T) { t.Parallel() @@ -94,19 +172,35 @@ func TestFloat64ToTerraformValue(t *testing.T) { expectation interface{} } tests := map[string]testCase{ - "value-int": { + "known-int": { + input: Float64Value(123), + expectation: tftypes.NewValue(tftypes.Number, big.NewFloat(123.0)), + }, + "known-float": { + input: Float64Value(123.456), + expectation: tftypes.NewValue(tftypes.Number, big.NewFloat(123.456)), + }, + "unknown": { + input: Float64Unknown(), + expectation: tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), + }, + "null": { + input: Float64Null(), + expectation: tftypes.NewValue(tftypes.Number, nil), + }, + "deprecated-value-int": { input: Float64{Value: 123}, expectation: tftypes.NewValue(tftypes.Number, big.NewFloat(123.0)), }, - "value-float": { + "deprecated-value-float": { input: Float64{Value: 123.456}, expectation: tftypes.NewValue(tftypes.Number, big.NewFloat(123.456)), }, - "unknown": { + "deprecated-unknown": { input: Float64{Unknown: true}, expectation: tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), }, - "null": { + "deprecated-null": { input: Float64{Null: true}, expectation: tftypes.NewValue(tftypes.Number, nil), }, @@ -138,82 +232,182 @@ func TestFloat64Equal(t *testing.T) { expectation bool } tests := map[string]testCase{ - "value-value-same": { + "known-known-same": { + input: Float64Value(123), + candidate: Float64Value(123), + expectation: true, + }, + "known-known-diff": { + input: Float64Value(123), + candidate: Float64Value(456), + expectation: false, + }, + "known-unknown": { + input: Float64Value(123), + candidate: Float64Unknown(), + expectation: false, + }, + "known-null": { + input: Float64Value(123), + candidate: Float64Null(), + expectation: false, + }, + "unknown-value": { + input: Float64Unknown(), + candidate: Float64Value(123), + expectation: false, + }, + "unknown-unknown": { + input: Float64Unknown(), + candidate: Float64Unknown(), + expectation: true, + }, + "unknown-null": { + input: Float64Unknown(), + candidate: Float64Null(), + expectation: false, + }, + "null-known": { + input: Float64Null(), + candidate: Float64Value(123), + expectation: false, + }, + "null-unknown": { + input: Float64Null(), + candidate: Float64Unknown(), + expectation: false, + }, + "null-null": { + input: Float64Null(), + candidate: Float64Null(), + expectation: true, + }, + "deprecated-known-known-same": { + input: Float64{Value: 123}, + candidate: Float64Value(123), + expectation: false, // intentional + }, + "deprecated-known-known-diff": { + input: Float64{Value: 123}, + candidate: Float64Value(456), + expectation: false, + }, + "deprecated-known-unknown": { + input: Float64{Value: 123}, + candidate: Float64Unknown(), + expectation: false, + }, + "deprecated-known-null": { + input: Float64{Value: 123}, + candidate: Float64Null(), + expectation: false, + }, + "deprecated-known-deprecated-known-same": { input: Float64{Value: 123}, candidate: Float64{Value: 123}, expectation: true, }, - "value-value-diff": { + "deprecated-known-deprecated-known-diff": { input: Float64{Value: 123}, candidate: Float64{Value: 456}, expectation: false, }, - "value-unknown": { + "deprecated-known-deprecated-unknown": { input: Float64{Value: 123}, candidate: Float64{Unknown: true}, expectation: false, }, - "value-null": { + "deprecated-known-deprecated-null": { input: Float64{Value: 123}, candidate: Float64{Null: true}, expectation: false, }, - "value-wrongType": { + "deprecated-known-wrongType": { input: Float64{Value: 123}, candidate: &String{Value: "oops"}, expectation: false, }, - "value-nil": { + "deprecated-known-nil": { input: Float64{Value: 123}, candidate: nil, expectation: false, }, - "unknown-value": { + "deprecated-unknown-value": { + input: Float64{Unknown: true}, + candidate: Float64Value(123), + expectation: false, + }, + "deprecated-unknown-unknown": { + input: Float64{Unknown: true}, + candidate: Float64Unknown(), + expectation: false, // intentional + }, + "deprecated-unknown-null": { + input: Float64{Unknown: true}, + candidate: Float64Null(), + expectation: false, + }, + "deprecated-unknown-deprecated-value": { input: Float64{Unknown: true}, candidate: Float64{Value: 123}, expectation: false, }, - "unknown-unknown": { + "deprecated-unknown-deprecated-unknown": { input: Float64{Unknown: true}, candidate: Float64{Unknown: true}, expectation: true, }, - "unknown-null": { + "deprecated-unknown-deprecated-null": { input: Float64{Unknown: true}, candidate: Float64{Null: true}, expectation: false, }, - "unknown-wrongType": { + "deprecated-unknown-wrongType": { input: Float64{Unknown: true}, candidate: &String{Value: "oops"}, expectation: false, }, - "unknown-nil": { + "deprecated-unknown-nil": { input: Float64{Unknown: true}, candidate: nil, expectation: false, }, - "null-value": { + "deprecated-null-known": { + input: Float64{Null: true}, + candidate: Float64Value(123), + expectation: false, + }, + "deprecated-null-unknown": { + input: Float64{Null: true}, + candidate: Float64Unknown(), + expectation: false, + }, + "deprecated-null-null": { + input: Float64{Null: true}, + candidate: Float64Null(), + expectation: false, // intentional + }, + "deprecated-null-deprecated-known": { input: Float64{Null: true}, candidate: Float64{Value: 123}, expectation: false, }, - "null-unknown": { + "deprecated-null-deprecated-unknown": { input: Float64{Null: true}, candidate: Float64{Unknown: true}, expectation: false, }, - "null-null": { + "deprecated-null-deprecated-null": { input: Float64{Null: true}, candidate: Float64{Null: true}, expectation: true, }, - "null-wrongType": { + "deprecated-null-wrongType": { input: Float64{Null: true}, candidate: &String{Value: "oops"}, expectation: false, }, - "null-nil": { + "deprecated-null-nil": { input: Float64{Null: true}, candidate: nil, expectation: false, @@ -232,6 +426,110 @@ func TestFloat64Equal(t *testing.T) { } } +func TestFloat64IsNull(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + input Float64 + expected bool + }{ + "known": { + input: Float64Value(2.4), + expected: false, + }, + "deprecated-known": { + input: Float64{Value: 2.4}, + expected: false, + }, + "null": { + input: Float64Null(), + expected: true, + }, + "deprecated-null": { + input: Float64{Null: true}, + expected: true, + }, + "unknown": { + input: Float64Unknown(), + expected: false, + }, + "deprecated-unknown": { + input: Float64{Unknown: true}, + expected: false, + }, + "deprecated-invalid": { + input: Float64{Null: true, Unknown: true}, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.input.IsNull() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat64IsUnknown(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + input Float64 + expected bool + }{ + "known": { + input: Float64Value(2.4), + expected: false, + }, + "deprecated-known": { + input: Float64{Value: 2.4}, + expected: false, + }, + "null": { + input: Float64Null(), + expected: false, + }, + "deprecated-null": { + input: Float64{Null: true}, + expected: false, + }, + "unknown": { + input: Float64Unknown(), + expected: true, + }, + "deprecated-unknown": { + input: Float64{Unknown: true}, + expected: true, + }, + "deprecated-invalid": { + input: Float64{Null: true, Unknown: true}, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.input.IsUnknown() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestFloat64String(t *testing.T) { t.Parallel() @@ -241,34 +539,66 @@ func TestFloat64String(t *testing.T) { } tests := map[string]testCase{ "less-than-one": { - input: Float64{Value: 0.12340984302980000}, + input: Float64Value(0.12340984302980000), expectation: "0.123410", }, "more-than-one": { - input: Float64{Value: 92387938173219.327663}, + input: Float64Value(92387938173219.327663), expectation: "92387938173219.328125", }, "negative-more-than-one": { - input: Float64{Value: -0.12340984302980000}, + input: Float64Value(-0.12340984302980000), expectation: "-0.123410", }, "negative-less-than-one": { - input: Float64{Value: -92387938173219.327663}, + input: Float64Value(-92387938173219.327663), expectation: "-92387938173219.328125", }, "min-float64": { - input: Float64{Value: math.SmallestNonzeroFloat64}, + input: Float64Value(math.SmallestNonzeroFloat64), expectation: "0.000000", }, "max-float64": { - input: Float64{Value: math.MaxFloat64}, + input: Float64Value(math.MaxFloat64), expectation: "179769313486231570814527423731704356798070567525844996598917476803157260780028538760589558632766878171540458953514382464234321326889464182768467546703537516986049910576551282076245490090389328944075868508455133942304583236903222948165808559332123348274797826204144723168738177180919299881250404026184124858368.000000", }, "unknown": { - input: Float64{Unknown: true}, + input: Float64Unknown(), expectation: "", }, "null": { + input: Float64Null(), + expectation: "", + }, + "deprecated-known-less-than-one": { + input: Float64{Value: 0.12340984302980000}, + expectation: "0.123410", + }, + "deprecated-known-more-than-one": { + input: Float64{Value: 92387938173219.327663}, + expectation: "92387938173219.328125", + }, + "deprecated-known-negative-more-than-one": { + input: Float64{Value: -0.12340984302980000}, + expectation: "-0.123410", + }, + "deprecated-known-negative-less-than-one": { + input: Float64{Value: -92387938173219.327663}, + expectation: "-92387938173219.328125", + }, + "deprecated-known-min-float64": { + input: Float64{Value: math.SmallestNonzeroFloat64}, + expectation: "0.000000", + }, + "deprecated-known-max-float64": { + input: Float64{Value: math.MaxFloat64}, + expectation: "179769313486231570814527423731704356798070567525844996598917476803157260780028538760589558632766878171540458953514382464234321326889464182768467546703537516986049910576551282076245490090389328944075868508455133942304583236903222948165808559332123348274797826204144723168738177180919299881250404026184124858368.000000", + }, + "deprecated-known-unknown": { + input: Float64{Unknown: true}, + expectation: "", + }, + "deprecated-known-null": { input: Float64{Null: true}, expectation: "", }, @@ -290,3 +620,55 @@ func TestFloat64String(t *testing.T) { }) } } + +func TestFloat64ValueFloat64(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + input Float64 + expected float64 + }{ + "known": { + input: Float64Value(2.4), + expected: 2.4, + }, + "deprecated-known": { + input: Float64{Value: 2.4}, + expected: 2.4, + }, + "null": { + input: Float64Null(), + expected: 0.0, + }, + "deprecated-null": { + input: Float64{Null: true}, + expected: 0.0, + }, + "unknown": { + input: Float64Unknown(), + expected: 0.0, + }, + "deprecated-unknown": { + input: Float64{Unknown: true}, + expected: 0.0, + }, + "deprecated-invalid": { + input: Float64{Null: true, Unknown: true}, + expected: 0.0, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.input.ValueFloat64() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/types/int64.go b/types/int64.go index 4e98e5491..8f6643071 100644 --- a/types/int64.go +++ b/types/int64.go @@ -15,6 +15,40 @@ var ( _ attr.Value = Int64{} ) +// Int64Null creates a Int64 with a null value. Determine whether the value is +// null via the Int64 type IsNull method. +// +// Setting the deprecated Int64 type Null, Unknown, or Value fields after +// creating a Int64 with this function has no effect. +func Int64Null() Int64 { + return Int64{ + state: valueStateNull, + } +} + +// Int64Unknown creates a Int64 with an unknown value. Determine whether the +// value is unknown via the Int64 type IsUnknown method. +// +// Setting the deprecated Int64 type Null, Unknown, or Value fields after +// creating a Int64 with this function has no effect. +func Int64Unknown() Int64 { + return Int64{ + state: valueStateUnknown, + } +} + +// Int64Value creates a Int64 with a known value. Access the value via the Int64 +// type ValueInt64 method. +// +// Setting the deprecated Int64 type Null, Unknown, or Value fields after +// creating a Int64 with this function has no effect. +func Int64Value(value int64) Int64 { + return Int64{ + state: valueStateKnown, + value: value, + } +} + func int64Validate(_ context.Context, in tftypes.Value, path path.Path) diag.Diagnostics { var diags diag.Diagnostics @@ -74,11 +108,17 @@ func int64Validate(_ context.Context, in tftypes.Value, path path.Path) diag.Dia func int64ValueFromTerraform(ctx context.Context, in tftypes.Value) (attr.Value, error) { if !in.IsKnown() { - return Int64{Unknown: true}, nil + return Int64{ + Unknown: true, + state: valueStateDeprecated, + }, nil } if in.IsNull() { - return Int64{Null: true}, nil + return Int64{ + Null: true, + state: valueStateDeprecated, + }, nil } var bigF *big.Float @@ -98,21 +138,50 @@ func int64ValueFromTerraform(ctx context.Context, in tftypes.Value) (attr.Value, return nil, fmt.Errorf("Value %s cannot be represented as a 64-bit integer.", bigF) } - return Int64{Value: i}, nil + return Int64{ + Value: i, + state: valueStateDeprecated, + }, nil } // Int64 represents a 64-bit integer value, exposed as an int64. type Int64 struct { // Unknown will be true if the value is not yet known. + // + // If the Int64 was created with the Int64Value, Int64Null, or Int64Unknown + // functions, changing this field has no effect. + // + // Deprecated: Use the Int64Unknown function to create an unknown Int64 + // value or use the IsUnknown method to determine whether the Int64 value + // is unknown instead. Unknown bool // Null will be true if the value was not set, or was explicitly set to // null. + // + // If the Int64 was created with the Int64Value, Int64Null, or Int64Unknown + // functions, changing this field has no effect. + // + // Deprecated: Use the Int64Null function to create a null Int64 value or + // use the IsNull method to determine whether the Int64 value is null + // instead. Null bool // Value contains the set value, as long as Unknown and Null are both // false. + // + // If the Int64 was created with the Int64Value, Int64Null, or Int64Unknown + // functions, changing this field has no effect. + // + // Deprecated: Use the Int64Value function to create a known Int64 value or + // use the ValueInt64 method to retrieve the Int64 value instead. Value int64 + + // state represents whether the Int64 is null, unknown, or known. + state valueState + + // value contains the known value, if not null or unknown. + value int64 } // Equal returns true if `other` is an Int64 and has the same value as `i`. @@ -123,6 +192,14 @@ func (i Int64) Equal(other attr.Value) bool { return false } + if i.state != o.state { + return false + } + + if i.state == valueStateKnown { + return i.value == o.value + } + if i.Unknown != o.Unknown { return false } @@ -136,19 +213,34 @@ func (i Int64) Equal(other attr.Value) bool { // ToTerraformValue returns the data contained in the Int64 as a tftypes.Value. func (i Int64) ToTerraformValue(ctx context.Context) (tftypes.Value, error) { - if i.Null { + switch i.state { + case valueStateDeprecated: + if i.Null { + return tftypes.NewValue(tftypes.Number, nil), nil + } + + if i.Unknown { + return tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), nil + } + + bf := new(big.Float).SetInt64(i.Value) + if err := tftypes.ValidateValue(tftypes.Number, bf); err != nil { + return tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), err + } + return tftypes.NewValue(tftypes.Number, bf), nil + case valueStateKnown: + if err := tftypes.ValidateValue(tftypes.Number, i.value); err != nil { + return tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), err + } + + return tftypes.NewValue(tftypes.Number, i.value), nil + case valueStateNull: return tftypes.NewValue(tftypes.Number, nil), nil - } - - if i.Unknown { + case valueStateUnknown: return tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), nil + default: + panic(fmt.Sprintf("unhandled Int64 state in ToTerraformValue: %s", i.state)) } - - bf := new(big.Float).SetInt64(i.Value) - if err := tftypes.ValidateValue(tftypes.Number, bf); err != nil { - return tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), err - } - return tftypes.NewValue(tftypes.Number, bf), nil } // Type returns a Int64Type. @@ -158,25 +250,47 @@ func (i Int64) Type(ctx context.Context) attr.Type { // IsNull returns true if the Int64 represents a null value. func (i Int64) IsNull() bool { - return i.Null + if i.state == valueStateNull { + return true + } + + return i.state == valueStateDeprecated && i.Null } // IsUnknown returns true if the Int64 represents a currently unknown value. func (i Int64) IsUnknown() bool { - return i.Unknown + if i.state == valueStateUnknown { + return true + } + + return i.state == valueStateDeprecated && i.Unknown } // String returns a human-readable representation of the Int64 value. // The string returned here is not protected by any compatibility guarantees, // and is intended for logging and error reporting. func (i Int64) String() string { - if i.Unknown { + if i.IsUnknown() { return attr.UnknownValueString } - if i.Null { + if i.IsNull() { return attr.NullValueString } + if i.state == valueStateKnown { + return fmt.Sprintf("%d", i.value) + } + return fmt.Sprintf("%d", i.Value) } + +// ValueInt64 returns the known float64 value. If Int64 is null or unknown, returns +// 0.0. +func (i Int64) ValueInt64() int64 { + if i.state == valueStateDeprecated { + return i.Value + } + + return i.value +} diff --git a/types/int64_test.go b/types/int64_test.go index 3e4452169..9fb4c544b 100644 --- a/types/int64_test.go +++ b/types/int64_test.go @@ -11,6 +11,84 @@ import ( "github.com/hashicorp/terraform-plugin-go/tftypes" ) +// This test verifies the assumptions that creating the Value via function then +// setting the fields directly has no effects. +func TestInt64ValueDeprecatedFieldSetting(t *testing.T) { + t.Parallel() + + knownInt64 := Int64Value(24) + + knownInt64.Null = true + + if knownInt64.IsNull() { + t.Error("unexpected null update after Null field setting") + } + + knownInt64.Unknown = true + + if knownInt64.IsUnknown() { + t.Error("unexpected unknown update after Unknown field setting") + } + + knownInt64.Value = 48 + + if knownInt64.ValueInt64() == 48 { + t.Error("unexpected value update after Value field setting") + } +} + +// This test verifies the assumptions that creating the Value via function then +// setting the fields directly has no effects. +func TestInt64NullDeprecatedFieldSetting(t *testing.T) { + t.Parallel() + + nullInt64 := Int64Null() + + nullInt64.Null = false + + if !nullInt64.IsNull() { + t.Error("unexpected null update after Null field setting") + } + + nullInt64.Unknown = true + + if nullInt64.IsUnknown() { + t.Error("unexpected unknown update after Unknown field setting") + } + + nullInt64.Value = 48 + + if nullInt64.ValueInt64() == 48 { + t.Error("unexpected value update after Value field setting") + } +} + +// This test verifies the assumptions that creating the Value via function then +// setting the fields directly has no effects. +func TestInt64UnknownDeprecatedFieldSetting(t *testing.T) { + t.Parallel() + + unknownInt64 := Int64Unknown() + + unknownInt64.Null = true + + if unknownInt64.IsNull() { + t.Error("unexpected null update after Null field setting") + } + + unknownInt64.Unknown = false + + if !unknownInt64.IsUnknown() { + t.Error("unexpected unknown update after Unknown field setting") + } + + unknownInt64.Value = 48 + + if unknownInt64.ValueInt64() == 48 { + t.Error("unexpected value update after Value field setting") + } +} + func TestInt64ValueFromTerraform(t *testing.T) { t.Parallel() @@ -90,15 +168,27 @@ func TestInt64ToTerraformValue(t *testing.T) { expectation interface{} } tests := map[string]testCase{ - "value": { - input: Int64{Value: 123}, + "known": { + input: Int64Value(123), expectation: tftypes.NewValue(tftypes.Number, big.NewFloat(123)), }, "unknown": { - input: Int64{Unknown: true}, + input: Int64Unknown(), expectation: tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), }, "null": { + input: Int64Null(), + expectation: tftypes.NewValue(tftypes.Number, nil), + }, + "deprecated-known": { + input: Int64{Value: 123}, + expectation: tftypes.NewValue(tftypes.Number, big.NewFloat(123)), + }, + "deprecated-unknown": { + input: Int64{Unknown: true}, + expectation: tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), + }, + "deprecated-null": { input: Int64{Null: true}, expectation: tftypes.NewValue(tftypes.Number, nil), }, @@ -130,82 +220,182 @@ func TestInt64Equal(t *testing.T) { expectation bool } tests := map[string]testCase{ - "value-value-same": { + "known-known-same": { + input: Int64Value(123), + candidate: Int64Value(123), + expectation: true, + }, + "known-known-diff": { + input: Int64Value(123), + candidate: Int64Value(456), + expectation: false, + }, + "known-unknown": { + input: Int64Value(123), + candidate: Int64Unknown(), + expectation: false, + }, + "known-null": { + input: Int64Value(123), + candidate: Int64Null(), + expectation: false, + }, + "unknown-value": { + input: Int64Unknown(), + candidate: Int64Value(123), + expectation: false, + }, + "unknown-unknown": { + input: Int64Unknown(), + candidate: Int64Unknown(), + expectation: true, + }, + "unknown-null": { + input: Int64Unknown(), + candidate: Int64Null(), + expectation: false, + }, + "null-known": { + input: Int64Null(), + candidate: Int64Value(123), + expectation: false, + }, + "null-unknown": { + input: Int64Null(), + candidate: Int64Unknown(), + expectation: false, + }, + "null-null": { + input: Int64Null(), + candidate: Int64Null(), + expectation: true, + }, + "deprecated-known-known-same": { + input: Int64{Value: 123}, + candidate: Int64Value(123), + expectation: false, // intentional + }, + "deprecated-known-known-diff": { + input: Int64{Value: 123}, + candidate: Int64Value(456), + expectation: false, + }, + "deprecated-known-unknown": { + input: Int64{Value: 123}, + candidate: Int64Unknown(), + expectation: false, + }, + "deprecated-known-null": { + input: Int64{Value: 123}, + candidate: Int64Null(), + expectation: false, + }, + "deprecated-known-deprecated-known-same": { input: Int64{Value: 123}, candidate: Int64{Value: 123}, expectation: true, }, - "value-value-diff": { + "deprecated-known-deprecated-known-diff": { input: Int64{Value: 123}, candidate: Int64{Value: 456}, expectation: false, }, - "value-unknown": { + "deprecated-known-deprecated-unknown": { input: Int64{Value: 123}, candidate: Int64{Unknown: true}, expectation: false, }, - "value-null": { + "deprecated-known-deprecated-null": { input: Int64{Value: 123}, candidate: Int64{Null: true}, expectation: false, }, - "value-wrongType": { + "deprecated-known-wrongType": { input: Int64{Value: 123}, candidate: &String{Value: "oops"}, expectation: false, }, - "value-nil": { + "deprecated-known-nil": { input: Int64{Value: 123}, candidate: nil, expectation: false, }, - "unknown-value": { + "deprecated-unknown-value": { + input: Int64{Unknown: true}, + candidate: Int64Value(123), + expectation: false, + }, + "deprecated-unknown-unknown": { + input: Int64{Unknown: true}, + candidate: Int64Unknown(), + expectation: false, // intentional + }, + "deprecated-unknown-null": { + input: Int64{Unknown: true}, + candidate: Int64Null(), + expectation: false, + }, + "deprecated-unknown-deprecated-known": { input: Int64{Unknown: true}, candidate: Int64{Value: 123}, expectation: false, }, - "unknown-unknown": { + "deprecated-unknown-deprecated-unknown": { input: Int64{Unknown: true}, candidate: Int64{Unknown: true}, expectation: true, }, - "unknown-null": { + "deprecated-unknown-deprecated-null": { input: Int64{Unknown: true}, candidate: Int64{Null: true}, expectation: false, }, - "unknown-wrongType": { + "deprecated-unknown-wrongType": { input: Int64{Unknown: true}, candidate: &String{Value: "oops"}, expectation: false, }, - "unknown-nil": { + "deprecated-unknown-nil": { input: Int64{Unknown: true}, candidate: nil, expectation: false, }, - "null-value": { + "deprecated-null-known": { + input: Int64{Null: true}, + candidate: Int64Value(123), + expectation: false, + }, + "deprecated-null-unknown": { + input: Int64{Null: true}, + candidate: Int64Unknown(), + expectation: false, + }, + "deprecated-null-null": { + input: Int64{Null: true}, + candidate: Int64Null(), + expectation: false, // intentional + }, + "deprecated-null-deprecated-known": { input: Int64{Null: true}, candidate: Int64{Value: 123}, expectation: false, }, - "null-unknown": { + "deprecated-null-deprecated-unknown": { input: Int64{Null: true}, candidate: Int64{Unknown: true}, expectation: false, }, - "null-null": { + "deprecated-null-deprecated-null": { input: Int64{Null: true}, candidate: Int64{Null: true}, expectation: true, }, - "null-wrongType": { + "deprecated-null-wrongType": { input: Int64{Null: true}, candidate: &String{Value: "oops"}, expectation: false, }, - "null-nil": { + "deprecated-null-nil": { input: Int64{Null: true}, candidate: nil, expectation: false, @@ -224,6 +414,110 @@ func TestInt64Equal(t *testing.T) { } } +func TestInt64IsNull(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + input Int64 + expected bool + }{ + "known": { + input: Int64Value(24), + expected: false, + }, + "deprecated-known": { + input: Int64{Value: 24}, + expected: false, + }, + "null": { + input: Int64Null(), + expected: true, + }, + "deprecated-null": { + input: Int64{Null: true}, + expected: true, + }, + "unknown": { + input: Int64Unknown(), + expected: false, + }, + "deprecated-unknown": { + input: Int64{Unknown: true}, + expected: false, + }, + "deprecated-invalid": { + input: Int64{Null: true, Unknown: true}, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.input.IsNull() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt64IsUnknown(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + input Int64 + expected bool + }{ + "known": { + input: Int64Value(24), + expected: false, + }, + "deprecated-known": { + input: Int64{Value: 24}, + expected: false, + }, + "null": { + input: Int64Null(), + expected: false, + }, + "deprecated-null": { + input: Int64{Null: true}, + expected: false, + }, + "unknown": { + input: Int64Unknown(), + expected: true, + }, + "deprecated-unknown": { + input: Int64{Unknown: true}, + expected: true, + }, + "deprecated-invalid": { + input: Int64{Null: true, Unknown: true}, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.input.IsUnknown() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestInt64String(t *testing.T) { t.Parallel() @@ -232,27 +526,51 @@ func TestInt64String(t *testing.T) { expectation string } tests := map[string]testCase{ - "less-than-one": { + "known-less-than-one": { + input: Int64Value(-12340984302980000), + expectation: "-12340984302980000", + }, + "known-more-than-one": { + input: Int64Value(92387938173219327), + expectation: "92387938173219327", + }, + "known-min-int64": { + input: Int64Value(math.MinInt64), + expectation: "-9223372036854775808", + }, + "known-max-int64": { + input: Int64Value(math.MaxInt64), + expectation: "9223372036854775807", + }, + "unknown": { + input: Int64Unknown(), + expectation: "", + }, + "null": { + input: Int64Null(), + expectation: "", + }, + "deprecated-known-less-than-one": { input: Int64{Value: -12340984302980000}, expectation: "-12340984302980000", }, - "more-than-one": { + "deprecated-known-more-than-one": { input: Int64{Value: 92387938173219327}, expectation: "92387938173219327", }, - "min-int64": { + "deprecated-known-min-int64": { input: Int64{Value: math.MinInt64}, expectation: "-9223372036854775808", }, - "max-int64": { + "deprecated-known-max-int64": { input: Int64{Value: math.MaxInt64}, expectation: "9223372036854775807", }, - "unknown": { + "deprecated-unknown": { input: Int64{Unknown: true}, expectation: "", }, - "null": { + "deprecated-null": { input: Int64{Null: true}, expectation: "", }, @@ -274,3 +592,55 @@ func TestInt64String(t *testing.T) { }) } } + +func TestInt64ValueInt64(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + input Int64 + expected int64 + }{ + "known": { + input: Int64Value(24), + expected: 24, + }, + "deprecated-known": { + input: Int64{Value: 24}, + expected: 24, + }, + "null": { + input: Int64Null(), + expected: 0, + }, + "deprecated-null": { + input: Int64{Null: true}, + expected: 0, + }, + "unknown": { + input: Int64Unknown(), + expected: 0, + }, + "deprecated-unknown": { + input: Int64{Unknown: true}, + expected: 0, + }, + "deprecated-invalid": { + input: Int64{Null: true, Unknown: true}, + expected: 0, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.input.ValueInt64() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/types/list.go b/types/list.go index bda4569d1..de11f8aef 100644 --- a/types/list.go +++ b/types/list.go @@ -54,6 +54,7 @@ func (l ListType) TerraformType(ctx context.Context) tftypes.Type { func (l ListType) ValueFromTerraform(ctx context.Context, in tftypes.Value) (attr.Value, error) { list := List{ ElemType: l.ElemType, + state: valueStateDeprecated, } if in.Type() == nil { list.Null = true @@ -170,6 +171,43 @@ func (t ListType) ValueType(_ context.Context) attr.Value { } } +// ListNull creates a List with a null value. Determine whether the value is +// null via the List type IsNull method. +// +// Setting the deprecated List type ElemType, Elems, Null, or Unknown fields +// after creating a List with this function has no effect. +func ListNull(elementType attr.Type) List { + return List{ + elementType: elementType, + state: valueStateNull, + } +} + +// ListUnknown creates a List with an unknown value. Determine whether the +// value is unknown via the List type IsUnknown method. +// +// Setting the deprecated List type ElemType, Elems, Null, or Unknown fields +// after creating a List with this function has no effect. +func ListUnknown(elementType attr.Type) List { + return List{ + elementType: elementType, + state: valueStateUnknown, + } +} + +// ListValue creates a List with a known value. Access the value via the List +// type ElementsAs method. +// +// Setting the deprecated List type ElemType, Elems, Null, or Unknown fields +// after creating a List with this function has no effect. +func ListValue(elementType attr.Type, elements []attr.Value) List { + return List{ + elementType: elementType, + elements: elements, + state: valueStateKnown, + } +} + // List represents a list of attr.Values, all of the same type, indicated // by ElemType. type List struct { @@ -179,19 +217,63 @@ type List struct { // surfaces that information. The List's Unknown property only tracks // if the number of elements in a List is known, not whether the // elements that are in the list are known. + // + // If the List was created with the ListValue, ListNull, or ListUnknown + // functions, changing this field has no effect. + // + // Deprecated: Use the ListUnknown function to create an unknown List + // value or use the IsUnknown method to determine whether the List value + // is unknown instead. Unknown bool // Null will be set to true if the list is null, either because it was // omitted from the configuration, state, or plan, or because it was // explicitly set to null. + // + // If the List was created with the ListValue, ListNull, or ListUnknown + // functions, changing this field has no effect. + // + // Deprecated: Use the ListNull function to create a null List value or + // use the IsNull method to determine whether the List value is null + // instead. Null bool // Elems are the elements in the list. + // + // If the List was created with the ListValue, ListNull, or ListUnknown + // functions, changing this field has no effect. + // + // Deprecated: Use the ListValue function to create a known List value or + // use the Elements or ElementsAs methods to retrieve the List elements + // instead. Elems []attr.Value // ElemType is the tftypes.Type of the elements in the list. All // elements in the list must be of this type. + // + // Deprecated: Use the ListValue, ListNull, or ListUnknown functions + // to create a List or use the ElementType method to retrieve the + // List element type instead. ElemType attr.Type + + // elements is the collection of known values in the List. + elements []attr.Value + + // elementType is the type of the elements in the List. + elementType attr.Type + + // state represents whether the List is null, unknown, or known. + state valueState +} + +// Elements returns the collection of elements for the List. Returns nil if the +// List is null or unknown. +func (l List) Elements() []attr.Value { + if l.state == valueStateDeprecated { + return l.Elems + } + + return l.elements } // ElementsAs populates `target` with the elements of the List, throwing an @@ -214,35 +296,72 @@ func (l List) ElementsAs(ctx context.Context, target interface{}, allowUnhandled }, path.Empty()) } +// ElementType returns the element type for the List. +func (l List) ElementType(_ context.Context) attr.Type { + if l.state == valueStateDeprecated { + return l.ElemType + } + + return l.elementType +} + // Type returns a ListType with the same element type as `l`. func (l List) Type(ctx context.Context) attr.Type { - return ListType{ElemType: l.ElemType} + return ListType{ElemType: l.ElementType(ctx)} } // ToTerraformValue returns the data contained in the List as a tftypes.Value. func (l List) ToTerraformValue(ctx context.Context) (tftypes.Value, error) { - if l.ElemType == nil { + if l.state == valueStateDeprecated && l.ElemType == nil { return tftypes.Value{}, fmt.Errorf("cannot convert List to tftypes.Value if ElemType field is not set") } - listType := tftypes.List{ElementType: l.ElemType.TerraformType(ctx)} - if l.Unknown { - return tftypes.NewValue(listType, tftypes.UnknownValue), nil - } - if l.Null { - return tftypes.NewValue(listType, nil), nil - } - vals := make([]tftypes.Value, 0, len(l.Elems)) - for _, elem := range l.Elems { - val, err := elem.ToTerraformValue(ctx) - if err != nil { + listType := tftypes.List{ElementType: l.ElementType(ctx).TerraformType(ctx)} + + switch l.state { + case valueStateDeprecated: + if l.Unknown { + return tftypes.NewValue(listType, tftypes.UnknownValue), nil + } + if l.Null { + return tftypes.NewValue(listType, nil), nil + } + vals := make([]tftypes.Value, 0, len(l.Elems)) + for _, elem := range l.Elems { + val, err := elem.ToTerraformValue(ctx) + if err != nil { + return tftypes.NewValue(listType, tftypes.UnknownValue), err + } + vals = append(vals, val) + } + if err := tftypes.ValidateValue(listType, vals); err != nil { return tftypes.NewValue(listType, tftypes.UnknownValue), err } - vals = append(vals, val) - } - if err := tftypes.ValidateValue(listType, vals); err != nil { - return tftypes.NewValue(listType, tftypes.UnknownValue), err + return tftypes.NewValue(listType, vals), nil + case valueStateKnown: + vals := make([]tftypes.Value, 0, len(l.elements)) + + for _, elem := range l.elements { + val, err := elem.ToTerraformValue(ctx) + + if err != nil { + return tftypes.NewValue(listType, tftypes.UnknownValue), err + } + + vals = append(vals, val) + } + + if err := tftypes.ValidateValue(listType, vals); err != nil { + return tftypes.NewValue(listType, tftypes.UnknownValue), err + } + + return tftypes.NewValue(listType, vals), nil + case valueStateNull: + return tftypes.NewValue(listType, nil), nil + case valueStateUnknown: + return tftypes.NewValue(listType, tftypes.UnknownValue), nil + default: + panic(fmt.Sprintf("unhandled List state in ToTerraformValue: %s", l.state)) } - return tftypes.NewValue(listType, vals), nil } // Equal returns true if the List is considered semantically equal @@ -252,6 +371,28 @@ func (l List) Equal(o attr.Value) bool { if !ok { return false } + if l.state != other.state { + return false + } + if l.state == valueStateKnown { + if !l.elementType.Equal(other.elementType) { + return false + } + + if len(l.elements) != len(other.elements) { + return false + } + + for idx, lElem := range l.elements { + otherElem := other.elements[idx] + + if !lElem.Equal(otherElem) { + return false + } + } + + return true + } if l.Unknown != other.Unknown { return false } @@ -278,30 +419,40 @@ func (l List) Equal(o attr.Value) bool { // IsNull returns true if the List represents a null value. func (l List) IsNull() bool { - return l.Null + if l.state == valueStateNull { + return true + } + + return l.state == valueStateDeprecated && l.Null } // IsUnknown returns true if the List represents a currently unknown value. +// Returns false if the List has a known number of elements, even if all are +// unknown values. func (l List) IsUnknown() bool { - return l.Unknown + if l.state == valueStateUnknown { + return true + } + + return l.state == valueStateDeprecated && l.Unknown } // String returns a human-readable representation of the List value. // The string returned here is not protected by any compatibility guarantees, // and is intended for logging and error reporting. func (l List) String() string { - if l.Unknown { + if l.IsUnknown() { return attr.UnknownValueString } - if l.Null { + if l.IsNull() { return attr.NullValueString } var res strings.Builder res.WriteString("[") - for i, e := range l.Elems { + for i, e := range l.Elements() { if i != 0 { res.WriteString(",") } diff --git a/types/list_test.go b/types/list_test.go index a1ebe027e..42379aa3f 100644 --- a/types/list_test.go +++ b/types/list_test.go @@ -265,6 +265,84 @@ func TestListTypeEqual(t *testing.T) { } } +// This test verifies the assumptions that creating the Value via function then +// setting the fields directly has no effects. +func TestListValue_DeprecatedFieldSetting(t *testing.T) { + t.Parallel() + + knownList := ListValue(StringType, []attr.Value{StringValue("test")}) + + knownList.Null = true + + if knownList.IsNull() { + t.Error("unexpected null update after Null field setting") + } + + knownList.Unknown = true + + if knownList.IsUnknown() { + t.Error("unexpected unknown update after Unknown field setting") + } + + knownList.Elems = []attr.Value{StringValue("not-test")} + + if knownList.Elements()[0].Equal(StringValue("not-test")) { + t.Error("unexpected value update after Value field setting") + } +} + +// This test verifies the assumptions that creating the Value via function then +// setting the fields directly has no effects. +func TestListNull_DeprecatedFieldSetting(t *testing.T) { + t.Parallel() + + nullList := ListNull(StringType) + + nullList.Null = false + + if !nullList.IsNull() { + t.Error("unexpected null update after Null field setting") + } + + nullList.Unknown = true + + if nullList.IsUnknown() { + t.Error("unexpected unknown update after Unknown field setting") + } + + nullList.Elems = []attr.Value{StringValue("test")} + + if len(nullList.Elements()) > 0 { + t.Error("unexpected value update after Value field setting") + } +} + +// This test verifies the assumptions that creating the Value via function then +// setting the fields directly has no effects. +func TestListUnknown_DeprecatedFieldSetting(t *testing.T) { + t.Parallel() + + unknownList := ListUnknown(StringType) + + unknownList.Null = true + + if unknownList.IsNull() { + t.Error("unexpected null update after Null field setting") + } + + unknownList.Unknown = false + + if !unknownList.IsUnknown() { + t.Error("unexpected unknown update after Unknown field setting") + } + + unknownList.Elems = []attr.Value{StringValue("test")} + + if len(unknownList.Elements()) > 0 { + t.Error("unexpected value update after Value field setting") + } +} + func TestListElementsAs_stringSlice(t *testing.T) { t.Parallel() @@ -313,11 +391,58 @@ func TestListToTerraformValue(t *testing.T) { type testCase struct { input List - expectation interface{} + expectation tftypes.Value expectedErr string } tests := map[string]testCase{ - "value": { + "known": { + input: ListValue( + StringType, + []attr.Value{ + StringValue("hello"), + StringValue("world"), + }, + ), + expectation: tftypes.NewValue(tftypes.List{ElementType: tftypes.String}, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "hello"), + tftypes.NewValue(tftypes.String, "world"), + }), + }, + "known-partial-unknown": { + input: ListValue( + StringType, + []attr.Value{ + StringUnknown(), + StringValue("hello, world"), + }, + ), + expectation: tftypes.NewValue(tftypes.List{ElementType: tftypes.String}, []tftypes.Value{ + tftypes.NewValue(tftypes.String, tftypes.UnknownValue), + tftypes.NewValue(tftypes.String, "hello, world"), + }), + }, + "known-partial-null": { + input: ListValue( + StringType, + []attr.Value{ + StringNull(), + StringValue("hello, world"), + }, + ), + expectation: tftypes.NewValue(tftypes.List{ElementType: tftypes.String}, []tftypes.Value{ + tftypes.NewValue(tftypes.String, nil), + tftypes.NewValue(tftypes.String, "hello, world"), + }), + }, + "unknown": { + input: ListUnknown(StringType), + expectation: tftypes.NewValue(tftypes.List{ElementType: tftypes.String}, tftypes.UnknownValue), + }, + "null": { + input: ListNull(StringType), + expectation: tftypes.NewValue(tftypes.List{ElementType: tftypes.String}, nil), + }, + "deprecated-known": { input: List{ ElemType: StringType, Elems: []attr.Value{ @@ -330,15 +455,30 @@ func TestListToTerraformValue(t *testing.T) { tftypes.NewValue(tftypes.String, "world"), }), }, - "unknown": { + "deprecated-known-duplicates": { + input: List{ + 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.NewValue(tftypes.List{ElementType: tftypes.String}, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "hello"), + tftypes.NewValue(tftypes.String, "hello"), + }), + }, + "deprecated-unknown": { input: List{ElemType: StringType, Unknown: true}, expectation: tftypes.NewValue(tftypes.List{ElementType: tftypes.String}, tftypes.UnknownValue), }, - "null": { + "deprecated-null": { input: List{ElemType: StringType, Null: true}, expectation: tftypes.NewValue(tftypes.List{ElementType: tftypes.String}, nil), }, - "partial-unknown": { + "deprecated-known-partial-unknown": { input: List{ ElemType: StringType, Elems: []attr.Value{ @@ -351,7 +491,7 @@ func TestListToTerraformValue(t *testing.T) { tftypes.NewValue(tftypes.String, "hello, world"), }), }, - "partial-null": { + "deprecated-known-partial-null": { input: List{ ElemType: StringType, Elems: []attr.Value{ @@ -367,12 +507,12 @@ func TestListToTerraformValue(t *testing.T) { "no-elem-type": { input: List{ Elems: []attr.Value{ - String{Null: true}, - String{Value: "hello, world"}, + String{Value: "hello"}, + String{Value: "world"}, }, }, - expectedErr: "cannot convert List to tftypes.Value if ElemType field is not set", expectation: tftypes.Value{}, + expectedErr: "cannot convert List to tftypes.Value if ElemType field is not set", }, } for name, test := range tests { @@ -406,6 +546,102 @@ func TestListToTerraformValue(t *testing.T) { } } +func TestListElements(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + input List + expected []attr.Value + }{ + "known": { + input: ListValue(StringType, []attr.Value{StringValue("test")}), + expected: []attr.Value{StringValue("test")}, + }, + "deprecated-known": { + input: List{ElemType: StringType, Elems: []attr.Value{StringValue("test")}}, + expected: []attr.Value{StringValue("test")}, + }, + "null": { + input: ListNull(StringType), + expected: nil, + }, + "deprecated-null": { + input: List{ElemType: StringType, Null: true}, + expected: nil, + }, + "unknown": { + input: ListUnknown(StringType), + expected: nil, + }, + "deprecated-unknown": { + input: List{ElemType: StringType, Unknown: true}, + expected: nil, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.input.Elements() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListElementType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + input List + expected attr.Type + }{ + "known": { + input: ListValue(StringType, []attr.Value{StringValue("test")}), + expected: StringType, + }, + "deprecated-known": { + input: List{ElemType: StringType, Elems: []attr.Value{StringValue("test")}}, + expected: StringType, + }, + "null": { + input: ListNull(StringType), + expected: StringType, + }, + "deprecated-null": { + input: List{ElemType: StringType, Null: true}, + expected: StringType, + }, + "unknown": { + input: ListUnknown(StringType), + expected: StringType, + }, + "deprecated-unknown": { + input: List{ElemType: StringType, Unknown: true}, + expected: StringType, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.input.ElementType(context.Background()) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestListEqual(t *testing.T) { t.Parallel() @@ -415,41 +651,199 @@ func TestListEqual(t *testing.T) { expected bool } tests := map[string]testCase{ - "list-value-list-value": { - receiver: List{ - ElemType: StringType, - Elems: []attr.Value{ - String{Value: "hello"}, - String{Value: "world"}, + "known-known": { + receiver: ListValue( + StringType, + []attr.Value{ + StringValue("hello"), + StringValue("world"), }, - }, - input: List{ - ElemType: StringType, - Elems: []attr.Value{ - String{Value: "hello"}, - String{Value: "world"}, + ), + input: ListValue( + StringType, + []attr.Value{ + StringValue("hello"), + StringValue("world"), }, - }, + ), expected: true, }, - "list-value-diff": { - receiver: List{ + "known-known-diff-value": { + receiver: ListValue( + StringType, + []attr.Value{ + StringValue("hello"), + StringValue("world"), + }, + ), + input: ListValue( + StringType, + []attr.Value{ + StringValue("goodnight"), + StringValue("moon"), + }, + ), + expected: false, + }, + "known-known-diff-length": { + receiver: ListValue( + StringType, + []attr.Value{ + StringValue("hello"), + StringValue("world"), + }, + ), + input: ListValue( + StringType, + []attr.Value{ + StringValue("hello"), + StringValue("world"), + StringValue("extra"), + }, + ), + expected: false, + }, + "known-known-diff-type": { + receiver: ListValue( + StringType, + []attr.Value{ + StringValue("hello"), + StringValue("world"), + }, + ), + input: SetValue( + BoolType, + []attr.Value{ + BoolValue(false), + BoolValue(true), + }, + ), + expected: false, + }, + "known-known-diff-unknown": { + receiver: ListValue( + StringType, + []attr.Value{ + StringValue("hello"), + StringUnknown(), + }, + ), + input: ListValue( + StringType, + []attr.Value{ + StringValue("hello"), + StringValue("world"), + }, + ), + expected: false, + }, + "known-known-diff-null": { + receiver: ListValue( + StringType, + []attr.Value{ + StringValue("hello"), + StringNull(), + }, + ), + input: ListValue( + StringType, + []attr.Value{ + StringValue("hello"), + StringValue("world"), + }, + ), + expected: false, + }, + "known-unknown": { + receiver: ListValue( + StringType, + []attr.Value{ + StringValue("hello"), + StringValue("world"), + }, + ), + input: ListUnknown(StringType), + expected: false, + }, + "known-null": { + receiver: ListValue( + StringType, + []attr.Value{ + StringValue("hello"), + StringValue("world"), + }, + ), + input: ListNull(StringType), + expected: false, + }, + "known-diff-type": { + receiver: ListValue( + StringType, + []attr.Value{ + StringValue("hello"), + StringValue("world"), + }, + ), + input: SetValue( + StringType, + []attr.Value{ + StringValue("hello"), + StringValue("world"), + }, + ), + expected: false, + }, + "known-nil": { + receiver: ListValue( + StringType, + []attr.Value{ + StringValue("hello"), + StringValue("world"), + }, + ), + input: nil, + expected: false, + }, + "known-deprecated-known": { + receiver: ListValue( + StringType, + []attr.Value{ + StringValue("hello"), + StringValue("world"), + }, + ), + input: List{ ElemType: StringType, Elems: []attr.Value{ String{Value: "hello"}, String{Value: "world"}, }, }, - input: List{ - ElemType: StringType, - Elems: []attr.Value{ - String{Value: "goodnight"}, - String{Value: "moon"}, + expected: false, // intentional + }, + "known-deprecated-unknown": { + receiver: ListValue( + StringType, + []attr.Value{ + StringValue("hello"), + StringValue("world"), }, - }, + ), + input: List{ElemType: StringType, Unknown: true}, + expected: false, + }, + "known-deprecated-null": { + receiver: ListValue( + StringType, + []attr.Value{ + StringValue("hello"), + StringValue("world"), + }, + ), + input: List{ElemType: StringType, Null: true}, expected: false, }, - "list-value-count-diff": { + "deprecated-known-deprecated-known": { receiver: List{ ElemType: StringType, Elems: []attr.Value{ @@ -462,12 +856,11 @@ func TestListEqual(t *testing.T) { Elems: []attr.Value{ String{Value: "hello"}, String{Value: "world"}, - String{Value: "test"}, }, }, - expected: false, + expected: true, }, - "list-value-type-diff": { + "deprecated-known-deprecated-known-diff-value": { receiver: List{ ElemType: StringType, Elems: []attr.Value{ @@ -476,15 +869,15 @@ func TestListEqual(t *testing.T) { }, }, input: List{ - ElemType: BoolType, + ElemType: StringType, Elems: []attr.Value{ - Bool{Value: false}, - Bool{Value: true}, + String{Value: "goodnight"}, + String{Value: "moon"}, }, }, expected: false, }, - "list-value-unknown": { + "deprecated-known-deprecated-known-diff-length": { receiver: List{ ElemType: StringType, Elems: []attr.Value{ @@ -492,21 +885,17 @@ func TestListEqual(t *testing.T) { String{Value: "world"}, }, }, - input: List{Unknown: true}, - expected: false, - }, - "list-value-null": { - receiver: List{ + input: List{ ElemType: StringType, Elems: []attr.Value{ String{Value: "hello"}, String{Value: "world"}, + String{Value: "test"}, }, }, - input: List{Null: true}, expected: false, }, - "list-value-wrongType": { + "deprecated-known-deprecated-known-diff-type": { receiver: List{ ElemType: StringType, Elems: []attr.Value{ @@ -514,21 +903,16 @@ func TestListEqual(t *testing.T) { String{Value: "world"}, }, }, - input: String{Value: "hello, world"}, - expected: false, - }, - "list-value-nil": { - receiver: List{ - ElemType: StringType, + input: List{ + ElemType: BoolType, Elems: []attr.Value{ - String{Value: "hello"}, - String{Value: "world"}, + Bool{Value: false}, + Bool{Value: true}, }, }, - input: nil, expected: false, }, - "partially-known-list-value-list-value": { + "deprecated-known-deprecated-known-diff-unknown": { receiver: List{ ElemType: StringType, Elems: []attr.Value{ @@ -540,17 +924,17 @@ func TestListEqual(t *testing.T) { ElemType: StringType, Elems: []attr.Value{ String{Value: "hello"}, - String{Unknown: true}, + String{Value: "world"}, }, }, - expected: true, + expected: false, }, - "partially-known-list-value-diff": { + "deprecated-known-deprecated-known-diff-null": { receiver: List{ ElemType: StringType, Elems: []attr.Value{ String{Value: "hello"}, - String{Unknown: true}, + String{Null: true}, }, }, input: List{ @@ -562,127 +946,84 @@ func TestListEqual(t *testing.T) { }, expected: false, }, - "partially-known-list-value-unknown": { + "deprecated-known-deprecated-unknown": { receiver: List{ ElemType: StringType, Elems: []attr.Value{ String{Value: "hello"}, - String{Unknown: true}, + String{Value: "world"}, }, }, input: List{Unknown: true}, expected: false, }, - "partially-known-list-value-null": { + "deprecated-known-deprecated-null": { receiver: List{ ElemType: StringType, Elems: []attr.Value{ String{Value: "hello"}, - String{Unknown: true}, + String{Value: "world"}, }, }, input: List{Null: true}, expected: false, }, - "partially-known-list-value-wrongType": { + "deprecated-known-known": { receiver: List{ ElemType: StringType, Elems: []attr.Value{ String{Value: "hello"}, - String{Unknown: true}, + String{Value: "world"}, }, }, - input: String{Value: "hello, world"}, - expected: false, - }, - "partially-known-list-value-nil": { - receiver: List{ - ElemType: StringType, - Elems: []attr.Value{ - String{Value: "hello"}, - String{Unknown: true}, + input: ListValue( + StringType, + []attr.Value{ + StringValue("hello"), + StringValue("world"), }, - }, - input: nil, - expected: false, + ), + expected: false, // intentional }, - "partially-null-list-value-list-value": { + "deprecated-known-unknown": { receiver: List{ - ElemType: StringType, - Elems: []attr.Value{ - String{Value: "hello"}, - String{Null: true}, - }, - }, - input: List{ - ElemType: StringType, - Elems: []attr.Value{ - String{Value: "hello"}, - String{Null: true}, - }, - }, - expected: true, - }, - "partially-null-list-value-diff": { - receiver: List{ - ElemType: StringType, - Elems: []attr.Value{ - String{Value: "hello"}, - String{Null: true}, - }, - }, - input: List{ ElemType: StringType, Elems: []attr.Value{ String{Value: "hello"}, String{Value: "world"}, }, }, + input: ListUnknown(StringType), expected: false, }, - "partially-null-list-value-unknown": { - receiver: List{ - ElemType: StringType, - Elems: []attr.Value{ - String{Value: "hello"}, - String{Null: true}, - }, - }, - input: List{ - Unknown: true, - }, - expected: false, - }, - "partially-null-list-value-null": { + "deprecated-known-null": { receiver: List{ ElemType: StringType, Elems: []attr.Value{ String{Value: "hello"}, - String{Null: true}, + String{Value: "world"}, }, }, - input: List{ - Null: true, - }, + input: ListNull(StringType), expected: false, }, - "partially-null-list-value-wrongType": { + "deprecated-known-diff-type": { receiver: List{ ElemType: StringType, Elems: []attr.Value{ String{Value: "hello"}, - String{Null: true}, + String{Value: "world"}, }, }, input: String{Value: "hello, world"}, expected: false, }, - "partially-null-list-value-nil": { + "deprecated-known-nil": { receiver: List{ ElemType: StringType, Elems: []attr.Value{ String{Value: "hello"}, - String{Null: true}, + String{Value: "world"}, }, }, input: nil, @@ -702,6 +1043,110 @@ func TestListEqual(t *testing.T) { } } +func TestListIsNull(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + input List + expected bool + }{ + "known": { + input: ListValue(StringType, []attr.Value{StringValue("test")}), + expected: false, + }, + "deprecated-known": { + input: List{ElemType: StringType, Elems: []attr.Value{StringValue("test")}}, + expected: false, + }, + "null": { + input: ListNull(StringType), + expected: true, + }, + "deprecated-null": { + input: List{ElemType: StringType, Null: true}, + expected: true, + }, + "unknown": { + input: ListUnknown(StringType), + expected: false, + }, + "deprecated-unknown": { + input: List{ElemType: StringType, Unknown: true}, + expected: false, + }, + "deprecated-invalid": { + input: List{ElemType: StringType, Null: true, Unknown: true}, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.input.IsNull() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListIsUnknown(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + input List + expected bool + }{ + "known": { + input: ListValue(StringType, []attr.Value{StringValue("test")}), + expected: false, + }, + "deprecated-known": { + input: List{ElemType: StringType, Elems: []attr.Value{StringValue("test")}}, + expected: false, + }, + "null": { + input: ListNull(StringType), + expected: false, + }, + "deprecated-null": { + input: List{ElemType: StringType, Null: true}, + expected: false, + }, + "unknown": { + input: ListUnknown(StringType), + expected: true, + }, + "deprecated-unknown": { + input: List{ElemType: StringType, Unknown: true}, + expected: true, + }, + "deprecated-invalid": { + input: List{ElemType: StringType, Null: true, Unknown: true}, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.input.IsUnknown() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestListString(t *testing.T) { t.Parallel() @@ -710,7 +1155,49 @@ func TestListString(t *testing.T) { expectation string } tests := map[string]testCase{ - "simple": { + "known": { + input: ListValue( + StringType, + []attr.Value{ + StringValue("hello"), + StringValue("world"), + }, + ), + expectation: `["hello","world"]`, + }, + "known-list-of-lists": { + input: ListValue( + ListType{ + ElemType: StringType, + }, + []attr.Value{ + ListValue( + StringType, + []attr.Value{ + StringValue("hello"), + StringValue("world"), + }, + ), + ListValue( + StringType, + []attr.Value{ + StringValue("foo"), + StringValue("bar"), + }, + ), + }, + ), + expectation: `[["hello","world"],["foo","bar"]]`, + }, + "unknown": { + input: ListUnknown(StringType), + expectation: "", + }, + "null": { + input: ListNull(StringType), + expectation: "", + }, + "deprecated-known": { input: List{ ElemType: StringType, Elems: []attr.Value{ @@ -720,7 +1207,7 @@ func TestListString(t *testing.T) { }, expectation: `["hello","world"]`, }, - "list-of-lists": { + "deprecated-known-list-of-lists": { input: List{ ElemType: ListType{ ElemType: StringType, @@ -744,11 +1231,11 @@ func TestListString(t *testing.T) { }, expectation: `[["hello","world"],["foo","bar"]]`, }, - "unknown": { + "deprecated-unknown": { input: List{Unknown: true}, expectation: "", }, - "null": { + "deprecated-null": { input: List{Null: true}, expectation: "", }, @@ -771,6 +1258,121 @@ func TestListString(t *testing.T) { } } +func TestListType(t *testing.T) { + t.Parallel() + + type testCase struct { + input List + expectation attr.Type + } + tests := map[string]testCase{ + "known": { + input: ListValue( + StringType, + []attr.Value{ + String{Value: "hello"}, + String{Value: "world"}, + }, + ), + expectation: ListType{ElemType: StringType}, + }, + "known-list-of-lists": { + input: ListValue( + ListType{ + ElemType: StringType, + }, + []attr.Value{ + ListValue( + StringType, + []attr.Value{ + String{Value: "hello"}, + String{Value: "world"}, + }, + ), + ListValue( + StringType, + []attr.Value{ + String{Value: "foo"}, + String{Value: "bar"}, + }, + ), + }, + ), + expectation: ListType{ + ElemType: ListType{ + ElemType: StringType, + }, + }, + }, + "unknown": { + input: ListUnknown(StringType), + expectation: ListType{ElemType: StringType}, + }, + "null": { + input: ListNull(StringType), + expectation: ListType{ElemType: StringType}, + }, + "deprecated-known": { + input: List{ + ElemType: StringType, + Elems: []attr.Value{ + String{Value: "hello"}, + String{Value: "world"}, + }, + }, + expectation: ListType{ElemType: StringType}, + }, + "deprecated-known-list-of-lists": { + input: List{ + ElemType: ListType{ + ElemType: StringType, + }, + Elems: []attr.Value{ + List{ + ElemType: StringType, + Elems: []attr.Value{ + String{Value: "hello"}, + String{Value: "world"}, + }, + }, + List{ + ElemType: StringType, + Elems: []attr.Value{ + String{Value: "foo"}, + String{Value: "bar"}, + }, + }, + }, + }, + expectation: ListType{ + ElemType: ListType{ + ElemType: StringType, + }, + }, + }, + "deprecated-unknown": { + input: List{ElemType: StringType, Unknown: true}, + expectation: ListType{ElemType: StringType}, + }, + "deprecated-null": { + input: List{ElemType: StringType, Null: true}, + expectation: ListType{ElemType: StringType}, + }, + } + + for name, test := range tests { + name, test := name, test + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := test.input.Type(context.Background()) + if !cmp.Equal(got, test.expectation) { + t.Errorf("Expected %q, got %q", test.expectation, got) + } + }) + } +} + func TestListTypeValidate(t *testing.T) { t.Parallel() diff --git a/types/map.go b/types/map.go index 196854e24..a83cbb772 100644 --- a/types/map.go +++ b/types/map.go @@ -55,6 +55,7 @@ func (m MapType) TerraformType(ctx context.Context) tftypes.Type { func (m MapType) ValueFromTerraform(ctx context.Context, in tftypes.Value) (attr.Value, error) { ma := Map{ ElemType: m.ElemType, + state: valueStateDeprecated, } if in.Type() == nil { ma.Null = true @@ -174,8 +175,45 @@ func (t MapType) ValueType(_ context.Context) attr.Value { } } -// Map represents a map of attr.Values, all of the same type, indicated by -// ElemType. Keys for the map will always be strings. +// MapNull creates a Map with a null value. Determine whether the value is +// null via the Map type IsNull method. +// +// Setting the deprecated Map type ElemType, Elems, Null, or Unknown fields +// after creating a Map with this function has no effect. +func MapNull(elementType attr.Type) Map { + return Map{ + elementType: elementType, + state: valueStateNull, + } +} + +// MapUnknown creates a Map with an unknown value. Determine whether the +// value is unknown via the Map type IsUnknown method. +// +// Setting the deprecated Map type ElemType, Elems, Null, or Unknown fields +// after creating a Map with this function has no effect. +func MapUnknown(elementType attr.Type) Map { + return Map{ + elementType: elementType, + state: valueStateUnknown, + } +} + +// MapValue creates a Map with a known value. Access the value via the Map +// type ElementsAs method. +// +// Setting the deprecated Map type ElemType, Elems, Null, or Unknown fields +// after creating a Map with this function has no effect. +func MapValue(elementType attr.Type, elements map[string]attr.Value) Map { + return Map{ + elementType: elementType, + elements: elements, + state: valueStateKnown, + } +} + +// Map represents a mapping of string keys to attr.Value values of a single +// type. type Map struct { // Unknown will be set to true if the entire map is an unknown value. // If only some of the elements in the map are unknown, their known or @@ -183,19 +221,63 @@ type Map struct { // surfaces that information. The Map's Unknown property only tracks if // the number of elements in a Map is known, not whether the elements // that are in the map are known. + // + // If the Map was created with the MapValue, MapNull, or MapUnknown + // functions, changing this field has no effect. + // + // Deprecated: Use the MapUnknown function to create an unknown Map + // value or use the IsUnknown method to determine whether the Map value + // is unknown instead. Unknown bool // Null will be set to true if the map is null, either because it was // omitted from the configuration, state, or plan, or because it was // explicitly set to null. + // + // If the Map was created with the MapValue, MapNull, or MapUnknown + // functions, changing this field has no effect. + // + // Deprecated: Use the MapNull function to create a null Map value or + // use the IsNull method to determine whether the Map value is null + // instead. Null bool // Elems are the elements in the map. + // + // If the Map was created with the MapValue, MapNull, or MapUnknown + // functions, changing this field has no effect. + // + // Deprecated: Use the MapValue function to create a known Map value or + // use the Elements or ElementsAs methods to retrieve the Map elements + // instead. Elems map[string]attr.Value // ElemType is the AttributeType of the elements in the map. All // elements in the map must be of this type. + // + // Deprecated: Use the MapValue, MapNull, or MapUnknown functions + // to create a Map or use the ElementType method to retrieve the + // Map element type instead. ElemType attr.Type + + // elements is the mapping of known values in the Map. + elements map[string]attr.Value + + // elementType is the type of the elements in the Map. + elementType attr.Type + + // state represents whether the Map is null, unknown, or known. + state valueState +} + +// Elements returns the mapping of elements for the Map. Returns nil if the +// Map is null or unknown. +func (m Map) Elements() map[string]attr.Value { + if m.state == valueStateDeprecated { + return m.Elems + } + + return m.elements } // ElementsAs populates `target` with the elements of the Map, throwing an @@ -220,35 +302,72 @@ func (m Map) ElementsAs(ctx context.Context, target interface{}, allowUnhandled }, path.Empty()) } +// ElementType returns the element type for the Map. +func (m Map) ElementType(_ context.Context) attr.Type { + if m.state == valueStateDeprecated { + return m.ElemType + } + + return m.elementType +} + // Type returns a MapType with the same element type as `m`. func (m Map) Type(ctx context.Context) attr.Type { - return MapType{ElemType: m.ElemType} + return MapType{ElemType: m.ElementType(ctx)} } // ToTerraformValue returns the data contained in the List as a tftypes.Value. func (m Map) ToTerraformValue(ctx context.Context) (tftypes.Value, error) { - if m.ElemType == nil { + if m.state == valueStateDeprecated && m.ElemType == nil { return tftypes.Value{}, fmt.Errorf("cannot convert Map to tftypes.Value if ElemType field is not set") } - mapType := tftypes.Map{ElementType: m.ElemType.TerraformType(ctx)} - if m.Unknown { - return tftypes.NewValue(mapType, tftypes.UnknownValue), nil - } - if m.Null { - return tftypes.NewValue(mapType, nil), nil - } - vals := make(map[string]tftypes.Value, len(m.Elems)) - for key, elem := range m.Elems { - val, err := elem.ToTerraformValue(ctx) - if err != nil { + mapType := tftypes.Map{ElementType: m.ElementType(ctx).TerraformType(ctx)} + + switch m.state { + case valueStateDeprecated: + if m.Unknown { + return tftypes.NewValue(mapType, tftypes.UnknownValue), nil + } + if m.Null { + return tftypes.NewValue(mapType, nil), nil + } + vals := make(map[string]tftypes.Value, len(m.Elems)) + for key, elem := range m.Elems { + val, err := elem.ToTerraformValue(ctx) + if err != nil { + return tftypes.NewValue(mapType, tftypes.UnknownValue), err + } + vals[key] = val + } + if err := tftypes.ValidateValue(mapType, vals); err != nil { return tftypes.NewValue(mapType, tftypes.UnknownValue), err } - vals[key] = val - } - if err := tftypes.ValidateValue(mapType, vals); err != nil { - return tftypes.NewValue(mapType, tftypes.UnknownValue), err + return tftypes.NewValue(mapType, vals), nil + case valueStateKnown: + vals := make(map[string]tftypes.Value, len(m.elements)) + + for key, elem := range m.elements { + val, err := elem.ToTerraformValue(ctx) + + if err != nil { + return tftypes.NewValue(mapType, tftypes.UnknownValue), err + } + + vals[key] = val + } + + if err := tftypes.ValidateValue(mapType, vals); err != nil { + return tftypes.NewValue(mapType, tftypes.UnknownValue), err + } + + return tftypes.NewValue(mapType, vals), nil + case valueStateNull: + return tftypes.NewValue(mapType, nil), nil + case valueStateUnknown: + return tftypes.NewValue(mapType, tftypes.UnknownValue), nil + default: + panic(fmt.Sprintf("unhandled Map state in ToTerraformValue: %s", m.state)) } - return tftypes.NewValue(mapType, vals), nil } // Equal returns true if the Map is considered semantically equal @@ -258,6 +377,28 @@ func (m Map) Equal(o attr.Value) bool { if !ok { return false } + if m.state != other.state { + return false + } + if m.state == valueStateKnown { + if !m.elementType.Equal(other.elementType) { + return false + } + + if len(m.elements) != len(other.elements) { + return false + } + + for key, mElem := range m.elements { + otherElem := other.elements[key] + + if !mElem.Equal(otherElem) { + return false + } + } + + return true + } if m.Unknown != other.Unknown { return false } @@ -284,29 +425,39 @@ func (m Map) Equal(o attr.Value) bool { // IsNull returns true if the Map represents a null value. func (m Map) IsNull() bool { - return m.Null + if m.state == valueStateNull { + return true + } + + return m.state == valueStateDeprecated && m.Null } // IsUnknown returns true if the Map represents a currently unknown value. +// Returns false if the Map has a known number of elements, even if all are +// unknown values. func (m Map) IsUnknown() bool { - return m.Unknown + if m.state == valueStateUnknown { + return true + } + + return m.state == valueStateDeprecated && m.Unknown } // String returns a human-readable representation of the Map value. // The string returned here is not protected by any compatibility guarantees, // and is intended for logging and error reporting. func (m Map) String() string { - if m.Unknown { + if m.IsUnknown() { return attr.UnknownValueString } - if m.Null { + if m.IsNull() { return attr.NullValueString } // We want the output to be consistent, so we sort the output by key - keys := make([]string, 0, len(m.Elems)) - for k := range m.Elems { + keys := make([]string, 0, len(m.Elements())) + for k := range m.Elements() { keys = append(keys, k) } sort.Strings(keys) @@ -318,7 +469,7 @@ func (m Map) String() string { if i != 0 { res.WriteString(",") } - res.WriteString(fmt.Sprintf("%q:%s", k, m.Elems[k].String())) + res.WriteString(fmt.Sprintf("%q:%s", k, m.Elements()[k].String())) } res.WriteString("}") diff --git a/types/map_test.go b/types/map_test.go index 9a58e6cab..9f35e4c12 100644 --- a/types/map_test.go +++ b/types/map_test.go @@ -244,6 +244,84 @@ func TestMapTypeEqual(t *testing.T) { } } +// This test verifies the assumptions that creating the Value via function then +// setting the fields directly has no effects. +func TestMapValue_DeprecatedFieldSetting(t *testing.T) { + t.Parallel() + + knownMap := MapValue(StringType, map[string]attr.Value{"test-key": StringValue("test-value")}) + + knownMap.Null = true + + if knownMap.IsNull() { + t.Error("unexpected null update after Null field setting") + } + + knownMap.Unknown = true + + if knownMap.IsUnknown() { + t.Error("unexpected unknown update after Unknown field setting") + } + + knownMap.Elems = map[string]attr.Value{"test-key": StringValue("not-test-value")} + + if knownMap.Elements()["test-key"].Equal(StringValue("not-test-value")) { + t.Error("unexpected value update after Value field setting") + } +} + +// This test verifies the assumptions that creating the Value via function then +// setting the fields directly has no effects. +func TestMapNull_DeprecatedFieldSetting(t *testing.T) { + t.Parallel() + + nullMap := MapNull(StringType) + + nullMap.Null = false + + if !nullMap.IsNull() { + t.Error("unexpected null update after Null field setting") + } + + nullMap.Unknown = true + + if nullMap.IsUnknown() { + t.Error("unexpected unknown update after Unknown field setting") + } + + nullMap.Elems = map[string]attr.Value{"test-key": StringValue("test")} + + if len(nullMap.Elements()) > 0 { + t.Error("unexpected value update after Value field setting") + } +} + +// This test verifies the assumptions that creating the Value via function then +// setting the fields directly has no effects. +func TestMapUnknown_DeprecatedFieldSetting(t *testing.T) { + t.Parallel() + + unknownMap := MapUnknown(StringType) + + unknownMap.Null = true + + if unknownMap.IsNull() { + t.Error("unexpected null update after Null field setting") + } + + unknownMap.Unknown = false + + if !unknownMap.IsUnknown() { + t.Error("unexpected unknown update after Unknown field setting") + } + + unknownMap.Elems = map[string]attr.Value{"test-key": StringValue("test")} + + if len(unknownMap.Elements()) > 0 { + t.Error("unexpected value update after Value field setting") + } +} + func TestMapElementsAs_mapStringString(t *testing.T) { t.Parallel() @@ -299,62 +377,124 @@ func TestMapToTerraformValue(t *testing.T) { expectedErr string } tests := map[string]testCase{ - "value": { + "known": { + input: MapValue( + StringType, + map[string]attr.Value{ + "key1": StringValue("hello"), + "key2": StringValue("world"), + }, + ), + expectation: tftypes.NewValue(tftypes.Map{ElementType: tftypes.String}, map[string]tftypes.Value{ + "key1": tftypes.NewValue(tftypes.String, "hello"), + "key2": tftypes.NewValue(tftypes.String, "world"), + }), + }, + "known-partial-unknown": { + input: MapValue( + StringType, + map[string]attr.Value{ + "key1": StringUnknown(), + "key2": StringValue("hello, world"), + }, + ), + expectation: tftypes.NewValue(tftypes.Map{ElementType: tftypes.String}, map[string]tftypes.Value{ + "key1": tftypes.NewValue(tftypes.String, tftypes.UnknownValue), + "key2": tftypes.NewValue(tftypes.String, "hello, world"), + }), + }, + "known-partial-null": { + input: MapValue( + StringType, + map[string]attr.Value{ + "key1": StringNull(), + "key2": StringValue("hello, world"), + }, + ), + expectation: tftypes.NewValue(tftypes.Map{ElementType: tftypes.String}, map[string]tftypes.Value{ + "key1": tftypes.NewValue(tftypes.String, nil), + "key2": tftypes.NewValue(tftypes.String, "hello, world"), + }), + }, + "unknown": { + input: MapUnknown(StringType), + expectation: tftypes.NewValue(tftypes.Map{ElementType: tftypes.String}, tftypes.UnknownValue), + }, + "null": { + input: MapNull(StringType), + expectation: tftypes.NewValue(tftypes.Map{ElementType: tftypes.String}, nil), + }, + "deprecated-known": { input: Map{ ElemType: StringType, Elems: map[string]attr.Value{ - "h": String{Value: "hello"}, - "w": String{Value: "world"}, + "key1": String{Value: "hello"}, + "key2": String{Value: "world"}, }, }, expectation: tftypes.NewValue(tftypes.Map{ElementType: tftypes.String}, map[string]tftypes.Value{ - "h": tftypes.NewValue(tftypes.String, "hello"), - "w": tftypes.NewValue(tftypes.String, "world"), + "key1": tftypes.NewValue(tftypes.String, "hello"), + "key2": tftypes.NewValue(tftypes.String, "world"), }), }, - "unknown": { + "deprecated-known-duplicates": { + input: Map{ + ElemType: StringType, + Elems: map[string]attr.Value{ + "key1": String{Value: "hello"}, + "key2": String{Value: "hello"}, + }, + }, + // Duplicate validation does not occur during this method. + // This is okay, as tftypes allows duplicates. + expectation: tftypes.NewValue(tftypes.Map{ElementType: tftypes.String}, map[string]tftypes.Value{ + "key1": tftypes.NewValue(tftypes.String, "hello"), + "key2": tftypes.NewValue(tftypes.String, "hello"), + }), + }, + "deprecated-unknown": { input: Map{ElemType: StringType, Unknown: true}, expectation: tftypes.NewValue(tftypes.Map{ElementType: tftypes.String}, tftypes.UnknownValue), }, - "null": { + "deprecated-null": { input: Map{ElemType: StringType, Null: true}, expectation: tftypes.NewValue(tftypes.Map{ElementType: tftypes.String}, nil), }, - "partial-unknown": { + "deprecated-known-partial-unknown": { input: Map{ ElemType: StringType, Elems: map[string]attr.Value{ - "unk": String{Unknown: true}, - "hw": String{Value: "hello, world"}, + "key1": String{Unknown: true}, + "key2": String{Value: "hello, world"}, }, }, expectation: tftypes.NewValue(tftypes.Map{ElementType: tftypes.String}, map[string]tftypes.Value{ - "unk": tftypes.NewValue(tftypes.String, tftypes.UnknownValue), - "hw": tftypes.NewValue(tftypes.String, "hello, world"), + "key1": tftypes.NewValue(tftypes.String, tftypes.UnknownValue), + "key2": tftypes.NewValue(tftypes.String, "hello, world"), }), }, - "partial-null": { + "deprecated-known-partial-null": { input: Map{ ElemType: StringType, Elems: map[string]attr.Value{ - "n": String{Null: true}, - "hw": String{Value: "hello, world"}, + "key1": String{Null: true}, + "key2": String{Value: "hello, world"}, }, }, expectation: tftypes.NewValue(tftypes.Map{ElementType: tftypes.String}, map[string]tftypes.Value{ - "n": tftypes.NewValue(tftypes.String, nil), - "hw": tftypes.NewValue(tftypes.String, "hello, world"), + "key1": tftypes.NewValue(tftypes.String, nil), + "key2": tftypes.NewValue(tftypes.String, "hello, world"), }), }, "no-elem-type": { input: Map{ Elems: map[string]attr.Value{ - "n": String{Null: true}, - "hw": String{Value: "hello, world"}, + "key1": String{Value: "hello"}, + "key2": String{Value: "world"}, }, }, - expectedErr: "cannot convert Map to tftypes.Value if ElemType field is not set", expectation: tftypes.Value{}, + expectedErr: "cannot convert Map to tftypes.Value if ElemType field is not set", }, } for name, test := range tests { @@ -388,6 +528,102 @@ func TestMapToTerraformValue(t *testing.T) { } } +func TestMapElements(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + input Map + expected map[string]attr.Value + }{ + "known": { + input: MapValue(StringType, map[string]attr.Value{"test-key": StringValue("test-value")}), + expected: map[string]attr.Value{"test-key": StringValue("test-value")}, + }, + "deprecated-known": { + input: Map{ElemType: StringType, Elems: map[string]attr.Value{"test-key": StringValue("test-value")}}, + expected: map[string]attr.Value{"test-key": StringValue("test-value")}, + }, + "null": { + input: MapNull(StringType), + expected: nil, + }, + "deprecated-null": { + input: Map{ElemType: StringType, Null: true}, + expected: nil, + }, + "unknown": { + input: MapUnknown(StringType), + expected: nil, + }, + "deprecated-unknown": { + input: Map{ElemType: StringType, Unknown: true}, + expected: nil, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.input.Elements() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestMapElementType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + input Map + expected attr.Type + }{ + "known": { + input: MapValue(StringType, map[string]attr.Value{"test-key": StringValue("test-value")}), + expected: StringType, + }, + "deprecated-known": { + input: Map{ElemType: StringType, Elems: map[string]attr.Value{"test-key": StringValue("test-value")}}, + expected: StringType, + }, + "null": { + input: MapNull(StringType), + expected: StringType, + }, + "deprecated-null": { + input: Map{ElemType: StringType, Null: true}, + expected: StringType, + }, + "unknown": { + input: MapUnknown(StringType), + expected: StringType, + }, + "deprecated-unknown": { + input: Map{ElemType: StringType, Unknown: true}, + expected: StringType, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.input.ElementType(context.Background()) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestMapEqual(t *testing.T) { t.Parallel() @@ -397,291 +633,379 @@ func TestMapEqual(t *testing.T) { expected bool } tests := map[string]testCase{ - "equal": { - receiver: Map{ - ElemType: StringType, - Elems: map[string]attr.Value{ - "h": String{Value: "hello"}, - "w": String{Value: "world"}, + "known-known": { + receiver: MapValue( + StringType, + map[string]attr.Value{ + "key1": StringValue("hello"), + "key2": StringValue("world"), }, - }, - input: Map{ - ElemType: StringType, - Elems: map[string]attr.Value{ - "h": String{Value: "hello"}, - "w": String{Value: "world"}, + ), + input: MapValue( + StringType, + map[string]attr.Value{ + "key1": StringValue("hello"), + "key2": StringValue("world"), }, - }, + ), expected: true, }, - "elem-value-diff": { - receiver: Map{ - ElemType: StringType, - Elems: map[string]attr.Value{ - "h": String{Value: "hello"}, - "w": String{Value: "world"}, + "known-known-diff-value": { + receiver: MapValue( + StringType, + map[string]attr.Value{ + "key1": StringValue("hello"), + "key2": StringValue("world"), }, - }, - input: Map{ - ElemType: StringType, - Elems: map[string]attr.Value{ - "h": String{Value: "goodnight"}, - "w": String{Value: "moon"}, + ), + input: MapValue( + StringType, + map[string]attr.Value{ + "key1": StringValue("goodnight"), + "key2": StringValue("moon"), }, - }, + ), expected: false, }, - "elem-key-diff": { - receiver: Map{ - ElemType: StringType, - Elems: map[string]attr.Value{ - "h": String{Value: "hello"}, - "w": String{Value: "world"}, + "known-known-diff-length": { + receiver: MapValue( + StringType, + map[string]attr.Value{ + "key1": StringValue("hello"), + "key2": StringValue("world"), }, - }, - input: Map{ - ElemType: StringType, - Elems: map[string]attr.Value{ - "no": String{Value: "hello"}, - "w": String{Value: "world"}, + ), + input: MapValue( + StringType, + map[string]attr.Value{ + "key1": StringValue("hello"), + "key2": StringValue("world"), + "key3": StringValue("extra"), }, - }, + ), expected: false, }, - "elem-count-diff": { - receiver: Map{ - ElemType: StringType, - Elems: map[string]attr.Value{ - "h": String{Value: "hello"}, - "w": String{Value: "world"}, + "known-known-diff-type": { + receiver: MapValue( + StringType, + map[string]attr.Value{ + "key1": StringValue("hello"), + "key2": StringValue("world"), }, - }, - input: Map{ - ElemType: StringType, - Elems: map[string]attr.Value{ - "h": String{Value: "hello"}, - "w": String{Value: "world"}, - "t": String{Value: "test"}, + ), + input: SetValue( + BoolType, + []attr.Value{ + BoolValue(false), + BoolValue(true), }, - }, + ), expected: false, }, - "elem-value-type-diff": { - receiver: Map{ - ElemType: StringType, - Elems: map[string]attr.Value{ - "h": String{Value: "hello"}, - "w": String{Value: "world"}, + "known-known-diff-unknown": { + receiver: MapValue( + StringType, + map[string]attr.Value{ + "key1": StringValue("hello"), + "key2": StringUnknown(), }, - }, - input: Map{ - ElemType: BoolType, - Elems: map[string]attr.Value{ - "h": Bool{Value: false}, - "w": Bool{Value: true}, + ), + input: MapValue( + StringType, + map[string]attr.Value{ + "key1": StringValue("hello"), + "key2": StringValue("world"), }, - }, + ), expected: false, }, - "map-value-unknown": { - receiver: Map{ - ElemType: StringType, - Elems: map[string]attr.Value{ - "h": String{Value: "hello"}, - "w": String{Value: "world"}, + "known-known-diff-null": { + receiver: MapValue( + StringType, + map[string]attr.Value{ + "key1": StringValue("hello"), + "key2": StringNull(), }, - }, - input: Map{Unknown: true}, + ), + input: MapValue( + StringType, + map[string]attr.Value{ + "key1": StringValue("hello"), + "key2": StringValue("world"), + }, + ), expected: false, }, - "map-value-null": { - receiver: Map{ - ElemType: StringType, - Elems: map[string]attr.Value{ - "h": String{Value: "hello"}, - "w": String{Value: "world"}, + "known-unknown": { + receiver: MapValue( + StringType, + map[string]attr.Value{ + "key1": StringValue("hello"), + "key2": StringValue("world"), }, - }, - input: Map{Null: true}, + ), + input: MapUnknown(StringType), expected: false, }, - "map-elem-wrongType": { - receiver: Map{ - ElemType: StringType, - Elems: map[string]attr.Value{ - "h": String{Value: "hello"}, - "w": String{Value: "world"}, + "known-null": { + receiver: MapValue( + StringType, + map[string]attr.Value{ + "key1": StringValue("hello"), + "key2": StringValue("world"), }, - }, - input: String{Value: "hello, world"}, + ), + input: MapNull(StringType), expected: false, }, - "value-nil": { - receiver: Map{ + "known-diff-type": { + receiver: MapValue( + StringType, + map[string]attr.Value{ + "key1": StringValue("hello"), + "key2": StringValue("world"), + }, + ), + input: SetValue( + StringType, + []attr.Value{ + StringValue("hello"), + StringValue("world"), + }, + ), + expected: false, + }, + "known-nil": { + receiver: MapValue( + StringType, + map[string]attr.Value{ + "key1": StringValue("hello"), + "key2": StringValue("world"), + }, + ), + input: nil, + expected: false, + }, + "known-deprecated-known": { + receiver: MapValue( + StringType, + map[string]attr.Value{ + "key1": StringValue("hello"), + "key2": StringValue("world"), + }, + ), + input: Map{ ElemType: StringType, Elems: map[string]attr.Value{ - "h": String{Value: "hello"}, - "w": String{Value: "world"}, + "key1": String{Value: "hello"}, + "key2": String{Value: "world"}, }, }, - input: nil, + expected: false, // intentional + }, + "known-deprecated-unknown": { + receiver: MapValue( + StringType, + map[string]attr.Value{ + "key1": StringValue("hello"), + "key2": StringValue("world"), + }, + ), + input: Map{ElemType: StringType, Unknown: true}, expected: false, }, - "partially-known": { + "known-deprecated-null": { + receiver: MapValue( + StringType, + map[string]attr.Value{ + "key1": StringValue("hello"), + "key2": StringValue("world"), + }, + ), + input: Map{ElemType: StringType, Null: true}, + expected: false, + }, + "deprecated-known-deprecated-known": { receiver: Map{ ElemType: StringType, Elems: map[string]attr.Value{ - "h": String{Value: "hello"}, - "w": String{Unknown: true}, + "key1": String{Value: "hello"}, + "key2": String{Value: "world"}, }, }, input: Map{ ElemType: StringType, Elems: map[string]attr.Value{ - "h": String{Value: "hello"}, - "w": String{Unknown: true}, + "key1": String{Value: "hello"}, + "key2": String{Value: "world"}, }, }, expected: true, }, - "partially-known-value-diff": { + "deprecated-known-deprecated-known-diff-value": { receiver: Map{ ElemType: StringType, Elems: map[string]attr.Value{ - "h": String{Value: "hello"}, - "w": String{Unknown: true}, + "key1": String{Value: "hello"}, + "key2": String{Value: "world"}, }, }, input: Map{ ElemType: StringType, Elems: map[string]attr.Value{ - "h": String{Value: "hello"}, - "w": String{Value: "world"}, + "key1": String{Value: "goodnight"}, + "key2": String{Value: "moon"}, }, }, expected: false, }, - "partially-known-map-value-unknown": { + "deprecated-known-deprecated-known-diff-length": { receiver: Map{ ElemType: StringType, Elems: map[string]attr.Value{ - "h": String{Value: "hello"}, - "w": String{Unknown: true}, + "key1": String{Value: "hello"}, + "key2": String{Value: "world"}, }, }, - input: Map{Unknown: true}, - expected: false, - }, - "partially-known-map-value-null": { - receiver: Map{ + input: Map{ ElemType: StringType, Elems: map[string]attr.Value{ - "h": String{Value: "hello"}, - "w": String{Unknown: true}, + "key1": String{Value: "hello"}, + "key2": String{Value: "world"}, + "key3": String{Value: "test"}, }, }, - input: Map{Null: true}, expected: false, }, - "partially-known-map-value-wrongType": { + "deprecated-known-deprecated-known-diff-type": { receiver: Map{ ElemType: StringType, Elems: map[string]attr.Value{ - "h": String{Value: "hello"}, - "w": String{Unknown: true}, + "key1": String{Value: "hello"}, + "key2": String{Value: "world"}, + }, + }, + input: Map{ + ElemType: BoolType, + Elems: map[string]attr.Value{ + "key1": Bool{Value: false}, + "key2": Bool{Value: true}, }, }, - input: String{Value: "hello, world"}, expected: false, }, - "partially-known-map-value-nil": { + "deprecated-known-deprecated-known-diff-unknown": { receiver: Map{ ElemType: StringType, Elems: map[string]attr.Value{ - "h": String{Value: "hello"}, - "w": String{Unknown: true}, + "key1": String{Value: "hello"}, + "key2": String{Unknown: true}, + }, + }, + input: Map{ + ElemType: StringType, + Elems: map[string]attr.Value{ + "key1": String{Value: "hello"}, + "key2": String{Value: "world"}, }, }, - input: nil, expected: false, }, - "partially-null-map-value-map-value": { + "deprecated-known-deprecated-known-diff-null": { receiver: Map{ ElemType: StringType, Elems: map[string]attr.Value{ - "h": String{Value: "hello"}, - "w": String{Null: true}, + "key1": String{Value: "hello"}, + "key2": String{Null: true}, }, }, input: Map{ ElemType: StringType, Elems: map[string]attr.Value{ - "h": String{Value: "hello"}, - "w": String{Null: true}, + "key1": String{Value: "hello"}, + "key2": String{Value: "world"}, }, }, - expected: true, + expected: false, }, - "partially-null-map-value-diff": { + "deprecated-known-deprecated-unknown": { receiver: Map{ ElemType: StringType, Elems: map[string]attr.Value{ - "h": String{Value: "hello"}, - "w": String{Null: true}, + "key1": String{Value: "hello"}, + "key2": String{Value: "world"}, }, }, - input: Map{ + input: Map{Unknown: true}, + expected: false, + }, + "deprecated-known-deprecated-null": { + receiver: Map{ ElemType: StringType, Elems: map[string]attr.Value{ - "h": String{Value: "hello"}, - "w": String{Value: "world"}, + "key1": String{Value: "hello"}, + "key2": String{Value: "world"}, }, }, + input: Map{Null: true}, expected: false, }, - "partially-null-map-value-unknown": { + "deprecated-known-known": { receiver: Map{ ElemType: StringType, Elems: map[string]attr.Value{ - "h": String{Value: "hello"}, - "w": String{Null: true}, + "key1": String{Value: "hello"}, + "key2": String{Value: "world"}, }, }, - input: Map{ - Unknown: true, + input: MapValue( + StringType, + map[string]attr.Value{ + "key1": StringValue("hello"), + "key2": StringValue("world"), + }, + ), + expected: false, // intentional + }, + "deprecated-known-unknown": { + receiver: Map{ + ElemType: StringType, + Elems: map[string]attr.Value{ + "key1": String{Value: "hello"}, + "key2": String{Value: "world"}, + }, }, + input: MapUnknown(StringType), expected: false, }, - "partially-null-map-value-null": { + "deprecated-known-null": { receiver: Map{ ElemType: StringType, Elems: map[string]attr.Value{ - "h": String{Value: "hello"}, - "w": String{Null: true}, + "key1": String{Value: "hello"}, + "key2": String{Value: "world"}, }, }, - input: Map{ - Null: true, - }, + input: MapNull(StringType), expected: false, }, - "partially-null-map-value-wrongType": { + "deprecated-known-diff-type": { receiver: Map{ ElemType: StringType, Elems: map[string]attr.Value{ - "h": String{Value: "hello"}, - "w": String{Null: true}, + "key1": String{Value: "hello"}, + "key2": String{Value: "world"}, }, }, input: String{Value: "hello, world"}, expected: false, }, - "partially-null-map-value-nil": { + "deprecated-known-nil": { receiver: Map{ ElemType: StringType, Elems: map[string]attr.Value{ - "h": String{Value: "hello"}, - "w": String{Null: true}, + "key1": String{Value: "hello"}, + "key2": String{Value: "world"}, }, }, input: nil, @@ -701,6 +1025,110 @@ func TestMapEqual(t *testing.T) { } } +func TestMapIsNull(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + input Map + expected bool + }{ + "known": { + input: MapValue(StringType, map[string]attr.Value{"test-key": StringValue("test-value")}), + expected: false, + }, + "deprecated-known": { + input: Map{ElemType: StringType, Elems: map[string]attr.Value{"test-key": StringValue("test-value")}}, + expected: false, + }, + "null": { + input: MapNull(StringType), + expected: true, + }, + "deprecated-null": { + input: Map{ElemType: StringType, Null: true}, + expected: true, + }, + "unknown": { + input: MapUnknown(StringType), + expected: false, + }, + "deprecated-unknown": { + input: Map{ElemType: StringType, Unknown: true}, + expected: false, + }, + "deprecated-invalid": { + input: Map{ElemType: StringType, Null: true, Unknown: true}, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.input.IsNull() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestMapIsUnknown(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + input Map + expected bool + }{ + "known": { + input: MapValue(StringType, map[string]attr.Value{"test-key": StringValue("test-value")}), + expected: false, + }, + "deprecated-known": { + input: Map{ElemType: StringType, Elems: map[string]attr.Value{"test-key": StringValue("test-value")}}, + expected: false, + }, + "null": { + input: MapNull(StringType), + expected: false, + }, + "deprecated-null": { + input: Map{ElemType: StringType, Null: true}, + expected: false, + }, + "unknown": { + input: MapUnknown(StringType), + expected: true, + }, + "deprecated-unknown": { + input: Map{ElemType: StringType, Unknown: true}, + expected: true, + }, + "deprecated-invalid": { + input: Map{ElemType: StringType, Null: true, Unknown: true}, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.input.IsUnknown() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestMapString(t *testing.T) { t.Parallel() @@ -709,7 +1137,64 @@ func TestMapString(t *testing.T) { expectation string } tests := map[string]testCase{ - "simple": { + "known": { + input: MapValue( + Int64Type, + map[string]attr.Value{ + "alpha": Int64{Value: 1234}, + "beta": Int64{Value: 56789}, + "gamma": Int64{Value: 9817}, + "sigma": Int64{Value: 62534}, + }, + ), + expectation: `{"alpha":1234,"beta":56789,"gamma":9817,"sigma":62534}`, + }, + "known-map-of-maps": { + input: MapValue( + MapType{ + ElemType: StringType, + }, + map[string]attr.Value{ + "first": Map{ + ElemType: StringType, + Elems: map[string]attr.Value{ + "alpha": String{Value: "hello"}, + "beta": String{Value: "world"}, + "gamma": String{Value: "foo"}, + "sigma": String{Value: "bar"}, + }, + }, + "second": Map{ + ElemType: Int64Type, + Elems: map[string]attr.Value{ + "x": Int64{Value: 0}, + "y": Int64{Value: 0}, + "z": Int64{Value: 0}, + "t": Int64{Value: 0}, + }, + }, + }, + ), + expectation: `{"first":{"alpha":"hello","beta":"world","gamma":"foo","sigma":"bar"},"second":{"t":0,"x":0,"y":0,"z":0}}`, + }, + "known-key-quotes": { + input: MapValue( + BoolType, + map[string]attr.Value{ + `testing is "fun"`: Bool{Value: true}, + }, + ), + expectation: `{"testing is \"fun\"":true}`, + }, + "unknown": { + input: MapUnknown(StringType), + expectation: "", + }, + "null": { + input: MapNull(StringType), + expectation: "", + }, + "deprecated-known": { input: Map{ ElemType: Int64Type, Elems: map[string]attr.Value{ @@ -721,7 +1206,7 @@ func TestMapString(t *testing.T) { }, expectation: `{"alpha":1234,"beta":56789,"gamma":9817,"sigma":62534}`, }, - "map-of-maps": { + "deprecated-known-map-of-maps": { input: Map{ ElemType: MapType{ ElemType: StringType, @@ -749,7 +1234,7 @@ func TestMapString(t *testing.T) { }, expectation: `{"first":{"alpha":"hello","beta":"world","gamma":"foo","sigma":"bar"},"second":{"t":0,"x":0,"y":0,"z":0}}`, }, - "key-quotes": { + "deprecated-known-key-quotes": { input: Map{ ElemType: BoolType, Elems: map[string]attr.Value{ @@ -758,11 +1243,11 @@ func TestMapString(t *testing.T) { }, expectation: `{"testing is \"fun\"":true}`, }, - "unknown": { + "deprecated-unknown": { input: Map{Unknown: true}, expectation: "", }, - "null": { + "deprecated-null": { input: Map{Null: true}, expectation: "", }, @@ -785,6 +1270,121 @@ func TestMapString(t *testing.T) { } } +func TestMapType(t *testing.T) { + t.Parallel() + + type testCase struct { + input Map + expectation attr.Type + } + tests := map[string]testCase{ + "known": { + input: MapValue( + StringType, + map[string]attr.Value{ + "key1": StringValue("hello"), + "key2": StringValue("world"), + }, + ), + expectation: MapType{ElemType: StringType}, + }, + "known-map-of-maps": { + input: MapValue( + MapType{ + ElemType: StringType, + }, + map[string]attr.Value{ + "key1": MapValue( + StringType, + map[string]attr.Value{ + "key1": StringValue("hello"), + "key2": StringValue("world"), + }, + ), + "key2": MapValue( + StringType, + map[string]attr.Value{ + "key1": StringValue("foo"), + "key2": StringValue("bar"), + }, + ), + }, + ), + expectation: MapType{ + ElemType: MapType{ + ElemType: StringType, + }, + }, + }, + "unknown": { + input: MapUnknown(StringType), + expectation: MapType{ElemType: StringType}, + }, + "null": { + input: MapNull(StringType), + expectation: MapType{ElemType: StringType}, + }, + "deprecated-known": { + input: Map{ + ElemType: StringType, + Elems: map[string]attr.Value{ + "key1": String{Value: "hello"}, + "key2": String{Value: "world"}, + }, + }, + expectation: MapType{ElemType: StringType}, + }, + "deprecated-known-list-of-lists": { + input: Map{ + ElemType: MapType{ + ElemType: StringType, + }, + Elems: map[string]attr.Value{ + "key1": Map{ + ElemType: StringType, + Elems: map[string]attr.Value{ + "key1": String{Value: "hello"}, + "key2": String{Value: "world"}, + }, + }, + "key2": Map{ + ElemType: StringType, + Elems: map[string]attr.Value{ + "key1": String{Value: "foo"}, + "key2": String{Value: "bar"}, + }, + }, + }, + }, + expectation: MapType{ + ElemType: MapType{ + ElemType: StringType, + }, + }, + }, + "deprecated-unknown": { + input: Map{ElemType: StringType, Unknown: true}, + expectation: MapType{ElemType: StringType}, + }, + "deprecated-null": { + input: Map{ElemType: StringType, Null: true}, + expectation: MapType{ElemType: StringType}, + }, + } + + for name, test := range tests { + name, test := name, test + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := test.input.Type(context.Background()) + if !cmp.Equal(got, test.expectation) { + t.Errorf("Expected %q, got %q", test.expectation, got) + } + }) + } +} + func TestMapTypeValidate(t *testing.T) { t.Parallel() diff --git a/types/number.go b/types/number.go index 478470010..95bb501c6 100644 --- a/types/number.go +++ b/types/number.go @@ -2,6 +2,7 @@ package types import ( "context" + "fmt" "math/big" "github.com/hashicorp/terraform-plugin-framework/attr" @@ -12,34 +13,107 @@ var ( _ attr.Value = Number{} ) +// NumberNull creates a Number with a null value. Determine whether the value is +// null via the Number type IsNull method. +// +// Setting the deprecated Number type Null, Unknown, or Value fields after +// creating a Number with this function has no effect. +func NumberNull() Number { + return Number{ + state: valueStateNull, + } +} + +// NumberUnknown creates a Number with an unknown value. Determine whether the +// value is unknown via the Number type IsUnknown method. +// +// Setting the deprecated Number type Null, Unknown, or Value fields after +// creating a Number with this function has no effect. +func NumberUnknown() Number { + return Number{ + state: valueStateUnknown, + } +} + +// NumberValue creates a Number with a known value. Access the value via the Number +// type ValueBigFloat method. If the given value is nil, a null Number is created. +// +// Setting the deprecated Number type Null, Unknown, or Value fields after +// creating a Number with this function has no effect. +func NumberValue(value *big.Float) Number { + if value == nil { + return NumberNull() + } + + return Number{ + state: valueStateKnown, + value: value, + } +} + func numberValueFromTerraform(ctx context.Context, in tftypes.Value) (attr.Value, error) { if !in.IsKnown() { - return Number{Unknown: true}, nil + return Number{ + Unknown: true, + state: valueStateDeprecated, + }, nil } if in.IsNull() { - return Number{Null: true}, nil + return Number{ + Null: true, + state: valueStateDeprecated, + }, nil } n := big.NewFloat(0) err := in.As(&n) if err != nil { return nil, err } - return Number{Value: n}, nil + return Number{ + Value: n, + state: valueStateDeprecated, + }, nil } // Number represents a number value, exposed as a *big.Float. Numbers can be // floats or integers. type Number struct { // Unknown will be true if the value is not yet known. + // + // If the Number was created with the NumberValue, NumberNull, or NumberUnknown + // functions, changing this field has no effect. + // + // Deprecated: Use the NumberUnknown function to create an unknown Number + // value or use the IsUnknown method to determine whether the Number value + // is unknown instead. Unknown bool // Null will be true if the value was not set, or was explicitly set to // null. + // + // If the Number was created with the NumberValue, NumberNull, or NumberUnknown + // functions, changing this field has no effect. + // + // Deprecated: Use the NumberNull function to create a null Number value or + // use the IsNull method to determine whether the Number value is null + // instead. Null bool // Value contains the set value, as long as Unknown and Null are both // false. + // + // If the Number was created with the NumberValue, NumberNull, or NumberUnknown + // functions, changing this field has no effect. + // + // Deprecated: Use the NumberValue function to create a known Number value or + // use the ValueBigFloat method to retrieve the Number value instead. Value *big.Float + + // state represents whether the Number is null, unknown, or known. + state valueState + + // value contains the known value, if not null or unknown. + value *big.Float } // Type returns a NumberType. @@ -49,19 +123,38 @@ func (n Number) Type(_ context.Context) attr.Type { // ToTerraformValue returns the data contained in the Number as a tftypes.Value. func (n Number) ToTerraformValue(_ context.Context) (tftypes.Value, error) { - if n.Null { + switch n.state { + case valueStateDeprecated: + if n.Null { + return tftypes.NewValue(tftypes.Number, nil), nil + } + if n.Unknown { + return tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), nil + } + if n.Value == nil { + return tftypes.NewValue(tftypes.Number, nil), nil + } + if err := tftypes.ValidateValue(tftypes.Number, n.Value); err != nil { + return tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), err + } + return tftypes.NewValue(tftypes.Number, n.Value), nil + case valueStateKnown: + if n.value == nil { + return tftypes.NewValue(tftypes.Number, nil), nil + } + + if err := tftypes.ValidateValue(tftypes.Number, n.value); err != nil { + return tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), err + } + + return tftypes.NewValue(tftypes.Number, n.value), nil + case valueStateNull: return tftypes.NewValue(tftypes.Number, nil), nil - } - if n.Unknown { + case valueStateUnknown: return tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), nil + default: + panic(fmt.Sprintf("unhandled Number state in ToTerraformValue: %s", n.state)) } - if n.Value == nil { - return tftypes.NewValue(tftypes.Number, nil), nil - } - if err := tftypes.ValidateValue(tftypes.Number, n.Value); err != nil { - return tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), err - } - return tftypes.NewValue(tftypes.Number, n.Value), nil } // Equal returns true if `other` is a Number and has the same value as `n`. @@ -70,6 +163,12 @@ func (n Number) Equal(other attr.Value) bool { if !ok { return false } + if n.state != o.state { + return false + } + if n.state == valueStateKnown { + return n.value.Cmp(o.value) == 0 + } if n.Unknown != o.Unknown { return false } @@ -87,19 +186,31 @@ func (n Number) Equal(other attr.Value) bool { // IsNull returns true if the Number represents a null value. func (n Number) IsNull() bool { - return n.Null || (!n.Unknown && n.Value == nil) + if n.state == valueStateNull { + return true + } + + if n.state == valueStateDeprecated && n.Null { + return true + } + + return n.state == valueStateDeprecated && (!n.Unknown && n.Value == nil) } // IsUnknown returns true if the Number represents a currently unknown value. func (n Number) IsUnknown() bool { - return n.Unknown + if n.state == valueStateUnknown { + return true + } + + return n.state == valueStateDeprecated && n.Unknown } // String returns a human-readable representation of the Number value. // The string returned here is not protected by any compatibility guarantees, // and is intended for logging and error reporting. func (n Number) String() string { - if n.Unknown { + if n.IsUnknown() { return attr.UnknownValueString } @@ -107,5 +218,19 @@ func (n Number) String() string { return attr.NullValueString } + if n.state == valueStateKnown { + return n.value.String() + } + return n.Value.String() } + +// ValueBigFloat returns the known *big.Float value. If Number is null or unknown, returns +// 0.0. +func (n Number) ValueBigFloat() *big.Float { + if n.state == valueStateDeprecated { + return n.Value + } + + return n.value +} diff --git a/types/number_test.go b/types/number_test.go index ae6b35669..eb88ef4aa 100644 --- a/types/number_test.go +++ b/types/number_test.go @@ -15,6 +15,84 @@ func numberComparer(i, j *big.Float) bool { return (i == nil && j == nil) || (i != nil && j != nil && i.Cmp(j) == 0) } +// This test verifies the assumptions that creating the Value via function then +// setting the fields directly has no effects. +func TestNumberValueDeprecatedFieldSetting(t *testing.T) { + t.Parallel() + + knownNumber := NumberValue(big.NewFloat(2.4)) + + knownNumber.Null = true + + if knownNumber.IsNull() { + t.Error("unexpected null update after Null field setting") + } + + knownNumber.Unknown = true + + if knownNumber.IsUnknown() { + t.Error("unexpected unknown update after Unknown field setting") + } + + knownNumber.Value = big.NewFloat(4.8) + + if knownNumber.ValueBigFloat().Cmp(big.NewFloat(4.8)) == 0 { + t.Error("unexpected value update after Value field setting") + } +} + +// This test verifies the assumptions that creating the Value via function then +// setting the fields directly has no effects. +func TestNumberNullDeprecatedFieldSetting(t *testing.T) { + t.Parallel() + + nullNumber := NumberNull() + + nullNumber.Null = false + + if !nullNumber.IsNull() { + t.Error("unexpected null update after Null field setting") + } + + nullNumber.Unknown = true + + if nullNumber.IsUnknown() { + t.Error("unexpected unknown update after Unknown field setting") + } + + nullNumber.Value = big.NewFloat(4.8) + + if nullNumber.ValueBigFloat() != nil { + t.Error("unexpected value update after Value field setting") + } +} + +// This test verifies the assumptions that creating the Value via function then +// setting the fields directly has no effects. +func TestNumberUnknownDeprecatedFieldSetting(t *testing.T) { + t.Parallel() + + unknownNumber := NumberUnknown() + + unknownNumber.Null = true + + if unknownNumber.IsNull() { + t.Error("unexpected null update after Null field setting") + } + + unknownNumber.Unknown = false + + if !unknownNumber.IsUnknown() { + t.Error("unexpected unknown update after Unknown field setting") + } + + unknownNumber.Value = big.NewFloat(4.8) + + if unknownNumber.ValueBigFloat() != nil { + t.Error("unexpected value update after Value field setting") + } +} + func TestNumberValueFromTerraform(t *testing.T) { t.Parallel() @@ -95,18 +173,34 @@ func TestNumberToTerraformValue(t *testing.T) { } tests := map[string]testCase{ "value": { + input: NumberValue(big.NewFloat(123)), + expectation: tftypes.NewValue(tftypes.Number, big.NewFloat(123)), + }, + "known-nil": { + input: NumberValue(nil), + expectation: tftypes.NewValue(tftypes.Number, nil), + }, + "unknown": { + input: NumberUnknown(), + expectation: tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), + }, + "null": { + input: NumberNull(), + expectation: tftypes.NewValue(tftypes.Number, nil), + }, + "deprecated-value": { input: Number{Value: big.NewFloat(123)}, expectation: tftypes.NewValue(tftypes.Number, big.NewFloat(123)), }, - "value-nil": { + "deprecated-known-nil": { input: Number{Value: nil}, expectation: tftypes.NewValue(tftypes.Number, nil), }, - "unknown": { + "deprecated-unknown": { input: Number{Unknown: true}, expectation: tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), }, - "null": { + "deprecated-null": { input: Number{Null: true}, expectation: tftypes.NewValue(tftypes.Number, nil), }, @@ -138,97 +232,242 @@ func TestNumberEqual(t *testing.T) { expectation bool } tests := map[string]testCase{ - "value-value-same": { + "known-known-same": { + input: NumberValue(big.NewFloat(123)), + candidate: NumberValue(big.NewFloat(123)), + expectation: true, + }, + "known-known-diff": { + input: NumberValue(big.NewFloat(123)), + candidate: NumberValue(big.NewFloat(456)), + expectation: false, + }, + "known-nil-known": { + input: NumberValue(nil), + candidate: NumberValue(big.NewFloat(456)), + expectation: false, + }, + "known-nil-null": { + input: NumberValue(nil), + candidate: NumberNull(), + expectation: true, + }, + "known-unknown": { + input: NumberValue(big.NewFloat(123)), + candidate: NumberUnknown(), + expectation: false, + }, + "known-null": { + input: NumberValue(big.NewFloat(123)), + candidate: NumberNull(), + expectation: false, + }, + "known-wrong-type": { + input: NumberValue(big.NewFloat(123)), + candidate: Float64Value(123), + expectation: false, + }, + "known-nil": { + input: NumberValue(big.NewFloat(123)), + candidate: nil, + expectation: false, + }, + "unknown-known": { + input: NumberUnknown(), + candidate: NumberValue(big.NewFloat(123)), + expectation: false, + }, + "unknown-unknown": { + input: NumberUnknown(), + candidate: NumberUnknown(), + expectation: true, + }, + "unknown-null": { + input: NumberUnknown(), + candidate: NumberNull(), + expectation: false, + }, + "unknown-wrong-type": { + input: NumberUnknown(), + candidate: Float64Unknown(), + expectation: false, + }, + "unknown-nil": { + input: NumberUnknown(), + candidate: nil, + expectation: false, + }, + "null-known": { + input: NumberNull(), + candidate: NumberValue(big.NewFloat(123)), + expectation: false, + }, + "null-known-nil": { + input: NumberNull(), + candidate: NumberValue(nil), + expectation: true, + }, + "null-unknown": { + input: NumberNull(), + candidate: NumberUnknown(), + expectation: false, + }, + "null-null": { + input: NumberNull(), + candidate: NumberNull(), + expectation: true, + }, + "null-wrong-type": { + input: NumberNull(), + candidate: Float64Null(), + expectation: false, + }, + "null-nil": { + input: NumberNull(), + candidate: nil, + expectation: false, + }, + "deprecated-known-known-same": { + input: Number{Value: big.NewFloat(123)}, + candidate: NumberValue(big.NewFloat(123)), + expectation: false, // intentional + }, + "deprecated-known-known-diff": { + input: Number{Value: big.NewFloat(123)}, + candidate: NumberValue(big.NewFloat(456)), + expectation: false, + }, + "deprecated-known-unknown": { + input: Number{Value: big.NewFloat(123)}, + candidate: NumberNull(), + expectation: false, + }, + "deprecated-known-null": { + input: Number{Value: big.NewFloat(123)}, + candidate: NumberNull(), + expectation: false, + }, + "deprecated-known-deprecated-known-same": { input: Number{Value: big.NewFloat(123)}, candidate: Number{Value: big.NewFloat(123)}, expectation: true, }, - "value-value-diff": { + "deprecated-known-deprecated-known-diff": { input: Number{Value: big.NewFloat(123)}, candidate: Number{Value: big.NewFloat(456)}, expectation: false, }, - "value-unknown": { + "deprecated-known-deprecated-unknown": { input: Number{Value: big.NewFloat(123)}, candidate: Number{Unknown: true}, expectation: false, }, - "value-null": { + "deprecated-known-deprecated-null": { input: Number{Value: big.NewFloat(123)}, candidate: Number{Null: true}, expectation: false, }, - "value-wrongType": { + "deprecated-known-wrongType": { input: Number{Value: big.NewFloat(123)}, candidate: &String{Value: "oops"}, expectation: false, }, - "value-nil": { + "deprecated-known-nil": { input: Number{Value: big.NewFloat(123)}, candidate: nil, expectation: false, }, - "value-nilValue": { + "deprecated-known-nilValue": { input: Number{Value: big.NewFloat(123)}, candidate: Number{Value: nil}, expectation: false, }, - "unknown-value": { + "deprecated-unknown-known": { + input: Number{Unknown: true}, + candidate: NumberValue(big.NewFloat(123)), + expectation: false, + }, + "deprecated-unknown-unknown": { + input: Number{Unknown: true}, + candidate: NumberUnknown(), + expectation: false, // intentional + }, + "deprecated-unknown-null": { + input: Number{Unknown: true}, + candidate: NumberNull(), + expectation: false, + }, + "deprecated-unknown-deprecated-known": { input: Number{Unknown: true}, candidate: Number{Value: big.NewFloat(123)}, expectation: false, }, - "unknown-unknown": { + "deprecated-unknown-deprecated-unknown": { input: Number{Unknown: true}, candidate: Number{Unknown: true}, expectation: true, }, - "unknown-null": { + "deprecated-unknown-deprecated-null": { input: Number{Unknown: true}, candidate: Number{Null: true}, expectation: false, }, - "unknown-wrongType": { + "deprecated-unknown-wrongType": { input: Number{Unknown: true}, candidate: &String{Value: "oops"}, expectation: false, }, - "unknown-nil": { + "deprecated-unknown-nil": { input: Number{Unknown: true}, candidate: nil, expectation: false, }, - "unknown-nilValue": { + "deprecated-unknown-nilValue": { input: Number{Unknown: true}, candidate: Number{Value: nil}, expectation: false, }, - "null-value": { + "deprecated-null-value": { + input: Number{Null: true}, + candidate: NumberValue(big.NewFloat(123)), + expectation: false, + }, + "deprecated-null-unknown": { + input: Number{Null: true}, + candidate: NumberUnknown(), + expectation: false, + }, + "deprecated-null-null": { + input: Number{Null: true}, + candidate: NumberNull(), + expectation: false, // intentional + }, + "deprecated-null-deprecated-value": { input: Number{Null: true}, candidate: Number{Value: big.NewFloat(123)}, expectation: false, }, - "null-unknown": { + "deprecated-null-deprecated-unknown": { input: Number{Null: true}, candidate: Number{Unknown: true}, expectation: false, }, - "null-null": { + "deprecated-null-deprecated-null": { input: Number{Null: true}, candidate: Number{Null: true}, expectation: true, }, - "null-wrongType": { + "deprecated-null-wrongType": { input: Number{Null: true}, candidate: &String{Value: "oops"}, expectation: false, }, - "null-nil": { + "deprecated-null-nil": { input: Number{Null: true}, candidate: nil, expectation: false, }, - "null-nilValue": { + "deprecated-null-nilValue": { input: Number{Null: true}, candidate: Number{Value: nil}, expectation: false, @@ -247,6 +486,110 @@ func TestNumberEqual(t *testing.T) { } } +func TestNumberIsNull(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + input Number + expected bool + }{ + "known": { + input: NumberValue(big.NewFloat(2.4)), + expected: false, + }, + "deprecated-known": { + input: Number{Value: big.NewFloat(2.4)}, + expected: false, + }, + "null": { + input: NumberNull(), + expected: true, + }, + "deprecated-null": { + input: Number{Null: true}, + expected: true, + }, + "unknown": { + input: NumberUnknown(), + expected: false, + }, + "deprecated-unknown": { + input: Number{Unknown: true}, + expected: false, + }, + "deprecated-invalid": { + input: Number{Null: true, Unknown: true}, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.input.IsNull() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNumberIsUnknown(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + input Number + expected bool + }{ + "known": { + input: NumberValue(big.NewFloat(2.4)), + expected: false, + }, + "deprecated-known": { + input: Number{Value: big.NewFloat(2.4)}, + expected: false, + }, + "null": { + input: NumberNull(), + expected: false, + }, + "deprecated-null": { + input: Number{Null: true}, + expected: false, + }, + "unknown": { + input: NumberUnknown(), + expected: true, + }, + "deprecated-unknown": { + input: Number{Unknown: true}, + expected: true, + }, + "deprecated-invalid": { + input: Number{Null: true, Unknown: true}, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.input.IsUnknown() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestNumberString(t *testing.T) { t.Parallel() @@ -255,35 +598,67 @@ func TestNumberString(t *testing.T) { expectation string } tests := map[string]testCase{ - "less-than-one": { + "known-less-than-one": { + input: NumberValue(big.NewFloat(0.12340984302980000)), + expectation: "0.123409843", + }, + "known-more-than-one": { + input: NumberValue(big.NewFloat(92387938173219.327663)), + expectation: "9.238793817e+13", + }, + "known-negative-more-than-one": { + input: NumberValue(big.NewFloat(-0.12340984302980000)), + expectation: "-0.123409843", + }, + "known-negative-less-than-one": { + input: NumberValue(big.NewFloat(-92387938173219.327663)), + expectation: "-9.238793817e+13", + }, + "known-min-float64": { + input: NumberValue(big.NewFloat(math.SmallestNonzeroFloat64)), + expectation: "4.940656458e-324", + }, + "known-max-float64": { + input: NumberValue(big.NewFloat(math.MaxFloat64)), + expectation: "1.797693135e+308", + }, + "unknown": { + input: NumberUnknown(), + expectation: "", + }, + "null": { + input: NumberNull(), + expectation: "", + }, + "deprecated-known-less-than-one": { input: Number{Value: big.NewFloat(0.12340984302980000)}, expectation: "0.123409843", }, - "more-than-one": { + "deprecated-known-more-than-one": { input: Number{Value: big.NewFloat(92387938173219.327663)}, expectation: "9.238793817e+13", }, - "negative-more-than-one": { + "deprecated-known-negative-more-than-one": { input: Number{Value: big.NewFloat(-0.12340984302980000)}, expectation: "-0.123409843", }, - "negative-less-than-one": { + "deprecated-known-negative-less-than-one": { input: Number{Value: big.NewFloat(-92387938173219.327663)}, expectation: "-9.238793817e+13", }, - "min-float64": { + "deprecated-known-min-float64": { input: Number{Value: big.NewFloat(math.SmallestNonzeroFloat64)}, expectation: "4.940656458e-324", }, - "max-float64": { + "deprecated-known-max-float64": { input: Number{Value: big.NewFloat(math.MaxFloat64)}, expectation: "1.797693135e+308", }, - "unknown": { + "deprecated-unknown": { input: Number{Unknown: true}, expectation: "", }, - "null": { + "deprecated-null": { input: Number{Null: true}, expectation: "", }, @@ -305,3 +680,69 @@ func TestNumberString(t *testing.T) { }) } } + +func TestNumberValueBigFloat(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + input Number + expected *big.Float + }{ + "known": { + input: NumberValue(big.NewFloat(2.4)), + expected: big.NewFloat(2.4), + }, + "known-nil": { + input: NumberValue(nil), + expected: nil, + }, + "deprecated-known": { + input: Number{Value: big.NewFloat(2.4)}, + expected: big.NewFloat(2.4), + }, + "null": { + input: NumberNull(), + expected: nil, + }, + "deprecated-null": { + input: Number{Null: true}, + expected: nil, + }, + "unknown": { + input: NumberUnknown(), + expected: nil, + }, + "deprecated-unknown": { + input: Number{Unknown: true}, + expected: nil, + }, + "deprecated-invalid": { + input: Number{Null: true, Unknown: true}, + expected: nil, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.input.ValueBigFloat() + + if got == nil && testCase.expected != nil { + t.Fatalf("got nil, expected: %s", testCase.expected) + } + + if got != nil { + if testCase.expected == nil { + t.Fatalf("expected nil, got: %s", got) + } + + if got.Cmp(testCase.expected) != 0 { + t.Fatalf("expected %s, got: %s", testCase.expected, got) + } + } + }) + } +} diff --git a/types/object.go b/types/object.go index 1779dff96..0899e96ee 100644 --- a/types/object.go +++ b/types/object.go @@ -152,6 +152,43 @@ func (t ObjectType) ValueType(_ context.Context) attr.Value { } } +// ObjectNull creates a Object with a null value. Determine whether the value is +// null via the Object type IsNull method. +// +// Setting the deprecated Object type AttrTypes, Attrs, Null, or Unknown fields +// after creating a Object with this function has no effect. +func ObjectNull(attributeTypes map[string]attr.Type) Object { + return Object{ + attributeTypes: attributeTypes, + state: valueStateNull, + } +} + +// ObjectUnknown creates a Object with an unknown value. Determine whether the +// value is unknown via the Object type IsUnknown method. +// +// Setting the deprecated Object type AttrTypes, Attrs, Null, or Unknown fields +// after creating a Object with this function has no effect. +func ObjectUnknown(attributeTypes map[string]attr.Type) Object { + return Object{ + attributeTypes: attributeTypes, + state: valueStateUnknown, + } +} + +// ObjectValue creates a Object with a known value. Access the value via the Object +// type ElementsAs method. +// +// Setting the deprecated Object type AttrTypes, Attrs, Null, or Unknown fields +// after creating a Object with this function has no effect. +func ObjectValue(attributeTypes map[string]attr.Type, attributes map[string]attr.Value) Object { + return Object{ + attributeTypes: attributeTypes, + attributes: attributes, + state: valueStateKnown, + } +} + // Object represents an object type Object struct { // Unknown will be set to true if the entire object is an unknown value. @@ -160,16 +197,53 @@ type Object struct { // surfaces that information. The Object's Unknown property only tracks // if the number of elements in a Object is known, not whether the // elements that are in the object are known. + // + // If the Object was created with the ObjectValue, ObjectNull, or ObjectUnknown + // functions, changing this field has no effect. + // + // Deprecated: Use the ObjectNull function to create a null Object value or + // use the IsNull method to determine whether the Object value is null + // instead. Unknown bool // Null will be set to true if the object is null, either because it was // omitted from the configuration, state, or plan, or because it was // explicitly set to null. + // + // If the Object was created with the ObjectValue, ObjectNull, or ObjectUnknown + // functions, changing this field has no effect. + // + // Deprecated: Use the ObjectNull function to create a null Object value or + // use the IsNull method to determine whether the Object value is null + // instead. Null bool + // Attrs is the mapping of known attribute values in the Object. + // + // If the Object was created with the ObjectValue, ObjectNull, or ObjectUnknown + // functions, changing this field has no effect. + // + // Deprecated: Use the ObjectValue function to create a known Object value or + // use the As or Attributes methods to retrieve the Object attributes + // instead. Attrs map[string]attr.Value + // AttrTypes is the mapping of attribute types in the Object. Required + // for a valid Object. + // + // Deprecated: Use the ObjectValue, ObjectNull, or ObjectUnknown functions + // to create a Object or use the AttributeTypes method to retrieve the + // Object attribute types instead. AttrTypes map[string]attr.Type + + // attributes is the mapping of known attribute values in the Object. + attributes map[string]attr.Value + + // attributeTypes is the type of the attributes in the Object. + attributeTypes map[string]attr.Type + + // state represents whether the Object is null, unknown, or known. + state valueState } // ObjectAsOptions is a collection of toggles to control the behavior of @@ -209,41 +283,88 @@ func (o Object) As(ctx context.Context, target interface{}, opts ObjectAsOptions }, path.Empty()) } +// Attributes returns the mapping of known attribute values for the Object. +// Returns nil if the Object is null or unknown. +func (o Object) Attributes() map[string]attr.Value { + if o.state == valueStateDeprecated { + return o.Attrs + } + + return o.attributes +} + +// AttributeTypes returns the mapping of attribute types for the Object. +func (o Object) AttributeTypes(_ context.Context) map[string]attr.Type { + if o.state == valueStateDeprecated { + return o.AttrTypes + } + + return o.attributeTypes +} + // Type returns an ObjectType with the same attribute types as `o`. -func (o Object) Type(_ context.Context) attr.Type { - return ObjectType{AttrTypes: o.AttrTypes} +func (o Object) Type(ctx context.Context) attr.Type { + return ObjectType{AttrTypes: o.AttributeTypes(ctx)} } // ToTerraformValue returns the data contained in the attr.Value as // a tftypes.Value. func (o Object) ToTerraformValue(ctx context.Context) (tftypes.Value, error) { - if o.AttrTypes == nil { + if o.state == valueStateDeprecated && o.AttrTypes == nil { return tftypes.Value{}, fmt.Errorf("cannot convert Object to tftypes.Value if AttrTypes field is not set") } attrTypes := map[string]tftypes.Type{} - for attr, typ := range o.AttrTypes { + for attr, typ := range o.AttributeTypes(ctx) { attrTypes[attr] = typ.TerraformType(ctx) } objectType := tftypes.Object{AttributeTypes: attrTypes} - if o.Unknown { - return tftypes.NewValue(objectType, tftypes.UnknownValue), nil - } - if o.Null { - return tftypes.NewValue(objectType, nil), nil - } - vals := map[string]tftypes.Value{} - for k, v := range o.Attrs { - val, err := v.ToTerraformValue(ctx) - if err != nil { + switch o.state { + case valueStateDeprecated: + if o.Unknown { + return tftypes.NewValue(objectType, tftypes.UnknownValue), nil + } + if o.Null { + return tftypes.NewValue(objectType, nil), nil + } + vals := map[string]tftypes.Value{} + + for k, v := range o.Attrs { + val, err := v.ToTerraformValue(ctx) + if err != nil { + return tftypes.NewValue(objectType, tftypes.UnknownValue), err + } + vals[k] = val + } + if err := tftypes.ValidateValue(objectType, vals); err != nil { return tftypes.NewValue(objectType, tftypes.UnknownValue), err } - vals[k] = val - } - if err := tftypes.ValidateValue(objectType, vals); err != nil { - return tftypes.NewValue(objectType, tftypes.UnknownValue), err + return tftypes.NewValue(objectType, vals), nil + case valueStateKnown: + vals := make(map[string]tftypes.Value, len(o.attributes)) + + for name, v := range o.attributes { + val, err := v.ToTerraformValue(ctx) + + if err != nil { + return tftypes.NewValue(objectType, tftypes.UnknownValue), err + } + + vals[name] = val + } + + if err := tftypes.ValidateValue(objectType, vals); err != nil { + return tftypes.NewValue(objectType, tftypes.UnknownValue), err + } + + return tftypes.NewValue(objectType, vals), nil + case valueStateNull: + return tftypes.NewValue(objectType, nil), nil + case valueStateUnknown: + return tftypes.NewValue(objectType, tftypes.UnknownValue), nil + default: + panic(fmt.Sprintf("unhandled Object state in ToTerraformValue: %s", o.state)) } - return tftypes.NewValue(objectType, vals), nil } // Equal returns true if the Object is considered semantically equal @@ -253,6 +374,44 @@ func (o Object) Equal(c attr.Value) bool { if !ok { return false } + if o.state != other.state { + return false + } + if o.state == valueStateKnown { + if len(o.attributeTypes) != len(other.attributeTypes) { + return false + } + + for name, oAttributeType := range o.attributeTypes { + otherAttributeType, ok := other.attributeTypes[name] + + if !ok { + return false + } + + if !oAttributeType.Equal(otherAttributeType) { + return false + } + } + + if len(o.attributes) != len(other.attributes) { + return false + } + + for name, oAttribute := range o.attributes { + otherAttribute, ok := other.attributes[name] + + if !ok { + return false + } + + if !oAttribute.Equal(otherAttribute) { + return false + } + } + + return true + } if o.Unknown != other.Unknown { return false } @@ -289,29 +448,37 @@ func (o Object) Equal(c attr.Value) bool { // IsNull returns true if the Object represents a null value. func (o Object) IsNull() bool { - return o.Null + if o.state == valueStateNull { + return true + } + + return o.state == valueStateDeprecated && o.Null } // IsUnknown returns true if the Object represents a currently unknown value. func (o Object) IsUnknown() bool { - return o.Unknown + if o.state == valueStateUnknown { + return true + } + + return o.state == valueStateDeprecated && o.Unknown } // String returns a human-readable representation of the Object value. // The string returned here is not protected by any compatibility guarantees, // and is intended for logging and error reporting. func (o Object) String() string { - if o.Unknown { + if o.IsUnknown() { return attr.UnknownValueString } - if o.Null { + if o.IsNull() { return attr.NullValueString } // We want the output to be consistent, so we sort the output by key - keys := make([]string, 0, len(o.Attrs)) - for k := range o.Attrs { + keys := make([]string, 0, len(o.Attributes())) + for k := range o.Attributes() { keys = append(keys, k) } sort.Strings(keys) @@ -323,7 +490,7 @@ func (o Object) String() string { if i != 0 { res.WriteString(",") } - res.WriteString(fmt.Sprintf(`"%s":%s`, k, o.Attrs[k].String())) + res.WriteString(fmt.Sprintf(`"%s":%s`, k, o.Attributes()[k].String())) } res.WriteString("}") diff --git a/types/object_test.go b/types/object_test.go index 04575eea8..42bef77fd 100644 --- a/types/object_test.go +++ b/types/object_test.go @@ -367,6 +367,87 @@ func TestObjectTypeEqual(t *testing.T) { } } +// This test verifies the assumptions that creating the Value via function then +// setting the fields directly has no effects. +func TestObjectValue_DeprecatedFieldSetting(t *testing.T) { + t.Parallel() + + knownObject := ObjectValue( + map[string]attr.Type{"test_attr": StringType}, + map[string]attr.Value{"test_attr": StringValue("test-value")}, + ) + + knownObject.Null = true + + if knownObject.IsNull() { + t.Error("unexpected null update after Null field setting") + } + + knownObject.Unknown = true + + if knownObject.IsUnknown() { + t.Error("unexpected unknown update after Unknown field setting") + } + + knownObject.Attrs = map[string]attr.Value{"test_attr": StringValue("not-test-value")} + + if knownObject.Attributes()["test_attr"].Equal(StringValue("not-test-value")) { + t.Error("unexpected value update after Value field setting") + } +} + +// This test verifies the assumptions that creating the Value via function then +// setting the fields directly has no effects. +func TestObjectNull_DeprecatedFieldSetting(t *testing.T) { + t.Parallel() + + nullObject := ObjectNull(map[string]attr.Type{"test_attr": StringType}) + + nullObject.Null = false + + if !nullObject.IsNull() { + t.Error("unexpected null update after Null field setting") + } + + nullObject.Unknown = true + + if nullObject.IsUnknown() { + t.Error("unexpected unknown update after Unknown field setting") + } + + nullObject.Attrs = map[string]attr.Value{"test_attr": StringValue("test")} + + if len(nullObject.Attributes()) > 0 { + t.Error("unexpected value update after Value field setting") + } +} + +// This test verifies the assumptions that creating the Value via function then +// setting the fields directly has no effects. +func TestObjectUnknown_DeprecatedFieldSetting(t *testing.T) { + t.Parallel() + + unknownObject := ObjectUnknown(map[string]attr.Type{"test_attr": StringType}) + + unknownObject.Null = true + + if unknownObject.IsNull() { + t.Error("unexpected null update after Null field setting") + } + + unknownObject.Unknown = false + + if !unknownObject.IsUnknown() { + t.Error("unexpected unknown update after Unknown field setting") + } + + unknownObject.Attrs = map[string]attr.Value{"test_attr": StringValue("test")} + + if len(unknownObject.Attributes()) > 0 { + t.Error("unexpected value update after Value field setting") + } +} + func TestObjectAs_struct(t *testing.T) { t.Parallel() @@ -628,6 +709,126 @@ func TestObjectAs_struct(t *testing.T) { } } +func TestObjectAttributes(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + input Object + expected map[string]attr.Value + }{ + "known": { + input: ObjectValue( + map[string]attr.Type{"test_attr": StringType}, + map[string]attr.Value{"test_attr": StringValue("test-value")}, + ), + expected: map[string]attr.Value{"test_attr": StringValue("test-value")}, + }, + "deprecated-known": { + input: Object{ + AttrTypes: map[string]attr.Type{"test_attr": StringType}, + Attrs: map[string]attr.Value{"test_attr": StringValue("test-value")}, + }, + expected: map[string]attr.Value{"test_attr": StringValue("test-value")}, + }, + "null": { + input: ObjectNull(map[string]attr.Type{"test_attr": StringType}), + expected: nil, + }, + "deprecated-null": { + input: Object{ + AttrTypes: map[string]attr.Type{"test_attr": StringType}, + Null: true, + }, + expected: nil, + }, + "unknown": { + input: ObjectUnknown(map[string]attr.Type{"test_attr": StringType}), + expected: nil, + }, + "deprecated-unknown": { + input: Object{ + AttrTypes: map[string]attr.Type{"test_attr": StringType}, + Unknown: true, + }, + expected: nil, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.input.Attributes() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestObjectAttributeTypes(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + input Object + expected map[string]attr.Type + }{ + "known": { + input: ObjectValue( + map[string]attr.Type{"test_attr": StringType}, + map[string]attr.Value{"test_attr": StringValue("test-value")}, + ), + expected: map[string]attr.Type{"test_attr": StringType}, + }, + "deprecated-known": { + input: Object{ + AttrTypes: map[string]attr.Type{"test_attr": StringType}, + Attrs: map[string]attr.Value{"test_attr": StringValue("test-value")}, + }, + expected: map[string]attr.Type{"test_attr": StringType}, + }, + "null": { + input: ObjectNull(map[string]attr.Type{"test_attr": StringType}), + expected: map[string]attr.Type{"test_attr": StringType}, + }, + "deprecated-null": { + input: Object{ + AttrTypes: map[string]attr.Type{"test_attr": StringType}, + Null: true, + }, + expected: map[string]attr.Type{"test_attr": StringType}, + }, + "unknown": { + input: ObjectUnknown(map[string]attr.Type{"test_attr": StringType}), + expected: map[string]attr.Type{"test_attr": StringType}, + }, + "deprecated-unknown": { + input: Object{ + AttrTypes: map[string]attr.Type{"test_attr": StringType}, + Unknown: true, + }, + expected: map[string]attr.Type{"test_attr": StringType}, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.input.AttributeTypes(context.Background()) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestObjectToTerraformValue(t *testing.T) { t.Parallel() type testCase struct { @@ -1148,144 +1349,212 @@ func TestObjectEqual(t *testing.T) { expected bool } tests := map[string]testCase{ - "equal": { - receiver: Object{ - AttrTypes: map[string]attr.Type{ + "known-known": { + receiver: ObjectValue( + map[string]attr.Type{ "string": StringType, "bool": BoolType, "number": NumberType, }, - Attrs: map[string]attr.Value{ - "string": String{Value: "hello"}, - "bool": Bool{Value: true}, - "number": Number{Value: big.NewFloat(123)}, + map[string]attr.Value{ + "string": StringValue("test"), + "bool": BoolValue(true), + "number": NumberValue(big.NewFloat(123)), }, - }, - arg: Object{ - AttrTypes: map[string]attr.Type{ + ), + arg: ObjectValue( + map[string]attr.Type{ "string": StringType, "bool": BoolType, "number": NumberType, }, - Attrs: map[string]attr.Value{ - "string": String{Value: "hello"}, - "bool": Bool{Value: true}, - "number": Number{Value: big.NewFloat(123)}, + map[string]attr.Value{ + "string": StringValue("test"), + "bool": BoolValue(true), + "number": NumberValue(big.NewFloat(123)), }, - }, + ), expected: true, }, - "diff": { - receiver: Object{ - AttrTypes: map[string]attr.Type{ + "known-known-diff-value": { + receiver: ObjectValue( + map[string]attr.Type{ "string": StringType, "bool": BoolType, "number": NumberType, }, - Attrs: map[string]attr.Value{ - "string": String{Value: "hello"}, - "bool": Bool{Value: true}, - "number": Number{Value: big.NewFloat(123)}, + map[string]attr.Value{ + "string": StringValue("test"), + "bool": BoolValue(true), + "number": NumberValue(big.NewFloat(123)), }, - }, - arg: Object{ - AttrTypes: map[string]attr.Type{ + ), + arg: ObjectValue( + map[string]attr.Type{ "string": StringType, "bool": BoolType, "number": NumberType, }, - Attrs: map[string]attr.Value{ - "string": String{Value: "world"}, - "bool": Bool{Value: true}, - "number": Number{Value: big.NewFloat(123)}, + map[string]attr.Value{ + "string": StringValue("not-test"), + "bool": BoolValue(true), + "number": NumberValue(big.NewFloat(123)), }, - }, + ), expected: false, }, - "equal-complex": { + "known-known-diff-attribute-types": { receiver: Object{ AttrTypes: map[string]attr.Type{ "string": StringType, - "list": ListType{ElemType: StringType}, }, Attrs: map[string]attr.Value{ - "string": String{Value: "hello"}, - "list": List{ElemType: StringType, Elems: []attr.Value{ - String{Value: "a"}, - String{Value: "b"}, - String{Value: "c"}, - }}, + "string": StringValue("hello"), }, }, arg: Object{ AttrTypes: map[string]attr.Type{ - "string": StringType, - "list": ListType{ElemType: StringType}, + "number": NumberType, }, Attrs: map[string]attr.Value{ - "string": String{Value: "hello"}, - "list": List{ElemType: StringType, Elems: []attr.Value{ - String{Value: "a"}, - String{Value: "b"}, - String{Value: "c"}, - }}, + "number": NumberValue(big.NewFloat(123)), }, }, - expected: true, + expected: false, }, - "diff-complex": { - receiver: Object{ - AttrTypes: map[string]attr.Type{ + "known-known-diff-attribute-count": { + receiver: ObjectValue( + map[string]attr.Type{ "string": StringType, - "list": ListType{ElemType: StringType}, }, - Attrs: map[string]attr.Value{ - "string": String{Value: "hello"}, - "list": List{ElemType: StringType, Elems: []attr.Value{ - String{Value: "a"}, - String{Value: "b"}, - String{Value: "c"}, - }}, + map[string]attr.Value{ + "string": StringValue("hello"), }, - }, - arg: Object{ - AttrTypes: map[string]attr.Type{ + ), + arg: ObjectValue( + map[string]attr.Type{ "string": StringType, "list": ListType{ElemType: StringType}, }, - Attrs: map[string]attr.Value{ - "string": String{Value: "hello"}, - "list": List{ElemType: StringType, Elems: []attr.Value{ - String{Value: "a"}, - String{Value: "b"}, - String{Value: "c"}, - String{Value: "d"}, - }}, + map[string]attr.Value{ + "string": StringValue("hello"), + "list": ListNull(BoolType), }, - }, + ), + expected: false, + }, + "known-known-diff-unknown": { + receiver: ObjectValue( + map[string]attr.Type{ + "string": StringType, + }, + map[string]attr.Value{ + "string": StringValue("hello"), + }, + ), + arg: ObjectValue( + map[string]attr.Type{ + "string": StringType, + }, + map[string]attr.Value{ + "string": StringUnknown(), + }, + ), + expected: false, + }, + "known-known-diff-null": { + receiver: ObjectValue( + map[string]attr.Type{ + "string": StringType, + }, + map[string]attr.Value{ + "string": StringValue("hello"), + }, + ), + arg: ObjectValue( + map[string]attr.Type{ + "string": StringType, + }, + map[string]attr.Value{ + "string": StringNull(), + }, + ), expected: false, }, - "both-unknown": { + "known-unknown": { receiver: Object{ AttrTypes: map[string]attr.Type{ "string": StringType, "bool": BoolType, "number": NumberType, }, - Unknown: true, + Attrs: map[string]attr.Value{ + "string": StringValue("hello"), + "bool": BoolValue(true), + "number": NumberValue(big.NewFloat(123)), + }, }, - arg: Object{ - AttrTypes: map[string]attr.Type{ + arg: ObjectUnknown( + map[string]attr.Type{ "string": StringType, "bool": BoolType, "number": NumberType, }, - Unknown: true, - }, - expected: true, + ), + expected: false, }, - "unknown": { - receiver: Object{ + "known-null": { + receiver: ObjectValue( + map[string]attr.Type{ + "string": StringType, + "bool": BoolType, + "number": NumberType, + }, + map[string]attr.Value{ + "string": StringValue("hello"), + "bool": BoolValue(true), + "number": NumberValue(big.NewFloat(123)), + }, + ), + arg: ObjectNull( + map[string]attr.Type{ + "string": StringType, + "bool": BoolType, + "number": NumberType, + }, + ), + expected: false, + }, + "known-diff-wrong-type": { + receiver: ObjectValue( + map[string]attr.Type{ + "string": StringType, + "bool": BoolType, + "number": NumberType, + }, + map[string]attr.Value{ + "string": StringValue("hello"), + "bool": BoolValue(true), + "number": NumberValue(big.NewFloat(123)), + }, + ), + arg: StringValue("whoops"), + expected: false, + }, + "known-deprecated-known": { + receiver: ObjectValue( + map[string]attr.Type{ + "string": StringType, + "bool": BoolType, + "number": NumberType, + }, + map[string]attr.Value{ + "string": StringValue("hello"), + "bool": BoolValue(true), + "number": NumberValue(big.NewFloat(123)), + }, + ), + arg: Object{ AttrTypes: map[string]attr.Type{ "string": StringType, "bool": BoolType, @@ -1297,6 +1566,21 @@ func TestObjectEqual(t *testing.T) { "number": Number{Value: big.NewFloat(123)}, }, }, + expected: false, // intentional + }, + "known-deprecated-unknown": { + receiver: ObjectValue( + map[string]attr.Type{ + "string": StringType, + "bool": BoolType, + "number": NumberType, + }, + map[string]attr.Value{ + "string": StringValue("hello"), + "bool": BoolValue(true), + "number": NumberValue(big.NewFloat(123)), + }, + ), arg: Object{ AttrTypes: map[string]attr.Type{ "string": StringType, @@ -1307,15 +1591,19 @@ func TestObjectEqual(t *testing.T) { }, expected: false, }, - "both-null": { - receiver: Object{ - AttrTypes: map[string]attr.Type{ + "known-deprecated-null": { + receiver: ObjectValue( + map[string]attr.Type{ "string": StringType, "bool": BoolType, "number": NumberType, }, - Null: true, - }, + map[string]attr.Value{ + "string": StringValue("hello"), + "bool": BoolValue(true), + "number": NumberValue(big.NewFloat(123)), + }, + ), arg: Object{ AttrTypes: map[string]attr.Type{ "string": StringType, @@ -1324,33 +1612,39 @@ func TestObjectEqual(t *testing.T) { }, Null: true, }, - expected: true, + expected: false, }, - "null": { - receiver: Object{ - AttrTypes: map[string]attr.Type{ + "unknown-known": { + receiver: ObjectUnknown( + map[string]attr.Type{ "string": StringType, "bool": BoolType, "number": NumberType, }, - Attrs: map[string]attr.Value{ + ), + arg: ObjectValue( + map[string]attr.Type{ + "string": StringType, + "bool": BoolType, + "number": NumberType, + }, + map[string]attr.Value{ "string": String{Value: "hello"}, "bool": Bool{Value: true}, "number": Number{Value: big.NewFloat(123)}, }, - }, - arg: Object{ - AttrTypes: map[string]attr.Type{ + ), + expected: false, + }, + "unknown-deprecated-known": { + receiver: ObjectUnknown( + map[string]attr.Type{ "string": StringType, "bool": BoolType, "number": NumberType, }, - Null: true, - }, - expected: false, - }, - "wrong-type": { - receiver: Object{ + ), + arg: Object{ AttrTypes: map[string]attr.Type{ "string": StringType, "bool": BoolType, @@ -1362,40 +1656,247 @@ func TestObjectEqual(t *testing.T) { "number": Number{Value: big.NewFloat(123)}, }, }, - arg: String{Value: "whoops"}, expected: false, }, - "wrong-type-complex": { - receiver: Object{ - AttrTypes: map[string]attr.Type{ + "unknown-unknown": { + receiver: ObjectUnknown( + map[string]attr.Type{ "string": StringType, - "list": ListType{ElemType: StringType}, + "bool": BoolType, + "number": NumberType, }, - Attrs: map[string]attr.Value{ - "string": String{Value: "hello"}, - "list": List{ElemType: StringType, Elems: []attr.Value{ - String{Value: "a"}, - String{Value: "b"}, - String{Value: "c"}, - }}, + ), + arg: ObjectUnknown( + map[string]attr.Type{ + "string": StringType, + "bool": BoolType, + "number": NumberType, }, - }, + ), + expected: true, + }, + "unknown-deprecated-unknown": { + receiver: ObjectUnknown( + map[string]attr.Type{ + "string": StringType, + "bool": BoolType, + "number": NumberType, + }, + ), arg: Object{ AttrTypes: map[string]attr.Type{ "string": StringType, - "list": ListType{ElemType: StringType}, + "bool": BoolType, + "number": NumberType, }, - Attrs: map[string]attr.Value{ - "string": String{Value: "hello"}, - "list": List{ElemType: BoolType, Elems: []attr.Value{ - Bool{Value: true}, - Bool{Value: false}, - }}, + Unknown: true, + }, + expected: false, // intentional + }, + "unknown-null": { + receiver: ObjectUnknown( + map[string]attr.Type{ + "string": StringType, + "bool": BoolType, + "number": NumberType, + }, + ), + arg: ObjectNull( + map[string]attr.Type{ + "string": StringType, + "bool": BoolType, + "number": NumberType, + }, + ), + expected: false, + }, + "unknown-deprecated-null": { + receiver: ObjectUnknown( + map[string]attr.Type{ + "string": StringType, + "bool": BoolType, + "number": NumberType, + }, + ), + arg: Object{ + AttrTypes: map[string]attr.Type{ + "string": StringType, + "bool": BoolType, + "number": NumberType, + }, + Null: true, + }, + expected: false, + }, + "null-known": { + receiver: ObjectNull( + map[string]attr.Type{ + "string": StringType, + "bool": BoolType, + "number": NumberType, + }, + ), + arg: ObjectValue( + map[string]attr.Type{ + "string": StringType, + "bool": BoolType, + "number": NumberType, + }, + map[string]attr.Value{ + "string": String{Value: "hello"}, + "bool": Bool{Value: true}, + "number": Number{Value: big.NewFloat(123)}, + }, + ), + expected: false, + }, + "null-deprecated-known": { + receiver: ObjectNull( + map[string]attr.Type{ + "string": StringType, + "bool": BoolType, + "number": NumberType, + }, + ), + arg: Object{ + AttrTypes: map[string]attr.Type{ + "string": StringType, + "bool": BoolType, + "number": NumberType, + }, + Attrs: map[string]attr.Value{ + "string": String{Value: "hello"}, + "bool": Bool{Value: true}, + "number": Number{Value: big.NewFloat(123)}, + }, + }, + expected: false, + }, + "null-unknown": { + receiver: ObjectNull( + map[string]attr.Type{ + "string": StringType, + "bool": BoolType, + "number": NumberType, + }, + ), + arg: ObjectUnknown( + map[string]attr.Type{ + "string": StringType, + "bool": BoolType, + "number": NumberType, + }, + ), + expected: false, + }, + "null-deprecated-unknown": { + receiver: ObjectNull( + map[string]attr.Type{ + "string": StringType, + "bool": BoolType, + "number": NumberType, + }, + ), + arg: Object{ + AttrTypes: map[string]attr.Type{ + "string": StringType, + "bool": BoolType, + "number": NumberType, + }, + Unknown: true, + }, + expected: false, + }, + "null-null": { + receiver: ObjectNull( + map[string]attr.Type{ + "string": StringType, + "bool": BoolType, + "number": NumberType, + }, + ), + arg: ObjectNull( + map[string]attr.Type{ + "string": StringType, + "bool": BoolType, + "number": NumberType, + }, + ), + expected: true, + }, + "null-deprecated-null": { + receiver: ObjectNull( + map[string]attr.Type{ + "string": StringType, + "bool": BoolType, + "number": NumberType, + }, + ), + arg: Object{ + AttrTypes: map[string]attr.Type{ + "string": StringType, + "bool": BoolType, + "number": NumberType, + }, + Null: true, + }, + expected: false, // intentional + }, + "deprecated-known-deprecated-known": { + receiver: Object{ + AttrTypes: map[string]attr.Type{ + "string": StringType, + "bool": BoolType, + "number": NumberType, + }, + Attrs: map[string]attr.Value{ + "string": String{Value: "hello"}, + "bool": Bool{Value: true}, + "number": Number{Value: big.NewFloat(123)}, + }, + }, + arg: Object{ + AttrTypes: map[string]attr.Type{ + "string": StringType, + "bool": BoolType, + "number": NumberType, + }, + Attrs: map[string]attr.Value{ + "string": String{Value: "hello"}, + "bool": Bool{Value: true}, + "number": Number{Value: big.NewFloat(123)}, + }, + }, + expected: true, + }, + "deprecated-known-deprecated-known-diff-value": { + receiver: Object{ + AttrTypes: map[string]attr.Type{ + "string": StringType, + "bool": BoolType, + "number": NumberType, + }, + Attrs: map[string]attr.Value{ + "string": String{Value: "hello"}, + "bool": Bool{Value: true}, + "number": Number{Value: big.NewFloat(123)}, + }, + }, + arg: Object{ + AttrTypes: map[string]attr.Type{ + "string": StringType, + "bool": BoolType, + "number": NumberType, + }, + Attrs: map[string]attr.Value{ + "string": String{Value: "world"}, + "bool": Bool{Value: true}, + "number": Number{Value: big.NewFloat(123)}, }, }, expected: false, }, - "diff-attribute-types": { + "deprecated-known-deprecated-known-diff-attribute-types": { receiver: Object{ AttrTypes: map[string]attr.Type{ "string": StringType, @@ -1414,7 +1915,7 @@ func TestObjectEqual(t *testing.T) { }, expected: false, }, - "diff-attribute-types-count": { + "deprecated-known-deprecated-known-diff-attribute-count": { receiver: Object{ AttrTypes: map[string]attr.Type{ "string": StringType, @@ -1438,7 +1939,7 @@ func TestObjectEqual(t *testing.T) { }, expected: false, }, - "diff-attribute-types-value": { + "deprecated-known-deprecated-known-diff-unknown": { receiver: Object{ AttrTypes: map[string]attr.Type{ "string": StringType, @@ -1449,15 +1950,15 @@ func TestObjectEqual(t *testing.T) { }, arg: Object{ AttrTypes: map[string]attr.Type{ - "string": NumberType, + "string": StringType, }, Attrs: map[string]attr.Value{ - "string": Number{Value: big.NewFloat(123)}, + "string": String{Unknown: true}, }, }, expected: false, }, - "diff-attribute-count": { + "deprecated-known-deprecated-known-diff-null": { receiver: Object{ AttrTypes: map[string]attr.Type{ "string": StringType, @@ -1470,28 +1971,237 @@ func TestObjectEqual(t *testing.T) { AttrTypes: map[string]attr.Type{ "string": StringType, }, - Attrs: map[string]attr.Value{}, + Attrs: map[string]attr.Value{ + "string": String{Null: true}, + }, + }, + expected: false, + }, + "deprecated-known-deprecated-unknown": { + receiver: Object{ + AttrTypes: map[string]attr.Type{ + "string": StringType, + "bool": BoolType, + "number": NumberType, + }, + Attrs: map[string]attr.Value{ + "string": String{Value: "hello"}, + "bool": Bool{Value: true}, + "number": Number{Value: big.NewFloat(123)}, + }, + }, + arg: Object{ + AttrTypes: map[string]attr.Type{ + "string": StringType, + "bool": BoolType, + "number": NumberType, + }, + Unknown: true, + }, + expected: false, + }, + "deprecated-known-deprecated-null": { + receiver: Object{ + AttrTypes: map[string]attr.Type{ + "string": StringType, + "bool": BoolType, + "number": NumberType, + }, + Attrs: map[string]attr.Value{ + "string": String{Value: "hello"}, + "bool": Bool{Value: true}, + "number": Number{Value: big.NewFloat(123)}, + }, + }, + arg: Object{ + AttrTypes: map[string]attr.Type{ + "string": StringType, + "bool": BoolType, + "number": NumberType, + }, + Null: true, + }, + expected: false, + }, + "deprecated-known-diff-wrong-type": { + receiver: Object{ + AttrTypes: map[string]attr.Type{ + "string": StringType, + "bool": BoolType, + "number": NumberType, + }, + Attrs: map[string]attr.Value{ + "string": String{Value: "hello"}, + "bool": Bool{Value: true}, + "number": Number{Value: big.NewFloat(123)}, + }, + }, + arg: String{Value: "whoops"}, + expected: false, + }, + "deprecated-known-invalid-attribute-name": { + receiver: Object{ + AttrTypes: map[string]attr.Type{ + "string": StringType, + }, + Attrs: map[string]attr.Value{ + "string": String{Value: "hello"}, + }, + }, + arg: Object{ + AttrTypes: map[string]attr.Type{ + "string": StringType, + }, + Attrs: map[string]attr.Value{ + "strng": String{Value: "hello"}, + }, + }, + expected: false, + }, + "deprecated-known-known": { + receiver: Object{ + AttrTypes: map[string]attr.Type{ + "string": StringType, + "bool": BoolType, + "number": NumberType, + }, + Attrs: map[string]attr.Value{ + "string": String{Value: "hello"}, + "bool": Bool{Value: true}, + "number": Number{Value: big.NewFloat(123)}, + }, + }, + arg: ObjectValue( + map[string]attr.Type{ + "string": StringType, + "bool": BoolType, + "number": NumberType, + }, + map[string]attr.Value{ + "string": String{Value: "hello"}, + "bool": Bool{Value: true}, + "number": Number{Value: big.NewFloat(123)}, + }, + ), + expected: false, // intentional + }, + "deprecated-known-unknown": { + receiver: Object{ + AttrTypes: map[string]attr.Type{ + "string": StringType, + "bool": BoolType, + "number": NumberType, + }, + Attrs: map[string]attr.Value{ + "string": String{Value: "hello"}, + "bool": Bool{Value: true}, + "number": Number{Value: big.NewFloat(123)}, + }, + }, + arg: ObjectUnknown( + map[string]attr.Type{ + "string": StringType, + "bool": BoolType, + "number": NumberType, + }, + ), + expected: false, + }, + "deprecated-known-null": { + receiver: Object{ + AttrTypes: map[string]attr.Type{ + "string": StringType, + "bool": BoolType, + "number": NumberType, + }, + Attrs: map[string]attr.Value{ + "string": String{Value: "hello"}, + "bool": Bool{Value: true}, + "number": Number{Value: big.NewFloat(123)}, + }, + }, + arg: ObjectNull( + map[string]attr.Type{ + "string": StringType, + "bool": BoolType, + "number": NumberType, + }, + ), + expected: false, + }, + "deprecated-unknown-deprecated-unknown": { + receiver: Object{ + AttrTypes: map[string]attr.Type{ + "string": StringType, + "bool": BoolType, + "number": NumberType, + }, + Unknown: true, + }, + arg: Object{ + AttrTypes: map[string]attr.Type{ + "string": StringType, + "bool": BoolType, + "number": NumberType, + }, + Unknown: true, + }, + expected: true, + }, + "deprecated-unknown-unknown": { + receiver: Object{ + AttrTypes: map[string]attr.Type{ + "string": StringType, + "bool": BoolType, + "number": NumberType, + }, + Unknown: true, + }, + arg: ObjectUnknown( + map[string]attr.Type{ + "string": StringType, + "bool": BoolType, + "number": NumberType, + }, + ), + expected: false, // intentional + }, + "deprecated-null-deprecated-null": { + receiver: Object{ + AttrTypes: map[string]attr.Type{ + "string": StringType, + "bool": BoolType, + "number": NumberType, + }, + Null: true, + }, + arg: Object{ + AttrTypes: map[string]attr.Type{ + "string": StringType, + "bool": BoolType, + "number": NumberType, + }, + Null: true, }, - expected: false, + expected: true, }, - "diff-attribute-names": { + "deprecated-null-null": { receiver: Object{ AttrTypes: map[string]attr.Type{ "string": StringType, + "bool": BoolType, + "number": NumberType, }, - Attrs: map[string]attr.Value{ - "string": String{Value: "hello"}, - }, + Null: true, }, - arg: Object{ - AttrTypes: map[string]attr.Type{ + arg: ObjectNull( + map[string]attr.Type{ "string": StringType, + "bool": BoolType, + "number": NumberType, }, - Attrs: map[string]attr.Value{ - "strng": String{Value: "hello"}, - }, - }, - expected: false, + ), + expected: false, // intentional }, } @@ -1508,6 +2218,142 @@ func TestObjectEqual(t *testing.T) { } } +func TestObjectIsNull(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + input Object + expected bool + }{ + "known": { + input: ObjectValue( + map[string]attr.Type{"test_attr": StringType}, + map[string]attr.Value{"test_attr": StringValue("test-value")}, + ), + expected: false, + }, + "deprecated-known": { + input: Object{ + AttrTypes: map[string]attr.Type{"test_attr": StringType}, + Attrs: map[string]attr.Value{"test_attr": StringValue("test-value")}, + }, + expected: false, + }, + "null": { + input: ObjectNull(map[string]attr.Type{"test_attr": StringType}), + expected: true, + }, + "deprecated-null": { + input: Object{ + AttrTypes: map[string]attr.Type{"test_attr": StringType}, + Null: true, + }, + expected: true, + }, + "unknown": { + input: ObjectUnknown(map[string]attr.Type{"test_attr": StringType}), + expected: false, + }, + "deprecated-unknown": { + input: Object{ + AttrTypes: map[string]attr.Type{"test_attr": StringType}, + Unknown: true, + }, + expected: false, + }, + "deprecated-invalid": { + input: Object{ + AttrTypes: map[string]attr.Type{"test_attr": StringType}, + Null: true, + Unknown: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.input.IsNull() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestObjectIsUnknown(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + input Object + expected bool + }{ + "known": { + input: ObjectValue( + map[string]attr.Type{"test_attr": StringType}, + map[string]attr.Value{"test_attr": StringValue("test-value")}, + ), + expected: false, + }, + "deprecated-known": { + input: Object{ + AttrTypes: map[string]attr.Type{"test_attr": StringType}, + Attrs: map[string]attr.Value{"test_attr": StringValue("test-value")}, + }, + expected: false, + }, + "null": { + input: ObjectNull(map[string]attr.Type{"test_attr": StringType}), + expected: false, + }, + "deprecated-null": { + input: Object{ + AttrTypes: map[string]attr.Type{"test_attr": StringType}, + Null: true, + }, + expected: false, + }, + "unknown": { + input: ObjectUnknown(map[string]attr.Type{"test_attr": StringType}), + expected: true, + }, + "deprecated-unknown": { + input: Object{ + AttrTypes: map[string]attr.Type{"test_attr": StringType}, + Unknown: true, + }, + expected: true, + }, + "deprecated-invalid": { + input: Object{ + AttrTypes: map[string]attr.Type{"test_attr": StringType}, + Null: true, + Unknown: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.input.IsUnknown() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestObjectString(t *testing.T) { t.Parallel() @@ -1516,7 +2362,87 @@ func TestObjectString(t *testing.T) { expectation string } tests := map[string]testCase{ - "simple": { + "known": { + input: ObjectValue( + map[string]attr.Type{ + "alpha": StringType, + "beta": Int64Type, + "gamma": Float64Type, + "sigma": NumberType, + "theta": BoolType, + }, + map[string]attr.Value{ + "alpha": String{Value: "hello"}, + "beta": Int64{Value: 98719827987189}, + "gamma": Float64{Value: -9876.782378}, + "sigma": Number{Unknown: true}, + "theta": Bool{Null: true}, + }, + ), + expectation: `{"alpha":"hello","beta":98719827987189,"gamma":-9876.782378,"sigma":,"theta":}`, + }, + "known-object-of-objects": { + input: ObjectValue( + map[string]attr.Type{ + "alpha": ObjectType{ + AttrTypes: map[string]attr.Type{ + "one": StringType, + "two": BoolType, + "three": NumberType, + }, + }, + "beta": ObjectType{ + AttrTypes: map[string]attr.Type{ + "uno": Int64Type, + "due": BoolType, + "tre": StringType, + }, + }, + "gamma": Float64Type, + "sigma": NumberType, + "theta": BoolType, + }, + map[string]attr.Value{ + "alpha": ObjectValue( + map[string]attr.Type{ + "one": StringType, + "two": BoolType, + "three": NumberType, + }, + map[string]attr.Value{ + "one": String{Value: "1"}, + "two": Bool{Value: true}, + "three": Number{Value: big.NewFloat(0.3)}, + }, + ), + "beta": ObjectValue( + map[string]attr.Type{ + "uno": Int64Type, + "due": BoolType, + "tre": StringType, + }, + map[string]attr.Value{ + "uno": Int64{Value: 1}, + "due": Bool{Value: false}, + "tre": String{Value: "3"}, + }, + ), + "gamma": Float64{Value: -9876.782378}, + "sigma": Number{Unknown: true}, + "theta": Bool{Null: true}, + }, + ), + expectation: `{"alpha":{"one":"1","three":0.3,"two":true},"beta":{"due":false,"tre":"3","uno":1},"gamma":-9876.782378,"sigma":,"theta":}`, + }, + "unknown": { + input: ObjectUnknown(map[string]attr.Type{"test_attr": StringType}), + expectation: "", + }, + "null": { + input: ObjectNull(map[string]attr.Type{"test_attr": StringType}), + expectation: "", + }, + "deprecated-known": { input: Object{ AttrTypes: map[string]attr.Type{ "alpha": StringType, @@ -1535,7 +2461,7 @@ func TestObjectString(t *testing.T) { }, expectation: `{"alpha":"hello","beta":98719827987189,"gamma":-9876.782378,"sigma":,"theta":}`, }, - "object-of-objects": { + "deprecated-known-object-of-objects": { input: Object{ AttrTypes: map[string]attr.Type{ "alpha": ObjectType{ @@ -1578,11 +2504,11 @@ func TestObjectString(t *testing.T) { }, expectation: `{"alpha":{"one":"1","three":0.3,"two":true},"beta":{"due":false,"tre":"3","uno":1},"gamma":-9876.782378,"sigma":,"theta":}`, }, - "unknown": { + "deprecated-unknown": { input: Object{Unknown: true}, expectation: "", }, - "null": { + "deprecated-null": { input: Object{Null: true}, expectation: "", }, @@ -1604,3 +2530,200 @@ func TestObjectString(t *testing.T) { }) } } + +func TestObjectType(t *testing.T) { + t.Parallel() + + type testCase struct { + input Object + expectation attr.Type + } + tests := map[string]testCase{ + "known": { + input: ObjectValue( + map[string]attr.Type{ + "test_attr1": StringType, + "test_attr2": StringType, + }, + map[string]attr.Value{ + "test_attr1": StringValue("hello"), + "test_attr2": StringValue("world"), + }, + ), + expectation: ObjectType{ + AttrTypes: map[string]attr.Type{ + "test_attr1": StringType, + "test_attr2": StringType, + }, + }, + }, + "known-object-of-objects": { + input: ObjectValue( + map[string]attr.Type{ + "test_attr1": ObjectType{ + AttrTypes: map[string]attr.Type{ + "test_attr1": StringType, + "test_attr2": StringType, + }, + }, + "test_attr2": ObjectType{ + AttrTypes: map[string]attr.Type{ + "test_attr1": StringType, + "test_attr2": StringType, + }, + }, + }, + map[string]attr.Value{ + "test_attr1": ObjectValue( + map[string]attr.Type{ + "test_attr1": StringType, + "test_attr2": StringType, + }, + map[string]attr.Value{ + "test_attr1": StringValue("hello"), + "test_attr2": StringValue("world"), + }, + ), + "test_attr2": ObjectValue( + map[string]attr.Type{ + "test_attr1": StringType, + "test_attr2": StringType, + }, + map[string]attr.Value{ + "test_attr1": StringValue("foo"), + "test_attr2": StringValue("bar"), + }, + ), + }, + ), + expectation: ObjectType{ + AttrTypes: map[string]attr.Type{ + "test_attr1": ObjectType{ + AttrTypes: map[string]attr.Type{ + "test_attr1": StringType, + "test_attr2": StringType, + }, + }, + "test_attr2": ObjectType{ + AttrTypes: map[string]attr.Type{ + "test_attr1": StringType, + "test_attr2": StringType, + }, + }, + }, + }, + }, + "unknown": { + input: ObjectUnknown(map[string]attr.Type{"test_attr": StringType}), + expectation: ObjectType{AttrTypes: map[string]attr.Type{"test_attr": StringType}}, + }, + "null": { + input: ObjectNull(map[string]attr.Type{"test_attr": StringType}), + expectation: ObjectType{AttrTypes: map[string]attr.Type{"test_attr": StringType}}, + }, + "deprecated-known": { + input: Object{ + AttrTypes: map[string]attr.Type{ + "test_attr1": StringType, + "test_attr2": StringType, + }, + Attrs: map[string]attr.Value{ + "test_attr1": String{Value: "hello"}, + "test_attr2": String{Value: "world"}, + }, + }, + expectation: ObjectType{ + AttrTypes: map[string]attr.Type{ + "test_attr1": StringType, + "test_attr2": StringType, + }, + }, + }, + "deprecated-known-object-of-objects": { + input: Object{ + AttrTypes: map[string]attr.Type{ + "test_attr1": ObjectType{ + AttrTypes: map[string]attr.Type{ + "test_attr1": StringType, + "test_attr2": StringType, + }, + }, + "test_attr2": ObjectType{ + AttrTypes: map[string]attr.Type{ + "test_attr1": StringType, + "test_attr2": StringType, + }, + }, + }, + Attrs: map[string]attr.Value{ + "test_attr1": ObjectValue( + map[string]attr.Type{ + "test_attr1": StringType, + "test_attr2": StringType, + }, + map[string]attr.Value{ + "test_attr1": StringValue("hello"), + "test_attr2": StringValue("world"), + }, + ), + "test_attr2": ObjectValue( + map[string]attr.Type{ + "test_attr1": StringType, + "test_attr2": StringType, + }, + map[string]attr.Value{ + "test_attr1": StringValue("foo"), + "test_attr2": StringValue("bar"), + }, + ), + }, + }, + expectation: ObjectType{ + AttrTypes: map[string]attr.Type{ + "test_attr1": ObjectType{ + AttrTypes: map[string]attr.Type{ + "test_attr1": StringType, + "test_attr2": StringType, + }, + }, + "test_attr2": ObjectType{ + AttrTypes: map[string]attr.Type{ + "test_attr1": StringType, + "test_attr2": StringType, + }, + }, + }, + }, + }, + "deprecated-unknown": { + input: Object{ + AttrTypes: map[string]attr.Type{"test_attr": StringType}, + Unknown: true, + }, + expectation: ObjectType{ + AttrTypes: map[string]attr.Type{"test_attr": StringType}, + }, + }, + "deprecated-null": { + input: Object{ + AttrTypes: map[string]attr.Type{"test_attr": StringType}, + Null: true, + }, + expectation: ObjectType{ + AttrTypes: map[string]attr.Type{"test_attr": StringType}, + }, + }, + } + + for name, test := range tests { + name, test := name, test + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := test.input.Type(context.Background()) + if !cmp.Equal(got, test.expectation) { + t.Errorf("Expected %q, got %q", test.expectation, got) + } + }) + } +} diff --git a/types/set.go b/types/set.go index c251dc126..9265c6bf6 100644 --- a/types/set.go +++ b/types/set.go @@ -55,6 +55,7 @@ func (st SetType) TerraformType(ctx context.Context) tftypes.Type { func (st SetType) ValueFromTerraform(ctx context.Context, in tftypes.Value) (attr.Value, error) { set := Set{ ElemType: st.ElemType, + state: valueStateDeprecated, } if in.Type() == nil { set.Null = true @@ -202,6 +203,43 @@ func (t SetType) ValueType(_ context.Context) attr.Value { } } +// SetNull creates a Set with a null value. Determine whether the value is +// null via the Set type IsNull method. +// +// Setting the deprecated Set type ElemType, Elems, Null, or Unknown fields +// after creating a Set with this function has no effect. +func SetNull(elementType attr.Type) Set { + return Set{ + elementType: elementType, + state: valueStateNull, + } +} + +// SetUnknown creates a Set with an unknown value. Determine whether the +// value is unknown via the Set type IsUnknown method. +// +// Setting the deprecated Set type ElemType, Elems, Null, or Unknown fields +// after creating a Set with this function has no effect. +func SetUnknown(elementType attr.Type) Set { + return Set{ + elementType: elementType, + state: valueStateUnknown, + } +} + +// SetValue creates a Set with a known value. Access the value via the Set +// type ElementsAs method. +// +// Setting the deprecated Set type ElemType, Elems, Null, or Unknown fields +// after creating a Set with this function has no effect. +func SetValue(elementType attr.Type, elements []attr.Value) Set { + return Set{ + elementType: elementType, + elements: elements, + state: valueStateKnown, + } +} + // Set represents a set of attr.Value, all of the same type, // indicated by ElemType. type Set struct { @@ -211,19 +249,63 @@ type Set struct { // 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. + // + // If the Set was created with the SetValue, SetNull, or SetUnknown + // functions, changing this field has no effect. + // + // Deprecated: Use the SetUnknown function to create an unknown Set + // value or use the IsUnknown method to determine whether the Set value + // is unknown instead. 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. + // + // If the Set was created with the SetValue, SetNull, or SetUnknown + // functions, changing this field has no effect. + // + // Deprecated: Use the SetNull function to create a null Set value or + // use the IsNull method to determine whether the Set value is null + // instead. Null bool // Elems are the elements in the set. + // + // If the Set was created with the SetValue, SetNull, or SetUnknown + // functions, changing this field has no effect. + // + // Deprecated: Use the SetValue function to create a known Set value or + // use the Elements or ElementsAs methods to retrieve the Set elements + // instead. Elems []attr.Value // ElemType is the tftypes.Type of the elements in the set. All // elements in the set must be of this type. + // + // Deprecated: Use the SetValue, SetNull, or SetUnknown functions + // to create a Set or use the ElementType method to retrieve the + // Set element type instead. ElemType attr.Type + + // elements is the collection of known values in the Set. + elements []attr.Value + + // elementType is the type of the elements in the Set. + elementType attr.Type + + // state represents whether the Set is null, unknown, or known. + state valueState +} + +// Elements returns the collection of elements for the Set. Returns nil if the +// Set is null or unknown. +func (s Set) Elements() []attr.Value { + if s.state == valueStateDeprecated { + return s.Elems + } + + return s.elements } // ElementsAs populates `target` with the elements of the Set, throwing an @@ -246,35 +328,72 @@ func (s Set) ElementsAs(ctx context.Context, target interface{}, allowUnhandled }, path.Empty()) } +// ElementType returns the element type for the Set. +func (s Set) ElementType(_ context.Context) attr.Type { + if s.state == valueStateDeprecated { + return s.ElemType + } + + return s.elementType +} + // 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} + return SetType{ElemType: s.ElementType(ctx)} } // ToTerraformValue returns the data contained in the Set as a tftypes.Value. func (s Set) ToTerraformValue(ctx context.Context) (tftypes.Value, error) { - if s.ElemType == nil { + if s.state == valueStateDeprecated && s.ElemType == nil { return tftypes.Value{}, fmt.Errorf("cannot convert Set to tftypes.Value if ElemType field is not set") } - setType := tftypes.Set{ElementType: s.ElemType.TerraformType(ctx)} - if s.Unknown { - return tftypes.NewValue(setType, tftypes.UnknownValue), nil - } - if s.Null { - return tftypes.NewValue(setType, nil), nil - } - vals := make([]tftypes.Value, 0, len(s.Elems)) - for _, elem := range s.Elems { - val, err := elem.ToTerraformValue(ctx) - if err != nil { + setType := tftypes.Set{ElementType: s.ElementType(ctx).TerraformType(ctx)} + + switch s.state { + case valueStateDeprecated: + if s.Unknown { + return tftypes.NewValue(setType, tftypes.UnknownValue), nil + } + if s.Null { + return tftypes.NewValue(setType, nil), nil + } + vals := make([]tftypes.Value, 0, len(s.Elems)) + for _, elem := range s.Elems { + val, err := elem.ToTerraformValue(ctx) + if err != nil { + return tftypes.NewValue(setType, tftypes.UnknownValue), err + } + vals = append(vals, val) + } + if err := tftypes.ValidateValue(setType, vals); err != nil { return tftypes.NewValue(setType, tftypes.UnknownValue), err } - vals = append(vals, val) - } - if err := tftypes.ValidateValue(setType, vals); err != nil { - return tftypes.NewValue(setType, tftypes.UnknownValue), err + return tftypes.NewValue(setType, vals), nil + case valueStateKnown: + vals := make([]tftypes.Value, 0, len(s.elements)) + + for _, elem := range s.elements { + val, err := elem.ToTerraformValue(ctx) + + if err != nil { + return tftypes.NewValue(setType, tftypes.UnknownValue), err + } + + vals = append(vals, val) + } + + if err := tftypes.ValidateValue(setType, vals); err != nil { + return tftypes.NewValue(setType, tftypes.UnknownValue), err + } + + return tftypes.NewValue(setType, vals), nil + case valueStateNull: + return tftypes.NewValue(setType, nil), nil + case valueStateUnknown: + return tftypes.NewValue(setType, tftypes.UnknownValue), nil + default: + panic(fmt.Sprintf("unhandled Set state in ToTerraformValue: %s", s.state)) } - return tftypes.NewValue(setType, vals), nil } // Equal returns true if the Set is considered semantically equal @@ -284,6 +403,26 @@ func (s Set) Equal(o attr.Value) bool { if !ok { return false } + if s.state != other.state { + return false + } + if s.state == valueStateKnown { + if !s.elementType.Equal(other.elementType) { + return false + } + + if len(s.elements) != len(other.elements) { + return false + } + + for _, elem := range s.elements { + if !other.contains(elem) { + return false + } + } + + return true + } if s.Unknown != other.Unknown { return false } @@ -308,7 +447,7 @@ func (s Set) Equal(o attr.Value) bool { } func (s Set) contains(v attr.Value) bool { - for _, elem := range s.Elems { + for _, elem := range s.Elements() { if elem.Equal(v) { return true } @@ -319,30 +458,40 @@ func (s Set) contains(v attr.Value) bool { // IsNull returns true if the Set represents a null value. func (s Set) IsNull() bool { - return s.Null + if s.state == valueStateNull { + return true + } + + return s.state == valueStateDeprecated && s.Null } // IsUnknown returns true if the Set represents a currently unknown value. +// Returns false if the Set has a known number of elements, even if all are +// unknown values. func (s Set) IsUnknown() bool { - return s.Unknown + if s.state == valueStateUnknown { + return true + } + + return s.state == valueStateDeprecated && s.Unknown } // String returns a human-readable representation of the Set value. // The string returned here is not protected by any compatibility guarantees, // and is intended for logging and error reporting. func (s Set) String() string { - if s.Unknown { + if s.IsUnknown() { return attr.UnknownValueString } - if s.Null { + if s.IsNull() { return attr.NullValueString } var res strings.Builder res.WriteString("[") - for i, e := range s.Elems { + for i, e := range s.Elements() { if i != 0 { res.WriteString(",") } diff --git a/types/set_test.go b/types/set_test.go index 6ba3075d1..048f7e339 100644 --- a/types/set_test.go +++ b/types/set_test.go @@ -570,6 +570,84 @@ func TestSetTypeValidate(t *testing.T) { } } +// This test verifies the assumptions that creating the Value via function then +// setting the fields directly has no effects. +func TestSetValue_DeprecatedFieldSetting(t *testing.T) { + t.Parallel() + + knownSet := SetValue(StringType, []attr.Value{StringValue("test")}) + + knownSet.Null = true + + if knownSet.IsNull() { + t.Error("unexpected null update after Null field setting") + } + + knownSet.Unknown = true + + if knownSet.IsUnknown() { + t.Error("unexpected unknown update after Unknown field setting") + } + + knownSet.Elems = []attr.Value{StringValue("not-test")} + + if knownSet.Elements()[0].Equal(StringValue("not-test")) { + t.Error("unexpected value update after Value field setting") + } +} + +// This test verifies the assumptions that creating the Value via function then +// setting the fields directly has no effects. +func TestSetNull_DeprecatedFieldSetting(t *testing.T) { + t.Parallel() + + nullSet := SetNull(StringType) + + nullSet.Null = false + + if !nullSet.IsNull() { + t.Error("unexpected null update after Null field setting") + } + + nullSet.Unknown = true + + if nullSet.IsUnknown() { + t.Error("unexpected unknown update after Unknown field setting") + } + + nullSet.Elems = []attr.Value{StringValue("test")} + + if len(nullSet.Elements()) > 0 { + t.Error("unexpected value update after Value field setting") + } +} + +// This test verifies the assumptions that creating the Value via function then +// setting the fields directly has no effects. +func TestSetUnknown_DeprecatedFieldSetting(t *testing.T) { + t.Parallel() + + unknownSet := SetUnknown(StringType) + + unknownSet.Null = true + + if unknownSet.IsNull() { + t.Error("unexpected null update after Null field setting") + } + + unknownSet.Unknown = false + + if !unknownSet.IsUnknown() { + t.Error("unexpected unknown update after Unknown field setting") + } + + unknownSet.Elems = []attr.Value{StringValue("test")} + + if len(unknownSet.Elements()) > 0 { + t.Error("unexpected value update after Value field setting") + } +} + func TestSetToTerraformValue(t *testing.T) { t.Parallel() @@ -579,7 +657,69 @@ func TestSetToTerraformValue(t *testing.T) { expectedErr string } tests := map[string]testCase{ - "value": { + "known": { + input: SetValue( + StringType, + []attr.Value{ + StringValue("hello"), + StringValue("world"), + }, + ), + expectation: tftypes.NewValue(tftypes.Set{ElementType: tftypes.String}, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "hello"), + tftypes.NewValue(tftypes.String, "world"), + }), + }, + "known-duplicates": { + input: SetValue( + StringType, + []attr.Value{ + StringValue("hello"), + StringValue("hello"), + }, + ), + // Duplicate validation does not occur during this method. + // This is okay, as tftypes allows duplicates. + expectation: tftypes.NewValue(tftypes.Set{ElementType: tftypes.String}, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "hello"), + tftypes.NewValue(tftypes.String, "hello"), + }), + }, + "known-partial-unknown": { + input: SetValue( + StringType, + []attr.Value{ + StringUnknown(), + StringValue("hello, world"), + }, + ), + expectation: tftypes.NewValue(tftypes.Set{ElementType: tftypes.String}, []tftypes.Value{ + tftypes.NewValue(tftypes.String, tftypes.UnknownValue), + tftypes.NewValue(tftypes.String, "hello, world"), + }), + }, + "known-partial-null": { + input: SetValue( + StringType, + []attr.Value{ + StringNull(), + StringValue("hello, world"), + }, + ), + expectation: tftypes.NewValue(tftypes.Set{ElementType: tftypes.String}, []tftypes.Value{ + tftypes.NewValue(tftypes.String, nil), + tftypes.NewValue(tftypes.String, "hello, world"), + }), + }, + "unknown": { + input: SetUnknown(StringType), + expectation: tftypes.NewValue(tftypes.Set{ElementType: tftypes.String}, tftypes.UnknownValue), + }, + "null": { + input: SetNull(StringType), + expectation: tftypes.NewValue(tftypes.Set{ElementType: tftypes.String}, nil), + }, + "deprecated-known": { input: Set{ ElemType: StringType, Elems: []attr.Value{ @@ -592,7 +732,7 @@ func TestSetToTerraformValue(t *testing.T) { tftypes.NewValue(tftypes.String, "world"), }), }, - "value-duplicates": { + "deprecated-known-duplicates": { input: Set{ ElemType: StringType, Elems: []attr.Value{ @@ -607,15 +747,15 @@ func TestSetToTerraformValue(t *testing.T) { tftypes.NewValue(tftypes.String, "hello"), }), }, - "unknown": { + "deprecated-unknown": { input: Set{ElemType: StringType, Unknown: true}, expectation: tftypes.NewValue(tftypes.Set{ElementType: tftypes.String}, tftypes.UnknownValue), }, - "null": { + "deprecated-null": { input: Set{ElemType: StringType, Null: true}, expectation: tftypes.NewValue(tftypes.Set{ElementType: tftypes.String}, nil), }, - "partial-unknown": { + "deprecated-known-partial-unknown": { input: Set{ ElemType: StringType, Elems: []attr.Value{ @@ -628,7 +768,7 @@ func TestSetToTerraformValue(t *testing.T) { tftypes.NewValue(tftypes.String, "hello, world"), }), }, - "partial-null": { + "deprecated-known-partial-null": { input: Set{ ElemType: StringType, Elems: []attr.Value{ @@ -683,6 +823,102 @@ func TestSetToTerraformValue(t *testing.T) { } } +func TestSetElements(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + input Set + expected []attr.Value + }{ + "known": { + input: SetValue(StringType, []attr.Value{StringValue("test")}), + expected: []attr.Value{StringValue("test")}, + }, + "deprecated-known": { + input: Set{ElemType: StringType, Elems: []attr.Value{StringValue("test")}}, + expected: []attr.Value{StringValue("test")}, + }, + "null": { + input: SetNull(StringType), + expected: nil, + }, + "deprecated-null": { + input: Set{ElemType: StringType, Null: true}, + expected: nil, + }, + "unknown": { + input: SetUnknown(StringType), + expected: nil, + }, + "deprecated-unknown": { + input: Set{ElemType: StringType, Unknown: true}, + expected: nil, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.input.Elements() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetElementType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + input Set + expected attr.Type + }{ + "known": { + input: SetValue(StringType, []attr.Value{StringValue("test")}), + expected: StringType, + }, + "deprecated-known": { + input: Set{ElemType: StringType, Elems: []attr.Value{StringValue("test")}}, + expected: StringType, + }, + "null": { + input: SetNull(StringType), + expected: StringType, + }, + "deprecated-null": { + input: Set{ElemType: StringType, Null: true}, + expected: StringType, + }, + "unknown": { + input: SetUnknown(StringType), + expected: StringType, + }, + "deprecated-unknown": { + input: Set{ElemType: StringType, Unknown: true}, + expected: StringType, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.input.ElementType(context.Background()) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestSetEqual(t *testing.T) { t.Parallel() @@ -692,41 +928,199 @@ func TestSetEqual(t *testing.T) { expected bool } tests := map[string]testCase{ - "set-value-set-value": { - receiver: Set{ - ElemType: StringType, - Elems: []attr.Value{ - String{Value: "hello"}, - String{Value: "world"}, + "known-known": { + receiver: SetValue( + StringType, + []attr.Value{ + StringValue("hello"), + StringValue("world"), }, - }, - input: Set{ - ElemType: StringType, - Elems: []attr.Value{ - String{Value: "hello"}, - String{Value: "world"}, + ), + input: SetValue( + StringType, + []attr.Value{ + StringValue("hello"), + StringValue("world"), }, - }, + ), expected: true, }, - "set-value-diff": { - receiver: Set{ + "known-known-diff-value": { + receiver: SetValue( + StringType, + []attr.Value{ + StringValue("hello"), + StringValue("world"), + }, + ), + input: SetValue( + StringType, + []attr.Value{ + StringValue("goodnight"), + StringValue("moon"), + }, + ), + expected: false, + }, + "known-known-diff-length": { + receiver: SetValue( + StringType, + []attr.Value{ + StringValue("hello"), + StringValue("world"), + }, + ), + input: SetValue( + StringType, + []attr.Value{ + StringValue("hello"), + StringValue("world"), + StringValue("extra"), + }, + ), + expected: false, + }, + "known-known-diff-type": { + receiver: SetValue( + StringType, + []attr.Value{ + StringValue("hello"), + StringValue("world"), + }, + ), + input: SetValue( + BoolType, + []attr.Value{ + BoolValue(false), + BoolValue(true), + }, + ), + expected: false, + }, + "known-known-diff-unknown": { + receiver: SetValue( + StringType, + []attr.Value{ + StringValue("hello"), + StringUnknown(), + }, + ), + input: SetValue( + StringType, + []attr.Value{ + StringValue("hello"), + StringValue("world"), + }, + ), + expected: false, + }, + "known-known-diff-null": { + receiver: SetValue( + StringType, + []attr.Value{ + StringValue("hello"), + StringNull(), + }, + ), + input: SetValue( + StringType, + []attr.Value{ + StringValue("hello"), + StringValue("world"), + }, + ), + expected: false, + }, + "known-unknown": { + receiver: SetValue( + StringType, + []attr.Value{ + StringValue("hello"), + StringValue("world"), + }, + ), + input: SetUnknown(StringType), + expected: false, + }, + "known-null": { + receiver: SetValue( + StringType, + []attr.Value{ + StringValue("hello"), + StringValue("world"), + }, + ), + input: SetNull(StringType), + expected: false, + }, + "known-diff-type": { + receiver: SetValue( + StringType, + []attr.Value{ + StringValue("hello"), + StringValue("world"), + }, + ), + input: ListValue( + StringType, + []attr.Value{ + StringValue("hello"), + StringValue("world"), + }, + ), + expected: false, + }, + "known-nil": { + receiver: SetValue( + StringType, + []attr.Value{ + StringValue("hello"), + StringValue("world"), + }, + ), + input: nil, + expected: false, + }, + "known-deprecated-known": { + receiver: SetValue( + StringType, + []attr.Value{ + StringValue("hello"), + StringValue("world"), + }, + ), + input: 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, // intentional + }, + "known-deprecated-unknown": { + receiver: SetValue( + StringType, + []attr.Value{ + StringValue("hello"), + StringValue("world"), }, - }, + ), + input: Set{ElemType: StringType, Unknown: true}, + expected: false, + }, + "known-deprecated-null": { + receiver: SetValue( + StringType, + []attr.Value{ + StringValue("hello"), + StringValue("world"), + }, + ), + input: Set{ElemType: StringType, Null: true}, expected: false, }, - "set-value-count-diff": { + "deprecated-known-deprecated-known": { receiver: Set{ ElemType: StringType, Elems: []attr.Value{ @@ -739,12 +1133,11 @@ func TestSetEqual(t *testing.T) { Elems: []attr.Value{ String{Value: "hello"}, String{Value: "world"}, - String{Value: "test"}, }, }, - expected: false, + expected: true, }, - "set-value-type-diff": { + "deprecated-known-deprecated-known-diff-value": { receiver: Set{ ElemType: StringType, Elems: []attr.Value{ @@ -753,15 +1146,15 @@ func TestSetEqual(t *testing.T) { }, }, input: Set{ - ElemType: BoolType, + ElemType: StringType, Elems: []attr.Value{ - Bool{Value: false}, - Bool{Value: true}, + String{Value: "goodnight"}, + String{Value: "moon"}, }, }, expected: false, }, - "set-value-unknown": { + "deprecated-known-deprecated-known-diff-length": { receiver: Set{ ElemType: StringType, Elems: []attr.Value{ @@ -769,21 +1162,17 @@ func TestSetEqual(t *testing.T) { String{Value: "world"}, }, }, - input: Set{Unknown: true}, - expected: false, - }, - "set-value-null": { - receiver: Set{ + input: Set{ ElemType: StringType, Elems: []attr.Value{ String{Value: "hello"}, String{Value: "world"}, + String{Value: "test"}, }, }, - input: Set{Null: true}, expected: false, }, - "set-value-wrongType": { + "deprecated-known-deprecated-known-diff-type": { receiver: Set{ ElemType: StringType, Elems: []attr.Value{ @@ -791,21 +1180,16 @@ func TestSetEqual(t *testing.T) { String{Value: "world"}, }, }, - input: String{Value: "hello, world"}, - expected: false, - }, - "set-value-nil": { - receiver: Set{ - ElemType: StringType, + input: Set{ + ElemType: BoolType, Elems: []attr.Value{ - String{Value: "hello"}, - String{Value: "world"}, + Bool{Value: false}, + Bool{Value: true}, }, }, - input: nil, expected: false, }, - "partially-known-set-value-set-value": { + "deprecated-known-deprecated-known-diff-unknown": { receiver: Set{ ElemType: StringType, Elems: []attr.Value{ @@ -817,17 +1201,17 @@ func TestSetEqual(t *testing.T) { ElemType: StringType, Elems: []attr.Value{ String{Value: "hello"}, - String{Unknown: true}, + String{Value: "world"}, }, }, - expected: true, + expected: false, }, - "partially-known-set-value-diff": { + "deprecated-known-deprecated-known-diff-null": { receiver: Set{ ElemType: StringType, Elems: []attr.Value{ String{Value: "hello"}, - String{Unknown: true}, + String{Null: true}, }, }, input: Set{ @@ -839,127 +1223,84 @@ func TestSetEqual(t *testing.T) { }, expected: false, }, - "partially-known-set-value-unknown": { + "deprecated-known-deprecated-unknown": { receiver: Set{ ElemType: StringType, Elems: []attr.Value{ String{Value: "hello"}, - String{Unknown: true}, + String{Value: "world"}, }, }, input: Set{Unknown: true}, expected: false, }, - "partially-known-set-value-null": { + "deprecated-known-deprecated-null": { receiver: Set{ ElemType: StringType, Elems: []attr.Value{ String{Value: "hello"}, - String{Unknown: true}, + String{Value: "world"}, }, }, 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": { + "deprecated-known-known": { 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}, + String{Value: "world"}, }, }, - input: Set{ - ElemType: StringType, - Elems: []attr.Value{ - String{Value: "hello"}, - String{Null: true}, + input: SetValue( + StringType, + []attr.Value{ + StringValue("hello"), + StringValue("world"), }, - }, - expected: true, + ), + expected: false, // intentional }, - "partially-null-set-value-diff": { + "deprecated-known-unknown": { 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"}, }, }, + input: SetUnknown(StringType), expected: false, }, - "partially-null-set-value-unknown": { + "deprecated-known-null": { 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}, + String{Value: "world"}, }, }, - input: Set{ - Null: true, - }, + input: SetNull(StringType), expected: false, }, - "partially-null-set-value-wrongType": { + "deprecated-known-diff-type": { receiver: Set{ ElemType: StringType, Elems: []attr.Value{ String{Value: "hello"}, - String{Null: true}, + String{Value: "world"}, }, }, input: String{Value: "hello, world"}, expected: false, }, - "partially-null-set-value-nil": { + "deprecated-known-nil": { receiver: Set{ ElemType: StringType, Elems: []attr.Value{ String{Value: "hello"}, - String{Null: true}, + String{Value: "world"}, }, }, input: nil, @@ -979,6 +1320,110 @@ func TestSetEqual(t *testing.T) { } } +func TestSetIsNull(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + input Set + expected bool + }{ + "known": { + input: SetValue(StringType, []attr.Value{StringValue("test")}), + expected: false, + }, + "deprecated-known": { + input: Set{ElemType: StringType, Elems: []attr.Value{StringValue("test")}}, + expected: false, + }, + "null": { + input: SetNull(StringType), + expected: true, + }, + "deprecated-null": { + input: Set{ElemType: StringType, Null: true}, + expected: true, + }, + "unknown": { + input: SetUnknown(StringType), + expected: false, + }, + "deprecated-unknown": { + input: Set{ElemType: StringType, Unknown: true}, + expected: false, + }, + "deprecated-invalid": { + input: Set{ElemType: StringType, Null: true, Unknown: true}, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.input.IsNull() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetIsUnknown(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + input Set + expected bool + }{ + "known": { + input: SetValue(StringType, []attr.Value{StringValue("test")}), + expected: false, + }, + "deprecated-known": { + input: Set{ElemType: StringType, Elems: []attr.Value{StringValue("test")}}, + expected: false, + }, + "null": { + input: SetNull(StringType), + expected: false, + }, + "deprecated-null": { + input: Set{ElemType: StringType, Null: true}, + expected: false, + }, + "unknown": { + input: SetUnknown(StringType), + expected: true, + }, + "deprecated-unknown": { + input: Set{ElemType: StringType, Unknown: true}, + expected: true, + }, + "deprecated-invalid": { + input: Set{ElemType: StringType, Null: true, Unknown: true}, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.input.IsUnknown() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestSetString(t *testing.T) { t.Parallel() @@ -987,7 +1432,49 @@ func TestSetString(t *testing.T) { expectation string } tests := map[string]testCase{ - "simple": { + "known": { + input: SetValue( + StringType, + []attr.Value{ + StringValue("hello"), + StringValue("world"), + }, + ), + expectation: `["hello","world"]`, + }, + "known-set-of-sets": { + input: SetValue( + SetType{ + ElemType: StringType, + }, + []attr.Value{ + SetValue( + StringType, + []attr.Value{ + StringValue("hello"), + StringValue("world"), + }, + ), + SetValue( + StringType, + []attr.Value{ + StringValue("foo"), + StringValue("bar"), + }, + ), + }, + ), + expectation: `[["hello","world"],["foo","bar"]]`, + }, + "unknown": { + input: SetUnknown(StringType), + expectation: "", + }, + "null": { + input: SetNull(StringType), + expectation: "", + }, + "deprecated-known": { input: Set{ ElemType: StringType, Elems: []attr.Value{ @@ -997,7 +1484,7 @@ func TestSetString(t *testing.T) { }, expectation: `["hello","world"]`, }, - "set-of-sets": { + "deprecated-known-set-of-sets": { input: Set{ ElemType: SetType{ ElemType: StringType, @@ -1021,11 +1508,11 @@ func TestSetString(t *testing.T) { }, expectation: `[["hello","world"],["foo","bar"]]`, }, - "unknown": { + "deprecated-unknown": { input: Set{Unknown: true}, expectation: "", }, - "null": { + "deprecated-null": { input: Set{Null: true}, expectation: "", }, @@ -1047,3 +1534,118 @@ func TestSetString(t *testing.T) { }) } } + +func TestSetType(t *testing.T) { + t.Parallel() + + type testCase struct { + input Set + expectation attr.Type + } + tests := map[string]testCase{ + "known": { + input: SetValue( + StringType, + []attr.Value{ + StringValue("hello"), + StringValue("world"), + }, + ), + expectation: SetType{ElemType: StringType}, + }, + "known-set-of-sets": { + input: SetValue( + SetType{ + ElemType: StringType, + }, + []attr.Value{ + SetValue( + StringType, + []attr.Value{ + StringValue("hello"), + StringValue("world"), + }, + ), + SetValue( + StringType, + []attr.Value{ + StringValue("foo"), + StringValue("bar"), + }, + ), + }, + ), + expectation: SetType{ + ElemType: SetType{ + ElemType: StringType, + }, + }, + }, + "unknown": { + input: SetUnknown(StringType), + expectation: SetType{ElemType: StringType}, + }, + "null": { + input: SetNull(StringType), + expectation: SetType{ElemType: StringType}, + }, + "deprecated-known": { + input: Set{ + ElemType: StringType, + Elems: []attr.Value{ + String{Value: "hello"}, + String{Value: "world"}, + }, + }, + expectation: SetType{ElemType: StringType}, + }, + "deprecated-known-set-of-sets": { + input: Set{ + ElemType: SetType{ + ElemType: StringType, + }, + Elems: []attr.Value{ + Set{ + ElemType: StringType, + Elems: []attr.Value{ + String{Value: "hello"}, + String{Value: "world"}, + }, + }, + Set{ + ElemType: StringType, + Elems: []attr.Value{ + String{Value: "foo"}, + String{Value: "bar"}, + }, + }, + }, + }, + expectation: SetType{ + ElemType: SetType{ + ElemType: StringType, + }, + }, + }, + "deprecated-unknown": { + input: Set{ElemType: StringType, Unknown: true}, + expectation: SetType{ElemType: StringType}, + }, + "deprecated-null": { + input: Set{ElemType: StringType, Null: true}, + expectation: SetType{ElemType: StringType}, + }, + } + + for name, test := range tests { + name, test := name, test + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := test.input.Type(context.Background()) + if !cmp.Equal(got, test.expectation) { + t.Errorf("Expected %q, got %q", test.expectation, got) + } + }) + } +} diff --git a/types/string.go b/types/string.go index def0ffe71..89d008a20 100644 --- a/types/string.go +++ b/types/string.go @@ -12,33 +12,102 @@ var ( _ attr.Value = String{} ) +// StringNull creates a String with a null value. Determine whether the value is +// null via the String type IsNull method. +// +// Setting the deprecated String type Null, Unknown, or Value fields after +// creating a String with this function has no effect. +func StringNull() String { + return String{ + state: valueStateNull, + } +} + +// StringUnknown creates a String with an unknown value. Determine whether the +// value is unknown via the String type IsUnknown method. +// +// Setting the deprecated String type Null, Unknown, or Value fields after +// creating a String with this function has no effect. +func StringUnknown() String { + return String{ + state: valueStateUnknown, + } +} + +// StringValue creates a String with a known value. Access the value via the String +// type ValueString method. +// +// Setting the deprecated String type Null, Unknown, or Value fields after +// creating a String with this function has no effect. +func StringValue(value string) String { + return String{ + state: valueStateKnown, + value: value, + } +} + func stringValueFromTerraform(ctx context.Context, in tftypes.Value) (attr.Value, error) { if !in.IsKnown() { - return String{Unknown: true}, nil + return String{ + Unknown: true, + state: valueStateDeprecated, + }, nil } if in.IsNull() { - return String{Null: true}, nil + return String{ + Null: true, + state: valueStateDeprecated, + }, nil } var s string err := in.As(&s) if err != nil { return nil, err } - return String{Value: s}, nil + return String{ + Value: s, + state: valueStateDeprecated, + }, nil } // String represents a UTF-8 string value. type String struct { // Unknown will be true if the value is not yet known. + // + // If the String was created with the StringValue, StringNull, or StringUnknown + // functions, changing this field has no effect. + // + // Deprecated: Use the StringUnknown function to create an unknown String + // value or use the IsUnknown method to determine whether the String value + // is unknown instead. Unknown bool // Null will be true if the value was not set, or was explicitly set to // null. + // + // If the String was created with the StringValue, StringNull, or StringUnknown + // functions, changing this field has no effect. + // + // Deprecated: Use the StringNull function to create a null String value or + // use the IsNull method to determine whether the String value is null + // instead. Null bool // Value contains the set value, as long as Unknown and Null are both // false. + // + // If the String was created with the StringValue, StringNull, or StringUnknown + // functions, changing this field has no effect. + // + // Deprecated: Use the StringValue function to create a known String value or + // use the ValueString method to retrieve the String value instead. Value string + + // state represents whether the String is null, unknown, or known. + state valueState + + // value contains the known value, if not null or unknown. + value string } // Type returns a StringType. @@ -48,16 +117,31 @@ func (s String) Type(_ context.Context) attr.Type { // ToTerraformValue returns the data contained in the *String as a tftypes.Value. func (s String) ToTerraformValue(_ context.Context) (tftypes.Value, error) { - if s.Null { + switch s.state { + case valueStateDeprecated: + if s.Null { + return tftypes.NewValue(tftypes.String, nil), nil + } + if s.Unknown { + return tftypes.NewValue(tftypes.String, tftypes.UnknownValue), nil + } + if err := tftypes.ValidateValue(tftypes.String, s.Value); err != nil { + return tftypes.NewValue(tftypes.String, tftypes.UnknownValue), err + } + return tftypes.NewValue(tftypes.String, s.Value), nil + case valueStateKnown: + if err := tftypes.ValidateValue(tftypes.String, s.value); err != nil { + return tftypes.NewValue(tftypes.String, tftypes.UnknownValue), err + } + + return tftypes.NewValue(tftypes.String, s.value), nil + case valueStateNull: return tftypes.NewValue(tftypes.String, nil), nil - } - if s.Unknown { + case valueStateUnknown: return tftypes.NewValue(tftypes.String, tftypes.UnknownValue), nil + default: + panic(fmt.Sprintf("unhandled String state in ToTerraformValue: %s", s.state)) } - if err := tftypes.ValidateValue(tftypes.String, s.Value); err != nil { - return tftypes.NewValue(tftypes.String, tftypes.UnknownValue), err - } - return tftypes.NewValue(tftypes.String, s.Value), nil } // Equal returns true if `other` is a String and has the same value as `s`. @@ -66,6 +150,12 @@ func (s String) Equal(other attr.Value) bool { if !ok { return false } + if s.state != o.state { + return false + } + if s.state == valueStateKnown { + return s.value == o.value + } if s.Unknown != o.Unknown { return false } @@ -77,25 +167,49 @@ func (s String) Equal(other attr.Value) bool { // IsNull returns true if the String represents a null value. func (s String) IsNull() bool { - return s.Null + if s.state == valueStateNull { + return true + } + + return s.state == valueStateDeprecated && s.Null } // IsUnknown returns true if the String represents a currently unknown value. func (s String) IsUnknown() bool { - return s.Unknown + if s.state == valueStateUnknown { + return true + } + + return s.state == valueStateDeprecated && s.Unknown } -// String returns a human-readable representation of the String value. +// String returns a human-readable representation of the String value. Use +// the ValueString method for Terraform data handling instead. +// // The string returned here is not protected by any compatibility guarantees, // and is intended for logging and error reporting. func (s String) String() string { - if s.Unknown { + if s.IsUnknown() { return attr.UnknownValueString } - if s.Null { + if s.IsNull() { return attr.NullValueString } + if s.state == valueStateKnown { + return fmt.Sprintf("%q", s.value) + } + return fmt.Sprintf("%q", s.Value) } + +// ValueString returns the known string value. If String is null or unknown, returns +// "". +func (s String) ValueString() string { + if s.state == valueStateDeprecated { + return s.Value + } + + return s.value +} diff --git a/types/string_test.go b/types/string_test.go index cb0088dbe..d0b978443 100644 --- a/types/string_test.go +++ b/types/string_test.go @@ -10,6 +10,84 @@ import ( "github.com/hashicorp/terraform-plugin-go/tftypes" ) +// This test verifies the assumptions that creating the Value via function then +// setting the fields directly has no effects. +func TestStringValueDeprecatedFieldSetting(t *testing.T) { + t.Parallel() + + knownString := StringValue("test") + + knownString.Null = true + + if knownString.IsNull() { + t.Error("unexpected null update after Null field setting") + } + + knownString.Unknown = true + + if knownString.IsUnknown() { + t.Error("unexpected unknown update after Unknown field setting") + } + + knownString.Value = "not-test" + + if knownString.ValueString() == "not-test" { + t.Error("unexpected value update after Value field setting") + } +} + +// This test verifies the assumptions that creating the Value via function then +// setting the fields directly has no effects. +func TestStringNullDeprecatedFieldSetting(t *testing.T) { + t.Parallel() + + nullString := StringNull() + + nullString.Null = false + + if !nullString.IsNull() { + t.Error("unexpected null update after Null field setting") + } + + nullString.Unknown = true + + if nullString.IsUnknown() { + t.Error("unexpected unknown update after Unknown field setting") + } + + nullString.Value = "test" + + if nullString.ValueString() == "test" { + t.Error("unexpected value update after Value field setting") + } +} + +// This test verifies the assumptions that creating the Value via function then +// setting the fields directly has no effects. +func TestStringUnknownDeprecatedFieldSetting(t *testing.T) { + t.Parallel() + + unknownString := StringUnknown() + + unknownString.Null = true + + if unknownString.IsNull() { + t.Error("unexpected null update after Null field setting") + } + + unknownString.Unknown = false + + if !unknownString.IsUnknown() { + t.Error("unexpected unknown update after Unknown field setting") + } + + unknownString.Value = "test" + + if unknownString.ValueString() == "test" { + t.Error("unexpected value update after Value field setting") + } +} + func TestStringValueFromTerraform(t *testing.T) { t.Parallel() @@ -89,15 +167,27 @@ func TestStringToTerraformValue(t *testing.T) { expectation interface{} } tests := map[string]testCase{ - "value": { + "known": { + input: StringValue("test"), + expectation: tftypes.NewValue(tftypes.String, "test"), + }, + "unknown": { + input: StringUnknown(), + expectation: tftypes.NewValue(tftypes.String, tftypes.UnknownValue), + }, + "null": { + input: StringNull(), + expectation: tftypes.NewValue(tftypes.String, nil), + }, + "deprecated-known": { input: String{Value: "hello"}, expectation: tftypes.NewValue(tftypes.String, "hello"), }, - "unknown": { + "deprecated-unknown": { input: String{Unknown: true}, expectation: tftypes.NewValue(tftypes.String, tftypes.UnknownValue), }, - "null": { + "deprecated-null": { input: String{Null: true}, expectation: tftypes.NewValue(tftypes.String, nil), }, @@ -129,82 +219,182 @@ func TestStringEqual(t *testing.T) { expectation bool } tests := map[string]testCase{ - "value-value": { + "known-known-same": { + input: StringValue("test"), + candidate: StringValue("test"), + expectation: true, + }, + "known-known-diff": { + input: StringValue("test"), + candidate: StringValue("not-test"), + expectation: false, + }, + "known-unknown": { + input: StringValue("test"), + candidate: StringUnknown(), + expectation: false, + }, + "known-null": { + input: StringValue("test"), + candidate: StringNull(), + expectation: false, + }, + "unknown-value": { + input: StringUnknown(), + candidate: StringValue("test"), + expectation: false, + }, + "unknown-unknown": { + input: StringUnknown(), + candidate: StringUnknown(), + expectation: true, + }, + "unknown-null": { + input: StringUnknown(), + candidate: StringNull(), + expectation: false, + }, + "null-known": { + input: StringNull(), + candidate: StringValue("test"), + expectation: false, + }, + "null-unknown": { + input: StringNull(), + candidate: StringUnknown(), + expectation: false, + }, + "null-null": { + input: StringNull(), + candidate: StringNull(), + expectation: true, + }, + "deprecated-known-known-same": { + input: String{Value: "test"}, + candidate: StringValue("test"), + expectation: false, // intentional + }, + "deprecated-known-known-diff": { + input: String{Value: "test"}, + candidate: StringValue("not-test"), + expectation: false, + }, + "deprecated-known-unknown": { + input: String{Value: "test"}, + candidate: StringUnknown(), + expectation: false, + }, + "deprecated-known-null": { + input: String{Value: "test"}, + candidate: StringNull(), + expectation: false, + }, + "deprecated-known-deprecated-known-same": { input: String{Value: "hello"}, candidate: String{Value: "hello"}, expectation: true, }, - "value-diff": { + "deprecated-known-deprecated-known-diff": { input: String{Value: "hello"}, candidate: String{Value: "world"}, expectation: false, }, - "value-unknown": { + "deprecated-known-deprecated-unknown": { input: String{Value: "hello"}, candidate: String{Unknown: true}, expectation: false, }, - "value-null": { + "deprecated-known-deprecated-null": { input: String{Value: "hello"}, candidate: String{Null: true}, expectation: false, }, - "value-wrongType": { + "deprecated-known-wrongType": { input: String{Value: "hello"}, candidate: Number{Value: big.NewFloat(123)}, expectation: false, }, - "value-nil": { + "deprecated-known-nil": { input: String{Value: "hello"}, candidate: nil, expectation: false, }, - "unknown-value": { + "deprecated-unknown-value": { + input: String{Unknown: true}, + candidate: StringValue("test"), + expectation: false, + }, + "deprecated-unknown-unknown": { + input: String{Unknown: true}, + candidate: StringUnknown(), + expectation: false, // intentional + }, + "deprecated-unknown-null": { + input: String{Unknown: true}, + candidate: StringNull(), + expectation: false, + }, + "deprecated-unknown-deprecated-known": { input: String{Unknown: true}, candidate: String{Value: "hello"}, expectation: false, }, - "unknown-unknown": { + "deprecated-unknown-deprecated-unknown": { input: String{Unknown: true}, candidate: String{Unknown: true}, expectation: true, }, - "unknown-null": { + "deprecated-unknown-deprecated-null": { input: String{Unknown: true}, candidate: String{Null: true}, expectation: false, }, - "unknown-wrongType": { + "deprecated-unknown-wrongType": { input: String{Unknown: true}, candidate: Number{Value: big.NewFloat(123)}, expectation: false, }, - "unknown-nil": { + "deprecated-unknown-nil": { input: String{Unknown: true}, candidate: nil, expectation: false, }, - "null-value": { + "deprecated-null-known": { + input: String{Null: true}, + candidate: StringValue("test"), + expectation: false, + }, + "deprecated-null-unknown": { + input: String{Null: true}, + candidate: StringUnknown(), + expectation: false, + }, + "deprecated-null-null": { + input: String{Null: true}, + candidate: StringNull(), + expectation: false, // intentional + }, + "deprecated-null-deprecated-known": { input: String{Null: true}, candidate: String{Value: "hello"}, expectation: false, }, - "null-unknown": { + "deprecated-null-deprecated-unknown": { input: String{Null: true}, candidate: String{Unknown: true}, expectation: false, }, - "null-null": { + "deprecated-null-deprecated-null": { input: String{Null: true}, candidate: String{Null: true}, expectation: true, }, - "null-wrongType": { + "deprecated-null-wrongType": { input: String{Null: true}, candidate: Number{Value: big.NewFloat(123)}, expectation: false, }, - "null-nil": { + "deprecated-null-nil": { input: String{Null: true}, candidate: nil, expectation: false, @@ -223,6 +413,110 @@ func TestStringEqual(t *testing.T) { } } +func TestStringIsNull(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + input String + expected bool + }{ + "known": { + input: StringValue("test"), + expected: false, + }, + "deprecated-known": { + input: String{Value: "test"}, + expected: false, + }, + "null": { + input: StringNull(), + expected: true, + }, + "deprecated-null": { + input: String{Null: true}, + expected: true, + }, + "unknown": { + input: StringUnknown(), + expected: false, + }, + "deprecated-unknown": { + input: String{Unknown: true}, + expected: false, + }, + "deprecated-invalid": { + input: String{Null: true, Unknown: true}, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.input.IsNull() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestStringIsUnknown(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + input String + expected bool + }{ + "known": { + input: StringValue("test"), + expected: false, + }, + "deprecated-known": { + input: String{Value: "test"}, + expected: false, + }, + "null": { + input: StringNull(), + expected: false, + }, + "deprecated-null": { + input: String{Null: true}, + expected: false, + }, + "unknown": { + input: StringUnknown(), + expected: true, + }, + "deprecated-unknown": { + input: String{Unknown: true}, + expected: true, + }, + "deprecated-invalid": { + input: String{Null: true, Unknown: true}, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.input.IsUnknown() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestStringString(t *testing.T) { t.Parallel() @@ -231,27 +525,43 @@ func TestStringString(t *testing.T) { expectation string } tests := map[string]testCase{ - "simple": { + "known-non-empty": { + input: StringValue("test"), + expectation: `"test"`, + }, + "known-empty": { + input: StringValue(""), + expectation: `""`, + }, + "known-quotes": { + input: StringValue(`testing is "fun"`), + expectation: `"testing is \"fun\""`, + }, + "unknown": { + input: StringUnknown(), + expectation: "", + }, + "null": { + input: StringNull(), + expectation: "", + }, + "deprecated-known-non-empty": { input: String{Value: "simple"}, expectation: `"simple"`, }, - "long-string": { - input: String{Value: "a really, really, really long string"}, - expectation: `"a really, really, really long string"`, - }, - "empty-string": { + "deprecated-known-empty": { input: String{Value: ""}, expectation: `""`, }, - "quotes": { + "deprecated-known-quotes": { input: String{Value: `testing is "fun"`}, expectation: `"testing is \"fun\""`, }, - "unknown": { + "deprecated-unknown": { input: String{Unknown: true}, expectation: "", }, - "null": { + "deprecated-null": { input: String{Null: true}, expectation: "", }, @@ -273,3 +583,55 @@ func TestStringString(t *testing.T) { }) } } + +func TestStringValueString(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + input String + expected string + }{ + "known": { + input: StringValue("test"), + expected: "test", + }, + "deprecated-known": { + input: String{Value: "test"}, + expected: "test", + }, + "null": { + input: StringNull(), + expected: "", + }, + "deprecated-null": { + input: String{Null: true}, + expected: "", + }, + "unknown": { + input: StringUnknown(), + expected: "", + }, + "deprecated-unknown": { + input: String{Unknown: true}, + expected: "", + }, + "deprecated-invalid": { + input: String{Null: true, Unknown: true}, + expected: "", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.input.ValueString() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/types/value_state.go b/types/value_state.go new file mode 100644 index 000000000..3ec78adcd --- /dev/null +++ b/types/value_state.go @@ -0,0 +1,42 @@ +package types + +import "fmt" + +const ( + // valueStateDeprecated represents a value where it can potentially be + // controlled by exported fields such as Null and Unknown. Since consumers + // can adjust those fields directly and it would not modify the internal + // valueState value, this sentinel value is a placeholder which can be + // checked in logic before assuming the valueState value is accurate. + // + // This value is 0 so it is the zero-value of existing implementations to + // preserve existing behaviors. A future version will switch the zero-value + // to null and export this implementation in the attr package. + valueStateDeprecated valueState = 0 + + // valueStateNull represents a value which is null. + valueStateNull valueState = 1 + + // valueStateUnknown represents a value which is unknown. + valueStateUnknown valueState = 2 + + // valueStateKnown represents a value which is known (not null or unknown). + valueStateKnown valueState = 3 +) + +type valueState uint8 + +func (s valueState) String() string { + switch s { + case valueStateDeprecated: + return "deprecated" + case valueStateKnown: + return "known" + case valueStateNull: + return "null" + case valueStateUnknown: + return "unknown" + default: + panic(fmt.Sprintf("unhandled valueState in String: %d", s)) + } +} diff --git a/website/docs/plugin/framework/accessing-values.mdx b/website/docs/plugin/framework/accessing-values.mdx index 376ae7f37..f23048014 100644 --- a/website/docs/plugin/framework/accessing-values.mdx +++ b/website/docs/plugin/framework/accessing-values.mdx @@ -55,7 +55,7 @@ func (r ThingResource) Create(ctx context.Context, return } - // values can now be accessed like plan.Name.Value + // values can now be accessed like plan.Name.ValueString() // check if things are null with plan.Name.IsNull() // check if things are unknown with plan.Name.IsUnknown() } diff --git a/website/docs/plugin/framework/data-sources/index.mdx b/website/docs/plugin/framework/data-sources/index.mdx index fd4728994..d9d8a8560 100644 --- a/website/docs/plugin/framework/data-sources/index.mdx +++ b/website/docs/plugin/framework/data-sources/index.mdx @@ -61,7 +61,7 @@ func (d *ThingDataSource) Read(ctx context.Context, req datasource.ReadRequest, // Typically data sources will make external calls, however this example // hardcodes setting the id attribute to a specific value for brevity. - data.ID = types.String{Value: "example-id"} + data.ID = types.StringValue("example-id") // Save data into Terraform state resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) diff --git a/website/docs/plugin/framework/migrating/attributes-blocks/default-values.mdx b/website/docs/plugin/framework/migrating/attributes-blocks/default-values.mdx index 3f6a117d8..724dc1387 100644 --- a/website/docs/plugin/framework/migrating/attributes-blocks/default-values.mdx +++ b/website/docs/plugin/framework/migrating/attributes-blocks/default-values.mdx @@ -49,7 +49,7 @@ func (r *resourceExample) GetSchema(context.Context) (tfsdk.Schema, diag.Diagnos Attributes: map[string]tfsdk.Attribute{ "attribute_example": { PlanModifiers: []tfsdk.AttributePlanModifier{ - defaultValue(types.Bool{Value: true}), + defaultValue(types.BoolValue(true)), /* ... */ ``` @@ -98,7 +98,7 @@ func (r *privateKeyResource) GetSchema(_ context.Context) (tfsdk.Schema, diag.Di Attributes: map[string]tfsdk.Attribute{ "rsa_bits": { PlanModifiers: []tfsdk.AttributePlanModifier{ - attribute_plan_modifier.DefaultValue(types.Int64{Value: 2048}), + attribute_plan_modifier.DefaultValue(types.Int64Value(2048)), /* ... */ }, /* ... */ diff --git a/website/docs/plugin/framework/migrating/resources/crud.mdx b/website/docs/plugin/framework/migrating/resources/crud.mdx index 08ef3705e..39d2669b1 100644 --- a/website/docs/plugin/framework/migrating/resources/crud.mdx +++ b/website/docs/plugin/framework/migrating/resources/crud.mdx @@ -142,16 +142,16 @@ func (r *passwordResource) Create(ctx context.Context, req resource.CreateReques } params := random.StringParams{ - Length: plan.Length.Value, - Upper: plan.Upper.Value, - MinUpper: plan.MinUpper.Value, - Lower: plan.Lower.Value, - MinLower: plan.MinLower.Value, - Numeric: plan.Numeric.Value, - MinNumeric: plan.MinNumeric.Value, - Special: plan.Special.Value, - MinSpecial: plan.MinSpecial.Value, - OverrideSpecial: plan.OverrideSpecial.Value, + Length: plan.Length.ValueInt64(), + Upper: plan.Upper.ValueBool(), + MinUpper: plan.MinUpper.ValueInt64(), + Lower: plan.Lower.ValueBool(), + MinLower: plan.MinLower.ValueInt64(), + Numeric: plan.Numeric.ValueBool(), + MinNumeric: plan.MinNumeric.ValueInt64(), + Special: plan.Special.ValueBool(), + MinSpecial: plan.MinSpecial.ValueInt64(), + OverrideSpecial: plan.OverrideSpecial.ValueString(), } result, err := random.CreateString(params) @@ -165,9 +165,9 @@ func (r *passwordResource) Create(ctx context.Context, req resource.CreateReques resp.Diagnostics.Append(diagnostics.HashGenerationError(err.Error())...) } - plan.BcryptHash = types.String{Value: hash} - plan.ID = types.String{Value: "none"} - plan.Result = types.String{Value: string(result)} + plan.BcryptHash = types.StringValue(hash) + plan.ID = types.StringValue("none") + plan.Result = types.StringValue(string(result)) diags = resp.State.Set(ctx, plan) resp.Diagnostics.Append(diags...) diff --git a/website/docs/plugin/framework/migrating/resources/import.mdx b/website/docs/plugin/framework/migrating/resources/import.mdx index 938c95b49..55bce56f3 100644 --- a/website/docs/plugin/framework/migrating/resources/import.mdx +++ b/website/docs/plugin/framework/migrating/resources/import.mdx @@ -164,17 +164,17 @@ func (r *passwordResource) ImportState(ctx context.Context, req resource.ImportS id := req.ID state := passwordModelV2{ - ID: types.String{Value: "none"}, - Result: types.String{Value: id}, - Length: types.Int64{Value: int64(len(id))}, - Special: types.Bool{Value: true}, - Upper: types.Bool{Value: true}, - Lower: types.Bool{Value: true}, - Numeric: types.Bool{Value: true}, - MinSpecial: types.Int64{Value: 0}, - MinUpper: types.Int64{Value: 0}, - MinLower: types.Int64{Value: 0}, - MinNumeric: types.Int64{Value: 0}, + ID: types.StringValue("none"), + Result: types.StringValue(id), + Length: types.Int64Value(int64(len(id))), + Special: types.BoolValue(true), + Upper: types.BoolValue(true), + Lower: types.BoolValue(true), + Numeric: types.BoolValue(true), + MinSpecial: types.Int64Value(0), + MinUpper: types.Int64Value(0), + MinLower: types.Int64Value(0), + MinNumeric: types.Int64Value(0), } state.Keepers.ElemType = types.StringType @@ -184,7 +184,7 @@ func (r *passwordResource) ImportState(ctx context.Context, req resource.ImportS resp.Diagnostics.Append(diagnostics.HashGenerationError(err.Error())...) } - state.BcryptHash = types.String{Value: hash} + state.BcryptHash = types.StringValue(hash) diags := resp.State.Set(ctx, &state) resp.Diagnostics.Append(diags...) diff --git a/website/docs/plugin/framework/migrating/resources/plan-modification.mdx b/website/docs/plugin/framework/migrating/resources/plan-modification.mdx index 0d9f04820..7524de5e4 100644 --- a/website/docs/plugin/framework/migrating/resources/plan-modification.mdx +++ b/website/docs/plugin/framework/migrating/resources/plan-modification.mdx @@ -149,21 +149,21 @@ func (d *numberNumericAttributePlanModifier) MarkdownDescription(ctx context.Con } func (d *numberNumericAttributePlanModifier) Modify(ctx context.Context, req tfsdk.ModifyAttributePlanRequest, resp *tfsdk.ModifyAttributePlanResponse) { - numberConfig := types.Bool{} + var numberConfig types.Bool diags := req.Config.GetAttribute(ctx, path.Root("number"), &numberConfig) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return } - numericConfig := types.Bool{} + var numericConfig types.Bool req.Config.GetAttribute(ctx, path.Root("numeric"), &numericConfig) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return } - if !numberConfig.Null && !numericConfig.Null && (numberConfig.Value != numericConfig.Value) { + if !numberConfig.IsNull() && !numericConfig.IsNull() && (numberConfig.ValueBool() != numericConfig.ValueBool()) { resp.Diagnostics.AddError( "Number and numeric are both configured with different values", "Number is deprecated, use numeric instead", @@ -172,19 +172,19 @@ func (d *numberNumericAttributePlanModifier) Modify(ctx context.Context, req tfs } // Default to true for both number and numeric when both are null. - if numberConfig.Null && numericConfig.Null { - resp.AttributePlan = types.Bool{Value: true} + if numberConfig.IsNull() && numericConfig.IsNull() { + resp.AttributePlan = types.BoolValue(true) return } // Default to using value for numeric if number is null - if numberConfig.Null && !numericConfig.Null { + if numberConfig.IsNull() && !numericConfig.IsNull() { resp.AttributePlan = numericConfig return } // Default to using value for number if numeric is null - if !numberConfig.Null && numericConfig.Null { + if !numberConfig.IsNull() && numericConfig.IsNull() { resp.AttributePlan = numberConfig return } diff --git a/website/docs/plugin/framework/migrating/resources/state-upgrade.mdx b/website/docs/plugin/framework/migrating/resources/state-upgrade.mdx index b46ca98d8..1e74df312 100644 --- a/website/docs/plugin/framework/migrating/resources/state-upgrade.mdx +++ b/website/docs/plugin/framework/migrating/resources/state-upgrade.mdx @@ -195,13 +195,13 @@ func upgradePasswordStateV0toV2(ctx context.Context, req resource.UpgradeStateRe ID: passwordDataV0.ID, } - hash, err := generateHash(passwordDataV2.Result.Value) + hash, err := generateHash(passwordDataV2.Result.ValueString()) if err != nil { resp.Diagnostics.Append(diagnostics.HashGenerationError(err.Error())...) return } - passwordDataV2.BcryptHash.Value = hash + passwordDataV2.BcryptHash = types.StringValue(hash) diags := resp.State.Set(ctx, passwordDataV2) resp.Diagnostics.Append(diags...) diff --git a/website/docs/plugin/framework/paths.mdx b/website/docs/plugin/framework/paths.mdx index 93eae9475..cd1d13993 100644 --- a/website/docs/plugin/framework/paths.mdx +++ b/website/docs/plugin/framework/paths.mdx @@ -290,7 +290,7 @@ The path which matches the set associated with the `root_set_attribute` attribut path.Root("root_set_attribute") ``` -Examples below will presume a `nested_string_attribute` string value of `types.String{Value: "example"}` for brevity. In real world usage, the string value may be `types.String{Null: true}`, `types.String{Unknown: true}` or `types.String{Value: "something-else"}`, which are all considered different set paths from each other. Each additional attribute or block introduces exponentially more possible paths given each attribute or block value may be null, unknown, or a unique known value. +Examples below will presume a `nested_string_attribute` string value of `types.StringValue("example")` for brevity. In real world usage, the string value may be `types.StringNull()`, `types.StringUnknown()` or `types.StringValue("something-else")`, which are all considered different set paths from each other. Each additional attribute or block introduces exponentially more possible paths given each attribute or block value may be null, unknown, or a unique known value. The path which matches the object associated with the `root_set_attribute` block is: @@ -300,7 +300,7 @@ path.Root("root_set_attribute").AtSetValue(types.Object{ "nested_string_attribute": types.StringType, }, Attrs: map[string]attr.Value{ - "nested_string_attribute": types.String{Value: "example"}, + "nested_string_attribute": types.StringValue("example"), } }) ``` @@ -313,7 +313,7 @@ path.Root("root_set_attribute").AtSetValue(types.Object{ "nested_string_attribute": types.StringType, }, Attrs: map[string]attr.Value{ - "nested_string_attribute": types.String{Value: "example"}, + "nested_string_attribute": types.StringValue("example"), } }).AtName("nested_string_attribute") ``` @@ -457,7 +457,7 @@ The path which matches the set associated with the `root_set_block` block is: path.Root("root_set_block") ``` -Examples below will presume a `block_string_attribute` string value of `types.String{Value: "example"}` for brevity. In real world usage, the string value may be `types.String{Null: true}`, `types.String{Unknown: true}` or `types.String{Value: "something-else"}`, which are all considered different set paths from each other. Each additional attribute or block introduces exponentially more possible paths given each attribute or block value may be null, unknown, or a unique known value. +Examples below will presume a `block_string_attribute` string value of `types.StringValue("example")` for brevity. In real world usage, the string value may be `types.StringNull()`, `types.StringUnknown()` or `types.StringValue("something-else")`, which are all considered different set paths from each other. Each additional attribute or block introduces exponentially more possible paths given each attribute or block value may be null, unknown, or a unique known value. The path which matches the object associated with the `root_set_block` block is: @@ -467,7 +467,7 @@ path.Root("root_set_block").AtSetValue(types.Object{ "block_string_attribute": types.StringType, }, Attrs: map[string]attr.Value{ - "block_string_attribute": types.String{Value: "example"}, + "block_string_attribute": types.StringValue("example"), } }) ``` @@ -480,7 +480,7 @@ path.Root("root_set_block").AtSetValue(types.Object{ "block_string_attribute": types.StringType, }, Attrs: map[string]attr.Value{ - "block_string_attribute": types.String{Value: "example"}, + "block_string_attribute": types.StringValue("example"), } }).AtName("block_string_attribute") ``` diff --git a/website/docs/plugin/framework/providers/index.mdx b/website/docs/plugin/framework/providers/index.mdx index d47347ca1..e72feb6c6 100644 --- a/website/docs/plugin/framework/providers/index.mdx +++ b/website/docs/plugin/framework/providers/index.mdx @@ -153,12 +153,12 @@ func (p *ExampleCloudProvider) Configure(ctx context.Context, req provider.Confi // Check configuration data, which should take precedence over // environment variable data, if found. - if data.ApiToken.Value != "" { - apiToken = data.ApiToken.Value + if data.ApiToken.ValueString() != "" { + apiToken = data.ApiToken.ValueString() } - if data.Endpoint.Value != "" { - endpoint = data.Endpoint.Value + if data.Endpoint.ValueString() != "" { + endpoint = data.Endpoint.ValueString() } if apiToken == "" { diff --git a/website/docs/plugin/framework/resources/index.mdx b/website/docs/plugin/framework/resources/index.mdx index 5051d8d97..22d00df0a 100644 --- a/website/docs/plugin/framework/resources/index.mdx +++ b/website/docs/plugin/framework/resources/index.mdx @@ -69,7 +69,7 @@ func (r *ThingResource) Create(ctx context.Context, req resource.CreateRequest, // Typically resources will make external calls, however this example // hardcodes setting the id attribute to a specific value for brevity. - data.ID = types.String{Value: "example-id"} + data.ID = types.StringValue("example-id") // Save data into Terraform state resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) diff --git a/website/docs/plugin/framework/resources/plan-modification.mdx b/website/docs/plugin/framework/resources/plan-modification.mdx index bb0eafdf3..e03afba3d 100644 --- a/website/docs/plugin/framework/resources/plan-modification.mdx +++ b/website/docs/plugin/framework/resources/plan-modification.mdx @@ -85,6 +85,11 @@ func (m stringDefaultModifier) MarkdownDescription(ctx context.Context) string { // `resp` contains fields for updating the planned value, triggering resource // replacement, and returning diagnostics. func (m stringDefaultModifier) Modify(ctx context.Context, req tfsdk.ModifyAttributePlanRequest, resp *tfsdk.ModifyAttributePlanResponse) { + // If the value is unknown or known, do not set default value. + if !req.AttributePlan.IsNull() { + return + } + // types.String must be the attr.Value produced by the attr.Type in the schema for this attribute // for generic plan modifiers, use // https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/tfsdk#ConvertValue @@ -96,11 +101,7 @@ func (m stringDefaultModifier) Modify(ctx context.Context, req tfsdk.ModifyAttri return } - if !str.Null { - return - } - - resp.AttributePlan = types.String{Value: m.Default} + resp.AttributePlan = types.StringValue(m.Default) } ``` diff --git a/website/docs/plugin/framework/types.mdx b/website/docs/plugin/framework/types.mdx index d095a03d4..1434bc7a4 100644 --- a/website/docs/plugin/framework/types.mdx +++ b/website/docs/plugin/framework/types.mdx @@ -65,182 +65,268 @@ guaranteed to have known values (or be null). Provider configuration values can be unknown, and providers should handle that situation, even if that means just returning an error. -## Built-In Types and Values - -A collection of attribute type and attribute value implementations is available -in the -[`types`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types) -package. +## Framework Types and Value Types + +A collection of attribute types (`attr.Type`) and attribute value types (`attr.Value`) is available in the [`types` package](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types). These types bridge the implementation details between Terraform's type system and Go code in providers. + +| Terraform Type | Framework Schema Type | Framework Value Type | Known Value Go Type | Use Case | +|----------------|-----------------------|----------------------|---------------------|----------| +| `bool` | `types.BoolType` | `types.Bool` | `bool` | Boolean true or false | +| `number` | `types.Float64Type` | `types.Float64` | `float64` | 64-bit floating point number | +| `number` | `types.Int64Type` | `types.Int64` | `int64` | 64-bit integer | +| `list` | `types.ListType` | `types.List` | `[]attr.Value` | Ordered collection of single element type | +| `map` | `types.MapType` | `types.Map` | `map[string]attr.Value` | Mapping of string keys to single element type | +| `number` | `types.NumberType` | `types.Number` | `*big.Float` | Large floating point or number | +| `object` | `types.ObjectType` | `types.Object` | `map[string]attr.Value` | Structure mapping string attibute keys to any value type | +| `set` | `types.SetType` | `types.Set` | `[]attr.Value` | Unordered, unique collection of single element type | +| `string` | `types.StringType` | `types.String` | `string` | Collection of UTF-8 encoded characters | ### StringType and String Strings are a UTF-8 encoded collection of bytes. +Given an example Terraform configuration that sets a string value to the `example_attribute` attribute: + ```tf -hello = "world" +example_attribute = "terraform" +``` + +The associated schema type is [`types.StringType`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types#StringType): + +```go +"example_attribute": { + Type: types.StringType, + // ... other fields ... +} ``` -They are used by specifying the `types.StringType` constant in your -`tfsdk.Attribute`'s `Type` property, and are represented by a `types.String` -struct in config, state, and plan. The `types.String` struct has the following -properties: +The associated value type is [`types.String`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types#String) in configuration, plan, and state data. + +Access `types.String` information via the following methods: + +* [`(types.String).IsNull() bool`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types#String.IsNull): Returns true if the string is null. +* [`(types.String).IsUnknown() bool`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types#String.IsUnknown): Returns true if the string is unknown. +* [`(types.String).ValueString() string`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types#String.ValueString): Returns the known string, or an empty string if null or unknown. + +The `(types.String).String()` method is reserved for debugging purposes and returns `""` if the value is null and `""` if the value is unknown. Use `(types.String).ValueString()` for any Terraform data handling. + +Call one of the following to create a `types.String`: -* `Value` contains the string's value as a Go `string` type. -* `Null` is set to `true` when the string's value is null. -* `Unknown` is set to `true` when the string's value is unknown. +* [`types.StringNull()`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types#StringNull): A null string value. +* [`types.StringUnknown()`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types#StringUnknown): An unknown string value. +* [`types.StringValue(string)`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types#StringValue): A known value. ### Int64Type and Int64 -Int64 are 64-bit integer values, such as `1234`. +Int64 are 64-bit integer values, such as `1234`. For 64-bit floating point numbers, use [`Float64Type` and `Float64`](#float64type-and-float64). For generic number handling, use [`NumberType` and `Number`](#numbertype-and-number). + +Given an example Terraform configuration that sets an integer value to the `example_attribute` attribute: ```tf -hello = 1234 +example_attribute = 1234 +``` + +The associated schema type is [`types.Int64Type`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types#Int64Type): + +```go +"example_attribute": { + Type: types.Int64Type, + // ... other fields ... +} ``` -They are used by specifying the `types.Int64Type` constant in your -`tfsdk.Attribute`'s `Type` property, and are represented by a `types.Int64` -struct in config, state, and plan. The `types.Int64` struct has the following -properties: +The associated value type is [`types.Int64`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types#Int64) in configuration, plan, and state data. -* `Value` contains the number's value as a Go `int64` type. -* `Null` is set to `true` when the number's value is null. -* `Unknown` is set to `true` when the number's value is unknown. +Access `types.Int64` information via the following methods: -For 64-bit floating point numbers, see [`Float64Type` and -`Float64`](#float64type-and-float64). For generic number handling, see -[`NumberType` and `Number64`](#numbertype-and-number). +* [`(types.Int64).IsNull() bool`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types#Int64.IsNull): Returns true if the integer is null. +* [`(types.Int64).IsUnknown() bool`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types#Int64.IsUnknown): Returns true if the integer is unknown. +* [`(types.Int64).ValueInt64() int64`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types#Int64.ValueInt64): Returns the known `int64` value, or `0` if null or unknown. + +Call one of the following to create a `types.Int64`: + +* [`types.Int64Null()`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types#Int64Null): A null integer value. +* [`types.Int64Unknown()`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types#Int64Unknown): An unknown integer value. +* [`types.Int64Value(int64)`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types#Int64Value): A known value. ### Float64Type and Float64 -Float64 are 64-bit floating point values, such as `1234.5`. +Float64 are 64-bit floating point values, such as `1234.5`. For 64-bit integer numbers, use [`Int64Type` and `Int64`](#int64type-and-int64). For generic number handling, use [`NumberType` and `Number`](#numbertype-and-number). + +Given an example Terraform configuration that sets a floating point value to the `example_attribute` attribute: ```tf -hello = 1234.5 +example_attribute = 1234.5 +``` + +The associated schema type is [`types.Float64Type`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types#Float64Type): + +```go +"example_attribute": { + Type: types.Float64Type, + // ... other fields ... +} ``` -They are used by specifying the `types.Float64Type` constant in your -`tfsdk.Attribute`'s `Type` property, and are represented by a `types.Float64` -struct in config, state, and plan. The `types.Float64` struct has the following -properties: +The associated value type is [`types.Float64`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types#Float64) in configuration, plan, and state data. + +Access `types.Float64` information via the following methods: + +* [`(types.Float64).IsNull() bool`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types#Float64.IsNull): Returns true if the number is null. +* [`(types.Float64).IsUnknown() bool`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types#Float64.IsUnknown): Returns true if the number is unknown. +* [`(types.Float64).ValueFloat64() float64`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types#Float64.ValueFloat64): Returns the known `float64` value, or `0.0` if null or unknown. -* `Value` contains the number's value as a Go `float64` type. -* `Null` is set to `true` when the number's value is null. -* `Unknown` is set to `true` when the number's value is unknown. +Call one of the following to create a `types.Float64`: -For 64-bit integer numbers, see [`Int64Type` and -`Int64`](#int64type-and-int64). For generic number handling, see -[`NumberType` and `Number64`](#numbertype-and-number). +* [`types.Float64Null()`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types#Float64Null): A null number value. +* [`types.Float64Unknown()`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types#Float64Unknown): An unknown number value. +* [`types.Float64Value(float64)`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types#Float64Value): A known value. ### NumberType and Number -Numbers are numeric values, both whole values like `12` or fractional values -like `3.14`. +Numbers are numeric values, both whole values like `12` or fractional values like `3.14`. Use this type for exceptionally large numbers. For 64-bit integer numbers, use [`Int64Type` and `Int64`](#int64type-and-int64). For 64-bit floating point numbers, use [`Float64Type` and `Float64`](#float64type-and-float64). + +Given an example Terraform configuration that sets a number value to the `example_attribute` attribute: ```tf -hello = 123 +example_attribute = 123 +``` + +The associated schema type is [`types.NumberType`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types#NumberType): + +```go +"example_attribute": { + Type: types.NumberType, + // ... other fields ... +} ``` -They are used by specifying the `types.NumberType` constant in your -`tfsdk.Attribute`'s `Type` property, and are represented by a `types.Number` -struct in config, state, and plan. The `types.Number` struct has the following -properties: +The associated value type is [`types.Number`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types#Number) in configuration, plan, and state data. -* `Value` contains the number's value as a Go - [`*big.Float`](https://pkg.go.dev/math/big#Float) type. -* `Null` is set to `true` when the number's value is null. -* `Unknown` is set to `true` when the number's value is unknown. +Access `types.Number` information via the following methods: -For 64-bit integer numbers, see [`Int64Type` and -`Int64`](#int64type-and-int64). For 64-bit floating point numbers, see -[`Float64Type` and `Float64`](#float64type-and-float64). +* [`(types.Number).IsNull() bool`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types#Number.IsNull): Returns true if the number is null. +* [`(types.Number).IsUnknown() bool`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types#Number.IsUnknown): Returns true if the number is unknown. +* [`(types.Number).ValueBigFloat() *big.Float`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types#Number.ValueBigFloat): Returns the known `*big.Float` value, or `nil` if null or unknown. + +Call one of the following to create a `types.Number`: + +* [`types.NumberNull()`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types#NumberNull): A null number value. +* [`types.NumberUnknown()`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types#NumberUnknown): An unknown number value. +* [`types.NumberValue(*big.Float)`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types#NumberValue): A known value. ### BoolType and Bool Bools are boolean values that can either be true or false. +Given an example Terraform configuration that sets a boolean value to the `example_attribute` attribute: + ```tf -hello = true +example_attribute = true ``` -They are used by specifying the `types.BoolType` constant in your -`tfsdk.Attribute`'s `Type` property, and are represented by a `types.Bool` -struct in config, state, and plan. The `types.Bool` struct has the following -properties: +The associated schema type is [`types.BoolType`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types#BoolType): + +```go +"example_attribute": { + Type: types.BoolType, + // ... other fields ... +} +``` + +The associated value type is [`types.Bool`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types#Bool) in configuration, plan, and state data. + +Access `types.Bool` information via the following methods: + +* [`(types.Bool).IsNull() bool`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types#Bool.IsNull): Returns true if the boolean is null. +* [`(types.Bool).IsUnknown() bool`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types#Bool.IsUnknown): Returns true if the boolean is unknown. +* [`(types.Bool).ValueBool() bool`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types#Bool.ValueBool): Returns the known `bool` value, or `false` if null or unknown. -* `Value` contains the boolean's value as a Go `bool` type. -* `Null` is set to `true` when the boolean's value is null. -* `Unknown` is set to `true` when the boolean's value is unknown. +Call one of the following to create a `types.Bool`: + +* [`types.BoolNull()`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types#BoolNull): A null boolean value. +* [`types.BoolUnknown()`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types#BoolUnknown): An unknown boolean value. +* [`types.BoolValue(bool)`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types#BoolValue): A known value. ### ListType and List -Lists are ordered collections of other types. Their elements, the values inside -the list, must all be of the same type. +Lists are ordered collections of a single element type. + +-> Use [ListNestedAttributes](/plugin/framework/schemas#ListNestedAttributes) for lists of objects that need additional schema information. Use [SetType and Set](#settype-and-set) for unordered collections. + +Given an example Terraform configuration that sets a list of string values to the `example_attribute` attribute: ```tf -hello = ["red", "blue", "green"] +example_attribute = ["red", "blue", "green"] ``` -They are used by specifying a `types.ListType` value in your -`tfsdk.Attribute`'s `Type` property. You must specify an `ElemType` property -for your list, indicating what type the elements should be. Lists are -represented by a `types.List` struct in config, state, and plan. The -`types.List` struct has the following properties: - -* `ElemType` will always contain the same type as the `ElemType` property of - the `types.ListType` that created the `types.List`. -* `Elem` contains a list of values, one for each element in the list. The - values will all be of the value type produced by the `ElemType` for the list. -* `Null` is set to `true` when the entire list's value is null. Individual - elements may still be null even if the list's `Null` property is `false`. -* `Unknown` is set to `true` when the entire list's value is unknown. - Individual elements may still be unknown even if the list's `Unknown` - property is `false`. +The associated schema type is [`types.ListType`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types#ListType): -Elements of a `types.List` with a non-null, non-unknown value can be accessed -without using type assertions by using the `types.List`'s [`ElementsAs` -method](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types#List.ElementsAs), -which uses the same conversion rules as the `Get` methods described in [Access -State, Config, and Plan](/plugin/framework/accessing-values). +```go +"example_attribute": { + Type: types.ListType{ + ElemType: types.StringType, + }, + // ... other fields ... +} +``` + +The associated value type is [`types.List`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types#List) in configuration, plan, and state data. + +Access `types.List` information via the following methods: -For an unordered collection with uniqueness constraints, see [`SetType` and -`Set`](#settype-and-set). +* [`(types.List).IsNull() bool`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types#List.IsNull): Returns true if the list is null. +* [`(types.List).IsUnknown() bool`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types#List.IsUnknown): Returns true if the list is unknown. Returns false if the number of elements is known, any of which may be unknown. +* [`(types.List).Elements() []attr.Value`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types#List.Elements): Returns the known `[]attr.Value` value, or `nil` if null or unknown. +* [`(types.List).ElementsAs(context.Context, any, bool) diag.Diagnostics`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types#List.ElementsAs): Converts the known values into the given Go type, if possible, using the [conversion rules](/plugin/framework/accessing-values#conversion-rules). + +Call one of the following to create a `types.List`: + +* [`types.ListNull(attr.Type)`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types#ListNull): A null list value with the given element type. +* [`types.ListUnknown(attr.Type)`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types#ListUnknown): An unknown list value with the given element type. +* [`types.ListValue(attr.Type, []attr.Value)`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types#ListValue): A known value with the given element type. ### MapType and Map -Maps are unordered collections of other types with unique string indexes. -Their elements, the values inside the map, must all be of the same type. The keys used to index the elements must be strings, but there are (theoretically) no limitations on what keys are acceptable or how many there -can be. +Maps are mappings of string keys to values of a single element type. + +-> Use [MapNestedAttributes](/plugin/framework/schemas#MapNestedAttributes) for maps of objects that need additional schema information. Use [ObjectType and Object](#objecttype-and-object) for structures of string attribute names to any value. + +Given an example Terraform configuration that sets a map of string values to the `example_attribute` attribute: ```tf -hello = { - pi = 3.14 - random = 4 - "meaning of life" = 42 +example_attribute = { + red = "fire", + blue = "sky", + green = "plant", } ``` -They are used by specifying a `types.MapType` value in your -`tfsdk.Attribute`'s `Type` property. You must specify an `ElemType` property -for your map, indicating what type the elements should be. Maps are -represented by a `types.Map` struct in config, state, and plan. The -`types.Map` struct has the following properties: - -* `ElemType` will always contain the same type as the `ElemType` property of - the `types.MapType` that created the `types.Map`. -* `Elem` contains a map of values, one for each element in the map. The keys - will be the keys defined in the config, state, or plan, and the values will - all be of the value type produced by the `ElemType` for the map. -* `Null` is set to `true` when the entire map's value is null. Individual - elements may still be null even if the map's `Null` property is `false`. -* `Unknown` is set to `true` when the entire map's value is unknown. - Individual elements may still be unknown even if the map's `Unknown` property - is `false`. - -Elements of a `types.Map` with a non-null, non-unknown value can be accessed -without using type assertions by using the `types.Map`'s [`ElementsAs` -method](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types#Map.ElementsAs), -which uses the same conversion rules as the `Get` methods described in [Access -State, Config, and Plan](/plugin/framework/accessing-values). +The associated schema type is [`types.MapType`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types#MapType): + +```go +"example_attribute": { + Type: types.MapType{ + ElemType: types.StringType, + }, + // ... other fields ... +} +``` + +The associated value type is [`types.Map`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types#Map) in configuration, plan, and state data. + +Access `types.Map` information via the following methods: + +* [`(types.Map).IsNull() bool`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types#Map.IsNull): Returns true if the map is null. +* [`(types.Map).IsUnknown() bool`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types#Map.IsUnknown): Returns true if the map is unknown. Returns false if the number of elements is known, any of which may be unknown. +* [`(types.Map).Elements() map[string]attr.Value`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types#Map.Elements): Returns the known `map[string]attr.Value` value, or `nil` if null or unknown. +* [`(types.Map).ElementsAs(context.Context, any, bool) diag.Diagnostics`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types#Map.ElementsAs): Converts the known values into the given Go type, if possible, using the [conversion rules](/plugin/framework/accessing-values#conversion-rules). + +Call one of the following to create a `types.Map`: + +* [`types.MapNull(attr.Type)`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types#MapNull): A null map value with the given element type. +* [`types.MapUnknown(attr.Type)`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types#MapUnknown): An unknown map value with the given element type. +* [`types.MapValue(attr.Type, map[string]attr.Value)`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types#MapValue): A known value with the given element type. ### ObjectType and Object @@ -285,38 +371,41 @@ State, Config, and Plan](/plugin/framework/accessing-values). ### SetType and Set -Sets are unordered collections of other types. Their elements, the values inside -the set, must all be of the same type and must be unique. +Set are unordered, unique collections of a single element type. + +-> Use [SetNestedAttributes](/plugin/framework/schemas#SetNestedAttributes) for sets of objects that need additional schema information. Use [ListType and List](#listtype-and-list) for ordered collections. + +Given an example Terraform configuration that sets a set of string values to the `example_attribute` attribute: ```tf -hello = ["red", "blue", "green"] +example_attribute = ["red", "blue", "green"] ``` -They are used by specifying a `types.SetType` value in your -`tfsdk.Attribute`'s `Type` property. You must specify an `ElemType` property -for your set, indicating what type the elements should be. Sets are -represented by a `types.Set` struct in config, state, and plan. The -`types.Set` struct has the following properties: - -* `ElemType` will always contain the same type as the `ElemType` property of - the `types.SetType` that created the `types.Set`. -* `Elem` contains a list of values, one for each element in the set. The - values will all be of the value type produced by the `ElemType` for the list. - Each element must be unique. -* `Null` is set to `true` when the entire set's value is null. Individual - elements may still be null even if the set's `Null` property is `false`. -* `Unknown` is set to `true` when the entire set's value is unknown. - Individual elements may still be unknown even if the set's `Unknown` - property is `false`. +The associated schema type is [`types.SetType`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types#SetType): -Elements of a `types.Set` with a non-null, non-unknown value can be accessed -without using type assertions by using the `types.Set`'s [`ElementsAs` -method](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types#List.ElementsAs), -which uses the same conversion rules as the `Get` methods described in [Access -State, Config, and Plan](/plugin/framework/accessing-values). +```go +"example_attribute": { + Type: types.SetType{ + ElemType: types.StringType, + }, + // ... other fields ... +} +``` + +The associated value type is [`types.Set`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types#Set) in configuration, plan, and state data. + +Access `types.Set` information via the following methods: + +* [`(types.Set).IsNull() bool`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types#Set.IsNull): Returns true if the list is null. +* [`(types.Set).IsUnknown() bool`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types#Set.IsUnknown): Returns true if the list is unknown. Returns false if the number of elements is known, any of which may be unknown. +* [`(types.Set).Elements() []attr.Value`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types#Set.Elements): Returns the known `[]attr.Value` value, or `nil` if null or unknown. +* [`(types.Set).ElementsAs(context.Context, any, bool) diag.Diagnostics`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types#Set.ElementsAs): Converts the known values into the given Go type, if possible, using the [conversion rules](/plugin/framework/accessing-values#conversion-rules). + +Call one of the following to create a `types.Set`: -For an ordered collection without uniqueness constraints, see [`ListType` and -`List`](#listtype-and-list). +* [`types.SetNull(attr.Type)`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types#SetNull): A null list value with the given element type. +* [`types.SetUnknown(attr.Type)`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types#SetUnknown): An unknown list value with the given element type. +* [`types.SetValue(attr.Type, []attr.Value)`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types#SetValue): A known value with the given element type. ## Create Provider-Defined Types and Values diff --git a/website/docs/plugin/framework/validation.mdx b/website/docs/plugin/framework/validation.mdx index dd3bd6c9c..91e480cbd 100644 --- a/website/docs/plugin/framework/validation.mdx +++ b/website/docs/plugin/framework/validation.mdx @@ -74,6 +74,11 @@ func (v stringLengthBetweenValidator) MarkdownDescription(ctx context.Context) s // Validate runs the main validation logic of the validator, reading configuration data out of `req` and updating `resp` with diagnostics. func (v stringLengthBetweenValidator) Validate(ctx context.Context, req tfsdk.ValidateAttributeRequest, resp *tfsdk.ValidateAttributeResponse) { + // If the value is unknown or null, there is nothing to validate. + if req.AttributeConfig.IsUnknown() || req.AttributeConfig.IsNull() { + return + } + // types.String must be the attr.Value produced by the attr.Type in the schema for this attribute // for generic validators, use // https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/tfsdk#ConvertValue @@ -85,11 +90,7 @@ func (v stringLengthBetweenValidator) Validate(ctx context.Context, req tfsdk.Va return } - if str.Unknown || str.Null { - return - } - - strLen := len(str.Value) + strLen := len(str.ValueString()) if strLen < v.Min || strLen > v.Max { resp.Diagnostics.AddAttributeError( @@ -230,11 +231,11 @@ func (v int64IsGreaterThanValidator) Validate(ctx context.Context, req tfsdk.Val continue } - if matchedPathConfig.Value >= attributeConfig.Value { + if matchedPathConfig.ValueInt64() >= attributeConfig.ValueInt64() { resp.Diagnostics.AddAttributeError( matchedPath, "Invalid Attribute Value", - fmt.Sprintf("Must be less than %s value: %d", req.AttributePath, attributeConfig.Value), + fmt.Sprintf("Must be less than %s value: %d", req.AttributePath, attributeConfig.ValueInt64()), ) } } diff --git a/website/docs/plugin/framework/writing-state.mdx b/website/docs/plugin/framework/writing-state.mdx index 79c94c098..613018fea 100644 --- a/website/docs/plugin/framework/writing-state.mdx +++ b/website/docs/plugin/framework/writing-state.mdx @@ -47,7 +47,7 @@ func (r ThingResource) Create(ctx context.Context, // ... // update newState by modifying each property as usual for Go values - newState.Name.Value = "J. Doe" + newState.Name = types.StringValue("J. Doe") // persist the values to state diags := resp.State.Set(ctx, &newState)