diff --git a/.changes/unreleased/FEATURES-20240626-123513.yaml b/.changes/unreleased/FEATURES-20240626-123513.yaml new file mode 100644 index 00000000..969d1ad2 --- /dev/null +++ b/.changes/unreleased/FEATURES-20240626-123513.yaml @@ -0,0 +1,6 @@ +kind: FEATURES +body: 'types/basetypes: Added `Float32Type` and `Float32Value` implementations for + Float32 value handling.' +time: 2024-06-26T12:35:13.705873-04:00 +custom: + Issue: "1014" diff --git a/.changes/unreleased/FEATURES-20240626-123533.yaml b/.changes/unreleased/FEATURES-20240626-123533.yaml new file mode 100644 index 00000000..daf1337d --- /dev/null +++ b/.changes/unreleased/FEATURES-20240626-123533.yaml @@ -0,0 +1,7 @@ +kind: FEATURES +body: 'types/basetypes: Added interfaces `basetypes.Float32Typable`, `basetypes.Float32Valuable`, + and `basetypes.Float32ValuableWithSemanticEquals` for Float32 custom type and value + implementations.' +time: 2024-06-26T12:35:33.490507-04:00 +custom: + Issue: "1014" diff --git a/.changes/unreleased/FEATURES-20240626-123545.yaml b/.changes/unreleased/FEATURES-20240626-123545.yaml new file mode 100644 index 00000000..4fb459bf --- /dev/null +++ b/.changes/unreleased/FEATURES-20240626-123545.yaml @@ -0,0 +1,6 @@ +kind: FEATURES +body: 'resource/schema: Added `Float32Attribute` implementation for Float32 value + handling.' +time: 2024-06-26T12:35:45.867977-04:00 +custom: + Issue: "1014" diff --git a/.changes/unreleased/FEATURES-20240626-123615.yaml b/.changes/unreleased/FEATURES-20240626-123615.yaml new file mode 100644 index 00000000..d03436d7 --- /dev/null +++ b/.changes/unreleased/FEATURES-20240626-123615.yaml @@ -0,0 +1,6 @@ +kind: FEATURES +body: 'datasource/schema: Added `Float32Attribute` implementation for Float32 value + handling.' +time: 2024-06-26T12:36:15.026076-04:00 +custom: + Issue: "1014" diff --git a/.changes/unreleased/FEATURES-20240626-123640.yaml b/.changes/unreleased/FEATURES-20240626-123640.yaml new file mode 100644 index 00000000..d64be1a7 --- /dev/null +++ b/.changes/unreleased/FEATURES-20240626-123640.yaml @@ -0,0 +1,6 @@ +kind: FEATURES +body: 'provider/schema: Added `Float32Attribute` implementation for Float32 value + handling.' +time: 2024-06-26T12:36:40.047935-04:00 +custom: + Issue: "1014" diff --git a/.changes/unreleased/FEATURES-20240626-123651.yaml b/.changes/unreleased/FEATURES-20240626-123651.yaml new file mode 100644 index 00000000..d521cf42 --- /dev/null +++ b/.changes/unreleased/FEATURES-20240626-123651.yaml @@ -0,0 +1,5 @@ +kind: FEATURES +body: 'function: Added `Float32Parameter` and `Float32Return` for Float32 value handling.' +time: 2024-06-26T12:36:51.027757-04:00 +custom: + Issue: "1014" diff --git a/.changes/unreleased/FEATURES-20240626-123738.yaml b/.changes/unreleased/FEATURES-20240626-123738.yaml new file mode 100644 index 00000000..018adb37 --- /dev/null +++ b/.changes/unreleased/FEATURES-20240626-123738.yaml @@ -0,0 +1,6 @@ +kind: FEATURES +body: 'resource/schema/float32default: New package with `StaticValue` implementation + for Float32 schema-based default values.' +time: 2024-06-26T12:37:38.728292-04:00 +custom: + Issue: "1014" diff --git a/.changes/unreleased/FEATURES-20240626-123820.yaml b/.changes/unreleased/FEATURES-20240626-123820.yaml new file mode 100644 index 00000000..95a9a315 --- /dev/null +++ b/.changes/unreleased/FEATURES-20240626-123820.yaml @@ -0,0 +1,6 @@ +kind: FEATURES +body: 'resource/schema/float32planmodifier: New package with built-in implementations + for Float32 value plan modification.' +time: 2024-06-26T12:38:20.591646-04:00 +custom: + Issue: "1014" diff --git a/.changes/unreleased/FEATURES-20240626-123836.yaml b/.changes/unreleased/FEATURES-20240626-123836.yaml new file mode 100644 index 00000000..11214097 --- /dev/null +++ b/.changes/unreleased/FEATURES-20240626-123836.yaml @@ -0,0 +1,6 @@ +kind: FEATURES +body: 'resource/schema/defaults: New `Float32` interface for Float32 schema-based + default implementations.' +time: 2024-06-26T12:38:36.234337-04:00 +custom: + Issue: "1014" diff --git a/.changes/unreleased/FEATURES-20240626-123909.yaml b/.changes/unreleased/FEATURES-20240626-123909.yaml new file mode 100644 index 00000000..81fc215b --- /dev/null +++ b/.changes/unreleased/FEATURES-20240626-123909.yaml @@ -0,0 +1,6 @@ +kind: FEATURES +body: 'resource/schema/planmodifier: New `Float32` interface for Float32 value plan + modification implementations.' +time: 2024-06-26T12:39:09.284352-04:00 +custom: + Issue: "1014" diff --git a/.changes/unreleased/FEATURES-20240626-123922.yaml b/.changes/unreleased/FEATURES-20240626-123922.yaml new file mode 100644 index 00000000..2deed63d --- /dev/null +++ b/.changes/unreleased/FEATURES-20240626-123922.yaml @@ -0,0 +1,5 @@ +kind: FEATURES +body: 'schema/validator: New `Float32` interface for Float32 value schema validation.' +time: 2024-06-26T12:39:22.726132-04:00 +custom: + Issue: "1014" diff --git a/datasource/schema/float32_attribute.go b/datasource/schema/float32_attribute.go new file mode 100644 index 00000000..8f3dbdc2 --- /dev/null +++ b/datasource/schema/float32_attribute.go @@ -0,0 +1,191 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema + +import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +// Ensure the implementation satisifies the desired interfaces. +var ( + _ Attribute = Float32Attribute{} + _ fwxschema.AttributeWithFloat32Validators = Float32Attribute{} +) + +// Float32Attribute represents a schema attribute that is a 32-bit floating +// point number. When retrieving the value for this attribute, use +// types.Float32 as the value type unless the CustomType field is set. +// +// Use Int32Attribute for 32-bit integer attributes or NumberAttribute for +// 512-bit generic number attributes. +// +// Terraform configurations configure this attribute using expressions that +// return a number or directly via a floating point value. +// +// example_attribute = 123.45 +// +// Terraform configurations reference this attribute using the attribute name. +// +// .example_attribute +type Float32Attribute struct { + // CustomType enables the use of a custom attribute type in place of the + // default basetypes.Float32Type. When retrieving data, the basetypes.Float32Valuable + // associated with this custom type must be used in place of types.Float32. + CustomType basetypes.Float32Typable + + // Required indicates whether the practitioner must enter a value for + // this attribute or not. Required and Optional cannot both be true, + // and Required and Computed cannot both be true. + Required bool + + // Optional indicates whether the practitioner can choose to enter a value + // for this attribute or not. Optional and Required cannot both be true. + Optional bool + + // Computed indicates whether the provider may return its own value for + // this Attribute or not. Required and Computed cannot both be true. If + // Required and Optional are both false, Computed must be true, and the + // attribute will be considered "read only" for the practitioner, with + // only the provider able to set its value. + Computed bool + + // Sensitive indicates whether the value of this attribute should be + // considered sensitive data. Setting it to true will obscure the value + // in CLI output. Sensitive does not impact how values are stored, and + // practitioners are encouraged to store their state as if the entire + // file is sensitive. + Sensitive bool + + // Description is used in various tooling, like the language server, to + // give practitioners more information about what this attribute is, + // what it's for, and how it should be used. It should be written as + // plain text, with no special formatting. + Description string + + // MarkdownDescription is used in various tooling, like the + // documentation generator, to give practitioners more information + // about what this attribute is, what it's for, and how it should be + // used. It should be formatted using Markdown. + MarkdownDescription string + + // DeprecationMessage defines warning diagnostic details to display when + // practitioner configurations use this Attribute. The warning diagnostic + // summary is automatically set to "Attribute Deprecated" along with + // configuration source file and line information. + // + // Set this field to a practitioner actionable message such as: + // + // - "Configure other_attribute instead. This attribute will be removed + // in the next major version of the provider." + // - "Remove this attribute's configuration as it no longer is used and + // the attribute will be removed in the next major version of the + // provider." + // + // In Terraform 1.2.7 and later, this warning diagnostic is displayed any + // time a practitioner attempts to configure a value for this attribute and + // certain scenarios where this attribute is referenced. + // + // In Terraform 1.2.6 and earlier, this warning diagnostic is only + // displayed when the Attribute is Required or Optional, and if the + // practitioner configuration sets the value to a known or unknown value + // (which may eventually be null). It has no effect when the Attribute is + // Computed-only (read-only; not Required or Optional). + // + // Across any Terraform version, there are no warnings raised for + // practitioner configuration values set directly to null, as there is no + // way for the framework to differentiate between an unset and null + // configuration due to how Terraform sends configuration information + // across the protocol. + // + // Additional information about deprecation enhancements for read-only + // attributes can be found in: + // + // - https://github.com/hashicorp/terraform/issues/7569 + // + DeprecationMessage string + + // Validators define value validation functionality for the attribute. All + // elements of the slice of AttributeValidator are run, regardless of any + // previous error diagnostics. + // + // Many common use case validators can be found in the + // github.com/hashicorp/terraform-plugin-framework-validators Go module. + // + // If the Type field points to a custom type that implements the + // xattr.TypeWithValidate interface, the validators defined in this field + // are run in addition to the validation defined by the type. + Validators []validator.Float32 +} + +// ApplyTerraform5AttributePathStep always returns an error as it is not +// possible to step further into a Float32Attribute. +func (a Float32Attribute) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { + return a.GetType().ApplyTerraform5AttributePathStep(step) +} + +// Equal returns true if the given Attribute is a Float32Attribute +// and all fields are equal. +func (a Float32Attribute) Equal(o fwschema.Attribute) bool { + if _, ok := o.(Float32Attribute); !ok { + return false + } + + return fwschema.AttributesEqual(a, o) +} + +// Float32Validators returns the Validators field value. +func (a Float32Attribute) Float32Validators() []validator.Float32 { + return a.Validators +} + +// GetDeprecationMessage returns the DeprecationMessage field value. +func (a Float32Attribute) GetDeprecationMessage() string { + return a.DeprecationMessage +} + +// GetDescription returns the Description field value. +func (a Float32Attribute) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription returns the MarkdownDescription field value. +func (a Float32Attribute) GetMarkdownDescription() string { + return a.MarkdownDescription +} + +// GetType returns types.Float32Type or the CustomType field value if defined. +func (a Float32Attribute) GetType() attr.Type { + if a.CustomType != nil { + return a.CustomType + } + + return types.Float32Type +} + +// IsComputed returns the Computed field value. +func (a Float32Attribute) IsComputed() bool { + return a.Computed +} + +// IsOptional returns the Optional field value. +func (a Float32Attribute) IsOptional() bool { + return a.Optional +} + +// IsRequired returns the Required field value. +func (a Float32Attribute) IsRequired() bool { + return a.Required +} + +// IsSensitive returns the Sensitive field value. +func (a Float32Attribute) IsSensitive() bool { + return a.Sensitive +} diff --git a/datasource/schema/float32_attribute_test.go b/datasource/schema/float32_attribute_test.go new file mode 100644 index 00000000..0da3cc94 --- /dev/null +++ b/datasource/schema/float32_attribute_test.go @@ -0,0 +1,426 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema_test + +import ( + "fmt" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func TestFloat32AttributeApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float32Attribute + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + attribute: schema.Float32Attribute{}, + step: tftypes.AttributeName("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.AttributeName to basetypes.Float32Type"), + }, + "ElementKeyInt": { + attribute: schema.Float32Attribute{}, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyInt to basetypes.Float32Type"), + }, + "ElementKeyString": { + attribute: schema.Float32Attribute{}, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyString to basetypes.Float32Type"), + }, + "ElementKeyValue": { + attribute: schema.Float32Attribute{}, + step: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyValue to basetypes.Float32Type"), + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.attribute.ApplyTerraform5AttributePathStep(testCase.step) + + if err != nil { + if testCase.expectedError == nil { + t.Fatalf("expected no error, got: %s", err) + } + + if !strings.Contains(err.Error(), testCase.expectedError.Error()) { + t.Fatalf("expected error %q, got: %s", testCase.expectedError, err) + } + } + + if err == nil && testCase.expectedError != nil { + t.Fatalf("got no error, expected: %s", testCase.expectedError) + } + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat32AttributeFloat32Validators(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float32Attribute + expected []validator.Float32 + }{ + "no-validators": { + attribute: schema.Float32Attribute{}, + expected: nil, + }, + "validators": { + attribute: schema.Float32Attribute{ + Validators: []validator.Float32{}, + }, + expected: []validator.Float32{}, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.Float32Validators() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat32AttributeGetDeprecationMessage(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float32Attribute + expected string + }{ + "no-deprecation-message": { + attribute: schema.Float32Attribute{}, + expected: "", + }, + "deprecation-message": { + attribute: schema.Float32Attribute{ + DeprecationMessage: "test deprecation message", + }, + expected: "test deprecation message", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDeprecationMessage() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat32AttributeEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float32Attribute + other fwschema.Attribute + expected bool + }{ + "different-type": { + attribute: schema.Float32Attribute{}, + other: testschema.AttributeWithFloat32Validators{}, + expected: false, + }, + "equal": { + attribute: schema.Float32Attribute{}, + other: schema.Float32Attribute{}, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.Equal(testCase.other) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat32AttributeGetDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float32Attribute + expected string + }{ + "no-description": { + attribute: schema.Float32Attribute{}, + expected: "", + }, + "description": { + attribute: schema.Float32Attribute{ + Description: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat32AttributeGetMarkdownDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float32Attribute + expected string + }{ + "no-markdown-description": { + attribute: schema.Float32Attribute{}, + expected: "", + }, + "markdown-description": { + attribute: schema.Float32Attribute{ + MarkdownDescription: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetMarkdownDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat32AttributeGetType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float32Attribute + expected attr.Type + }{ + "base": { + attribute: schema.Float32Attribute{}, + expected: types.Float32Type, + }, + "custom-type": { + attribute: schema.Float32Attribute{ + CustomType: testtypes.Float32Type{}, + }, + expected: testtypes.Float32Type{}, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetType() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat32AttributeIsComputed(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float32Attribute + expected bool + }{ + "not-computed": { + attribute: schema.Float32Attribute{}, + expected: false, + }, + "computed": { + attribute: schema.Float32Attribute{ + Computed: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsComputed() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat32AttributeIsOptional(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float32Attribute + expected bool + }{ + "not-optional": { + attribute: schema.Float32Attribute{}, + expected: false, + }, + "optional": { + attribute: schema.Float32Attribute{ + Optional: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptional() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat32AttributeIsRequired(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float32Attribute + expected bool + }{ + "not-required": { + attribute: schema.Float32Attribute{}, + expected: false, + }, + "required": { + attribute: schema.Float32Attribute{ + Required: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequired() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat32AttributeIsSensitive(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float32Attribute + expected bool + }{ + "not-sensitive": { + attribute: schema.Float32Attribute{}, + expected: false, + }, + "sensitive": { + attribute: schema.Float32Attribute{ + Sensitive: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsSensitive() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/function/float32_parameter.go b/function/float32_parameter.go new file mode 100644 index 00000000..cb1a1b91 --- /dev/null +++ b/function/float32_parameter.go @@ -0,0 +1,124 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package function + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/internal/fwfunction" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +// Ensure the implementation satisifies the desired interfaces. +var _ Parameter = Float32Parameter{} +var _ ParameterWithFloat32Validators = Float32Parameter{} +var _ fwfunction.ParameterWithValidateImplementation = Float32Parameter{} + +// Float32Parameter represents a function parameter that is a 32-bit floating +// point number. +// +// When retrieving the argument value for this parameter: +// +// - If CustomType is set, use its associated value type. +// - If AllowUnknownValues is enabled, you must use the [types.Float32] value +// type. +// - If AllowNullValue is enabled, you must use [types.Float32] or *float32 +// value types. +// - Otherwise, use [types.Float32] or *float32, or float32 value types. +// +// Terraform configurations set this parameter's argument data using expressions +// that return a number or directly via numeric syntax. +type Float32Parameter struct { + // AllowNullValue when enabled denotes that a null argument value can be + // passed to the function. When disabled, Terraform returns an error if the + // argument value is null. + AllowNullValue bool + + // AllowUnknownValues when enabled denotes that an unknown argument value + // can be passed to the function. When disabled, Terraform skips the + // function call entirely and assumes an unknown value result from the + // function. + AllowUnknownValues bool + + // CustomType enables the use of a custom data type in place of the + // default [basetypes.Float32Type]. When retrieving data, the + // [basetypes.Float32Valuable] implementation associated with this custom + // type must be used in place of [types.Float32]. + CustomType basetypes.Float32Typable + + // Description is used in various tooling, like the language server, to + // give practitioners more information about what this parameter is, + // what it is for, and how it should be used. It should be written as + // plain text, with no special formatting. + Description string + + // MarkdownDescription is used in various tooling, like the + // documentation generator, to give practitioners more information + // about what this parameter is, what it is for, and how it should be + // used. It should be formatted using Markdown. + MarkdownDescription string + + // Name is a short usage name for the parameter, such as "data". This name + // is used in documentation, such as generating a function signature, + // however its usage may be extended in the future. + // + // If no name is provided, this will default to "param" with a suffix of the + // position the parameter is in the function definition. ("param1", "param2", etc.) + // If the parameter is variadic, the default name will be "varparam". + // + // This must be a valid Terraform identifier, such as starting with an + // alphabetical character and followed by alphanumeric or underscore + // characters. + Name string + + // Validators is a list of float32 validators that should be applied to the + // parameter. + Validators []Float32ParameterValidator +} + +// GetValidators returns the list of validators for the parameter. +func (p Float32Parameter) GetValidators() []Float32ParameterValidator { + return p.Validators +} + +// GetAllowNullValue returns if the parameter accepts a null value. +func (p Float32Parameter) GetAllowNullValue() bool { + return p.AllowNullValue +} + +// GetAllowUnknownValues returns if the parameter accepts an unknown value. +func (p Float32Parameter) GetAllowUnknownValues() bool { + return p.AllowUnknownValues +} + +// GetDescription returns the parameter plaintext description. +func (p Float32Parameter) GetDescription() string { + return p.Description +} + +// GetMarkdownDescription returns the parameter Markdown description. +func (p Float32Parameter) GetMarkdownDescription() string { + return p.MarkdownDescription +} + +// GetName returns the parameter name. +func (p Float32Parameter) GetName() string { + return p.Name +} + +// GetType returns the parameter data type. +func (p Float32Parameter) GetType() attr.Type { + if p.CustomType != nil { + return p.CustomType + } + + return basetypes.Float32Type{} +} + +func (p Float32Parameter) ValidateImplementation(ctx context.Context, req fwfunction.ValidateParameterImplementationRequest, resp *fwfunction.ValidateParameterImplementationResponse) { + if p.GetName() == "" { + resp.Diagnostics.Append(fwfunction.MissingParameterNameDiag(req.FunctionName, req.ParameterPosition)) + } +} diff --git a/function/float32_parameter_test.go b/function/float32_parameter_test.go new file mode 100644 index 00000000..9292bf8c --- /dev/null +++ b/function/float32_parameter_test.go @@ -0,0 +1,344 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package function_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/function" + "github.com/hashicorp/terraform-plugin-framework/internal/fwfunction" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testvalidator" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +func TestFloat32ParameterGetAllowNullValue(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + parameter function.Float32Parameter + expected bool + }{ + "unset": { + parameter: function.Float32Parameter{}, + expected: false, + }, + "AllowNullValue-false": { + parameter: function.Float32Parameter{ + AllowNullValue: false, + }, + expected: false, + }, + "AllowNullValue-true": { + parameter: function.Float32Parameter{ + AllowNullValue: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.parameter.GetAllowNullValue() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat32ParameterGetAllowUnknownValues(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + parameter function.Float32Parameter + expected bool + }{ + "unset": { + parameter: function.Float32Parameter{}, + expected: false, + }, + "AllowUnknownValues-false": { + parameter: function.Float32Parameter{ + AllowUnknownValues: false, + }, + expected: false, + }, + "AllowUnknownValues-true": { + parameter: function.Float32Parameter{ + AllowUnknownValues: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.parameter.GetAllowUnknownValues() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat32ParameterGetDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + parameter function.Float32Parameter + expected string + }{ + "unset": { + parameter: function.Float32Parameter{}, + expected: "", + }, + "Description-empty": { + parameter: function.Float32Parameter{ + Description: "", + }, + expected: "", + }, + "Description-nonempty": { + parameter: function.Float32Parameter{ + Description: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.parameter.GetDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat32ParameterGetMarkdownDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + parameter function.Float32Parameter + expected string + }{ + "unset": { + parameter: function.Float32Parameter{}, + expected: "", + }, + "MarkdownDescription-empty": { + parameter: function.Float32Parameter{ + MarkdownDescription: "", + }, + expected: "", + }, + "MarkdownDescription-nonempty": { + parameter: function.Float32Parameter{ + MarkdownDescription: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.parameter.GetMarkdownDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat32ParameterGetName(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + parameter function.Float32Parameter + expected string + }{ + "unset": { + parameter: function.Float32Parameter{}, + expected: "", + }, + "Name-nonempty": { + parameter: function.Float32Parameter{ + Name: "test", + }, + expected: "test", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.parameter.GetName() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat32ParameterGetType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + parameter function.Float32Parameter + expected attr.Type + }{ + "unset": { + parameter: function.Float32Parameter{}, + expected: basetypes.Float32Type{}, + }, + "CustomType": { + parameter: function.Float32Parameter{ + CustomType: testtypes.Float32TypeWithSemanticEquals{}, + }, + expected: testtypes.Float32TypeWithSemanticEquals{}, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.parameter.GetType() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat32ParameterFloat32Validators(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + parameter function.Float32Parameter + expected []function.Float32ParameterValidator + }{ + "unset": { + parameter: function.Float32Parameter{}, + expected: nil, + }, + "Validators - empty": { + parameter: function.Float32Parameter{ + Validators: []function.Float32ParameterValidator{}}, + expected: []function.Float32ParameterValidator{}, + }, + "Validators": { + parameter: function.Float32Parameter{ + Validators: []function.Float32ParameterValidator{ + testvalidator.Float32{}, + }}, + expected: []function.Float32ParameterValidator{ + testvalidator.Float32{}, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.parameter.GetValidators() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat32ParameterValidateImplementation(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + param function.Float32Parameter + request fwfunction.ValidateParameterImplementationRequest + expected *fwfunction.ValidateParameterImplementationResponse + }{ + "name": { + param: function.Float32Parameter{ + Name: "testparam", + }, + request: fwfunction.ValidateParameterImplementationRequest{ + FunctionName: "testfunc", + ParameterPosition: pointer(int64(0)), + }, + expected: &fwfunction.ValidateParameterImplementationResponse{}, + }, + "name-missing": { + param: function.Float32Parameter{ + // Name intentionally missing + }, + request: fwfunction.ValidateParameterImplementationRequest{ + FunctionName: "testfunc", + ParameterPosition: pointer(int64(0)), + }, + expected: &fwfunction.ValidateParameterImplementationResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid Function Definition", + "When validating the function definition, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "Function \"testfunc\" - Parameter at position 0 does not have a name", + ), + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := &fwfunction.ValidateParameterImplementationResponse{} + testCase.param.ValidateImplementation(context.Background(), testCase.request, got) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/function/float32_parameter_validator.go b/function/float32_parameter_validator.go new file mode 100644 index 00000000..3710ad97 --- /dev/null +++ b/function/float32_parameter_validator.go @@ -0,0 +1,33 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package function + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Float32ParameterValidator is a function validator for types.Float32 parameters. +type Float32ParameterValidator interface { + + // ValidateParameterFloat32 performs the validation. + ValidateParameterFloat32(context.Context, Float32ParameterValidatorRequest, *Float32ParameterValidatorResponse) +} + +// Float32ParameterValidatorRequest is a request for types.Float32 schema validation. +type Float32ParameterValidatorRequest struct { + // ArgumentPosition contains the position of the argument for validation. + // Use this position for any response diagnostics. + ArgumentPosition int64 + + // Value contains the value of the argument for validation. + Value types.Float32 +} + +// Float32ParameterValidatorResponse is a response to a Float32ParameterValidatorRequest. +type Float32ParameterValidatorResponse struct { + // Error is a function error generated during validation of the Value. + Error *FuncError +} diff --git a/function/float32_return.go b/function/float32_return.go new file mode 100644 index 00000000..44401f12 --- /dev/null +++ b/function/float32_return.go @@ -0,0 +1,53 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package function + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +// Ensure the implementation satisifies the desired interfaces. +var _ Return = Float32Return{} + +// Float32Return represents a function return that is a 32-bit floating point +// number. +// +// When setting the value for this return: +// +// - If CustomType is set, use its associated value type. +// - Otherwise, use [types.Float32], *float32, or float32. +// +// Return documentation is expected in the function [Definition] documentation. +type Float32Return struct { + // CustomType enables the use of a custom data type in place of the + // default [basetypes.Float32Type]. When setting data, the + // [basetypes.Float32Valuable] implementation associated with this custom + // type must be used in place of [types.Float32]. + CustomType basetypes.Float32Typable +} + +// GetType returns the return data type. +func (r Float32Return) GetType() attr.Type { + if r.CustomType != nil { + return r.CustomType + } + + return basetypes.Float32Type{} +} + +// NewResultData returns a new result data based on the type. +func (r Float32Return) NewResultData(ctx context.Context) (ResultData, *FuncError) { + value := basetypes.NewFloat32Unknown() + + if r.CustomType == nil { + return NewResultData(value), nil + } + + valuable, diags := r.CustomType.ValueFromFloat32(ctx, value) + + return NewResultData(valuable), FuncErrorFromDiags(ctx, diags) +} diff --git a/function/float32_return_test.go b/function/float32_return_test.go new file mode 100644 index 00000000..242041eb --- /dev/null +++ b/function/float32_return_test.go @@ -0,0 +1,49 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package function_test + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/function" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +func TestFloat32ReturnGetType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + parameter function.Float32Return + expected attr.Type + }{ + "unset": { + parameter: function.Float32Return{}, + expected: basetypes.Float32Type{}, + }, + "CustomType": { + parameter: function.Float32Return{ + CustomType: testtypes.Float32TypeWithSemanticEquals{}, + }, + expected: testtypes.Float32TypeWithSemanticEquals{}, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.parameter.GetType() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/function/parameter_validation.go b/function/parameter_validation.go index 6d57ef08..cc31ae7b 100644 --- a/function/parameter_validation.go +++ b/function/parameter_validation.go @@ -30,6 +30,15 @@ type ParameterWithInt64Validators interface { GetValidators() []Int64ParameterValidator } +// ParameterWithFloat32Validators is an optional interface on Parameter which +// enables Float32 validation support. +type ParameterWithFloat32Validators interface { + Parameter + + // GetValidators should return a list of Float64 validators. + GetValidators() []Float32ParameterValidator +} + // ParameterWithFloat64Validators is an optional interface on Parameter which // enables Float64 validation support. type ParameterWithFloat64Validators interface { diff --git a/internal/fromproto5/arguments_data.go b/internal/fromproto5/arguments_data.go index 5e342617..7c9195ea 100644 --- a/internal/fromproto5/arguments_data.go +++ b/internal/fromproto5/arguments_data.go @@ -226,6 +226,39 @@ func ArgumentsData(ctx context.Context, arguments []*tfprotov5.DynamicValue, def )) } } + case function.ParameterWithFloat32Validators: + for _, functionValidator := range parameterWithValidators.GetValidators() { + float32Valuable, ok := attrValue.(basetypes.Float32Valuable) + if !ok { + funcError = function.ConcatFuncErrors(funcError, function.NewArgumentFuncError( + pos, + "Invalid Argument Type: "+ + "An unexpected error was encountered when converting the function argument from the protocol type. "+ + "This is always an issue in terraform-plugin-framework used to implement the provider and should be reported to the provider developers.\n\n"+ + "Please report this to the provider developer:\n\n"+ + fmt.Sprintf("Expected basetypes.Float32Valuable at position %d", pos), + )) + + continue + } + float32Val, diags := float32Valuable.ToFloat32Value(ctx) + if diags.HasError() { + funcError = function.ConcatFuncErrors(funcError, function.FuncErrorFromDiags(ctx, diags)) + continue + } + req := function.Float32ParameterValidatorRequest{ + ArgumentPosition: pos, + Value: float32Val, + } + resp := &function.Float32ParameterValidatorResponse{} + functionValidator.ValidateParameterFloat32(ctx, req, resp) + if resp.Error != nil { + funcError = function.ConcatFuncErrors(funcError, function.NewArgumentFuncError( + pos, + resp.Error.Error(), + )) + } + } case function.ParameterWithFloat64Validators: for _, functionValidator := range parameterWithValidators.GetValidators() { float64Valuable, ok := attrValue.(basetypes.Float64Valuable) diff --git a/internal/fromproto5/arguments_data_test.go b/internal/fromproto5/arguments_data_test.go index 42a2fac2..cf5754ad 100644 --- a/internal/fromproto5/arguments_data_test.go +++ b/internal/fromproto5/arguments_data_test.go @@ -1060,6 +1060,170 @@ func TestArgumentsData_ParameterValidators(t *testing.T) { 0, "Error Diagnostic: This is an error.", ), }, + "float32-parameter-Validators": { + input: []*tfprotov5.DynamicValue{ + DynamicValueMust(tftypes.NewValue(tftypes.Number, 1.0)), + }, + definition: function.Definition{ + Parameters: []function.Parameter{ + function.Float32Parameter{ + Validators: []function.Float32ParameterValidator{ + testvalidator.Float32{ + ValidateMethod: func(ctx context.Context, req function.Float32ParameterValidatorRequest, resp *function.Float32ParameterValidatorResponse) { + got := req.Value + expected := types.Float32Value(1.0) + + if !got.Equal(expected) { + resp.Error = function.NewArgumentFuncError( + req.ArgumentPosition, + "Error Diagnostic: This is an error.", + ) + } + }, + }, + }, + }, + }, + }, + expected: function.NewArgumentsData([]attr.Value{ + basetypes.NewFloat32Value(1.0), + }), + }, + "float32-parameter-Validators-error": { + input: []*tfprotov5.DynamicValue{ + DynamicValueMust(tftypes.NewValue(tftypes.Number, 1.0)), + }, + definition: function.Definition{ + Parameters: []function.Parameter{ + function.Float32Parameter{ + Validators: []function.Float32ParameterValidator{ + testvalidator.Float32{ + ValidateMethod: func(ctx context.Context, req function.Float32ParameterValidatorRequest, resp *function.Float32ParameterValidatorResponse) { + got := req.Value + expected := types.Float32Value(2.0) + + if !got.Equal(expected) { + resp.Error = function.NewArgumentFuncError( + req.ArgumentPosition, + "Error Diagnostic: This is an error.", + ) + } + }, + }, + }, + }, + }, + }, + expected: function.NewArgumentsData(nil), + expectedFuncError: function.NewArgumentFuncError( + 0, "Error Diagnostic: This is an error.", + ), + }, + "float32-parameter-Validators-multiple-errors": { + input: []*tfprotov5.DynamicValue{ + DynamicValueMust(tftypes.NewValue(tftypes.Number, 1.0)), + }, + definition: function.Definition{ + Parameters: []function.Parameter{ + function.Float32Parameter{ + Validators: []function.Float32ParameterValidator{ + testvalidator.Float32{ + ValidateMethod: func(ctx context.Context, req function.Float32ParameterValidatorRequest, resp *function.Float32ParameterValidatorResponse) { + got := req.Value + expected := types.Float32Value(2.0) + + if !got.Equal(expected) { + resp.Error = function.NewArgumentFuncError( + req.ArgumentPosition, + "Error Diagnostic: error 1.", + ) + } + }, + }, + testvalidator.Float32{ + ValidateMethod: func(ctx context.Context, req function.Float32ParameterValidatorRequest, resp *function.Float32ParameterValidatorResponse) { + got := req.Value + expected := types.Float32Value(3.0) + + if !got.Equal(expected) { + resp.Error = function.NewArgumentFuncError( + req.ArgumentPosition, + "Error Diagnostic: error 2.", + ) + } + }, + }, + }, + }, + }, + }, + expected: function.NewArgumentsData(nil), + expectedFuncError: function.NewArgumentFuncError( + 0, "Error Diagnostic: error 1."+ + "\nError Diagnostic: error 2.", + ), + }, + "float32-parameter-custom-type-Validators": { + input: []*tfprotov5.DynamicValue{ + DynamicValueMust(tftypes.NewValue(tftypes.Number, 1.0)), + }, + definition: function.Definition{ + Parameters: []function.Parameter{ + function.Float32Parameter{ + CustomType: testtypes.Float32Type{}, + Validators: []function.Float32ParameterValidator{ + testvalidator.Float32{ + ValidateMethod: func(ctx context.Context, req function.Float32ParameterValidatorRequest, resp *function.Float32ParameterValidatorResponse) { + got := req.Value + expected := types.Float32Value(1.0) + + if !got.Equal(expected) { + resp.Error = function.NewArgumentFuncError( + req.ArgumentPosition, + "Error Diagnostic: This is an error.", + ) + } + }, + }, + }, + }, + }, + }, + expected: function.NewArgumentsData([]attr.Value{ + basetypes.NewFloat32Value(1.0), + }), + }, + "float32-parameter-custom-type-Validators-error": { + input: []*tfprotov5.DynamicValue{ + DynamicValueMust(tftypes.NewValue(tftypes.Number, 1.0)), + }, + definition: function.Definition{ + Parameters: []function.Parameter{ + function.Float32Parameter{ + CustomType: testtypes.Float32Type{}, + Validators: []function.Float32ParameterValidator{ + testvalidator.Float32{ + ValidateMethod: func(ctx context.Context, req function.Float32ParameterValidatorRequest, resp *function.Float32ParameterValidatorResponse) { + got := req.Value + expected := types.Float32Value(2.0) + + if !got.Equal(expected) { + resp.Error = function.NewArgumentFuncError( + req.ArgumentPosition, + "Error Diagnostic: This is an error.", + ) + } + }, + }, + }, + }, + }, + }, + expected: function.NewArgumentsData(nil), + expectedFuncError: function.NewArgumentFuncError( + 0, "Error Diagnostic: This is an error.", + ), + }, "float64-parameter-Validators": { input: []*tfprotov5.DynamicValue{ DynamicValueMust(tftypes.NewValue(tftypes.Number, 1.0)), diff --git a/internal/fromproto6/arguments_data.go b/internal/fromproto6/arguments_data.go index 192e4517..f4a34e26 100644 --- a/internal/fromproto6/arguments_data.go +++ b/internal/fromproto6/arguments_data.go @@ -226,6 +226,39 @@ func ArgumentsData(ctx context.Context, arguments []*tfprotov6.DynamicValue, def )) } } + case function.ParameterWithFloat32Validators: + for _, functionValidator := range parameterWithValidators.GetValidators() { + float32Valuable, ok := attrValue.(basetypes.Float32Valuable) + if !ok { + funcError = function.ConcatFuncErrors(funcError, function.NewArgumentFuncError( + pos, + "Invalid Argument Type: "+ + "An unexpected error was encountered when converting the function argument from the protocol type. "+ + "This is always an issue in terraform-plugin-framework used to implement the provider and should be reported to the provider developers.\n\n"+ + "Please report this to the provider developer:\n\n"+ + fmt.Sprintf("Expected basetypes.Float32Valuable at position %d", pos), + )) + + continue + } + float32Val, diags := float32Valuable.ToFloat32Value(ctx) + if diags.HasError() { + funcError = function.ConcatFuncErrors(funcError, function.FuncErrorFromDiags(ctx, diags)) + continue + } + req := function.Float32ParameterValidatorRequest{ + ArgumentPosition: pos, + Value: float32Val, + } + resp := &function.Float32ParameterValidatorResponse{} + functionValidator.ValidateParameterFloat32(ctx, req, resp) + if resp.Error != nil { + funcError = function.ConcatFuncErrors(funcError, function.NewArgumentFuncError( + pos, + resp.Error.Error(), + )) + } + } case function.ParameterWithFloat64Validators: for _, functionValidator := range parameterWithValidators.GetValidators() { float64Valuable, ok := attrValue.(basetypes.Float64Valuable) diff --git a/internal/fromproto6/arguments_data_test.go b/internal/fromproto6/arguments_data_test.go index 1f0e587a..4a6f7f8b 100644 --- a/internal/fromproto6/arguments_data_test.go +++ b/internal/fromproto6/arguments_data_test.go @@ -1061,6 +1061,170 @@ func TestArgumentsData_ParameterValidators(t *testing.T) { 0, "Error Diagnostic: This is an error.", ), }, + "float32-parameter-Validators": { + input: []*tfprotov6.DynamicValue{ + DynamicValueMust(tftypes.NewValue(tftypes.Number, 1.0)), + }, + definition: function.Definition{ + Parameters: []function.Parameter{ + function.Float32Parameter{ + Validators: []function.Float32ParameterValidator{ + testvalidator.Float32{ + ValidateMethod: func(ctx context.Context, req function.Float32ParameterValidatorRequest, resp *function.Float32ParameterValidatorResponse) { + got := req.Value + expected := types.Float32Value(1.0) + + if !got.Equal(expected) { + resp.Error = function.NewArgumentFuncError( + req.ArgumentPosition, + "Error Diagnostic: This is an error.", + ) + } + }, + }, + }, + }, + }, + }, + expected: function.NewArgumentsData([]attr.Value{ + basetypes.NewFloat32Value(1.0), + }), + }, + "float32-parameter-Validators-error": { + input: []*tfprotov6.DynamicValue{ + DynamicValueMust(tftypes.NewValue(tftypes.Number, 1.0)), + }, + definition: function.Definition{ + Parameters: []function.Parameter{ + function.Float32Parameter{ + Validators: []function.Float32ParameterValidator{ + testvalidator.Float32{ + ValidateMethod: func(ctx context.Context, req function.Float32ParameterValidatorRequest, resp *function.Float32ParameterValidatorResponse) { + got := req.Value + expected := types.Float32Value(2.0) + + if !got.Equal(expected) { + resp.Error = function.NewArgumentFuncError( + req.ArgumentPosition, + "Error Diagnostic: This is an error.", + ) + } + }, + }, + }, + }, + }, + }, + expected: function.NewArgumentsData(nil), + expectedFuncError: function.NewArgumentFuncError( + 0, "Error Diagnostic: This is an error.", + ), + }, + "float32-parameter-Validators-multiple-errors": { + input: []*tfprotov6.DynamicValue{ + DynamicValueMust(tftypes.NewValue(tftypes.Number, 1.0)), + }, + definition: function.Definition{ + Parameters: []function.Parameter{ + function.Float32Parameter{ + Validators: []function.Float32ParameterValidator{ + testvalidator.Float32{ + ValidateMethod: func(ctx context.Context, req function.Float32ParameterValidatorRequest, resp *function.Float32ParameterValidatorResponse) { + got := req.Value + expected := types.Float32Value(2.0) + + if !got.Equal(expected) { + resp.Error = function.NewArgumentFuncError( + req.ArgumentPosition, + "Error Diagnostic: error 1.", + ) + } + }, + }, + testvalidator.Float32{ + ValidateMethod: func(ctx context.Context, req function.Float32ParameterValidatorRequest, resp *function.Float32ParameterValidatorResponse) { + got := req.Value + expected := types.Float32Value(3.0) + + if !got.Equal(expected) { + resp.Error = function.NewArgumentFuncError( + req.ArgumentPosition, + "Error Diagnostic: error 2.", + ) + } + }, + }, + }, + }, + }, + }, + expected: function.NewArgumentsData(nil), + expectedFuncError: function.NewArgumentFuncError( + 0, "Error Diagnostic: error 1."+ + "\nError Diagnostic: error 2.", + ), + }, + "float32-parameter-custom-type-Validators": { + input: []*tfprotov6.DynamicValue{ + DynamicValueMust(tftypes.NewValue(tftypes.Number, 1.0)), + }, + definition: function.Definition{ + Parameters: []function.Parameter{ + function.Float32Parameter{ + CustomType: testtypes.Float32Type{}, + Validators: []function.Float32ParameterValidator{ + testvalidator.Float32{ + ValidateMethod: func(ctx context.Context, req function.Float32ParameterValidatorRequest, resp *function.Float32ParameterValidatorResponse) { + got := req.Value + expected := types.Float32Value(1.0) + + if !got.Equal(expected) { + resp.Error = function.NewArgumentFuncError( + req.ArgumentPosition, + "Error Diagnostic: This is an error.", + ) + } + }, + }, + }, + }, + }, + }, + expected: function.NewArgumentsData([]attr.Value{ + basetypes.NewFloat32Value(1.0), + }), + }, + "float32-parameter-custom-type-Validators-error": { + input: []*tfprotov6.DynamicValue{ + DynamicValueMust(tftypes.NewValue(tftypes.Number, 1.0)), + }, + definition: function.Definition{ + Parameters: []function.Parameter{ + function.Float32Parameter{ + CustomType: testtypes.Float32Type{}, + Validators: []function.Float32ParameterValidator{ + testvalidator.Float32{ + ValidateMethod: func(ctx context.Context, req function.Float32ParameterValidatorRequest, resp *function.Float32ParameterValidatorResponse) { + got := req.Value + expected := types.Float32Value(2.0) + + if !got.Equal(expected) { + resp.Error = function.NewArgumentFuncError( + req.ArgumentPosition, + "Error Diagnostic: This is an error.", + ) + } + }, + }, + }, + }, + }, + }, + expected: function.NewArgumentsData(nil), + expectedFuncError: function.NewArgumentFuncError( + 0, "Error Diagnostic: This is an error.", + ), + }, "float64-parameter-Validators": { input: []*tfprotov6.DynamicValue{ DynamicValueMust(tftypes.NewValue(tftypes.Number, 1.0)), diff --git a/internal/fromtftypes/value_test.go b/internal/fromtftypes/value_test.go index a7d860a3..d633c815 100644 --- a/internal/fromtftypes/value_test.go +++ b/internal/fromtftypes/value_test.go @@ -22,6 +22,8 @@ import ( func TestValue(t *testing.T) { t.Parallel() + var float32Value float32 = 1.2 + testCases := map[string]struct { tfType tftypes.Value attrType attr.Type @@ -60,6 +62,21 @@ func TestValue(t *testing.T) { attrType: types.BoolType, expected: types.BoolValue(true), }, + "float32-null": { + tfType: tftypes.NewValue(tftypes.Number, nil), + attrType: types.Float32Type, + expected: types.Float32Null(), + }, + "float32-unknown": { + tfType: tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), + attrType: types.Float32Type, + expected: types.Float32Unknown(), + }, + "float32-value": { + tfType: tftypes.NewValue(tftypes.Number, big.NewFloat(float64(float32Value))), + attrType: types.Float32Type, + expected: types.Float32Value(float32Value), + }, "float64-null": { tfType: tftypes.NewValue(tftypes.Number, nil), attrType: types.Float64Type, diff --git a/internal/fwschema/attribute_default.go b/internal/fwschema/attribute_default.go index 39d27b1a..c0d092e3 100644 --- a/internal/fwschema/attribute_default.go +++ b/internal/fwschema/attribute_default.go @@ -15,6 +15,14 @@ type AttributeWithBoolDefaultValue interface { BoolDefaultValue() defaults.Bool } +// AttributeWithFloat32DefaultValue is an optional interface on Attribute which +// enables Float32 default value support. +type AttributeWithFloat32DefaultValue interface { + Attribute + + Float32DefaultValue() defaults.Float32 +} + // AttributeWithFloat64DefaultValue is an optional interface on Attribute which // enables Float64 default value support. type AttributeWithFloat64DefaultValue interface { diff --git a/internal/fwschema/fwxschema/attribute_plan_modification.go b/internal/fwschema/fwxschema/attribute_plan_modification.go index 6f71e226..d50e0228 100644 --- a/internal/fwschema/fwxschema/attribute_plan_modification.go +++ b/internal/fwschema/fwxschema/attribute_plan_modification.go @@ -17,6 +17,15 @@ type AttributeWithBoolPlanModifiers interface { BoolPlanModifiers() []planmodifier.Bool } +// AttributeWithFloat32PlanModifiers is an optional interface on Attribute which +// enables Float32 plan modifier support. +type AttributeWithFloat32PlanModifiers interface { + fwschema.Attribute + + // Float32PlanModifiers should return a list of Float32 plan modifiers. + Float32PlanModifiers() []planmodifier.Float32 +} + // AttributeWithFloat64PlanModifiers is an optional interface on Attribute which // enables Float64 plan modifier support. type AttributeWithFloat64PlanModifiers interface { diff --git a/internal/fwschema/fwxschema/attribute_validation.go b/internal/fwschema/fwxschema/attribute_validation.go index b806aa08..d94b65af 100644 --- a/internal/fwschema/fwxschema/attribute_validation.go +++ b/internal/fwschema/fwxschema/attribute_validation.go @@ -17,6 +17,15 @@ type AttributeWithBoolValidators interface { BoolValidators() []validator.Bool } +// AttributeWithFloat32Validators is an optional interface on Attribute which +// enables Float32 validation support. +type AttributeWithFloat32Validators interface { + fwschema.Attribute + + // Float32Validators should return a list of Float32 validators. + Float32Validators() []validator.Float32 +} + // AttributeWithFloat64Validators is an optional interface on Attribute which // enables Float64 validation support. type AttributeWithFloat64Validators interface { diff --git a/internal/fwschemadata/data_default.go b/internal/fwschemadata/data_default.go index de5b72a3..88015b3d 100644 --- a/internal/fwschemadata/data_default.go +++ b/internal/fwschemadata/data_default.go @@ -122,6 +122,29 @@ func (d *Data) TransformDefaults(ctx context.Context, configRaw tftypes.Value) d logging.FrameworkTrace(ctx, fmt.Sprintf("setting attribute %s to default value: %s", fwPath, resp.PlanValue)) + return resp.PlanValue.ToTerraformValue(ctx) + case fwschema.AttributeWithFloat32DefaultValue: + defaultValue := a.Float32DefaultValue() + + if defaultValue == nil { + return tfTypeValue, nil + } + + req := defaults.Float32Request{ + Path: fwPath, + } + resp := defaults.Float32Response{} + + defaultValue.DefaultFloat32(ctx, req, &resp) + + diags.Append(resp.Diagnostics...) + + if resp.Diagnostics.HasError() { + return tfTypeValue, nil + } + + logging.FrameworkTrace(ctx, fmt.Sprintf("setting attribute %s to default value: %s", fwPath, resp.PlanValue)) + return resp.PlanValue.ToTerraformValue(ctx) case fwschema.AttributeWithFloat64DefaultValue: defaultValue := a.Float64DefaultValue() diff --git a/internal/fwschemadata/data_default_test.go b/internal/fwschemadata/data_default_test.go index c38f2da6..d6820843 100644 --- a/internal/fwschemadata/data_default_test.go +++ b/internal/fwschemadata/data_default_test.go @@ -23,6 +23,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" "github.com/hashicorp/terraform-plugin-framework/resource/schema/defaults" "github.com/hashicorp/terraform-plugin-framework/resource/schema/dynamicdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/float32default" "github.com/hashicorp/terraform-plugin-framework/resource/schema/float64default" "github.com/hashicorp/terraform-plugin-framework/resource/schema/int32default" "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" @@ -38,6 +39,9 @@ import ( func TestDataDefault(t *testing.T) { t.Parallel() + var float32AttributeValue float32 = 1.2345 + var float32DefaultValue float32 = 5.4321 + testCases := map[string]struct { data *fwschemadata.Data rawConfig tftypes.Value @@ -398,6 +402,360 @@ func TestDataDefault(t *testing.T) { ), }, }, + "float32-attribute-request-path": { + data: &fwschemadata.Data{ + Description: fwschemadata.DataDescriptionPlan, + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "float32_attribute": testschema.AttributeWithFloat32DefaultValue{ + Optional: true, + Computed: true, + Default: testdefaults.Float32{ + DefaultFloat32Method: func(ctx context.Context, req defaults.Float32Request, resp *defaults.Float32Response) { + if !req.Path.Equal(path.Root("float32_attribute")) { + resp.Diagnostics.AddError( + "unexpected req.Path value", + fmt.Sprintf("expected %s, got: %s", path.Root("float32_attribute"), req.Path), + ) + } + }, + }, + }, + }, + }, + TerraformValue: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "float32_attribute": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "float32_attribute": tftypes.NewValue(tftypes.Number, nil), + }, + ), + }, + rawConfig: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "float32_attribute": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "float32_attribute": tftypes.NewValue(tftypes.Number, nil), + }, + ), + expected: &fwschemadata.Data{ + Description: fwschemadata.DataDescriptionPlan, + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "float32_attribute": testschema.AttributeWithFloat32DefaultValue{ + Optional: true, + Computed: true, + Default: testdefaults.Float32{ + DefaultFloat32Method: func(ctx context.Context, req defaults.Float32Request, resp *defaults.Float32Response) { + if !req.Path.Equal(path.Root("float32_attribute")) { + resp.Diagnostics.AddError( + "unexpected req.Path value", + fmt.Sprintf("expected %s, got: %s", path.Root("float32_attribute"), req.Path), + ) + } + }, + }, + }, + }, + }, + TerraformValue: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "float32_attribute": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "float32_attribute": tftypes.NewValue(tftypes.Number, nil), + }, + ), + }, + }, + "float32-attribute-response-diagnostics": { + data: &fwschemadata.Data{ + Description: fwschemadata.DataDescriptionPlan, + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "float32_attribute": testschema.AttributeWithFloat32DefaultValue{ + Optional: true, + Computed: true, + Default: testdefaults.Float32{ + DefaultFloat32Method: func(ctx context.Context, req defaults.Float32Request, resp *defaults.Float32Response) { + resp.Diagnostics.AddError("test error summary", "test error detail") + resp.Diagnostics.AddWarning("test warning summary", "test warning detail") + }, + }, + }, + }, + }, + TerraformValue: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "float32_attribute": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "float32_attribute": tftypes.NewValue(tftypes.Number, nil), + }, + ), + }, + rawConfig: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "float32_attribute": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "float32_attribute": tftypes.NewValue(tftypes.Number, nil), + }, + ), + expected: &fwschemadata.Data{ + Description: fwschemadata.DataDescriptionPlan, + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "float32_attribute": testschema.AttributeWithFloat32DefaultValue{ + Optional: true, + Computed: true, + Default: testdefaults.Float32{ + DefaultFloat32Method: func(ctx context.Context, req defaults.Float32Request, resp *defaults.Float32Response) { + resp.Diagnostics.AddError("test error summary", "test error detail") + resp.Diagnostics.AddWarning("test warning summary", "test warning detail") + }, + }, + }, + }, + }, + TerraformValue: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "float32_attribute": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "float32_attribute": tftypes.NewValue(tftypes.Number, nil), + }, + ), + }, + expectedDiags: diag.Diagnostics{ + diag.NewErrorDiagnostic("test error summary", "test error detail"), + diag.NewWarningDiagnostic("test warning summary", "test warning detail"), + }, + }, + "float32-attribute-not-null-unmodified-default": { + data: &fwschemadata.Data{ + Description: fwschemadata.DataDescriptionState, + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "float32_attribute": testschema.AttributeWithFloat32DefaultValue{ + Computed: true, + Default: float32default.StaticFloat32(5.4321), + }, + }, + }, + TerraformValue: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "float32_attribute": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "float32_attribute": tftypes.NewValue(tftypes.Number, 1.2345), + }, + ), + }, + rawConfig: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "float32_attribute": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "float32_attribute": tftypes.NewValue(tftypes.Number, 5.4321), // value in rawConfig + }, + ), + expected: &fwschemadata.Data{ + Description: fwschemadata.DataDescriptionState, + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "float32_attribute": testschema.AttributeWithFloat32DefaultValue{ + Computed: true, + Default: float32default.StaticFloat32(5.4321), + }, + }, + }, + TerraformValue: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "float32_attribute": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "float32_attribute": tftypes.NewValue(tftypes.Number, 1.2345), + }, + ), + }, + }, + "float32-attribute-null-unmodified-no-default": { + data: &fwschemadata.Data{ + Description: fwschemadata.DataDescriptionState, + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "float32_attribute": testschema.Attribute{ + Computed: true, + Type: types.Float32Type, + }, + }, + }, + TerraformValue: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "float32_attribute": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "float32_attribute": tftypes.NewValue(tftypes.Number, 1.2345), + }, + ), + }, + rawConfig: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "float32_attribute": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "float32_attribute": tftypes.NewValue(tftypes.Number, nil), // value in rawConfig + }, + ), + expected: &fwschemadata.Data{ + Description: fwschemadata.DataDescriptionState, + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "float32_attribute": testschema.Attribute{ + Computed: true, + Type: types.Float32Type, + }, + }, + }, + TerraformValue: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "float32_attribute": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "float32_attribute": tftypes.NewValue(tftypes.Number, 1.2345), + }, + ), + }, + }, + "float32-attribute-null-modified-default": { + data: &fwschemadata.Data{ + Description: fwschemadata.DataDescriptionState, + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "float32_attribute": testschema.AttributeWithFloat32DefaultValue{ + Computed: true, + Default: float32default.StaticFloat32(float32DefaultValue), + }, + }, + }, + TerraformValue: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "float32_attribute": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "float32_attribute": tftypes.NewValue(tftypes.Number, float64(float32AttributeValue)), + }, + ), + }, + rawConfig: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "float32_attribute": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "float32_attribute": tftypes.NewValue(tftypes.Number, nil), // value in rawConfig + }, + ), + expected: &fwschemadata.Data{ + Description: fwschemadata.DataDescriptionState, + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "float32_attribute": testschema.AttributeWithFloat32DefaultValue{ + Computed: true, + Default: float32default.StaticFloat32(float32DefaultValue), + }, + }, + }, + TerraformValue: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "float32_attribute": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "float32_attribute": tftypes.NewValue(tftypes.Number, float64(float32DefaultValue)), + }, + ), + }, + }, + "float32-attribute-null-unmodified-default-nil": { + data: &fwschemadata.Data{ + Description: fwschemadata.DataDescriptionState, + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "float32_attribute": testschema.AttributeWithFloat32DefaultValue{ + Computed: true, + Default: nil, + }, + }, + }, + TerraformValue: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "float32_attribute": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "float32_attribute": tftypes.NewValue(tftypes.Number, 1.2345), + }, + ), + }, + rawConfig: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "float32_attribute": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "float32_attribute": tftypes.NewValue(tftypes.Number, nil), // value in rawConfig + }, + ), + expected: &fwschemadata.Data{ + Description: fwschemadata.DataDescriptionState, + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "float32_attribute": testschema.AttributeWithFloat32DefaultValue{ + Computed: true, + Default: nil, + }, + }, + }, + TerraformValue: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "float32_attribute": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "float32_attribute": tftypes.NewValue(tftypes.Number, 1.2345), + }, + ), + }, + }, "float64-attribute-request-path": { data: &fwschemadata.Data{ Description: fwschemadata.DataDescriptionPlan, diff --git a/internal/fwschemadata/data_get_at_path_test.go b/internal/fwschemadata/data_get_at_path_test.go index 6b464a20..4bb5b95b 100644 --- a/internal/fwschemadata/data_get_at_path_test.go +++ b/internal/fwschemadata/data_get_at_path_test.go @@ -27,6 +27,8 @@ import ( func TestDataGetAtPath(t *testing.T) { t.Parallel() + var Float32Value float32 = 1.2 + testCases := map[string]struct { data fwschemadata.Data path path.Path @@ -475,6 +477,258 @@ func TestDataGetAtPath(t *testing.T) { target: new(bool), expected: pointer(true), }, + "Float32Type-types.Float32-null": { + data: fwschemadata.Data{ + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "float32": testschema.Attribute{ + Optional: true, + Type: types.Float32Type, + }, + }, + }, + TerraformValue: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "float32": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "float32": tftypes.NewValue(tftypes.Number, nil), + }, + ), + }, + path: path.Root("float32"), + target: new(types.Float32), + expected: pointer(types.Float32Null()), + }, + "Float32Type-types.Float32-unknown": { + data: fwschemadata.Data{ + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "float32": testschema.Attribute{ + Optional: true, + Type: types.Float32Type, + }, + }, + }, + TerraformValue: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "float32": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "float32": tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), + }, + ), + }, + path: path.Root("float32"), + target: new(types.Float32), + expected: pointer(types.Float32Unknown()), + }, + "Float32Type-types.Float32-value": { + data: fwschemadata.Data{ + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "float32": testschema.Attribute{ + Optional: true, + Type: types.Float32Type, + }, + }, + }, + TerraformValue: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "float32": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "float32": tftypes.NewValue(tftypes.Number, float64(Float32Value)), + }, + ), + }, + path: path.Root("float32"), + target: new(types.Float32), + expected: pointer(types.Float32Value(Float32Value)), + }, + "Float32Type-*float32-null": { + data: fwschemadata.Data{ + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "float32": testschema.Attribute{ + Optional: true, + Type: types.Float32Type, + }, + }, + }, + TerraformValue: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "float32": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "float32": tftypes.NewValue(tftypes.Number, nil), + }, + ), + }, + path: path.Root("float32"), + target: new(*float32), + expected: new(*float32), + }, + "Float32Type-*float32-unknown": { + data: fwschemadata.Data{ + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "float32": testschema.Attribute{ + Optional: true, + Type: types.Float32Type, + }, + }, + }, + TerraformValue: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "float32": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "float32": tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), + }, + ), + }, + path: path.Root("float32"), + target: new(*float32), + expected: new(*float32), + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("float32"), + "Value Conversion Error", + "An unexpected error was encountered trying to build a value. This is always an error in the provider. Please report the following to the provider developer:\n\n"+ + "Received unknown value, however the target type cannot handle unknown values. Use the corresponding `types` package type or a custom type that handles unknown values.\n\n"+ + "Path: float32\nTarget Type: *float32\nSuggested Type: basetypes.Float32Value", + ), + }, + }, + "Float32Type-*float32-value": { + data: fwschemadata.Data{ + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "float32": testschema.Attribute{ + Optional: true, + Type: types.Float32Type, + }, + }, + }, + TerraformValue: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "float32": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "float32": tftypes.NewValue(tftypes.Number, float64(Float32Value)), + }, + ), + }, + path: path.Root("float32"), + target: new(*float32), + expected: pointer(pointer(Float32Value)), + }, + "Float32Type-float32-null": { + data: fwschemadata.Data{ + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "float32": testschema.Attribute{ + Optional: true, + Type: types.Float32Type, + }, + }, + }, + TerraformValue: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "float32": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "float32": tftypes.NewValue(tftypes.Number, nil), + }, + ), + }, + path: path.Root("float32"), + target: new(float32), + expected: new(float32), + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("float32"), + "Value Conversion Error", + "An unexpected error was encountered trying to build a value. This is always an error in the provider. Please report the following to the provider developer:\n\n"+ + "Received null value, however the target type cannot handle null values. Use the corresponding `types` package type, a pointer type or a custom type that handles null values.\n\n"+ + "Path: float32\nTarget Type: float32\nSuggested `types` Type: basetypes.Float32Value\nSuggested Pointer Type: *float32", + ), + }, + }, + "Float32Type-float32-unknown": { + data: fwschemadata.Data{ + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "float32": testschema.Attribute{ + Optional: true, + Type: types.Float32Type, + }, + }, + }, + TerraformValue: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "float32": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "float32": tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), + }, + ), + }, + path: path.Root("float32"), + target: new(float32), + expected: new(float32), + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("float32"), + "Value Conversion Error", + "An unexpected error was encountered trying to build a value. This is always an error in the provider. Please report the following to the provider developer:\n\n"+ + "Received unknown value, however the target type cannot handle unknown values. Use the corresponding `types` package type or a custom type that handles unknown values.\n\n"+ + "Path: float32\nTarget Type: float32\nSuggested Type: basetypes.Float32Value", + ), + }, + }, + "Float32Type-float32-value": { + data: fwschemadata.Data{ + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "float32": testschema.Attribute{ + Optional: true, + Type: types.Float32Type, + }, + }, + }, + TerraformValue: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "float32": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "float32": tftypes.NewValue(tftypes.Number, float64(Float32Value)), + }, + ), + }, + path: path.Root("float32"), + target: new(float32), + expected: pointer(Float32Value), + }, "Float64Type-types.Float64-null": { data: fwschemadata.Data{ Schema: testschema.Schema{ @@ -7108,6 +7362,9 @@ func TestDataGetAtPath(t *testing.T) { cmp.Comparer(func(i, j *types.Bool) bool { return (i == nil && j == nil) || (i != nil && j != nil && cmp.Equal(*i, *j)) }), + cmp.Comparer(func(i, j *types.Float32) bool { + return (i == nil && j == nil) || (i != nil && j != nil && cmp.Equal(*i, *j)) + }), cmp.Comparer(func(i, j *types.Float64) bool { return (i == nil && j == nil) || (i != nil && j != nil && cmp.Equal(*i, *j)) }), diff --git a/internal/fwschemadata/data_get_test.go b/internal/fwschemadata/data_get_test.go index c22d84a6..6528e985 100644 --- a/internal/fwschemadata/data_get_test.go +++ b/internal/fwschemadata/data_get_test.go @@ -26,6 +26,8 @@ import ( func TestDataGet(t *testing.T) { t.Parallel() + var float32Value float32 = 1.2 + testCases := map[string]struct { data fwschemadata.Data target any @@ -527,6 +529,303 @@ func TestDataGet(t *testing.T) { Bool: true, }, }, + "Float32Type-types.Float32-null": { + data: fwschemadata.Data{ + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "float32": testschema.Attribute{ + Optional: true, + Type: types.Float32Type, + }, + }, + }, + TerraformValue: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "float32": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "float32": tftypes.NewValue(tftypes.Number, nil), + }, + ), + }, + target: new(struct { + Float32 types.Float32 `tfsdk:"float32"` + }), + expected: &struct { + Float32 types.Float32 `tfsdk:"float32"` + }{ + Float32: types.Float32Null(), + }, + }, + "Float32Type-types.Float32-unknown": { + data: fwschemadata.Data{ + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "float32": testschema.Attribute{ + Optional: true, + Type: types.Float32Type, + }, + }, + }, + TerraformValue: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "float32": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "float32": tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), + }, + ), + }, + target: new(struct { + Float32 types.Float32 `tfsdk:"float32"` + }), + expected: &struct { + Float32 types.Float32 `tfsdk:"float32"` + }{ + Float32: types.Float32Unknown(), + }, + }, + "Float32Type-types.Float32-value": { + data: fwschemadata.Data{ + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "float32": testschema.Attribute{ + Optional: true, + Type: types.Float32Type, + }, + }, + }, + TerraformValue: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "float32": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "float32": tftypes.NewValue(tftypes.Number, float64(float32Value)), + }, + ), + }, + target: new(struct { + Float32 types.Float32 `tfsdk:"float32"` + }), + expected: &struct { + Float32 types.Float32 `tfsdk:"float32"` + }{ + Float32: types.Float32Value(float32Value), + }, + }, + "Float32Type-*float32-null": { + data: fwschemadata.Data{ + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "float32": testschema.Attribute{ + Optional: true, + Type: types.Float32Type, + }, + }, + }, + TerraformValue: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "float32": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "float32": tftypes.NewValue(tftypes.Number, nil), + }, + ), + }, + target: new(struct { + Float32 *float32 `tfsdk:"float32"` + }), + expected: &struct { + Float32 *float32 `tfsdk:"float32"` + }{ + Float32: nil, + }, + }, + "Float32Type-*float32-unknown": { + data: fwschemadata.Data{ + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "float32": testschema.Attribute{ + Optional: true, + Type: types.Float32Type, + }, + }, + }, + TerraformValue: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "float32": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "float32": tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), + }, + ), + }, + target: new(struct { + Float32 *float32 `tfsdk:"float32"` + }), + expected: &struct { + Float32 *float32 `tfsdk:"float32"` + }{ + Float32: nil, + }, + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("float32"), + "Value Conversion Error", + "An unexpected error was encountered trying to build a value. This is always an error in the provider. Please report the following to the provider developer:\n\n"+ + "Received unknown value, however the target type cannot handle unknown values. Use the corresponding `types` package type or a custom type that handles unknown values.\n\n"+ + "Path: float32\nTarget Type: *float32\nSuggested Type: basetypes.Float32Value", + ), + }, + }, + "Float32Type-*float32-value": { + data: fwschemadata.Data{ + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "float32": testschema.Attribute{ + Optional: true, + Type: types.Float32Type, + }, + }, + }, + TerraformValue: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "float32": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "float32": tftypes.NewValue(tftypes.Number, float64(float32Value)), + }, + ), + }, + target: new(struct { + Float32 *float32 `tfsdk:"float32"` + }), + expected: &struct { + Float32 *float32 `tfsdk:"float32"` + }{ + Float32: pointer(float32Value), + }, + }, + "Float32Type-float32-null": { + data: fwschemadata.Data{ + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "float32": testschema.Attribute{ + Optional: true, + Type: types.Float32Type, + }, + }, + }, + TerraformValue: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "float32": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "float32": tftypes.NewValue(tftypes.Number, nil), + }, + ), + }, + target: new(struct { + Float32 float32 `tfsdk:"float32"` + }), + expected: &struct { + Float32 float32 `tfsdk:"float32"` + }{ + Float32: 0.0, + }, + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("float32"), + "Value Conversion Error", + "An unexpected error was encountered trying to build a value. This is always an error in the provider. Please report the following to the provider developer:\n\n"+ + "Received null value, however the target type cannot handle null values. Use the corresponding `types` package type, a pointer type or a custom type that handles null values.\n\n"+ + "Path: float32\nTarget Type: float32\nSuggested `types` Type: basetypes.Float32Value\nSuggested Pointer Type: *float32", + ), + }, + }, + "Float32Type-float32-unknown": { + data: fwschemadata.Data{ + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "float32": testschema.Attribute{ + Optional: true, + Type: types.Float32Type, + }, + }, + }, + TerraformValue: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "float32": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "float32": tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), + }, + ), + }, + target: new(struct { + Float32 float32 `tfsdk:"float32"` + }), + expected: &struct { + Float32 float32 `tfsdk:"float32"` + }{ + Float32: 0.0, + }, + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("float32"), + "Value Conversion Error", + "An unexpected error was encountered trying to build a value. This is always an error in the provider. Please report the following to the provider developer:\n\n"+ + "Received unknown value, however the target type cannot handle unknown values. Use the corresponding `types` package type or a custom type that handles unknown values.\n\n"+ + "Path: float32\nTarget Type: float32\nSuggested Type: basetypes.Float32Value", + ), + }, + }, + "Float32Type-float32-value": { + data: fwschemadata.Data{ + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "float32": testschema.Attribute{ + Optional: true, + Type: types.Float32Type, + }, + }, + }, + TerraformValue: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "float32": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "float32": tftypes.NewValue(tftypes.Number, float64(float32Value)), + }, + ), + }, + target: new(struct { + Float32 float32 `tfsdk:"float32"` + }), + expected: &struct { + Float32 float32 `tfsdk:"float32"` + }{ + Float32: float32Value, + }, + }, "Float64Type-types.Float64-null": { data: fwschemadata.Data{ Schema: testschema.Schema{ diff --git a/internal/fwschemadata/data_value_test.go b/internal/fwschemadata/data_value_test.go index 86d9cd96..10cdb9af 100644 --- a/internal/fwschemadata/data_value_test.go +++ b/internal/fwschemadata/data_value_test.go @@ -1074,6 +1074,71 @@ func TestDataValueAtPath(t *testing.T) { )).AtName("sub_test"), expected: types.StringValue("value"), }, + "WithAttributeName-SingleBlock-null-WithAttributeName-Float32": { + data: fwschemadata.Data{ + TerraformValue: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "other_attr": tftypes.Bool, + "other_block": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "sub_test": tftypes.Bool, + }, + }, + "test": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "sub_test": tftypes.Number, + }, + }, + }, + }, map[string]tftypes.Value{ + "other_attr": tftypes.NewValue(tftypes.Bool, nil), + "other_block": tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "sub_test": tftypes.Bool, + }, + }, nil), + "test": tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "sub_test": tftypes.Number, + }, + }, nil), + }), + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "other_attr": testschema.Attribute{ + Type: types.BoolType, + Optional: true, + }, + }, + Blocks: map[string]fwschema.Block{ + "other_block": testschema.Block{ + NestedObject: testschema.NestedBlockObject{ + Attributes: map[string]fwschema.Attribute{ + "sub_test": testschema.Attribute{ + Type: types.BoolType, + Optional: true, + }, + }, + }, + NestingMode: fwschema.BlockNestingModeSingle, + }, + "test": testschema.Block{ + NestedObject: testschema.NestedBlockObject{ + Attributes: map[string]fwschema.Attribute{ + "sub_test": testschema.Attribute{ + Type: types.Float32Type, + Optional: true, + }, + }, + }, + NestingMode: fwschema.BlockNestingModeSingle, + }, + }, + }, + }, + path: path.Root("test").AtName("sub_test"), + expected: types.Float32Null(), + }, "WithAttributeName-SingleBlock-null-WithAttributeName-Float64": { data: fwschemadata.Data{ TerraformValue: tftypes.NewValue(tftypes.Object{ @@ -1480,6 +1545,49 @@ func TestDataValueAtPath(t *testing.T) { path: path.Root("test").AtName("sub_test"), expected: types.StringValue("value"), }, + "WithAttributeName-SingleNestedAttributes-null-WithAttributeName-Float32": { + data: fwschemadata.Data{ + TerraformValue: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "sub_test": tftypes.Number, + }, + }, + "other": tftypes.Bool, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "sub_test": tftypes.Number, + }, + }, nil), + "other": tftypes.NewValue(tftypes.Bool, nil), + }), + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "test": testschema.NestedAttribute{ + NestedObject: testschema.NestedAttributeObject{ + Attributes: map[string]fwschema.Attribute{ + "sub_test": testschema.Attribute{ + Type: types.Float32Type, + Optional: true, + }, + }, + }, + NestingMode: fwschema.NestingModeSingle, + Optional: true, + }, + "other": testschema.Attribute{ + Type: types.BoolType, + Optional: true, + }, + }, + }, + }, + path: path.Root("test").AtName("sub_test"), + expected: types.Float32Null(), + }, "WithAttributeName-SingleNestedAttributes-null-WithAttributeName-Float64": { data: fwschemadata.Data{ TerraformValue: tftypes.NewValue(tftypes.Object{ diff --git a/internal/fwschemadata/value_semantic_equality.go b/internal/fwschemadata/value_semantic_equality.go index 92e06b1f..eca25c56 100644 --- a/internal/fwschemadata/value_semantic_equality.go +++ b/internal/fwschemadata/value_semantic_equality.go @@ -63,6 +63,8 @@ func ValueSemanticEquality(ctx context.Context, req ValueSemanticEqualityRequest switch req.ProposedNewValue.(type) { case basetypes.BoolValuable: ValueSemanticEqualityBool(ctx, req, resp) + case basetypes.Float32Valuable: + ValueSemanticEqualityFloat32(ctx, req, resp) case basetypes.Float64Valuable: ValueSemanticEqualityFloat64(ctx, req, resp) case basetypes.Int32Valuable: diff --git a/internal/fwschemadata/value_semantic_equality_float32.go b/internal/fwschemadata/value_semantic_equality_float32.go new file mode 100644 index 00000000..337c4d0a --- /dev/null +++ b/internal/fwschemadata/value_semantic_equality_float32.go @@ -0,0 +1,54 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fwschemadata + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/internal/logging" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +// ValueSemanticEqualityFloat32 performs float32 type semantic equality. +func ValueSemanticEqualityFloat32(ctx context.Context, req ValueSemanticEqualityRequest, resp *ValueSemanticEqualityResponse) { + priorValuable, ok := req.PriorValue.(basetypes.Float32ValuableWithSemanticEquals) + + // No changes required if the interface is not implemented. + if !ok { + return + } + + proposedNewValuable, ok := req.ProposedNewValue.(basetypes.Float32ValuableWithSemanticEquals) + + // No changes required if the interface is not implemented. + if !ok { + return + } + + logging.FrameworkTrace( + ctx, + "Calling provider defined type-based SemanticEquals", + map[string]interface{}{ + logging.KeyValueType: proposedNewValuable.String(), + }, + ) + + usePriorValue, diags := proposedNewValuable.Float32SemanticEquals(ctx, priorValuable) + + logging.FrameworkTrace( + ctx, + "Called provider defined type-based SemanticEquals", + map[string]interface{}{ + logging.KeyValueType: proposedNewValuable.String(), + }, + ) + + resp.Diagnostics.Append(diags...) + + if !usePriorValue { + return + } + + resp.NewValue = priorValuable +} diff --git a/internal/fwschemadata/value_semantic_equality_float32_test.go b/internal/fwschemadata/value_semantic_equality_float32_test.go new file mode 100644 index 00000000..9af96709 --- /dev/null +++ b/internal/fwschemadata/value_semantic_equality_float32_test.go @@ -0,0 +1,128 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fwschemadata_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschemadata" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func TestValueSemanticEqualityFloat32(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + request fwschemadata.ValueSemanticEqualityRequest + expected *fwschemadata.ValueSemanticEqualityResponse + }{ + "Float32Value": { + request: fwschemadata.ValueSemanticEqualityRequest{ + Path: path.Root("test"), + PriorValue: types.Float32Value(1.2), + ProposedNewValue: types.Float32Value(2.4), + }, + expected: &fwschemadata.ValueSemanticEqualityResponse{ + NewValue: types.Float32Value(2.4), + }, + }, + "Float32ValuableWithSemanticEquals-true": { + request: fwschemadata.ValueSemanticEqualityRequest{ + Path: path.Root("test"), + PriorValue: testtypes.Float32ValueWithSemanticEquals{ + Float32Value: types.Float32Value(1.2), + SemanticEquals: true, + }, + ProposedNewValue: testtypes.Float32ValueWithSemanticEquals{ + Float32Value: types.Float32Value(2.4), + SemanticEquals: true, + }, + }, + expected: &fwschemadata.ValueSemanticEqualityResponse{ + NewValue: testtypes.Float32ValueWithSemanticEquals{ + Float32Value: types.Float32Value(1.2), + SemanticEquals: true, + }, + }, + }, + "Float32ValuableWithSemanticEquals-false": { + request: fwschemadata.ValueSemanticEqualityRequest{ + Path: path.Root("test"), + PriorValue: testtypes.Float32ValueWithSemanticEquals{ + Float32Value: types.Float32Value(1.2), + SemanticEquals: false, + }, + ProposedNewValue: testtypes.Float32ValueWithSemanticEquals{ + Float32Value: types.Float32Value(2.4), + SemanticEquals: false, + }, + }, + expected: &fwschemadata.ValueSemanticEqualityResponse{ + NewValue: testtypes.Float32ValueWithSemanticEquals{ + Float32Value: types.Float32Value(2.4), + SemanticEquals: false, + }, + }, + }, + "Float32ValuableWithSemanticEquals-diagnostics": { + request: fwschemadata.ValueSemanticEqualityRequest{ + Path: path.Root("test"), + PriorValue: testtypes.Float32ValueWithSemanticEquals{ + Float32Value: types.Float32Value(1.2), + SemanticEquals: false, + SemanticEqualsDiagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic("test summary 1", "test detail 1"), + diag.NewErrorDiagnostic("test summary 2", "test detail 2"), + }, + }, + ProposedNewValue: testtypes.Float32ValueWithSemanticEquals{ + Float32Value: types.Float32Value(2.4), + SemanticEquals: false, + SemanticEqualsDiagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic("test summary 1", "test detail 1"), + diag.NewErrorDiagnostic("test summary 2", "test detail 2"), + }, + }, + }, + expected: &fwschemadata.ValueSemanticEqualityResponse{ + NewValue: testtypes.Float32ValueWithSemanticEquals{ + Float32Value: types.Float32Value(2.4), + SemanticEquals: false, + SemanticEqualsDiagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic("test summary 1", "test detail 1"), + diag.NewErrorDiagnostic("test summary 2", "test detail 2"), + }, + }, + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic("test summary 1", "test detail 1"), + diag.NewErrorDiagnostic("test summary 2", "test detail 2"), + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := &fwschemadata.ValueSemanticEqualityResponse{ + NewValue: testCase.request.ProposedNewValue, + } + + fwschemadata.ValueSemanticEqualityFloat32(context.Background(), testCase.request, got) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/internal/fwschemadata/value_semantic_equality_test.go b/internal/fwschemadata/value_semantic_equality_test.go index 7d061ec5..81c85e33 100644 --- a/internal/fwschemadata/value_semantic_equality_test.go +++ b/internal/fwschemadata/value_semantic_equality_test.go @@ -108,6 +108,89 @@ func TestValueSemanticEquality(t *testing.T) { }, }, }, + "Float32Value": { + request: fwschemadata.ValueSemanticEqualityRequest{ + Path: path.Root("test"), + PriorValue: types.Float32Value(1.2), + ProposedNewValue: types.Float32Value(2.4), + }, + expected: &fwschemadata.ValueSemanticEqualityResponse{ + NewValue: types.Float32Value(2.4), + }, + }, + "Float32ValuableWithSemanticEquals-true": { + request: fwschemadata.ValueSemanticEqualityRequest{ + Path: path.Root("test"), + PriorValue: testtypes.Float32ValueWithSemanticEquals{ + Float32Value: types.Float32Value(1.2), + SemanticEquals: true, + }, + ProposedNewValue: testtypes.Float32ValueWithSemanticEquals{ + Float32Value: types.Float32Value(2.4), + SemanticEquals: true, + }, + }, + expected: &fwschemadata.ValueSemanticEqualityResponse{ + NewValue: testtypes.Float32ValueWithSemanticEquals{ + Float32Value: types.Float32Value(1.2), + SemanticEquals: true, + }, + }, + }, + "Float32ValuableWithSemanticEquals-false": { + request: fwschemadata.ValueSemanticEqualityRequest{ + Path: path.Root("test"), + PriorValue: testtypes.Float32ValueWithSemanticEquals{ + Float32Value: types.Float32Value(1.2), + SemanticEquals: false, + }, + ProposedNewValue: testtypes.Float32ValueWithSemanticEquals{ + Float32Value: types.Float32Value(2.4), + SemanticEquals: false, + }, + }, + expected: &fwschemadata.ValueSemanticEqualityResponse{ + NewValue: testtypes.Float32ValueWithSemanticEquals{ + Float32Value: types.Float32Value(2.4), + SemanticEquals: false, + }, + }, + }, + "Float32ValuableWithSemanticEquals-diagnostics": { + request: fwschemadata.ValueSemanticEqualityRequest{ + Path: path.Root("test"), + PriorValue: testtypes.Float32ValueWithSemanticEquals{ + Float32Value: types.Float32Value(1.2), + SemanticEquals: false, + SemanticEqualsDiagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic("test summary 1", "test detail 1"), + diag.NewErrorDiagnostic("test summary 2", "test detail 2"), + }, + }, + ProposedNewValue: testtypes.Float32ValueWithSemanticEquals{ + Float32Value: types.Float32Value(2.4), + SemanticEquals: false, + SemanticEqualsDiagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic("test summary 1", "test detail 1"), + diag.NewErrorDiagnostic("test summary 2", "test detail 2"), + }, + }, + }, + expected: &fwschemadata.ValueSemanticEqualityResponse{ + NewValue: testtypes.Float32ValueWithSemanticEquals{ + Float32Value: types.Float32Value(2.4), + SemanticEquals: false, + SemanticEqualsDiagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic("test summary 1", "test detail 1"), + diag.NewErrorDiagnostic("test summary 2", "test detail 2"), + }, + }, + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic("test summary 1", "test detail 1"), + diag.NewErrorDiagnostic("test summary 2", "test detail 2"), + }, + }, + }, "Float64Value": { request: fwschemadata.ValueSemanticEqualityRequest{ Path: path.Root("test"), diff --git a/internal/fwserver/attr_type.go b/internal/fwserver/attr_type.go index c9665eab..93449818 100644 --- a/internal/fwserver/attr_type.go +++ b/internal/fwserver/attr_type.go @@ -26,6 +26,21 @@ func coerceBoolTypable(ctx context.Context, schemaPath path.Path, valuable baset return typable, nil } +func coerceFloat32Typable(ctx context.Context, schemaPath path.Path, valuable basetypes.Float32Valuable) (basetypes.Float32Typable, diag.Diagnostics) { + typable, ok := valuable.Type(ctx).(basetypes.Float32Typable) + + // Type() of a Valuable should always be a Typable to recreate the Valuable, + // but if for some reason it is not, raise an implementation error instead + // of a panic. + if !ok { + return nil, diag.Diagnostics{ + attributePlanModificationTypableError(schemaPath, valuable), + } + } + + return typable, nil +} + func coerceFloat64Typable(ctx context.Context, schemaPath path.Path, valuable basetypes.Float64Valuable) (basetypes.Float64Typable, diag.Diagnostics) { typable, ok := valuable.Type(ctx).(basetypes.Float64Typable) diff --git a/internal/fwserver/attribute_plan_modification.go b/internal/fwserver/attribute_plan_modification.go index bd12b7fe..c82139c6 100644 --- a/internal/fwserver/attribute_plan_modification.go +++ b/internal/fwserver/attribute_plan_modification.go @@ -92,6 +92,8 @@ func AttributeModifyPlan(ctx context.Context, a fwschema.Attribute, req ModifyAt switch attributeWithPlanModifiers := a.(type) { case fwxschema.AttributeWithBoolPlanModifiers: AttributePlanModifyBool(ctx, attributeWithPlanModifiers, req, resp) + case fwxschema.AttributeWithFloat32PlanModifiers: + AttributePlanModifyFloat32(ctx, attributeWithPlanModifiers, req, resp) case fwxschema.AttributeWithFloat64PlanModifiers: AttributePlanModifyFloat64(ctx, attributeWithPlanModifiers, req, resp) case fwxschema.AttributeWithInt32PlanModifiers: @@ -843,6 +845,166 @@ func AttributePlanModifyBool(ctx context.Context, attribute fwxschema.AttributeW } } +// AttributePlanModifyFloat32 performs all types.Float32 plan modification. +func AttributePlanModifyFloat32(ctx context.Context, attribute fwxschema.AttributeWithFloat32PlanModifiers, req ModifyAttributePlanRequest, resp *ModifyAttributePlanResponse) { + // Use basetypes.Float32Valuable until custom types cannot re-implement + // ValueFromTerraform. Until then, custom types are not technically + // required to implement this interface. This opts to enforce the + // requirement before compatibility promises would interfere. + configValuable, ok := req.AttributeConfig.(basetypes.Float32Valuable) + + if !ok { + resp.Diagnostics.AddAttributeError( + req.AttributePath, + "Invalid Float32 Attribute Plan Modifier Value Type", + "An unexpected value type was encountered while attempting to perform Float32 attribute plan modification. "+ + "The value type must implement the basetypes.Float32Valuable interface. "+ + "Please report this to the provider developers.\n\n"+ + fmt.Sprintf("Incoming Value Type: %T", req.AttributeConfig), + ) + + return + } + + configValue, diags := configValuable.ToFloat32Value(ctx) + + resp.Diagnostics.Append(diags...) + + // Only return early on new errors as the resp.Diagnostics may have errors + // from other attributes. + if diags.HasError() { + return + } + + planValuable, ok := req.AttributePlan.(basetypes.Float32Valuable) + + if !ok { + resp.Diagnostics.AddAttributeError( + req.AttributePath, + "Invalid Float32 Attribute Plan Modifier Value Type", + "An unexpected value type was encountered while attempting to perform Float32 attribute plan modification. "+ + "The value type must implement the basetypes.Float32Valuable interface. "+ + "Please report this to the provider developers.\n\n"+ + fmt.Sprintf("Incoming Value Type: %T", req.AttributePlan), + ) + + return + } + + planValue, diags := planValuable.ToFloat32Value(ctx) + + resp.Diagnostics.Append(diags...) + + // Only return early on new errors as the resp.Diagnostics may have errors + // from other attributes. + if diags.HasError() { + return + } + + stateValuable, ok := req.AttributeState.(basetypes.Float32Valuable) + + if !ok { + resp.Diagnostics.AddAttributeError( + req.AttributePath, + "Invalid Float32 Attribute Plan Modifier Value Type", + "An unexpected value type was encountered while attempting to perform Float32 attribute plan modification. "+ + "The value type must implement the basetypes.Float32Valuable interface. "+ + "Please report this to the provider developers.\n\n"+ + fmt.Sprintf("Incoming Value Type: %T", req.AttributeState), + ) + + return + } + + stateValue, diags := stateValuable.ToFloat32Value(ctx) + + resp.Diagnostics.Append(diags...) + + // Only return early on new errors as the resp.Diagnostics may have errors + // from other attributes. + if diags.HasError() { + return + } + + typable, diags := coerceFloat32Typable(ctx, req.AttributePath, planValuable) + + resp.Diagnostics.Append(diags...) + + // Only return early on new errors as the resp.Diagnostics may have errors + // from other attributes. + if diags.HasError() { + return + } + + planModifyReq := planmodifier.Float32Request{ + Config: req.Config, + ConfigValue: configValue, + Path: req.AttributePath, + PathExpression: req.AttributePathExpression, + Plan: req.Plan, + PlanValue: planValue, + Private: req.Private, + State: req.State, + StateValue: stateValue, + } + + for _, planModifier := range attribute.Float32PlanModifiers() { + // Instantiate a new response for each request to prevent plan modifiers + // from modifying or removing diagnostics. + planModifyResp := &planmodifier.Float32Response{ + PlanValue: planModifyReq.PlanValue, + Private: resp.Private, + } + + logging.FrameworkTrace( + ctx, + "Calling provider defined planmodifier.Float32", + map[string]interface{}{ + logging.KeyDescription: planModifier.Description(ctx), + }, + ) + + planModifier.PlanModifyFloat32(ctx, planModifyReq, planModifyResp) + + logging.FrameworkTrace( + ctx, + "Called provider defined planmodifier.Float32", + map[string]interface{}{ + logging.KeyDescription: planModifier.Description(ctx), + }, + ) + + // Prepare next request with base type. + planModifyReq.PlanValue = planModifyResp.PlanValue + + resp.Diagnostics.Append(planModifyResp.Diagnostics...) + resp.Private = planModifyResp.Private + + if planModifyResp.RequiresReplace { + resp.RequiresReplace.Append(req.AttributePath) + } + + // Only on new errors. + if planModifyResp.Diagnostics.HasError() { + return + } + + // A custom value type must be returned in the final response to prevent + // later correctness errors. + // Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/754 + valuable, valueFromDiags := typable.ValueFromFloat32(ctx, planModifyResp.PlanValue) + + resp.Diagnostics.Append(valueFromDiags...) + + // Only on new errors. + if valueFromDiags.HasError() { + return + } + + resp.AttributePlan = valuable + } +} + // AttributePlanModifyFloat64 performs all types.Float64 plan modification. func AttributePlanModifyFloat64(ctx context.Context, attribute fwxschema.AttributeWithFloat64PlanModifiers, req ModifyAttributePlanRequest, resp *ModifyAttributePlanResponse) { // Use basetypes.Float64Valuable until custom types cannot re-implement diff --git a/internal/fwserver/attribute_plan_modification_test.go b/internal/fwserver/attribute_plan_modification_test.go index ee4553fd..f390311a 100644 --- a/internal/fwserver/attribute_plan_modification_test.go +++ b/internal/fwserver/attribute_plan_modification_test.go @@ -3943,6 +3943,642 @@ func TestAttributePlanModifyBool(t *testing.T) { } } +func TestAttributePlanModifyFloat32(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute fwxschema.AttributeWithFloat32PlanModifiers + request ModifyAttributePlanRequest + response *ModifyAttributePlanResponse + expected *ModifyAttributePlanResponse + }{ + "request-path": { + attribute: testschema.AttributeWithFloat32PlanModifiers{ + PlanModifiers: []planmodifier.Float32{ + testplanmodifier.Float32{ + PlanModifyFloat32Method: func(ctx context.Context, req planmodifier.Float32Request, resp *planmodifier.Float32Response) { + got := req.Path + expected := path.Root("test") + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected Float32Request.Path", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.Float32Value(1.2), + AttributePlan: types.Float32Value(1.2), + AttributeState: types.Float32Value(1.2), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.Float32Value(1.2), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.Float32Value(1.2), + }, + }, + "request-pathexpression": { + attribute: testschema.AttributeWithFloat32PlanModifiers{ + PlanModifiers: []planmodifier.Float32{ + testplanmodifier.Float32{ + PlanModifyFloat32Method: func(ctx context.Context, req planmodifier.Float32Request, resp *planmodifier.Float32Response) { + got := req.PathExpression + expected := path.MatchRoot("test") + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected Float32Request.PathExpression", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), + AttributeConfig: types.Float32Value(1.2), + AttributePlan: types.Float32Value(1.2), + AttributeState: types.Float32Value(1.2), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.Float32Value(1.2), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.Float32Value(1.2), + }, + }, + "request-config": { + attribute: testschema.AttributeWithFloat32PlanModifiers{ + PlanModifiers: []planmodifier.Float32{ + testplanmodifier.Float32{ + PlanModifyFloat32Method: func(ctx context.Context, req planmodifier.Float32Request, resp *planmodifier.Float32Response) { + got := req.Config + expected := tfsdk.Config{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.Number, 1.2), + }, + ), + } + + if !got.Raw.Equal(expected.Raw) { + resp.Diagnostics.AddError( + "Unexpected Float32Request.Config", + fmt.Sprintf("expected %s, got: %s", expected.Raw, got.Raw), + ) + } + }, + }, + }, + }, + request: ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.Float32Value(1.2), + AttributePlan: types.Float32Value(1.2), + AttributeState: types.Float32Value(1.2), + Config: tfsdk.Config{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.Number, 1.2), + }, + ), + }, + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.Float32Value(1.2), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.Float32Value(1.2), + }, + }, + "request-configvalue": { + attribute: testschema.AttributeWithFloat32PlanModifiers{ + PlanModifiers: []planmodifier.Float32{ + testplanmodifier.Float32{ + PlanModifyFloat32Method: func(ctx context.Context, req planmodifier.Float32Request, resp *planmodifier.Float32Response) { + got := req.ConfigValue + expected := types.Float32Value(1.2) + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected Float32Request.ConfigValue", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.Float32Value(1.2), + AttributePlan: types.Float32Null(), + AttributeState: types.Float32Null(), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.Float32Null(), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.Float32Null(), + }, + }, + "request-plan": { + attribute: testschema.AttributeWithFloat32PlanModifiers{ + PlanModifiers: []planmodifier.Float32{ + testplanmodifier.Float32{ + PlanModifyFloat32Method: func(ctx context.Context, req planmodifier.Float32Request, resp *planmodifier.Float32Response) { + got := req.Plan + expected := tfsdk.Plan{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.Number, 1.2), + }, + ), + } + + if !got.Raw.Equal(expected.Raw) { + resp.Diagnostics.AddError( + "Unexpected Float32Request.Plan", + fmt.Sprintf("expected %s, got: %s", expected.Raw, got.Raw), + ) + } + }, + }, + }, + }, + request: ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.Float32Value(1.2), + AttributePlan: types.Float32Value(1.2), + AttributeState: types.Float32Value(1.2), + Plan: tfsdk.Plan{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.Number, 1.2), + }, + ), + }, + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.Float32Value(1.2), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.Float32Value(1.2), + }, + }, + "request-planvalue": { + attribute: testschema.AttributeWithFloat32PlanModifiers{ + PlanModifiers: []planmodifier.Float32{ + testplanmodifier.Float32{ + PlanModifyFloat32Method: func(ctx context.Context, req planmodifier.Float32Request, resp *planmodifier.Float32Response) { + got := req.PlanValue + expected := types.Float32Value(1.2) + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected Float32Request.PlanValue", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.Float32Null(), + AttributePlan: types.Float32Value(1.2), + AttributeState: types.Float32Null(), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.Float32Value(1.2), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.Float32Value(1.2), + }, + }, + "request-private": { + attribute: testschema.AttributeWithFloat32PlanModifiers{ + PlanModifiers: []planmodifier.Float32{ + testplanmodifier.Float32{ + PlanModifyFloat32Method: func(ctx context.Context, req planmodifier.Float32Request, resp *planmodifier.Float32Response) { + got, diags := req.Private.GetKey(ctx, "testkey") + expected := []byte(`{"testproperty":true}`) + + resp.Diagnostics.Append(diags...) + + if diff := cmp.Diff(got, expected); diff != "" { + resp.Diagnostics.AddError( + "Unexpected Float32Request.Private", + diff, + ) + } + }, + }, + }, + }, + request: ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.Float32Null(), + AttributePlan: types.Float32Value(1.2), + AttributeState: types.Float32Null(), + Private: privatestate.MustProviderData( + context.Background(), + privatestate.MustMarshalToJson(map[string][]byte{ + "testkey": []byte(`{"testproperty":true}`), + }), + ), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.Float32Value(1.2), + Private: privatestate.MustProviderData( + context.Background(), + privatestate.MustMarshalToJson(map[string][]byte{ + "testkey": []byte(`{"testproperty":true}`), // copied from request + }), + ), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.Float32Value(1.2), + Private: privatestate.MustProviderData( + context.Background(), + privatestate.MustMarshalToJson(map[string][]byte{ + "testkey": []byte(`{"testproperty":true}`), + }), + ), + }, + }, + "request-state": { + attribute: testschema.AttributeWithFloat32PlanModifiers{ + PlanModifiers: []planmodifier.Float32{ + testplanmodifier.Float32{ + PlanModifyFloat32Method: func(ctx context.Context, req planmodifier.Float32Request, resp *planmodifier.Float32Response) { + got := req.State + expected := tfsdk.State{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.Number, 1.2), + }, + ), + } + + if !got.Raw.Equal(expected.Raw) { + resp.Diagnostics.AddError( + "Unexpected Float32Request.State", + fmt.Sprintf("expected %s, got: %s", expected.Raw, got.Raw), + ) + } + }, + }, + }, + }, + request: ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.Float32Value(1.2), + AttributePlan: types.Float32Value(1.2), + AttributeState: types.Float32Value(1.2), + State: tfsdk.State{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.Number, 1.2), + }, + ), + }, + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.Float32Value(1.2), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.Float32Value(1.2), + }, + }, + "request-statevalue": { + attribute: testschema.AttributeWithFloat32PlanModifiers{ + PlanModifiers: []planmodifier.Float32{ + testplanmodifier.Float32{ + PlanModifyFloat32Method: func(ctx context.Context, req planmodifier.Float32Request, resp *planmodifier.Float32Response) { + got := req.StateValue + expected := types.Float32Value(1.2) + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected Float32Request.StateValue", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.Float32Null(), + AttributePlan: types.Float32Null(), + AttributeState: types.Float32Value(1.2), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.Float32Null(), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.Float32Null(), + }, + }, + "response-diagnostics": { + attribute: testschema.AttributeWithFloat32PlanModifiers{ + PlanModifiers: []planmodifier.Float32{ + testplanmodifier.Float32{ + PlanModifyFloat32Method: func(ctx context.Context, req planmodifier.Float32Request, resp *planmodifier.Float32Response) { + resp.Diagnostics.AddAttributeWarning(req.Path, "New Warning Summary", "New Warning Details") + resp.Diagnostics.AddAttributeError(req.Path, "New Error Summary", "New Error Details") + }, + }, + }, + }, + request: ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.Float32Value(1.2), + AttributePlan: types.Float32Value(1.2), + AttributeState: types.Float32Value(1.2), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.Float32Value(1.2), + Diagnostics: diag.Diagnostics{ + diag.NewAttributeWarningDiagnostic( + path.Root("other"), + "Existing Warning Summary", + "Existing Warning Details", + ), + diag.NewAttributeErrorDiagnostic( + path.Root("other"), + "Existing Error Summary", + "Existing Error Details", + ), + }, + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.Float32Value(1.2), + Diagnostics: diag.Diagnostics{ + diag.NewAttributeWarningDiagnostic( + path.Root("other"), + "Existing Warning Summary", + "Existing Warning Details", + ), + diag.NewAttributeErrorDiagnostic( + path.Root("other"), + "Existing Error Summary", + "Existing Error Details", + ), + diag.NewAttributeWarningDiagnostic( + path.Root("test"), + "New Warning Summary", + "New Warning Details", + ), + diag.NewAttributeErrorDiagnostic( + path.Root("test"), + "New Error Summary", + "New Error Details", + ), + }, + }, + }, + "response-planvalue": { + attribute: testschema.AttributeWithFloat32PlanModifiers{ + PlanModifiers: []planmodifier.Float32{ + testplanmodifier.Float32{ + PlanModifyFloat32Method: func(ctx context.Context, req planmodifier.Float32Request, resp *planmodifier.Float32Response) { + resp.PlanValue = types.Float32Value(1.2) + }, + }, + }, + }, + request: ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.Float32Null(), + AttributePlan: types.Float32Unknown(), + AttributeState: types.Float32Null(), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.Float32Unknown(), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.Float32Value(1.2), + }, + }, + "response-planvalue-custom-type": { + attribute: testschema.AttributeWithFloat32PlanModifiers{ + PlanModifiers: []planmodifier.Float32{ + testplanmodifier.Float32{ + PlanModifyFloat32Method: func(ctx context.Context, req planmodifier.Float32Request, resp *planmodifier.Float32Response) { + resp.PlanValue = types.Float32Value(1.2) + }, + }, + }, + }, + request: ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: testtypes.Float32ValueWithSemanticEquals{ + Float32Value: types.Float32Null(), + }, + AttributePlan: testtypes.Float32ValueWithSemanticEquals{ + Float32Value: types.Float32Unknown(), + }, + AttributeState: testtypes.Float32ValueWithSemanticEquals{ + Float32Value: types.Float32Null(), + }, + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: testtypes.Float32ValueWithSemanticEquals{ + Float32Value: types.Float32Unknown(), + }, + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: testtypes.Float32ValueWithSemanticEquals{ + Float32Value: types.Float32Value(1.2), + }, + }, + }, + "response-private": { + attribute: testschema.AttributeWithFloat32PlanModifiers{ + PlanModifiers: []planmodifier.Float32{ + testplanmodifier.Float32{ + PlanModifyFloat32Method: func(ctx context.Context, req planmodifier.Float32Request, resp *planmodifier.Float32Response) { + resp.Diagnostics.Append( + resp.Private.SetKey(ctx, "testkey", []byte(`{"newtestproperty":true}`))..., + ) + }, + }, + }, + }, + request: ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.Float32Null(), + AttributePlan: types.Float32Value(1.2), + AttributeState: types.Float32Null(), + Private: privatestate.MustProviderData( + context.Background(), + privatestate.MustMarshalToJson(map[string][]byte{ + "testkey": []byte(`{"testproperty":true}`), + }), + ), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.Float32Value(1.2), + Private: privatestate.MustProviderData( + context.Background(), + privatestate.MustMarshalToJson(map[string][]byte{ + "testkey": []byte(`{"testproperty":true}`), // copied from request + }), + ), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.Float32Value(1.2), + Private: privatestate.MustProviderData( + context.Background(), + privatestate.MustMarshalToJson(map[string][]byte{ + "testkey": []byte(`{"newtestproperty":true}`), + }), + ), + }, + }, + "response-requiresreplace-add": { + attribute: testschema.AttributeWithFloat32PlanModifiers{ + PlanModifiers: []planmodifier.Float32{ + testplanmodifier.Float32{ + PlanModifyFloat32Method: func(ctx context.Context, req planmodifier.Float32Request, resp *planmodifier.Float32Response) { + resp.RequiresReplace = true + }, + }, + }, + }, + request: ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.Float32Value(1.2), + AttributePlan: types.Float32Value(1.2), + AttributeState: types.Float32Value(2.4), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.Float32Value(1.2), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.Float32Value(1.2), + RequiresReplace: path.Paths{ + path.Root("test"), + }, + }, + }, + "response-requiresreplace-false": { + attribute: testschema.AttributeWithFloat32PlanModifiers{ + PlanModifiers: []planmodifier.Float32{ + testplanmodifier.Float32{ + PlanModifyFloat32Method: func(ctx context.Context, req planmodifier.Float32Request, resp *planmodifier.Float32Response) { + resp.RequiresReplace = false // same as not being set + }, + }, + }, + }, + request: ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.Float32Value(1.2), + AttributePlan: types.Float32Value(1.2), + AttributeState: types.Float32Value(2.4), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.Float32Value(1.2), + RequiresReplace: path.Paths{ + path.Root("test"), // Set by prior plan modifier + }, + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.Float32Value(1.2), + RequiresReplace: path.Paths{ + path.Root("test"), // Remains as it should not be removed + }, + }, + }, + "response-requiresreplace-update": { + attribute: testschema.AttributeWithFloat32PlanModifiers{ + PlanModifiers: []planmodifier.Float32{ + testplanmodifier.Float32{ + PlanModifyFloat32Method: func(ctx context.Context, req planmodifier.Float32Request, resp *planmodifier.Float32Response) { + resp.RequiresReplace = true + }, + }, + }, + }, + request: ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.Float32Value(1.2), + AttributePlan: types.Float32Value(1.2), + AttributeState: types.Float32Value(2.4), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.Float32Value(1.2), + RequiresReplace: path.Paths{ + path.Root("test"), // Set by prior plan modifier + }, + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.Float32Value(1.2), + RequiresReplace: path.Paths{ + path.Root("test"), // Remains deduplicated + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + AttributePlanModifyFloat32(context.Background(), testCase.attribute, testCase.request, testCase.response) + + if diff := cmp.Diff(testCase.response, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestAttributePlanModifyFloat64(t *testing.T) { t.Parallel() diff --git a/internal/fwserver/attribute_validation.go b/internal/fwserver/attribute_validation.go index d5b51dba..98b05f0f 100644 --- a/internal/fwserver/attribute_validation.go +++ b/internal/fwserver/attribute_validation.go @@ -111,6 +111,8 @@ func AttributeValidate(ctx context.Context, a fwschema.Attribute, req ValidateAt switch attributeWithValidators := a.(type) { case fwxschema.AttributeWithBoolValidators: AttributeValidateBool(ctx, attributeWithValidators, req, resp) + case fwxschema.AttributeWithFloat32Validators: + AttributeValidateFloat32(ctx, attributeWithValidators, req, resp) case fwxschema.AttributeWithFloat64Validators: AttributeValidateFloat64(ctx, attributeWithValidators, req, resp) case fwxschema.AttributeWithInt32Validators: @@ -231,6 +233,71 @@ func AttributeValidateBool(ctx context.Context, attribute fwxschema.AttributeWit } } +// AttributeValidateFloat32 performs all types.Float32 validation. +func AttributeValidateFloat32(ctx context.Context, attribute fwxschema.AttributeWithFloat32Validators, req ValidateAttributeRequest, resp *ValidateAttributeResponse) { + // Use basetypes.Float32Valuable until custom types cannot re-implement + // ValueFromTerraform. Until then, custom types are not technically + // required to implement this interface. This opts to enforce the + // requirement before compatibility promises would interfere. + configValuable, ok := req.AttributeConfig.(basetypes.Float32Valuable) + + if !ok { + resp.Diagnostics.AddAttributeError( + req.AttributePath, + "Invalid Float32 Attribute Validator Value Type", + "An unexpected value type was encountered while attempting to perform Float32 attribute validation. "+ + "The value type must implement the basetypes.Float32Valuable interface. "+ + "Please report this to the provider developers.\n\n"+ + fmt.Sprintf("Incoming Value Type: %T", req.AttributeConfig), + ) + + return + } + + configValue, diags := configValuable.ToFloat32Value(ctx) + + resp.Diagnostics.Append(diags...) + + // Only return early on new errors as the resp.Diagnostics may have errors + // from other attributes. + if diags.HasError() { + return + } + + validateReq := validator.Float32Request{ + Config: req.Config, + ConfigValue: configValue, + Path: req.AttributePath, + PathExpression: req.AttributePathExpression, + } + + for _, attributeValidator := range attribute.Float32Validators() { + // Instantiate a new response for each request to prevent validators + // from modifying or removing diagnostics. + validateResp := &validator.Float32Response{} + + logging.FrameworkTrace( + ctx, + "Calling provider defined validator.Float32", + map[string]interface{}{ + logging.KeyDescription: attributeValidator.Description(ctx), + }, + ) + + attributeValidator.ValidateFloat32(ctx, validateReq, validateResp) + + logging.FrameworkTrace( + ctx, + "Called provider defined validator.Float32", + map[string]interface{}{ + logging.KeyDescription: attributeValidator.Description(ctx), + }, + ) + + resp.Diagnostics.Append(validateResp.Diagnostics...) + } +} + // AttributeValidateFloat64 performs all types.Float64 validation. func AttributeValidateFloat64(ctx context.Context, attribute fwxschema.AttributeWithFloat64Validators, req ValidateAttributeRequest, resp *ValidateAttributeResponse) { // Use basetypes.Float64Valuable until custom types cannot re-implement diff --git a/internal/fwserver/attribute_validation_test.go b/internal/fwserver/attribute_validation_test.go index b0d076d8..457264e0 100644 --- a/internal/fwserver/attribute_validation_test.go +++ b/internal/fwserver/attribute_validation_test.go @@ -1930,6 +1930,210 @@ func TestAttributeValidateBool(t *testing.T) { } } +func TestAttributeValidateFloat32(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute fwxschema.AttributeWithFloat32Validators + request ValidateAttributeRequest + response *ValidateAttributeResponse + expected *ValidateAttributeResponse + }{ + "request-path": { + attribute: testschema.AttributeWithFloat32Validators{ + Validators: []validator.Float32{ + testvalidator.Float32{ + ValidateFloat32Method: func(ctx context.Context, req validator.Float32Request, resp *validator.Float32Response) { + got := req.Path + expected := path.Root("test") + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected Float32Request.Path", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.Float32Value(1.2), + }, + response: &ValidateAttributeResponse{}, + expected: &ValidateAttributeResponse{}, + }, + "request-pathexpression": { + attribute: testschema.AttributeWithFloat32Validators{ + Validators: []validator.Float32{ + testvalidator.Float32{ + ValidateFloat32Method: func(ctx context.Context, req validator.Float32Request, resp *validator.Float32Response) { + got := req.PathExpression + expected := path.MatchRoot("test") + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected Float32Request.PathExpression", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), + AttributeConfig: types.Float32Value(1.2), + }, + response: &ValidateAttributeResponse{}, + expected: &ValidateAttributeResponse{}, + }, + "request-config": { + attribute: testschema.AttributeWithFloat32Validators{ + Validators: []validator.Float32{ + testvalidator.Float32{ + ValidateFloat32Method: func(ctx context.Context, req validator.Float32Request, resp *validator.Float32Response) { + got := req.Config + expected := tfsdk.Config{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.Number, 1.2), + }, + ), + } + + if !got.Raw.Equal(expected.Raw) { + resp.Diagnostics.AddError( + "Unexpected Float32Request.Config", + fmt.Sprintf("expected %s, got: %s", expected.Raw, got.Raw), + ) + } + }, + }, + }, + }, + request: ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.Float32Value(1.2), + Config: tfsdk.Config{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.Number, 1.2), + }, + ), + }, + }, + response: &ValidateAttributeResponse{}, + expected: &ValidateAttributeResponse{}, + }, + "request-configvalue": { + attribute: testschema.AttributeWithFloat32Validators{ + Validators: []validator.Float32{ + testvalidator.Float32{ + ValidateFloat32Method: func(ctx context.Context, req validator.Float32Request, resp *validator.Float32Response) { + got := req.ConfigValue + expected := types.Float32Value(1.2) + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected Float32Request.ConfigValue", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.Float32Value(1.2), + }, + response: &ValidateAttributeResponse{}, + expected: &ValidateAttributeResponse{}, + }, + "response-diagnostics": { + attribute: testschema.AttributeWithFloat32Validators{ + Validators: []validator.Float32{ + testvalidator.Float32{ + ValidateFloat32Method: func(ctx context.Context, req validator.Float32Request, resp *validator.Float32Response) { + resp.Diagnostics.AddAttributeWarning(req.Path, "New Warning Summary", "New Warning Details") + resp.Diagnostics.AddAttributeError(req.Path, "New Error Summary", "New Error Details") + }, + }, + }, + }, + request: ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.Float32Value(1.2), + }, + response: &ValidateAttributeResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewAttributeWarningDiagnostic( + path.Root("other"), + "Existing Warning Summary", + "Existing Warning Details", + ), + diag.NewAttributeErrorDiagnostic( + path.Root("other"), + "Existing Error Summary", + "Existing Error Details", + ), + }, + }, + expected: &ValidateAttributeResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewAttributeWarningDiagnostic( + path.Root("other"), + "Existing Warning Summary", + "Existing Warning Details", + ), + diag.NewAttributeErrorDiagnostic( + path.Root("other"), + "Existing Error Summary", + "Existing Error Details", + ), + diag.NewAttributeWarningDiagnostic( + path.Root("test"), + "New Warning Summary", + "New Warning Details", + ), + diag.NewAttributeErrorDiagnostic( + path.Root("test"), + "New Error Summary", + "New Error Details", + ), + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + AttributeValidateFloat32(context.Background(), testCase.attribute, testCase.request, testCase.response) + + if diff := cmp.Diff(testCase.response, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestAttributeValidateFloat64(t *testing.T) { t.Parallel() diff --git a/internal/fwserver/server_planresourcechange.go b/internal/fwserver/server_planresourcechange.go index 54402da2..d66fc489 100644 --- a/internal/fwserver/server_planresourcechange.go +++ b/internal/fwserver/server_planresourcechange.go @@ -408,6 +408,10 @@ func MarkComputedNilsAsUnknown(ctx context.Context, config tftypes.Value, resour if a.BoolDefaultValue() != nil { return val, nil } + case fwschema.AttributeWithFloat32DefaultValue: + if a.Float32DefaultValue() != nil { + return val, nil + } case fwschema.AttributeWithFloat64DefaultValue: if a.Float64DefaultValue() != nil { return val, nil diff --git a/internal/fwserver/server_planresourcechange_test.go b/internal/fwserver/server_planresourcechange_test.go index ef8757b0..66e69393 100644 --- a/internal/fwserver/server_planresourcechange_test.go +++ b/internal/fwserver/server_planresourcechange_test.go @@ -27,6 +27,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" "github.com/hashicorp/terraform-plugin-framework/resource/schema/dynamicdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/float32default" "github.com/hashicorp/terraform-plugin-framework/resource/schema/float64default" "github.com/hashicorp/terraform-plugin-framework/resource/schema/int32default" "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" @@ -422,6 +423,14 @@ func TestNormaliseRequiresReplace(t *testing.T) { func TestServerPlanResourceChange(t *testing.T) { t.Parallel() + // Float32 values need to be explicitly declared in a variable. + // Using a float literal results in a conversion from float64 to + // float32 which may result in accuracy loss. + var float32Value float32 = 1.2 + var computedFloat32Value float32 = 1.2345 + var updatedFloat32Value float32 = 5.4321 + var configuredFloat32Value float32 = 2.4 + testSchemaType := tftypes.Object{ AttributeTypes: map[string]tftypes.Type{ "test_computed": tftypes.String, @@ -432,6 +441,7 @@ func TestServerPlanResourceChange(t *testing.T) { testSchemaTypeDefault := tftypes.Object{ AttributeTypes: map[string]tftypes.Type{ "test_computed_bool": tftypes.Bool, + "test_computed_float32": tftypes.Number, "test_computed_float64": tftypes.Number, "test_computed_int32": tftypes.Number, "test_computed_int64": tftypes.Number, @@ -452,6 +462,7 @@ func TestServerPlanResourceChange(t *testing.T) { "test_computed_nested_single": tftypes.Object{AttributeTypes: map[string]tftypes.Type{"string_attribute": tftypes.String}}, "test_computed_nested_single_attribute": tftypes.Object{AttributeTypes: map[string]tftypes.Type{"string_attribute": tftypes.String}}, "test_configured_bool": tftypes.Bool, + "test_configured_float32": tftypes.Number, "test_configured_float64": tftypes.Number, "test_configured_int32": tftypes.Number, "test_configured_int64": tftypes.Number, @@ -562,6 +573,10 @@ func TestServerPlanResourceChange(t *testing.T) { Computed: true, Default: booldefault.StaticBool(true), }, + "test_computed_float32": schema.Float32Attribute{ + Computed: true, + Default: float32default.StaticFloat32(computedFloat32Value), + }, "test_computed_float64": schema.Float64Attribute{ Computed: true, Default: float64default.StaticFloat64(1.2345), @@ -752,6 +767,11 @@ func TestServerPlanResourceChange(t *testing.T) { Optional: true, Default: booldefault.StaticBool(true), }, + "test_configured_float32": schema.Float32Attribute{ + Optional: true, + Computed: true, + Default: float32default.StaticFloat32(computedFloat32Value), + }, "test_configured_float64": schema.Float64Attribute{ Optional: true, Computed: true, @@ -1356,6 +1376,7 @@ func TestServerPlanResourceChange(t *testing.T) { Config: &tfsdk.Config{ Raw: tftypes.NewValue(testSchemaTypeDefault, map[string]tftypes.Value{ "test_computed_bool": tftypes.NewValue(tftypes.Bool, nil), + "test_computed_float32": tftypes.NewValue(tftypes.Number, nil), "test_computed_float64": tftypes.NewValue(tftypes.Number, nil), "test_computed_int32": tftypes.NewValue(tftypes.Number, nil), "test_computed_int64": tftypes.NewValue(tftypes.Number, nil), @@ -1479,6 +1500,7 @@ func TestServerPlanResourceChange(t *testing.T) { }, ), "test_configured_bool": tftypes.NewValue(tftypes.Bool, true), + "test_configured_float32": tftypes.NewValue(tftypes.Number, float64(float32Value)), "test_configured_float64": tftypes.NewValue(tftypes.Number, 1.2), "test_configured_int32": tftypes.NewValue(tftypes.Number, 123), "test_configured_int64": tftypes.NewValue(tftypes.Number, 123), @@ -1732,6 +1754,7 @@ func TestServerPlanResourceChange(t *testing.T) { ProposedNewState: &tfsdk.Plan{ Raw: tftypes.NewValue(testSchemaTypeDefault, map[string]tftypes.Value{ "test_computed_bool": tftypes.NewValue(tftypes.Bool, nil), + "test_computed_float32": tftypes.NewValue(tftypes.Number, nil), "test_computed_float64": tftypes.NewValue(tftypes.Number, nil), "test_computed_int32": tftypes.NewValue(tftypes.Number, nil), "test_computed_int64": tftypes.NewValue(tftypes.Number, nil), @@ -1855,6 +1878,7 @@ func TestServerPlanResourceChange(t *testing.T) { }, ), "test_configured_bool": tftypes.NewValue(tftypes.Bool, true), + "test_configured_float32": tftypes.NewValue(tftypes.Number, float64(float32Value)), "test_configured_float64": tftypes.NewValue(tftypes.Number, 1.2), "test_configured_int32": tftypes.NewValue(tftypes.Number, 123), "test_configured_int64": tftypes.NewValue(tftypes.Number, 123), @@ -2113,6 +2137,7 @@ func TestServerPlanResourceChange(t *testing.T) { PlannedState: &tfsdk.State{ Raw: tftypes.NewValue(testSchemaTypeDefault, map[string]tftypes.Value{ "test_computed_bool": tftypes.NewValue(tftypes.Bool, true), + "test_computed_float32": tftypes.NewValue(tftypes.Number, float64(computedFloat32Value)), "test_computed_float64": tftypes.NewValue(tftypes.Number, 1.2345), "test_computed_int32": tftypes.NewValue(tftypes.Number, 12345), "test_computed_int64": tftypes.NewValue(tftypes.Number, 12345), @@ -2271,6 +2296,7 @@ func TestServerPlanResourceChange(t *testing.T) { }, ), "test_configured_bool": tftypes.NewValue(tftypes.Bool, true), + "test_configured_float32": tftypes.NewValue(tftypes.Number, float64(float32Value)), "test_configured_float64": tftypes.NewValue(tftypes.Number, 1.2), "test_configured_int32": tftypes.NewValue(tftypes.Number, 123), "test_configured_int64": tftypes.NewValue(tftypes.Number, 123), @@ -3866,6 +3892,7 @@ func TestServerPlanResourceChange(t *testing.T) { Config: &tfsdk.Config{ Raw: tftypes.NewValue(testSchemaTypeDefault, map[string]tftypes.Value{ "test_computed_bool": tftypes.NewValue(tftypes.Bool, nil), + "test_computed_float32": tftypes.NewValue(tftypes.Number, nil), "test_computed_float64": tftypes.NewValue(tftypes.Number, nil), "test_computed_int32": tftypes.NewValue(tftypes.Number, nil), "test_computed_int64": tftypes.NewValue(tftypes.Number, nil), @@ -3989,6 +4016,7 @@ func TestServerPlanResourceChange(t *testing.T) { }, ), "test_configured_bool": tftypes.NewValue(tftypes.Bool, true), + "test_configured_float32": tftypes.NewValue(tftypes.Number, float64(float32Value)), "test_configured_float64": tftypes.NewValue(tftypes.Number, 1.2), "test_configured_int32": tftypes.NewValue(tftypes.Number, 123), "test_configured_int64": tftypes.NewValue(tftypes.Number, 123), @@ -4244,6 +4272,7 @@ func TestServerPlanResourceChange(t *testing.T) { ProposedNewState: &tfsdk.Plan{ Raw: tftypes.NewValue(testSchemaTypeDefault, map[string]tftypes.Value{ "test_computed_bool": tftypes.NewValue(tftypes.Bool, nil), + "test_computed_float32": tftypes.NewValue(tftypes.Number, nil), "test_computed_float64": tftypes.NewValue(tftypes.Number, nil), "test_computed_int32": tftypes.NewValue(tftypes.Number, nil), "test_computed_int64": tftypes.NewValue(tftypes.Number, nil), @@ -4367,6 +4396,7 @@ func TestServerPlanResourceChange(t *testing.T) { }, ), "test_configured_bool": tftypes.NewValue(tftypes.Bool, true), + "test_configured_float32": tftypes.NewValue(tftypes.Number, float64(float32Value)), "test_configured_float64": tftypes.NewValue(tftypes.Number, 1.2), "test_configured_int64": tftypes.NewValue(tftypes.Number, 123), "test_configured_int32": tftypes.NewValue(tftypes.Number, 123), @@ -4622,6 +4652,7 @@ func TestServerPlanResourceChange(t *testing.T) { PriorState: &tfsdk.State{ Raw: tftypes.NewValue(testSchemaTypeDefault, map[string]tftypes.Value{ "test_computed_bool": tftypes.NewValue(tftypes.Bool, false), + "test_computed_float32": tftypes.NewValue(tftypes.Number, float64(updatedFloat32Value)), "test_computed_float64": tftypes.NewValue(tftypes.Number, 5.4321), "test_computed_int32": tftypes.NewValue(tftypes.Number, 54321), "test_computed_int64": tftypes.NewValue(tftypes.Number, 54321), @@ -4780,6 +4811,7 @@ func TestServerPlanResourceChange(t *testing.T) { }, ), "test_configured_bool": tftypes.NewValue(tftypes.Bool, false), + "test_configured_float32": tftypes.NewValue(tftypes.Number, float64(configuredFloat32Value)), "test_configured_float64": tftypes.NewValue(tftypes.Number, 2.4), "test_configured_int32": tftypes.NewValue(tftypes.Number, 456), "test_configured_int64": tftypes.NewValue(tftypes.Number, 456), @@ -5053,6 +5085,7 @@ func TestServerPlanResourceChange(t *testing.T) { PlannedState: &tfsdk.State{ Raw: tftypes.NewValue(testSchemaTypeDefault, map[string]tftypes.Value{ "test_computed_bool": tftypes.NewValue(tftypes.Bool, true), + "test_computed_float32": tftypes.NewValue(tftypes.Number, float64(computedFloat32Value)), "test_computed_float64": tftypes.NewValue(tftypes.Number, 1.2345), "test_computed_int32": tftypes.NewValue(tftypes.Number, 12345), "test_computed_int64": tftypes.NewValue(tftypes.Number, 12345), @@ -5211,6 +5244,7 @@ func TestServerPlanResourceChange(t *testing.T) { }, ), "test_configured_bool": tftypes.NewValue(tftypes.Bool, true), + "test_configured_float32": tftypes.NewValue(tftypes.Number, float64(float32Value)), "test_configured_float64": tftypes.NewValue(tftypes.Number, 1.2), "test_configured_int32": tftypes.NewValue(tftypes.Number, 123), "test_configured_int64": tftypes.NewValue(tftypes.Number, 123), @@ -6672,6 +6706,290 @@ func TestServerPlanResourceChange_AttributeRoundtrip(t *testing.T) { }, ), }), + "create-float32-computed-null": generateTestCase(testCaseData{ + Schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "float32_attribute": schema.Float32Attribute{ + Computed: true, + }, + }, + }, + Config: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "float32_attribute": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "float32_attribute": tftypes.NewValue(tftypes.Number, nil), + }, + ), + ProposedNewState: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "float32_attribute": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "float32_attribute": tftypes.NewValue(tftypes.Number, nil), + }, + ), + PriorState: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "float32_attribute": tftypes.Number, + }, + }, + nil, + ), + PlannedState: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "float32_attribute": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "float32_attribute": tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), // Computed nulls as unknown + }, + ), + }), + "create-float32-computed-unknown": generateTestCase(testCaseData{ + Schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "float32_attribute": schema.Float32Attribute{ + Computed: true, + }, + }, + }, + Config: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "float32_attribute": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "float32_attribute": tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), + }, + ), + ProposedNewState: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "float32_attribute": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "float32_attribute": tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), + }, + ), + PriorState: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "float32_attribute": tftypes.Number, + }, + }, + nil, + ), + PlannedState: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "float32_attribute": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "float32_attribute": tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), + }, + ), + }), + "create-float32-optional-null": generateTestCase(testCaseData{ + Schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "float32_attribute": schema.Float32Attribute{ + Optional: true, + }, + }, + }, + Config: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "float32_attribute": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "float32_attribute": tftypes.NewValue(tftypes.Number, nil), + }, + ), + ProposedNewState: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "float32_attribute": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "float32_attribute": tftypes.NewValue(tftypes.Number, nil), + }, + ), + PriorState: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "float32_attribute": tftypes.Number, + }, + }, + nil, + ), + PlannedState: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "float32_attribute": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "float32_attribute": tftypes.NewValue(tftypes.Number, nil), + }, + ), + }), + "create-float32-optional-unknown": generateTestCase(testCaseData{ + Schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "float32_attribute": schema.Float32Attribute{ + Optional: true, + }, + }, + }, + Config: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "float32_attribute": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "float32_attribute": tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), + }, + ), + ProposedNewState: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "float32_attribute": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "float32_attribute": tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), + }, + ), + PriorState: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "float32_attribute": tftypes.Number, + }, + }, + nil, + ), + PlannedState: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "float32_attribute": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "float32_attribute": tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), + }, + ), + }), + "create-float32-optional-and-computed-null": generateTestCase(testCaseData{ + Schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "float32_attribute": schema.Float32Attribute{ + Optional: true, + Computed: true, + }, + }, + }, + Config: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "float32_attribute": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "float32_attribute": tftypes.NewValue(tftypes.Number, nil), + }, + ), + ProposedNewState: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "float32_attribute": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "float32_attribute": tftypes.NewValue(tftypes.Number, nil), + }, + ), + PriorState: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "float32_attribute": tftypes.Number, + }, + }, + nil, + ), + PlannedState: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "float32_attribute": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "float32_attribute": tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), // Computed nulls as unknown + }, + ), + }), + "create-float32-optional-and-computed-unknown": generateTestCase(testCaseData{ + Schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "float32_attribute": schema.Float32Attribute{ + Optional: true, + Computed: true, + }, + }, + }, + Config: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "float32_attribute": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "float32_attribute": tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), + }, + ), + ProposedNewState: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "float32_attribute": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "float32_attribute": tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), + }, + ), + PriorState: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "float32_attribute": tftypes.Number, + }, + }, + nil, + ), + PlannedState: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "float32_attribute": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "float32_attribute": tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), + }, + ), + }), "create-float64-computed-null": generateTestCase(testCaseData{ Schema: schema.Schema{ Attributes: map[string]schema.Attribute{ diff --git a/internal/fwtype/missing_underlying_type_validation_test.go b/internal/fwtype/missing_underlying_type_validation_test.go index dee78714..dea491b1 100644 --- a/internal/fwtype/missing_underlying_type_validation_test.go +++ b/internal/fwtype/missing_underlying_type_validation_test.go @@ -37,6 +37,10 @@ func TestContainsMissingUnderlyingType(t *testing.T) { attrTyp: testtypes.DynamicType{}, expected: false, }, + "custom-float32": { + attrTyp: testtypes.Float32Type{}, + expected: false, + }, "custom-float64": { attrTyp: testtypes.Float64Type{}, expected: false, @@ -92,6 +96,10 @@ func TestContainsMissingUnderlyingType(t *testing.T) { attrTyp: types.DynamicType, expected: false, }, + "float32": { + attrTyp: types.Float32Type, + expected: false, + }, "float64": { attrTyp: types.Float64Type, expected: false, @@ -126,6 +134,12 @@ func TestContainsMissingUnderlyingType(t *testing.T) { }, expected: false, }, + "list-custom-float32": { + attrTyp: types.ListType{ + ElemType: testtypes.Float32Type{}, + }, + expected: false, + }, "list-custom-float64": { attrTyp: types.ListType{ ElemType: testtypes.Float64Type{}, @@ -262,6 +276,12 @@ func TestContainsMissingUnderlyingType(t *testing.T) { }, expected: false, }, + "list-float32": { + attrTyp: types.ListType{ + ElemType: types.Float32Type, + }, + expected: false, + }, "list-float64": { attrTyp: types.ListType{ ElemType: types.Float64Type, @@ -411,6 +431,12 @@ func TestContainsMissingUnderlyingType(t *testing.T) { }, expected: false, }, + "map-custom-float32": { + attrTyp: types.MapType{ + ElemType: testtypes.Float32Type{}, + }, + expected: false, + }, "map-custom-float64": { attrTyp: types.MapType{ ElemType: testtypes.Float64Type{}, @@ -547,6 +573,12 @@ func TestContainsMissingUnderlyingType(t *testing.T) { }, expected: false, }, + "map-float32": { + attrTyp: types.MapType{ + ElemType: types.Float32Type, + }, + expected: false, + }, "map-float64": { attrTyp: types.MapType{ ElemType: types.Float64Type, @@ -2059,6 +2091,12 @@ func TestContainsMissingUnderlyingType(t *testing.T) { }, expected: false, }, + "set-custom-float32": { + attrTyp: types.SetType{ + ElemType: testtypes.Float32Type{}, + }, + expected: false, + }, "set-custom-float64": { attrTyp: types.SetType{ ElemType: testtypes.Float64Type{}, @@ -2195,6 +2233,12 @@ func TestContainsMissingUnderlyingType(t *testing.T) { }, expected: false, }, + "set-float32": { + attrTyp: types.SetType{ + ElemType: types.Float32Type, + }, + expected: false, + }, "set-float64": { attrTyp: types.SetType{ ElemType: types.Float64Type, @@ -2342,6 +2386,12 @@ func TestContainsMissingUnderlyingType(t *testing.T) { }, expected: false, }, + "tuple-float32": { + attrTyp: types.TupleType{ + ElemTypes: []attr.Type{types.Float32Type}, + }, + expected: false, + }, "tuple-float64": { attrTyp: types.TupleType{ ElemTypes: []attr.Type{types.Float64Type}, diff --git a/internal/reflect/interfaces_test.go b/internal/reflect/interfaces_test.go index efa24c8b..ae1022c5 100644 --- a/internal/reflect/interfaces_test.go +++ b/internal/reflect/interfaces_test.go @@ -624,6 +624,16 @@ func TestFromAttributeValue(t *testing.T) { diag.NewAttributeErrorDiagnostic(path.Root("test"), "Error Diagnostic", "This is an error."), }, }, + "Float32Type-Float32Value": { + typ: types.Float32Type, + val: types.Float32Null(), + expected: types.Float32Null(), + }, + "Float32Typable-Float32Value": { + typ: testtypes.Float32TypeWithSemanticEquals{}, + val: types.Float32Null(), + expected: types.Float32Null(), + }, "Float64Type-Float64Value": { typ: types.Float64Type, val: types.Float64Null(), diff --git a/internal/reflect/number_test.go b/internal/reflect/number_test.go index 228308e6..a028a68c 100644 --- a/internal/reflect/number_test.go +++ b/internal/reflect/number_test.go @@ -587,6 +587,17 @@ func TestNumber_uint64UnderflowError(t *testing.T) { func TestNumber_float32(t *testing.T) { t.Parallel() + + var n float32 + + result, diags := refl.Number(context.Background(), types.NumberType, tftypes.NewValue(tftypes.Number, 123), reflect.ValueOf(n), refl.Options{}, path.Empty()) + if diags.HasError() { + t.Errorf("Unexpected error: %v", diags) + } + reflect.ValueOf(&n).Elem().Set(result) + if n != 123 { + t.Errorf("Expected %v, got %v", 123, n) + } } func TestNumber_float32OverflowError(t *testing.T) { diff --git a/internal/testing/testdefaults/float32.go b/internal/testing/testdefaults/float32.go new file mode 100644 index 00000000..91a59a71 --- /dev/null +++ b/internal/testing/testdefaults/float32.go @@ -0,0 +1,49 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package testdefaults + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/resource/schema/defaults" +) + +var _ defaults.Float32 = Float32{} + +// Declarative defaults.Float32 for unit testing. +type Float32 struct { + // defaults.Describer interface methods + DescriptionMethod func(context.Context) string + MarkdownDescriptionMethod func(context.Context) string + + // defaults.Float32 interface methods + DefaultFloat32Method func(context.Context, defaults.Float32Request, *defaults.Float32Response) +} + +// Description satisfies the defaults.Describer interface. +func (v Float32) Description(ctx context.Context) string { + if v.DescriptionMethod == nil { + return "" + } + + return v.DescriptionMethod(ctx) +} + +// MarkdownDescription satisfies the defaults.Describer interface. +func (v Float32) MarkdownDescription(ctx context.Context) string { + if v.MarkdownDescriptionMethod == nil { + return "" + } + + return v.MarkdownDescriptionMethod(ctx) +} + +// DefaultFloat32 satisfies the defaults.Float32 interface. +func (v Float32) DefaultFloat32(ctx context.Context, req defaults.Float32Request, resp *defaults.Float32Response) { + if v.DefaultFloat32Method == nil { + return + } + + v.DefaultFloat32Method(ctx, req, resp) +} diff --git a/internal/testing/testplanmodifier/float32.go b/internal/testing/testplanmodifier/float32.go new file mode 100644 index 00000000..4314ffad --- /dev/null +++ b/internal/testing/testplanmodifier/float32.go @@ -0,0 +1,47 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package testplanmodifier + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" +) + +var _ planmodifier.Float32 = &Float32{} + +// Declarative planmodifier.Float32 for unit testing. +type Float32 struct { + // Float32 interface methods + DescriptionMethod func(context.Context) string + MarkdownDescriptionMethod func(context.Context) string + PlanModifyFloat32Method func(context.Context, planmodifier.Float32Request, *planmodifier.Float32Response) +} + +// Description satisfies the planmodifier.Float32 interface. +func (v Float32) Description(ctx context.Context) string { + if v.DescriptionMethod == nil { + return "" + } + + return v.DescriptionMethod(ctx) +} + +// MarkdownDescription satisfies the planmodifier.Float32 interface. +func (v Float32) MarkdownDescription(ctx context.Context) string { + if v.MarkdownDescriptionMethod == nil { + return "" + } + + return v.MarkdownDescriptionMethod(ctx) +} + +// PlanModify satisfies the planmodifier.Float32 interface. +func (v Float32) PlanModifyFloat32(ctx context.Context, req planmodifier.Float32Request, resp *planmodifier.Float32Response) { + if v.PlanModifyFloat32Method == nil { + return + } + + v.PlanModifyFloat32Method(ctx, req, resp) +} diff --git a/internal/testing/testschema/attributewithfloat32default.go b/internal/testing/testschema/attributewithfloat32default.go new file mode 100644 index 00000000..e1dabdd8 --- /dev/null +++ b/internal/testing/testschema/attributewithfloat32default.go @@ -0,0 +1,87 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package testschema + +import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/defaults" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +var _ fwschema.AttributeWithFloat32DefaultValue = AttributeWithFloat32DefaultValue{} + +type AttributeWithFloat32DefaultValue struct { + Computed bool + DeprecationMessage string + Description string + MarkdownDescription string + Optional bool + Required bool + Sensitive bool + Default defaults.Float32 +} + +// ApplyTerraform5AttributePathStep satisfies the fwschema.Attribute interface. +func (a AttributeWithFloat32DefaultValue) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (any, error) { + return a.GetType().ApplyTerraform5AttributePathStep(step) +} + +// Float32DefaultValue satisfies the fwxschema.AttributeWithFloat32DefaultValue interface. +func (a AttributeWithFloat32DefaultValue) Float32DefaultValue() defaults.Float32 { + return a.Default +} + +// Equal satisfies the fwschema.Attribute interface. +func (a AttributeWithFloat32DefaultValue) Equal(o fwschema.Attribute) bool { + _, ok := o.(AttributeWithFloat32DefaultValue) + + if !ok { + return false + } + + return fwschema.AttributesEqual(a, o) +} + +// GetDeprecationMessage satisfies the fwschema.Attribute interface. +func (a AttributeWithFloat32DefaultValue) GetDeprecationMessage() string { + return a.DeprecationMessage +} + +// GetDescription satisfies the fwschema.Attribute interface. +func (a AttributeWithFloat32DefaultValue) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription satisfies the fwschema.Attribute interface. +func (a AttributeWithFloat32DefaultValue) GetMarkdownDescription() string { + return a.MarkdownDescription +} + +// GetType satisfies the fwschema.Attribute interface. +func (a AttributeWithFloat32DefaultValue) GetType() attr.Type { + return types.Float32Type +} + +// IsComputed satisfies the fwschema.Attribute interface. +func (a AttributeWithFloat32DefaultValue) IsComputed() bool { + return a.Computed +} + +// IsOptional satisfies the fwschema.Attribute interface. +func (a AttributeWithFloat32DefaultValue) IsOptional() bool { + return a.Optional +} + +// IsRequired satisfies the fwschema.Attribute interface. +func (a AttributeWithFloat32DefaultValue) IsRequired() bool { + return a.Required +} + +// IsSensitive satisfies the fwschema.Attribute interface. +func (a AttributeWithFloat32DefaultValue) IsSensitive() bool { + return a.Sensitive +} diff --git a/internal/testing/testschema/attributewithfloat32planmodifiers.go b/internal/testing/testschema/attributewithfloat32planmodifiers.go new file mode 100644 index 00000000..b3ef10eb --- /dev/null +++ b/internal/testing/testschema/attributewithfloat32planmodifiers.go @@ -0,0 +1,88 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package testschema + +import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +var _ fwxschema.AttributeWithFloat32PlanModifiers = AttributeWithFloat32PlanModifiers{} + +type AttributeWithFloat32PlanModifiers struct { + Computed bool + DeprecationMessage string + Description string + MarkdownDescription string + Optional bool + Required bool + Sensitive bool + PlanModifiers []planmodifier.Float32 +} + +// ApplyTerraform5AttributePathStep satisfies the fwschema.Attribute interface. +func (a AttributeWithFloat32PlanModifiers) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (any, error) { + return a.GetType().ApplyTerraform5AttributePathStep(step) +} + +// Equal satisfies the fwschema.Attribute interface. +func (a AttributeWithFloat32PlanModifiers) Equal(o fwschema.Attribute) bool { + _, ok := o.(AttributeWithFloat32PlanModifiers) + + if !ok { + return false + } + + return fwschema.AttributesEqual(a, o) +} + +// Float32PlanModifiers satisfies the fwxschema.AttributeWithFloat32PlanModifiers interface. +func (a AttributeWithFloat32PlanModifiers) Float32PlanModifiers() []planmodifier.Float32 { + return a.PlanModifiers +} + +// GetDeprecationMessage satisfies the fwschema.Attribute interface. +func (a AttributeWithFloat32PlanModifiers) GetDeprecationMessage() string { + return a.DeprecationMessage +} + +// GetDescription satisfies the fwschema.Attribute interface. +func (a AttributeWithFloat32PlanModifiers) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription satisfies the fwschema.Attribute interface. +func (a AttributeWithFloat32PlanModifiers) GetMarkdownDescription() string { + return a.MarkdownDescription +} + +// GetType satisfies the fwschema.Attribute interface. +func (a AttributeWithFloat32PlanModifiers) GetType() attr.Type { + return types.Float32Type +} + +// IsComputed satisfies the fwschema.Attribute interface. +func (a AttributeWithFloat32PlanModifiers) IsComputed() bool { + return a.Computed +} + +// IsOptional satisfies the fwschema.Attribute interface. +func (a AttributeWithFloat32PlanModifiers) IsOptional() bool { + return a.Optional +} + +// IsRequired satisfies the fwschema.Attribute interface. +func (a AttributeWithFloat32PlanModifiers) IsRequired() bool { + return a.Required +} + +// IsSensitive satisfies the fwschema.Attribute interface. +func (a AttributeWithFloat32PlanModifiers) IsSensitive() bool { + return a.Sensitive +} diff --git a/internal/testing/testschema/attributewithfloat32validators.go b/internal/testing/testschema/attributewithfloat32validators.go new file mode 100644 index 00000000..d9d38f9e --- /dev/null +++ b/internal/testing/testschema/attributewithfloat32validators.go @@ -0,0 +1,88 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package testschema + +import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +var _ fwxschema.AttributeWithFloat32Validators = AttributeWithFloat32Validators{} + +type AttributeWithFloat32Validators struct { + Computed bool + DeprecationMessage string + Description string + MarkdownDescription string + Optional bool + Required bool + Sensitive bool + Validators []validator.Float32 +} + +// ApplyTerraform5AttributePathStep satisfies the fwschema.Attribute interface. +func (a AttributeWithFloat32Validators) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (any, error) { + return a.GetType().ApplyTerraform5AttributePathStep(step) +} + +// Equal satisfies the fwschema.Attribute interface. +func (a AttributeWithFloat32Validators) Equal(o fwschema.Attribute) bool { + _, ok := o.(AttributeWithFloat32Validators) + + if !ok { + return false + } + + return fwschema.AttributesEqual(a, o) +} + +// Float32Validators satisfies the fwxschema.AttributeWithFloat32Validators interface. +func (a AttributeWithFloat32Validators) Float32Validators() []validator.Float32 { + return a.Validators +} + +// GetDeprecationMessage satisfies the fwschema.Attribute interface. +func (a AttributeWithFloat32Validators) GetDeprecationMessage() string { + return a.DeprecationMessage +} + +// GetDescription satisfies the fwschema.Attribute interface. +func (a AttributeWithFloat32Validators) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription satisfies the fwschema.Attribute interface. +func (a AttributeWithFloat32Validators) GetMarkdownDescription() string { + return a.MarkdownDescription +} + +// GetType satisfies the fwschema.Attribute interface. +func (a AttributeWithFloat32Validators) GetType() attr.Type { + return types.Float32Type +} + +// IsComputed satisfies the fwschema.Attribute interface. +func (a AttributeWithFloat32Validators) IsComputed() bool { + return a.Computed +} + +// IsOptional satisfies the fwschema.Attribute interface. +func (a AttributeWithFloat32Validators) IsOptional() bool { + return a.Optional +} + +// IsRequired satisfies the fwschema.Attribute interface. +func (a AttributeWithFloat32Validators) IsRequired() bool { + return a.Required +} + +// IsSensitive satisfies the fwschema.Attribute interface. +func (a AttributeWithFloat32Validators) IsSensitive() bool { + return a.Sensitive +} diff --git a/internal/testing/testtypes/float32.go b/internal/testing/testtypes/float32.go new file mode 100644 index 00000000..27ec1c10 --- /dev/null +++ b/internal/testing/testtypes/float32.go @@ -0,0 +1,42 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package testtypes + +import ( + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +var ( + _ basetypes.Float32Typable = Float32Type{} + _ basetypes.Float32Valuable = Float32Value{} +) + +type Float32Type struct { + basetypes.Float32Type +} + +func (t Float32Type) Equal(o attr.Type) bool { + other, ok := o.(Float32Type) + + if !ok { + return false + } + + return t.Float32Type.Equal(other.Float32Type) +} + +type Float32Value struct { + basetypes.Float32Value +} + +func (v Float32Value) Equal(o attr.Value) bool { + other, ok := o.(Float32Value) + + if !ok { + return false + } + + return v.Float32Value.Equal(other.Float32Value) +} diff --git a/internal/testing/testtypes/float32withsemanticequals.go b/internal/testing/testtypes/float32withsemanticequals.go new file mode 100644 index 00000000..99a40775 --- /dev/null +++ b/internal/testing/testtypes/float32withsemanticequals.go @@ -0,0 +1,117 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package testtypes + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +var ( + _ basetypes.Float32Typable = Float32TypeWithSemanticEquals{} + _ basetypes.Float32ValuableWithSemanticEquals = Float32ValueWithSemanticEquals{} +) + +// Float32TypeWithSemanticEquals is a Float32Type associated with +// Float32ValueWithSemanticEquals, which implements semantic equality logic that +// returns the SemanticEquals boolean for testing. +type Float32TypeWithSemanticEquals struct { + basetypes.Float32Type + + SemanticEquals bool + SemanticEqualsDiagnostics diag.Diagnostics +} + +func (t Float32TypeWithSemanticEquals) Equal(o attr.Type) bool { + other, ok := o.(Float32TypeWithSemanticEquals) + + if !ok { + return false + } + + if t.SemanticEquals != other.SemanticEquals { + return false + } + + return t.Float32Type.Equal(other.Float32Type) +} + +func (t Float32TypeWithSemanticEquals) String() string { + return fmt.Sprintf("Float32TypeWithSemanticEquals(%t)", t.SemanticEquals) +} + +func (t Float32TypeWithSemanticEquals) ValueFromFloat32(ctx context.Context, in basetypes.Float32Value) (basetypes.Float32Valuable, diag.Diagnostics) { + var diags diag.Diagnostics + + value := Float32ValueWithSemanticEquals{ + Float32Value: in, + SemanticEquals: t.SemanticEquals, + SemanticEqualsDiagnostics: t.SemanticEqualsDiagnostics, + } + + return value, diags +} + +func (t Float32TypeWithSemanticEquals) ValueFromTerraform(ctx context.Context, in tftypes.Value) (attr.Value, error) { + attrValue, err := t.Float32Type.ValueFromTerraform(ctx, in) + + if err != nil { + return nil, err + } + + stringValue, ok := attrValue.(basetypes.Float32Value) + + if !ok { + return nil, fmt.Errorf("unexpected value type of %T", attrValue) + } + + stringValuable, diags := t.ValueFromFloat32(ctx, stringValue) + + if diags.HasError() { + return nil, fmt.Errorf("unexpected error converting Float32Value to Float32Valuable: %v", diags) + } + + return stringValuable, nil +} + +func (t Float32TypeWithSemanticEquals) ValueType(ctx context.Context) attr.Value { + return Float32ValueWithSemanticEquals{ + SemanticEquals: t.SemanticEquals, + SemanticEqualsDiagnostics: t.SemanticEqualsDiagnostics, + } +} + +type Float32ValueWithSemanticEquals struct { + basetypes.Float32Value + + SemanticEquals bool + SemanticEqualsDiagnostics diag.Diagnostics +} + +func (v Float32ValueWithSemanticEquals) Equal(o attr.Value) bool { + other, ok := o.(Float32ValueWithSemanticEquals) + + if !ok { + return false + } + + return v.Float32Value.Equal(other.Float32Value) +} + +func (v Float32ValueWithSemanticEquals) Float32SemanticEquals(ctx context.Context, otherV basetypes.Float32Valuable) (bool, diag.Diagnostics) { + return v.SemanticEquals, v.SemanticEqualsDiagnostics +} + +func (v Float32ValueWithSemanticEquals) Type(ctx context.Context) attr.Type { + return Float32TypeWithSemanticEquals{ + SemanticEquals: v.SemanticEquals, + SemanticEqualsDiagnostics: v.SemanticEqualsDiagnostics, + } +} diff --git a/internal/testing/testvalidator/float32.go b/internal/testing/testvalidator/float32.go new file mode 100644 index 00000000..b41cad9c --- /dev/null +++ b/internal/testing/testvalidator/float32.go @@ -0,0 +1,61 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package testvalidator + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/function" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" +) + +var ( + _ validator.Float32 = &Float32{} + _ function.Float32ParameterValidator = &Float32{} +) + +// Declarative validator.Float32 for unit testing. +type Float32 struct { + // Float32 interface methods + DescriptionMethod func(context.Context) string + MarkdownDescriptionMethod func(context.Context) string + ValidateFloat32Method func(context.Context, validator.Float32Request, *validator.Float32Response) + ValidateMethod func(context.Context, function.Float32ParameterValidatorRequest, *function.Float32ParameterValidatorResponse) +} + +// Description satisfies the validator.Float32 interface. +func (v Float32) Description(ctx context.Context) string { + if v.DescriptionMethod == nil { + return "" + } + + return v.DescriptionMethod(ctx) +} + +// MarkdownDescription satisfies the validator.Float32 interface. +func (v Float32) MarkdownDescription(ctx context.Context) string { + if v.MarkdownDescriptionMethod == nil { + return "" + } + + return v.MarkdownDescriptionMethod(ctx) +} + +// ValidateFloat32 satisfies the validator.Float32 interface. +func (v Float32) ValidateFloat32(ctx context.Context, req validator.Float32Request, resp *validator.Float32Response) { + if v.ValidateFloat32Method == nil { + return + } + + v.ValidateFloat32Method(ctx, req, resp) +} + +// ValidateParameterFloat32 satisfies the function.Float32ParameterValidator interface. +func (v Float32) ValidateParameterFloat32(ctx context.Context, req function.Float32ParameterValidatorRequest, resp *function.Float32ParameterValidatorResponse) { + if v.ValidateMethod == nil { + return + } + + v.ValidateMethod(ctx, req, resp) +} diff --git a/internal/toproto5/function_test.go b/internal/toproto5/function_test.go index 94caaa6b..8f941c80 100644 --- a/internal/toproto5/function_test.go +++ b/internal/toproto5/function_test.go @@ -343,6 +343,15 @@ func TestFunctionParameter(t *testing.T) { Type: tftypes.Bool, }, }, + "type-float32": { + fw: function.Float32Parameter{ + Name: "float32", + }, + expected: &tfprotov5.FunctionParameter{ + Name: "float32", + Type: tftypes.Number, + }, + }, "type-float64": { fw: function.Float64Parameter{ Name: "float64", diff --git a/internal/toproto5/getproviderschema_test.go b/internal/toproto5/getproviderschema_test.go index 0fb3acfe..1683b0e7 100644 --- a/internal/toproto5/getproviderschema_test.go +++ b/internal/toproto5/getproviderschema_test.go @@ -300,6 +300,36 @@ func TestGetProviderSchemaResponse(t *testing.T) { ResourceSchemas: map[string]*tfprotov5.Schema{}, }, }, + "data-source-attribute-type-float32": { + input: &fwserver.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]fwschema.Schema{ + "test_data_source": datasourceschema.Schema{ + Attributes: map[string]datasourceschema.Attribute{ + "test_attribute": datasourceschema.Float32Attribute{ + Required: true, + }, + }, + }, + }, + }, + expected: &tfprotov5.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov5.Schema{ + "test_data_source": { + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "test_attribute", + Required: true, + Type: tftypes.Number, + }, + }, + }, + }, + }, + Functions: map[string]*tfprotov5.Function{}, + ResourceSchemas: map[string]*tfprotov5.Schema{}, + }, + }, "data-source-attribute-type-float64": { input: &fwserver.GetProviderSchemaResponse{ DataSourceSchemas: map[string]fwschema.Schema{ @@ -1356,6 +1386,33 @@ func TestGetProviderSchemaResponse(t *testing.T) { ResourceSchemas: map[string]*tfprotov5.Schema{}, }, }, + "provider-attribute-type-float32": { + input: &fwserver.GetProviderSchemaResponse{ + Provider: providerschema.Schema{ + Attributes: map[string]providerschema.Attribute{ + "test_attribute": providerschema.Float32Attribute{ + Required: true, + }, + }, + }, + }, + expected: &tfprotov5.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, + Provider: &tfprotov5.Schema{ + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "test_attribute", + Required: true, + Type: tftypes.Number, + }, + }, + }, + }, + ResourceSchemas: map[string]*tfprotov5.Schema{}, + }, + }, "provider-attribute-type-float64": { input: &fwserver.GetProviderSchemaResponse{ Provider: providerschema.Schema{ @@ -2864,6 +2921,36 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, + "resource-attribute-type-float32": { + input: &fwserver.GetProviderSchemaResponse{ + ResourceSchemas: map[string]fwschema.Schema{ + "test_resource": resourceschema.Schema{ + Attributes: map[string]resourceschema.Attribute{ + "test_attribute": resourceschema.Float32Attribute{ + Required: true, + }, + }, + }, + }, + }, + expected: &tfprotov5.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, + ResourceSchemas: map[string]*tfprotov5.Schema{ + "test_resource": { + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "test_attribute", + Required: true, + Type: tftypes.Number, + }, + }, + }, + }, + }, + }, + }, "resource-attribute-type-float64": { input: &fwserver.GetProviderSchemaResponse{ ResourceSchemas: map[string]fwschema.Schema{ diff --git a/provider/schema/float32_attribute.go b/provider/schema/float32_attribute.go new file mode 100644 index 00000000..a36c5c43 --- /dev/null +++ b/provider/schema/float32_attribute.go @@ -0,0 +1,183 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema + +import ( + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +// Ensure the implementation satisifies the desired interfaces. +var ( + _ Attribute = Float32Attribute{} + _ fwxschema.AttributeWithFloat32Validators = Float32Attribute{} +) + +// Float32Attribute represents a schema attribute that is a 32-bit floating +// point number. When retrieving the value for this attribute, use +// types.Float32 as the value type unless the CustomType field is set. +// +// Use Int32Attribute for 32-bit integer attributes or NumberAttribute for +// 512-bit generic number attributes. +// +// Terraform configurations configure this attribute using expressions that +// return a number or directly via a floating point value. +// +// example_attribute = 123.45 +// +// Terraform configurations reference this attribute using the attribute name. +// +// .example_attribute +type Float32Attribute struct { + // CustomType enables the use of a custom attribute type in place of the + // default basetypes.Float32Type. When retrieving data, the basetypes.Float32Valuable + // associated with this custom type must be used in place of types.Float32. + CustomType basetypes.Float32Typable + + // Required indicates whether the practitioner must enter a value for + // this attribute or not. Required and Optional cannot both be true, + // and Required and Computed cannot both be true. + Required bool + + // Optional indicates whether the practitioner can choose to enter a value + // for this attribute or not. Optional and Required cannot both be true. + Optional bool + + // Sensitive indicates whether the value of this attribute should be + // considered sensitive data. Setting it to true will obscure the value + // in CLI output. Sensitive does not impact how values are stored, and + // practitioners are encouraged to store their state as if the entire + // file is sensitive. + Sensitive bool + + // Description is used in various tooling, like the language server, to + // give practitioners more information about what this attribute is, + // what it's for, and how it should be used. It should be written as + // plain text, with no special formatting. + Description string + + // MarkdownDescription is used in various tooling, like the + // documentation generator, to give practitioners more information + // about what this attribute is, what it's for, and how it should be + // used. It should be formatted using Markdown. + MarkdownDescription string + + // DeprecationMessage defines warning diagnostic details to display when + // practitioner configurations use this Attribute. The warning diagnostic + // summary is automatically set to "Attribute Deprecated" along with + // configuration source file and line information. + // + // Set this field to a practitioner actionable message such as: + // + // - "Configure other_attribute instead. This attribute will be removed + // in the next major version of the provider." + // - "Remove this attribute's configuration as it no longer is used and + // the attribute will be removed in the next major version of the + // provider." + // + // In Terraform 1.2.7 and later, this warning diagnostic is displayed any + // time a practitioner attempts to configure a value for this attribute and + // certain scenarios where this attribute is referenced. + // + // In Terraform 1.2.6 and earlier, this warning diagnostic is only + // displayed when the Attribute is Required or Optional, and if the + // practitioner configuration sets the value to a known or unknown value + // (which may eventually be null). It has no effect when the Attribute is + // Computed-only (read-only; not Required or Optional). + // + // Across any Terraform version, there are no warnings raised for + // practitioner configuration values set directly to null, as there is no + // way for the framework to differentiate between an unset and null + // configuration due to how Terraform sends configuration information + // across the protocol. + // + // Additional information about deprecation enhancements for read-only + // attributes can be found in: + // + // - https://github.com/hashicorp/terraform/issues/7569 + // + DeprecationMessage string + + // Validators define value validation functionality for the attribute. All + // elements of the slice of AttributeValidator are run, regardless of any + // previous error diagnostics. + // + // Many common use case validators can be found in the + // github.com/hashicorp/terraform-plugin-framework-validators Go module. + // + // If the Type field points to a custom type that implements the + // xattr.TypeWithValidate interface, the validators defined in this field + // are run in addition to the validation defined by the type. + Validators []validator.Float32 +} + +// ApplyTerraform5AttributePathStep always returns an error as it is not +// possible to step further into a Float32Attribute. +func (a Float32Attribute) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { + return a.GetType().ApplyTerraform5AttributePathStep(step) +} + +// Equal returns true if the given Attribute is a Float32Attribute +// and all fields are equal. +func (a Float32Attribute) Equal(o fwschema.Attribute) bool { + if _, ok := o.(Float32Attribute); !ok { + return false + } + + return fwschema.AttributesEqual(a, o) +} + +// Float32Validators returns the Validators field value. +func (a Float32Attribute) Float32Validators() []validator.Float32 { + return a.Validators +} + +// GetDeprecationMessage returns the DeprecationMessage field value. +func (a Float32Attribute) GetDeprecationMessage() string { + return a.DeprecationMessage +} + +// GetDescription returns the Description field value. +func (a Float32Attribute) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription returns the MarkdownDescription field value. +func (a Float32Attribute) GetMarkdownDescription() string { + return a.MarkdownDescription +} + +// GetType returns types.Float32Type or the CustomType field value if defined. +func (a Float32Attribute) GetType() attr.Type { + if a.CustomType != nil { + return a.CustomType + } + + return types.Float32Type +} + +// IsComputed always returns false as provider schemas cannot be Computed. +func (a Float32Attribute) IsComputed() bool { + return false +} + +// IsOptional returns the Optional field value. +func (a Float32Attribute) IsOptional() bool { + return a.Optional +} + +// IsRequired returns the Required field value. +func (a Float32Attribute) IsRequired() bool { + return a.Required +} + +// IsSensitive returns the Sensitive field value. +func (a Float32Attribute) IsSensitive() bool { + return a.Sensitive +} diff --git a/provider/schema/float32_attribute_test.go b/provider/schema/float32_attribute_test.go new file mode 100644 index 00000000..c792d326 --- /dev/null +++ b/provider/schema/float32_attribute_test.go @@ -0,0 +1,418 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema_test + +import ( + "fmt" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" + "github.com/hashicorp/terraform-plugin-framework/provider/schema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestFloat32AttributeApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float32Attribute + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + attribute: schema.Float32Attribute{}, + step: tftypes.AttributeName("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.AttributeName to basetypes.Float32Type"), + }, + "ElementKeyInt": { + attribute: schema.Float32Attribute{}, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyInt to basetypes.Float32Type"), + }, + "ElementKeyString": { + attribute: schema.Float32Attribute{}, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyString to basetypes.Float32Type"), + }, + "ElementKeyValue": { + attribute: schema.Float32Attribute{}, + step: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyValue to basetypes.Float32Type"), + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.attribute.ApplyTerraform5AttributePathStep(testCase.step) + + if err != nil { + if testCase.expectedError == nil { + t.Fatalf("expected no error, got: %s", err) + } + + if !strings.Contains(err.Error(), testCase.expectedError.Error()) { + t.Fatalf("expected error %q, got: %s", testCase.expectedError, err) + } + } + + if err == nil && testCase.expectedError != nil { + t.Fatalf("got no error, expected: %s", testCase.expectedError) + } + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat32AttributeFloat32Validators(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float32Attribute + expected []validator.Float32 + }{ + "no-validators": { + attribute: schema.Float32Attribute{}, + expected: nil, + }, + "validators": { + attribute: schema.Float32Attribute{ + Validators: []validator.Float32{}, + }, + expected: []validator.Float32{}, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.Float32Validators() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat32AttributeGetDeprecationMessage(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float32Attribute + expected string + }{ + "no-deprecation-message": { + attribute: schema.Float32Attribute{}, + expected: "", + }, + "deprecation-message": { + attribute: schema.Float32Attribute{ + DeprecationMessage: "test deprecation message", + }, + expected: "test deprecation message", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDeprecationMessage() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat32AttributeEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float32Attribute + other fwschema.Attribute + expected bool + }{ + "different-type": { + attribute: schema.Float32Attribute{}, + other: testschema.AttributeWithFloat32Validators{}, + expected: false, + }, + "equal": { + attribute: schema.Float32Attribute{}, + other: schema.Float32Attribute{}, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.Equal(testCase.other) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat32AttributeGetDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float32Attribute + expected string + }{ + "no-description": { + attribute: schema.Float32Attribute{}, + expected: "", + }, + "description": { + attribute: schema.Float32Attribute{ + Description: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat32AttributeGetMarkdownDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float32Attribute + expected string + }{ + "no-markdown-description": { + attribute: schema.Float32Attribute{}, + expected: "", + }, + "markdown-description": { + attribute: schema.Float32Attribute{ + MarkdownDescription: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetMarkdownDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat32AttributeGetType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float32Attribute + expected attr.Type + }{ + "base": { + attribute: schema.Float32Attribute{}, + expected: types.Float32Type, + }, + // "custom-type": { + // attribute: schema.Float32Attribute{ + // CustomType: testtypes.Float32Type{}, + // }, + // expected: testtypes.Float32Type{}, + // }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetType() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat32AttributeIsComputed(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float32Attribute + expected bool + }{ + "not-computed": { + attribute: schema.Float32Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsComputed() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat32AttributeIsOptional(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float32Attribute + expected bool + }{ + "not-optional": { + attribute: schema.Float32Attribute{}, + expected: false, + }, + "optional": { + attribute: schema.Float32Attribute{ + Optional: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptional() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat32AttributeIsRequired(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float32Attribute + expected bool + }{ + "not-required": { + attribute: schema.Float32Attribute{}, + expected: false, + }, + "required": { + attribute: schema.Float32Attribute{ + Required: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequired() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat32AttributeIsSensitive(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float32Attribute + expected bool + }{ + "not-sensitive": { + attribute: schema.Float32Attribute{}, + expected: false, + }, + "sensitive": { + attribute: schema.Float32Attribute{ + Sensitive: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsSensitive() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/resource/schema/defaults/float32.go b/resource/schema/defaults/float32.go new file mode 100644 index 00000000..c54645d5 --- /dev/null +++ b/resource/schema/defaults/float32.go @@ -0,0 +1,36 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package defaults + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Float32 is a schema default value for types.Float32 attributes. +type Float32 interface { + Describer + + // DefaultFloat32 should set the default value. + DefaultFloat32(context.Context, Float32Request, *Float32Response) +} + +type Float32Request struct { + // Path contains the path of the attribute for setting the + // default value. Use this path for any response diagnostics. + Path path.Path +} + +type Float32Response struct { + // Diagnostics report errors or warnings related to setting the + // default value resource configuration. An empty slice + // indicates success, with no warnings or errors generated. + Diagnostics diag.Diagnostics + + // PlanValue is the planned new state for the attribute. + PlanValue types.Float32 +} diff --git a/resource/schema/float32_attribute.go b/resource/schema/float32_attribute.go new file mode 100644 index 00000000..9e8e7a22 --- /dev/null +++ b/resource/schema/float32_attribute.go @@ -0,0 +1,243 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/defaults" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +// Ensure the implementation satisfies the desired interfaces. +var ( + _ Attribute = Float32Attribute{} + _ fwschema.AttributeWithValidateImplementation = Float32Attribute{} + _ fwschema.AttributeWithFloat32DefaultValue = Float32Attribute{} + _ fwxschema.AttributeWithFloat32PlanModifiers = Float32Attribute{} + _ fwxschema.AttributeWithFloat32Validators = Float32Attribute{} +) + +// Float32Attribute represents a schema attribute that is a 32-bit floating +// point number. When retrieving the value for this attribute, use +// types.Float32 as the value type unless the CustomType field is set. +// +// Use Int32Attribute for 32-bit integer attributes or NumberAttribute for +// 512-bit generic number attributes. +// +// Terraform configurations configure this attribute using expressions that +// return a number or directly via a floating point value. +// +// example_attribute = 123.45 +// +// Terraform configurations reference this attribute using the attribute name. +// +// .example_attribute +type Float32Attribute struct { + // CustomType enables the use of a custom attribute type in place of the + // default basetypes.Float32Type. When retrieving data, the basetypes.Float32Valuable + // associated with this custom type must be used in place of types.Float32. + CustomType basetypes.Float32Typable + + // Required indicates whether the practitioner must enter a value for + // this attribute or not. Required and Optional cannot both be true, + // and Required and Computed cannot both be true. + Required bool + + // Optional indicates whether the practitioner can choose to enter a value + // for this attribute or not. Optional and Required cannot both be true. + Optional bool + + // Computed indicates whether the provider may return its own value for + // this Attribute or not. Required and Computed cannot both be true. If + // Required and Optional are both false, Computed must be true, and the + // attribute will be considered "read only" for the practitioner, with + // only the provider able to set its value. + Computed bool + + // Sensitive indicates whether the value of this attribute should be + // considered sensitive data. Setting it to true will obscure the value + // in CLI output. Sensitive does not impact how values are stored, and + // practitioners are encouraged to store their state as if the entire + // file is sensitive. + Sensitive bool + + // Description is used in various tooling, like the language server, to + // give practitioners more information about what this attribute is, + // what it's for, and how it should be used. It should be written as + // plain text, with no special formatting. + Description string + + // MarkdownDescription is used in various tooling, like the + // documentation generator, to give practitioners more information + // about what this attribute is, what it's for, and how it should be + // used. It should be formatted using Markdown. + MarkdownDescription string + + // DeprecationMessage defines warning diagnostic details to display when + // practitioner configurations use this Attribute. The warning diagnostic + // summary is automatically set to "Attribute Deprecated" along with + // configuration source file and line information. + // + // Set this field to a practitioner actionable message such as: + // + // - "Configure other_attribute instead. This attribute will be removed + // in the next major version of the provider." + // - "Remove this attribute's configuration as it no longer is used and + // the attribute will be removed in the next major version of the + // provider." + // + // In Terraform 1.2.7 and later, this warning diagnostic is displayed any + // time a practitioner attempts to configure a value for this attribute and + // certain scenarios where this attribute is referenced. + // + // In Terraform 1.2.6 and earlier, this warning diagnostic is only + // displayed when the Attribute is Required or Optional, and if the + // practitioner configuration sets the value to a known or unknown value + // (which may eventually be null). It has no effect when the Attribute is + // Computed-only (read-only; not Required or Optional). + // + // Across any Terraform version, there are no warnings raised for + // practitioner configuration values set directly to null, as there is no + // way for the framework to differentiate between an unset and null + // configuration due to how Terraform sends configuration information + // across the protocol. + // + // Additional information about deprecation enhancements for read-only + // attributes can be found in: + // + // - https://github.com/hashicorp/terraform/issues/7569 + // + DeprecationMessage string + + // Validators define value validation functionality for the attribute. All + // elements of the slice of AttributeValidator are run, regardless of any + // previous error diagnostics. + // + // Many common use case validators can be found in the + // github.com/hashicorp/terraform-plugin-framework-validators Go module. + // + // If the Type field points to a custom type that implements the + // xattr.TypeWithValidate interface, the validators defined in this field + // are run in addition to the validation defined by the type. + Validators []validator.Float32 + + // PlanModifiers defines a sequence of modifiers for this attribute at + // plan time. Schema-based plan modifications occur before any + // resource-level plan modifications. + // + // Schema-based plan modifications can adjust Terraform's plan by: + // + // - Requiring resource recreation. Typically used for configuration + // updates which cannot be done in-place. + // - Setting the planned value. Typically used for enhancing the plan + // to replace unknown values. Computed must be true or Terraform will + // return an error. If the plan value is known due to a known + // configuration value, the plan value cannot be changed or Terraform + // will return an error. + // + // Any errors will prevent further execution of this sequence or modifiers. + PlanModifiers []planmodifier.Float32 + + // Default defines a proposed new state (plan) value for the attribute + // if the configuration value is null. Default prevents the framework + // from automatically marking the value as unknown during planning when + // other proposed new state changes are detected. If the attribute is + // computed and the value could be altered by other changes then a default + // should be avoided and a plan modifier should be used instead. + Default defaults.Float32 +} + +// ApplyTerraform5AttributePathStep always returns an error as it is not +// possible to step further into a Float32Attribute. +func (a Float32Attribute) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { + return a.GetType().ApplyTerraform5AttributePathStep(step) +} + +// Equal returns true if the given Attribute is a Float32Attribute +// and all fields are equal. +func (a Float32Attribute) Equal(o fwschema.Attribute) bool { + if _, ok := o.(Float32Attribute); !ok { + return false + } + + return fwschema.AttributesEqual(a, o) +} + +// Float32DefaultValue returns the Default field value. +func (a Float32Attribute) Float32DefaultValue() defaults.Float32 { + return a.Default +} + +// Float32PlanModifiers returns the PlanModifiers field value. +func (a Float32Attribute) Float32PlanModifiers() []planmodifier.Float32 { + return a.PlanModifiers +} + +// Float32Validators returns the Validators field value. +func (a Float32Attribute) Float32Validators() []validator.Float32 { + return a.Validators +} + +// GetDeprecationMessage returns the DeprecationMessage field value. +func (a Float32Attribute) GetDeprecationMessage() string { + return a.DeprecationMessage +} + +// GetDescription returns the Description field value. +func (a Float32Attribute) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription returns the MarkdownDescription field value. +func (a Float32Attribute) GetMarkdownDescription() string { + return a.MarkdownDescription +} + +// GetType returns types.Float32Type or the CustomType field value if defined. +func (a Float32Attribute) GetType() attr.Type { + if a.CustomType != nil { + return a.CustomType + } + + return types.Float32Type +} + +// IsComputed returns the Computed field value. +func (a Float32Attribute) IsComputed() bool { + return a.Computed +} + +// IsOptional returns the Optional field value. +func (a Float32Attribute) IsOptional() bool { + return a.Optional +} + +// IsRequired returns the Required field value. +func (a Float32Attribute) IsRequired() bool { + return a.Required +} + +// IsSensitive returns the Sensitive field value. +func (a Float32Attribute) IsSensitive() bool { + return a.Sensitive +} + +// ValidateImplementation contains logic for validating the +// provider-defined implementation of the attribute to prevent unexpected +// errors or panics. This logic runs during the GetProviderSchema RPC and +// should never include false positives. +func (a Float32Attribute) ValidateImplementation(ctx context.Context, req fwschema.ValidateImplementationRequest, resp *fwschema.ValidateImplementationResponse) { + if !a.IsComputed() && a.Float32DefaultValue() != nil { + resp.Diagnostics.Append(nonComputedAttributeWithDefaultDiag(req.Path)) + } +} diff --git a/resource/schema/float32_attribute_test.go b/resource/schema/float32_attribute_test.go new file mode 100644 index 00000000..0b211351 --- /dev/null +++ b/resource/schema/float32_attribute_test.go @@ -0,0 +1,577 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema_test + +import ( + "context" + "fmt" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/defaults" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/float32default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func TestFloat32AttributeApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float32Attribute + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + attribute: schema.Float32Attribute{}, + step: tftypes.AttributeName("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.AttributeName to basetypes.Float32Type"), + }, + "ElementKeyInt": { + attribute: schema.Float32Attribute{}, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyInt to basetypes.Float32Type"), + }, + "ElementKeyString": { + attribute: schema.Float32Attribute{}, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyString to basetypes.Float32Type"), + }, + "ElementKeyValue": { + attribute: schema.Float32Attribute{}, + step: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyValue to basetypes.Float32Type"), + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.attribute.ApplyTerraform5AttributePathStep(testCase.step) + + if err != nil { + if testCase.expectedError == nil { + t.Fatalf("expected no error, got: %s", err) + } + + if !strings.Contains(err.Error(), testCase.expectedError.Error()) { + t.Fatalf("expected error %q, got: %s", testCase.expectedError, err) + } + } + + if err == nil && testCase.expectedError != nil { + t.Fatalf("got no error, expected: %s", testCase.expectedError) + } + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat32AttributeFloat32DefaultValue(t *testing.T) { + t.Parallel() + + opt := cmp.Comparer(func(x, y defaults.Float32) bool { + ctx := context.Background() + req := defaults.Float32Request{} + + xResp := defaults.Float32Response{} + x.DefaultFloat32(ctx, req, &xResp) + + yResp := defaults.Float32Response{} + y.DefaultFloat32(ctx, req, &yResp) + + return xResp.PlanValue.Equal(yResp.PlanValue) + }) + + testCases := map[string]struct { + attribute schema.Float32Attribute + expected defaults.Float32 + }{ + "no-default": { + attribute: schema.Float32Attribute{}, + expected: nil, + }, + "default": { + attribute: schema.Float32Attribute{ + Default: float32default.StaticFloat32(1.2345), + }, + expected: float32default.StaticFloat32(1.2345), + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.Float32DefaultValue() + + if diff := cmp.Diff(got, testCase.expected, opt); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat32AttributeFloat32PlanModifiers(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float32Attribute + expected []planmodifier.Float32 + }{ + "no-planmodifiers": { + attribute: schema.Float32Attribute{}, + expected: nil, + }, + "planmodifiers": { + attribute: schema.Float32Attribute{ + PlanModifiers: []planmodifier.Float32{}, + }, + expected: []planmodifier.Float32{}, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.Float32PlanModifiers() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat32AttributeFloat32Validators(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float32Attribute + expected []validator.Float32 + }{ + "no-validators": { + attribute: schema.Float32Attribute{}, + expected: nil, + }, + "validators": { + attribute: schema.Float32Attribute{ + Validators: []validator.Float32{}, + }, + expected: []validator.Float32{}, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.Float32Validators() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat32AttributeGetDeprecationMessage(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float32Attribute + expected string + }{ + "no-deprecation-message": { + attribute: schema.Float32Attribute{}, + expected: "", + }, + "deprecation-message": { + attribute: schema.Float32Attribute{ + DeprecationMessage: "test deprecation message", + }, + expected: "test deprecation message", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDeprecationMessage() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat32AttributeEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float32Attribute + other fwschema.Attribute + expected bool + }{ + "different-type": { + attribute: schema.Float32Attribute{}, + other: testschema.AttributeWithFloat32Validators{}, + expected: false, + }, + "equal": { + attribute: schema.Float32Attribute{}, + other: schema.Float32Attribute{}, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.Equal(testCase.other) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat32AttributeGetDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float32Attribute + expected string + }{ + "no-description": { + attribute: schema.Float32Attribute{}, + expected: "", + }, + "description": { + attribute: schema.Float32Attribute{ + Description: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat32AttributeGetMarkdownDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float32Attribute + expected string + }{ + "no-markdown-description": { + attribute: schema.Float32Attribute{}, + expected: "", + }, + "markdown-description": { + attribute: schema.Float32Attribute{ + MarkdownDescription: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetMarkdownDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat32AttributeGetType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float32Attribute + expected attr.Type + }{ + "base": { + attribute: schema.Float32Attribute{}, + expected: types.Float32Type, + }, + // "custom-type": { + // attribute: schema.Float32Attribute{ + // CustomType: testtypes.Float32Type{}, + // }, + // expected: testtypes.Float32Type{}, + // }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetType() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat32AttributeIsComputed(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float32Attribute + expected bool + }{ + "not-computed": { + attribute: schema.Float32Attribute{}, + expected: false, + }, + "computed": { + attribute: schema.Float32Attribute{ + Computed: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsComputed() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat32AttributeIsOptional(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float32Attribute + expected bool + }{ + "not-optional": { + attribute: schema.Float32Attribute{}, + expected: false, + }, + "optional": { + attribute: schema.Float32Attribute{ + Optional: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptional() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat32AttributeIsRequired(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float32Attribute + expected bool + }{ + "not-required": { + attribute: schema.Float32Attribute{}, + expected: false, + }, + "required": { + attribute: schema.Float32Attribute{ + Required: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequired() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat32AttributeIsSensitive(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float32Attribute + expected bool + }{ + "not-sensitive": { + attribute: schema.Float32Attribute{}, + expected: false, + }, + "sensitive": { + attribute: schema.Float32Attribute{ + Sensitive: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsSensitive() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat32AttributeValidateImplementation(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float32Attribute + request fwschema.ValidateImplementationRequest + expected *fwschema.ValidateImplementationResponse + }{ + "computed": { + attribute: schema.Float32Attribute{ + Computed: true, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{}, + }, + "default-without-computed": { + attribute: schema.Float32Attribute{ + Default: float32default.StaticFloat32(1.2), + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Schema Using Attribute Default For Non-Computed Attribute", + "Attribute \"test\" must be computed when using default. "+ + "This is an issue with the provider and should be reported to the provider developers.", + ), + }, + }, + }, + "default-with-computed": { + attribute: schema.Float32Attribute{ + Computed: true, + Default: float32default.StaticFloat32(1.2), + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{}, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := &fwschema.ValidateImplementationResponse{} + testCase.attribute.ValidateImplementation(context.Background(), testCase.request, got) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/resource/schema/float32default/doc.go b/resource/schema/float32default/doc.go new file mode 100644 index 00000000..607384e9 --- /dev/null +++ b/resource/schema/float32default/doc.go @@ -0,0 +1,5 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +// Package float32default provides default values for types.Float32 attributes. +package float32default diff --git a/resource/schema/float32default/static_value.go b/resource/schema/float32default/static_value.go new file mode 100644 index 00000000..aa3566a3 --- /dev/null +++ b/resource/schema/float32default/static_value.go @@ -0,0 +1,42 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package float32default + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/resource/schema/defaults" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// StaticFloat32 returns a static float32 value default handler. +// +// Use StaticFloat32 if a static default value for a float32 should be set. +func StaticFloat32(defaultVal float32) defaults.Float32 { + return staticFloat32Default{ + defaultVal: defaultVal, + } +} + +// staticFloat32Default is static value default handler that +// sets a value on a float32 attribute. +type staticFloat32Default struct { + defaultVal float32 +} + +// Description returns a human-readable description of the default value handler. +func (d staticFloat32Default) Description(_ context.Context) string { + return fmt.Sprintf("value defaults to %f", d.defaultVal) +} + +// MarkdownDescription returns a markdown description of the default value handler. +func (d staticFloat32Default) MarkdownDescription(_ context.Context) string { + return fmt.Sprintf("value defaults to `%f`", d.defaultVal) +} + +// DefaultFloat32 implements the static default value logic. +func (d staticFloat32Default) DefaultFloat32(_ context.Context, req defaults.Float32Request, resp *defaults.Float32Response) { + resp.PlanValue = types.Float32Value(d.defaultVal) +} diff --git a/resource/schema/float32default/static_value_test.go b/resource/schema/float32default/static_value_test.go new file mode 100644 index 00000000..543677b0 --- /dev/null +++ b/resource/schema/float32default/static_value_test.go @@ -0,0 +1,47 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package float32default_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + + "github.com/hashicorp/terraform-plugin-framework/resource/schema/defaults" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/float32default" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func TestStaticFloat32DefaultFloat32(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + defaultVal float32 + expected *defaults.Float32Response + }{ + "float32": { + defaultVal: 1.2345, + expected: &defaults.Float32Response{ + PlanValue: types.Float32Value(1.2345), + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + resp := &defaults.Float32Response{} + + float32default.StaticFloat32(testCase.defaultVal).DefaultFloat32(context.Background(), defaults.Float32Request{}, resp) + + if diff := cmp.Diff(testCase.expected, resp); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/resource/schema/float32planmodifier/doc.go b/resource/schema/float32planmodifier/doc.go new file mode 100644 index 00000000..73fc23f2 --- /dev/null +++ b/resource/schema/float32planmodifier/doc.go @@ -0,0 +1,5 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +// Package float32planmodifier provides plan modifiers for types.Float32 attributes. +package float32planmodifier diff --git a/resource/schema/float32planmodifier/requires_replace.go b/resource/schema/float32planmodifier/requires_replace.go new file mode 100644 index 00000000..06803f5f --- /dev/null +++ b/resource/schema/float32planmodifier/requires_replace.go @@ -0,0 +1,30 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package float32planmodifier + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" +) + +// RequiresReplace returns a plan modifier that conditionally requires +// resource replacement if: +// +// - The resource is planned for update. +// - The plan and state values are not equal. +// +// Use RequiresReplaceIfConfigured if the resource replacement should +// only occur if there is a configuration value (ignore unconfigured drift +// detection changes). Use RequiresReplaceIf if the resource replacement +// should check provider-defined conditional logic. +func RequiresReplace() planmodifier.Float32 { + return RequiresReplaceIf( + func(_ context.Context, _ planmodifier.Float32Request, resp *RequiresReplaceIfFuncResponse) { + resp.RequiresReplace = true + }, + "If the value of this attribute changes, Terraform will destroy and recreate the resource.", + "If the value of this attribute changes, Terraform will destroy and recreate the resource.", + ) +} diff --git a/resource/schema/float32planmodifier/requires_replace_if.go b/resource/schema/float32planmodifier/requires_replace_if.go new file mode 100644 index 00000000..4699b642 --- /dev/null +++ b/resource/schema/float32planmodifier/requires_replace_if.go @@ -0,0 +1,73 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package float32planmodifier + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" +) + +// RequiresReplaceIf returns a plan modifier that conditionally requires +// resource replacement if: +// +// - The resource is planned for update. +// - The plan and state values are not equal. +// - The given function returns true. Returning false will not unset any +// prior resource replacement. +// +// Use RequiresReplace if the resource replacement should always occur on value +// changes. Use RequiresReplaceIfConfigured if the resource replacement should +// occur on value changes, but only if there is a configuration value (ignore +// unconfigured drift detection changes). +func RequiresReplaceIf(f RequiresReplaceIfFunc, description, markdownDescription string) planmodifier.Float32 { + return requiresReplaceIfModifier{ + ifFunc: f, + description: description, + markdownDescription: markdownDescription, + } +} + +// requiresReplaceIfModifier is an plan modifier that sets RequiresReplace +// on the attribute if a given function is true. +type requiresReplaceIfModifier struct { + ifFunc RequiresReplaceIfFunc + description string + markdownDescription string +} + +// Description returns a human-readable description of the plan modifier. +func (m requiresReplaceIfModifier) Description(_ context.Context) string { + return m.description +} + +// MarkdownDescription returns a markdown description of the plan modifier. +func (m requiresReplaceIfModifier) MarkdownDescription(_ context.Context) string { + return m.markdownDescription +} + +// PlanModifyFloat32 implements the plan modification logic. +func (m requiresReplaceIfModifier) PlanModifyFloat32(ctx context.Context, req planmodifier.Float32Request, resp *planmodifier.Float32Response) { + // Do not replace on resource creation. + if req.State.Raw.IsNull() { + return + } + + // Do not replace on resource destroy. + if req.Plan.Raw.IsNull() { + return + } + + // Do not replace if the plan and state values are equal. + if req.PlanValue.Equal(req.StateValue) { + return + } + + ifFuncResp := &RequiresReplaceIfFuncResponse{} + + m.ifFunc(ctx, req, ifFuncResp) + + resp.Diagnostics.Append(ifFuncResp.Diagnostics...) + resp.RequiresReplace = ifFuncResp.RequiresReplace +} diff --git a/resource/schema/float32planmodifier/requires_replace_if_configured.go b/resource/schema/float32planmodifier/requires_replace_if_configured.go new file mode 100644 index 00000000..361a0c45 --- /dev/null +++ b/resource/schema/float32planmodifier/requires_replace_if_configured.go @@ -0,0 +1,34 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package float32planmodifier + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" +) + +// RequiresReplaceIfConfigured returns a plan modifier that conditionally requires +// resource replacement if: +// +// - The resource is planned for update. +// - The plan and state values are not equal. +// - The configuration value is not null. +// +// Use RequiresReplace if the resource replacement should occur regardless of +// the presence of a configuration value. Use RequiresReplaceIf if the resource +// replacement should check provider-defined conditional logic. +func RequiresReplaceIfConfigured() planmodifier.Float32 { + return RequiresReplaceIf( + func(_ context.Context, req planmodifier.Float32Request, resp *RequiresReplaceIfFuncResponse) { + if req.ConfigValue.IsNull() { + return + } + + resp.RequiresReplace = true + }, + "If the value of this attribute is configured and changes, Terraform will destroy and recreate the resource.", + "If the value of this attribute is configured and changes, Terraform will destroy and recreate the resource.", + ) +} diff --git a/resource/schema/float32planmodifier/requires_replace_if_configured_test.go b/resource/schema/float32planmodifier/requires_replace_if_configured_test.go new file mode 100644 index 00000000..2d13f3d9 --- /dev/null +++ b/resource/schema/float32planmodifier/requires_replace_if_configured_test.go @@ -0,0 +1,170 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package float32planmodifier_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/float32planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestRequiresReplaceIfConfiguredModifierPlanModifyFloat32(t *testing.T) { + t.Parallel() + + testSchema := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.Float32Attribute{}, + }, + } + + nullPlan := tfsdk.Plan{ + Schema: testSchema, + Raw: tftypes.NewValue( + testSchema.Type().TerraformType(context.Background()), + nil, + ), + } + + nullState := tfsdk.State{ + Schema: testSchema, + Raw: tftypes.NewValue( + testSchema.Type().TerraformType(context.Background()), + nil, + ), + } + + testPlan := func(value types.Float32) tfsdk.Plan { + tfValue, err := value.ToTerraformValue(context.Background()) + + if err != nil { + panic("ToTerraformValue error: " + err.Error()) + } + + return tfsdk.Plan{ + Schema: testSchema, + Raw: tftypes.NewValue( + testSchema.Type().TerraformType(context.Background()), + map[string]tftypes.Value{ + "testattr": tfValue, + }, + ), + } + } + + testState := func(value types.Float32) tfsdk.State { + tfValue, err := value.ToTerraformValue(context.Background()) + + if err != nil { + panic("ToTerraformValue error: " + err.Error()) + } + + return tfsdk.State{ + Schema: testSchema, + Raw: tftypes.NewValue( + testSchema.Type().TerraformType(context.Background()), + map[string]tftypes.Value{ + "testattr": tfValue, + }, + ), + } + } + + testCases := map[string]struct { + request planmodifier.Float32Request + expected *planmodifier.Float32Response + }{ + "state-null": { + // resource creation + request: planmodifier.Float32Request{ + ConfigValue: types.Float32Value(1.2), + Plan: testPlan(types.Float32Value(1.2)), + PlanValue: types.Float32Value(1.2), + State: nullState, + StateValue: types.Float32Null(), + }, + expected: &planmodifier.Float32Response{ + PlanValue: types.Float32Value(1.2), + RequiresReplace: false, + }, + }, + "plan-null": { + // resource destroy + request: planmodifier.Float32Request{ + ConfigValue: types.Float32Null(), + Plan: nullPlan, + PlanValue: types.Float32Null(), + State: testState(types.Float32Value(1.2)), + StateValue: types.Float32Value(1.2), + }, + expected: &planmodifier.Float32Response{ + PlanValue: types.Float32Null(), + RequiresReplace: false, + }, + }, + "planvalue-statevalue-different-configured": { + request: planmodifier.Float32Request{ + ConfigValue: types.Float32Value(2.4), + Plan: testPlan(types.Float32Value(2.4)), + PlanValue: types.Float32Value(2.4), + State: testState(types.Float32Value(1.2)), + StateValue: types.Float32Value(1.2), + }, + expected: &planmodifier.Float32Response{ + PlanValue: types.Float32Value(2.4), + RequiresReplace: true, + }, + }, + "planvalue-statevalue-different-unconfigured": { + request: planmodifier.Float32Request{ + ConfigValue: types.Float32Null(), + Plan: testPlan(types.Float32Value(2.4)), + PlanValue: types.Float32Value(2.4), + State: testState(types.Float32Value(1.2)), + StateValue: types.Float32Value(1.2), + }, + expected: &planmodifier.Float32Response{ + PlanValue: types.Float32Value(2.4), + RequiresReplace: false, + }, + }, + "planvalue-statevalue-equal": { + request: planmodifier.Float32Request{ + ConfigValue: types.Float32Value(1.2), + Plan: testPlan(types.Float32Value(1.2)), + PlanValue: types.Float32Value(1.2), + State: testState(types.Float32Value(1.2)), + StateValue: types.Float32Value(1.2), + }, + expected: &planmodifier.Float32Response{ + PlanValue: types.Float32Value(1.2), + RequiresReplace: false, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + resp := &planmodifier.Float32Response{ + PlanValue: testCase.request.PlanValue, + } + + float32planmodifier.RequiresReplaceIfConfigured().PlanModifyFloat32(context.Background(), testCase.request, resp) + + if diff := cmp.Diff(testCase.expected, resp); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/resource/schema/float32planmodifier/requires_replace_if_func.go b/resource/schema/float32planmodifier/requires_replace_if_func.go new file mode 100644 index 00000000..cdd31949 --- /dev/null +++ b/resource/schema/float32planmodifier/requires_replace_if_func.go @@ -0,0 +1,25 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package float32planmodifier + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" +) + +// RequiresReplaceIfFunc is a conditional function used in the RequiresReplaceIf +// plan modifier to determine whether the attribute requires replacement. +type RequiresReplaceIfFunc func(context.Context, planmodifier.Float32Request, *RequiresReplaceIfFuncResponse) + +// RequiresReplaceIfFuncResponse is the response type for a RequiresReplaceIfFunc. +type RequiresReplaceIfFuncResponse struct { + // Diagnostics report errors or warnings related to this logic. An empty + // or unset slice indicates success, with no warnings or errors generated. + Diagnostics diag.Diagnostics + + // RequiresReplace should be enabled if the resource should be replaced. + RequiresReplace bool +} diff --git a/resource/schema/float32planmodifier/requires_replace_if_test.go b/resource/schema/float32planmodifier/requires_replace_if_test.go new file mode 100644 index 00000000..f8185a66 --- /dev/null +++ b/resource/schema/float32planmodifier/requires_replace_if_test.go @@ -0,0 +1,181 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package float32planmodifier_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/float32planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestRequiresReplaceIfModifierPlanModifyFloat32(t *testing.T) { + t.Parallel() + + testSchema := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.Float32Attribute{}, + }, + } + + nullPlan := tfsdk.Plan{ + Schema: testSchema, + Raw: tftypes.NewValue( + testSchema.Type().TerraformType(context.Background()), + nil, + ), + } + + nullState := tfsdk.State{ + Schema: testSchema, + Raw: tftypes.NewValue( + testSchema.Type().TerraformType(context.Background()), + nil, + ), + } + + testPlan := func(value types.Float32) tfsdk.Plan { + tfValue, err := value.ToTerraformValue(context.Background()) + + if err != nil { + panic("ToTerraformValue error: " + err.Error()) + } + + return tfsdk.Plan{ + Schema: testSchema, + Raw: tftypes.NewValue( + testSchema.Type().TerraformType(context.Background()), + map[string]tftypes.Value{ + "testattr": tfValue, + }, + ), + } + } + + testState := func(value types.Float32) tfsdk.State { + tfValue, err := value.ToTerraformValue(context.Background()) + + if err != nil { + panic("ToTerraformValue error: " + err.Error()) + } + + return tfsdk.State{ + Schema: testSchema, + Raw: tftypes.NewValue( + testSchema.Type().TerraformType(context.Background()), + map[string]tftypes.Value{ + "testattr": tfValue, + }, + ), + } + } + + testCases := map[string]struct { + request planmodifier.Float32Request + ifFunc float32planmodifier.RequiresReplaceIfFunc + expected *planmodifier.Float32Response + }{ + "state-null": { + // resource creation + request: planmodifier.Float32Request{ + Plan: testPlan(types.Float32Unknown()), + PlanValue: types.Float32Unknown(), + State: nullState, + StateValue: types.Float32Null(), + }, + ifFunc: func(ctx context.Context, req planmodifier.Float32Request, resp *float32planmodifier.RequiresReplaceIfFuncResponse) { + resp.RequiresReplace = true // should never reach here + }, + expected: &planmodifier.Float32Response{ + PlanValue: types.Float32Unknown(), + RequiresReplace: false, + }, + }, + "plan-null": { + // resource destroy + request: planmodifier.Float32Request{ + Plan: nullPlan, + PlanValue: types.Float32Null(), + State: testState(types.Float32Value(1.2)), + StateValue: types.Float32Value(1.2), + }, + ifFunc: func(ctx context.Context, req planmodifier.Float32Request, resp *float32planmodifier.RequiresReplaceIfFuncResponse) { + resp.RequiresReplace = true // should never reach here + }, + expected: &planmodifier.Float32Response{ + PlanValue: types.Float32Null(), + RequiresReplace: false, + }, + }, + "planvalue-statevalue-different-if-false": { + request: planmodifier.Float32Request{ + Plan: testPlan(types.Float32Value(2.4)), + PlanValue: types.Float32Value(2.4), + State: testState(types.Float32Value(1.2)), + StateValue: types.Float32Value(1.2), + }, + ifFunc: func(ctx context.Context, req planmodifier.Float32Request, resp *float32planmodifier.RequiresReplaceIfFuncResponse) { + resp.RequiresReplace = false // no change + }, + expected: &planmodifier.Float32Response{ + PlanValue: types.Float32Value(2.4), + RequiresReplace: false, + }, + }, + "planvalue-statevalue-different-if-true": { + request: planmodifier.Float32Request{ + Plan: testPlan(types.Float32Value(2.4)), + PlanValue: types.Float32Value(2.4), + State: testState(types.Float32Value(1.2)), + StateValue: types.Float32Value(1.2), + }, + ifFunc: func(ctx context.Context, req planmodifier.Float32Request, resp *float32planmodifier.RequiresReplaceIfFuncResponse) { + resp.RequiresReplace = true // should reach here + }, + expected: &planmodifier.Float32Response{ + PlanValue: types.Float32Value(2.4), + RequiresReplace: true, + }, + }, + "planvalue-statevalue-equal": { + request: planmodifier.Float32Request{ + Plan: testPlan(types.Float32Value(1.2)), + PlanValue: types.Float32Value(1.2), + State: testState(types.Float32Value(1.2)), + StateValue: types.Float32Value(1.2), + }, + ifFunc: func(ctx context.Context, req planmodifier.Float32Request, resp *float32planmodifier.RequiresReplaceIfFuncResponse) { + resp.RequiresReplace = true // should never reach here + }, + expected: &planmodifier.Float32Response{ + PlanValue: types.Float32Value(1.2), + RequiresReplace: false, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + resp := &planmodifier.Float32Response{ + PlanValue: testCase.request.PlanValue, + } + + float32planmodifier.RequiresReplaceIf(testCase.ifFunc, "test", "test").PlanModifyFloat32(context.Background(), testCase.request, resp) + + if diff := cmp.Diff(testCase.expected, resp); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/resource/schema/float32planmodifier/requires_replace_test.go b/resource/schema/float32planmodifier/requires_replace_test.go new file mode 100644 index 00000000..32f35e86 --- /dev/null +++ b/resource/schema/float32planmodifier/requires_replace_test.go @@ -0,0 +1,153 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package float32planmodifier_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/float32planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestRequiresReplaceModifierPlanModifyFloat32(t *testing.T) { + t.Parallel() + + testSchema := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.Float32Attribute{}, + }, + } + + nullPlan := tfsdk.Plan{ + Schema: testSchema, + Raw: tftypes.NewValue( + testSchema.Type().TerraformType(context.Background()), + nil, + ), + } + + nullState := tfsdk.State{ + Schema: testSchema, + Raw: tftypes.NewValue( + testSchema.Type().TerraformType(context.Background()), + nil, + ), + } + + testPlan := func(value types.Float32) tfsdk.Plan { + tfValue, err := value.ToTerraformValue(context.Background()) + + if err != nil { + panic("ToTerraformValue error: " + err.Error()) + } + + return tfsdk.Plan{ + Schema: testSchema, + Raw: tftypes.NewValue( + testSchema.Type().TerraformType(context.Background()), + map[string]tftypes.Value{ + "testattr": tfValue, + }, + ), + } + } + + testState := func(value types.Float32) tfsdk.State { + tfValue, err := value.ToTerraformValue(context.Background()) + + if err != nil { + panic("ToTerraformValue error: " + err.Error()) + } + + return tfsdk.State{ + Schema: testSchema, + Raw: tftypes.NewValue( + testSchema.Type().TerraformType(context.Background()), + map[string]tftypes.Value{ + "testattr": tfValue, + }, + ), + } + } + + testCases := map[string]struct { + request planmodifier.Float32Request + expected *planmodifier.Float32Response + }{ + "state-null": { + // resource creation + request: planmodifier.Float32Request{ + Plan: testPlan(types.Float32Unknown()), + PlanValue: types.Float32Unknown(), + State: nullState, + StateValue: types.Float32Null(), + }, + expected: &planmodifier.Float32Response{ + PlanValue: types.Float32Unknown(), + RequiresReplace: false, + }, + }, + "plan-null": { + // resource destroy + request: planmodifier.Float32Request{ + Plan: nullPlan, + PlanValue: types.Float32Null(), + State: testState(types.Float32Value(1.2)), + StateValue: types.Float32Value(1.2), + }, + expected: &planmodifier.Float32Response{ + PlanValue: types.Float32Null(), + RequiresReplace: false, + }, + }, + "planvalue-statevalue-different": { + request: planmodifier.Float32Request{ + Plan: testPlan(types.Float32Value(2.4)), + PlanValue: types.Float32Value(2.4), + State: testState(types.Float32Value(1.2)), + StateValue: types.Float32Value(1.2), + }, + expected: &planmodifier.Float32Response{ + PlanValue: types.Float32Value(2.4), + RequiresReplace: true, + }, + }, + "planvalue-statevalue-equal": { + request: planmodifier.Float32Request{ + Plan: testPlan(types.Float32Value(1.2)), + PlanValue: types.Float32Value(1.2), + State: testState(types.Float32Value(1.2)), + StateValue: types.Float32Value(1.2), + }, + expected: &planmodifier.Float32Response{ + PlanValue: types.Float32Value(1.2), + RequiresReplace: false, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + resp := &planmodifier.Float32Response{ + PlanValue: testCase.request.PlanValue, + } + + float32planmodifier.RequiresReplace().PlanModifyFloat32(context.Background(), testCase.request, resp) + + if diff := cmp.Diff(testCase.expected, resp); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/resource/schema/float32planmodifier/use_state_for_unknown.go b/resource/schema/float32planmodifier/use_state_for_unknown.go new file mode 100644 index 00000000..fc8eadb9 --- /dev/null +++ b/resource/schema/float32planmodifier/use_state_for_unknown.go @@ -0,0 +1,55 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package float32planmodifier + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" +) + +// UseStateForUnknown returns a plan modifier that copies a known prior state +// value into the planned value. Use this when it is known that an unconfigured +// value will remain the same after a resource update. +// +// To prevent Terraform errors, the framework automatically sets unconfigured +// and Computed attributes to an unknown value "(known after apply)" on update. +// Using this plan modifier will instead display the prior state value in the +// plan, unless a prior plan modifier adjusts the value. +func UseStateForUnknown() planmodifier.Float32 { + return useStateForUnknownModifier{} +} + +// useStateForUnknownModifier implements the plan modifier. +type useStateForUnknownModifier struct{} + +// Description returns a human-readable description of the plan modifier. +func (m useStateForUnknownModifier) Description(_ context.Context) string { + return "Once set, the value of this attribute in state will not change." +} + +// MarkdownDescription returns a markdown description of the plan modifier. +func (m useStateForUnknownModifier) MarkdownDescription(_ context.Context) string { + return "Once set, the value of this attribute in state will not change." +} + +// PlanModifyFloat32 implements the plan modification logic. +func (m useStateForUnknownModifier) PlanModifyFloat32(_ context.Context, req planmodifier.Float32Request, resp *planmodifier.Float32Response) { + // Do nothing if there is no state value. + if req.StateValue.IsNull() { + return + } + + // Do nothing if there is a known planned value. + if !req.PlanValue.IsUnknown() { + return + } + + // Do nothing if there is an unknown configuration value, otherwise interpolation gets messed up. + if req.ConfigValue.IsUnknown() { + return + } + + resp.PlanValue = req.StateValue +} diff --git a/resource/schema/float32planmodifier/use_state_for_unknown_test.go b/resource/schema/float32planmodifier/use_state_for_unknown_test.go new file mode 100644 index 00000000..cb8c514f --- /dev/null +++ b/resource/schema/float32planmodifier/use_state_for_unknown_test.go @@ -0,0 +1,140 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package float32planmodifier_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/float32planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func TestUseStateForUnknownModifierPlanModifyFloat32(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + request planmodifier.Float32Request + expected *planmodifier.Float32Response + }{ + "null-state": { + // when we first create the resource, use the unknown + // value + request: planmodifier.Float32Request{ + StateValue: types.Float32Null(), + PlanValue: types.Float32Unknown(), + ConfigValue: types.Float32Null(), + }, + expected: &planmodifier.Float32Response{ + PlanValue: types.Float32Unknown(), + }, + }, + "known-plan": { + // this would really only happen if we had a plan + // modifier setting the value before this plan modifier + // got to it + // + // but we still want to preserve that value, in this + // case + request: planmodifier.Float32Request{ + StateValue: types.Float32Value(2.4), + PlanValue: types.Float32Value(1.2), + ConfigValue: types.Float32Null(), + }, + expected: &planmodifier.Float32Response{ + PlanValue: types.Float32Value(1.2), + }, + }, + "non-null-state-unknown-plan": { + // this is the situation we want to preserve the state + // in + request: planmodifier.Float32Request{ + StateValue: types.Float32Value(1.2), + PlanValue: types.Float32Unknown(), + ConfigValue: types.Float32Null(), + }, + expected: &planmodifier.Float32Response{ + PlanValue: types.Float32Value(1.2), + }, + }, + "unknown-config": { + // this is the situation in which a user is + // interpolating into a field. We want that to still + // show up as unknown, otherwise they'll get apply-time + // errors for changing the value even though we knew it + // was legitimately possible for it to change and the + // provider can't prevent this from happening + request: planmodifier.Float32Request{ + StateValue: types.Float32Value(1.2), + PlanValue: types.Float32Unknown(), + ConfigValue: types.Float32Unknown(), + }, + expected: &planmodifier.Float32Response{ + PlanValue: types.Float32Unknown(), + }, + }, + "under-list": { + request: planmodifier.Float32Request{ + ConfigValue: types.Float32Null(), + Path: path.Root("test").AtListIndex(0).AtName("nested_test"), + PlanValue: types.Float32Unknown(), + StateValue: types.Float32Null(), + }, + expected: &planmodifier.Float32Response{ + PlanValue: types.Float32Unknown(), + }, + }, + "under-set": { + request: planmodifier.Float32Request{ + ConfigValue: types.Float32Null(), + Path: path.Root("test").AtSetValue( + types.SetValueMust( + types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "nested_test": types.Float32Type, + }, + }, + []attr.Value{ + types.ObjectValueMust( + map[string]attr.Type{ + "nested_test": types.Float32Type, + }, + map[string]attr.Value{ + "nested_test": types.Float32Unknown(), + }, + ), + }, + ), + ).AtName("nested_test"), + PlanValue: types.Float32Unknown(), + StateValue: types.Float32Null(), + }, + expected: &planmodifier.Float32Response{ + PlanValue: types.Float32Unknown(), + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + resp := &planmodifier.Float32Response{ + PlanValue: testCase.request.PlanValue, + } + + float32planmodifier.UseStateForUnknown().PlanModifyFloat32(context.Background(), testCase.request, resp) + + if diff := cmp.Diff(testCase.expected, resp); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/resource/schema/planmodifier/float32.go b/resource/schema/planmodifier/float32.go new file mode 100644 index 00000000..6cb6c482 --- /dev/null +++ b/resource/schema/planmodifier/float32.go @@ -0,0 +1,88 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package planmodifier + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "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" +) + +// Float32 is a schema plan modifier for types.Float32 attributes. +type Float32 interface { + Describer + + // PlanModifyFloat32 should perform the modification. + PlanModifyFloat32(context.Context, Float32Request, *Float32Response) +} + +// Float32Request is a request for types.Float32 schema plan modification. +type Float32Request struct { + // Path contains the path of the attribute for modification. Use this path + // for any response diagnostics. + Path path.Path + + // PathExpression contains the expression matching the exact path + // of the attribute for modification. + PathExpression path.Expression + + // Config contains the entire configuration of the resource. + Config tfsdk.Config + + // ConfigValue contains the value of the attribute for modification from the configuration. + ConfigValue types.Float32 + + // Plan contains the entire proposed new state of the resource. + Plan tfsdk.Plan + + // PlanValue contains the value of the attribute for modification from the proposed new state. + PlanValue types.Float32 + + // State contains the entire prior state of the resource. + State tfsdk.State + + // StateValue contains the value of the attribute for modification from the prior state. + StateValue types.Float32 + + // Private is provider-defined resource private state data which was previously + // stored with the resource state. This data is opaque to Terraform and does + // not affect plan output. Any existing data is copied to + // Float32Response.Private to prevent accidental private state data loss. + // + // The private state data is always the original data when the schema-based plan + // modification began or, is updated as the logic traverses deeper into underlying + // attributes. + // + // Use the GetKey method to read data. Use the SetKey method on + // Float32Response.Private to update or remove a value. + Private *privatestate.ProviderData +} + +// Float32Response is a response to a Float32Request. +type Float32Response struct { + // PlanValue is the planned new state for the attribute. + PlanValue types.Float32 + + // RequiresReplace indicates whether a change in the attribute + // requires replacement of the whole resource. + RequiresReplace bool + + // Private is the private state resource data following the PlanModifyFloat32 operation. + // This field is pre-populated from Float32Request.Private and + // can be modified during the resource's PlanModifyFloat32 operation. + // + // The private state data is always the original data when the schema-based plan + // modification began or, is updated as the logic traverses deeper into underlying + // attributes. + Private *privatestate.ProviderData + + // Diagnostics report errors or warnings related to modifying the resource + // plan. An empty slice indicates success, with no warnings or + // errors generated. + Diagnostics diag.Diagnostics +} diff --git a/schema/validator/float32.go b/schema/validator/float32.go new file mode 100644 index 00000000..c1cd8421 --- /dev/null +++ b/schema/validator/float32.go @@ -0,0 +1,46 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package validator + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Float32 is a schema validator for types.Float32 attributes. +type Float32 interface { + Describer + + // ValidateFloat32 should perform the validation. + ValidateFloat32(context.Context, Float32Request, *Float32Response) +} + +// Float32Request is a request for types.Float32 schema validation. +type Float32Request struct { + // Path contains the path of the attribute for validation. Use this path + // for any response diagnostics. + Path path.Path + + // PathExpression contains the expression matching the exact path + // of the attribute for validation. + PathExpression path.Expression + + // Config contains the entire configuration of the data source, provider, or resource. + Config tfsdk.Config + + // ConfigValue contains the value of the attribute for validation from the configuration. + ConfigValue types.Float32 +} + +// Float32Response is a response to a Float32Request. +type Float32Response struct { + // Diagnostics report errors or warnings related to validating the data source, provider, or resource + // configuration. An empty slice indicates success, with no warnings + // or errors generated. + Diagnostics diag.Diagnostics +} diff --git a/tfsdk/value_as_test.go b/tfsdk/value_as_test.go index 39e5f1d4..a5e0cf80 100644 --- a/tfsdk/value_as_test.go +++ b/tfsdk/value_as_test.go @@ -30,11 +30,20 @@ func newBoolPointerPointer(in bool) **bool { return &boolPointer } -func newFloatPointer(in float64) *float64 { +func newFloat32Pointer(in float32) *float32 { return &in } -func newFloatPointerPointer(in float64) **float64 { +func newFloat32PointerPointer(in float32) **float32 { + floatPointer := &in + return &floatPointer +} + +func newFloat64Pointer(in float64) *float64 { + return &in +} + +func newFloat64PointerPointer(in float64) **float64 { floatPointer := &in return &floatPointer } @@ -108,15 +117,25 @@ func TestValueAs(t *testing.T) { target: newBoolPointerPointer(false), expected: newBoolPointerPointer(true), }, + "primitive float32 pointer": { + val: types.Float32Value(12.3), + target: newFloat32Pointer(0.0), + expected: newFloat32Pointer(12.3), + }, + "primitive float32 pointer pointer": { + val: types.Float32Value(12.3), + target: newFloat32PointerPointer(0.0), + expected: newFloat32PointerPointer(12.3), + }, "primitive float64 pointer": { val: types.Float64Value(12.3), - target: newFloatPointer(0.0), - expected: newFloatPointer(12.3), + target: newFloat64Pointer(0.0), + expected: newFloat64Pointer(12.3), }, "primitive float64 pointer pointer": { val: types.Float64Value(12.3), - target: newFloatPointerPointer(0.0), - expected: newFloatPointerPointer(12.3), + target: newFloat64PointerPointer(0.0), + expected: newFloat64PointerPointer(12.3), }, "primitive int32 pointer": { val: types.Int32Value(12), diff --git a/types/basetypes/float32_type.go b/types/basetypes/float32_type.go new file mode 100644 index 00000000..77d35286 --- /dev/null +++ b/types/basetypes/float32_type.go @@ -0,0 +1,113 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package basetypes + +import ( + "context" + "fmt" + "math" + "math/big" + + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/logging" +) + +// Float32Typable extends attr.Type for float32 types. +// Implement this interface to create a custom Float32Type type. +type Float32Typable interface { + attr.Type + + // ValueFromFloat32 should convert the Float32 to a Float32Valuable type. + ValueFromFloat32(context.Context, Float32Value) (Float32Valuable, diag.Diagnostics) +} + +var _ Float32Typable = Float32Type{} + +// Float32Type is the base framework type for a floating point number. +// Float32Value is the associated value type. +type Float32Type struct{} + +// ApplyTerraform5AttributePathStep applies the given AttributePathStep to the +// type. +func (t Float32Type) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { + return nil, fmt.Errorf("cannot apply AttributePathStep %T to %s", step, t.String()) +} + +// Equal returns true if the given type is equivalent. +func (t Float32Type) Equal(o attr.Type) bool { + _, ok := o.(Float32Type) + + return ok +} + +// String returns a human readable string of the type name. +func (t Float32Type) String() string { + return "basetypes.Float32Type" +} + +// TerraformType returns the tftypes.Type that should be used to represent this +// framework type. +func (t Float32Type) TerraformType(_ context.Context) tftypes.Type { + return tftypes.Number +} + +// ValueFromFloat32 returns a Float32Valuable type given a Float32Value. +func (t Float32Type) ValueFromFloat32(_ context.Context, v Float32Value) (Float32Valuable, diag.Diagnostics) { + return v, nil +} + +// ValueFromTerraform returns a Value given a tftypes.Value. This is meant to +// convert the tftypes.Value into a more convenient Go type for the provider to +// consume the data with. +func (t Float32Type) ValueFromTerraform(ctx context.Context, in tftypes.Value) (attr.Value, error) { + if !in.IsKnown() { + return NewFloat32Unknown(), nil + } + + if in.IsNull() { + return NewFloat32Null(), nil + } + + var bigF *big.Float + err := in.As(&bigF) + + if err != nil { + return nil, err + } + + f, accuracy := bigF.Float32() + f64, f64accuracy := bigF.Float64() + + if accuracy == big.Exact && f64accuracy == big.Exact { + logging.FrameworkDebug(ctx, fmt.Sprintf("Float32Type ValueFromTerraform: big.Float value has distinct float32 and float64 representations "+ + "(float32 value: %f, float64 value: %f)", f, f64)) + } + + // Underflow + // Reference: https://pkg.go.dev/math/big#Float.Float32 + if f == 0 && accuracy != big.Exact { + return nil, fmt.Errorf("Value %s cannot be represented as a 32-bit floating point.", bigF) + } + + // Overflow + // Reference: https://pkg.go.dev/math/big#Float.Float32 + if math.IsInf(float64(f), 0) { + return nil, fmt.Errorf("Value %s cannot be represented as a 32-bit floating point.", bigF) + } + + // Underlying *big.Float values are not exposed with helper functions, so creating Float32Value via struct literal + return Float32Value{ + state: attr.ValueStateKnown, + value: bigF, + }, nil +} + +// ValueType returns the Value type. +func (t Float32Type) ValueType(_ context.Context) attr.Value { + // This Value does not need to be valid. + return Float32Value{} +} diff --git a/types/basetypes/float32_type_test.go b/types/basetypes/float32_type_test.go new file mode 100644 index 00000000..a409b35c --- /dev/null +++ b/types/basetypes/float32_type_test.go @@ -0,0 +1,138 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package basetypes + +import ( + "context" + "fmt" + "math" + "math/big" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/attr" +) + +func TestFloat32TypeValueFromTerraform(t *testing.T) { + t.Parallel() + + var v float32 = 123.456 + + type testCase struct { + input tftypes.Value + expectation attr.Value + expectedErr string + } + tests := map[string]testCase{ + "value-int": { + input: tftypes.NewValue(tftypes.Number, 123), + expectation: NewFloat32Value(float32(123.0)), + }, + "value-float": { + input: tftypes.NewValue(tftypes.Number, float64(v)), + expectation: NewFloat32Value(v), + }, + "unknown": { + input: tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), + expectation: NewFloat32Unknown(), + }, + "null": { + input: tftypes.NewValue(tftypes.Number, nil), + expectation: NewFloat32Null(), + }, + "wrongType": { + input: tftypes.NewValue(tftypes.String, "oops"), + expectedErr: "can't unmarshal tftypes.String into *big.Float, expected *big.Float", + }, + // Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/327 + // To ensure underlying *big.Float precision matches, create `expectation` via struct literal + "zero-string-float": { + input: tftypes.NewValue(tftypes.Number, testMustParseFloat("0.0")), + expectation: Float32Value{ + state: attr.ValueStateKnown, + value: testMustParseFloat("0.0"), + }, + }, + "positive-string-float": { + input: tftypes.NewValue(tftypes.Number, testMustParseFloat("123.2")), + expectation: Float32Value{ + state: attr.ValueStateKnown, + value: testMustParseFloat("123.2"), + }, + }, + "negative-string-float": { + input: tftypes.NewValue(tftypes.Number, testMustParseFloat("-123.2")), + expectation: Float32Value{ + state: attr.ValueStateKnown, + value: testMustParseFloat("-123.2"), + }, + }, + // Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/815 + // To ensure underlying *big.Float precision matches, create `expectation` via struct literal + "retain-string-float-512-precision": { + input: tftypes.NewValue(tftypes.Number, testMustParseFloat("0.010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003")), + expectation: Float32Value{ + state: attr.ValueStateKnown, + value: testMustParseFloat("0.010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003"), + }, + }, + // Reference: https://pkg.go.dev/math/big#Float.Float32 + // Reference: https://pkg.go.dev/math#pkg-constants + "SmallestNonzeroFloat32": { + input: tftypes.NewValue(tftypes.Number, big.NewFloat(math.SmallestNonzeroFloat32)), + expectation: NewFloat32Value(math.SmallestNonzeroFloat32), + }, + "SmallestNonzeroFloat32-below": { + input: tftypes.NewValue(tftypes.Number, testMustParseFloat("1.401298464324817070923729583289916131280e-46")), + expectedErr: fmt.Sprintf("Value %s cannot be represented as a 32-bit floating point.", testMustParseFloat("1.401298464324817070923729583289916131280e-46")), + }, + // Reference: https://pkg.go.dev/math/big#Float.Float32 + // Reference: https://pkg.go.dev/math#pkg-constants + "MaxFloat32": { + input: tftypes.NewValue(tftypes.Number, big.NewFloat(math.MaxFloat32)), + expectation: NewFloat32Value(math.MaxFloat32), + }, + "MaxFloat32-above": { + input: tftypes.NewValue(tftypes.Number, testMustParseFloat("3.40282346638528859811704183484516925440e+39")), + expectedErr: fmt.Sprintf("Value %s cannot be represented as a 32-bit floating point.", testMustParseFloat("3.40282346638528859811704183484516925440e+39")), + }, + } + for name, test := range tests { + name, test := name, test + t.Run(name, func(t *testing.T) { + t.Parallel() + ctx := context.Background() + + got, err := Float32Type{}.ValueFromTerraform(ctx, test.input) + if err != nil { + if test.expectedErr == "" { + t.Errorf("Unexpected error: %s", err) + return + } + if test.expectedErr != err.Error() { + t.Errorf("Expected error to be %q, got %q", test.expectedErr, err.Error()) + return + } + // we have an error, and it matches our + // expectations, we're good + return + } + if err == nil && test.expectedErr != "" { + t.Errorf("Expected error to be %q, didn't get an error", test.expectedErr) + return + } + if diff := cmp.Diff(got, test.expectation); diff != "" { + t.Errorf("Unexpected response (+wanted, -got): %s", diff) + } + if test.expectation.IsNull() != test.input.IsNull() { + t.Errorf("Expected null-ness match: expected %t, got %t", test.expectation.IsNull(), test.input.IsNull()) + } + if test.expectation.IsUnknown() != !test.input.IsKnown() { + t.Errorf("Expected unknown-ness match: expected %t, got %t", test.expectation.IsUnknown(), !test.input.IsKnown()) + } + }) + } +} diff --git a/types/basetypes/float32_value.go b/types/basetypes/float32_value.go new file mode 100644 index 00000000..1085b849 --- /dev/null +++ b/types/basetypes/float32_value.go @@ -0,0 +1,218 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package basetypes + +import ( + "context" + "fmt" + "math/big" + + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" +) + +var ( + _ Float32Valuable = Float32Value{} + _ Float32ValuableWithSemanticEquals = Float32Value{} +) + +// Float32Valuable extends attr.Value for float32 value types. +// Implement this interface to create a custom Float32 value type. +type Float32Valuable interface { + attr.Value + + // ToFloat32Value should convert the value type to a Float32. + ToFloat32Value(ctx context.Context) (Float32Value, diag.Diagnostics) +} + +// Float32ValuableWithSemanticEquals extends Float32Valuable with semantic +// equality logic. +type Float32ValuableWithSemanticEquals interface { + Float32Valuable + + // Float32SemanticEquals should return true if the given value is + // semantically equal to the current value. This logic is used to prevent + // Terraform data consistency errors and resource drift where a value change + // may have inconsequential differences, such as rounding. + // + // Only known values are compared with this method as changing a value's + // state implicitly represents a different value. + Float32SemanticEquals(context.Context, Float32Valuable) (bool, diag.Diagnostics) +} + +// NewFloat32Null creates a Float32 with a null value. Determine whether the value is +// null via the Float32 type IsNull method. +func NewFloat32Null() Float32Value { + return Float32Value{ + state: attr.ValueStateNull, + } +} + +// NewFloat32Unknown creates a Float32 with an unknown value. Determine whether the +// value is unknown via the Float32 type IsUnknown method. +func NewFloat32Unknown() Float32Value { + return Float32Value{ + state: attr.ValueStateUnknown, + } +} + +// NewFloat32Value creates a Float32 with a known value. Access the value via the Float32 +// type ValueFloat32 method. Passing a value of `NaN` will result in a panic. +func NewFloat32Value(value float32) Float32Value { + return Float32Value{ + state: attr.ValueStateKnown, + value: big.NewFloat(float64(value)), + } +} + +// NewFloat32PointerValue creates a Float32 with a null value if nil or a known +// value. Access the value via the Float32 type ValueFloat32Pointer method. +// Passing a value of `NaN` will result in a panic. +func NewFloat32PointerValue(value *float32) Float32Value { + if value == nil { + return NewFloat32Null() + } + + return NewFloat32Value(*value) +} + +// Float32Value represents a 32-bit floating point value, exposed as a float32. +type Float32Value struct { + // state represents whether the value is null, unknown, or known. The + // zero-value is null. + state attr.ValueState + + // value contains the known value, if not null or unknown. + value *big.Float +} + +// Float32SemanticEquals returns true if the given Float32Value is semantically equal to the current Float32Value. +// The underlying value *big.Float can store more precise float values then the Go built-in float32 type. This only +// compares the precision of the value that can be represented as the Go built-in float32, which is 53 bits of precision. +func (f Float32Value) Float32SemanticEquals(ctx context.Context, newValuable Float32Valuable) (bool, diag.Diagnostics) { + var diags diag.Diagnostics + + newValue, ok := newValuable.(Float32Value) + if !ok { + diags.AddError( + "Semantic Equality Check Error", + "An unexpected value type was received while performing semantic equality checks. "+ + "Please report this to the provider developers.\n\n"+ + "Expected Value Type: "+fmt.Sprintf("%T", f)+"\n"+ + "Got Value Type: "+fmt.Sprintf("%T", newValuable), + ) + + return false, diags + } + + return f.ValueFloat32() == newValue.ValueFloat32(), diags +} + +// Equal returns true if `other` is a Float32 and has the same value as `f`. +func (f Float32Value) Equal(other attr.Value) bool { + o, ok := other.(Float32Value) + + if !ok { + return false + } + + if f.state != o.state { + return false + } + + if f.state != attr.ValueStateKnown { + return true + } + + // Not possible to create a known Float32Value with a nil value, but check anyways + if f.value == nil || o.value == nil { + return f.value == o.value + } + + return f.value.Cmp(o.value) == 0 +} + +// ToTerraformValue returns the data contained in the Float32 as a tftypes.Value. +func (f Float32Value) ToTerraformValue(ctx context.Context) (tftypes.Value, error) { + switch f.state { + case attr.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 attr.ValueStateNull: + return tftypes.NewValue(tftypes.Number, nil), nil + case attr.ValueStateUnknown: + return tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), nil + default: + panic(fmt.Sprintf("unhandled Float32 state in ToTerraformValue: %s", f.state)) + } +} + +// Type returns a Float32Type. +func (f Float32Value) Type(ctx context.Context) attr.Type { + return Float32Type{} +} + +// IsNull returns true if the Float32 represents a null value. +func (f Float32Value) IsNull() bool { + return f.state == attr.ValueStateNull +} + +// IsUnknown returns true if the Float32 represents a currently unknown value. +func (f Float32Value) IsUnknown() bool { + return f.state == attr.ValueStateUnknown +} + +// String returns a human-readable representation of the Float32 value. +// The string returned here is not protected by any compatibility guarantees, +// and is intended for logging and error reporting. +func (f Float32Value) String() string { + if f.IsUnknown() { + return attr.UnknownValueString + } + + if f.IsNull() { + return attr.NullValueString + } + + f32 := f.ValueFloat32() + + return fmt.Sprintf("%f", f32) +} + +// ValueFloat32 returns the known float32 value. If Float32 is null or unknown, returns +// 0.0. +func (f Float32Value) ValueFloat32() float32 { + if f.IsNull() || f.IsUnknown() { + return float32(0.0) + } + + f32, _ := f.value.Float32() + return f32 +} + +// ValueFloat32Pointer returns a pointer to the known float32 value, nil for a +// null value, or a pointer to 0.0 for an unknown value. +func (f Float32Value) ValueFloat32Pointer() *float32 { + if f.IsNull() { + return nil + } + + if f.IsUnknown() { + f32 := float32(0.0) + return &f32 + } + + f32, _ := f.value.Float32() + return &f32 +} + +// ToFloat32Value returns Float32. +func (f Float32Value) ToFloat32Value(context.Context) (Float32Value, diag.Diagnostics) { + return f, nil +} diff --git a/types/basetypes/float32_value_test.go b/types/basetypes/float32_value_test.go new file mode 100644 index 00000000..73781613 --- /dev/null +++ b/types/basetypes/float32_value_test.go @@ -0,0 +1,545 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package basetypes + +import ( + "context" + "math" + "math/big" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" +) + +func TestFloat32ValueToTerraformValue(t *testing.T) { + t.Parallel() + + var Float32Val float32 = 123.456 + + type testCase struct { + input Float32Value + expectation interface{} + } + tests := map[string]testCase{ + "known-int": { + input: NewFloat32Value(123), + expectation: tftypes.NewValue(tftypes.Number, big.NewFloat(123.0)), + }, + "known-float": { + input: NewFloat32Value(Float32Val), + expectation: tftypes.NewValue(tftypes.Number, big.NewFloat(float64(Float32Val))), + }, + "unknown": { + input: NewFloat32Unknown(), + expectation: tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), + }, + "null": { + input: NewFloat32Null(), + expectation: tftypes.NewValue(tftypes.Number, nil), + }, + "deprecated-value-int": { + input: NewFloat32Value(123), + expectation: tftypes.NewValue(tftypes.Number, big.NewFloat(123.0)), + }, + "deprecated-value-float": { + input: NewFloat32Value(Float32Val), + expectation: tftypes.NewValue(tftypes.Number, big.NewFloat(float64(Float32Val))), + }, + "deprecated-unknown": { + input: NewFloat32Unknown(), + expectation: tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), + }, + "deprecated-null": { + input: NewFloat32Null(), + expectation: tftypes.NewValue(tftypes.Number, nil), + }, + } + for name, test := range tests { + name, test := name, test + t.Run(name, func(t *testing.T) { + t.Parallel() + ctx := context.Background() + + got, err := test.input.ToTerraformValue(ctx) + if err != nil { + t.Errorf("Unexpected error: %s", err) + return + } + if !cmp.Equal(got, test.expectation, cmp.Comparer(numberComparer)) { + t.Errorf("Expected %+v, got %+v", test.expectation, got) + } + }) + } +} + +func TestFloat32ValueEqual(t *testing.T) { + t.Parallel() + + type testCase struct { + input Float32Value + candidate attr.Value + expectation bool + } + tests := map[string]testCase{ + "known-known-53-precison-same": { + input: NewFloat32Value(123.123), + candidate: NewFloat32Value(123.123), + expectation: true, + }, + "known-known-53-precison-diff": { + input: NewFloat32Value(123.123), + candidate: NewFloat32Value(456.456), + expectation: false, + }, + "known-known-512-precision-same": { + input: Float32Value{ + state: attr.ValueStateKnown, + value: testMustParseFloat("0.010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001"), + }, + candidate: Float32Value{ + state: attr.ValueStateKnown, + value: testMustParseFloat("0.010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001"), + }, + expectation: true, + }, + "known-known-512-precision-diff": { + input: Float32Value{ + state: attr.ValueStateKnown, + value: testMustParseFloat("0.010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001"), + }, + candidate: Float32Value{ + state: attr.ValueStateKnown, + value: testMustParseFloat("0.010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000009"), + }, + expectation: false, + }, + "known-known-512-precision-mantissa-diff": { + input: Float32Value{ + state: attr.ValueStateKnown, + value: testMustParseFloat("0.010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001"), + }, + candidate: Float32Value{ + state: attr.ValueStateKnown, + value: testMustParseFloat("0.01"), + }, + expectation: false, + }, + "known-known-precisiondiff-mantissa-same": { + input: NewFloat32Value(123), + candidate: Float32Value{ + state: attr.ValueStateKnown, + value: testMustParseFloat("123"), + }, + expectation: true, + }, + "known-known-precisiondiff-mantissa-diff": { + input: NewFloat32Value(0.1), + candidate: Float32Value{ + state: attr.ValueStateKnown, + value: testMustParseFloat("0.1"), + }, + expectation: false, + }, + "knownnil-known": { + input: Float32Value{ + state: attr.ValueStateKnown, + value: nil, + }, + candidate: NewFloat32Value(0.1), + expectation: false, + }, + "known-knownnil": { + input: NewFloat32Value(0.1), + candidate: Float32Value{ + state: attr.ValueStateKnown, + value: nil, + }, + expectation: false, + }, + "knownnil-knownnil": { + input: Float32Value{ + state: attr.ValueStateKnown, + value: nil, + }, + candidate: Float32Value{ + state: attr.ValueStateKnown, + value: nil, + }, + expectation: true, + }, + "known-unknown": { + input: NewFloat32Value(123), + candidate: NewFloat32Unknown(), + expectation: false, + }, + "known-null": { + input: NewFloat32Value(123), + candidate: NewFloat32Null(), + expectation: false, + }, + "unknown-value": { + input: NewFloat32Unknown(), + candidate: NewFloat32Value(123), + expectation: false, + }, + "unknown-unknown": { + input: NewFloat32Unknown(), + candidate: NewFloat32Unknown(), + expectation: true, + }, + "unknown-null": { + input: NewFloat32Unknown(), + candidate: NewFloat32Null(), + expectation: false, + }, + "null-known": { + input: NewFloat32Null(), + candidate: NewFloat32Value(123), + expectation: false, + }, + "null-unknown": { + input: NewFloat32Null(), + candidate: NewFloat32Unknown(), + expectation: false, + }, + "null-null": { + input: NewFloat32Null(), + candidate: NewFloat32Null(), + expectation: true, + }, + } + for name, test := range tests { + name, test := name, test + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := test.input.Equal(test.candidate) + if !cmp.Equal(got, test.expectation) { + t.Errorf("Expected %v, got %v", test.expectation, got) + } + }) + } +} + +func TestFloat32ValueIsNull(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + input Float32Value + expected bool + }{ + "known": { + input: NewFloat32Value(2.4), + expected: false, + }, + "null": { + input: NewFloat32Null(), + expected: true, + }, + "unknown": { + input: NewFloat32Unknown(), + expected: false, + }, + } + + 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 TestFloat32ValueIsUnknown(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + input Float32Value + expected bool + }{ + "known": { + input: NewFloat32Value(2.4), + expected: false, + }, + "null": { + input: NewFloat32Null(), + expected: false, + }, + "unknown": { + input: NewFloat32Unknown(), + 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 TestFloat32ValueString(t *testing.T) { + t.Parallel() + + var lessThanOne float32 = 0.12340984302980000 + var moreThanOne float32 = 923879.32812 + var negativeLessThanOne float32 = -0.12340984302980000 + var negativeMoreThanOne float32 = -923879.32812 + var smallestNonZero float32 = math.SmallestNonzeroFloat32 + var largestFloat32 float32 = math.MaxFloat32 + + type testCase struct { + input Float32Value + expectation string + } + tests := map[string]testCase{ + "less-than-one": { + input: NewFloat32Value(lessThanOne), + expectation: "0.123410", + }, + "more-than-one": { + input: NewFloat32Value(moreThanOne), + expectation: "923879.312500", + }, + "negative-less-than-one": { + input: NewFloat32Value(negativeLessThanOne), + expectation: "-0.123410", + }, + "negative-more-than-one": { + input: NewFloat32Value(negativeMoreThanOne), + expectation: "-923879.312500", + }, + "min-float32": { + input: NewFloat32Value(smallestNonZero), + expectation: "0.000000", + }, + "max-float32": { + input: NewFloat32Value(largestFloat32), + expectation: "340282346638528859811704183484516925440.000000", + }, + "unknown": { + input: NewFloat32Unknown(), + expectation: "", + }, + "null": { + input: NewFloat32Null(), + expectation: "", + }, + "zero-value": { + input: Float32Value{}, + expectation: "", + }, + } + + for name, test := range tests { + name, test := name, test + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := test.input.String() + if !cmp.Equal(got, test.expectation) { + t.Errorf("Expected %q, got %q", test.expectation, got) + } + }) + } +} + +func TestFloat32ValueValueFloat32(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + input Float32Value + expected float32 + }{ + "known": { + input: NewFloat32Value(2.4), + expected: 2.4, + }, + "null": { + input: NewFloat32Null(), + expected: 0.0, + }, + "unknown": { + input: NewFloat32Unknown(), + expected: 0.0, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.input.ValueFloat32() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat32ValueValueFloat32Pointer(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + input Float32Value + expected *float32 + }{ + "known": { + input: NewFloat32Value(2.4), + expected: pointer(float32(2.4)), + }, + "null": { + input: NewFloat32Null(), + expected: nil, + }, + "unknown": { + input: NewFloat32Unknown(), + expected: pointer(float32(0.0)), + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.input.ValueFloat32Pointer() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNewFloat32PointerValue(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + value *float32 + expected Float32Value + }{ + "nil": { + value: nil, + expected: NewFloat32Null(), + }, + "value": { + value: pointer(float32(1.2)), + expected: NewFloat32Value(1.2), + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := NewFloat32PointerValue(testCase.value) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat32ValueFloat32SemanticEquals(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + currentFloat32 Float32Value + givenFloat32 Float32Value + expectedMatch bool + expectedDiags diag.Diagnostics + }{ + "not equal - whole number": { + currentFloat32: NewFloat32Value(1), + givenFloat32: NewFloat32Value(2), + expectedMatch: false, + }, + "not equal - float": { + currentFloat32: NewFloat32Value(1.1), + givenFloat32: NewFloat32Value(1.2), + expectedMatch: false, + }, + "not equal - float differing precision": { + currentFloat32: Float32Value{ + state: attr.ValueStateKnown, + value: testMustParseFloat("0.01"), + }, + givenFloat32: NewFloat32Value(0.02), + expectedMatch: false, + }, + "semantically equal - whole number": { + currentFloat32: NewFloat32Value(1), + givenFloat32: NewFloat32Value(1), + expectedMatch: true, + }, + "semantically equal - float": { + currentFloat32: NewFloat32Value(1.1), + givenFloat32: NewFloat32Value(1.1), + expectedMatch: true, + }, + "semantically equal - float differing precision": { + currentFloat32: Float32Value{ + state: attr.ValueStateKnown, + value: testMustParseFloat("0.01"), + }, + givenFloat32: NewFloat32Value(0.01), + expectedMatch: true, + }, + // Only 53 bits of precision are compared, Go built-in float32 + "semantically equal - float 512 precision, different value not significant": { + currentFloat32: Float32Value{ + state: attr.ValueStateKnown, + value: testMustParseFloat("0.010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001"), + }, + givenFloat32: Float32Value{ + state: attr.ValueStateKnown, + value: testMustParseFloat("0.010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000009"), + }, + expectedMatch: true, + }, + } + for name, testCase := range testCases { + name, testCase := name, testCase + t.Run(name, func(t *testing.T) { + t.Parallel() + + match, diags := testCase.currentFloat32.Float32SemanticEquals(context.Background(), testCase.givenFloat32) + + if testCase.expectedMatch != match { + t.Errorf("Expected Float32SemanticEquals to return: %t, but got: %t", testCase.expectedMatch, match) + } + + if diff := cmp.Diff(diags, testCase.expectedDiags); diff != "" { + t.Errorf("Unexpected diagnostics (-got, +expected): %s", diff) + } + }) + } +} diff --git a/types/float32_type.go b/types/float32_type.go new file mode 100644 index 00000000..19259af1 --- /dev/null +++ b/types/float32_type.go @@ -0,0 +1,8 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package types + +import "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + +var Float32Type = basetypes.Float32Type{} diff --git a/types/float32_value.go b/types/float32_value.go new file mode 100644 index 00000000..a244ea45 --- /dev/null +++ b/types/float32_value.go @@ -0,0 +1,31 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package types + +import "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + +type Float32 = basetypes.Float32Value + +// Float32Null creates a Float32 with a null value. Determine whether the value is +// null via the Float32 type IsNull method. +func Float32Null() basetypes.Float32Value { + return basetypes.NewFloat32Null() +} + +// Float32Unknown creates a Float32 with an unknown value. Determine whether the +// value is unknown via the Float32 type IsUnknown method. +func Float32Unknown() basetypes.Float32Value { + return basetypes.NewFloat32Unknown() +} + +// Float32Value creates a Float32 with a known value. Access the value via the Float32 +// type ValueFloat32 method. +func Float32Value(value float32) basetypes.Float32Value { + return basetypes.NewFloat32Value(value) +} + +// Float32PointerValue creates a Float32 with a null value if nil or a known value. +func Float32PointerValue(value *float32) basetypes.Float32Value { + return basetypes.NewFloat32PointerValue(value) +} diff --git a/website/data/plugin-framework-nav-data.json b/website/data/plugin-framework-nav-data.json index 27838ab6..b1fb0c9a 100644 --- a/website/data/plugin-framework-nav-data.json +++ b/website/data/plugin-framework-nav-data.json @@ -152,6 +152,10 @@ "title": "Dynamic", "path": "functions/parameters/dynamic" }, + { + "title": "Float32", + "path": "functions/parameters/float32" + }, { "title": "Float64", "path": "functions/parameters/float64" @@ -205,6 +209,10 @@ "title": "Dynamic", "path": "functions/returns/dynamic" }, + { + "title": "Float32", + "path": "functions/returns/float32" + }, { "title": "Float64", "path": "functions/returns/float64" @@ -283,6 +291,10 @@ "title": "Dynamic", "path": "handling-data/attributes/dynamic" }, + { + "title": "Float32", + "path": "handling-data/attributes/float32" + }, { "title": "Float64", "path": "handling-data/attributes/float64" @@ -373,6 +385,10 @@ "title": "Dynamic", "path": "handling-data/types/dynamic" }, + { + "title": "Float32", + "path": "handling-data/types/float32" + }, { "title": "Float64", "path": "handling-data/types/float64" diff --git a/website/docs/plugin/framework/functions/parameters/float32.mdx b/website/docs/plugin/framework/functions/parameters/float32.mdx new file mode 100644 index 00000000..f41a8cb3 --- /dev/null +++ b/website/docs/plugin/framework/functions/parameters/float32.mdx @@ -0,0 +1,101 @@ +--- +page_title: 'Plugin Development - Framework: Float32 Function Parameter' +description: >- + Learn the float32 function parameter type in the provider development framework. +--- + +# Float32 Function Parameter + + + +Use [Int32 Parameter](/terraform/plugin/framework/functions/parameters/int32) for 32-bit integer numbers. Use [Number Parameter](/terraform/plugin/framework/functions/parameters/number) for arbitrary precision numbers. + + + +Float32 function parameters expect a 32-bit floating point number value from a practitioner configuration. Values are accessible in function logic by the Go built-in `float32` type, Go built-in `*float32` type, or the [framework float32 type](/terraform/plugin/framework/handling-data/types/float32). + +In this Terraform configuration example, a float32 parameter is set to the value `1.23`: + +```hcl +provider::example::example(1.23) +``` + +## Function Definition + +Use the [`function.Float32Parameter` type](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/function#Float32Parameter) in the [function definition](/terraform/plugin/framework/functions/implementation#definition-method) to accept a float32 value. + +In this example, a function definition includes a first position float32 parameter: + +```go +func (f ExampleFunction) Definition(ctx context.Context, req function.DefinitionRequest, resp *function.DefinitionResponse) { + resp.Definition = function.Definition{ + // ... other Definition fields ... + Parameters: []function.Parameter{ + function.Float32Parameter{ + Name: "float32_param", + // ... potentially other Float32Parameter fields ... + }, + }, + } +} +``` + +If the float32 value should be the element type of a [collection parameter type](/terraform/plugin/framework/functions/parameters#collection-parameter-types), set the `ElementType` field according to the [framework float32 type](/terraform/plugin/framework/handling-data/types/float32). Refer to the collection parameter type documentation for additional details. + +If the float32 value should be a value type of an [object parameter type](/terraform/plugin/framework/functions/parameters#object-parameter-type), set the `AttributeTypes` map value according to the [framework float32 type](/terraform/plugin/framework/handling-data/types/float32). Refer to the object parameter type documentation for additional details. + +### Allow Null Values + +By default, Terraform will not pass null values to the function logic. Use the `AllowNullValue` field to explicitly allow null values, if there is a meaningful distinction that should occur in function logic. Enabling `AllowNullValue` requires using a Go pointer type or [framework float32 type](/terraform/plugin/framework/handling-data/types/float32) when reading argument data. + +### Allow Unknown Values + +By default, Terraform will not pass unknown values to the function logic. Use the `AllowUnknownValues` field to explicitly allow unknown values, if there is a meaningful distinction that should occur in function logic. Enabling `AllowUnknownValues` requires using a [framework float32 type](/terraform/plugin/framework/handling-data/types/float32) when reading argument data. + +### Custom Types + +You may want to build your own data value and type implementations to allow your provider to combine validation and other behaviors into a reusable bundle. This helps avoid duplication and ensures consistency. These implementations use the `CustomType` field in the parameter type. + +Refer to [Custom Types](/terraform/plugin/framework/handling-data/types/custom) for further details on creating provider-defined types and values. + +### Documentation + +Refer to [function documentation](/terraform/plugin/framework/functions/documentation) for information about the `Name`, `Description`, and `MarkdownDescription` fields available. + +## Reading Argument Data + +The [function implementation](/terraform/plugin/framework/functions/implementation) documentation covers the general methods for reading function argument data in function logic. + +When retrieving the argument value for this parameter: + +* If `CustomType` is set, use its associated value type. +* If `AllowUnknownValues` is enabled, you must use the [framework float32 type](/terraform/plugin/framework/handling-data/types/float32). +* If `AllowNullValue` is enabled, you must use the Go built-in `*float32` type or [framework float32 type](/terraform/plugin/framework/handling-data/types/float32). +* Otherwise, use the Go built-in `float32` type, Go built-in `*float32` type, or [framework float32 type](/terraform/plugin/framework/handling-data/types/float32). + +In this example, a function defines a single float32 parameter and accesses its argument value: + +```go +func (f ExampleFunction) Definition(ctx context.Context, req function.DefinitionRequest, resp *function.DefinitionResponse) { + resp.Definition = function.Definition{ + // ... other Definition fields ... + Parameters: []function.Parameter{ + function.Float32Parameter{ + Name: "float32_param", + // ... potentially other Float32Parameter fields ... + }, + }, + } +} + +func (f ExampleFunction) Run(ctx context.Context, req function.RunRequest, resp *function.RunResponse) { + var float32Arg float32 + // var float32Arg *float32 // e.g. with AllowNullValue, where Go nil equals Terraform null + // var float32Arg types.Float32 // e.g. with AllowUnknownValues or AllowNullValue + + resp.Error = function.ConcatFuncErrors(resp.Error, req.Arguments.Get(ctx, &float32Arg)) + + // float32Arg is now populated + // ... other logic ... +} +``` diff --git a/website/docs/plugin/framework/functions/parameters/index.mdx b/website/docs/plugin/framework/functions/parameters/index.mdx index 5382ab89..65fb61e4 100644 --- a/website/docs/plugin/framework/functions/parameters/index.mdx +++ b/website/docs/plugin/framework/functions/parameters/index.mdx @@ -25,6 +25,7 @@ Parameter types that accepts a single data value, such as a boolean, number, or | Parameter Type | Use Case | |----------------|----------| | [Bool](/terraform/plugin/framework/functions/parameters/bool) | Boolean true or false | +| [Float32](/terraform/plugin/framework/functions/parameters/float32) | 32-bit floating point number | | [Float64](/terraform/plugin/framework/functions/parameters/float64) | 64-bit floating point number | | [Int32](/terraform/plugin/framework/functions/parameters/int32) | 32-bit integer number | | [Int64](/terraform/plugin/framework/functions/parameters/int64) | 64-bit integer number | diff --git a/website/docs/plugin/framework/functions/returns/float32.mdx b/website/docs/plugin/framework/functions/returns/float32.mdx new file mode 100644 index 00000000..b4688bb4 --- /dev/null +++ b/website/docs/plugin/framework/functions/returns/float32.mdx @@ -0,0 +1,71 @@ +--- +page_title: 'Plugin Development - Framework: Float32 Function Return' +description: >- + Learn the float32 function return type in the provider development framework. +--- + +# Float32 Function Return + + + +Use [Int32 Return](/terraform/plugin/framework/functions/returns/int32) for 32-bit integer numbers. Use [Number Return](/terraform/plugin/framework/functions/returns/number) for arbitrary precision numbers. + + + +Float32 function return expects a 32-bit floating point number value from function logic. Set values in function logic with the Go built-in `float32` type, Go built-in `*float32` type, or the [framework float32 type](/terraform/plugin/framework/handling-data/types/float32). + +## Function Definition + +Use the [`function.Float32Return` type](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/function#Float32Return) in the [function definition](/terraform/plugin/framework/functions/implementation#definition-method). + +In this example, a function definition includes a float32 return: + +```go +func (f ExampleFunction) Definition(ctx context.Context, req function.DefinitionRequest, resp *function.DefinitionResponse) { + resp.Definition = function.Definition{ + // ... other Definition fields ... + Return: function.Float32Return{ + // ... potentially other Float32Return fields ... + }, + } +} +``` + +### Custom Types + +You may want to build your own data value and type implementations to allow your provider to combine validation and other behaviors into a reusable bundle. This helps avoid duplication and ensures consistency. These implementations use the `CustomType` field in the return type. + +Refer to [Custom Types](/terraform/plugin/framework/handling-data/types/custom) for further details on creating provider-defined types and values. + +### Documentation + +Return documentation is expected in the top-level function documentation. Refer to [function documentation](/terraform/plugin/framework/functions/documentation) for information about the `Summary`, `Description`, and `MarkdownDescription` fields available. + +## Setting Return Data + +The [function implementation](/terraform/plugin/framework/functions/implementation) documentation covers the general methods for setting function return data in function logic. + +When setting the value for this return: + +* If `CustomType` is set, use its associated value type. +* Otherwise, use the Go built-in `float32` type, Go built-in `*float32` type, or [framework float32 type](/terraform/plugin/framework/handling-data/types/float32). + +In this example, a function defines a float32 return and sets its value: + +```go +func (f ExampleFunction) Definition(ctx context.Context, req function.DefinitionRequest, resp *function.DefinitionResponse) { + resp.Definition = function.Definition{ + // ... other Definition fields ... + Return: function.Float32Return{}, + } +} + +func (f ExampleFunction) Run(ctx context.Context, req function.RunRequest, resp *function.RunResponse) { + // ... other logic ... + + // hardcoded value for example brevity + var result float32 = 1.23 + + resp.Error = function.ConcatFuncErrors(resp.Error, resp.Result.Set(ctx, &result)) +} +``` diff --git a/website/docs/plugin/framework/functions/returns/index.mdx b/website/docs/plugin/framework/functions/returns/index.mdx index 7a924451..8a9dd6f1 100644 --- a/website/docs/plugin/framework/functions/returns/index.mdx +++ b/website/docs/plugin/framework/functions/returns/index.mdx @@ -25,8 +25,9 @@ Return types that expect a single data value, such as a boolean, number, or stri | Return Type | Use Case | |----------------|----------| | [Bool](/terraform/plugin/framework/functions/returns/bool) | Boolean true or false | +| [Float32](/terraform/plugin/framework/functions/returns/float32) | 32-bit floating point number | | [Float64](/terraform/plugin/framework/functions/returns/float64) | 64-bit floating point number | -| [Int64](/terraform/plugin/framework/functions/returns/int32) | 32-bit integer number | +| [Int32](/terraform/plugin/framework/functions/returns/int32) | 32-bit integer number | | [Int64](/terraform/plugin/framework/functions/returns/int64) | 64-bit integer number | | [Number](/terraform/plugin/framework/functions/returns/number) | Arbitrary precision (generally over 64-bit, up to 512-bit) number | | [String](/terraform/plugin/framework/functions/returns/string) | Collection of UTF-8 encoded characters | diff --git a/website/docs/plugin/framework/handling-data/attributes/float32.mdx b/website/docs/plugin/framework/handling-data/attributes/float32.mdx new file mode 100644 index 00000000..0d794db9 --- /dev/null +++ b/website/docs/plugin/framework/handling-data/attributes/float32.mdx @@ -0,0 +1,126 @@ +--- +page_title: 'Plugin Development - Framework: Float32 Attribute' +description: >- + Learn the float32 attribute type in the provider development framework. +--- + +# Float32 Attribute + + + +Use [Int32 Attribute](/terraform/plugin/framework/handling-data/attributes/int32) for 32-bit integer numbers. Use [Number Attribute](/terraform/plugin/framework/handling-data/attributes/number) for arbitrary precision numbers. + + + +Float32 attributes store a 32-bit floating point number. Values are represented by a [float32 type](/terraform/plugin/framework/handling-data/types/float32) in the framework. + +In this Terraform configuration example, a float32 attribute named `example_attribute` is set to the value `1.23`: + +```hcl +resource "examplecloud_thing" "example" { + example_attribute = 1.23 +} +``` + +## Schema Definition + +Use one of the following attribute types to directly add a float32 value to a [schema](/terraform/plugin/framework/handling-data/schemas) or [nested attribute type](/terraform/plugin/framework/handling-data/attributes#nested-attribute-types): + +| Schema Type | Attribute Type | +|-------------|----------------| +| [Data Source](/terraform/plugin/framework/data-sources) | [`schema.Float32Attribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/datasource/schema#Float32Attribute) | +| [Provider](/terraform/plugin/framework/provider) | [`schema.Float32Attribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/provider/schema#Float32Attribute) | +| [Resource](/terraform/plugin/framework/resources) | [`schema.Float32Attribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/resource/schema#Float32Attribute) | + +In this example, a resource schema defines a top level required float32 attribute named `example_attribute`: + +```go +func (r ThingResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "example_attribute": schema.Float32Attribute{ + Required: true, + // ... potentially other fields ... + }, + // ... potentially other attributes ... + }, + } +} +``` + +If the float32 value should be the element type of a [collection attribute type](/terraform/plugin/framework/handling-data/attributes#collection-attribute-types), set the `ElementType` field according to the [float32 type](/terraform/plugin/framework/handling-data/types/float32). Refer to the collection attribute type documentation for additional details. + +If the float32 value should be a value type of an [object attribute type](/terraform/plugin/framework/handling-data/attributes#object-attribute-type), set the `AttributeTypes` map value according to the [float32 type](/terraform/plugin/framework/handling-data/types/float32). Refer to the object attribute type documentation for additional details. + +### Configurability + +At least one of the `Computed`, `Optional`, or `Required` fields must be set to `true`. This defines how Terraform and the framework should expect data to set, whether the value is from the practitioner configuration or from the provider logic, such as API response value. + +The acceptable behaviors of these configurability options are: + +- `Required` only: The value must be practitioner configured to an eventually known value (not null), otherwise the framework will automatically raise an error diagnostic for the missing value. +- `Optional` only: The value may be practitioner configured to a known value or null. +- `Optional` and `Computed`: The value may be practitioner configured or the value may be set in provider logic when the practitioner configuration is null. +- `Computed` only: The value will be set in provider logic and any practitioner configuration causes the framework to automatically raise an error diagnostic for the unexpected configuration value. + +### Custom Types + +You may want to build your own attribute value and type implementations to allow your provider to combine validation, description, and plan customization behaviors into a reusable bundle. This helps avoid duplication or reimplementation and ensures consistency. These implementations use the `CustomType` field in the attribute type. + +Refer to [Custom Types](/terraform/plugin/framework/handling-data/types/custom) for further details on creating provider-defined types and values. + +### Deprecation + +Set the `DeprecationMessage` field to a practitioner-focused message for how to handle the deprecation. The framework will automatically raise a warning diagnostic with this message if the practitioner configuration contains a known value for the attribute. Terraform version 1.2.7 and later will raise a warning diagnostic in certain scenarios if the deprecated attribute value is referenced elsewhere in a practitioner configuration. The framework [deprecations](/terraform/plugin/framework/deprecations) documentation fully describes the recommended practices for deprecating an attribute or resource. + +Some practitioner-focused examples of a deprecation message include: + +- Configure `other_attribute` instead. This attribute will be removed in the next major version of the provider. +- Remove this attribute's configuration as it no longer is used and the attribute will be removed in the next major version of the provider. + +### Description + +The framework provides two description fields, `Description` and `MarkdownDescription`, which various tools use to show additional information about an attribute and its intended purpose. This includes, but is not limited to, [`terraform-plugin-docs`](https://github.com/hashicorp/terraform-plugin-docs) for automated provider documentation generation and [`terraform-ls`](https://github.com/hashicorp/terraform-ls) for Terraform configuration editor integrations. + +### Plan Modification + + + +Only managed resources implement this concept. + + + +The framework provides two plan modification fields for managed resource attributes, `Default` and `PlanModifiers`, which define resource and attribute value planning behaviors. The resource [default](/terraform/plugin/framework/resources/default) and [plan modification](/terraform/plugin/framework/resources/plan-modification) documentation covers these features more in-depth. + +#### Common Use Case Plan Modification + +The [`float32default`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/resource/schema/float32default) package defines common use case `Default` implementations: + +- [`StaticFloat32(float32)`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/resource/schema/float32default#StaticFloat32): Define a static float32 default value for the attribute. + +The [`float32planmodifier`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/resource/schema/float32planmodifier) package defines common use case `PlanModifiers` implementations: + +- [`RequiresReplace()`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/resource/schema/float32planmodifier#RequiresReplace): Marks the resource for replacement if the resource is being updated and the plan value does not match the prior state value. +- [`RequiresReplaceIf()`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/resource/schema/float32planmodifier#RequiresReplaceIf): Similar to `RequiresReplace()`, but also checks if a given function returns true. +- [`RequiresReplaceIfConfigured()`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/resource/schema/float32planmodifier#RequiresReplaceIfConfigured): Similar to `RequiresReplace()`, but also checks if the configuration value is not null. +- [`UseStateForUnknown()`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/resource/schema/float32planmodifier#UseStateForUnknown): Copies a known prior state value into the planned value. Use this when it is known that an unconfigured value will remain the same after a resource update. + +### Sensitive + +Set the `Sensitive` field if the attribute value should always be considered [sensitive data](/terraform/language/state/sensitive-data). In Terraform, this will generally mask the value in practitioner output. This setting cannot be conditionally set and does not impact how data is stored in the state. + +### Validation + +Set the `Validators` field to define [validation](/terraform/plugin/framework/validation#attribute-validation). This validation logic is ran in addition to any validation contained within a [custom type](#custom-types). + +#### Common Use Case Validators + +HashiCorp provides the additional [`terraform-plugin-framework-validators`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework-validators) Go module which contains validation logic for common use cases. The [`float32validator`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework-validators/float32validator) package within that module has float32 attribute validators such as minimum, maximum, and defining conflicting attributes. + +## Accessing Values + +The [accessing values](/terraform/plugin/framework/handling-data/accessing-values) documentation covers general methods for reading [schema](/terraform/plugin/framework/handling-data/schemas) (configuration, plan, and state) data, which is necessary before accessing an attribute value directly. The [float32 type](/terraform/plugin/framework/handling-data/types/float32#accessing-values) documentation covers methods for interacting with the attribute value itself. + +## Setting Values + +The [float32 type](/terraform/plugin/framework/handling-data/types/float32#setting-values) documentation covers methods for creating or setting the appropriate value. The [writing data](/terraform/plugin/framework/handling-data/writing-state) documentation covers general methods for writing [schema](/terraform/plugin/framework/handling-data/schemas) (plan and state) data, which is necessary afterwards. diff --git a/website/docs/plugin/framework/handling-data/attributes/index.mdx b/website/docs/plugin/framework/handling-data/attributes/index.mdx index 877d5fc8..0424c858 100644 --- a/website/docs/plugin/framework/handling-data/attributes/index.mdx +++ b/website/docs/plugin/framework/handling-data/attributes/index.mdx @@ -26,6 +26,7 @@ Attribute types that contain a single data value, such as a boolean, number, or | Attribute Type | Use Case | |----------------|----------| | [Bool](/terraform/plugin/framework/handling-data/attributes/bool) | Boolean true or false | +| [Float32](/terraform/plugin/framework/handling-data/attributes/float32) | 32-bit floating point number | | [Float64](/terraform/plugin/framework/handling-data/attributes/float64) | 64-bit floating point number | | [Int32](/terraform/plugin/framework/handling-data/attributes/int32) | 32-bit integer number | | [Int64](/terraform/plugin/framework/handling-data/attributes/int64) | 64-bit integer number | diff --git a/website/docs/plugin/framework/handling-data/paths.mdx b/website/docs/plugin/framework/handling-data/paths.mdx index 176fc8cb..f08cbec1 100644 --- a/website/docs/plugin/framework/handling-data/paths.mdx +++ b/website/docs/plugin/framework/handling-data/paths.mdx @@ -89,6 +89,7 @@ The following table shows the different [`path.Path` type](https://pkg.go.dev/gi |---------------------------|--------------------------------------------------------------------------------------------------------| | `schema.BoolAttribute` | N/A | | `schema.DynamicAttribute` | N/A | +| `schema.Float32Attribute` | N/A | | `schema.Float64Attribute` | N/A | | `schema.Int32Attribute` | N/A | | `schema.Int64Attribute` | N/A | diff --git a/website/docs/plugin/framework/handling-data/types/custom.mdx b/website/docs/plugin/framework/handling-data/types/custom.mdx index e6f44bf0..9632d9bb 100644 --- a/website/docs/plugin/framework/handling-data/types/custom.mdx +++ b/website/docs/plugin/framework/handling-data/types/custom.mdx @@ -91,6 +91,7 @@ The commonly used `types` package types are aliases to the `basetypes` package t | --- | --- | | [`basetypes.BoolType`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types/basetypes#BoolType) | [`basetypes.BoolTypable`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types/basetypes#BoolTypable) | | [`basetypes.DynamicType`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types/basetypes#DynamicType) | [`basetypes.DynamicTypable`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types/basetypes#DynamicTypable) | +| [`basetypes.Float32Type`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types/basetypes#Float32Type) | [`basetypes.Float32Typable`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types/basetypes#Float32Typable) | | [`basetypes.Float64Type`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types/basetypes#Float64Type) | [`basetypes.Float64Typable`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types/basetypes#Float64Typable) | | [`basetypes.Int32Type`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types/basetypes#Int32Type) | [`basetypes.Int32Typable`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types/basetypes#Int32Typable) | | [`basetypes.Int64Type`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types/basetypes#Int64Type) | [`basetypes.Int64Typable`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types/basetypes#Int64Typable) | diff --git a/website/docs/plugin/framework/handling-data/types/float32.mdx b/website/docs/plugin/framework/handling-data/types/float32.mdx new file mode 100644 index 00000000..abc351d3 --- /dev/null +++ b/website/docs/plugin/framework/handling-data/types/float32.mdx @@ -0,0 +1,120 @@ +--- +page_title: 'Plugin Development - Framework: Float32 Type' +description: >- + Learn the float32 value type in the provider development framework. +--- + +# Float32 Type + + + +Use [Int32 Type](/terraform/plugin/framework/handling-data/types/int32) for 32-bit integer numbers. Use [Number Attribute](/terraform/plugin/framework/handling-data/types/number) for arbitrary precision numbers. + + + +Float32 types store a 32-bit floating point number. + +By default, float32 from [schema](/terraform/plugin/framework/handling-data/schemas) (configuration, plan, and state) data are represented in the framework by [`types.Float32Type`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types#Float32Type) and its associated value storage type of [`types.Float32`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types#Float32). These types fully support Terraform's [type system concepts](/terraform/plugin/framework/handling-data/terraform-concepts) that cannot be represented in Go built-in types, such as `*float32`. Framework types can be [extended](#extending) by provider code or shared libraries to provide specific use case functionality. + +## Schema Definitions + +Use one of the following attribute types to directly add a float32 value to a [schema](/terraform/plugin/framework/handling-data/schemas) or [nested attribute type](/terraform/plugin/framework/handling-data/attributes#nested-attribute-types): + +| Schema Type | Attribute Type | +|-------------|----------------| +| [Data Source](/terraform/plugin/framework/data-sources) | [`schema.Float32Attribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/datasource/schema#Float32Attribute) | +| [Provider](/terraform/plugin/framework/provider) | [`schema.Float32Attribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/provider/schema#Float32Attribute) | +| [Resource](/terraform/plugin/framework/resources) | [`schema.Float32Attribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/resource/schema#Float32Attribute) | + +If the float32 value should be the element type of a [collection attribute type](/terraform/plugin/framework/handling-data/attributes#collection-attribute-types), set the `ElemType` field to `types.Float32Type` or the appropriate [custom type](#extending). + +If the float32 value should be a value type of an [object attribute type](/terraform/plugin/framework/handling-data/attributes#object-attribute-type), set the `AttrTypes` map value to `types.Float32Type` or the appropriate [custom type](#extending). + +## Accessing Values + + + +Review the [attribute documentation](/terraform/plugin/framework/handling-data/attributes/float32#accessing-values) to understand how schema-based data gets mapped into accessible values, such as a `types.Float32` in this case. + + + +Access `types.Float32` information via the following methods: + +* [`(types.Float32).IsNull() bool`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types/basetypes#Float32Value.IsNull): Returns true if the float32 is null. +* [`(types.Float32).IsUnknown() bool`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types/basetypes#Float32Value.IsUnknown): Returns true if the float32 is unknown. +* [`(types.Float32).ValueFloat32() float32`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types/basetypes#Float32Value.ValueFloat32): Returns the known float32, or `0.0` if null or unknown. +* [`(types.Float32).ValueFloat32Pointer() *float32`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types/basetypes#Float32Value.ValueFloat32Pointer): Returns a float32 pointer to a known value, `nil` if null, or a pointer to `0.0` if unknown. + +In this example, a float32 value is checked for being null or unknown value first, before accessing its known value: + +```go +// Example data model definition +// type ExampleModel struct { +// ExampleAttribute types.Float32 `tfsdk:"example_attribute"` +// } +// +// This would be filled in, such as calling: req.Plan.Get(ctx, &data) +var data ExampleModel + +// optional logic for handling null value +if data.ExampleAttribute.IsNull() { + // ... +} + +// optional logic for handling unknown value +if data.ExampleAttribute.IsUnknown() { + // ... +} + +// myFloat32 now contains a Go float32 with the known value +myFloat32 := data.ExampleAttribute.ValueFloat32() +``` + +## Setting Values + +Call one of the following to create a `types.Float32` value: + +* [`types.Float32Null()`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types#Float32Null): A null float32 value. +* [`types.Float32Unknown()`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types#Float32Unknown): An unknown float32 value. +* [`types.Float32Value(float32)`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types#Float32Value): A known value. +* [`types.Float32PointerValue(*float32)`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types#Float32PointerValue): A known value. + +In this example, a known float32 value is created: + +```go +types.Float32Value(1.23) +``` + +Otherwise, for certain framework functionality that does not require `types` implementations directly, such as: + +* [`(tfsdk.State).SetAttribute()`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/tfsdk#State.SetAttribute) +* [`types.ListValueFrom()`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types#ListValueFrom) +* [`types.MapValueFrom()`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types#MapValueFrom) +* [`types.ObjectValueFrom()`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types#ObjectValueFrom) +* [`types.SetValueFrom()`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types#SetValueFrom) + +Numbers can be automatically converted from the following Go types, pointers to these types, or any aliases of these types, such `type MyNumber int`: + +* `int`, `int8`, `int16`, `int32`, `int64` +* `uint`, `uint8`, `uint16`, `uint32`, `uint64` +* `float32`, `float64` +* [`*big.Int`](https://pkg.go.dev/math/big#Int), [`*big.Float`](https://pkg.go.dev/math/big#Float) + +An error will be returned if the value of the number cannot be stored in the numeric type supplied because of an overflow or other loss of precision. + +In this example, a `float32` is directly used to set a float32 attribute value: + +```go +var value float32 = 1.23 +diags := resp.State.SetAttribute(ctx, path.Root("example_attribute"), value) +``` + +In this example, a `types.List` of `types.Float32` is created from a `[]float32`: + +```go +listValue, diags := types.ListValueFrom(ctx, types.Float32Type, []float32{1.2, 2.4}) +``` + +## Extending + +The framework supports extending its base type implementations with [custom types](/terraform/plugin/framework/handling-data/types/custom). These can adjust expected provider code usage depending on their implementation. diff --git a/website/docs/plugin/framework/handling-data/types/index.mdx b/website/docs/plugin/framework/handling-data/types/index.mdx index 17a7d256..8f0b3ead 100644 --- a/website/docs/plugin/framework/handling-data/types/index.mdx +++ b/website/docs/plugin/framework/handling-data/types/index.mdx @@ -26,6 +26,7 @@ Types that contain a single data value, such as a boolean, number, or string. Th | Type | Use Case | |----------------|----------| | [Bool](/terraform/plugin/framework/handling-data/types/bool) | Boolean true or false | +| [Float32](/terraform/plugin/framework/handling-data/types/float32) | 32-bit floating point number | | [Float64](/terraform/plugin/framework/handling-data/types/float64) | 64-bit floating point number | | [Int32](/terraform/plugin/framework/handling-data/types/int32) | 32-bit integer number | | [Int64](/terraform/plugin/framework/handling-data/types/int64) | 64-bit integer number | diff --git a/website/docs/plugin/framework/resources/default.mdx b/website/docs/plugin/framework/resources/default.mdx index 84ec283a..2787a5e8 100644 --- a/website/docs/plugin/framework/resources/default.mdx +++ b/website/docs/plugin/framework/resources/default.mdx @@ -50,6 +50,7 @@ The framework implements static value defaults in the typed packages under `reso |---|---| | [`schema.BoolAttribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/resource/schema#BoolAttribute) | [`resource/schema/booldefault` package](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault) | | [`schema.DynamicAttribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/resource/schema#DynamicAttribute) | [`resource/schema/dynamicdefault` package](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/resource/schema/dynamicdefault) | +| [`schema.Float32Attribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/resource/schema#Float32Attribute) | [`resource/schema/float32default` package](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/resource/schema/float32default) | | [`schema.Float64Attribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/resource/schema#Float64Attribute) | [`resource/schema/float64default` package](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/resource/schema/float64default) | | [`schema.Int32Attribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/resource/schema#Int32Attribute) | [`resource/schema/int32default` package](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/resource/schema/int32default) | | [`schema.Int64Attribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/resource/schema#Int64Attribute) | [`resource/schema/int64default` package](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default) |