diff --git a/.changes/unreleased/FEATURES-20240621-132125.yaml b/.changes/unreleased/FEATURES-20240621-132125.yaml new file mode 100644 index 000000000..3c06ca4ca --- /dev/null +++ b/.changes/unreleased/FEATURES-20240621-132125.yaml @@ -0,0 +1,6 @@ +kind: FEATURES +body: 'types/basetypes: Added `Int32Type` and `Int32Value` implementations for Int32 + value handling.' +time: 2024-06-21T13:21:25.992757-04:00 +custom: + Issue: "1010" diff --git a/.changes/unreleased/FEATURES-20240621-132243.yaml b/.changes/unreleased/FEATURES-20240621-132243.yaml new file mode 100644 index 000000000..459f28831 --- /dev/null +++ b/.changes/unreleased/FEATURES-20240621-132243.yaml @@ -0,0 +1,7 @@ +kind: FEATURES +body: 'types/basetypes: Added interfaces `basetypes.Int32Typable`, `basetypes.Int32Valuable`, + and `basetypes.Int32ValuableWithSemanticEquals` for Int32 custom type and value + implementations.' +time: 2024-06-21T13:22:43.724425-04:00 +custom: + Issue: "1010" diff --git a/.changes/unreleased/FEATURES-20240621-132259.yaml b/.changes/unreleased/FEATURES-20240621-132259.yaml new file mode 100644 index 000000000..69ff192af --- /dev/null +++ b/.changes/unreleased/FEATURES-20240621-132259.yaml @@ -0,0 +1,5 @@ +kind: FEATURES +body: 'resource/schema: Added `Int32Attribute` implementation for Int32 value handling.' +time: 2024-06-21T13:22:59.609657-04:00 +custom: + Issue: "1010" diff --git a/.changes/unreleased/FEATURES-20240621-132322.yaml b/.changes/unreleased/FEATURES-20240621-132322.yaml new file mode 100644 index 000000000..fb374f2a4 --- /dev/null +++ b/.changes/unreleased/FEATURES-20240621-132322.yaml @@ -0,0 +1,5 @@ +kind: FEATURES +body: 'datasource/schema: Added `Int32Attribute` implementation for Int32 value handling.' +time: 2024-06-21T13:23:22.546135-04:00 +custom: + Issue: "1010" diff --git a/.changes/unreleased/FEATURES-20240621-132350.yaml b/.changes/unreleased/FEATURES-20240621-132350.yaml new file mode 100644 index 000000000..984763a03 --- /dev/null +++ b/.changes/unreleased/FEATURES-20240621-132350.yaml @@ -0,0 +1,5 @@ +kind: FEATURES +body: 'provider/schema: Added `Int32Attribute` implementation for Int32 value handling.' +time: 2024-06-21T13:23:50.972247-04:00 +custom: + Issue: "1010" diff --git a/.changes/unreleased/FEATURES-20240621-132410.yaml b/.changes/unreleased/FEATURES-20240621-132410.yaml new file mode 100644 index 000000000..03c9ffc87 --- /dev/null +++ b/.changes/unreleased/FEATURES-20240621-132410.yaml @@ -0,0 +1,5 @@ +kind: FEATURES +body: 'function: Added `Int32Parameter` and `Int32Return` for Int32 value handling.' +time: 2024-06-21T13:24:10.410418-04:00 +custom: + Issue: "1010" diff --git a/.changes/unreleased/FEATURES-20240621-132424.yaml b/.changes/unreleased/FEATURES-20240621-132424.yaml new file mode 100644 index 000000000..d63a0a64b --- /dev/null +++ b/.changes/unreleased/FEATURES-20240621-132424.yaml @@ -0,0 +1,6 @@ +kind: FEATURES +body: 'resource/schema/int32default: New package with `StaticValue` implementation + for Int32 schema-based default values.' +time: 2024-06-21T13:24:24.170126-04:00 +custom: + Issue: "1010" diff --git a/.changes/unreleased/FEATURES-20240621-132441.yaml b/.changes/unreleased/FEATURES-20240621-132441.yaml new file mode 100644 index 000000000..c85c52961 --- /dev/null +++ b/.changes/unreleased/FEATURES-20240621-132441.yaml @@ -0,0 +1,6 @@ +kind: FEATURES +body: 'resource/schema/int32planmodifier: New package with built-in implementations + for Int32 value plan modification.' +time: 2024-06-21T13:24:41.31229-04:00 +custom: + Issue: "1010" diff --git a/.changes/unreleased/FEATURES-20240621-132456.yaml b/.changes/unreleased/FEATURES-20240621-132456.yaml new file mode 100644 index 000000000..0501dfdff --- /dev/null +++ b/.changes/unreleased/FEATURES-20240621-132456.yaml @@ -0,0 +1,6 @@ +kind: FEATURES +body: 'resource/schema/defaults: New `Int32` interface for Int32 schema-based default + implementations.' +time: 2024-06-21T13:24:56.747674-04:00 +custom: + Issue: "1010" diff --git a/.changes/unreleased/FEATURES-20240621-132514.yaml b/.changes/unreleased/FEATURES-20240621-132514.yaml new file mode 100644 index 000000000..2a27e06ca --- /dev/null +++ b/.changes/unreleased/FEATURES-20240621-132514.yaml @@ -0,0 +1,6 @@ +kind: FEATURES +body: 'resource/schema/planmodifier: New `Int32` interface for Int32 value plan modification + implementations.' +time: 2024-06-21T13:25:14.959554-04:00 +custom: + Issue: "1010" diff --git a/.changes/unreleased/FEATURES-20240621-132528.yaml b/.changes/unreleased/FEATURES-20240621-132528.yaml new file mode 100644 index 000000000..833c1ec5b --- /dev/null +++ b/.changes/unreleased/FEATURES-20240621-132528.yaml @@ -0,0 +1,5 @@ +kind: FEATURES +body: 'schema/validator: New `Int32` interface for Int32 value schema validation.' +time: 2024-06-21T13:25:28.832045-04:00 +custom: + Issue: "1010" diff --git a/datasource/schema/int32_attribute.go b/datasource/schema/int32_attribute.go new file mode 100644 index 000000000..89f852e8c --- /dev/null +++ b/datasource/schema/int32_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 = Int32Attribute{} + _ fwxschema.AttributeWithInt32Validators = Int32Attribute{} +) + +// Int32Attribute represents a schema attribute that is a 32-bit integer. +// When retrieving the value for this attribute, use types.Int32 as the value +// type unless the CustomType field is set. +// +// Use Float32Attribute for 32-bit floating point number attributes or +// NumberAttribute for 512-bit generic number attributes. +// +// Terraform configurations configure this attribute using expressions that +// return a number or directly via an integer value. +// +// example_attribute = 123 +// +// Terraform configurations reference this attribute using the attribute name. +// +// .example_attribute +type Int32Attribute struct { + // CustomType enables the use of a custom attribute type in place of the + // default basetypes.Int32Type. When retrieving data, the basetypes.Int32Valuable + // associated with this custom type must be used in place of types.Int32. + CustomType basetypes.Int32Typable + + // 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.Int32 +} + +// ApplyTerraform5AttributePathStep always returns an error as it is not +// possible to step further into a Int32Attribute. +func (a Int32Attribute) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { + return a.GetType().ApplyTerraform5AttributePathStep(step) +} + +// Equal returns true if the given Attribute is a Int32Attribute +// and all fields are equal. +func (a Int32Attribute) Equal(o fwschema.Attribute) bool { + if _, ok := o.(Int32Attribute); !ok { + return false + } + + return fwschema.AttributesEqual(a, o) +} + +// GetDeprecationMessage returns the DeprecationMessage field value. +func (a Int32Attribute) GetDeprecationMessage() string { + return a.DeprecationMessage +} + +// GetDescription returns the Description field value. +func (a Int32Attribute) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription returns the MarkdownDescription field value. +func (a Int32Attribute) GetMarkdownDescription() string { + return a.MarkdownDescription +} + +// GetType returns types.Int32Type or the CustomType field value if defined. +func (a Int32Attribute) GetType() attr.Type { + if a.CustomType != nil { + return a.CustomType + } + + return types.Int32Type +} + +// Int32Validators returns the Validators field value. +func (a Int32Attribute) Int32Validators() []validator.Int32 { + return a.Validators +} + +// IsComputed returns the Computed field value. +func (a Int32Attribute) IsComputed() bool { + return a.Computed +} + +// IsOptional returns the Optional field value. +func (a Int32Attribute) IsOptional() bool { + return a.Optional +} + +// IsRequired returns the Required field value. +func (a Int32Attribute) IsRequired() bool { + return a.Required +} + +// IsSensitive returns the Sensitive field value. +func (a Int32Attribute) IsSensitive() bool { + return a.Sensitive +} diff --git a/datasource/schema/int32_attribute_test.go b/datasource/schema/int32_attribute_test.go new file mode 100644 index 000000000..889e3686d --- /dev/null +++ b/datasource/schema/int32_attribute_test.go @@ -0,0 +1,425 @@ +// 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/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func TestInt32AttributeApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int32Attribute + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + attribute: schema.Int32Attribute{}, + step: tftypes.AttributeName("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.AttributeName to basetypes.Int32Type"), + }, + "ElementKeyInt": { + attribute: schema.Int32Attribute{}, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyInt to basetypes.Int32Type"), + }, + "ElementKeyString": { + attribute: schema.Int32Attribute{}, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyString to basetypes.Int32Type"), + }, + "ElementKeyValue": { + attribute: schema.Int32Attribute{}, + step: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyValue to basetypes.Int32Type"), + }, + } + + 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 TestInt32AttributeGetDeprecationMessage(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int32Attribute + expected string + }{ + "no-deprecation-message": { + attribute: schema.Int32Attribute{}, + expected: "", + }, + "deprecation-message": { + attribute: schema.Int32Attribute{ + 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 TestInt32AttributeEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int32Attribute + other fwschema.Attribute + expected bool + }{ + "different-type": { + attribute: schema.Int32Attribute{}, + other: testschema.AttributeWithInt32Validators{}, + expected: false, + }, + "equal": { + attribute: schema.Int32Attribute{}, + other: schema.Int32Attribute{}, + 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 TestInt32AttributeGetDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int32Attribute + expected string + }{ + "no-description": { + attribute: schema.Int32Attribute{}, + expected: "", + }, + "description": { + attribute: schema.Int32Attribute{ + 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 TestInt32AttributeGetMarkdownDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int32Attribute + expected string + }{ + "no-markdown-description": { + attribute: schema.Int32Attribute{}, + expected: "", + }, + "markdown-description": { + attribute: schema.Int32Attribute{ + 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 TestInt32AttributeGetType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int32Attribute + expected attr.Type + }{ + "base": { + attribute: schema.Int32Attribute{}, + expected: types.Int32Type, + }, + // "custom-type": { + // attribute: schema.Int32Attribute{ + // CustomType: testtypes.Int32Type{}, + // }, + // expected: testtypes.Int32Type{}, + // }, + } + + 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 TestInt32AttributeInt32Validators(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int32Attribute + expected []validator.Int32 + }{ + "no-validators": { + attribute: schema.Int32Attribute{}, + expected: nil, + }, + "validators": { + attribute: schema.Int32Attribute{ + Validators: []validator.Int32{}, + }, + expected: []validator.Int32{}, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.Int32Validators() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt32AttributeIsComputed(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int32Attribute + expected bool + }{ + "not-computed": { + attribute: schema.Int32Attribute{}, + expected: false, + }, + "computed": { + attribute: schema.Int32Attribute{ + 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 TestInt32AttributeIsOptional(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int32Attribute + expected bool + }{ + "not-optional": { + attribute: schema.Int32Attribute{}, + expected: false, + }, + "optional": { + attribute: schema.Int32Attribute{ + 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 TestInt32AttributeIsRequired(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int32Attribute + expected bool + }{ + "not-required": { + attribute: schema.Int32Attribute{}, + expected: false, + }, + "required": { + attribute: schema.Int32Attribute{ + 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 TestInt32AttributeIsSensitive(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int32Attribute + expected bool + }{ + "not-sensitive": { + attribute: schema.Int32Attribute{}, + expected: false, + }, + "sensitive": { + attribute: schema.Int32Attribute{ + 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/int32_parameter.go b/function/int32_parameter.go new file mode 100644 index 000000000..3707f4314 --- /dev/null +++ b/function/int32_parameter.go @@ -0,0 +1,123 @@ +// 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 = Int32Parameter{} +var _ ParameterWithInt32Validators = Int32Parameter{} +var _ fwfunction.ParameterWithValidateImplementation = Int32Parameter{} + +// Int32Parameter represents a function parameter that is a 32-bit integer. +// +// 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.Int32] value +// type. +// - If AllowNullValue is enabled, you must use [types.Int32] or *int32 +// value types. +// - Otherwise, use [types.Int32] or *int32, or int32 value types. +// +// Terraform configurations set this parameter's argument data using expressions +// that return a number or directly via numeric syntax. +type Int32Parameter 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.Int32Type]. When retrieving data, the + // [basetypes.Int32Valuable] implementation associated with this custom + // type must be used in place of [types.Int32]. + CustomType basetypes.Int32Typable + + // 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 int32 validators that should be applied to the + // parameter. + Validators []Int32ParameterValidator +} + +// GetValidators returns the list of validators for the parameter. +func (p Int32Parameter) GetValidators() []Int32ParameterValidator { + return p.Validators +} + +// GetAllowNullValue returns if the parameter accepts a null value. +func (p Int32Parameter) GetAllowNullValue() bool { + return p.AllowNullValue +} + +// GetAllowUnknownValues returns if the parameter accepts an unknown value. +func (p Int32Parameter) GetAllowUnknownValues() bool { + return p.AllowUnknownValues +} + +// GetDescription returns the parameter plaintext description. +func (p Int32Parameter) GetDescription() string { + return p.Description +} + +// GetMarkdownDescription returns the parameter Markdown description. +func (p Int32Parameter) GetMarkdownDescription() string { + return p.MarkdownDescription +} + +// GetName returns the parameter name. +func (p Int32Parameter) GetName() string { + return p.Name +} + +// GetType returns the parameter data type. +func (p Int32Parameter) GetType() attr.Type { + if p.CustomType != nil { + return p.CustomType + } + + return basetypes.Int32Type{} +} + +func (p Int32Parameter) 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/int32_parameter_test.go b/function/int32_parameter_test.go new file mode 100644 index 000000000..29bb0e29d --- /dev/null +++ b/function/int32_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 TestInt32ParameterGetAllowNullValue(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + parameter function.Int32Parameter + expected bool + }{ + "unset": { + parameter: function.Int32Parameter{}, + expected: false, + }, + "AllowNullValue-false": { + parameter: function.Int32Parameter{ + AllowNullValue: false, + }, + expected: false, + }, + "AllowNullValue-true": { + parameter: function.Int32Parameter{ + 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 TestInt32ParameterGetAllowUnknownValues(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + parameter function.Int32Parameter + expected bool + }{ + "unset": { + parameter: function.Int32Parameter{}, + expected: false, + }, + "AllowUnknownValues-false": { + parameter: function.Int32Parameter{ + AllowUnknownValues: false, + }, + expected: false, + }, + "AllowUnknownValues-true": { + parameter: function.Int32Parameter{ + 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 TestInt32ParameterGetDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + parameter function.Int32Parameter + expected string + }{ + "unset": { + parameter: function.Int32Parameter{}, + expected: "", + }, + "Description-empty": { + parameter: function.Int32Parameter{ + Description: "", + }, + expected: "", + }, + "Description-nonempty": { + parameter: function.Int32Parameter{ + 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 TestInt32ParameterGetMarkdownDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + parameter function.Int32Parameter + expected string + }{ + "unset": { + parameter: function.Int32Parameter{}, + expected: "", + }, + "MarkdownDescription-empty": { + parameter: function.Int32Parameter{ + MarkdownDescription: "", + }, + expected: "", + }, + "MarkdownDescription-nonempty": { + parameter: function.Int32Parameter{ + 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 TestInt32ParameterGetName(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + parameter function.Int32Parameter + expected string + }{ + "unset": { + parameter: function.Int32Parameter{}, + expected: "", + }, + "Name-nonempty": { + parameter: function.Int32Parameter{ + 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 TestInt32ParameterGetType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + parameter function.Int32Parameter + expected attr.Type + }{ + "unset": { + parameter: function.Int32Parameter{}, + expected: basetypes.Int32Type{}, + }, + "CustomType": { + parameter: function.Int32Parameter{ + CustomType: testtypes.Int32TypeWithSemanticEquals{}, + }, + expected: testtypes.Int32TypeWithSemanticEquals{}, + }, + } + + 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 TestInt32ParameterInt32Validators(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + parameter function.Int32Parameter + expected []function.Int32ParameterValidator + }{ + "unset": { + parameter: function.Int32Parameter{}, + expected: nil, + }, + "Validators - empty": { + parameter: function.Int32Parameter{ + Validators: []function.Int32ParameterValidator{}}, + expected: []function.Int32ParameterValidator{}, + }, + "Validators": { + parameter: function.Int32Parameter{ + Validators: []function.Int32ParameterValidator{ + testvalidator.Int32{}, + }}, + expected: []function.Int32ParameterValidator{ + testvalidator.Int32{}, + }, + }, + } + + 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 TestInt32ParameterValidateImplementation(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + param function.Int32Parameter + request fwfunction.ValidateParameterImplementationRequest + expected *fwfunction.ValidateParameterImplementationResponse + }{ + "name": { + param: function.Int32Parameter{ + Name: "testparam", + }, + request: fwfunction.ValidateParameterImplementationRequest{ + FunctionName: "testfunc", + ParameterPosition: pointer(int64(0)), + }, + expected: &fwfunction.ValidateParameterImplementationResponse{}, + }, + "name-missing": { + param: function.Int32Parameter{ + // 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/int32_parameter_validator.go b/function/int32_parameter_validator.go new file mode 100644 index 000000000..a5972fdef --- /dev/null +++ b/function/int32_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" +) + +// Int32ParameterValidator is a function validator for types.Int32 parameters. +type Int32ParameterValidator interface { + + // ValidateParameterInt32 performs the validation. + ValidateParameterInt32(context.Context, Int32ParameterValidatorRequest, *Int32ParameterValidatorResponse) +} + +// Int32ParameterValidatorRequest is a request for types.Int32 schema validation. +type Int32ParameterValidatorRequest 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.Int32 +} + +// Int32ParameterValidatorResponse is a response to a Int32ParameterValidatorRequest. +type Int32ParameterValidatorResponse struct { + // Error is a function error generated during validation of the Value. + Error *FuncError +} diff --git a/function/int32_return.go b/function/int32_return.go new file mode 100644 index 000000000..aecfe7c91 --- /dev/null +++ b/function/int32_return.go @@ -0,0 +1,52 @@ +// 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 = Int32Return{} + +// Int32Return represents a function return that is a 32-bit integer number. +// +// When setting the value for this return: +// +// - If CustomType is set, use its associated value type. +// - Otherwise, use [types.Int32], *int32, or int32. +// +// Return documentation is expected in the function [Definition] documentation. +type Int32Return struct { + // CustomType enables the use of a custom data type in place of the + // default [basetypes.Int32Type]. When setting data, the + // [basetypes.Int32Valuable] implementation associated with this custom + // type must be used in place of [types.Int32]. + CustomType basetypes.Int32Typable +} + +// GetType returns the return data type. +func (r Int32Return) GetType() attr.Type { + if r.CustomType != nil { + return r.CustomType + } + + return basetypes.Int32Type{} +} + +// NewResultData returns a new result data based on the type. +func (r Int32Return) NewResultData(ctx context.Context) (ResultData, *FuncError) { + value := basetypes.NewInt32Unknown() + + if r.CustomType == nil { + return NewResultData(value), nil + } + + valuable, diags := r.CustomType.ValueFromInt32(ctx, value) + + return NewResultData(valuable), FuncErrorFromDiags(ctx, diags) +} diff --git a/function/int32_return_test.go b/function/int32_return_test.go new file mode 100644 index 000000000..555fe0c1a --- /dev/null +++ b/function/int32_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 TestInt32ReturnGetType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + parameter function.Int32Return + expected attr.Type + }{ + "unset": { + parameter: function.Int32Return{}, + expected: basetypes.Int32Type{}, + }, + "CustomType": { + parameter: function.Int32Return{ + CustomType: testtypes.Int32TypeWithSemanticEquals{}, + }, + expected: testtypes.Int32TypeWithSemanticEquals{}, + }, + } + + 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 df5957600..6d57ef081 100644 --- a/function/parameter_validation.go +++ b/function/parameter_validation.go @@ -12,6 +12,15 @@ type ParameterWithBoolValidators interface { GetValidators() []BoolParameterValidator } +// ParameterWithInt32Validators is an optional interface on Parameter which +// enables Int32 validation support. +type ParameterWithInt32Validators interface { + Parameter + + // GetValidators should return a list of Int32 validators. + GetValidators() []Int32ParameterValidator +} + // ParameterWithInt64Validators is an optional interface on Parameter which // enables Int64 validation support. type ParameterWithInt64Validators interface { diff --git a/internal/fromproto5/arguments_data.go b/internal/fromproto5/arguments_data.go index fefd8a191..5e342617c 100644 --- a/internal/fromproto5/arguments_data.go +++ b/internal/fromproto5/arguments_data.go @@ -259,6 +259,39 @@ func ArgumentsData(ctx context.Context, arguments []*tfprotov5.DynamicValue, def )) } } + case function.ParameterWithInt32Validators: + for _, functionValidator := range parameterWithValidators.GetValidators() { + int32Valuable, ok := attrValue.(basetypes.Int32Valuable) + 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.Int32Valuable at position %d", pos), + )) + + continue + } + int32Val, diags := int32Valuable.ToInt32Value(ctx) + if diags.HasError() { + funcError = function.ConcatFuncErrors(funcError, function.FuncErrorFromDiags(ctx, diags)) + continue + } + req := function.Int32ParameterValidatorRequest{ + ArgumentPosition: pos, + Value: int32Val, + } + resp := &function.Int32ParameterValidatorResponse{} + functionValidator.ValidateParameterInt32(ctx, req, resp) + if resp.Error != nil { + funcError = function.ConcatFuncErrors(funcError, function.NewArgumentFuncError( + pos, + resp.Error.Error(), + )) + } + } case function.ParameterWithInt64Validators: for _, functionValidator := range parameterWithValidators.GetValidators() { int64Valuable, ok := attrValue.(basetypes.Int64Valuable) diff --git a/internal/fromproto5/arguments_data_test.go b/internal/fromproto5/arguments_data_test.go index a7948fafb..42a2fac23 100644 --- a/internal/fromproto5/arguments_data_test.go +++ b/internal/fromproto5/arguments_data_test.go @@ -1224,6 +1224,170 @@ func TestArgumentsData_ParameterValidators(t *testing.T) { 0, "Error Diagnostic: This is an error.", ), }, + "int32-parameter-Validators": { + input: []*tfprotov5.DynamicValue{ + DynamicValueMust(tftypes.NewValue(tftypes.Number, 1)), + }, + definition: function.Definition{ + Parameters: []function.Parameter{ + function.Int32Parameter{ + Validators: []function.Int32ParameterValidator{ + testvalidator.Int32{ + ValidateMethod: func(ctx context.Context, req function.Int32ParameterValidatorRequest, resp *function.Int32ParameterValidatorResponse) { + got := req.Value + expected := types.Int32Value(1) + + if !got.Equal(expected) { + resp.Error = function.NewArgumentFuncError( + req.ArgumentPosition, + "Error Diagnostic: This is an error.", + ) + } + }, + }, + }, + }, + }, + }, + expected: function.NewArgumentsData([]attr.Value{ + basetypes.NewInt32Value(1), + }), + }, + "int32-parameter-Validators-error": { + input: []*tfprotov5.DynamicValue{ + DynamicValueMust(tftypes.NewValue(tftypes.Number, 1)), + }, + definition: function.Definition{ + Parameters: []function.Parameter{ + function.Int32Parameter{ + Validators: []function.Int32ParameterValidator{ + testvalidator.Int32{ + ValidateMethod: func(ctx context.Context, req function.Int32ParameterValidatorRequest, resp *function.Int32ParameterValidatorResponse) { + got := req.Value + expected := types.Int32Value(2) + + 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.", + ), + }, + "int32-parameter-Validators-multiple-errors": { + input: []*tfprotov5.DynamicValue{ + DynamicValueMust(tftypes.NewValue(tftypes.Number, 1)), + }, + definition: function.Definition{ + Parameters: []function.Parameter{ + function.Int32Parameter{ + Validators: []function.Int32ParameterValidator{ + testvalidator.Int32{ + ValidateMethod: func(ctx context.Context, req function.Int32ParameterValidatorRequest, resp *function.Int32ParameterValidatorResponse) { + got := req.Value + expected := types.Int32Value(2) + + if !got.Equal(expected) { + resp.Error = function.NewArgumentFuncError( + req.ArgumentPosition, + "Error Diagnostic: error 1.", + ) + } + }, + }, + testvalidator.Int32{ + ValidateMethod: func(ctx context.Context, req function.Int32ParameterValidatorRequest, resp *function.Int32ParameterValidatorResponse) { + got := req.Value + expected := types.Int32Value(3) + + 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.", + ), + }, + "int32-parameter-custom-type-Validators": { + input: []*tfprotov5.DynamicValue{ + DynamicValueMust(tftypes.NewValue(tftypes.Number, 1)), + }, + definition: function.Definition{ + Parameters: []function.Parameter{ + function.Int32Parameter{ + CustomType: testtypes.Int32Type{}, + Validators: []function.Int32ParameterValidator{ + testvalidator.Int32{ + ValidateMethod: func(ctx context.Context, req function.Int32ParameterValidatorRequest, resp *function.Int32ParameterValidatorResponse) { + got := req.Value + expected := types.Int32Value(1) + + if !got.Equal(expected) { + resp.Error = function.NewArgumentFuncError( + req.ArgumentPosition, + "Error Diagnostic: This is an error.", + ) + } + }, + }, + }, + }, + }, + }, + expected: function.NewArgumentsData([]attr.Value{ + basetypes.NewInt32Value(1), + }), + }, + "int32-parameter-custom-type-Validators-error": { + input: []*tfprotov5.DynamicValue{ + DynamicValueMust(tftypes.NewValue(tftypes.Number, 1)), + }, + definition: function.Definition{ + Parameters: []function.Parameter{ + function.Int32Parameter{ + CustomType: testtypes.Int32Type{}, + Validators: []function.Int32ParameterValidator{ + testvalidator.Int32{ + ValidateMethod: func(ctx context.Context, req function.Int32ParameterValidatorRequest, resp *function.Int32ParameterValidatorResponse) { + got := req.Value + expected := types.Int32Value(2) + + 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.", + ), + }, "int64-parameter-Validators": { input: []*tfprotov5.DynamicValue{ DynamicValueMust(tftypes.NewValue(tftypes.Number, 1)), diff --git a/internal/fromproto6/arguments_data.go b/internal/fromproto6/arguments_data.go index 1fcf39229..192e45178 100644 --- a/internal/fromproto6/arguments_data.go +++ b/internal/fromproto6/arguments_data.go @@ -259,6 +259,39 @@ func ArgumentsData(ctx context.Context, arguments []*tfprotov6.DynamicValue, def )) } } + case function.ParameterWithInt32Validators: + for _, functionValidator := range parameterWithValidators.GetValidators() { + int32Valuable, ok := attrValue.(basetypes.Int32Valuable) + 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.Int32Valuable at position %d", pos), + )) + + continue + } + int32Val, diags := int32Valuable.ToInt32Value(ctx) + if diags.HasError() { + funcError = function.ConcatFuncErrors(funcError, function.FuncErrorFromDiags(ctx, diags)) + continue + } + req := function.Int32ParameterValidatorRequest{ + ArgumentPosition: pos, + Value: int32Val, + } + resp := &function.Int32ParameterValidatorResponse{} + functionValidator.ValidateParameterInt32(ctx, req, resp) + if resp.Error != nil { + funcError = function.ConcatFuncErrors(funcError, function.NewArgumentFuncError( + pos, + resp.Error.Error(), + )) + } + } case function.ParameterWithInt64Validators: for _, functionValidator := range parameterWithValidators.GetValidators() { int64Valuable, ok := attrValue.(basetypes.Int64Valuable) diff --git a/internal/fromproto6/arguments_data_test.go b/internal/fromproto6/arguments_data_test.go index 8aae7abd5..1f0e587ad 100644 --- a/internal/fromproto6/arguments_data_test.go +++ b/internal/fromproto6/arguments_data_test.go @@ -1225,6 +1225,170 @@ func TestArgumentsData_ParameterValidators(t *testing.T) { 0, "Error Diagnostic: This is an error.", ), }, + "int32-parameter-Validators": { + input: []*tfprotov6.DynamicValue{ + DynamicValueMust(tftypes.NewValue(tftypes.Number, 1)), + }, + definition: function.Definition{ + Parameters: []function.Parameter{ + function.Int32Parameter{ + Validators: []function.Int32ParameterValidator{ + testvalidator.Int32{ + ValidateMethod: func(ctx context.Context, req function.Int32ParameterValidatorRequest, resp *function.Int32ParameterValidatorResponse) { + got := req.Value + expected := types.Int32Value(1) + + if !got.Equal(expected) { + resp.Error = function.NewArgumentFuncError( + req.ArgumentPosition, + "Error Diagnostic: This is an error.", + ) + } + }, + }, + }, + }, + }, + }, + expected: function.NewArgumentsData([]attr.Value{ + basetypes.NewInt32Value(1), + }), + }, + "int32-parameter-Validators-error": { + input: []*tfprotov6.DynamicValue{ + DynamicValueMust(tftypes.NewValue(tftypes.Number, 1)), + }, + definition: function.Definition{ + Parameters: []function.Parameter{ + function.Int32Parameter{ + Validators: []function.Int32ParameterValidator{ + testvalidator.Int32{ + ValidateMethod: func(ctx context.Context, req function.Int32ParameterValidatorRequest, resp *function.Int32ParameterValidatorResponse) { + got := req.Value + expected := types.Int32Value(2) + + 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.", + ), + }, + "int32-parameter-Validators-multiple-errors": { + input: []*tfprotov6.DynamicValue{ + DynamicValueMust(tftypes.NewValue(tftypes.Number, 1)), + }, + definition: function.Definition{ + Parameters: []function.Parameter{ + function.Int32Parameter{ + Validators: []function.Int32ParameterValidator{ + testvalidator.Int32{ + ValidateMethod: func(ctx context.Context, req function.Int32ParameterValidatorRequest, resp *function.Int32ParameterValidatorResponse) { + got := req.Value + expected := types.Int32Value(2) + + if !got.Equal(expected) { + resp.Error = function.NewArgumentFuncError( + req.ArgumentPosition, + "Error Diagnostic: error 1.", + ) + } + }, + }, + testvalidator.Int32{ + ValidateMethod: func(ctx context.Context, req function.Int32ParameterValidatorRequest, resp *function.Int32ParameterValidatorResponse) { + got := req.Value + expected := types.Int32Value(3) + + 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.", + ), + }, + "int32-parameter-custom-type-Validators": { + input: []*tfprotov6.DynamicValue{ + DynamicValueMust(tftypes.NewValue(tftypes.Number, 1)), + }, + definition: function.Definition{ + Parameters: []function.Parameter{ + function.Int32Parameter{ + CustomType: testtypes.Int32Type{}, + Validators: []function.Int32ParameterValidator{ + testvalidator.Int32{ + ValidateMethod: func(ctx context.Context, req function.Int32ParameterValidatorRequest, resp *function.Int32ParameterValidatorResponse) { + got := req.Value + expected := types.Int32Value(1) + + if !got.Equal(expected) { + resp.Error = function.NewArgumentFuncError( + req.ArgumentPosition, + "Error Diagnostic: This is an error.", + ) + } + }, + }, + }, + }, + }, + }, + expected: function.NewArgumentsData([]attr.Value{ + basetypes.NewInt32Value(1), + }), + }, + "int32-parameter-custom-type-Validators-error": { + input: []*tfprotov6.DynamicValue{ + DynamicValueMust(tftypes.NewValue(tftypes.Number, 1)), + }, + definition: function.Definition{ + Parameters: []function.Parameter{ + function.Int32Parameter{ + CustomType: testtypes.Int32Type{}, + Validators: []function.Int32ParameterValidator{ + testvalidator.Int32{ + ValidateMethod: func(ctx context.Context, req function.Int32ParameterValidatorRequest, resp *function.Int32ParameterValidatorResponse) { + got := req.Value + expected := types.Int32Value(2) + + 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.", + ), + }, "int64-parameter-Validators": { input: []*tfprotov6.DynamicValue{ DynamicValueMust(tftypes.NewValue(tftypes.Number, 1)), diff --git a/internal/fromtftypes/value_test.go b/internal/fromtftypes/value_test.go index f87ef1089..a7d860a34 100644 --- a/internal/fromtftypes/value_test.go +++ b/internal/fromtftypes/value_test.go @@ -11,11 +11,12 @@ import ( "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/internal/fromtftypes" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestValue(t *testing.T) { @@ -74,6 +75,21 @@ func TestValue(t *testing.T) { attrType: types.Float64Type, expected: types.Float64Value(1.2), }, + "int32-null": { + tfType: tftypes.NewValue(tftypes.Number, nil), + attrType: types.Int32Type, + expected: types.Int32Null(), + }, + "int32-unknown": { + tfType: tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), + attrType: types.Int32Type, + expected: types.Int32Unknown(), + }, + "int32-value": { + tfType: tftypes.NewValue(tftypes.Number, 123), + attrType: types.Int32Type, + expected: types.Int32Value(123), + }, "int64-null": { tfType: tftypes.NewValue(tftypes.Number, nil), attrType: types.Int64Type, diff --git a/internal/fwschema/attribute_default.go b/internal/fwschema/attribute_default.go index 29e63a7bb..39d27b1a0 100644 --- a/internal/fwschema/attribute_default.go +++ b/internal/fwschema/attribute_default.go @@ -23,6 +23,14 @@ type AttributeWithFloat64DefaultValue interface { Float64DefaultValue() defaults.Float64 } +// AttributeWithInt32DefaultValue is an optional interface on Attribute which +// enables Int32 default value support. +type AttributeWithInt32DefaultValue interface { + Attribute + + Int32DefaultValue() defaults.Int32 +} + // AttributeWithInt64DefaultValue is an optional interface on Attribute which // enables Int64 default value support. type AttributeWithInt64DefaultValue interface { diff --git a/internal/fwschema/fwxschema/attribute_plan_modification.go b/internal/fwschema/fwxschema/attribute_plan_modification.go index f4cb841de..6f71e2264 100644 --- a/internal/fwschema/fwxschema/attribute_plan_modification.go +++ b/internal/fwschema/fwxschema/attribute_plan_modification.go @@ -26,6 +26,15 @@ type AttributeWithFloat64PlanModifiers interface { Float64PlanModifiers() []planmodifier.Float64 } +// AttributeWithInt32PlanModifiers is an optional interface on Attribute which +// enables Int32 plan modifier support. +type AttributeWithInt32PlanModifiers interface { + fwschema.Attribute + + // Int32PlanModifiers should return a list of Int32 plan modifiers. + Int32PlanModifiers() []planmodifier.Int32 +} + // AttributeWithInt64PlanModifiers is an optional interface on Attribute which // enables Int64 plan modifier support. type AttributeWithInt64PlanModifiers interface { diff --git a/internal/fwschema/fwxschema/attribute_validation.go b/internal/fwschema/fwxschema/attribute_validation.go index e8274de4d..b806aa083 100644 --- a/internal/fwschema/fwxschema/attribute_validation.go +++ b/internal/fwschema/fwxschema/attribute_validation.go @@ -26,6 +26,15 @@ type AttributeWithFloat64Validators interface { Float64Validators() []validator.Float64 } +// AttributeWithInt32Validators is an optional interface on Attribute which +// enables Int32 validation support. +type AttributeWithInt32Validators interface { + fwschema.Attribute + + // Int32Validators should return a list of Int32 validators. + Int32Validators() []validator.Int32 +} + // AttributeWithInt64Validators is an optional interface on Attribute which // enables Int64 validation support. type AttributeWithInt64Validators interface { diff --git a/internal/fwschemadata/data_default.go b/internal/fwschemadata/data_default.go index d83f5ee05..de5b72a31 100644 --- a/internal/fwschemadata/data_default.go +++ b/internal/fwschemadata/data_default.go @@ -145,6 +145,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.AttributeWithInt32DefaultValue: + defaultValue := a.Int32DefaultValue() + + if defaultValue == nil { + return tfTypeValue, nil + } + + req := defaults.Int32Request{ + Path: fwPath, + } + resp := defaults.Int32Response{} + + defaultValue.DefaultInt32(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.AttributeWithInt64DefaultValue: defaultValue := a.Int64DefaultValue() diff --git a/internal/fwschemadata/data_default_test.go b/internal/fwschemadata/data_default_test.go index cb7bd4b47..c38f2da6e 100644 --- a/internal/fwschemadata/data_default_test.go +++ b/internal/fwschemadata/data_default_test.go @@ -24,6 +24,7 @@ import ( "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/float64default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int32default" "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" "github.com/hashicorp/terraform-plugin-framework/resource/schema/listdefault" "github.com/hashicorp/terraform-plugin-framework/resource/schema/mapdefault" @@ -751,6 +752,360 @@ func TestDataDefault(t *testing.T) { ), }, }, + "int32-attribute-request-path": { + data: &fwschemadata.Data{ + Description: fwschemadata.DataDescriptionPlan, + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "int32_attribute": testschema.AttributeWithInt32DefaultValue{ + Optional: true, + Computed: true, + Default: testdefaults.Int32{ + DefaultInt32Method: func(ctx context.Context, req defaults.Int32Request, resp *defaults.Int32Response) { + if !req.Path.Equal(path.Root("int32_attribute")) { + resp.Diagnostics.AddError( + "unexpected req.Path value", + fmt.Sprintf("expected %s, got: %s", path.Root("int32_attribute"), req.Path), + ) + } + }, + }, + }, + }, + }, + TerraformValue: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "int32_attribute": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "int32_attribute": tftypes.NewValue(tftypes.Number, nil), + }, + ), + }, + rawConfig: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "int32_attribute": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "int32_attribute": tftypes.NewValue(tftypes.Number, nil), + }, + ), + expected: &fwschemadata.Data{ + Description: fwschemadata.DataDescriptionPlan, + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "int32_attribute": testschema.AttributeWithInt32DefaultValue{ + Optional: true, + Computed: true, + Default: testdefaults.Int32{ + DefaultInt32Method: func(ctx context.Context, req defaults.Int32Request, resp *defaults.Int32Response) { + if !req.Path.Equal(path.Root("int32_attribute")) { + resp.Diagnostics.AddError( + "unexpected req.Path value", + fmt.Sprintf("expected %s, got: %s", path.Root("int32_attribute"), req.Path), + ) + } + }, + }, + }, + }, + }, + TerraformValue: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "int32_attribute": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "int32_attribute": tftypes.NewValue(tftypes.Number, nil), + }, + ), + }, + }, + "int32-attribute-response-diagnostics": { + data: &fwschemadata.Data{ + Description: fwschemadata.DataDescriptionPlan, + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "int32_attribute": testschema.AttributeWithInt32DefaultValue{ + Optional: true, + Computed: true, + Default: testdefaults.Int32{ + DefaultInt32Method: func(ctx context.Context, req defaults.Int32Request, resp *defaults.Int32Response) { + 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{ + "int32_attribute": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "int32_attribute": tftypes.NewValue(tftypes.Number, nil), + }, + ), + }, + rawConfig: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "int32_attribute": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "int32_attribute": tftypes.NewValue(tftypes.Number, nil), + }, + ), + expected: &fwschemadata.Data{ + Description: fwschemadata.DataDescriptionPlan, + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "int32_attribute": testschema.AttributeWithInt32DefaultValue{ + Optional: true, + Computed: true, + Default: testdefaults.Int32{ + DefaultInt32Method: func(ctx context.Context, req defaults.Int32Request, resp *defaults.Int32Response) { + 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{ + "int32_attribute": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "int32_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"), + }, + }, + "int32-attribute-not-null-unmodified-default": { + data: &fwschemadata.Data{ + Description: fwschemadata.DataDescriptionState, + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "int32_attribute": testschema.AttributeWithInt32DefaultValue{ + Computed: true, + Default: int32default.StaticInt32(54321), + }, + }, + }, + TerraformValue: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "int32_attribute": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "int32_attribute": tftypes.NewValue(tftypes.Number, 12345), + }, + ), + }, + rawConfig: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "int32_attribute": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "int32_attribute": tftypes.NewValue(tftypes.Number, 54321), // value in rawConfig + }, + ), + expected: &fwschemadata.Data{ + Description: fwschemadata.DataDescriptionState, + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "int32_attribute": testschema.AttributeWithInt32DefaultValue{ + Computed: true, + Default: int32default.StaticInt32(54321), + }, + }, + }, + TerraformValue: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "int32_attribute": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "int32_attribute": tftypes.NewValue(tftypes.Number, 12345), + }, + ), + }, + }, + "int32-attribute-null-unmodified-no-default": { + data: &fwschemadata.Data{ + Description: fwschemadata.DataDescriptionState, + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "int32_attribute": testschema.Attribute{ + Computed: true, + Type: types.Int32Type, + }, + }, + }, + TerraformValue: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "int32_attribute": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "int32_attribute": tftypes.NewValue(tftypes.Number, 12345), + }, + ), + }, + rawConfig: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "int32_attribute": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "int32_attribute": tftypes.NewValue(tftypes.Number, nil), // value in rawConfig + }, + ), + expected: &fwschemadata.Data{ + Description: fwschemadata.DataDescriptionState, + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "int32_attribute": testschema.Attribute{ + Computed: true, + Type: types.Int32Type, + }, + }, + }, + TerraformValue: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "int32_attribute": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "int32_attribute": tftypes.NewValue(tftypes.Number, 12345), + }, + ), + }, + }, + "int32-attribute-null-modified-default": { + data: &fwschemadata.Data{ + Description: fwschemadata.DataDescriptionState, + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "int32_attribute": testschema.AttributeWithInt32DefaultValue{ + Computed: true, + Default: int32default.StaticInt32(54321), + }, + }, + }, + TerraformValue: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "int32_attribute": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "int32_attribute": tftypes.NewValue(tftypes.Number, 12345), + }, + ), + }, + rawConfig: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "int32_attribute": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "int32_attribute": tftypes.NewValue(tftypes.Number, nil), // value in rawConfig + }, + ), + expected: &fwschemadata.Data{ + Description: fwschemadata.DataDescriptionState, + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "int32_attribute": testschema.AttributeWithInt32DefaultValue{ + Computed: true, + Default: int32default.StaticInt32(54321), + }, + }, + }, + TerraformValue: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "int32_attribute": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "int32_attribute": tftypes.NewValue(tftypes.Number, 54321), + }, + ), + }, + }, + "int32-attribute-null-unmodified-default-nil": { + data: &fwschemadata.Data{ + Description: fwschemadata.DataDescriptionState, + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "int32_attribute": testschema.AttributeWithInt32DefaultValue{ + Computed: true, + Default: nil, + }, + }, + }, + TerraformValue: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "int32_attribute": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "int32_attribute": tftypes.NewValue(tftypes.Number, 12345), + }, + ), + }, + rawConfig: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "int32_attribute": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "int32_attribute": tftypes.NewValue(tftypes.Number, nil), // value in rawConfig + }, + ), + expected: &fwschemadata.Data{ + Description: fwschemadata.DataDescriptionState, + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "int32_attribute": testschema.AttributeWithInt32DefaultValue{ + Computed: true, + Default: nil, + }, + }, + }, + TerraformValue: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "int32_attribute": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "int32_attribute": tftypes.NewValue(tftypes.Number, 12345), + }, + ), + }, + }, "int64-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 cb8ad8522..6b464a20d 100644 --- a/internal/fwschemadata/data_get_at_path_test.go +++ b/internal/fwschemadata/data_get_at_path_test.go @@ -727,6 +727,258 @@ func TestDataGetAtPath(t *testing.T) { target: new(float64), expected: pointer(1.2), }, + "Int32Type-types.Int32-null": { + data: fwschemadata.Data{ + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "int32": testschema.Attribute{ + Optional: true, + Type: types.Int32Type, + }, + }, + }, + TerraformValue: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "int32": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "int32": tftypes.NewValue(tftypes.Number, nil), + }, + ), + }, + path: path.Root("int32"), + target: new(types.Int32), + expected: pointer(types.Int32Null()), + }, + "Int32Type-types.Int32-unknown": { + data: fwschemadata.Data{ + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "int32": testschema.Attribute{ + Optional: true, + Type: types.Int32Type, + }, + }, + }, + TerraformValue: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "int32": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "int32": tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), + }, + ), + }, + path: path.Root("int32"), + target: new(types.Int32), + expected: pointer(types.Int32Unknown()), + }, + "Int32Type-types.Int32-value": { + data: fwschemadata.Data{ + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "int32": testschema.Attribute{ + Optional: true, + Type: types.Int32Type, + }, + }, + }, + TerraformValue: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "int32": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "int32": tftypes.NewValue(tftypes.Number, 12), + }, + ), + }, + path: path.Root("int32"), + target: new(types.Int32), + expected: pointer(types.Int32Value(12)), + }, + "Int32Type-*int32-null": { + data: fwschemadata.Data{ + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "int32": testschema.Attribute{ + Optional: true, + Type: types.Int32Type, + }, + }, + }, + TerraformValue: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "int32": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "int32": tftypes.NewValue(tftypes.Number, nil), + }, + ), + }, + path: path.Root("int32"), + target: new(*int32), + expected: new(*int32), + }, + "Int32Type-*int32-unknown": { + data: fwschemadata.Data{ + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "int32": testschema.Attribute{ + Optional: true, + Type: types.Int32Type, + }, + }, + }, + TerraformValue: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "int32": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "int32": tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), + }, + ), + }, + path: path.Root("int32"), + target: new(*int32), + expected: new(*int32), + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("int32"), + "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: int32\nTarget Type: *int32\nSuggested Type: basetypes.Int32Value", + ), + }, + }, + "Int32Type-*int32-value": { + data: fwschemadata.Data{ + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "int32": testschema.Attribute{ + Optional: true, + Type: types.Int32Type, + }, + }, + }, + TerraformValue: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "int32": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "int32": tftypes.NewValue(tftypes.Number, 12), + }, + ), + }, + path: path.Root("int32"), + target: new(*int32), + expected: pointer(pointer(int32(12))), + }, + "Int32Type-int32-null": { + data: fwschemadata.Data{ + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "int32": testschema.Attribute{ + Optional: true, + Type: types.Int32Type, + }, + }, + }, + TerraformValue: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "int32": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "int32": tftypes.NewValue(tftypes.Number, nil), + }, + ), + }, + path: path.Root("int32"), + target: new(int32), + expected: new(int32), + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("int32"), + "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: int32\nTarget Type: int32\nSuggested `types` Type: basetypes.Int32Value\nSuggested Pointer Type: *int32", + ), + }, + }, + "Int32Type-int32-unknown": { + data: fwschemadata.Data{ + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "int32": testschema.Attribute{ + Optional: true, + Type: types.Int32Type, + }, + }, + }, + TerraformValue: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "int32": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "int32": tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), + }, + ), + }, + path: path.Root("int32"), + target: new(int32), + expected: new(int32), + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("int32"), + "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: int32\nTarget Type: int32\nSuggested Type: basetypes.Int32Value", + ), + }, + }, + "Int32Type-int32-value": { + data: fwschemadata.Data{ + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "int32": testschema.Attribute{ + Optional: true, + Type: types.Int32Type, + }, + }, + }, + TerraformValue: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "int32": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "int32": tftypes.NewValue(tftypes.Number, 12), + }, + ), + }, + path: path.Root("int32"), + target: new(int32), + expected: pointer(int32(12)), + }, "Int64Type-types.Int64-null": { data: fwschemadata.Data{ Schema: testschema.Schema{ @@ -6859,6 +7111,9 @@ func TestDataGetAtPath(t *testing.T) { cmp.Comparer(func(i, j *types.Float64) bool { return (i == nil && j == nil) || (i != nil && j != nil && cmp.Equal(*i, *j)) }), + cmp.Comparer(func(i, j *types.Int32) bool { + return (i == nil && j == nil) || (i != nil && j != nil && cmp.Equal(*i, *j)) + }), cmp.Comparer(func(i, j *types.Int64) 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 85116667e..c22d84a66 100644 --- a/internal/fwschemadata/data_get_test.go +++ b/internal/fwschemadata/data_get_test.go @@ -824,6 +824,303 @@ func TestDataGet(t *testing.T) { Float64: 1.2, }, }, + "Int32Type-types.Int32-null": { + data: fwschemadata.Data{ + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "int32": testschema.Attribute{ + Optional: true, + Type: types.Int32Type, + }, + }, + }, + TerraformValue: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "int32": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "int32": tftypes.NewValue(tftypes.Number, nil), + }, + ), + }, + target: new(struct { + Int32 types.Int32 `tfsdk:"int32"` + }), + expected: &struct { + Int32 types.Int32 `tfsdk:"int32"` + }{ + Int32: types.Int32Null(), + }, + }, + "Int32Type-types.Int32-unknown": { + data: fwschemadata.Data{ + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "int32": testschema.Attribute{ + Optional: true, + Type: types.Int32Type, + }, + }, + }, + TerraformValue: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "int32": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "int32": tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), + }, + ), + }, + target: new(struct { + Int32 types.Int32 `tfsdk:"int32"` + }), + expected: &struct { + Int32 types.Int32 `tfsdk:"int32"` + }{ + Int32: types.Int32Unknown(), + }, + }, + "Int32Type-types.Int32-value": { + data: fwschemadata.Data{ + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "int32": testschema.Attribute{ + Optional: true, + Type: types.Int32Type, + }, + }, + }, + TerraformValue: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "int32": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "int32": tftypes.NewValue(tftypes.Number, 12), + }, + ), + }, + target: new(struct { + Int32 types.Int32 `tfsdk:"int32"` + }), + expected: &struct { + Int32 types.Int32 `tfsdk:"int32"` + }{ + Int32: types.Int32Value(12), + }, + }, + "Int32Type-*int32-null": { + data: fwschemadata.Data{ + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "int32": testschema.Attribute{ + Optional: true, + Type: types.Int32Type, + }, + }, + }, + TerraformValue: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "int32": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "int32": tftypes.NewValue(tftypes.Number, nil), + }, + ), + }, + target: new(struct { + Int32 *int32 `tfsdk:"int32"` + }), + expected: &struct { + Int32 *int32 `tfsdk:"int32"` + }{ + Int32: nil, + }, + }, + "Int32Type-*int32-unknown": { + data: fwschemadata.Data{ + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "int32": testschema.Attribute{ + Optional: true, + Type: types.Int32Type, + }, + }, + }, + TerraformValue: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "int32": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "int32": tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), + }, + ), + }, + target: new(struct { + Int32 *int32 `tfsdk:"int32"` + }), + expected: &struct { + Int32 *int32 `tfsdk:"int32"` + }{ + Int32: nil, + }, + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("int32"), + "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: int32\nTarget Type: *int32\nSuggested Type: basetypes.Int32Value", + ), + }, + }, + "Int32Type-*int32-value": { + data: fwschemadata.Data{ + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "int32": testschema.Attribute{ + Optional: true, + Type: types.Int32Type, + }, + }, + }, + TerraformValue: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "int32": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "int32": tftypes.NewValue(tftypes.Number, 12), + }, + ), + }, + target: new(struct { + Int32 *int32 `tfsdk:"int32"` + }), + expected: &struct { + Int32 *int32 `tfsdk:"int32"` + }{ + Int32: pointer(int32(12)), + }, + }, + "Int32Type-int32-null": { + data: fwschemadata.Data{ + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "int32": testschema.Attribute{ + Optional: true, + Type: types.Int32Type, + }, + }, + }, + TerraformValue: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "int32": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "int32": tftypes.NewValue(tftypes.Number, nil), + }, + ), + }, + target: new(struct { + Int32 int32 `tfsdk:"int32"` + }), + expected: &struct { + Int32 int32 `tfsdk:"int32"` + }{ + Int32: 0.0, + }, + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("int32"), + "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: int32\nTarget Type: int32\nSuggested `types` Type: basetypes.Int32Value\nSuggested Pointer Type: *int32", + ), + }, + }, + "Int32Type-int32-unknown": { + data: fwschemadata.Data{ + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "int32": testschema.Attribute{ + Optional: true, + Type: types.Int32Type, + }, + }, + }, + TerraformValue: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "int32": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "int32": tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), + }, + ), + }, + target: new(struct { + Int32 int32 `tfsdk:"int32"` + }), + expected: &struct { + Int32 int32 `tfsdk:"int32"` + }{ + Int32: 0, + }, + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("int32"), + "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: int32\nTarget Type: int32\nSuggested Type: basetypes.Int32Value", + ), + }, + }, + "Int32Type-int32-value": { + data: fwschemadata.Data{ + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "int32": testschema.Attribute{ + Optional: true, + Type: types.Int32Type, + }, + }, + }, + TerraformValue: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "int32": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "int32": tftypes.NewValue(tftypes.Number, 12), + }, + ), + }, + target: new(struct { + Int32 int32 `tfsdk:"int32"` + }), + expected: &struct { + Int32 int32 `tfsdk:"int32"` + }{ + Int32: 12, + }, + }, "Int64Type-types.Int64-null": { data: fwschemadata.Data{ Schema: testschema.Schema{ diff --git a/internal/fwschemadata/data_value_test.go b/internal/fwschemadata/data_value_test.go index b13b7e961..86d9cd96c 100644 --- a/internal/fwschemadata/data_value_test.go +++ b/internal/fwschemadata/data_value_test.go @@ -1139,6 +1139,71 @@ func TestDataValueAtPath(t *testing.T) { path: path.Root("test").AtName("sub_test"), expected: types.Float64Null(), }, + "WithAttributeName-SingleBlock-null-WithAttributeName-Int32": { + 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.Int32Type, + Optional: true, + }, + }, + }, + NestingMode: fwschema.BlockNestingModeSingle, + }, + }, + }, + }, + path: path.Root("test").AtName("sub_test"), + expected: types.Int32Null(), + }, "WithAttributeName-SingleBlock-null-WithAttributeName-Int64": { data: fwschemadata.Data{ TerraformValue: tftypes.NewValue(tftypes.Object{ @@ -1458,6 +1523,49 @@ func TestDataValueAtPath(t *testing.T) { path: path.Root("test").AtName("sub_test"), expected: types.Float64Null(), }, + "WithAttributeName-SingleNestedAttributes-null-WithAttributeName-Int32": { + 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.Int32Type, + Optional: true, + }, + }, + }, + NestingMode: fwschema.NestingModeSingle, + Optional: true, + }, + "other": testschema.Attribute{ + Type: types.BoolType, + Optional: true, + }, + }, + }, + }, + path: path.Root("test").AtName("sub_test"), + expected: types.Int32Null(), + }, "WithAttributeName-SingleNestedAttributes-null-WithAttributeName-Int64": { 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 e93ae8396..92e06b1f6 100644 --- a/internal/fwschemadata/value_semantic_equality.go +++ b/internal/fwschemadata/value_semantic_equality.go @@ -65,6 +65,8 @@ func ValueSemanticEquality(ctx context.Context, req ValueSemanticEqualityRequest ValueSemanticEqualityBool(ctx, req, resp) case basetypes.Float64Valuable: ValueSemanticEqualityFloat64(ctx, req, resp) + case basetypes.Int32Valuable: + ValueSemanticEqualityInt32(ctx, req, resp) case basetypes.Int64Valuable: ValueSemanticEqualityInt64(ctx, req, resp) case basetypes.ListValuable: diff --git a/internal/fwschemadata/value_semantic_equality_int32.go b/internal/fwschemadata/value_semantic_equality_int32.go new file mode 100644 index 000000000..101c750c7 --- /dev/null +++ b/internal/fwschemadata/value_semantic_equality_int32.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" +) + +// ValueSemanticEqualityInt32 performs int32 type semantic equality. +func ValueSemanticEqualityInt32(ctx context.Context, req ValueSemanticEqualityRequest, resp *ValueSemanticEqualityResponse) { + priorValuable, ok := req.PriorValue.(basetypes.Int32ValuableWithSemanticEquals) + + // No changes required if the interface is not implemented. + if !ok { + return + } + + proposedNewValuable, ok := req.ProposedNewValue.(basetypes.Int32ValuableWithSemanticEquals) + + // 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.Int32SemanticEquals(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_int32_test.go b/internal/fwschemadata/value_semantic_equality_int32_test.go new file mode 100644 index 000000000..4c48b9ae0 --- /dev/null +++ b/internal/fwschemadata/value_semantic_equality_int32_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 TestValueSemanticEqualityInt32(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + request fwschemadata.ValueSemanticEqualityRequest + expected *fwschemadata.ValueSemanticEqualityResponse + }{ + "Int32Value": { + request: fwschemadata.ValueSemanticEqualityRequest{ + Path: path.Root("test"), + PriorValue: types.Int32Value(12), + ProposedNewValue: types.Int32Value(24), + }, + expected: &fwschemadata.ValueSemanticEqualityResponse{ + NewValue: types.Int32Value(24), + }, + }, + "Int32ValuableWithSemanticEquals-true": { + request: fwschemadata.ValueSemanticEqualityRequest{ + Path: path.Root("test"), + PriorValue: testtypes.Int32ValueWithSemanticEquals{ + Int32Value: types.Int32Value(12), + SemanticEquals: true, + }, + ProposedNewValue: testtypes.Int32ValueWithSemanticEquals{ + Int32Value: types.Int32Value(24), + SemanticEquals: true, + }, + }, + expected: &fwschemadata.ValueSemanticEqualityResponse{ + NewValue: testtypes.Int32ValueWithSemanticEquals{ + Int32Value: types.Int32Value(12), + SemanticEquals: true, + }, + }, + }, + "Int32ValuableWithSemanticEquals-false": { + request: fwschemadata.ValueSemanticEqualityRequest{ + Path: path.Root("test"), + PriorValue: testtypes.Int32ValueWithSemanticEquals{ + Int32Value: types.Int32Value(12), + SemanticEquals: false, + }, + ProposedNewValue: testtypes.Int32ValueWithSemanticEquals{ + Int32Value: types.Int32Value(24), + SemanticEquals: false, + }, + }, + expected: &fwschemadata.ValueSemanticEqualityResponse{ + NewValue: testtypes.Int32ValueWithSemanticEquals{ + Int32Value: types.Int32Value(24), + SemanticEquals: false, + }, + }, + }, + "Int32ValuableWithSemanticEquals-diagnostics": { + request: fwschemadata.ValueSemanticEqualityRequest{ + Path: path.Root("test"), + PriorValue: testtypes.Int32ValueWithSemanticEquals{ + Int32Value: types.Int32Value(12), + SemanticEquals: false, + SemanticEqualsDiagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic("test summary 1", "test detail 1"), + diag.NewErrorDiagnostic("test summary 2", "test detail 2"), + }, + }, + ProposedNewValue: testtypes.Int32ValueWithSemanticEquals{ + Int32Value: types.Int32Value(24), + 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.Int32ValueWithSemanticEquals{ + Int32Value: types.Int32Value(24), + 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.ValueSemanticEqualityInt32(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 60ecae80a..7d061ec5b 100644 --- a/internal/fwschemadata/value_semantic_equality_test.go +++ b/internal/fwschemadata/value_semantic_equality_test.go @@ -9,6 +9,7 @@ import ( "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/internal/fwschemadata" @@ -190,6 +191,89 @@ func TestValueSemanticEquality(t *testing.T) { }, }, }, + "Int32Value": { + request: fwschemadata.ValueSemanticEqualityRequest{ + Path: path.Root("test"), + PriorValue: types.Int32Value(12), + ProposedNewValue: types.Int32Value(24), + }, + expected: &fwschemadata.ValueSemanticEqualityResponse{ + NewValue: types.Int32Value(24), + }, + }, + "Int32ValuableWithSemanticEquals-true": { + request: fwschemadata.ValueSemanticEqualityRequest{ + Path: path.Root("test"), + PriorValue: testtypes.Int32ValueWithSemanticEquals{ + Int32Value: types.Int32Value(12), + SemanticEquals: true, + }, + ProposedNewValue: testtypes.Int32ValueWithSemanticEquals{ + Int32Value: types.Int32Value(24), + SemanticEquals: true, + }, + }, + expected: &fwschemadata.ValueSemanticEqualityResponse{ + NewValue: testtypes.Int32ValueWithSemanticEquals{ + Int32Value: types.Int32Value(12), + SemanticEquals: true, + }, + }, + }, + "Int32ValuableWithSemanticEquals-false": { + request: fwschemadata.ValueSemanticEqualityRequest{ + Path: path.Root("test"), + PriorValue: testtypes.Int32ValueWithSemanticEquals{ + Int32Value: types.Int32Value(12), + SemanticEquals: false, + }, + ProposedNewValue: testtypes.Int32ValueWithSemanticEquals{ + Int32Value: types.Int32Value(24), + SemanticEquals: false, + }, + }, + expected: &fwschemadata.ValueSemanticEqualityResponse{ + NewValue: testtypes.Int32ValueWithSemanticEquals{ + Int32Value: types.Int32Value(24), + SemanticEquals: false, + }, + }, + }, + "Int32ValuableWithSemanticEquals-diagnostics": { + request: fwschemadata.ValueSemanticEqualityRequest{ + Path: path.Root("test"), + PriorValue: testtypes.Int32ValueWithSemanticEquals{ + Int32Value: types.Int32Value(12), + SemanticEquals: false, + SemanticEqualsDiagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic("test summary 1", "test detail 1"), + diag.NewErrorDiagnostic("test summary 2", "test detail 2"), + }, + }, + ProposedNewValue: testtypes.Int32ValueWithSemanticEquals{ + Int32Value: types.Int32Value(24), + 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.Int32ValueWithSemanticEquals{ + Int32Value: types.Int32Value(24), + 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"), + }, + }, + }, "Int64Value": { request: fwschemadata.ValueSemanticEqualityRequest{ Path: path.Root("test"), diff --git a/internal/fwserver/attr_type.go b/internal/fwserver/attr_type.go index 85dd3bff0..c9665eabb 100644 --- a/internal/fwserver/attr_type.go +++ b/internal/fwserver/attr_type.go @@ -41,6 +41,21 @@ func coerceFloat64Typable(ctx context.Context, schemaPath path.Path, valuable ba return typable, nil } +func coerceInt32Typable(ctx context.Context, schemaPath path.Path, valuable basetypes.Int32Valuable) (basetypes.Int32Typable, diag.Diagnostics) { + typable, ok := valuable.Type(ctx).(basetypes.Int32Typable) + + // 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 coerceInt64Typable(ctx context.Context, schemaPath path.Path, valuable basetypes.Int64Valuable) (basetypes.Int64Typable, diag.Diagnostics) { typable, ok := valuable.Type(ctx).(basetypes.Int64Typable) diff --git a/internal/fwserver/attribute_plan_modification.go b/internal/fwserver/attribute_plan_modification.go index feaae9000..bd12b7fee 100644 --- a/internal/fwserver/attribute_plan_modification.go +++ b/internal/fwserver/attribute_plan_modification.go @@ -94,6 +94,8 @@ func AttributeModifyPlan(ctx context.Context, a fwschema.Attribute, req ModifyAt AttributePlanModifyBool(ctx, attributeWithPlanModifiers, req, resp) case fwxschema.AttributeWithFloat64PlanModifiers: AttributePlanModifyFloat64(ctx, attributeWithPlanModifiers, req, resp) + case fwxschema.AttributeWithInt32PlanModifiers: + AttributePlanModifyInt32(ctx, attributeWithPlanModifiers, req, resp) case fwxschema.AttributeWithInt64PlanModifiers: AttributePlanModifyInt64(ctx, attributeWithPlanModifiers, req, resp) case fwxschema.AttributeWithListPlanModifiers: @@ -1001,6 +1003,166 @@ func AttributePlanModifyFloat64(ctx context.Context, attribute fwxschema.Attribu } } +// AttributePlanModifyInt32 performs all types.Int32 plan modification. +func AttributePlanModifyInt32(ctx context.Context, attribute fwxschema.AttributeWithInt32PlanModifiers, req ModifyAttributePlanRequest, resp *ModifyAttributePlanResponse) { + // Use basetypes.Int32Valuable 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.Int32Valuable) + + if !ok { + resp.Diagnostics.AddAttributeError( + req.AttributePath, + "Invalid Int32 Attribute Plan Modifier Value Type", + "An unexpected value type was encountered while attempting to perform Int32 attribute plan modification. "+ + "The value type must implement the basetypes.Int32Valuable interface. "+ + "Please report this to the provider developers.\n\n"+ + fmt.Sprintf("Incoming Value Type: %T", req.AttributeConfig), + ) + + return + } + + configValue, diags := configValuable.ToInt32Value(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.Int32Valuable) + + if !ok { + resp.Diagnostics.AddAttributeError( + req.AttributePath, + "Invalid Int32 Attribute Plan Modifier Value Type", + "An unexpected value type was encountered while attempting to perform Int32 attribute plan modification. "+ + "The value type must implement the basetypes.Int32Valuable interface. "+ + "Please report this to the provider developers.\n\n"+ + fmt.Sprintf("Incoming Value Type: %T", req.AttributePlan), + ) + + return + } + + planValue, diags := planValuable.ToInt32Value(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.Int32Valuable) + + if !ok { + resp.Diagnostics.AddAttributeError( + req.AttributePath, + "Invalid Int32 Attribute Plan Modifier Value Type", + "An unexpected value type was encountered while attempting to perform Int32 attribute plan modification. "+ + "The value type must implement the basetypes.Int32Valuable interface. "+ + "Please report this to the provider developers.\n\n"+ + fmt.Sprintf("Incoming Value Type: %T", req.AttributeState), + ) + + return + } + + stateValue, diags := stateValuable.ToInt32Value(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 := coerceInt32Typable(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.Int32Request{ + 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.Int32PlanModifiers() { + // Instantiate a new response for each request to prevent plan modifiers + // from modifying or removing diagnostics. + planModifyResp := &planmodifier.Int32Response{ + PlanValue: planModifyReq.PlanValue, + Private: resp.Private, + } + + logging.FrameworkTrace( + ctx, + "Calling provider defined planmodifier.Int32", + map[string]interface{}{ + logging.KeyDescription: planModifier.Description(ctx), + }, + ) + + planModifier.PlanModifyInt32(ctx, planModifyReq, planModifyResp) + + logging.FrameworkTrace( + ctx, + "Called provider defined planmodifier.Int32", + 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.ValueFromInt32(ctx, planModifyResp.PlanValue) + + resp.Diagnostics.Append(valueFromDiags...) + + // Only on new errors. + if valueFromDiags.HasError() { + return + } + + resp.AttributePlan = valuable + } +} + // AttributePlanModifyInt64 performs all types.Int64 plan modification. func AttributePlanModifyInt64(ctx context.Context, attribute fwxschema.AttributeWithInt64PlanModifiers, req ModifyAttributePlanRequest, resp *ModifyAttributePlanResponse) { // Use basetypes.Int64Valuable 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 1a7446898..ee4553fd8 100644 --- a/internal/fwserver/attribute_plan_modification_test.go +++ b/internal/fwserver/attribute_plan_modification_test.go @@ -4579,6 +4579,642 @@ func TestAttributePlanModifyFloat64(t *testing.T) { } } +func TestAttributePlanModifyInt32(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute fwxschema.AttributeWithInt32PlanModifiers + request ModifyAttributePlanRequest + response *ModifyAttributePlanResponse + expected *ModifyAttributePlanResponse + }{ + "request-path": { + attribute: testschema.AttributeWithInt32PlanModifiers{ + PlanModifiers: []planmodifier.Int32{ + testplanmodifier.Int32{ + PlanModifyInt32Method: func(ctx context.Context, req planmodifier.Int32Request, resp *planmodifier.Int32Response) { + got := req.Path + expected := path.Root("test") + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected Int32Request.Path", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.Int32Value(1), + AttributePlan: types.Int32Value(1), + AttributeState: types.Int32Value(1), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.Int32Value(1), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.Int32Value(1), + }, + }, + "request-pathexpression": { + attribute: testschema.AttributeWithInt32PlanModifiers{ + PlanModifiers: []planmodifier.Int32{ + testplanmodifier.Int32{ + PlanModifyInt32Method: func(ctx context.Context, req planmodifier.Int32Request, resp *planmodifier.Int32Response) { + got := req.PathExpression + expected := path.MatchRoot("test") + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected Int32Request.PathExpression", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), + AttributeConfig: types.Int32Value(1), + AttributePlan: types.Int32Value(1), + AttributeState: types.Int32Value(1), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.Int32Value(1), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.Int32Value(1), + }, + }, + "request-config": { + attribute: testschema.AttributeWithInt32PlanModifiers{ + PlanModifiers: []planmodifier.Int32{ + testplanmodifier.Int32{ + PlanModifyInt32Method: func(ctx context.Context, req planmodifier.Int32Request, resp *planmodifier.Int32Response) { + 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 Int32Request.Config", + fmt.Sprintf("expected %s, got: %s", expected.Raw, got.Raw), + ) + } + }, + }, + }, + }, + request: ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.Int32Value(1), + AttributePlan: types.Int32Value(1), + AttributeState: types.Int32Value(1), + 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.Int32Value(1), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.Int32Value(1), + }, + }, + "request-configvalue": { + attribute: testschema.AttributeWithInt32PlanModifiers{ + PlanModifiers: []planmodifier.Int32{ + testplanmodifier.Int32{ + PlanModifyInt32Method: func(ctx context.Context, req planmodifier.Int32Request, resp *planmodifier.Int32Response) { + got := req.ConfigValue + expected := types.Int32Value(1) + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected Int32Request.ConfigValue", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.Int32Value(1), + AttributePlan: types.Int32Null(), + AttributeState: types.Int32Null(), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.Int32Null(), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.Int32Null(), + }, + }, + "request-plan": { + attribute: testschema.AttributeWithInt32PlanModifiers{ + PlanModifiers: []planmodifier.Int32{ + testplanmodifier.Int32{ + PlanModifyInt32Method: func(ctx context.Context, req planmodifier.Int32Request, resp *planmodifier.Int32Response) { + 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 Int32Request.Plan", + fmt.Sprintf("expected %s, got: %s", expected.Raw, got.Raw), + ) + } + }, + }, + }, + }, + request: ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.Int32Value(1), + AttributePlan: types.Int32Value(1), + AttributeState: types.Int32Value(1), + 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.Int32Value(1), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.Int32Value(1), + }, + }, + "request-planvalue": { + attribute: testschema.AttributeWithInt32PlanModifiers{ + PlanModifiers: []planmodifier.Int32{ + testplanmodifier.Int32{ + PlanModifyInt32Method: func(ctx context.Context, req planmodifier.Int32Request, resp *planmodifier.Int32Response) { + got := req.PlanValue + expected := types.Int32Value(1) + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected Int32Request.PlanValue", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.Int32Null(), + AttributePlan: types.Int32Value(1), + AttributeState: types.Int32Null(), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.Int32Value(1), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.Int32Value(1), + }, + }, + "request-private": { + attribute: testschema.AttributeWithInt32PlanModifiers{ + PlanModifiers: []planmodifier.Int32{ + testplanmodifier.Int32{ + PlanModifyInt32Method: func(ctx context.Context, req planmodifier.Int32Request, resp *planmodifier.Int32Response) { + 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 Int32Request.Private", + diff, + ) + } + }, + }, + }, + }, + request: ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.Int32Null(), + AttributePlan: types.Int32Value(1), + AttributeState: types.Int32Null(), + Private: privatestate.MustProviderData( + context.Background(), + privatestate.MustMarshalToJson(map[string][]byte{ + "testkey": []byte(`{"testproperty":true}`), + }), + ), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.Int32Value(1), + Private: privatestate.MustProviderData( + context.Background(), + privatestate.MustMarshalToJson(map[string][]byte{ + "testkey": []byte(`{"testproperty":true}`), // copied from request + }), + ), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.Int32Value(1), + Private: privatestate.MustProviderData( + context.Background(), + privatestate.MustMarshalToJson(map[string][]byte{ + "testkey": []byte(`{"testproperty":true}`), + }), + ), + }, + }, + "request-state": { + attribute: testschema.AttributeWithInt32PlanModifiers{ + PlanModifiers: []planmodifier.Int32{ + testplanmodifier.Int32{ + PlanModifyInt32Method: func(ctx context.Context, req planmodifier.Int32Request, resp *planmodifier.Int32Response) { + 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 Int32Request.State", + fmt.Sprintf("expected %s, got: %s", expected.Raw, got.Raw), + ) + } + }, + }, + }, + }, + request: ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.Int32Value(1), + AttributePlan: types.Int32Value(1), + AttributeState: types.Int32Value(1), + 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.Int32Value(1), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.Int32Value(1), + }, + }, + "request-statevalue": { + attribute: testschema.AttributeWithInt32PlanModifiers{ + PlanModifiers: []planmodifier.Int32{ + testplanmodifier.Int32{ + PlanModifyInt32Method: func(ctx context.Context, req planmodifier.Int32Request, resp *planmodifier.Int32Response) { + got := req.StateValue + expected := types.Int32Value(1) + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected Int32Request.StateValue", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.Int32Null(), + AttributePlan: types.Int32Null(), + AttributeState: types.Int32Value(1), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.Int32Null(), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.Int32Null(), + }, + }, + "response-diagnostics": { + attribute: testschema.AttributeWithInt32PlanModifiers{ + PlanModifiers: []planmodifier.Int32{ + testplanmodifier.Int32{ + PlanModifyInt32Method: func(ctx context.Context, req planmodifier.Int32Request, resp *planmodifier.Int32Response) { + 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.Int32Value(1), + AttributePlan: types.Int32Value(1), + AttributeState: types.Int32Value(1), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.Int32Value(1), + 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.Int32Value(1), + 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.AttributeWithInt32PlanModifiers{ + PlanModifiers: []planmodifier.Int32{ + testplanmodifier.Int32{ + PlanModifyInt32Method: func(ctx context.Context, req planmodifier.Int32Request, resp *planmodifier.Int32Response) { + resp.PlanValue = types.Int32Value(1) + }, + }, + }, + }, + request: ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.Int32Null(), + AttributePlan: types.Int32Unknown(), + AttributeState: types.Int32Null(), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.Int32Unknown(), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.Int32Value(1), + }, + }, + "response-planvalue-custom-type": { + attribute: testschema.AttributeWithInt32PlanModifiers{ + PlanModifiers: []planmodifier.Int32{ + testplanmodifier.Int32{ + PlanModifyInt32Method: func(ctx context.Context, req planmodifier.Int32Request, resp *planmodifier.Int32Response) { + resp.PlanValue = types.Int32Value(1) + }, + }, + }, + }, + request: ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: testtypes.Int32ValueWithSemanticEquals{ + Int32Value: types.Int32Null(), + }, + AttributePlan: testtypes.Int32ValueWithSemanticEquals{ + Int32Value: types.Int32Unknown(), + }, + AttributeState: testtypes.Int32ValueWithSemanticEquals{ + Int32Value: types.Int32Null(), + }, + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: testtypes.Int32ValueWithSemanticEquals{ + Int32Value: types.Int32Unknown(), + }, + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: testtypes.Int32ValueWithSemanticEquals{ + Int32Value: types.Int32Value(1), + }, + }, + }, + "response-private": { + attribute: testschema.AttributeWithInt32PlanModifiers{ + PlanModifiers: []planmodifier.Int32{ + testplanmodifier.Int32{ + PlanModifyInt32Method: func(ctx context.Context, req planmodifier.Int32Request, resp *planmodifier.Int32Response) { + resp.Diagnostics.Append( + resp.Private.SetKey(ctx, "testkey", []byte(`{"newtestproperty":true}`))..., + ) + }, + }, + }, + }, + request: ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.Int32Null(), + AttributePlan: types.Int32Value(1), + AttributeState: types.Int32Null(), + Private: privatestate.MustProviderData( + context.Background(), + privatestate.MustMarshalToJson(map[string][]byte{ + "testkey": []byte(`{"testproperty":true}`), + }), + ), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.Int32Value(1), + Private: privatestate.MustProviderData( + context.Background(), + privatestate.MustMarshalToJson(map[string][]byte{ + "testkey": []byte(`{"testproperty":true}`), // copied from request + }), + ), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.Int32Value(1), + Private: privatestate.MustProviderData( + context.Background(), + privatestate.MustMarshalToJson(map[string][]byte{ + "testkey": []byte(`{"newtestproperty":true}`), + }), + ), + }, + }, + "response-requiresreplace-add": { + attribute: testschema.AttributeWithInt32PlanModifiers{ + PlanModifiers: []planmodifier.Int32{ + testplanmodifier.Int32{ + PlanModifyInt32Method: func(ctx context.Context, req planmodifier.Int32Request, resp *planmodifier.Int32Response) { + resp.RequiresReplace = true + }, + }, + }, + }, + request: ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.Int32Value(1), + AttributePlan: types.Int32Value(1), + AttributeState: types.Int32Value(2), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.Int32Value(1), + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.Int32Value(1), + RequiresReplace: path.Paths{ + path.Root("test"), + }, + }, + }, + "response-requiresreplace-false": { + attribute: testschema.AttributeWithInt32PlanModifiers{ + PlanModifiers: []planmodifier.Int32{ + testplanmodifier.Int32{ + PlanModifyInt32Method: func(ctx context.Context, req planmodifier.Int32Request, resp *planmodifier.Int32Response) { + resp.RequiresReplace = false // same as not being set + }, + }, + }, + }, + request: ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.Int32Value(1), + AttributePlan: types.Int32Value(1), + AttributeState: types.Int32Value(2), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.Int32Value(1), + RequiresReplace: path.Paths{ + path.Root("test"), // Set by prior plan modifier + }, + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.Int32Value(1), + RequiresReplace: path.Paths{ + path.Root("test"), // Remains as it should not be removed + }, + }, + }, + "response-requiresreplace-update": { + attribute: testschema.AttributeWithInt32PlanModifiers{ + PlanModifiers: []planmodifier.Int32{ + testplanmodifier.Int32{ + PlanModifyInt32Method: func(ctx context.Context, req planmodifier.Int32Request, resp *planmodifier.Int32Response) { + resp.RequiresReplace = true + }, + }, + }, + }, + request: ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.Int32Value(1), + AttributePlan: types.Int32Value(1), + AttributeState: types.Int32Value(2), + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: types.Int32Value(1), + RequiresReplace: path.Paths{ + path.Root("test"), // Set by prior plan modifier + }, + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: types.Int32Value(1), + 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() + + AttributePlanModifyInt32(context.Background(), testCase.attribute, testCase.request, testCase.response) + + if diff := cmp.Diff(testCase.response, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestAttributePlanModifyInt64(t *testing.T) { t.Parallel() diff --git a/internal/fwserver/attribute_validation.go b/internal/fwserver/attribute_validation.go index 1fa4883b0..d5b51dbaa 100644 --- a/internal/fwserver/attribute_validation.go +++ b/internal/fwserver/attribute_validation.go @@ -113,6 +113,8 @@ func AttributeValidate(ctx context.Context, a fwschema.Attribute, req ValidateAt AttributeValidateBool(ctx, attributeWithValidators, req, resp) case fwxschema.AttributeWithFloat64Validators: AttributeValidateFloat64(ctx, attributeWithValidators, req, resp) + case fwxschema.AttributeWithInt32Validators: + AttributeValidateInt32(ctx, attributeWithValidators, req, resp) case fwxschema.AttributeWithInt64Validators: AttributeValidateInt64(ctx, attributeWithValidators, req, resp) case fwxschema.AttributeWithListValidators: @@ -294,6 +296,71 @@ func AttributeValidateFloat64(ctx context.Context, attribute fwxschema.Attribute } } +// AttributeValidateInt32 performs all types.Int32 validation. +func AttributeValidateInt32(ctx context.Context, attribute fwxschema.AttributeWithInt32Validators, req ValidateAttributeRequest, resp *ValidateAttributeResponse) { + // Use basetypes.Int32Valuable 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.Int32Valuable) + + if !ok { + resp.Diagnostics.AddAttributeError( + req.AttributePath, + "Invalid Int32 Attribute Validator Value Type", + "An unexpected value type was encountered while attempting to perform Int32 attribute validation. "+ + "The value type must implement the basetypes.Int32Valuable interface. "+ + "Please report this to the provider developers.\n\n"+ + fmt.Sprintf("Incoming Value Type: %T", req.AttributeConfig), + ) + + return + } + + configValue, diags := configValuable.ToInt32Value(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.Int32Request{ + Config: req.Config, + ConfigValue: configValue, + Path: req.AttributePath, + PathExpression: req.AttributePathExpression, + } + + for _, attributeValidator := range attribute.Int32Validators() { + // Instantiate a new response for each request to prevent validators + // from modifying or removing diagnostics. + validateResp := &validator.Int32Response{} + + logging.FrameworkTrace( + ctx, + "Calling provider defined validator.Int32", + map[string]interface{}{ + logging.KeyDescription: attributeValidator.Description(ctx), + }, + ) + + attributeValidator.ValidateInt32(ctx, validateReq, validateResp) + + logging.FrameworkTrace( + ctx, + "Called provider defined validator.Int32", + map[string]interface{}{ + logging.KeyDescription: attributeValidator.Description(ctx), + }, + ) + + resp.Diagnostics.Append(validateResp.Diagnostics...) + } +} + // AttributeValidateInt64 performs all types.Int64 validation. func AttributeValidateInt64(ctx context.Context, attribute fwxschema.AttributeWithInt64Validators, req ValidateAttributeRequest, resp *ValidateAttributeResponse) { // Use basetypes.Int64Valuable until custom types cannot re-implement diff --git a/internal/fwserver/attribute_validation_test.go b/internal/fwserver/attribute_validation_test.go index c649f63e2..b0d076d8e 100644 --- a/internal/fwserver/attribute_validation_test.go +++ b/internal/fwserver/attribute_validation_test.go @@ -2134,6 +2134,210 @@ func TestAttributeValidateFloat64(t *testing.T) { } } +func TestAttributeValidateInt32(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute fwxschema.AttributeWithInt32Validators + request ValidateAttributeRequest + response *ValidateAttributeResponse + expected *ValidateAttributeResponse + }{ + "request-path": { + attribute: testschema.AttributeWithInt32Validators{ + Validators: []validator.Int32{ + testvalidator.Int32{ + ValidateInt32Method: func(ctx context.Context, req validator.Int32Request, resp *validator.Int32Response) { + got := req.Path + expected := path.Root("test") + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected Int32Request.Path", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.Int32Value(123), + }, + response: &ValidateAttributeResponse{}, + expected: &ValidateAttributeResponse{}, + }, + "request-pathexpression": { + attribute: testschema.AttributeWithInt32Validators{ + Validators: []validator.Int32{ + testvalidator.Int32{ + ValidateInt32Method: func(ctx context.Context, req validator.Int32Request, resp *validator.Int32Response) { + got := req.PathExpression + expected := path.MatchRoot("test") + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected Int32Request.PathExpression", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), + AttributeConfig: types.Int32Value(123), + }, + response: &ValidateAttributeResponse{}, + expected: &ValidateAttributeResponse{}, + }, + "request-config": { + attribute: testschema.AttributeWithInt32Validators{ + Validators: []validator.Int32{ + testvalidator.Int32{ + ValidateInt32Method: func(ctx context.Context, req validator.Int32Request, resp *validator.Int32Response) { + 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, 123), + }, + ), + } + + if !got.Raw.Equal(expected.Raw) { + resp.Diagnostics.AddError( + "Unexpected Int32Request.Config", + fmt.Sprintf("expected %s, got: %s", expected.Raw, got.Raw), + ) + } + }, + }, + }, + }, + request: ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.Int32Value(123), + 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, 123), + }, + ), + }, + }, + response: &ValidateAttributeResponse{}, + expected: &ValidateAttributeResponse{}, + }, + "request-configvalue": { + attribute: testschema.AttributeWithInt32Validators{ + Validators: []validator.Int32{ + testvalidator.Int32{ + ValidateInt32Method: func(ctx context.Context, req validator.Int32Request, resp *validator.Int32Response) { + got := req.ConfigValue + expected := types.Int32Value(123) + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected Int32Request.ConfigValue", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.Int32Value(123), + }, + response: &ValidateAttributeResponse{}, + expected: &ValidateAttributeResponse{}, + }, + "response-diagnostics": { + attribute: testschema.AttributeWithInt32Validators{ + Validators: []validator.Int32{ + testvalidator.Int32{ + ValidateInt32Method: func(ctx context.Context, req validator.Int32Request, resp *validator.Int32Response) { + 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.Int32Value(123), + }, + 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() + + AttributeValidateInt32(context.Background(), testCase.attribute, testCase.request, testCase.response) + + if diff := cmp.Diff(testCase.response, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestAttributeValidateInt64(t *testing.T) { t.Parallel() diff --git a/internal/fwserver/server_planresourcechange.go b/internal/fwserver/server_planresourcechange.go index fc7413e38..54402da20 100644 --- a/internal/fwserver/server_planresourcechange.go +++ b/internal/fwserver/server_planresourcechange.go @@ -412,6 +412,10 @@ func MarkComputedNilsAsUnknown(ctx context.Context, config tftypes.Value, resour if a.Float64DefaultValue() != nil { return val, nil } + case fwschema.AttributeWithInt32DefaultValue: + if a.Int32DefaultValue() != nil { + return val, nil + } case fwschema.AttributeWithInt64DefaultValue: if a.Int64DefaultValue() != nil { return val, nil diff --git a/internal/fwserver/server_planresourcechange_test.go b/internal/fwserver/server_planresourcechange_test.go index 565fe5370..ef8757b03 100644 --- a/internal/fwserver/server_planresourcechange_test.go +++ b/internal/fwserver/server_planresourcechange_test.go @@ -28,6 +28,7 @@ import ( "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/float64default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int32default" "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" "github.com/hashicorp/terraform-plugin-framework/resource/schema/listdefault" "github.com/hashicorp/terraform-plugin-framework/resource/schema/mapdefault" @@ -432,6 +433,7 @@ func TestServerPlanResourceChange(t *testing.T) { AttributeTypes: map[string]tftypes.Type{ "test_computed_bool": tftypes.Bool, "test_computed_float64": tftypes.Number, + "test_computed_int32": tftypes.Number, "test_computed_int64": tftypes.Number, "test_computed_list": tftypes.List{ElementType: tftypes.String}, "test_computed_map": tftypes.Map{ElementType: tftypes.String}, @@ -451,6 +453,7 @@ func TestServerPlanResourceChange(t *testing.T) { "test_computed_nested_single_attribute": tftypes.Object{AttributeTypes: map[string]tftypes.Type{"string_attribute": tftypes.String}}, "test_configured_bool": tftypes.Bool, "test_configured_float64": tftypes.Number, + "test_configured_int32": tftypes.Number, "test_configured_int64": tftypes.Number, "test_configured_list": tftypes.List{ElementType: tftypes.String}, "test_configured_map": tftypes.Map{ElementType: tftypes.String}, @@ -563,6 +566,10 @@ func TestServerPlanResourceChange(t *testing.T) { Computed: true, Default: float64default.StaticFloat64(1.2345), }, + "test_computed_int32": schema.Int32Attribute{ + Computed: true, + Default: int32default.StaticInt32(12345), + }, "test_computed_int64": schema.Int64Attribute{ Computed: true, Default: int64default.StaticInt64(12345), @@ -750,6 +757,11 @@ func TestServerPlanResourceChange(t *testing.T) { Computed: true, Default: float64default.StaticFloat64(1.2345), }, + "test_configured_int32": schema.Int32Attribute{ + Optional: true, + Computed: true, + Default: int32default.StaticInt32(12345), + }, "test_configured_int64": schema.Int64Attribute{ Optional: true, Computed: true, @@ -1345,6 +1357,7 @@ func TestServerPlanResourceChange(t *testing.T) { Raw: tftypes.NewValue(testSchemaTypeDefault, map[string]tftypes.Value{ "test_computed_bool": tftypes.NewValue(tftypes.Bool, 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), "test_computed_list": tftypes.NewValue(tftypes.List{ElementType: tftypes.String}, nil), "test_computed_map": tftypes.NewValue(tftypes.Map{ElementType: tftypes.String}, nil), @@ -1467,6 +1480,7 @@ func TestServerPlanResourceChange(t *testing.T) { ), "test_configured_bool": tftypes.NewValue(tftypes.Bool, true), "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), "test_configured_list": tftypes.NewValue( tftypes.List{ElementType: tftypes.String}, @@ -1719,6 +1733,7 @@ func TestServerPlanResourceChange(t *testing.T) { Raw: tftypes.NewValue(testSchemaTypeDefault, map[string]tftypes.Value{ "test_computed_bool": tftypes.NewValue(tftypes.Bool, 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), "test_computed_list": tftypes.NewValue(tftypes.List{ElementType: tftypes.String}, nil), "test_computed_map": tftypes.NewValue(tftypes.Map{ElementType: tftypes.String}, nil), @@ -1841,6 +1856,7 @@ func TestServerPlanResourceChange(t *testing.T) { ), "test_configured_bool": tftypes.NewValue(tftypes.Bool, true), "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), "test_configured_list": tftypes.NewValue( tftypes.List{ElementType: tftypes.String}, @@ -2098,6 +2114,7 @@ func TestServerPlanResourceChange(t *testing.T) { Raw: tftypes.NewValue(testSchemaTypeDefault, map[string]tftypes.Value{ "test_computed_bool": tftypes.NewValue(tftypes.Bool, true), "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), "test_computed_list": tftypes.NewValue(tftypes.List{ElementType: tftypes.String}, []tftypes.Value{tftypes.NewValue(tftypes.String, "default")}), "test_computed_map": tftypes.NewValue(tftypes.Map{ElementType: tftypes.String}, map[string]tftypes.Value{"a": tftypes.NewValue(tftypes.String, "default")}), @@ -2255,6 +2272,7 @@ func TestServerPlanResourceChange(t *testing.T) { ), "test_configured_bool": tftypes.NewValue(tftypes.Bool, true), "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), "test_configured_list": tftypes.NewValue( tftypes.List{ElementType: tftypes.String}, @@ -3849,6 +3867,7 @@ func TestServerPlanResourceChange(t *testing.T) { Raw: tftypes.NewValue(testSchemaTypeDefault, map[string]tftypes.Value{ "test_computed_bool": tftypes.NewValue(tftypes.Bool, 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), "test_computed_list": tftypes.NewValue(tftypes.List{ElementType: tftypes.String}, nil), "test_computed_map": tftypes.NewValue(tftypes.Map{ElementType: tftypes.String}, nil), @@ -3971,6 +3990,7 @@ func TestServerPlanResourceChange(t *testing.T) { ), "test_configured_bool": tftypes.NewValue(tftypes.Bool, true), "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), "test_configured_list": tftypes.NewValue( tftypes.List{ElementType: tftypes.String}, @@ -4225,6 +4245,7 @@ func TestServerPlanResourceChange(t *testing.T) { Raw: tftypes.NewValue(testSchemaTypeDefault, map[string]tftypes.Value{ "test_computed_bool": tftypes.NewValue(tftypes.Bool, 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), "test_computed_list": tftypes.NewValue(tftypes.List{ElementType: tftypes.String}, nil), "test_computed_map": tftypes.NewValue(tftypes.Map{ElementType: tftypes.String}, nil), @@ -4348,6 +4369,7 @@ func TestServerPlanResourceChange(t *testing.T) { "test_configured_bool": tftypes.NewValue(tftypes.Bool, true), "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), "test_configured_list": tftypes.NewValue( tftypes.List{ElementType: tftypes.String}, []tftypes.Value{ @@ -4601,6 +4623,7 @@ func TestServerPlanResourceChange(t *testing.T) { Raw: tftypes.NewValue(testSchemaTypeDefault, map[string]tftypes.Value{ "test_computed_bool": tftypes.NewValue(tftypes.Bool, false), "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), "test_computed_list": tftypes.NewValue(tftypes.List{ElementType: tftypes.String}, []tftypes.Value{tftypes.NewValue(tftypes.String, "prior-state")}), "test_computed_map": tftypes.NewValue(tftypes.Map{ElementType: tftypes.String}, map[string]tftypes.Value{"a": tftypes.NewValue(tftypes.String, "prior-state")}), @@ -4758,6 +4781,7 @@ func TestServerPlanResourceChange(t *testing.T) { ), "test_configured_bool": tftypes.NewValue(tftypes.Bool, false), "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), "test_configured_list": tftypes.NewValue( tftypes.List{ElementType: tftypes.String}, @@ -5030,6 +5054,7 @@ func TestServerPlanResourceChange(t *testing.T) { Raw: tftypes.NewValue(testSchemaTypeDefault, map[string]tftypes.Value{ "test_computed_bool": tftypes.NewValue(tftypes.Bool, true), "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), "test_computed_list": tftypes.NewValue(tftypes.List{ElementType: tftypes.String}, []tftypes.Value{tftypes.NewValue(tftypes.String, "default")}), "test_computed_map": tftypes.NewValue(tftypes.Map{ElementType: tftypes.String}, map[string]tftypes.Value{"a": tftypes.NewValue(tftypes.String, "default")}), @@ -5187,6 +5212,7 @@ func TestServerPlanResourceChange(t *testing.T) { ), "test_configured_bool": tftypes.NewValue(tftypes.Bool, true), "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), "test_configured_list": tftypes.NewValue( tftypes.List{ElementType: tftypes.String}, @@ -6930,6 +6956,290 @@ func TestServerPlanResourceChange_AttributeRoundtrip(t *testing.T) { }, ), }), + "create-int32-computed-null": generateTestCase(testCaseData{ + Schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "int32_attribute": schema.Int32Attribute{ + Computed: true, + }, + }, + }, + Config: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "int32_attribute": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "int32_attribute": tftypes.NewValue(tftypes.Number, nil), + }, + ), + ProposedNewState: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "int32_attribute": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "int32_attribute": tftypes.NewValue(tftypes.Number, nil), + }, + ), + PriorState: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "int32_attribute": tftypes.Number, + }, + }, + nil, + ), + PlannedState: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "int32_attribute": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "int32_attribute": tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), // Compute nulls as unknown + }, + ), + }), + "create-int32-computed-unknown": generateTestCase(testCaseData{ + Schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "int32_attribute": schema.Int32Attribute{ + Computed: true, + }, + }, + }, + Config: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "int32_attribute": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "int32_attribute": tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), + }, + ), + ProposedNewState: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "int32_attribute": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "int32_attribute": tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), + }, + ), + PriorState: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "int32_attribute": tftypes.Number, + }, + }, + nil, + ), + PlannedState: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "int32_attribute": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "int32_attribute": tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), + }, + ), + }), + "create-int32-optional-null": generateTestCase(testCaseData{ + Schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "int32_attribute": schema.Int32Attribute{ + Optional: true, + }, + }, + }, + Config: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "int32_attribute": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "int32_attribute": tftypes.NewValue(tftypes.Number, nil), + }, + ), + ProposedNewState: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "int32_attribute": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "int32_attribute": tftypes.NewValue(tftypes.Number, nil), + }, + ), + PriorState: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "int32_attribute": tftypes.Number, + }, + }, + nil, + ), + PlannedState: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "int32_attribute": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "int32_attribute": tftypes.NewValue(tftypes.Number, nil), + }, + ), + }), + "create-int32-optional-unknown": generateTestCase(testCaseData{ + Schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "int32_attribute": schema.Int32Attribute{ + Optional: true, + }, + }, + }, + Config: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "int32_attribute": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "int32_attribute": tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), + }, + ), + ProposedNewState: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "int32_attribute": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "int32_attribute": tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), + }, + ), + PriorState: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "int32_attribute": tftypes.Number, + }, + }, + nil, + ), + PlannedState: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "int32_attribute": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "int32_attribute": tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), + }, + ), + }), + "create-int32-optional-and-computed-null": generateTestCase(testCaseData{ + Schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "int32_attribute": schema.Int32Attribute{ + Optional: true, + Computed: true, + }, + }, + }, + Config: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "int32_attribute": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "int32_attribute": tftypes.NewValue(tftypes.Number, nil), + }, + ), + ProposedNewState: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "int32_attribute": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "int32_attribute": tftypes.NewValue(tftypes.Number, nil), + }, + ), + PriorState: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "int32_attribute": tftypes.Number, + }, + }, + nil, + ), + PlannedState: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "int32_attribute": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "int32_attribute": tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), // Computed nulls as unknown + }, + ), + }), + "create-int32-optional-and-computed-unknown": generateTestCase(testCaseData{ + Schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "int32_attribute": schema.Int32Attribute{ + Optional: true, + Computed: true, + }, + }, + }, + Config: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "int32_attribute": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "int32_attribute": tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), + }, + ), + ProposedNewState: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "int32_attribute": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "int32_attribute": tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), + }, + ), + PriorState: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "int32_attribute": tftypes.Number, + }, + }, + nil, + ), + PlannedState: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "int32_attribute": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "int32_attribute": tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), + }, + ), + }), "create-int64-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 99931d2b8..dee787140 100644 --- a/internal/fwtype/missing_underlying_type_validation_test.go +++ b/internal/fwtype/missing_underlying_type_validation_test.go @@ -7,6 +7,7 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwtype" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" @@ -40,6 +41,10 @@ func TestContainsMissingUnderlyingType(t *testing.T) { attrTyp: testtypes.Float64Type{}, expected: false, }, + "custom-int32": { + attrTyp: testtypes.Int32Type{}, + expected: false, + }, "custom-int64": { attrTyp: testtypes.Int64Type{}, expected: false, @@ -91,8 +96,12 @@ func TestContainsMissingUnderlyingType(t *testing.T) { attrTyp: types.Float64Type, expected: false, }, + "int32": { + attrTyp: types.Int32Type, + expected: false, + }, "int64": { - attrTyp: types.Float64Type, + attrTyp: types.Int64Type, expected: false, }, "list-nil": { @@ -123,6 +132,12 @@ func TestContainsMissingUnderlyingType(t *testing.T) { }, expected: false, }, + "list-custom-int32": { + attrTyp: types.ListType{ + ElemType: testtypes.Int32Type{}, + }, + expected: false, + }, "list-custom-int64": { attrTyp: types.ListType{ ElemType: testtypes.Int64Type{}, @@ -253,6 +268,12 @@ func TestContainsMissingUnderlyingType(t *testing.T) { }, expected: false, }, + "list-int32": { + attrTyp: types.ListType{ + ElemType: types.Int32Type, + }, + expected: false, + }, "list-int64": { attrTyp: types.ListType{ ElemType: types.Int64Type, @@ -396,6 +417,12 @@ func TestContainsMissingUnderlyingType(t *testing.T) { }, expected: false, }, + "map-custom-int32": { + attrTyp: types.MapType{ + ElemType: testtypes.Int32Type{}, + }, + expected: false, + }, "map-custom-int64": { attrTyp: types.MapType{ ElemType: testtypes.Int64Type{}, @@ -526,6 +553,12 @@ func TestContainsMissingUnderlyingType(t *testing.T) { }, expected: false, }, + "map-int32": { + attrTyp: types.MapType{ + ElemType: types.Int32Type, + }, + expected: false, + }, "map-int64": { attrTyp: types.MapType{ ElemType: types.Int64Type, @@ -2032,6 +2065,12 @@ func TestContainsMissingUnderlyingType(t *testing.T) { }, expected: false, }, + "set-custom-int32": { + attrTyp: types.SetType{ + ElemType: testtypes.Int32Type{}, + }, + expected: false, + }, "set-custom-int64": { attrTyp: types.SetType{ ElemType: testtypes.Int64Type{}, @@ -2162,6 +2201,12 @@ func TestContainsMissingUnderlyingType(t *testing.T) { }, expected: false, }, + "set-int32": { + attrTyp: types.SetType{ + ElemType: types.Int32Type, + }, + expected: false, + }, "set-int64": { attrTyp: types.SetType{ ElemType: types.Int64Type, @@ -2303,6 +2348,12 @@ func TestContainsMissingUnderlyingType(t *testing.T) { }, expected: false, }, + "tuple-int32": { + attrTyp: types.TupleType{ + ElemTypes: []attr.Type{types.Int32Type}, + }, + expected: false, + }, "tuple-int64": { attrTyp: types.TupleType{ ElemTypes: []attr.Type{types.Int64Type}, diff --git a/internal/reflect/interfaces_test.go b/internal/reflect/interfaces_test.go index f3e197b78..efa24c8bf 100644 --- a/internal/reflect/interfaces_test.go +++ b/internal/reflect/interfaces_test.go @@ -634,6 +634,16 @@ func TestFromAttributeValue(t *testing.T) { val: types.Float64Null(), expected: types.Float64Null(), }, + "Int32Type-Int32Value": { + typ: types.Int32Type, + val: types.Int32Null(), + expected: types.Int32Null(), + }, + "Int32Typable-Int32Value": { + typ: testtypes.Int32TypeWithSemanticEquals{}, + val: types.Int32Null(), + expected: types.Int32Null(), + }, "Int64Type-Int64Value": { typ: types.Int64Type, val: types.Int64Null(), diff --git a/internal/reflect/number_test.go b/internal/reflect/number_test.go index 5c13d45b2..228308e65 100644 --- a/internal/reflect/number_test.go +++ b/internal/reflect/number_test.go @@ -238,6 +238,17 @@ func TestNumber_int16UnderflowError(t *testing.T) { func TestNumber_int32(t *testing.T) { t.Parallel() + + var n int32 + + 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_int32OverflowError(t *testing.T) { diff --git a/internal/testing/testdefaults/int32.go b/internal/testing/testdefaults/int32.go new file mode 100644 index 000000000..4a8237b72 --- /dev/null +++ b/internal/testing/testdefaults/int32.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.Int32 = Int32{} + +// Declarative defaults.Int32 for unit testing. +type Int32 struct { + // defaults.Describer interface methods + DescriptionMethod func(context.Context) string + MarkdownDescriptionMethod func(context.Context) string + + // defaults.Int32 interface methods + DefaultInt32Method func(context.Context, defaults.Int32Request, *defaults.Int32Response) +} + +// Description satisfies the defaults.Describer interface. +func (v Int32) Description(ctx context.Context) string { + if v.DescriptionMethod == nil { + return "" + } + + return v.DescriptionMethod(ctx) +} + +// MarkdownDescription satisfies the defaults.Describer interface. +func (v Int32) MarkdownDescription(ctx context.Context) string { + if v.MarkdownDescriptionMethod == nil { + return "" + } + + return v.MarkdownDescriptionMethod(ctx) +} + +// DefaultInt32 satisfies the defaults.Int32 interface. +func (v Int32) DefaultInt32(ctx context.Context, req defaults.Int32Request, resp *defaults.Int32Response) { + if v.DefaultInt32Method == nil { + return + } + + v.DefaultInt32Method(ctx, req, resp) +} diff --git a/internal/testing/testplanmodifier/int32.go b/internal/testing/testplanmodifier/int32.go new file mode 100644 index 000000000..c634b399a --- /dev/null +++ b/internal/testing/testplanmodifier/int32.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.Int32 = &Int32{} + +// Declarative planmodifier.Int32 for unit testing. +type Int32 struct { + // Int32 interface methods + DescriptionMethod func(context.Context) string + MarkdownDescriptionMethod func(context.Context) string + PlanModifyInt32Method func(context.Context, planmodifier.Int32Request, *planmodifier.Int32Response) +} + +// Description satisfies the planmodifier.Int32 interface. +func (v Int32) Description(ctx context.Context) string { + if v.DescriptionMethod == nil { + return "" + } + + return v.DescriptionMethod(ctx) +} + +// MarkdownDescription satisfies the planmodifier.Int32 interface. +func (v Int32) MarkdownDescription(ctx context.Context) string { + if v.MarkdownDescriptionMethod == nil { + return "" + } + + return v.MarkdownDescriptionMethod(ctx) +} + +// PlanModify satisfies the planmodifier.Int32 interface. +func (v Int32) PlanModifyInt32(ctx context.Context, req planmodifier.Int32Request, resp *planmodifier.Int32Response) { + if v.PlanModifyInt32Method == nil { + return + } + + v.PlanModifyInt32Method(ctx, req, resp) +} diff --git a/internal/testing/testschema/attributewithint32default.go b/internal/testing/testschema/attributewithint32default.go new file mode 100644 index 000000000..bbfe22af8 --- /dev/null +++ b/internal/testing/testschema/attributewithint32default.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.AttributeWithInt32DefaultValue = AttributeWithInt32DefaultValue{} + +type AttributeWithInt32DefaultValue struct { + Computed bool + DeprecationMessage string + Description string + MarkdownDescription string + Optional bool + Required bool + Sensitive bool + Default defaults.Int32 +} + +// ApplyTerraform5AttributePathStep satisfies the fwschema.Attribute interface. +func (a AttributeWithInt32DefaultValue) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (any, error) { + return a.GetType().ApplyTerraform5AttributePathStep(step) +} + +// Int32DefaultValue satisfies the fwxschema.AttributeWithInt32DefaultValue interface. +func (a AttributeWithInt32DefaultValue) Int32DefaultValue() defaults.Int32 { + return a.Default +} + +// Equal satisfies the fwschema.Attribute interface. +func (a AttributeWithInt32DefaultValue) Equal(o fwschema.Attribute) bool { + _, ok := o.(AttributeWithInt32DefaultValue) + + if !ok { + return false + } + + return fwschema.AttributesEqual(a, o) +} + +// GetDeprecationMessage satisfies the fwschema.Attribute interface. +func (a AttributeWithInt32DefaultValue) GetDeprecationMessage() string { + return a.DeprecationMessage +} + +// GetDescription satisfies the fwschema.Attribute interface. +func (a AttributeWithInt32DefaultValue) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription satisfies the fwschema.Attribute interface. +func (a AttributeWithInt32DefaultValue) GetMarkdownDescription() string { + return a.MarkdownDescription +} + +// GetType satisfies the fwschema.Attribute interface. +func (a AttributeWithInt32DefaultValue) GetType() attr.Type { + return types.Int32Type +} + +// IsComputed satisfies the fwschema.Attribute interface. +func (a AttributeWithInt32DefaultValue) IsComputed() bool { + return a.Computed +} + +// IsOptional satisfies the fwschema.Attribute interface. +func (a AttributeWithInt32DefaultValue) IsOptional() bool { + return a.Optional +} + +// IsRequired satisfies the fwschema.Attribute interface. +func (a AttributeWithInt32DefaultValue) IsRequired() bool { + return a.Required +} + +// IsSensitive satisfies the fwschema.Attribute interface. +func (a AttributeWithInt32DefaultValue) IsSensitive() bool { + return a.Sensitive +} diff --git a/internal/testing/testschema/attributewithint32planmodifiers.go b/internal/testing/testschema/attributewithint32planmodifiers.go new file mode 100644 index 000000000..aff453d9c --- /dev/null +++ b/internal/testing/testschema/attributewithint32planmodifiers.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.AttributeWithInt32PlanModifiers = AttributeWithInt32PlanModifiers{} + +type AttributeWithInt32PlanModifiers struct { + Computed bool + DeprecationMessage string + Description string + MarkdownDescription string + Optional bool + Required bool + Sensitive bool + PlanModifiers []planmodifier.Int32 +} + +// ApplyTerraform5AttributePathStep satisfies the fwschema.Attribute interface. +func (a AttributeWithInt32PlanModifiers) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (any, error) { + return a.GetType().ApplyTerraform5AttributePathStep(step) +} + +// Equal satisfies the fwschema.Attribute interface. +func (a AttributeWithInt32PlanModifiers) Equal(o fwschema.Attribute) bool { + _, ok := o.(AttributeWithInt32PlanModifiers) + + if !ok { + return false + } + + return fwschema.AttributesEqual(a, o) +} + +// GetDeprecationMessage satisfies the fwschema.Attribute interface. +func (a AttributeWithInt32PlanModifiers) GetDeprecationMessage() string { + return a.DeprecationMessage +} + +// GetDescription satisfies the fwschema.Attribute interface. +func (a AttributeWithInt32PlanModifiers) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription satisfies the fwschema.Attribute interface. +func (a AttributeWithInt32PlanModifiers) GetMarkdownDescription() string { + return a.MarkdownDescription +} + +// GetType satisfies the fwschema.Attribute interface. +func (a AttributeWithInt32PlanModifiers) GetType() attr.Type { + return types.Int32Type +} + +// Int32PlanModifiers satisfies the fwxschema.AttributeWithInt32PlanModifiers interface. +func (a AttributeWithInt32PlanModifiers) Int32PlanModifiers() []planmodifier.Int32 { + return a.PlanModifiers +} + +// IsComputed satisfies the fwschema.Attribute interface. +func (a AttributeWithInt32PlanModifiers) IsComputed() bool { + return a.Computed +} + +// IsOptional satisfies the fwschema.Attribute interface. +func (a AttributeWithInt32PlanModifiers) IsOptional() bool { + return a.Optional +} + +// IsRequired satisfies the fwschema.Attribute interface. +func (a AttributeWithInt32PlanModifiers) IsRequired() bool { + return a.Required +} + +// IsSensitive satisfies the fwschema.Attribute interface. +func (a AttributeWithInt32PlanModifiers) IsSensitive() bool { + return a.Sensitive +} diff --git a/internal/testing/testschema/attributewithint32validators.go b/internal/testing/testschema/attributewithint32validators.go new file mode 100644 index 000000000..7c6913bcc --- /dev/null +++ b/internal/testing/testschema/attributewithint32validators.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.AttributeWithInt32Validators = AttributeWithInt32Validators{} + +type AttributeWithInt32Validators struct { + Computed bool + DeprecationMessage string + Description string + MarkdownDescription string + Optional bool + Required bool + Sensitive bool + Validators []validator.Int32 +} + +// ApplyTerraform5AttributePathStep satisfies the fwschema.Attribute interface. +func (a AttributeWithInt32Validators) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (any, error) { + return a.GetType().ApplyTerraform5AttributePathStep(step) +} + +// Equal satisfies the fwschema.Attribute interface. +func (a AttributeWithInt32Validators) Equal(o fwschema.Attribute) bool { + _, ok := o.(AttributeWithInt32Validators) + + if !ok { + return false + } + + return fwschema.AttributesEqual(a, o) +} + +// GetDeprecationMessage satisfies the fwschema.Attribute interface. +func (a AttributeWithInt32Validators) GetDeprecationMessage() string { + return a.DeprecationMessage +} + +// GetDescription satisfies the fwschema.Attribute interface. +func (a AttributeWithInt32Validators) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription satisfies the fwschema.Attribute interface. +func (a AttributeWithInt32Validators) GetMarkdownDescription() string { + return a.MarkdownDescription +} + +// GetType satisfies the fwschema.Attribute interface. +func (a AttributeWithInt32Validators) GetType() attr.Type { + return types.Int32Type +} + +// Int32Validators satisfies the fwxschema.AttributeWithInt32Validators interface. +func (a AttributeWithInt32Validators) Int32Validators() []validator.Int32 { + return a.Validators +} + +// IsComputed satisfies the fwschema.Attribute interface. +func (a AttributeWithInt32Validators) IsComputed() bool { + return a.Computed +} + +// IsOptional satisfies the fwschema.Attribute interface. +func (a AttributeWithInt32Validators) IsOptional() bool { + return a.Optional +} + +// IsRequired satisfies the fwschema.Attribute interface. +func (a AttributeWithInt32Validators) IsRequired() bool { + return a.Required +} + +// IsSensitive satisfies the fwschema.Attribute interface. +func (a AttributeWithInt32Validators) IsSensitive() bool { + return a.Sensitive +} diff --git a/internal/testing/testtypes/int32.go b/internal/testing/testtypes/int32.go new file mode 100644 index 000000000..51adefd8f --- /dev/null +++ b/internal/testing/testtypes/int32.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.Int32Typable = Int32Type{} + _ basetypes.Int32Valuable = Int32Value{} +) + +type Int32Type struct { + basetypes.Int32Type +} + +func (t Int32Type) Equal(o attr.Type) bool { + other, ok := o.(Int32Type) + + if !ok { + return false + } + + return t.Int32Type.Equal(other.Int32Type) +} + +type Int32Value struct { + basetypes.Int32Value +} + +func (v Int32Value) Equal(o attr.Value) bool { + other, ok := o.(Int32Value) + + if !ok { + return false + } + + return v.Int32Value.Equal(other.Int32Value) +} diff --git a/internal/testing/testtypes/int32withsemanticequals.go b/internal/testing/testtypes/int32withsemanticequals.go new file mode 100644 index 000000000..d6e0a704a --- /dev/null +++ b/internal/testing/testtypes/int32withsemanticequals.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.Int32Typable = Int32TypeWithSemanticEquals{} + _ basetypes.Int32ValuableWithSemanticEquals = Int32ValueWithSemanticEquals{} +) + +// Int32TypeWithSemanticEquals is an Int32Type associated with +// Int32ValueWithSemanticEquals, which implements semantic equality logic that +// returns the SemanticEquals boolean for testing. +type Int32TypeWithSemanticEquals struct { + basetypes.Int32Type + + SemanticEquals bool + SemanticEqualsDiagnostics diag.Diagnostics +} + +func (t Int32TypeWithSemanticEquals) Equal(o attr.Type) bool { + other, ok := o.(Int32TypeWithSemanticEquals) + + if !ok { + return false + } + + if t.SemanticEquals != other.SemanticEquals { + return false + } + + return t.Int32Type.Equal(other.Int32Type) +} + +func (t Int32TypeWithSemanticEquals) String() string { + return fmt.Sprintf("Int32TypeWithSemanticEquals(%t)", t.SemanticEquals) +} + +func (t Int32TypeWithSemanticEquals) ValueFromInt32(ctx context.Context, in basetypes.Int32Value) (basetypes.Int32Valuable, diag.Diagnostics) { + var diags diag.Diagnostics + + value := Int32ValueWithSemanticEquals{ + Int32Value: in, + SemanticEquals: t.SemanticEquals, + SemanticEqualsDiagnostics: t.SemanticEqualsDiagnostics, + } + + return value, diags +} + +func (t Int32TypeWithSemanticEquals) ValueFromTerraform(ctx context.Context, in tftypes.Value) (attr.Value, error) { + attrValue, err := t.Int32Type.ValueFromTerraform(ctx, in) + + if err != nil { + return nil, err + } + + stringValue, ok := attrValue.(basetypes.Int32Value) + + if !ok { + return nil, fmt.Errorf("unexpected value type of %T", attrValue) + } + + stringValuable, diags := t.ValueFromInt32(ctx, stringValue) + + if diags.HasError() { + return nil, fmt.Errorf("unexpected error converting Int32Value to Int32Valuable: %v", diags) + } + + return stringValuable, nil +} + +func (t Int32TypeWithSemanticEquals) ValueType(ctx context.Context) attr.Value { + return Int32ValueWithSemanticEquals{ + SemanticEquals: t.SemanticEquals, + SemanticEqualsDiagnostics: t.SemanticEqualsDiagnostics, + } +} + +type Int32ValueWithSemanticEquals struct { + basetypes.Int32Value + + SemanticEquals bool + SemanticEqualsDiagnostics diag.Diagnostics +} + +func (v Int32ValueWithSemanticEquals) Equal(o attr.Value) bool { + other, ok := o.(Int32ValueWithSemanticEquals) + + if !ok { + return false + } + + return v.Int32Value.Equal(other.Int32Value) +} + +func (v Int32ValueWithSemanticEquals) Int32SemanticEquals(ctx context.Context, otherV basetypes.Int32Valuable) (bool, diag.Diagnostics) { + return v.SemanticEquals, v.SemanticEqualsDiagnostics +} + +func (v Int32ValueWithSemanticEquals) Type(ctx context.Context) attr.Type { + return Int32TypeWithSemanticEquals{ + SemanticEquals: v.SemanticEquals, + SemanticEqualsDiagnostics: v.SemanticEqualsDiagnostics, + } +} diff --git a/internal/testing/testvalidator/int32.go b/internal/testing/testvalidator/int32.go new file mode 100644 index 000000000..765186e15 --- /dev/null +++ b/internal/testing/testvalidator/int32.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.Int32 = &Int32{} + _ function.Int32ParameterValidator = &Int32{} +) + +// Declarative validator.Int32 for unit testing. +type Int32 struct { + // Int32 interface methods + DescriptionMethod func(context.Context) string + MarkdownDescriptionMethod func(context.Context) string + ValidateInt32Method func(context.Context, validator.Int32Request, *validator.Int32Response) + ValidateMethod func(context.Context, function.Int32ParameterValidatorRequest, *function.Int32ParameterValidatorResponse) +} + +// Description satisfies the validator.Int32 interface. +func (v Int32) Description(ctx context.Context) string { + if v.DescriptionMethod == nil { + return "" + } + + return v.DescriptionMethod(ctx) +} + +// MarkdownDescription satisfies the validator.Int32 interface. +func (v Int32) MarkdownDescription(ctx context.Context) string { + if v.MarkdownDescriptionMethod == nil { + return "" + } + + return v.MarkdownDescriptionMethod(ctx) +} + +// ValidateInt32 satisfies the validator.Int32 interface. +func (v Int32) ValidateInt32(ctx context.Context, req validator.Int32Request, resp *validator.Int32Response) { + if v.ValidateInt32Method == nil { + return + } + + v.ValidateInt32Method(ctx, req, resp) +} + +// ValidateParameterInt32 satisfies the function.Int32ParameterValidator interface. +func (v Int32) ValidateParameterInt32(ctx context.Context, req function.Int32ParameterValidatorRequest, resp *function.Int32ParameterValidatorResponse) { + 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 758746e14..94caaa6b0 100644 --- a/internal/toproto5/function_test.go +++ b/internal/toproto5/function_test.go @@ -352,6 +352,15 @@ func TestFunctionParameter(t *testing.T) { Type: tftypes.Number, }, }, + "type-int32": { + fw: function.Int32Parameter{ + Name: "int32", + }, + expected: &tfprotov5.FunctionParameter{ + Name: "int32", + Type: tftypes.Number, + }, + }, "type-int64": { fw: function.Int64Parameter{ Name: "int64", diff --git a/internal/toproto5/getproviderschema_test.go b/internal/toproto5/getproviderschema_test.go index c89fb31cd..0fb3acfe0 100644 --- a/internal/toproto5/getproviderschema_test.go +++ b/internal/toproto5/getproviderschema_test.go @@ -330,6 +330,36 @@ func TestGetProviderSchemaResponse(t *testing.T) { ResourceSchemas: map[string]*tfprotov5.Schema{}, }, }, + "data-source-attribute-type-int32": { + input: &fwserver.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]fwschema.Schema{ + "test_data_source": datasourceschema.Schema{ + Attributes: map[string]datasourceschema.Attribute{ + "test_attribute": datasourceschema.Int32Attribute{ + 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-int64": { input: &fwserver.GetProviderSchemaResponse{ DataSourceSchemas: map[string]fwschema.Schema{ @@ -1353,6 +1383,33 @@ func TestGetProviderSchemaResponse(t *testing.T) { ResourceSchemas: map[string]*tfprotov5.Schema{}, }, }, + "provider-attribute-type-int32": { + input: &fwserver.GetProviderSchemaResponse{ + Provider: providerschema.Schema{ + Attributes: map[string]providerschema.Attribute{ + "test_attribute": providerschema.Int32Attribute{ + 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-int64": { input: &fwserver.GetProviderSchemaResponse{ Provider: providerschema.Schema{ @@ -2837,6 +2894,36 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, + "resource-attribute-type-int32": { + input: &fwserver.GetProviderSchemaResponse{ + ResourceSchemas: map[string]fwschema.Schema{ + "test_resource": resourceschema.Schema{ + Attributes: map[string]resourceschema.Attribute{ + "test_attribute": resourceschema.Int32Attribute{ + 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-int64": { input: &fwserver.GetProviderSchemaResponse{ ResourceSchemas: map[string]fwschema.Schema{ diff --git a/internal/toproto6/function_test.go b/internal/toproto6/function_test.go index 47af4e72a..f831cbe6a 100644 --- a/internal/toproto6/function_test.go +++ b/internal/toproto6/function_test.go @@ -352,6 +352,15 @@ func TestFunctionParameter(t *testing.T) { Type: tftypes.Number, }, }, + "type-int32": { + fw: function.Int32Parameter{ + Name: "int32", + }, + expected: &tfprotov6.FunctionParameter{ + Name: "int32", + Type: tftypes.Number, + }, + }, "type-int64": { fw: function.Int64Parameter{ Name: "int64", diff --git a/internal/toproto6/getproviderschema_test.go b/internal/toproto6/getproviderschema_test.go index 4ea37606c..cba045d18 100644 --- a/internal/toproto6/getproviderschema_test.go +++ b/internal/toproto6/getproviderschema_test.go @@ -330,6 +330,36 @@ func TestGetProviderSchemaResponse(t *testing.T) { ResourceSchemas: map[string]*tfprotov6.Schema{}, }, }, + "data-source-attribute-type-int32": { + input: &fwserver.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]fwschema.Schema{ + "test_data_source": datasourceschema.Schema{ + Attributes: map[string]datasourceschema.Attribute{ + "test_attribute": datasourceschema.Int32Attribute{ + Required: true, + }, + }, + }, + }, + }, + expected: &tfprotov6.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov6.Schema{ + "test_data_source": { + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "test_attribute", + Required: true, + Type: tftypes.Number, + }, + }, + }, + }, + }, + Functions: map[string]*tfprotov6.Function{}, + ResourceSchemas: map[string]*tfprotov6.Schema{}, + }, + }, "data-source-attribute-type-int64": { input: &fwserver.GetProviderSchemaResponse{ DataSourceSchemas: map[string]fwschema.Schema{ @@ -1401,6 +1431,33 @@ func TestGetProviderSchemaResponse(t *testing.T) { ResourceSchemas: map[string]*tfprotov6.Schema{}, }, }, + "provider-attribute-type-int32": { + input: &fwserver.GetProviderSchemaResponse{ + Provider: providerschema.Schema{ + Attributes: map[string]providerschema.Attribute{ + "test_attribute": providerschema.Int32Attribute{ + Required: true, + }, + }, + }, + }, + expected: &tfprotov6.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, + Provider: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "test_attribute", + Required: true, + Type: tftypes.Number, + }, + }, + }, + }, + ResourceSchemas: map[string]*tfprotov6.Schema{}, + }, + }, "provider-attribute-type-int64": { input: &fwserver.GetProviderSchemaResponse{ Provider: providerschema.Schema{ @@ -2989,6 +3046,36 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, + "resource-attribute-type-int32": { + input: &fwserver.GetProviderSchemaResponse{ + ResourceSchemas: map[string]fwschema.Schema{ + "test_resource": resourceschema.Schema{ + Attributes: map[string]resourceschema.Attribute{ + "test_attribute": resourceschema.Int32Attribute{ + Required: true, + }, + }, + }, + }, + }, + expected: &tfprotov6.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, + ResourceSchemas: map[string]*tfprotov6.Schema{ + "test_resource": { + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "test_attribute", + Required: true, + Type: tftypes.Number, + }, + }, + }, + }, + }, + }, + }, "resource-attribute-type-int64": { input: &fwserver.GetProviderSchemaResponse{ ResourceSchemas: map[string]fwschema.Schema{ diff --git a/provider/schema/int32_attribute.go b/provider/schema/int32_attribute.go new file mode 100644 index 000000000..16ff58f0f --- /dev/null +++ b/provider/schema/int32_attribute.go @@ -0,0 +1,184 @@ +// 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 = Int32Attribute{} + _ fwxschema.AttributeWithInt32Validators = Int32Attribute{} +) + +// Int32Attribute represents a schema attribute that is a 32-bit integer. +// When retrieving the value for this attribute, use types.Int32 as the value +// type unless the CustomType field is set. +// +// Use Float32Attribute for 32-bit floating point number attributes or +// NumberAttribute for 512-bit generic number attributes. +// +// Terraform configurations configure this attribute using expressions that +// return a number or directly via an integer value. +// +// example_attribute = 123 +// +// Terraform configurations reference this attribute using the attribute name. +// +// .example_attribute +type Int32Attribute struct { + // CustomType enables the use of a custom attribute type in place of the + // default basetypes.Int32Type. When retrieving data, the basetypes.Int32Valuable + // associated with this custom type must be used in place of types.Int32. + CustomType basetypes.Int32Typable + + // 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.Int32 +} + +// ApplyTerraform5AttributePathStep always returns an error as it is not +// possible to step further into a Int32Attribute. +func (a Int32Attribute) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { + return a.GetType().ApplyTerraform5AttributePathStep(step) +} + +// Equal returns true if the given Attribute is a Int32Attribute +// and all fields are equal. +func (a Int32Attribute) Equal(o fwschema.Attribute) bool { + if _, ok := o.(Int32Attribute); !ok { + return false + } + + return fwschema.AttributesEqual(a, o) +} + +// GetDeprecationMessage returns the DeprecationMessage field value. +func (a Int32Attribute) GetDeprecationMessage() string { + return a.DeprecationMessage +} + +// GetDescription returns the Description field value. +func (a Int32Attribute) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription returns the MarkdownDescription field value. +func (a Int32Attribute) GetMarkdownDescription() string { + return a.MarkdownDescription +} + +// GetType returns types.Int32Type or the CustomType field value if defined. +func (a Int32Attribute) GetType() attr.Type { + if a.CustomType != nil { + return a.CustomType + } + + return types.Int32Type +} + +// Int32Validators returns the Validators field value. +func (a Int32Attribute) Int32Validators() []validator.Int32 { + return a.Validators +} + +// IsComputed always returns false as provider schemas cannot be Computed. +func (a Int32Attribute) IsComputed() bool { + return false +} + +// IsOptional returns the Optional field value. +func (a Int32Attribute) IsOptional() bool { + return a.Optional +} + +// IsRequired returns the Required field value. +func (a Int32Attribute) IsRequired() bool { + return a.Required +} + +// IsSensitive returns the Sensitive field value. +func (a Int32Attribute) IsSensitive() bool { + return a.Sensitive +} diff --git a/provider/schema/int32_attribute_test.go b/provider/schema/int32_attribute_test.go new file mode 100644 index 000000000..b3ac0687c --- /dev/null +++ b/provider/schema/int32_attribute_test.go @@ -0,0 +1,420 @@ +// 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/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/provider/schema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func TestInt32AttributeApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int32Attribute + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + attribute: schema.Int32Attribute{}, + step: tftypes.AttributeName("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.AttributeName to basetypes.Int32Type"), + }, + "ElementKeyInt": { + attribute: schema.Int32Attribute{}, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyInt to basetypes.Int32Type"), + }, + "ElementKeyString": { + attribute: schema.Int32Attribute{}, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyString to basetypes.Int32Type"), + }, + "ElementKeyValue": { + attribute: schema.Int32Attribute{}, + step: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyValue to basetypes.Int32Type"), + }, + } + + 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 TestInt32AttributeGetDeprecationMessage(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int32Attribute + expected string + }{ + "no-deprecation-message": { + attribute: schema.Int32Attribute{}, + expected: "", + }, + "deprecation-message": { + attribute: schema.Int32Attribute{ + 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 TestInt32AttributeEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int32Attribute + other fwschema.Attribute + expected bool + }{ + "different-type": { + attribute: schema.Int32Attribute{}, + other: testschema.AttributeWithInt32Validators{}, + expected: false, + }, + "equal": { + attribute: schema.Int32Attribute{}, + other: schema.Int32Attribute{}, + 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 TestInt32AttributeGetDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int32Attribute + expected string + }{ + "no-description": { + attribute: schema.Int32Attribute{}, + expected: "", + }, + "description": { + attribute: schema.Int32Attribute{ + 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 TestInt32AttributeGetMarkdownDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int32Attribute + expected string + }{ + "no-markdown-description": { + attribute: schema.Int32Attribute{}, + expected: "", + }, + "markdown-description": { + attribute: schema.Int32Attribute{ + 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 TestInt32AttributeGetType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int32Attribute + expected attr.Type + }{ + "base": { + attribute: schema.Int32Attribute{}, + expected: types.Int32Type, + }, + "custom-type": { + attribute: schema.Int32Attribute{ + CustomType: testtypes.Int32Type{}, + }, + expected: testtypes.Int32Type{}, + }, + } + + 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 TestInt32AttributeInt32Validators(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int32Attribute + expected []validator.Int32 + }{ + "no-validators": { + attribute: schema.Int32Attribute{}, + expected: nil, + }, + "validators": { + attribute: schema.Int32Attribute{ + Validators: []validator.Int32{}, + }, + expected: []validator.Int32{}, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.Int32Validators() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt32AttributeIsComputed(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int32Attribute + expected bool + }{ + "not-computed": { + attribute: schema.Int32Attribute{}, + 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 TestInt32AttributeIsOptional(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int32Attribute + expected bool + }{ + "not-optional": { + attribute: schema.Int32Attribute{}, + expected: false, + }, + "optional": { + attribute: schema.Int32Attribute{ + 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 TestInt32AttributeIsRequired(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int32Attribute + expected bool + }{ + "not-required": { + attribute: schema.Int32Attribute{}, + expected: false, + }, + "required": { + attribute: schema.Int32Attribute{ + 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 TestInt32AttributeIsSensitive(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int32Attribute + expected bool + }{ + "not-sensitive": { + attribute: schema.Int32Attribute{}, + expected: false, + }, + "sensitive": { + attribute: schema.Int32Attribute{ + 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/int32.go b/resource/schema/defaults/int32.go new file mode 100644 index 000000000..a26da30ff --- /dev/null +++ b/resource/schema/defaults/int32.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" +) + +// Int32 is a schema default value for types.Int32 attributes. +type Int32 interface { + Describer + + // DefaultInt32 should set the default value. + DefaultInt32(context.Context, Int32Request, *Int32Response) +} + +type Int32Request struct { + // Path contains the path of the attribute for setting the + // default value. Use this path for any response diagnostics. + Path path.Path +} + +type Int32Response 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.Int32 +} diff --git a/resource/schema/int32_attribute.go b/resource/schema/int32_attribute.go new file mode 100644 index 000000000..41b74bcf3 --- /dev/null +++ b/resource/schema/int32_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 = Int32Attribute{} + _ fwschema.AttributeWithValidateImplementation = Int32Attribute{} + _ fwschema.AttributeWithInt32DefaultValue = Int32Attribute{} + _ fwxschema.AttributeWithInt32PlanModifiers = Int32Attribute{} + _ fwxschema.AttributeWithInt32Validators = Int32Attribute{} +) + +// Int32Attribute represents a schema attribute that is a 32-bit integer. +// When retrieving the value for this attribute, use types.Int32 as the value +// type unless the CustomType field is set. +// +// Use Float32Attribute for 32-bit floating point number attributes or +// NumberAttribute for 512-bit generic number attributes. +// +// Terraform configurations configure this attribute using expressions that +// return a number or directly via an integer value. +// +// example_attribute = 123 +// +// Terraform configurations reference this attribute using the attribute name. +// +// .example_attribute +type Int32Attribute struct { + // CustomType enables the use of a custom attribute type in place of the + // default basetypes.Int32Type. When retrieving data, the basetypes.Int32Valuable + // associated with this custom type must be used in place of types.Int32. + CustomType basetypes.Int32Typable + + // 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.Int32 + + // 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.Int32 + + // 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.Int32 +} + +// ApplyTerraform5AttributePathStep always returns an error as it is not +// possible to step further into a Int32Attribute. +func (a Int32Attribute) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { + return a.GetType().ApplyTerraform5AttributePathStep(step) +} + +// Equal returns true if the given Attribute is a Int32Attribute +// and all fields are equal. +func (a Int32Attribute) Equal(o fwschema.Attribute) bool { + if _, ok := o.(Int32Attribute); !ok { + return false + } + + return fwschema.AttributesEqual(a, o) +} + +// GetDeprecationMessage returns the DeprecationMessage field value. +func (a Int32Attribute) GetDeprecationMessage() string { + return a.DeprecationMessage +} + +// GetDescription returns the Description field value. +func (a Int32Attribute) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription returns the MarkdownDescription field value. +func (a Int32Attribute) GetMarkdownDescription() string { + return a.MarkdownDescription +} + +// GetType returns types.Int32Type or the CustomType field value if defined. +func (a Int32Attribute) GetType() attr.Type { + if a.CustomType != nil { + return a.CustomType + } + + return types.Int32Type +} + +// Int32DefaultValue returns the Default field value. +func (a Int32Attribute) Int32DefaultValue() defaults.Int32 { + return a.Default +} + +// Int32PlanModifiers returns the PlanModifiers field value. +func (a Int32Attribute) Int32PlanModifiers() []planmodifier.Int32 { + return a.PlanModifiers +} + +// Int32Validators returns the Validators field value. +func (a Int32Attribute) Int32Validators() []validator.Int32 { + return a.Validators +} + +// IsComputed returns the Computed field value. +func (a Int32Attribute) IsComputed() bool { + return a.Computed +} + +// IsOptional returns the Optional field value. +func (a Int32Attribute) IsOptional() bool { + return a.Optional +} + +// IsRequired returns the Required field value. +func (a Int32Attribute) IsRequired() bool { + return a.Required +} + +// IsSensitive returns the Sensitive field value. +func (a Int32Attribute) 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 Int32Attribute) ValidateImplementation(ctx context.Context, req fwschema.ValidateImplementationRequest, resp *fwschema.ValidateImplementationResponse) { + if !a.IsComputed() && a.Int32DefaultValue() != nil { + resp.Diagnostics.Append(nonComputedAttributeWithDefaultDiag(req.Path)) + } +} diff --git a/resource/schema/int32_attribute_test.go b/resource/schema/int32_attribute_test.go new file mode 100644 index 000000000..152d957f7 --- /dev/null +++ b/resource/schema/int32_attribute_test.go @@ -0,0 +1,578 @@ +// 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/internal/testing/testtypes" + "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/int32default" + "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 TestInt32AttributeApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int32Attribute + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + attribute: schema.Int32Attribute{}, + step: tftypes.AttributeName("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.AttributeName to basetypes.Int32Type"), + }, + "ElementKeyInt": { + attribute: schema.Int32Attribute{}, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyInt to basetypes.Int32Type"), + }, + "ElementKeyString": { + attribute: schema.Int32Attribute{}, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyString to basetypes.Int32Type"), + }, + "ElementKeyValue": { + attribute: schema.Int32Attribute{}, + step: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyValue to basetypes.Int32Type"), + }, + } + + 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 TestInt32AttributeGetDeprecationMessage(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int32Attribute + expected string + }{ + "no-deprecation-message": { + attribute: schema.Int32Attribute{}, + expected: "", + }, + "deprecation-message": { + attribute: schema.Int32Attribute{ + 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 TestInt32AttributeEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int32Attribute + other fwschema.Attribute + expected bool + }{ + "different-type": { + attribute: schema.Int32Attribute{}, + other: testschema.AttributeWithInt32Validators{}, + expected: false, + }, + "equal": { + attribute: schema.Int32Attribute{}, + other: schema.Int32Attribute{}, + 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 TestInt32AttributeGetDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int32Attribute + expected string + }{ + "no-description": { + attribute: schema.Int32Attribute{}, + expected: "", + }, + "description": { + attribute: schema.Int32Attribute{ + 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 TestInt32AttributeGetMarkdownDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int32Attribute + expected string + }{ + "no-markdown-description": { + attribute: schema.Int32Attribute{}, + expected: "", + }, + "markdown-description": { + attribute: schema.Int32Attribute{ + 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 TestInt32AttributeGetType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int32Attribute + expected attr.Type + }{ + "base": { + attribute: schema.Int32Attribute{}, + expected: types.Int32Type, + }, + "custom-type": { + attribute: schema.Int32Attribute{ + CustomType: testtypes.Int32Type{}, + }, + expected: testtypes.Int32Type{}, + }, + } + + 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 TestInt32AttributeInt32DefaultValue(t *testing.T) { + t.Parallel() + + opt := cmp.Comparer(func(x, y defaults.Int32) bool { + ctx := context.Background() + req := defaults.Int32Request{} + + xResp := defaults.Int32Response{} + x.DefaultInt32(ctx, req, &xResp) + + yResp := defaults.Int32Response{} + y.DefaultInt32(ctx, req, &yResp) + + return xResp.PlanValue.Equal(yResp.PlanValue) + }) + + testCases := map[string]struct { + attribute schema.Int32Attribute + expected defaults.Int32 + }{ + "no-default": { + attribute: schema.Int32Attribute{}, + expected: nil, + }, + "default": { + attribute: schema.Int32Attribute{ + Default: int32default.StaticInt32(12345), + }, + expected: int32default.StaticInt32(12345), + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.Int32DefaultValue() + + if diff := cmp.Diff(got, testCase.expected, opt); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt32AttributeInt32PlanModifiers(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int32Attribute + expected []planmodifier.Int32 + }{ + "no-planmodifiers": { + attribute: schema.Int32Attribute{}, + expected: nil, + }, + "planmodifiers": { + attribute: schema.Int32Attribute{ + PlanModifiers: []planmodifier.Int32{}, + }, + expected: []planmodifier.Int32{}, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.Int32PlanModifiers() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt32AttributeInt32Validators(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int32Attribute + expected []validator.Int32 + }{ + "no-validators": { + attribute: schema.Int32Attribute{}, + expected: nil, + }, + "validators": { + attribute: schema.Int32Attribute{ + Validators: []validator.Int32{}, + }, + expected: []validator.Int32{}, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.Int32Validators() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt32AttributeIsComputed(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int32Attribute + expected bool + }{ + "not-computed": { + attribute: schema.Int32Attribute{}, + expected: false, + }, + "computed": { + attribute: schema.Int32Attribute{ + 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 TestInt32AttributeIsOptional(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int32Attribute + expected bool + }{ + "not-optional": { + attribute: schema.Int32Attribute{}, + expected: false, + }, + "optional": { + attribute: schema.Int32Attribute{ + 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 TestInt32AttributeIsRequired(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int32Attribute + expected bool + }{ + "not-required": { + attribute: schema.Int32Attribute{}, + expected: false, + }, + "required": { + attribute: schema.Int32Attribute{ + 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 TestInt32AttributeIsSensitive(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int32Attribute + expected bool + }{ + "not-sensitive": { + attribute: schema.Int32Attribute{}, + expected: false, + }, + "sensitive": { + attribute: schema.Int32Attribute{ + 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 TestInt32AttributeValidateImplementation(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int32Attribute + request fwschema.ValidateImplementationRequest + expected *fwschema.ValidateImplementationResponse + }{ + "computed": { + attribute: schema.Int32Attribute{ + Computed: true, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{}, + }, + "default-without-computed": { + attribute: schema.Int32Attribute{ + Default: int32default.StaticInt32(123), + }, + 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.Int32Attribute{ + Computed: true, + Default: int32default.StaticInt32(123), + }, + 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/int32default/doc.go b/resource/schema/int32default/doc.go new file mode 100644 index 000000000..f03864f9d --- /dev/null +++ b/resource/schema/int32default/doc.go @@ -0,0 +1,5 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +// Package int32default provides default values for types.Int32 attributes. +package int32default diff --git a/resource/schema/int32default/static_value.go b/resource/schema/int32default/static_value.go new file mode 100644 index 000000000..756dcc3b5 --- /dev/null +++ b/resource/schema/int32default/static_value.go @@ -0,0 +1,42 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package int32default + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/resource/schema/defaults" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// StaticInt32 returns a static int32 value default handler. +// +// Use StaticInt32 if a static default value for a int32 should be set. +func StaticInt32(defaultVal int32) defaults.Int32 { + return staticInt32Default{ + defaultVal: defaultVal, + } +} + +// staticInt32Default is static value default handler that +// sets a value on an int32 attribute. +type staticInt32Default struct { + defaultVal int32 +} + +// Description returns a human-readable description of the default value handler. +func (d staticInt32Default) Description(_ context.Context) string { + return fmt.Sprintf("value defaults to %d", d.defaultVal) +} + +// MarkdownDescription returns a markdown description of the default value handler. +func (d staticInt32Default) MarkdownDescription(_ context.Context) string { + return fmt.Sprintf("value defaults to `%d`", d.defaultVal) +} + +// DefaultInt32 implements the static default value logic. +func (d staticInt32Default) DefaultInt32(_ context.Context, req defaults.Int32Request, resp *defaults.Int32Response) { + resp.PlanValue = types.Int32Value(d.defaultVal) +} diff --git a/resource/schema/int32default/static_value_test.go b/resource/schema/int32default/static_value_test.go new file mode 100644 index 000000000..6b34cd9d7 --- /dev/null +++ b/resource/schema/int32default/static_value_test.go @@ -0,0 +1,47 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package int32default_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/int32default" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func TestStaticInt32DefaultInt32(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + defaultVal int32 + expected *defaults.Int32Response + }{ + "int32": { + defaultVal: 12345, + expected: &defaults.Int32Response{ + PlanValue: types.Int32Value(12345), + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + resp := &defaults.Int32Response{} + + int32default.StaticInt32(testCase.defaultVal).DefaultInt32(context.Background(), defaults.Int32Request{}, resp) + + if diff := cmp.Diff(testCase.expected, resp); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/resource/schema/int32planmodifier/doc.go b/resource/schema/int32planmodifier/doc.go new file mode 100644 index 000000000..3c257cda2 --- /dev/null +++ b/resource/schema/int32planmodifier/doc.go @@ -0,0 +1,5 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +// Package int32planmodifier provides plan modifiers for types.Int32 attributes. +package int32planmodifier diff --git a/resource/schema/int32planmodifier/requires_replace.go b/resource/schema/int32planmodifier/requires_replace.go new file mode 100644 index 000000000..ebb66a2ea --- /dev/null +++ b/resource/schema/int32planmodifier/requires_replace.go @@ -0,0 +1,30 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package int32planmodifier + +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.Int32 { + return RequiresReplaceIf( + func(_ context.Context, _ planmodifier.Int32Request, 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/int32planmodifier/requires_replace_if.go b/resource/schema/int32planmodifier/requires_replace_if.go new file mode 100644 index 000000000..4b5a1a9ef --- /dev/null +++ b/resource/schema/int32planmodifier/requires_replace_if.go @@ -0,0 +1,73 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package int32planmodifier + +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.Int32 { + 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 +} + +// PlanModifyInt32 implements the plan modification logic. +func (m requiresReplaceIfModifier) PlanModifyInt32(ctx context.Context, req planmodifier.Int32Request, resp *planmodifier.Int32Response) { + // 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/int32planmodifier/requires_replace_if_configured.go b/resource/schema/int32planmodifier/requires_replace_if_configured.go new file mode 100644 index 000000000..163c88b12 --- /dev/null +++ b/resource/schema/int32planmodifier/requires_replace_if_configured.go @@ -0,0 +1,34 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package int32planmodifier + +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.Int32 { + return RequiresReplaceIf( + func(_ context.Context, req planmodifier.Int32Request, 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/int32planmodifier/requires_replace_if_configured_test.go b/resource/schema/int32planmodifier/requires_replace_if_configured_test.go new file mode 100644 index 000000000..4f6873025 --- /dev/null +++ b/resource/schema/int32planmodifier/requires_replace_if_configured_test.go @@ -0,0 +1,171 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package int32planmodifier_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int32planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func TestRequiresReplaceIfConfiguredModifierPlanModifyInt32(t *testing.T) { + t.Parallel() + + testSchema := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.Int32Attribute{}, + }, + } + + 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.Int32) 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.Int32) 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.Int32Request + expected *planmodifier.Int32Response + }{ + "state-null": { + // resource creation + request: planmodifier.Int32Request{ + ConfigValue: types.Int32Value(1), + Plan: testPlan(types.Int32Value(1)), + PlanValue: types.Int32Value(1), + State: nullState, + StateValue: types.Int32Null(), + }, + expected: &planmodifier.Int32Response{ + PlanValue: types.Int32Value(1), + RequiresReplace: false, + }, + }, + "plan-null": { + // resource destroy + request: planmodifier.Int32Request{ + ConfigValue: types.Int32Null(), + Plan: nullPlan, + PlanValue: types.Int32Null(), + State: testState(types.Int32Value(1)), + StateValue: types.Int32Value(1), + }, + expected: &planmodifier.Int32Response{ + PlanValue: types.Int32Null(), + RequiresReplace: false, + }, + }, + "planvalue-statevalue-different-configured": { + request: planmodifier.Int32Request{ + ConfigValue: types.Int32Value(2), + Plan: testPlan(types.Int32Value(2)), + PlanValue: types.Int32Value(2), + State: testState(types.Int32Value(1)), + StateValue: types.Int32Value(1), + }, + expected: &planmodifier.Int32Response{ + PlanValue: types.Int32Value(2), + RequiresReplace: true, + }, + }, + "planvalue-statevalue-different-unconfigured": { + request: planmodifier.Int32Request{ + ConfigValue: types.Int32Null(), + Plan: testPlan(types.Int32Value(2)), + PlanValue: types.Int32Value(2), + State: testState(types.Int32Value(1)), + StateValue: types.Int32Value(1), + }, + expected: &planmodifier.Int32Response{ + PlanValue: types.Int32Value(2), + RequiresReplace: false, + }, + }, + "planvalue-statevalue-equal": { + request: planmodifier.Int32Request{ + ConfigValue: types.Int32Value(1), + Plan: testPlan(types.Int32Value(1)), + PlanValue: types.Int32Value(1), + State: testState(types.Int32Value(1)), + StateValue: types.Int32Value(1), + }, + expected: &planmodifier.Int32Response{ + PlanValue: types.Int32Value(1), + RequiresReplace: false, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + resp := &planmodifier.Int32Response{ + PlanValue: testCase.request.PlanValue, + } + + int32planmodifier.RequiresReplaceIfConfigured().PlanModifyInt32(context.Background(), testCase.request, resp) + + if diff := cmp.Diff(testCase.expected, resp); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/resource/schema/int32planmodifier/requires_replace_if_func.go b/resource/schema/int32planmodifier/requires_replace_if_func.go new file mode 100644 index 000000000..62c5a84fd --- /dev/null +++ b/resource/schema/int32planmodifier/requires_replace_if_func.go @@ -0,0 +1,25 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package int32planmodifier + +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.Int32Request, *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/int32planmodifier/requires_replace_if_test.go b/resource/schema/int32planmodifier/requires_replace_if_test.go new file mode 100644 index 000000000..950c94f47 --- /dev/null +++ b/resource/schema/int32planmodifier/requires_replace_if_test.go @@ -0,0 +1,182 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package int32planmodifier_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int32planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func TestRequiresReplaceIfModifierPlanModifyInt32(t *testing.T) { + t.Parallel() + + testSchema := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.Int32Attribute{}, + }, + } + + 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.Int32) 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.Int32) 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.Int32Request + ifFunc int32planmodifier.RequiresReplaceIfFunc + expected *planmodifier.Int32Response + }{ + "state-null": { + // resource creation + request: planmodifier.Int32Request{ + Plan: testPlan(types.Int32Unknown()), + PlanValue: types.Int32Unknown(), + State: nullState, + StateValue: types.Int32Null(), + }, + ifFunc: func(ctx context.Context, req planmodifier.Int32Request, resp *int32planmodifier.RequiresReplaceIfFuncResponse) { + resp.RequiresReplace = true // should never reach here + }, + expected: &planmodifier.Int32Response{ + PlanValue: types.Int32Unknown(), + RequiresReplace: false, + }, + }, + "plan-null": { + // resource destroy + request: planmodifier.Int32Request{ + Plan: nullPlan, + PlanValue: types.Int32Null(), + State: testState(types.Int32Value(1)), + StateValue: types.Int32Value(1), + }, + ifFunc: func(ctx context.Context, req planmodifier.Int32Request, resp *int32planmodifier.RequiresReplaceIfFuncResponse) { + resp.RequiresReplace = true // should never reach here + }, + expected: &planmodifier.Int32Response{ + PlanValue: types.Int32Null(), + RequiresReplace: false, + }, + }, + "planvalue-statevalue-different-if-false": { + request: planmodifier.Int32Request{ + Plan: testPlan(types.Int32Value(2)), + PlanValue: types.Int32Value(2), + State: testState(types.Int32Value(1)), + StateValue: types.Int32Value(1), + }, + ifFunc: func(ctx context.Context, req planmodifier.Int32Request, resp *int32planmodifier.RequiresReplaceIfFuncResponse) { + resp.RequiresReplace = false // no change + }, + expected: &planmodifier.Int32Response{ + PlanValue: types.Int32Value(2), + RequiresReplace: false, + }, + }, + "planvalue-statevalue-different-if-true": { + request: planmodifier.Int32Request{ + Plan: testPlan(types.Int32Value(2)), + PlanValue: types.Int32Value(2), + State: testState(types.Int32Value(1)), + StateValue: types.Int32Value(1), + }, + ifFunc: func(ctx context.Context, req planmodifier.Int32Request, resp *int32planmodifier.RequiresReplaceIfFuncResponse) { + resp.RequiresReplace = true // should reach here + }, + expected: &planmodifier.Int32Response{ + PlanValue: types.Int32Value(2), + RequiresReplace: true, + }, + }, + "planvalue-statevalue-equal": { + request: planmodifier.Int32Request{ + Plan: testPlan(types.Int32Value(1)), + PlanValue: types.Int32Value(1), + State: testState(types.Int32Value(1)), + StateValue: types.Int32Value(1), + }, + ifFunc: func(ctx context.Context, req planmodifier.Int32Request, resp *int32planmodifier.RequiresReplaceIfFuncResponse) { + resp.RequiresReplace = true // should never reach here + }, + expected: &planmodifier.Int32Response{ + PlanValue: types.Int32Value(1), + RequiresReplace: false, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + resp := &planmodifier.Int32Response{ + PlanValue: testCase.request.PlanValue, + } + + int32planmodifier.RequiresReplaceIf(testCase.ifFunc, "test", "test").PlanModifyInt32(context.Background(), testCase.request, resp) + + if diff := cmp.Diff(testCase.expected, resp); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/resource/schema/int32planmodifier/requires_replace_test.go b/resource/schema/int32planmodifier/requires_replace_test.go new file mode 100644 index 000000000..71881c8a6 --- /dev/null +++ b/resource/schema/int32planmodifier/requires_replace_test.go @@ -0,0 +1,154 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package int32planmodifier_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int32planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func TestRequiresReplaceModifierPlanModifyInt32(t *testing.T) { + t.Parallel() + + testSchema := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.Int32Attribute{}, + }, + } + + 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.Int32) 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.Int32) 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.Int32Request + expected *planmodifier.Int32Response + }{ + "state-null": { + // resource creation + request: planmodifier.Int32Request{ + Plan: testPlan(types.Int32Unknown()), + PlanValue: types.Int32Unknown(), + State: nullState, + StateValue: types.Int32Null(), + }, + expected: &planmodifier.Int32Response{ + PlanValue: types.Int32Unknown(), + RequiresReplace: false, + }, + }, + "plan-null": { + // resource destroy + request: planmodifier.Int32Request{ + Plan: nullPlan, + PlanValue: types.Int32Null(), + State: testState(types.Int32Value(1)), + StateValue: types.Int32Value(1), + }, + expected: &planmodifier.Int32Response{ + PlanValue: types.Int32Null(), + RequiresReplace: false, + }, + }, + "planvalue-statevalue-different": { + request: planmodifier.Int32Request{ + Plan: testPlan(types.Int32Value(2)), + PlanValue: types.Int32Value(2), + State: testState(types.Int32Value(1)), + StateValue: types.Int32Value(1), + }, + expected: &planmodifier.Int32Response{ + PlanValue: types.Int32Value(2), + RequiresReplace: true, + }, + }, + "planvalue-statevalue-equal": { + request: planmodifier.Int32Request{ + Plan: testPlan(types.Int32Value(1)), + PlanValue: types.Int32Value(1), + State: testState(types.Int32Value(1)), + StateValue: types.Int32Value(1), + }, + expected: &planmodifier.Int32Response{ + PlanValue: types.Int32Value(1), + RequiresReplace: false, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + resp := &planmodifier.Int32Response{ + PlanValue: testCase.request.PlanValue, + } + + int32planmodifier.RequiresReplace().PlanModifyInt32(context.Background(), testCase.request, resp) + + if diff := cmp.Diff(testCase.expected, resp); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/resource/schema/int32planmodifier/use_state_for_unknown.go b/resource/schema/int32planmodifier/use_state_for_unknown.go new file mode 100644 index 000000000..1a09f8707 --- /dev/null +++ b/resource/schema/int32planmodifier/use_state_for_unknown.go @@ -0,0 +1,55 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package int32planmodifier + +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.Int32 { + 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." +} + +// PlanModifyInt32 implements the plan modification logic. +func (m useStateForUnknownModifier) PlanModifyInt32(_ context.Context, req planmodifier.Int32Request, resp *planmodifier.Int32Response) { + // 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/int32planmodifier/use_state_for_unknown_test.go b/resource/schema/int32planmodifier/use_state_for_unknown_test.go new file mode 100644 index 000000000..6cb27e802 --- /dev/null +++ b/resource/schema/int32planmodifier/use_state_for_unknown_test.go @@ -0,0 +1,141 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package int32planmodifier_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/int32planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func TestUseStateForUnknownModifierPlanModifyInt32(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + request planmodifier.Int32Request + expected *planmodifier.Int32Response + }{ + "null-state": { + // when we first create the resource, use the unknown + // value + request: planmodifier.Int32Request{ + StateValue: types.Int32Null(), + PlanValue: types.Int32Unknown(), + ConfigValue: types.Int32Null(), + }, + expected: &planmodifier.Int32Response{ + PlanValue: types.Int32Unknown(), + }, + }, + "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.Int32Request{ + StateValue: types.Int32Value(2), + PlanValue: types.Int32Value(1), + ConfigValue: types.Int32Null(), + }, + expected: &planmodifier.Int32Response{ + PlanValue: types.Int32Value(1), + }, + }, + "non-null-state-unknown-plan": { + // this is the situation we want to preserve the state + // in + request: planmodifier.Int32Request{ + StateValue: types.Int32Value(1), + PlanValue: types.Int32Unknown(), + ConfigValue: types.Int32Null(), + }, + expected: &planmodifier.Int32Response{ + PlanValue: types.Int32Value(1), + }, + }, + "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.Int32Request{ + StateValue: types.Int32Value(1), + PlanValue: types.Int32Unknown(), + ConfigValue: types.Int32Unknown(), + }, + expected: &planmodifier.Int32Response{ + PlanValue: types.Int32Unknown(), + }, + }, + "under-list": { + request: planmodifier.Int32Request{ + ConfigValue: types.Int32Null(), + Path: path.Root("test").AtListIndex(0).AtName("nested_test"), + PlanValue: types.Int32Unknown(), + StateValue: types.Int32Null(), + }, + expected: &planmodifier.Int32Response{ + PlanValue: types.Int32Unknown(), + }, + }, + "under-set": { + request: planmodifier.Int32Request{ + ConfigValue: types.Int32Null(), + Path: path.Root("test").AtSetValue( + types.SetValueMust( + types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "nested_test": types.Int32Type, + }, + }, + []attr.Value{ + types.ObjectValueMust( + map[string]attr.Type{ + "nested_test": types.Int32Type, + }, + map[string]attr.Value{ + "nested_test": types.Int32Unknown(), + }, + ), + }, + ), + ).AtName("nested_test"), + PlanValue: types.Int32Unknown(), + StateValue: types.Int32Null(), + }, + expected: &planmodifier.Int32Response{ + PlanValue: types.Int32Unknown(), + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + resp := &planmodifier.Int32Response{ + PlanValue: testCase.request.PlanValue, + } + + int32planmodifier.UseStateForUnknown().PlanModifyInt32(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/int32.go b/resource/schema/planmodifier/int32.go new file mode 100644 index 000000000..88136157f --- /dev/null +++ b/resource/schema/planmodifier/int32.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" +) + +// Int32 is a schema plan modifier for types.Int32 attributes. +type Int32 interface { + Describer + + // PlanModifyInt32 should perform the modification. + PlanModifyInt32(context.Context, Int32Request, *Int32Response) +} + +// Int32Request is a request for types.Int32 schema plan modification. +type Int32Request 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.Int32 + + // 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.Int32 + + // 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.Int32 + + // 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 + // Int32Response.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 + // Int32Response.Private to update or remove a value. + Private *privatestate.ProviderData +} + +// Int32Response is a response to an Int32Request. +type Int32Response struct { + // PlanValue is the planned new state for the attribute. + PlanValue types.Int32 + + // 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 PlanModifyInt32 operation. + // This field is pre-populated from Int32Request.Private and + // can be modified during the resource's PlanModifyInt32 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/int32.go b/schema/validator/int32.go new file mode 100644 index 000000000..2cbbc3cc2 --- /dev/null +++ b/schema/validator/int32.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" +) + +// Int32 is a schema validator for types.Int32 attributes. +type Int32 interface { + Describer + + // ValidateInt32 should perform the validation. + ValidateInt32(context.Context, Int32Request, *Int32Response) +} + +// Int32Request is a request for types.Int32 schema validation. +type Int32Request 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.Int32 +} + +// Int32Response is a response to a Int32Request. +type Int32Response 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 612fde445..39e5f1d45 100644 --- a/tfsdk/value_as_test.go +++ b/tfsdk/value_as_test.go @@ -39,6 +39,15 @@ func newFloatPointerPointer(in float64) **float64 { return &floatPointer } +func newInt32Pointer(in int32) *int32 { + return &in +} + +func newInt32PointerPointer(in int32) **int32 { + intPointer := &in + return &intPointer +} + func newInt64Pointer(in int64) *int64 { return &in } @@ -109,6 +118,16 @@ func TestValueAs(t *testing.T) { target: newFloatPointerPointer(0.0), expected: newFloatPointerPointer(12.3), }, + "primitive int32 pointer": { + val: types.Int32Value(12), + target: newInt32Pointer(0), + expected: newInt32Pointer(12), + }, + "primitive int32 pointer pointer": { + val: types.Int32Value(12), + target: newInt32PointerPointer(0), + expected: newInt32PointerPointer(12), + }, "primitive int64 pointer": { val: types.Int64Value(12), target: newInt64Pointer(0), diff --git a/types/basetypes/int32_type.go b/types/basetypes/int32_type.go new file mode 100644 index 000000000..3943e88af --- /dev/null +++ b/types/basetypes/int32_type.go @@ -0,0 +1,102 @@ +// 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" +) + +// Int32Typable extends attr.Type for int32 types. +// Implement this interface to create a custom Int32Type type. +type Int32Typable interface { + attr.Type + + // ValueFromInt32 should convert the Int32 to a Int32Valuable type. + ValueFromInt32(context.Context, Int32Value) (Int32Valuable, diag.Diagnostics) +} + +var _ Int32Typable = Int32Type{} + +// Int32Type is the base framework type for an integer number. +// Int32Value is the associated value type. +type Int32Type struct{} + +// ApplyTerraform5AttributePathStep applies the given AttributePathStep to the +// type. +func (t Int32Type) 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 Int32Type) Equal(o attr.Type) bool { + _, ok := o.(Int32Type) + + return ok +} + +// String returns a human-readable string of the type name. +func (t Int32Type) String() string { + return "basetypes.Int32Type" +} + +// TerraformType returns the tftypes.Type that should be used to represent this +// framework type. +func (t Int32Type) TerraformType(_ context.Context) tftypes.Type { + return tftypes.Number +} + +// ValueFromInt32 returns a Int32Valuable type given a Int32Value. +func (t Int32Type) ValueFromInt32(_ context.Context, v Int32Value) (Int32Valuable, 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 Int32Type) ValueFromTerraform(ctx context.Context, in tftypes.Value) (attr.Value, error) { + if !in.IsKnown() { + return NewInt32Unknown(), nil + } + + if in.IsNull() { + return NewInt32Null(), nil + } + + var bigF *big.Float + err := in.As(&bigF) + + if err != nil { + return nil, err + } + + if !bigF.IsInt() { + return nil, fmt.Errorf("Value %s is not an integer.", bigF) + } + + i, accuracy := bigF.Int64() + + if accuracy != 0 { + return nil, fmt.Errorf("Value %s cannot be represented as a 32-bit integer.", bigF) + } + + if i < math.MinInt32 || i > math.MaxInt32 { + return nil, fmt.Errorf("Value %s cannot be represented as a 32-bit integer.", bigF) + } + + return NewInt32Value(int32(i)), nil +} + +// ValueType returns the Value type. +func (t Int32Type) ValueType(_ context.Context) attr.Value { + // This Value does not need to be valid. + return Int32Value{} +} diff --git a/types/basetypes/int32_type_test.go b/types/basetypes/int32_type_test.go new file mode 100644 index 000000000..af5c8bdd0 --- /dev/null +++ b/types/basetypes/int32_type_test.go @@ -0,0 +1,97 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package basetypes + +import ( + "context" + "math" + "testing" + + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/attr" +) + +func TestInt32TypeValueFromTerraform(t *testing.T) { + t.Parallel() + + type testCase struct { + input tftypes.Value + expectation attr.Value + expectedErr string + } + tests := map[string]testCase{ + "value": { + input: tftypes.NewValue(tftypes.Number, 123), + expectation: NewInt32Value(123), + }, + "unknown": { + input: tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), + expectation: NewInt32Unknown(), + }, + "null": { + input: tftypes.NewValue(tftypes.Number, nil), + expectation: NewInt32Null(), + }, + "wrongType": { + input: tftypes.NewValue(tftypes.String, "oops"), + expectedErr: "can't unmarshal tftypes.String into *big.Float, expected *big.Float", + }, + "Int32Min value": { + input: tftypes.NewValue(tftypes.Number, math.MinInt32), + expectation: NewInt32Value(math.MinInt32), + }, + "Int32Max value": { + input: tftypes.NewValue(tftypes.Number, math.MaxInt32), + expectation: NewInt32Value(math.MaxInt32), + }, + "Int32Min - 1: error": { + input: tftypes.NewValue(tftypes.Number, math.MinInt32-1), + expectedErr: "Value %!s(*big.Float=-2147483649) cannot be represented as a 32-bit integer.", + }, + "Int32Max + 1: error": { + input: tftypes.NewValue(tftypes.Number, math.MaxInt32+1), + expectedErr: "Value %!s(*big.Float=2147483648) cannot be represented as a 32-bit integer.", + }, + "float value: error": { + input: tftypes.NewValue(tftypes.Number, 32.1), + expectedErr: "Value %!s(*big.Float=32.1) is not an integer.", + }, + } + for name, test := range tests { + name, test := name, test + t.Run(name, func(t *testing.T) { + t.Parallel() + ctx := context.Background() + + got, err := Int32Type{}.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 !got.Equal(test.expectation) { + t.Errorf("Expected %+v, got %+v", test.expectation, got) + } + 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/int32_value.go b/types/basetypes/int32_value.go new file mode 100644 index 000000000..1561cb894 --- /dev/null +++ b/types/basetypes/int32_value.go @@ -0,0 +1,175 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package basetypes + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" +) + +var ( + _ Int32Valuable = Int32Value{} +) + +// Int32Valuable extends attr.Value for int32 value types. +// Implement this interface to create a custom Int32 value type. +type Int32Valuable interface { + attr.Value + + // ToInt32Value should convert the value type to an Int32. + ToInt32Value(ctx context.Context) (Int32Value, diag.Diagnostics) +} + +// Int32ValuableWithSemanticEquals extends Int32Valuable with semantic +// equality logic. +type Int32ValuableWithSemanticEquals interface { + Int32Valuable + + // Int32SemanticEquals 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. + Int32SemanticEquals(context.Context, Int32Valuable) (bool, diag.Diagnostics) +} + +// NewInt32Null creates an Int32 with a null value. Determine whether the value is +// null via the Int32 type IsNull method. +func NewInt32Null() Int32Value { + return Int32Value{ + state: attr.ValueStateNull, + } +} + +// NewInt32Unknown creates an Int32 with an unknown value. Determine whether the +// value is unknown via the Int32 type IsUnknown method. +func NewInt32Unknown() Int32Value { + return Int32Value{ + state: attr.ValueStateUnknown, + } +} + +// NewInt32Value creates an Int32 with a known value. Access the value via the Int32 +// type ValueInt32 method. +func NewInt32Value(value int32) Int32Value { + return Int32Value{ + state: attr.ValueStateKnown, + value: value, + } +} + +// NewInt32PointerValue creates an Int32 with a null value if nil or a known +// value. Access the value via the Int32 type ValueInt32Pointer method. +func NewInt32PointerValue(value *int32) Int32Value { + if value == nil { + return NewInt32Null() + } + + return NewInt32Value(*value) +} + +// Int32Value represents a 32-bit integer value, exposed as an int32. +type Int32Value 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 int32 +} + +// Equal returns true if `other` is an Int32 and has the same value as `i`. +func (i Int32Value) Equal(other attr.Value) bool { + o, ok := other.(Int32Value) + + if !ok { + return false + } + + if i.state != o.state { + return false + } + + if i.state != attr.ValueStateKnown { + return true + } + + return i.value == o.value +} + +// ToTerraformValue returns the data contained in the Int32 as a tftypes.Value. +func (i Int32Value) ToTerraformValue(ctx context.Context) (tftypes.Value, error) { + switch i.state { + case attr.ValueStateKnown: + if err := tftypes.ValidateValue(tftypes.Number, i.value); err != nil { + return tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), err + } + + return tftypes.NewValue(tftypes.Number, i.value), nil + case 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 Int32 state in ToTerraformValue: %s", i.state)) + } +} + +// Type returns a Int32Type. +func (i Int32Value) Type(ctx context.Context) attr.Type { + return Int32Type{} +} + +// IsNull returns true if the Int32 represents a null value. +func (i Int32Value) IsNull() bool { + return i.state == attr.ValueStateNull +} + +// IsUnknown returns true if the Int32 represents a currently unknown value. +func (i Int32Value) IsUnknown() bool { + return i.state == attr.ValueStateUnknown +} + +// String returns a human-readable representation of the Int32 value. +// The string returned here is not protected by any compatibility guarantees, +// and is intended for logging and error reporting. +func (i Int32Value) String() string { + if i.IsUnknown() { + return attr.UnknownValueString + } + + if i.IsNull() { + return attr.NullValueString + } + + return fmt.Sprintf("%d", i.value) +} + +// ValueInt32 returns the known int32 value. If Int32 is null or unknown, returns +// 0. +func (i Int32Value) ValueInt32() int32 { + return i.value +} + +// ValueInt32Pointer returns a pointer to the known int32 value, nil for a +// null value, or a pointer to 0 for an unknown value. +func (i Int32Value) ValueInt32Pointer() *int32 { + if i.IsNull() { + return nil + } + + return &i.value +} + +// ToInt32Value returns Int32. +func (i Int32Value) ToInt32Value(context.Context) (Int32Value, diag.Diagnostics) { + return i, nil +} diff --git a/types/basetypes/int32_value_test.go b/types/basetypes/int32_value_test.go new file mode 100644 index 000000000..15f505cac --- /dev/null +++ b/types/basetypes/int32_value_test.go @@ -0,0 +1,355 @@ +// 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" +) + +func TestInt32ValueToTerraformValue(t *testing.T) { + t.Parallel() + + type testCase struct { + input Int32Value + expectation interface{} + } + tests := map[string]testCase{ + "known": { + input: NewInt32Value(123), + expectation: tftypes.NewValue(tftypes.Number, big.NewFloat(123)), + }, + "unknown": { + input: NewInt32Unknown(), + expectation: tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), + }, + "null": { + input: NewInt32Null(), + 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 TestInt32ValueEqual(t *testing.T) { + t.Parallel() + + type testCase struct { + input Int32Value + candidate attr.Value + expectation bool + } + tests := map[string]testCase{ + "known-known-same": { + input: NewInt32Value(123), + candidate: NewInt32Value(123), + expectation: true, + }, + "known-known-diff": { + input: NewInt32Value(123), + candidate: NewInt32Value(456), + expectation: false, + }, + "known-unknown": { + input: NewInt32Value(123), + candidate: NewInt32Unknown(), + expectation: false, + }, + "known-null": { + input: NewInt32Value(123), + candidate: NewInt32Null(), + expectation: false, + }, + "unknown-value": { + input: NewInt32Unknown(), + candidate: NewInt32Value(123), + expectation: false, + }, + "unknown-unknown": { + input: NewInt32Unknown(), + candidate: NewInt32Unknown(), + expectation: true, + }, + "unknown-null": { + input: NewInt32Unknown(), + candidate: NewInt32Null(), + expectation: false, + }, + "null-known": { + input: NewInt32Null(), + candidate: NewInt32Value(123), + expectation: false, + }, + "null-unknown": { + input: NewInt32Null(), + candidate: NewInt32Unknown(), + expectation: false, + }, + "null-null": { + input: NewInt32Null(), + candidate: NewInt32Null(), + 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 TestInt32ValueIsNull(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + input Int32Value + expected bool + }{ + "known": { + input: NewInt32Value(24), + expected: false, + }, + "null": { + input: NewInt32Null(), + expected: true, + }, + "unknown": { + input: NewInt32Unknown(), + 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 TestInt32ValueIsUnknown(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + input Int32Value + expected bool + }{ + "known": { + input: NewInt32Value(24), + expected: false, + }, + "null": { + input: NewInt32Null(), + expected: false, + }, + "unknown": { + input: NewInt32Unknown(), + 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 TestInt32ValueString(t *testing.T) { + t.Parallel() + + type testCase struct { + input Int32Value + expectation string + } + tests := map[string]testCase{ + "known-less-than-one": { + input: NewInt32Value(-2147183641), + expectation: "-2147183641", + }, + "known-more-than-one": { + input: NewInt32Value(2147483620), + expectation: "2147483620", + }, + "known-min-int32": { + input: NewInt32Value(math.MinInt32), + expectation: "-2147483648", + }, + "known-max-int32": { + input: NewInt32Value(math.MaxInt32), + expectation: "2147483647", + }, + "unknown": { + input: NewInt32Unknown(), + expectation: "", + }, + "null": { + input: NewInt32Null(), + expectation: "", + }, + "zero-value": { + input: Int32Value{}, + 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 TestInt32ValueValueInt32(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + input Int32Value + expected int32 + }{ + "known": { + input: NewInt32Value(24), + expected: 24, + }, + "null": { + input: NewInt32Null(), + expected: 0, + }, + "unknown": { + input: NewInt32Unknown(), + expected: 0, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.input.ValueInt32() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt32ValueValueInt32Pointer(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + input Int32Value + expected *int32 + }{ + "known": { + input: NewInt32Value(24), + expected: pointer(int32(24)), + }, + "null": { + input: NewInt32Null(), + expected: nil, + }, + "unknown": { + input: NewInt32Unknown(), + expected: pointer(int32(0)), + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.input.ValueInt32Pointer() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNewInt32PointerValue(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + value *int32 + expected Int32Value + }{ + "nil": { + value: nil, + expected: NewInt32Null(), + }, + "value": { + value: pointer(int32(123)), + expected: NewInt32Value(123), + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := NewInt32PointerValue(testCase.value) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/types/int32_type.go b/types/int32_type.go new file mode 100644 index 000000000..b2d5703b4 --- /dev/null +++ b/types/int32_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 Int32Type = basetypes.Int32Type{} diff --git a/types/int32_value.go b/types/int32_value.go new file mode 100644 index 000000000..c19cdc02b --- /dev/null +++ b/types/int32_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 Int32 = basetypes.Int32Value + +// Int32Null creates a Int32 with a null value. Determine whether the value is +// null via the Int32 type IsNull method. +func Int32Null() basetypes.Int32Value { + return basetypes.NewInt32Null() +} + +// Int32Unknown creates a Int32 with an unknown value. Determine whether the +// value is unknown via the Int32 type IsUnknown method. +func Int32Unknown() basetypes.Int32Value { + return basetypes.NewInt32Unknown() +} + +// Int32Value creates a Int32 with a known value. Access the value via the +// Int32 type ValueInt32 method. +func Int32Value(value int32) basetypes.Int32Value { + return basetypes.NewInt32Value(value) +} + +// Int32PointerValue creates a Int32 with a null value if nil or a known value. +func Int32PointerValue(value *int32) basetypes.Int32Value { + return basetypes.NewInt32PointerValue(value) +} diff --git a/website/data/plugin-framework-nav-data.json b/website/data/plugin-framework-nav-data.json index 6d63120b7..27838ab6e 100644 --- a/website/data/plugin-framework-nav-data.json +++ b/website/data/plugin-framework-nav-data.json @@ -156,6 +156,10 @@ "title": "Float64", "path": "functions/parameters/float64" }, + { + "title": "Int32", + "path": "functions/parameters/int32" + }, { "title": "Int64", "path": "functions/parameters/int64" @@ -205,6 +209,10 @@ "title": "Float64", "path": "functions/returns/float64" }, + { + "title": "Int32", + "path": "functions/returns/int32" + }, { "title": "Int64", "path": "functions/returns/int64" @@ -279,6 +287,10 @@ "title": "Float64", "path": "handling-data/attributes/float64" }, + { + "title": "Int32", + "path": "handling-data/attributes/int32" + }, { "title": "Int64", "path": "handling-data/attributes/int64" @@ -365,6 +377,10 @@ "title": "Float64", "path": "handling-data/types/float64" }, + { + "title": "Int32", + "path": "handling-data/types/int32" + }, { "title": "Int64", "path": "handling-data/types/int64" diff --git a/website/docs/plugin/framework/functions/parameters/index.mdx b/website/docs/plugin/framework/functions/parameters/index.mdx index d055141d9..5382ab89b 100644 --- a/website/docs/plugin/framework/functions/parameters/index.mdx +++ b/website/docs/plugin/framework/functions/parameters/index.mdx @@ -26,6 +26,7 @@ Parameter types that accepts a single data value, such as a boolean, number, or |----------------|----------| | [Bool](/terraform/plugin/framework/functions/parameters/bool) | Boolean true or false | | [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 | | [Number](/terraform/plugin/framework/functions/parameters/number) | Arbitrary precision (generally over 64-bit, up to 512-bit) number | | [String](/terraform/plugin/framework/functions/parameters/string) | Collection of UTF-8 encoded characters | diff --git a/website/docs/plugin/framework/functions/parameters/int32.mdx b/website/docs/plugin/framework/functions/parameters/int32.mdx new file mode 100644 index 000000000..9cb7c6885 --- /dev/null +++ b/website/docs/plugin/framework/functions/parameters/int32.mdx @@ -0,0 +1,101 @@ +--- +page_title: 'Plugin Development - Framework: Int32 Function Parameter' +description: >- + Learn the int32 function parameter type in the provider development framework. +--- + +# Int32 Function Parameter + + + +Use [Float32 Parameter](/terraform/plugin/framework/functions/parameters/float32) for 32-bit floating point numbers. Use [Number Parameter](/terraform/plugin/framework/functions/parameters/number) for arbitrary precision numbers. + + + +Int32 function parameters expect a 32-bit integer number value from a practitioner configuration. Values are accessible in function logic by the Go built-in `int32` type, Go built-in `*int32` type, or the [framework int32 type](/terraform/plugin/framework/handling-data/types/int32). + +In this Terraform configuration example, a int32 parameter is set to the value `123`: + +```hcl +provider::example::example(123) +``` + +## Function Definition + +Use the [`function.Int32Parameter` type](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/function#Int32Parameter) in the [function definition](/terraform/plugin/framework/functions/implementation#definition-method) to accept a int32 value. + +In this example, a function definition includes a first position int32 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.Int32Parameter{ + Name: "int32_param", + // ... potentially other Int32Parameter fields ... + }, + }, + } +} +``` + +If the int32 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 int32 type](/terraform/plugin/framework/handling-data/types/int32). Refer to the collection parameter type documentation for additional details. + +If the int32 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 int32 type](/terraform/plugin/framework/handling-data/types/int32). 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 int32 type](/terraform/plugin/framework/handling-data/types/int32) 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 int32 type](/terraform/plugin/framework/handling-data/types/int32) 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 int32 type](/terraform/plugin/framework/handling-data/types/int32). +* If `AllowNullValue` is enabled, you must use the Go built-in `*int32` type or [framework int32 type](/terraform/plugin/framework/handling-data/types/int32). +* Otherwise, use the Go built-in `int32` type, Go built-in `*int32` type, or [framework int32 type](/terraform/plugin/framework/handling-data/types/int32). + +In this example, a function defines a single int32 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.Int32Parameter{ + Name: "int32_param", + // ... potentially other Int32Parameter fields ... + }, + }, + } +} + +func (f ExampleFunction) Run(ctx context.Context, req function.RunRequest, resp *function.RunResponse) { + var int32Arg int32 + // var int32Arg *int32 // e.g. with AllowNullValue, where Go nil equals Terraform null + // var int32Arg types.Int32 // e.g. with AllowUnknownValues or AllowNullValue + + resp.Error = function.ConcatFuncErrors(resp.Error, req.Arguments.Get(ctx, &int32Arg)) + + // int32Arg is now populated + // ... other logic ... +} +``` diff --git a/website/docs/plugin/framework/functions/returns/index.mdx b/website/docs/plugin/framework/functions/returns/index.mdx index fc2921871..7a924451f 100644 --- a/website/docs/plugin/framework/functions/returns/index.mdx +++ b/website/docs/plugin/framework/functions/returns/index.mdx @@ -26,6 +26,7 @@ Return types that expect a single data value, such as a boolean, number, or stri |----------------|----------| | [Bool](/terraform/plugin/framework/functions/returns/bool) | Boolean true or false | | [Float64](/terraform/plugin/framework/functions/returns/float64) | 64-bit floating point number | +| [Int64](/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/functions/returns/int32.mdx b/website/docs/plugin/framework/functions/returns/int32.mdx new file mode 100644 index 000000000..ff0e8f57a --- /dev/null +++ b/website/docs/plugin/framework/functions/returns/int32.mdx @@ -0,0 +1,71 @@ +--- +page_title: 'Plugin Development - Framework: Int32 Function Return' +description: >- + Learn the int32 function return type in the provider development framework. +--- + +# Int32 Function Return + + + +Use [Float32 Return](/terraform/plugin/framework/functions/returns/float32) for 32-bit floating point numbers. Use [Number Return](/terraform/plugin/framework/functions/returns/number) for arbitrary precision numbers. + + + +Int32 function return expects a 32-bit integer number value from function logic. Set values in function logic with the Go built-in `int32` type, Go built-in `*int32` type, or the [framework int32 type](/terraform/plugin/framework/handling-data/types/int32). + +## Function Definition + +Use the [`function.Int32Return` type](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/function#Int32Return) in the [function definition](/terraform/plugin/framework/functions/implementation#definition-method). + +In this example, a function definition includes a int32 return: + +```go +func (f ExampleFunction) Definition(ctx context.Context, req function.DefinitionRequest, resp *function.DefinitionResponse) { + resp.Definition = function.Definition{ + // ... other Definition fields ... + Return: function.Int32Return{ + // ... potentially other Int32Return 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 `int32` type, Go built-in `*int32` type, or [framework int32 type](/terraform/plugin/framework/handling-data/types/int32). + +In this example, a function defines a int32 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.Int32Return{}, + } +} + +func (f ExampleFunction) Run(ctx context.Context, req function.RunRequest, resp *function.RunResponse) { + // ... other logic ... + + // hardcoded value for example brevity + result := 123 + + resp.Error = function.ConcatFuncErrors(resp.Error, resp.Result.Set(ctx, &result)) +} +``` diff --git a/website/docs/plugin/framework/handling-data/attributes/float64.mdx b/website/docs/plugin/framework/handling-data/attributes/float64.mdx index 3b9d035b8..b8364f462 100644 --- a/website/docs/plugin/framework/handling-data/attributes/float64.mdx +++ b/website/docs/plugin/framework/handling-data/attributes/float64.mdx @@ -48,9 +48,9 @@ func (r ThingResource) Schema(ctx context.Context, req resource.SchemaRequest, r } ``` -If the string 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 [float64 type](/terraform/plugin/framework/handling-data/types/float64). Refer to the collection attribute type documentation for additional details. +If the float64 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 [float64 type](/terraform/plugin/framework/handling-data/types/float64). Refer to the collection attribute type documentation for additional details. -If the string 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 [float64 type](/terraform/plugin/framework/handling-data/types/float64). Refer to the object attribute type documentation for additional details. +If the float64 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 [float64 type](/terraform/plugin/framework/handling-data/types/float64). Refer to the object attribute type documentation for additional details. ### Configurability diff --git a/website/docs/plugin/framework/handling-data/attributes/index.mdx b/website/docs/plugin/framework/handling-data/attributes/index.mdx index 7f6bcafc6..877d5fc8e 100644 --- a/website/docs/plugin/framework/handling-data/attributes/index.mdx +++ b/website/docs/plugin/framework/handling-data/attributes/index.mdx @@ -27,6 +27,7 @@ Attribute types that contain a single data value, such as a boolean, number, or |----------------|----------| | [Bool](/terraform/plugin/framework/handling-data/attributes/bool) | Boolean true or false | | [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 | | [Number](/terraform/plugin/framework/handling-data/attributes/number) | Arbitrary precision (generally over 64-bit, up to 512-bit) number | | [String](/terraform/plugin/framework/handling-data/attributes/string) | Collection of UTF-8 encoded characters | diff --git a/website/docs/plugin/framework/handling-data/attributes/int32.mdx b/website/docs/plugin/framework/handling-data/attributes/int32.mdx new file mode 100644 index 000000000..ad4b46a8f --- /dev/null +++ b/website/docs/plugin/framework/handling-data/attributes/int32.mdx @@ -0,0 +1,126 @@ +--- +page_title: 'Plugin Development - Framework: Int32 Attribute' +description: >- + Learn the int32 attribute type in the provider development framework. +--- + +# Int32 Attribute + + + +Use [Float32 Attribute](/terraform/plugin/framework/handling-data/attributes/float32) for 32-bit floating point numbers. Use [Number Attribute](/terraform/plugin/framework/handling-data/attributes/number) for arbitrary precision numbers. + + + +Int32 attributes store a 32-bit integer number. Values are represented by a [int32 type](/terraform/plugin/framework/handling-data/types/int32) in the framework. + +In this Terraform configuration example, an int32 attribute named `example_attribute` is set to the value `123`: + +```hcl +resource "examplecloud_thing" "example" { + example_attribute = 123 +} +``` + +## Schema Definition + +Use one of the following attribute types to directly add a int32 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.Int32Attribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/datasource/schema#Int32Attribute) | +| [Provider](/terraform/plugin/framework/provider) | [`schema.Int32Attribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/provider/schema#Int32Attribute) | +| [Resource](/terraform/plugin/framework/resources) | [`schema.Int32Attribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/resource/schema#Int32Attribute) | + +In this example, a resource schema defines a top level required int32 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.Int32Attribute{ + Required: true, + // ... potentially other fields ... + }, + // ... potentially other attributes ... + }, + } +} +``` + +If the int32 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 [int32 type](/terraform/plugin/framework/handling-data/types/int32). Refer to the collection attribute type documentation for additional details. + +If the int32 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 [int32 type](/terraform/plugin/framework/handling-data/types/int32). 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 [`int32default`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/resource/schema/int32default) package defines common use case `Default` implementations: + +- [`StaticInt32(int32)`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/resource/schema/int32default#StaticInt32): Define a static int32 default value for the attribute. + +The [`int32planmodifier`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/resource/schema/int32planmodifier) package defines common use case `PlanModifiers` implementations: + +- [`RequiresReplace()`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/resource/schema/int32planmodifier#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/int32planmodifier#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/int32planmodifier#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/int32planmodifier#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 [`int32validator`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework-validators/int32validator) package within that module has int32 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 [int32 type](/terraform/plugin/framework/handling-data/types/int32#accessing-values) documentation covers methods for interacting with the attribute value itself. + +## Setting Values + +The [int32 type](/terraform/plugin/framework/handling-data/types/int32#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/int64.mdx b/website/docs/plugin/framework/handling-data/attributes/int64.mdx index 8d2e6bd7d..5d793a9cf 100644 --- a/website/docs/plugin/framework/handling-data/attributes/int64.mdx +++ b/website/docs/plugin/framework/handling-data/attributes/int64.mdx @@ -48,9 +48,9 @@ func (r ThingResource) Schema(ctx context.Context, req resource.SchemaRequest, r } ``` -If the string 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 [int64 type](/terraform/plugin/framework/handling-data/types/int64). Refer to the collection attribute type documentation for additional details. +If the int64 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 [int64 type](/terraform/plugin/framework/handling-data/types/int64). Refer to the collection attribute type documentation for additional details. -If the string 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 [int64 type](/terraform/plugin/framework/handling-data/types/int64). Refer to the object attribute type documentation for additional details. +If the int64 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 [int64 type](/terraform/plugin/framework/handling-data/types/int64). Refer to the object attribute type documentation for additional details. ### Configurability diff --git a/website/docs/plugin/framework/handling-data/attributes/number.mdx b/website/docs/plugin/framework/handling-data/attributes/number.mdx index 73d26b36d..3f47e5d5a 100644 --- a/website/docs/plugin/framework/handling-data/attributes/number.mdx +++ b/website/docs/plugin/framework/handling-data/attributes/number.mdx @@ -48,9 +48,9 @@ func (r ThingResource) Schema(ctx context.Context, req resource.SchemaRequest, r } ``` -If the string 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 [number type](/terraform/plugin/framework/handling-data/types/number). Refer to the collection attribute type documentation for additional details. +If the number 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 [number type](/terraform/plugin/framework/handling-data/types/number). Refer to the collection attribute type documentation for additional details. -If the string 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 [number type](/terraform/plugin/framework/handling-data/types/number). Refer to the object attribute type documentation for additional details. +If the number 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 [number type](/terraform/plugin/framework/handling-data/types/number). Refer to the object attribute type documentation for additional details. ### Configurability diff --git a/website/docs/plugin/framework/handling-data/paths.mdx b/website/docs/plugin/framework/handling-data/paths.mdx index 831db78ef..176fc8cbe 100644 --- a/website/docs/plugin/framework/handling-data/paths.mdx +++ b/website/docs/plugin/framework/handling-data/paths.mdx @@ -90,6 +90,7 @@ The following table shows the different [`path.Path` type](https://pkg.go.dev/gi | `schema.BoolAttribute` | N/A | | `schema.DynamicAttribute` | N/A | | `schema.Float64Attribute` | N/A | +| `schema.Int32Attribute` | N/A | | `schema.Int64Attribute` | N/A | | `schema.ListAttribute` | `AtListIndex()` | | `schema.MapAttribute` | `AtMapKey()` | diff --git a/website/docs/plugin/framework/handling-data/types/custom.mdx b/website/docs/plugin/framework/handling-data/types/custom.mdx index a82914fc2..e6f44bf0d 100644 --- a/website/docs/plugin/framework/handling-data/types/custom.mdx +++ b/website/docs/plugin/framework/handling-data/types/custom.mdx @@ -92,6 +92,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.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) | | [`basetypes.ListType`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types/basetypes#ListType) | [`basetypes.ListTypable`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types/basetypes#ListTypable) | | [`basetypes.MapType`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types/basetypes#MapType) | [`basetypes.MapTypable`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types/basetypes#MapTypable) | diff --git a/website/docs/plugin/framework/handling-data/types/index.mdx b/website/docs/plugin/framework/handling-data/types/index.mdx index ebe453013..17a7d2563 100644 --- a/website/docs/plugin/framework/handling-data/types/index.mdx +++ b/website/docs/plugin/framework/handling-data/types/index.mdx @@ -27,6 +27,7 @@ Types that contain a single data value, such as a boolean, number, or string. Th |----------------|----------| | [Bool](/terraform/plugin/framework/handling-data/types/bool) | Boolean true or false | | [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 | | [Number](/terraform/plugin/framework/handling-data/types/number) | Arbitrary precision (generally over 64-bit, up to 512-bit) number | | [String](/terraform/plugin/framework/handling-data/types/string) | Collection of UTF-8 encoded characters | diff --git a/website/docs/plugin/framework/handling-data/types/int32.mdx b/website/docs/plugin/framework/handling-data/types/int32.mdx new file mode 100644 index 000000000..1a26debc4 --- /dev/null +++ b/website/docs/plugin/framework/handling-data/types/int32.mdx @@ -0,0 +1,119 @@ +--- +page_title: 'Plugin Development - Framework: Int32 Type' +description: >- + Learn the int32 value type in the provider development framework. +--- + +# Int32 Type + + + +Use [Float32 Type](/terraform/plugin/framework/handling-data/types/float32) for 32-bit floating point numbers. Use [Number Attribute](/terraform/plugin/framework/handling-data/types/number) for arbitrary precision numbers. + + + +Int32 types store a 32-bit integer number. + +By default, int32 from [schema](/terraform/plugin/framework/handling-data/schemas) (configuration, plan, and state) data are represented in the framework by [`types.Int32Type`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types#Int32Type) and its associated value storage type of [`types.Int32`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types#Int32). 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 `*int32`. 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 int32 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.Int32Attribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/datasource/schema#Int32Attribute) | +| [Provider](/terraform/plugin/framework/provider) | [`schema.Int32Attribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/provider/schema#Int32Attribute) | +| [Resource](/terraform/plugin/framework/resources) | [`schema.Int32Attribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/resource/schema#Int32Attribute) | + +If the int32 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.Int32Type` or the appropriate [custom type](#extending). + +If the int32 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.Int32Type` or the appropriate [custom type](#extending). + +## Accessing Values + + + +Review the [attribute documentation](/terraform/plugin/framework/handling-data/attributes/int32#accessing-values) to understand how schema-based data gets mapped into accessible values, such as a `types.Int32` in this case. + + + +Access `types.Int32` information via the following methods: + +* [`(types.Int32).IsNull() bool`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types/basetypes#Int32Value.IsNull): Returns true if the int32 is null. +* [`(types.Int32).IsUnknown() bool`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types/basetypes#Int32Value.IsUnknown): Returns true if the int32 is unknown. +* [`(types.Int32).ValueInt32() int32`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types/basetypes#Int32Value.ValueInt32): Returns the known int32, or `0` if null or unknown. +* [`(types.Int32).ValueInt32Pointer() *int32`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types/basetypes#Int32Value.ValueInt32Pointer): Returns a int32 pointer to a known value, `nil` if null, or a pointer to `0` if unknown. + +In this example, a int32 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.Int32 `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() { + // ... +} + +// myInt32 now contains a Go int32 with the known value +myInt32 := data.ExampleAttribute.ValueInt32() +``` + +## Setting Values + +Call one of the following to create a `types.Int32` value: + +* [`types.Int32Null()`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types#Int32Null): A null int32 value. +* [`types.Int32Unknown()`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types#Int32Unknown): An unknown int32 value. +* [`types.Int32Value(int32)`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types#Int32Value): A known value. +* [`types.Int32PointerValue(*int32)`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/types#Int32PointerValue): A known value. + +In this example, a known int32 value is created: + +```go +types.Int32Value(123) +``` + +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 `int32` is directly used to set a int32 attribute value: + +```go +diags := resp.State.SetAttribute(ctx, path.Root("example_attribute"), 123) +``` + +In this example, a `types.List` of `types.Int32` is created from a `[]int32`: + +```go +listValue, diags := types.ListValueFrom(ctx, types.Int32Type, []int32{123, 456}) +``` + +## 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/resources/default.mdx b/website/docs/plugin/framework/resources/default.mdx index d543c7d2d..84ec283a2 100644 --- a/website/docs/plugin/framework/resources/default.mdx +++ b/website/docs/plugin/framework/resources/default.mdx @@ -51,6 +51,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.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) | | [`schema.ListAttribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/resource/schema#ListAttribute) / [`schema.ListNestedAttribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/resource/schema#ListNestedAttribute) | [`resource/schema/listdefault` package](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/resource/schema/listdefault) | | [`schema.MapAttribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/resource/schema#MapAttribute) / [`schema.MapNestedAttribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/resource/schema#MapNestedAttribute) | [`resource/schema/mapdefault` package](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/resource/schema/mapdefault) |