From 76b7831b5d6d8be63a7259ea3459172c3d05220e Mon Sep 17 00:00:00 2001 From: Steph Date: Fri, 4 Jul 2025 11:52:21 +0200 Subject: [PATCH 1/4] add primitive types as well as list and map collection types --- list/schema/attribute.go | 7 + list/schema/bool_attribute.go | 191 +++++++++ list/schema/bool_attribute_test.go | 470 +++++++++++++++++++++ list/schema/float32_attribute.go | 194 +++++++++ list/schema/float32_attribute_test.go | 470 +++++++++++++++++++++ list/schema/float64_attribute.go | 194 +++++++++ list/schema/float64_attribute_test.go | 470 +++++++++++++++++++++ list/schema/int32_attribute.go | 194 +++++++++ list/schema/int32_attribute_test.go | 470 +++++++++++++++++++++ list/schema/int64_attribute.go | 194 +++++++++ list/schema/int64_attribute_test.go | 470 +++++++++++++++++++++ list/schema/list_attribute.go | 226 +++++++++++ list/schema/list_attribute_test.go | 560 ++++++++++++++++++++++++++ list/schema/map_attribute.go | 229 +++++++++++ list/schema/map_attribute_test.go | 560 ++++++++++++++++++++++++++ 15 files changed, 4899 insertions(+) create mode 100644 list/schema/bool_attribute.go create mode 100644 list/schema/bool_attribute_test.go create mode 100644 list/schema/float32_attribute.go create mode 100644 list/schema/float32_attribute_test.go create mode 100644 list/schema/float64_attribute.go create mode 100644 list/schema/float64_attribute_test.go create mode 100644 list/schema/int32_attribute.go create mode 100644 list/schema/int32_attribute_test.go create mode 100644 list/schema/int64_attribute.go create mode 100644 list/schema/int64_attribute_test.go create mode 100644 list/schema/list_attribute.go create mode 100644 list/schema/list_attribute_test.go create mode 100644 list/schema/map_attribute.go create mode 100644 list/schema/map_attribute_test.go diff --git a/list/schema/attribute.go b/list/schema/attribute.go index 00a674338..567903bb8 100644 --- a/list/schema/attribute.go +++ b/list/schema/attribute.go @@ -9,6 +9,13 @@ import ( // Attribute define a value field inside the Schema. Implementations in this // package include: +// - BoolAttribute +// - Float32Attribute +// - Float64Attribute +// - Int32Attribute +// - Int64Attribute +// - ListAttribute +// - MapAttribute // - StringAttribute // // In practitioner configurations, an equals sign (=) is required to set diff --git a/list/schema/bool_attribute.go b/list/schema/bool_attribute.go new file mode 100644 index 000000000..87a3d3a0e --- /dev/null +++ b/list/schema/bool_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 = BoolAttribute{} + _ fwxschema.AttributeWithBoolValidators = BoolAttribute{} +) + +// BoolAttribute represents a schema attribute that is a boolean. When +// retrieving the value for this attribute, use types.Bool as the value type +// unless the CustomType field is set. +// +// Terraform configurations configure this attribute using expressions that +// return a boolean or directly via the true/false keywords. +// +// example_attribute = true +// +// Terraform configurations reference this attribute using the attribute name. +// +// .example_attribute +type BoolAttribute struct { + // CustomType enables the use of a custom attribute type in place of the + // default basetypes.BoolType. When retrieving data, the basetypes.BoolValuable + // associated with this custom type must be used in place of types.Bool. + CustomType basetypes.BoolTypable + + // 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 + + // 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.Bool +} + +// ApplyTerraform5AttributePathStep always returns an error as it is not +// possible to step further into a BoolAttribute. +func (a BoolAttribute) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { + return a.GetType().ApplyTerraform5AttributePathStep(step) +} + +// BoolValidators returns the Validators field value. +func (a BoolAttribute) BoolValidators() []validator.Bool { + return a.Validators +} + +// Equal returns true if the given Attribute is a BoolAttribute +// and all fields are equal. +func (a BoolAttribute) Equal(o fwschema.Attribute) bool { + if _, ok := o.(BoolAttribute); !ok { + return false + } + + return fwschema.AttributesEqual(a, o) +} + +// GetDeprecationMessage returns the DeprecationMessage field value. +func (a BoolAttribute) GetDeprecationMessage() string { + return a.DeprecationMessage +} + +// GetDescription returns the Description field value. +func (a BoolAttribute) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription returns the MarkdownDescription field value. +func (a BoolAttribute) GetMarkdownDescription() string { + return a.MarkdownDescription +} + +// GetType returns types.StringType or the CustomType field value if defined. +func (a BoolAttribute) GetType() attr.Type { + if a.CustomType != nil { + return a.CustomType + } + + return types.BoolType +} + +// IsComputed returns false because it does not apply to ListResource schemas. +func (a BoolAttribute) IsComputed() bool { + return false +} + +// IsOptional returns the Optional field value. +func (a BoolAttribute) IsOptional() bool { + return a.Optional +} + +// IsRequired returns the Required field value. +func (a BoolAttribute) IsRequired() bool { + return a.Required +} + +// IsSensitive returns false because it does not apply to ListResource schemas. +func (a BoolAttribute) IsSensitive() bool { + return false +} + +// IsWriteOnly returns false because it does not apply to ListResource schemas. +func (a BoolAttribute) IsWriteOnly() bool { + return false +} + +// IsRequiredForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a BoolAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a BoolAttribute) IsOptionalForImport() bool { + return false +} diff --git a/list/schema/bool_attribute_test.go b/list/schema/bool_attribute_test.go new file mode 100644 index 000000000..9e5c0648b --- /dev/null +++ b/list/schema/bool_attribute_test.go @@ -0,0 +1,470 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema_test + +import ( + "fmt" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" + "github.com/hashicorp/terraform-plugin-framework/list/schema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestBoolAttributeApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.BoolAttribute + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + attribute: schema.BoolAttribute{}, + step: tftypes.AttributeName("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.AttributeName to basetypes.BoolType"), + }, + "ElementKeyInt": { + attribute: schema.BoolAttribute{}, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyInt to basetypes.BoolType"), + }, + "ElementKeyString": { + attribute: schema.BoolAttribute{}, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyString to basetypes.BoolType"), + }, + "ElementKeyValue": { + attribute: schema.BoolAttribute{}, + step: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyValue to basetypes.BoolType"), + }, + } + + for name, testCase := range testCases { + 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 TestBoolAttributeBoolValidators(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.BoolAttribute + expected []validator.Bool + }{ + "no-validators": { + attribute: schema.BoolAttribute{}, + expected: nil, + }, + "validators": { + attribute: schema.BoolAttribute{ + Validators: []validator.Bool{}, + }, + expected: []validator.Bool{}, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.BoolValidators() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestBoolAttributeGetDeprecationMessage(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.BoolAttribute + expected string + }{ + "no-deprecation-message": { + attribute: schema.BoolAttribute{}, + expected: "", + }, + "deprecation-message": { + attribute: schema.BoolAttribute{ + DeprecationMessage: "test deprecation message", + }, + expected: "test deprecation message", + }, + } + + for name, testCase := range testCases { + 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 TestBoolAttributeEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.BoolAttribute + other fwschema.Attribute + expected bool + }{ + "different-type": { + attribute: schema.BoolAttribute{}, + other: testschema.AttributeWithBoolValidators{}, + expected: false, + }, + "equal": { + attribute: schema.BoolAttribute{}, + other: schema.BoolAttribute{}, + expected: true, + }, + } + + for name, testCase := range testCases { + 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 TestBoolAttributeGetDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.BoolAttribute + expected string + }{ + "no-description": { + attribute: schema.BoolAttribute{}, + expected: "", + }, + "description": { + attribute: schema.BoolAttribute{ + Description: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + 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 TestBoolAttributeGetMarkdownDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.BoolAttribute + expected string + }{ + "no-markdown-description": { + attribute: schema.BoolAttribute{}, + expected: "", + }, + "markdown-description": { + attribute: schema.BoolAttribute{ + MarkdownDescription: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + 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 TestBoolAttributeGetType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.BoolAttribute + expected attr.Type + }{ + "base": { + attribute: schema.BoolAttribute{}, + expected: types.BoolType, + }, + "custom-type": { + attribute: schema.BoolAttribute{ + CustomType: testtypes.BoolType{}, + }, + expected: testtypes.BoolType{}, + }, + } + + for name, testCase := range testCases { + 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 TestBoolAttributeIsComputed(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.BoolAttribute + expected bool + }{ + "not-computed": { + attribute: schema.BoolAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + 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 TestBoolAttributeIsOptional(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.BoolAttribute + expected bool + }{ + "not-optional": { + attribute: schema.BoolAttribute{}, + expected: false, + }, + "optional": { + attribute: schema.BoolAttribute{ + Optional: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + 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 TestBoolAttributeIsRequired(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.BoolAttribute + expected bool + }{ + "not-required": { + attribute: schema.BoolAttribute{}, + expected: false, + }, + "required": { + attribute: schema.BoolAttribute{ + Required: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + 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 TestBoolAttributeIsSensitive(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.BoolAttribute + expected bool + }{ + "not-sensitive": { + attribute: schema.BoolAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + 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 TestBoolAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.BoolAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.BoolAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestBoolAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.BoolAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.BoolAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestBoolAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.BoolAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.BoolAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/list/schema/float32_attribute.go b/list/schema/float32_attribute.go new file mode 100644 index 000000000..4adbbabdd --- /dev/null +++ b/list/schema/float32_attribute.go @@ -0,0 +1,194 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema + +import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +// Ensure the implementation satisifies the desired interfaces. +var ( + _ Attribute = Float32Attribute{} + _ fwxschema.AttributeWithFloat32Validators = Float32Attribute{} +) + +// Float32Attribute represents a schema attribute that is a 32-bit floating +// point number. When retrieving the value for this attribute, use +// types.Float32 as the value type unless the CustomType field is set. +// +// Use Int32Attribute for 32-bit integer attributes or NumberAttribute for +// 512-bit generic number attributes. +// +// Terraform configurations configure this attribute using expressions that +// return a number or directly via a floating point value. +// +// example_attribute = 123.45 +// +// Terraform configurations reference this attribute using the attribute name. +// +// .example_attribute +type Float32Attribute struct { + // CustomType enables the use of a custom attribute type in place of the + // default basetypes.Float32Type. When retrieving data, the basetypes.Float32Valuable + // associated with this custom type must be used in place of types.Float32. + CustomType basetypes.Float32Typable + + // Required indicates whether the practitioner must enter a value for + // this attribute or not. Required and Optional cannot both be true, + // and Required and Computed cannot both be true. + Required bool + + // Optional indicates whether the practitioner can choose to enter a value + // for this attribute or not. Optional and Required cannot both be true. + Optional bool + + // Description is used in various tooling, like the language server, to + // give practitioners more information about what this attribute is, + // what it's for, and how it should be used. It should be written as + // plain text, with no special formatting. + Description string + + // MarkdownDescription is used in various tooling, like the + // documentation generator, to give practitioners more information + // about what this attribute is, what it's for, and how it should be + // used. It should be formatted using Markdown. + MarkdownDescription string + + // DeprecationMessage defines warning diagnostic details to display when + // practitioner configurations use this Attribute. The warning diagnostic + // summary is automatically set to "Attribute Deprecated" along with + // configuration source file and line information. + // + // Set this field to a practitioner actionable message such as: + // + // - "Configure other_attribute instead. This attribute will be removed + // in the next major version of the provider." + // - "Remove this attribute's configuration as it no longer is used and + // the attribute will be removed in the next major version of the + // provider." + // + // In Terraform 1.2.7 and later, this warning diagnostic is displayed any + // time a practitioner attempts to configure a value for this attribute and + // certain scenarios where this attribute is referenced. + // + // In Terraform 1.2.6 and earlier, this warning diagnostic is only + // displayed when the Attribute is Required or Optional, and if the + // practitioner configuration sets the value to a known or unknown value + // (which may eventually be null). It has no effect when the Attribute is + // Computed-only (read-only; not Required or Optional). + // + // Across any Terraform version, there are no warnings raised for + // practitioner configuration values set directly to null, as there is no + // way for the framework to differentiate between an unset and null + // configuration due to how Terraform sends configuration information + // across the protocol. + // + // Additional information about deprecation enhancements for read-only + // attributes can be found in: + // + // - https://github.com/hashicorp/terraform/issues/7569 + // + DeprecationMessage string + + // Validators define value validation functionality for the attribute. All + // elements of the slice of AttributeValidator are run, regardless of any + // previous error diagnostics. + // + // Many common use case validators can be found in the + // github.com/hashicorp/terraform-plugin-framework-validators Go module. + // + // If the Type field points to a custom type that implements the + // xattr.TypeWithValidate interface, the validators defined in this field + // are run in addition to the validation defined by the type. + Validators []validator.Float32 +} + +// ApplyTerraform5AttributePathStep always returns an error as it is not +// possible to step further into a Float32Attribute. +func (a Float32Attribute) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { + return a.GetType().ApplyTerraform5AttributePathStep(step) +} + +// Equal returns true if the given Attribute is a Float32Attribute +// and all fields are equal. +func (a Float32Attribute) Equal(o fwschema.Attribute) bool { + if _, ok := o.(Float32Attribute); !ok { + return false + } + + return fwschema.AttributesEqual(a, o) +} + +// Float32Validators returns the Validators field value. +func (a Float32Attribute) Float32Validators() []validator.Float32 { + return a.Validators +} + +// GetDeprecationMessage returns the DeprecationMessage field value. +func (a Float32Attribute) GetDeprecationMessage() string { + return a.DeprecationMessage +} + +// GetDescription returns the Description field value. +func (a Float32Attribute) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription returns the MarkdownDescription field value. +func (a Float32Attribute) GetMarkdownDescription() string { + return a.MarkdownDescription +} + +// GetType returns types.Float32Type or the CustomType field value if defined. +func (a Float32Attribute) GetType() attr.Type { + if a.CustomType != nil { + return a.CustomType + } + + return types.Float32Type +} + +// IsComputed returns false because it does not apply to ListResource schemas. +func (a Float32Attribute) IsComputed() bool { + return false +} + +// IsOptional returns the Optional field value. +func (a Float32Attribute) IsOptional() bool { + return a.Optional +} + +// IsRequired returns the Required field value. +func (a Float32Attribute) IsRequired() bool { + return a.Required +} + +// IsSensitive returns false because it does not apply to ListResource schemas. +func (a Float32Attribute) IsSensitive() bool { + return false +} + +// IsWriteOnly returns false because it does not apply to ListResource schemas. +func (a Float32Attribute) IsWriteOnly() bool { + return false +} + +// IsRequiredForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a Float32Attribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a Float32Attribute) IsOptionalForImport() bool { + return false +} diff --git a/list/schema/float32_attribute_test.go b/list/schema/float32_attribute_test.go new file mode 100644 index 000000000..165a89606 --- /dev/null +++ b/list/schema/float32_attribute_test.go @@ -0,0 +1,470 @@ +// 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/list/schema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func TestFloat32AttributeApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float32Attribute + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + attribute: schema.Float32Attribute{}, + step: tftypes.AttributeName("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.AttributeName to basetypes.Float32Type"), + }, + "ElementKeyInt": { + attribute: schema.Float32Attribute{}, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyInt to basetypes.Float32Type"), + }, + "ElementKeyString": { + attribute: schema.Float32Attribute{}, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyString to basetypes.Float32Type"), + }, + "ElementKeyValue": { + attribute: schema.Float32Attribute{}, + step: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyValue to basetypes.Float32Type"), + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.attribute.ApplyTerraform5AttributePathStep(testCase.step) + + if err != nil { + if testCase.expectedError == nil { + t.Fatalf("expected no error, got: %s", err) + } + + if !strings.Contains(err.Error(), testCase.expectedError.Error()) { + t.Fatalf("expected error %q, got: %s", testCase.expectedError, err) + } + } + + if err == nil && testCase.expectedError != nil { + t.Fatalf("got no error, expected: %s", testCase.expectedError) + } + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat32AttributeFloat32Validators(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float32Attribute + expected []validator.Float32 + }{ + "no-validators": { + attribute: schema.Float32Attribute{}, + expected: nil, + }, + "validators": { + attribute: schema.Float32Attribute{ + Validators: []validator.Float32{}, + }, + expected: []validator.Float32{}, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.Float32Validators() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat32AttributeGetDeprecationMessage(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float32Attribute + expected string + }{ + "no-deprecation-message": { + attribute: schema.Float32Attribute{}, + expected: "", + }, + "deprecation-message": { + attribute: schema.Float32Attribute{ + DeprecationMessage: "test deprecation message", + }, + expected: "test deprecation message", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDeprecationMessage() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat32AttributeEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float32Attribute + other fwschema.Attribute + expected bool + }{ + "different-type": { + attribute: schema.Float32Attribute{}, + other: testschema.AttributeWithFloat32Validators{}, + expected: false, + }, + "equal": { + attribute: schema.Float32Attribute{}, + other: schema.Float32Attribute{}, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.Equal(testCase.other) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat32AttributeGetDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float32Attribute + expected string + }{ + "no-description": { + attribute: schema.Float32Attribute{}, + expected: "", + }, + "description": { + attribute: schema.Float32Attribute{ + Description: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat32AttributeGetMarkdownDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float32Attribute + expected string + }{ + "no-markdown-description": { + attribute: schema.Float32Attribute{}, + expected: "", + }, + "markdown-description": { + attribute: schema.Float32Attribute{ + MarkdownDescription: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetMarkdownDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat32AttributeGetType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float32Attribute + expected attr.Type + }{ + "base": { + attribute: schema.Float32Attribute{}, + expected: types.Float32Type, + }, + "custom-type": { + attribute: schema.Float32Attribute{ + CustomType: testtypes.Float32Type{}, + }, + expected: testtypes.Float32Type{}, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetType() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat32AttributeIsComputed(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float32Attribute + expected bool + }{ + "not-computed": { + attribute: schema.Float32Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsComputed() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat32AttributeIsOptional(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float32Attribute + expected bool + }{ + "not-optional": { + attribute: schema.Float32Attribute{}, + expected: false, + }, + "optional": { + attribute: schema.Float32Attribute{ + Optional: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptional() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat32AttributeIsRequired(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float32Attribute + expected bool + }{ + "not-required": { + attribute: schema.Float32Attribute{}, + expected: false, + }, + "required": { + attribute: schema.Float32Attribute{ + Required: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequired() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat32AttributeIsSensitive(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float32Attribute + expected bool + }{ + "not-sensitive": { + attribute: schema.Float32Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + 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 TestFloat32AttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float32Attribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.Float32Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat32AttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float32Attribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.Float32Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat32AttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float32Attribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.Float32Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/list/schema/float64_attribute.go b/list/schema/float64_attribute.go new file mode 100644 index 000000000..b680a4ce5 --- /dev/null +++ b/list/schema/float64_attribute.go @@ -0,0 +1,194 @@ +// 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 = Float64Attribute{} + _ fwxschema.AttributeWithFloat64Validators = Float64Attribute{} +) + +// Float64Attribute represents a schema attribute that is a 64-bit floating +// point number. When retrieving the value for this attribute, use +// types.Float64 as the value type unless the CustomType field is set. +// +// Use Int64Attribute for 64-bit integer attributes or NumberAttribute for +// 512-bit generic number attributes. +// +// Terraform configurations configure this attribute using expressions that +// return a number or directly via a floating point value. +// +// example_attribute = 123.45 +// +// Terraform configurations reference this attribute using the attribute name. +// +// .example_attribute +type Float64Attribute struct { + // CustomType enables the use of a custom attribute type in place of the + // default basetypes.Float64Type. When retrieving data, the basetypes.Float64Valuable + // associated with this custom type must be used in place of types.Float64. + CustomType basetypes.Float64Typable + + // 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 + + // 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.Float64 +} + +// ApplyTerraform5AttributePathStep always returns an error as it is not +// possible to step further into a Float64Attribute. +func (a Float64Attribute) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { + return a.GetType().ApplyTerraform5AttributePathStep(step) +} + +// Equal returns true if the given Attribute is a Float64Attribute +// and all fields are equal. +func (a Float64Attribute) Equal(o fwschema.Attribute) bool { + if _, ok := o.(Float64Attribute); !ok { + return false + } + + return fwschema.AttributesEqual(a, o) +} + +// Float64Validators returns the Validators field value. +func (a Float64Attribute) Float64Validators() []validator.Float64 { + return a.Validators +} + +// GetDeprecationMessage returns the DeprecationMessage field value. +func (a Float64Attribute) GetDeprecationMessage() string { + return a.DeprecationMessage +} + +// GetDescription returns the Description field value. +func (a Float64Attribute) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription returns the MarkdownDescription field value. +func (a Float64Attribute) GetMarkdownDescription() string { + return a.MarkdownDescription +} + +// GetType returns types.Float64Type or the CustomType field value if defined. +func (a Float64Attribute) GetType() attr.Type { + if a.CustomType != nil { + return a.CustomType + } + + return types.Float64Type +} + +// IsComputed returns false because it does not apply to ListResource schemas. +func (a Float64Attribute) IsComputed() bool { + return false +} + +// IsOptional returns the Optional field value. +func (a Float64Attribute) IsOptional() bool { + return a.Optional +} + +// IsRequired returns the Required field value. +func (a Float64Attribute) IsRequired() bool { + return a.Required +} + +// IsSensitive returns false because it does not apply to ListResource schemas. +func (a Float64Attribute) IsSensitive() bool { + return false +} + +// IsWriteOnly returns false because it does not apply to ListResource schemas. +func (a Float64Attribute) IsWriteOnly() bool { + return false +} + +// IsRequiredForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a Float64Attribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a Float64Attribute) IsOptionalForImport() bool { + return false +} diff --git a/list/schema/float64_attribute_test.go b/list/schema/float64_attribute_test.go new file mode 100644 index 000000000..231f6ba03 --- /dev/null +++ b/list/schema/float64_attribute_test.go @@ -0,0 +1,470 @@ +// 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/list/schema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func TestFloat64AttributeApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float64Attribute + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + attribute: schema.Float64Attribute{}, + step: tftypes.AttributeName("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.AttributeName to basetypes.Float64Type"), + }, + "ElementKeyInt": { + attribute: schema.Float64Attribute{}, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyInt to basetypes.Float64Type"), + }, + "ElementKeyString": { + attribute: schema.Float64Attribute{}, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyString to basetypes.Float64Type"), + }, + "ElementKeyValue": { + attribute: schema.Float64Attribute{}, + step: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyValue to basetypes.Float64Type"), + }, + } + + for name, testCase := range testCases { + 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 TestFloat64AttributeFloat64Validators(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float64Attribute + expected []validator.Float64 + }{ + "no-validators": { + attribute: schema.Float64Attribute{}, + expected: nil, + }, + "validators": { + attribute: schema.Float64Attribute{ + Validators: []validator.Float64{}, + }, + expected: []validator.Float64{}, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.Float64Validators() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat64AttributeGetDeprecationMessage(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float64Attribute + expected string + }{ + "no-deprecation-message": { + attribute: schema.Float64Attribute{}, + expected: "", + }, + "deprecation-message": { + attribute: schema.Float64Attribute{ + DeprecationMessage: "test deprecation message", + }, + expected: "test deprecation message", + }, + } + + for name, testCase := range testCases { + 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 TestFloat64AttributeEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float64Attribute + other fwschema.Attribute + expected bool + }{ + "different-type": { + attribute: schema.Float64Attribute{}, + other: testschema.AttributeWithFloat64Validators{}, + expected: false, + }, + "equal": { + attribute: schema.Float64Attribute{}, + other: schema.Float64Attribute{}, + expected: true, + }, + } + + for name, testCase := range testCases { + 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 TestFloat64AttributeGetDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float64Attribute + expected string + }{ + "no-description": { + attribute: schema.Float64Attribute{}, + expected: "", + }, + "description": { + attribute: schema.Float64Attribute{ + Description: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + 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 TestFloat64AttributeGetMarkdownDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float64Attribute + expected string + }{ + "no-markdown-description": { + attribute: schema.Float64Attribute{}, + expected: "", + }, + "markdown-description": { + attribute: schema.Float64Attribute{ + MarkdownDescription: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + 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 TestFloat64AttributeGetType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float64Attribute + expected attr.Type + }{ + "base": { + attribute: schema.Float64Attribute{}, + expected: types.Float64Type, + }, + "custom-type": { + attribute: schema.Float64Attribute{ + CustomType: testtypes.Float64Type{}, + }, + expected: testtypes.Float64Type{}, + }, + } + + for name, testCase := range testCases { + 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 TestFloat64AttributeIsComputed(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float64Attribute + expected bool + }{ + "not-computed": { + attribute: schema.Float64Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + 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 TestFloat64AttributeIsOptional(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float64Attribute + expected bool + }{ + "not-optional": { + attribute: schema.Float64Attribute{}, + expected: false, + }, + "optional": { + attribute: schema.Float64Attribute{ + Optional: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + 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 TestFloat64AttributeIsRequired(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float64Attribute + expected bool + }{ + "not-required": { + attribute: schema.Float64Attribute{}, + expected: false, + }, + "required": { + attribute: schema.Float64Attribute{ + Required: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + 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 TestFloat64AttributeIsSensitive(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float64Attribute + expected bool + }{ + "not-sensitive": { + attribute: schema.Float64Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + 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 TestFloat54AttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float64Attribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.Float64Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat64AttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float64Attribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.Float64Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat64AttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float64Attribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.Float64Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/list/schema/int32_attribute.go b/list/schema/int32_attribute.go new file mode 100644 index 000000000..ea1bbfede --- /dev/null +++ b/list/schema/int32_attribute.go @@ -0,0 +1,194 @@ +// 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 + + // 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 false because it does not apply to ListResource schemas. +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 false because it does not apply to ListResource schemas. +func (a Int32Attribute) IsSensitive() bool { + return false +} + +// IsWriteOnly returns false because it does not apply to ListResource schemas. +func (a Int32Attribute) IsWriteOnly() bool { + return false +} + +// IsRequiredForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a Int32Attribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a Int32Attribute) IsOptionalForImport() bool { + return false +} diff --git a/list/schema/int32_attribute_test.go b/list/schema/int32_attribute_test.go new file mode 100644 index 000000000..0c3603332 --- /dev/null +++ b/list/schema/int32_attribute_test.go @@ -0,0 +1,470 @@ +// 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/list/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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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, + }, + } + + for name, testCase := range testCases { + 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 TestInt2AttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int32Attribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.Int32Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt32AttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int32Attribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.Int32Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt32AttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int32Attribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.Int32Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/list/schema/int64_attribute.go b/list/schema/int64_attribute.go new file mode 100644 index 000000000..fc93b78a9 --- /dev/null +++ b/list/schema/int64_attribute.go @@ -0,0 +1,194 @@ +// 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 = Int64Attribute{} + _ fwxschema.AttributeWithInt64Validators = Int64Attribute{} +) + +// Int64Attribute represents a schema attribute that is a 64-bit integer. +// When retrieving the value for this attribute, use types.Int64 as the value +// type unless the CustomType field is set. +// +// Use Float64Attribute for 64-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 Int64Attribute struct { + // CustomType enables the use of a custom attribute type in place of the + // default basetypes.Int64Type. When retrieving data, the basetypes.Int64Valuable + // associated with this custom type must be used in place of types.Int64. + CustomType basetypes.Int64Typable + + // 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 + + // 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.Int64 +} + +// ApplyTerraform5AttributePathStep always returns an error as it is not +// possible to step further into a Int64Attribute. +func (a Int64Attribute) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { + return a.GetType().ApplyTerraform5AttributePathStep(step) +} + +// Equal returns true if the given Attribute is a Int64Attribute +// and all fields are equal. +func (a Int64Attribute) Equal(o fwschema.Attribute) bool { + if _, ok := o.(Int64Attribute); !ok { + return false + } + + return fwschema.AttributesEqual(a, o) +} + +// GetDeprecationMessage returns the DeprecationMessage field value. +func (a Int64Attribute) GetDeprecationMessage() string { + return a.DeprecationMessage +} + +// GetDescription returns the Description field value. +func (a Int64Attribute) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription returns the MarkdownDescription field value. +func (a Int64Attribute) GetMarkdownDescription() string { + return a.MarkdownDescription +} + +// GetType returns types.Int64Type or the CustomType field value if defined. +func (a Int64Attribute) GetType() attr.Type { + if a.CustomType != nil { + return a.CustomType + } + + return types.Int64Type +} + +// Int64Validators returns the Validators field value. +func (a Int64Attribute) Int64Validators() []validator.Int64 { + return a.Validators +} + +// IsComputed returns false because it does not apply to ListResource schemas. +func (a Int64Attribute) IsComputed() bool { + return false +} + +// IsOptional returns the Optional field value. +func (a Int64Attribute) IsOptional() bool { + return a.Optional +} + +// IsRequired returns the Required field value. +func (a Int64Attribute) IsRequired() bool { + return a.Required +} + +// IsSensitive returns false because it does not apply to ephemeral resource schemas. +func (a Int64Attribute) IsSensitive() bool { + return false +} + +// IsWriteOnly returns false because it does not apply to ListResource schemas. +func (a Int64Attribute) IsWriteOnly() bool { + return false +} + +// IsRequiredForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a Int64Attribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a Int64Attribute) IsOptionalForImport() bool { + return false +} diff --git a/list/schema/int64_attribute_test.go b/list/schema/int64_attribute_test.go new file mode 100644 index 000000000..d772580e8 --- /dev/null +++ b/list/schema/int64_attribute_test.go @@ -0,0 +1,470 @@ +// 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/list/schema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func TestInt64AttributeApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int64Attribute + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + attribute: schema.Int64Attribute{}, + step: tftypes.AttributeName("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.AttributeName to basetypes.Int64Type"), + }, + "ElementKeyInt": { + attribute: schema.Int64Attribute{}, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyInt to basetypes.Int64Type"), + }, + "ElementKeyString": { + attribute: schema.Int64Attribute{}, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyString to basetypes.Int64Type"), + }, + "ElementKeyValue": { + attribute: schema.Int64Attribute{}, + step: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyValue to basetypes.Int64Type"), + }, + } + + for name, testCase := range testCases { + 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 TestInt64AttributeGetDeprecationMessage(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int64Attribute + expected string + }{ + "no-deprecation-message": { + attribute: schema.Int64Attribute{}, + expected: "", + }, + "deprecation-message": { + attribute: schema.Int64Attribute{ + DeprecationMessage: "test deprecation message", + }, + expected: "test deprecation message", + }, + } + + for name, testCase := range testCases { + 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 TestInt64AttributeEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int64Attribute + other fwschema.Attribute + expected bool + }{ + "different-type": { + attribute: schema.Int64Attribute{}, + other: testschema.AttributeWithInt64Validators{}, + expected: false, + }, + "equal": { + attribute: schema.Int64Attribute{}, + other: schema.Int64Attribute{}, + expected: true, + }, + } + + for name, testCase := range testCases { + 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 TestInt64AttributeGetDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int64Attribute + expected string + }{ + "no-description": { + attribute: schema.Int64Attribute{}, + expected: "", + }, + "description": { + attribute: schema.Int64Attribute{ + Description: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + 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 TestInt64AttributeGetMarkdownDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int64Attribute + expected string + }{ + "no-markdown-description": { + attribute: schema.Int64Attribute{}, + expected: "", + }, + "markdown-description": { + attribute: schema.Int64Attribute{ + MarkdownDescription: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + 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 TestInt64AttributeGetType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int64Attribute + expected attr.Type + }{ + "base": { + attribute: schema.Int64Attribute{}, + expected: types.Int64Type, + }, + "custom-type": { + attribute: schema.Int64Attribute{ + CustomType: testtypes.Int64Type{}, + }, + expected: testtypes.Int64Type{}, + }, + } + + for name, testCase := range testCases { + 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 TestInt64AttributeInt64Validators(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int64Attribute + expected []validator.Int64 + }{ + "no-validators": { + attribute: schema.Int64Attribute{}, + expected: nil, + }, + "validators": { + attribute: schema.Int64Attribute{ + Validators: []validator.Int64{}, + }, + expected: []validator.Int64{}, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.Int64Validators() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt64AttributeIsComputed(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int64Attribute + expected bool + }{ + "not-computed": { + attribute: schema.Int64Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + 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 TestInt64AttributeIsOptional(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int64Attribute + expected bool + }{ + "not-optional": { + attribute: schema.Int64Attribute{}, + expected: false, + }, + "optional": { + attribute: schema.Int64Attribute{ + Optional: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + 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 TestInt64AttributeIsRequired(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int64Attribute + expected bool + }{ + "not-required": { + attribute: schema.Int64Attribute{}, + expected: false, + }, + "required": { + attribute: schema.Int64Attribute{ + Required: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + 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 TestInt64AttributeIsSensitive(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int64Attribute + expected bool + }{ + "not-sensitive": { + attribute: schema.Int64Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + 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 TestInt64AttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int64Attribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.Int64Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt64AttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int64Attribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.Int64Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt64AttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int64Attribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.Int64Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/list/schema/list_attribute.go b/list/schema/list_attribute.go new file mode 100644 index 000000000..4043641be --- /dev/null +++ b/list/schema/list_attribute.go @@ -0,0 +1,226 @@ +// 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/internal/fwtype" + "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 = ListAttribute{} + _ fwschema.AttributeWithValidateImplementation = ListAttribute{} + _ fwxschema.AttributeWithListValidators = ListAttribute{} +) + +// ListAttribute represents a schema attribute that is a list with a single +// element type. When retrieving the value for this attribute, use types.List +// as the value type unless the CustomType field is set. The ElementType field +// must be set. +// +// Use ListNestedAttribute if the underlying elements should be objects and +// require definition beyond type information. +// +// Terraform configurations configure this attribute using expressions that +// return a list or directly via square brace syntax. +// +// # list of strings +// example_attribute = ["first", "second"] +// +// Terraform configurations reference this attribute using expressions that +// accept a list or an element directly via square brace 0-based index syntax: +// +// # first known element +// .example_attribute[0] +type ListAttribute struct { + // ElementType is the type for all elements of the list. This field must be + // set. + // + // Element types that contain a dynamic type (i.e. types.Dynamic) are not supported. + // If underlying dynamic values are required, replace this attribute definition with + // DynamicAttribute instead. + ElementType attr.Type + + // CustomType enables the use of a custom attribute type in place of the + // default basetypes.ListType. When retrieving data, the basetypes.ListValuable + // associated with this custom type must be used in place of types.List. + CustomType basetypes.ListTypable + + // 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 + + // 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.List +} + +// ApplyTerraform5AttributePathStep returns the result of stepping into a list +// index or an error. +func (a ListAttribute) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { + return a.GetType().ApplyTerraform5AttributePathStep(step) +} + +// Equal returns true if the given Attribute is a ListAttribute +// and all fields are equal. +func (a ListAttribute) Equal(o fwschema.Attribute) bool { + if _, ok := o.(ListAttribute); !ok { + return false + } + + return fwschema.AttributesEqual(a, o) +} + +// GetDeprecationMessage returns the DeprecationMessage field value. +func (a ListAttribute) GetDeprecationMessage() string { + return a.DeprecationMessage +} + +// GetDescription returns the Description field value. +func (a ListAttribute) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription returns the MarkdownDescription field value. +func (a ListAttribute) GetMarkdownDescription() string { + return a.MarkdownDescription +} + +// GetType returns types.ListType or the CustomType field value if defined. +func (a ListAttribute) GetType() attr.Type { + if a.CustomType != nil { + return a.CustomType + } + + return types.ListType{ + ElemType: a.ElementType, + } +} + +// IsComputed returns false because it does not apply to ListResource schemas. +func (a ListAttribute) IsComputed() bool { + return false +} + +// IsOptional returns the Optional field value. +func (a ListAttribute) IsOptional() bool { + return a.Optional +} + +// IsRequired returns the Required field value. +func (a ListAttribute) IsRequired() bool { + return a.Required +} + +// IsSensitive returns false because it does not apply to ListResource schemas. +func (a ListAttribute) IsSensitive() bool { + return false +} + +// IsWriteOnly returns false because it does not apply to ListResource schemas. +func (a ListAttribute) IsWriteOnly() bool { + return false +} + +// IsRequiredForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a ListAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a ListAttribute) IsOptionalForImport() bool { + return false +} + +// ListValidators returns the Validators field value. +func (a ListAttribute) ListValidators() []validator.List { + return a.Validators +} + +// 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 ListAttribute) ValidateImplementation(ctx context.Context, req fwschema.ValidateImplementationRequest, resp *fwschema.ValidateImplementationResponse) { + if a.CustomType == nil && a.ElementType == nil { + resp.Diagnostics.Append(fwschema.AttributeMissingElementTypeDiag(req.Path)) + } + + if a.CustomType == nil && fwtype.ContainsCollectionWithDynamic(a.GetType()) { + resp.Diagnostics.Append(fwtype.AttributeCollectionWithDynamicTypeDiag(req.Path)) + } +} diff --git a/list/schema/list_attribute_test.go b/list/schema/list_attribute_test.go new file mode 100644 index 000000000..6346f7b33 --- /dev/null +++ b/list/schema/list_attribute_test.go @@ -0,0 +1,560 @@ +// 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-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/list/schema" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestListAttributeApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListAttribute + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + attribute: schema.ListAttribute{ElementType: types.StringType}, + step: tftypes.AttributeName("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.AttributeName to ListType"), + }, + "ElementKeyInt": { + attribute: schema.ListAttribute{ElementType: types.StringType}, + step: tftypes.ElementKeyInt(1), + expected: types.StringType, + expectedError: nil, + }, + "ElementKeyString": { + attribute: schema.ListAttribute{ElementType: types.StringType}, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyString to ListType"), + }, + "ElementKeyValue": { + attribute: schema.ListAttribute{ElementType: types.StringType}, + step: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyValue to ListType"), + }, + } + + for name, testCase := range testCases { + 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 TestListAttributeGetDeprecationMessage(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListAttribute + expected string + }{ + "no-deprecation-message": { + attribute: schema.ListAttribute{ElementType: types.StringType}, + expected: "", + }, + "deprecation-message": { + attribute: schema.ListAttribute{ + DeprecationMessage: "test deprecation message", + }, + expected: "test deprecation message", + }, + } + + for name, testCase := range testCases { + 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 TestListAttributeEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListAttribute + other fwschema.Attribute + expected bool + }{ + "different-type": { + attribute: schema.ListAttribute{ElementType: types.StringType}, + other: testschema.AttributeWithListValidators{}, + expected: false, + }, + "different-element-type": { + attribute: schema.ListAttribute{ElementType: types.StringType}, + other: schema.ListAttribute{ElementType: types.BoolType}, + expected: false, + }, + "equal": { + attribute: schema.ListAttribute{ElementType: types.StringType}, + other: schema.ListAttribute{ElementType: types.StringType}, + expected: true, + }, + } + + for name, testCase := range testCases { + 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 TestListAttributeGetDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListAttribute + expected string + }{ + "no-description": { + attribute: schema.ListAttribute{ElementType: types.StringType}, + expected: "", + }, + "description": { + attribute: schema.ListAttribute{ + Description: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + 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 TestListAttributeGetMarkdownDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListAttribute + expected string + }{ + "no-markdown-description": { + attribute: schema.ListAttribute{ElementType: types.StringType}, + expected: "", + }, + "markdown-description": { + attribute: schema.ListAttribute{ + MarkdownDescription: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + 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 TestListAttributeGetType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListAttribute + expected attr.Type + }{ + "base": { + attribute: schema.ListAttribute{ElementType: types.StringType}, + expected: types.ListType{ElemType: types.StringType}, + }, + // "custom-type": { + // attribute: schema.ListAttribute{ + // CustomType: testtypes.ListType{}, + // }, + // expected: testtypes.ListType{}, + // }, + } + + for name, testCase := range testCases { + 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 TestListAttributeIsComputed(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListAttribute + expected bool + }{ + "not-computed": { + attribute: schema.ListAttribute{ElementType: types.StringType}, + expected: false, + }, + } + + for name, testCase := range testCases { + 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 TestListAttributeIsOptional(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListAttribute + expected bool + }{ + "not-optional": { + attribute: schema.ListAttribute{ElementType: types.StringType}, + expected: false, + }, + "optional": { + attribute: schema.ListAttribute{ + Optional: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + 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 TestListAttributeIsRequired(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListAttribute + expected bool + }{ + "not-required": { + attribute: schema.ListAttribute{ElementType: types.StringType}, + expected: false, + }, + "required": { + attribute: schema.ListAttribute{ + Required: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + 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 TestListAttributeIsSensitive(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListAttribute + expected bool + }{ + "not-sensitive": { + attribute: schema.ListAttribute{ElementType: types.StringType}, + expected: false, + }, + } + + for name, testCase := range testCases { + 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 TestListAttributeListValidators(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListAttribute + expected []validator.List + }{ + "no-validators": { + attribute: schema.ListAttribute{ElementType: types.StringType}, + expected: nil, + }, + "validators": { + attribute: schema.ListAttribute{ + Validators: []validator.List{}, + }, + expected: []validator.List{}, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.ListValidators() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListAttributeValidateImplementation(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListAttribute + request fwschema.ValidateImplementationRequest + expected *fwschema.ValidateImplementationResponse + }{ + "customtype": { + attribute: schema.ListAttribute{ + CustomType: testtypes.ListType{}, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{}, + }, + "elementtype": { + attribute: schema.ListAttribute{ + ElementType: types.StringType, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{}, + }, + "elementtype-dynamic": { + attribute: schema.ListAttribute{ + ElementType: types.DynamicType, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid Schema Implementation", + "When validating the schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "\"test\" is an attribute that contains a collection type with a nested dynamic type.\n\n"+ + "Dynamic types inside of collections are not currently supported in terraform-plugin-framework. "+ + "If underlying dynamic values are required, replace the \"test\" attribute definition with DynamicAttribute instead.", + ), + }, + }, + }, + "elementtype-missing": { + attribute: schema.ListAttribute{}, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid Attribute Implementation", + "When validating the schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "\"test\" is missing the CustomType or ElementType field on a collection Attribute. "+ + "One of these fields is required to prevent other unexpected errors or panics.", + ), + }, + }, + }, + } + + for name, testCase := range testCases { + 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) + } + }) + } +} + +func TestListAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.ListAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.ListAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.ListAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/list/schema/map_attribute.go b/list/schema/map_attribute.go new file mode 100644 index 000000000..8be70093b --- /dev/null +++ b/list/schema/map_attribute.go @@ -0,0 +1,229 @@ +// 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/internal/fwtype" + "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 = MapAttribute{} + _ fwschema.AttributeWithValidateImplementation = MapAttribute{} + _ fwxschema.AttributeWithMapValidators = MapAttribute{} +) + +// MapAttribute represents a schema attribute that is a map with a single +// element type. When retrieving the value for this attribute, use types.Map +// as the value type unless the CustomType field is set. The ElementType field +// must be set. +// +// Use MapNestedAttribute if the underlying elements should be objects and +// require definition beyond type information. +// +// Terraform configurations configure this attribute using expressions that +// return a map or directly via curly brace syntax. +// +// # map of strings +// example_attribute = { +// key1 = "first", +// key2 = "second", +// } +// +// Terraform configurations reference this attribute using expressions that +// accept a map or an element directly via square brace string syntax: +// +// # key1 known element +// .example_attribute["key1"] +type MapAttribute struct { + // ElementType is the type for all elements of the map. This field must be + // set. + // + // Element types that contain a dynamic type (i.e. types.Dynamic) are not supported. + // If underlying dynamic values are required, replace this attribute definition with + // DynamicAttribute instead. + ElementType attr.Type + + // CustomType enables the use of a custom attribute type in place of the + // default basetypes.MapType. When retrieving data, the basetypes.MapValuable + // associated with this custom type must be used in place of types.Map. + CustomType basetypes.MapTypable + + // 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 + + // 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.Map +} + +// ApplyTerraform5AttributePathStep returns the result of stepping into a map +// index or an error. +func (a MapAttribute) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { + return a.GetType().ApplyTerraform5AttributePathStep(step) +} + +// Equal returns true if the given Attribute is a MapAttribute +// and all fields are equal. +func (a MapAttribute) Equal(o fwschema.Attribute) bool { + if _, ok := o.(MapAttribute); !ok { + return false + } + + return fwschema.AttributesEqual(a, o) +} + +// GetDeprecationMessage returns the DeprecationMessage field value. +func (a MapAttribute) GetDeprecationMessage() string { + return a.DeprecationMessage +} + +// GetDescription returns the Description field value. +func (a MapAttribute) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription returns the MarkdownDescription field value. +func (a MapAttribute) GetMarkdownDescription() string { + return a.MarkdownDescription +} + +// GetType returns types.MapType or the CustomType field value if defined. +func (a MapAttribute) GetType() attr.Type { + if a.CustomType != nil { + return a.CustomType + } + + return types.MapType{ + ElemType: a.ElementType, + } +} + +// IsComputed returns false because it does not apply to ListResource schemas. +func (a MapAttribute) IsComputed() bool { + return false +} + +// IsOptional returns the Optional field value. +func (a MapAttribute) IsOptional() bool { + return a.Optional +} + +// IsRequired returns the Required field value. +func (a MapAttribute) IsRequired() bool { + return a.Required +} + +// IsSensitive returns false because it does not apply to ListResource schemas. +func (a MapAttribute) IsSensitive() bool { + return false +} + +// IsWriteOnly returns false because it does not apply to ListResource schemas. +func (a MapAttribute) IsWriteOnly() bool { + return false +} + +// IsRequiredForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a MapAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a MapAttribute) IsOptionalForImport() bool { + return false +} + +// MapValidators returns the Validators field value. +func (a MapAttribute) MapValidators() []validator.Map { + return a.Validators +} + +// 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 MapAttribute) ValidateImplementation(ctx context.Context, req fwschema.ValidateImplementationRequest, resp *fwschema.ValidateImplementationResponse) { + if a.CustomType == nil && a.ElementType == nil { + resp.Diagnostics.Append(fwschema.AttributeMissingElementTypeDiag(req.Path)) + } + + if a.CustomType == nil && fwtype.ContainsCollectionWithDynamic(a.GetType()) { + resp.Diagnostics.Append(fwtype.AttributeCollectionWithDynamicTypeDiag(req.Path)) + } +} diff --git a/list/schema/map_attribute_test.go b/list/schema/map_attribute_test.go new file mode 100644 index 000000000..d74324d8c --- /dev/null +++ b/list/schema/map_attribute_test.go @@ -0,0 +1,560 @@ +// 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-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/list/schema" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestMapAttributeApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapAttribute + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + attribute: schema.MapAttribute{ElementType: types.StringType}, + step: tftypes.AttributeName("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.AttributeName to MapType"), + }, + "ElementKeyInt": { + attribute: schema.MapAttribute{ElementType: types.StringType}, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyInt to MapType"), + }, + "ElementKeyString": { + attribute: schema.MapAttribute{ElementType: types.StringType}, + step: tftypes.ElementKeyString("test"), + expected: types.StringType, + expectedError: nil, + }, + "ElementKeyValue": { + attribute: schema.MapAttribute{ElementType: types.StringType}, + step: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyValue to MapType"), + }, + } + + for name, testCase := range testCases { + 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 TestMapAttributeGetDeprecationMessage(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapAttribute + expected string + }{ + "no-deprecation-message": { + attribute: schema.MapAttribute{ElementType: types.StringType}, + expected: "", + }, + "deprecation-message": { + attribute: schema.MapAttribute{ + DeprecationMessage: "test deprecation message", + }, + expected: "test deprecation message", + }, + } + + for name, testCase := range testCases { + 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 TestMapAttributeEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapAttribute + other fwschema.Attribute + expected bool + }{ + "different-type": { + attribute: schema.MapAttribute{ElementType: types.StringType}, + other: testschema.AttributeWithMapValidators{}, + expected: false, + }, + "different-element-type": { + attribute: schema.MapAttribute{ElementType: types.StringType}, + other: schema.MapAttribute{ElementType: types.BoolType}, + expected: false, + }, + "equal": { + attribute: schema.MapAttribute{ElementType: types.StringType}, + other: schema.MapAttribute{ElementType: types.StringType}, + expected: true, + }, + } + + for name, testCase := range testCases { + 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 TestMapAttributeGetDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapAttribute + expected string + }{ + "no-description": { + attribute: schema.MapAttribute{ElementType: types.StringType}, + expected: "", + }, + "description": { + attribute: schema.MapAttribute{ + Description: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + 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 TestMapAttributeGetMarkdownDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapAttribute + expected string + }{ + "no-markdown-description": { + attribute: schema.MapAttribute{ElementType: types.StringType}, + expected: "", + }, + "markdown-description": { + attribute: schema.MapAttribute{ + MarkdownDescription: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + 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 TestMapAttributeGetType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapAttribute + expected attr.Type + }{ + "base": { + attribute: schema.MapAttribute{ElementType: types.StringType}, + expected: types.MapType{ElemType: types.StringType}, + }, + // "custom-type": { + // attribute: schema.MapAttribute{ + // CustomType: testtypes.MapType{}, + // }, + // expected: testtypes.MapType{}, + // }, + } + + for name, testCase := range testCases { + 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 TestMapAttributeIsComputed(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapAttribute + expected bool + }{ + "not-computed": { + attribute: schema.MapAttribute{ElementType: types.StringType}, + expected: false, + }, + } + + for name, testCase := range testCases { + 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 TestMapAttributeIsOptional(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapAttribute + expected bool + }{ + "not-optional": { + attribute: schema.MapAttribute{ElementType: types.StringType}, + expected: false, + }, + "optional": { + attribute: schema.MapAttribute{ + Optional: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + 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 TestMapAttributeIsRequired(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapAttribute + expected bool + }{ + "not-required": { + attribute: schema.MapAttribute{ElementType: types.StringType}, + expected: false, + }, + "required": { + attribute: schema.MapAttribute{ + Required: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + 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 TestMapAttributeIsSensitive(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapAttribute + expected bool + }{ + "not-sensitive": { + attribute: schema.MapAttribute{ElementType: types.StringType}, + expected: false, + }, + } + + for name, testCase := range testCases { + 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 TestMapAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.MapAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestMapAttributeMapValidators(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapAttribute + expected []validator.Map + }{ + "no-validators": { + attribute: schema.MapAttribute{ElementType: types.StringType}, + expected: nil, + }, + "validators": { + attribute: schema.MapAttribute{ + Validators: []validator.Map{}, + }, + expected: []validator.Map{}, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.MapValidators() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestMapAttributeValidateImplementation(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapAttribute + request fwschema.ValidateImplementationRequest + expected *fwschema.ValidateImplementationResponse + }{ + "customtype": { + attribute: schema.MapAttribute{ + CustomType: testtypes.MapType{}, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{}, + }, + "elementtype": { + attribute: schema.MapAttribute{ + ElementType: types.StringType, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{}, + }, + "elementtype-dynamic": { + attribute: schema.MapAttribute{ + ElementType: types.DynamicType, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid Schema Implementation", + "When validating the schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "\"test\" is an attribute that contains a collection type with a nested dynamic type.\n\n"+ + "Dynamic types inside of collections are not currently supported in terraform-plugin-framework. "+ + "If underlying dynamic values are required, replace the \"test\" attribute definition with DynamicAttribute instead.", + ), + }, + }, + }, + "elementtype-missing": { + attribute: schema.MapAttribute{}, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid Attribute Implementation", + "When validating the schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "\"test\" is missing the CustomType or ElementType field on a collection Attribute. "+ + "One of these fields is required to prevent other unexpected errors or panics.", + ), + }, + }, + }, + } + + for name, testCase := range testCases { + 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) + } + }) + } +} + +func TestMapAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.MapAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestMapAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.MapAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} From 72794873160e8cd9b6c6ea1886460509c3d33626 Mon Sep 17 00:00:00 2001 From: Steph Date: Fri, 4 Jul 2025 14:06:33 +0200 Subject: [PATCH 2/4] sort the listresources result to prevent false positives in test --- internal/proto5server/server_getmetadata_test.go | 4 ++++ internal/proto6server/server_getmetadata_test.go | 4 ++++ .../testprovider/{list_resource.go => listresource.go} | 0 3 files changed, 8 insertions(+) rename internal/testing/testprovider/{list_resource.go => listresource.go} (100%) diff --git a/internal/proto5server/server_getmetadata_test.go b/internal/proto5server/server_getmetadata_test.go index d1505e0e4..01e0dc43c 100644 --- a/internal/proto5server/server_getmetadata_test.go +++ b/internal/proto5server/server_getmetadata_test.go @@ -810,6 +810,10 @@ func TestServerGetMetadata(t *testing.T) { return got.Functions[i].Name < got.Functions[j].Name }) + sort.Slice(got.ListResources, func(i int, j int) bool { + return got.ListResources[i].TypeName < got.ListResources[j].TypeName + }) + sort.Slice(got.Resources, func(i int, j int) bool { return got.Resources[i].TypeName < got.Resources[j].TypeName }) diff --git a/internal/proto6server/server_getmetadata_test.go b/internal/proto6server/server_getmetadata_test.go index e103314fc..36c0f39d0 100644 --- a/internal/proto6server/server_getmetadata_test.go +++ b/internal/proto6server/server_getmetadata_test.go @@ -810,6 +810,10 @@ func TestServerGetMetadata(t *testing.T) { return got.Functions[i].Name < got.Functions[j].Name }) + sort.Slice(got.ListResources, func(i int, j int) bool { + return got.ListResources[i].TypeName < got.ListResources[j].TypeName + }) + sort.Slice(got.Resources, func(i int, j int) bool { return got.Resources[i].TypeName < got.Resources[j].TypeName }) diff --git a/internal/testing/testprovider/list_resource.go b/internal/testing/testprovider/listresource.go similarity index 100% rename from internal/testing/testprovider/list_resource.go rename to internal/testing/testprovider/listresource.go From e64d1a7d215d28497f0e45559a92e491224eb293 Mon Sep 17 00:00:00 2001 From: Steph Date: Tue, 8 Jul 2025 10:10:16 +0200 Subject: [PATCH 3/4] omit mention of Computed in the code comment for the Required attribute --- list/schema/bool_attribute.go | 3 +-- list/schema/float32_attribute.go | 3 +-- list/schema/float64_attribute.go | 3 +-- list/schema/int32_attribute.go | 3 +-- list/schema/int64_attribute.go | 3 +-- list/schema/list_attribute.go | 3 +-- list/schema/map_attribute.go | 3 +-- 7 files changed, 7 insertions(+), 14 deletions(-) diff --git a/list/schema/bool_attribute.go b/list/schema/bool_attribute.go index 87a3d3a0e..0d2e58bf0 100644 --- a/list/schema/bool_attribute.go +++ b/list/schema/bool_attribute.go @@ -39,8 +39,7 @@ type BoolAttribute struct { CustomType basetypes.BoolTypable // 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. + // this attribute or not. Required and Optional cannot both be true. Required bool // Optional indicates whether the practitioner can choose to enter a value diff --git a/list/schema/float32_attribute.go b/list/schema/float32_attribute.go index 4adbbabdd..68c6e5c1e 100644 --- a/list/schema/float32_attribute.go +++ b/list/schema/float32_attribute.go @@ -42,8 +42,7 @@ type Float32Attribute struct { CustomType basetypes.Float32Typable // Required indicates whether the practitioner must enter a value for - // this attribute or not. Required and Optional cannot both be true, - // and Required and Computed cannot both be true. + // this attribute or not. Required and Optional cannot both be true. Required bool // Optional indicates whether the practitioner can choose to enter a value diff --git a/list/schema/float64_attribute.go b/list/schema/float64_attribute.go index b680a4ce5..115c6fb27 100644 --- a/list/schema/float64_attribute.go +++ b/list/schema/float64_attribute.go @@ -42,8 +42,7 @@ type Float64Attribute struct { CustomType basetypes.Float64Typable // 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. + // this attribute or not. Required and Optional cannot both be true. Required bool // Optional indicates whether the practitioner can choose to enter a value diff --git a/list/schema/int32_attribute.go b/list/schema/int32_attribute.go index ea1bbfede..2a9fb49a7 100644 --- a/list/schema/int32_attribute.go +++ b/list/schema/int32_attribute.go @@ -42,8 +42,7 @@ type Int32Attribute struct { 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. + // this attribute or not. Required and Optional cannot both be true. Required bool // Optional indicates whether the practitioner can choose to enter a value diff --git a/list/schema/int64_attribute.go b/list/schema/int64_attribute.go index fc93b78a9..5e124f0cf 100644 --- a/list/schema/int64_attribute.go +++ b/list/schema/int64_attribute.go @@ -42,8 +42,7 @@ type Int64Attribute struct { CustomType basetypes.Int64Typable // 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. + // this attribute or not. Required and Optional cannot both be true. Required bool // Optional indicates whether the practitioner can choose to enter a value diff --git a/list/schema/list_attribute.go b/list/schema/list_attribute.go index 4043641be..3beabde9a 100644 --- a/list/schema/list_attribute.go +++ b/list/schema/list_attribute.go @@ -58,8 +58,7 @@ type ListAttribute struct { CustomType basetypes.ListTypable // 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. + // this attribute or not. Required and Optional cannot both be true. Required bool // Optional indicates whether the practitioner can choose to enter a value diff --git a/list/schema/map_attribute.go b/list/schema/map_attribute.go index 8be70093b..efa494513 100644 --- a/list/schema/map_attribute.go +++ b/list/schema/map_attribute.go @@ -61,8 +61,7 @@ type MapAttribute struct { CustomType basetypes.MapTypable // 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. + // this attribute or not. Required and Optional cannot both be true. Required bool // Optional indicates whether the practitioner can choose to enter a value From 50ce0a52257a381ff877ecbb7f4803f530707160 Mon Sep 17 00:00:00 2001 From: Steph Date: Tue, 8 Jul 2025 11:48:52 +0200 Subject: [PATCH 4/4] add number type --- list/schema/attribute.go | 1 + list/schema/number_attribute.go | 194 +++++++++++ list/schema/number_attribute_test.go | 470 +++++++++++++++++++++++++++ 3 files changed, 665 insertions(+) create mode 100644 list/schema/number_attribute.go create mode 100644 list/schema/number_attribute_test.go diff --git a/list/schema/attribute.go b/list/schema/attribute.go index 567903bb8..bbced8302 100644 --- a/list/schema/attribute.go +++ b/list/schema/attribute.go @@ -16,6 +16,7 @@ import ( // - Int64Attribute // - ListAttribute // - MapAttribute +// - NumberAttribute // - StringAttribute // // In practitioner configurations, an equals sign (=) is required to set diff --git a/list/schema/number_attribute.go b/list/schema/number_attribute.go new file mode 100644 index 000000000..c3f053b29 --- /dev/null +++ b/list/schema/number_attribute.go @@ -0,0 +1,194 @@ +// 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 = NumberAttribute{} + _ fwxschema.AttributeWithNumberValidators = NumberAttribute{} +) + +// NumberAttribute represents a schema attribute that is a generic number with +// up to 512 bits of floating point or integer precision. When retrieving the +// value for this attribute, use types.Number as the value type unless the +// CustomType field is set. +// +// Use Float64Attribute for 64-bit floating point number attributes or +// Int64Attribute for 64-bit integer number attributes. +// +// Terraform configurations configure this attribute using expressions that +// return a number or directly via a floating point or integer value. +// +// example_attribute = 123 +// +// Terraform configurations reference this attribute using the attribute name. +// +// .example_attribute +type NumberAttribute struct { + // CustomType enables the use of a custom attribute type in place of the + // default basetypes.NumberType. When retrieving data, the basetypes.NumberValuable + // associated with this custom type must be used in place of types.Number. + CustomType basetypes.NumberTypable + + // Required indicates whether the practitioner must enter a value for + // this attribute or not. Required and Optional 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 + + // 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.Number +} + +// ApplyTerraform5AttributePathStep always returns an error as it is not +// possible to step further into a NumberAttribute. +func (a NumberAttribute) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { + return a.GetType().ApplyTerraform5AttributePathStep(step) +} + +// Equal returns true if the given Attribute is a NumberAttribute +// and all fields are equal. +func (a NumberAttribute) Equal(o fwschema.Attribute) bool { + if _, ok := o.(NumberAttribute); !ok { + return false + } + + return fwschema.AttributesEqual(a, o) +} + +// GetDeprecationMessage returns the DeprecationMessage field value. +func (a NumberAttribute) GetDeprecationMessage() string { + return a.DeprecationMessage +} + +// GetDescription returns the Description field value. +func (a NumberAttribute) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription returns the MarkdownDescription field value. +func (a NumberAttribute) GetMarkdownDescription() string { + return a.MarkdownDescription +} + +// GetType returns types.NumberType or the CustomType field value if defined. +func (a NumberAttribute) GetType() attr.Type { + if a.CustomType != nil { + return a.CustomType + } + + return types.NumberType +} + +// IsComputed returns false because it does not apply to ListResource schemas. +func (a NumberAttribute) IsComputed() bool { + return false +} + +// IsOptional returns the Optional field value. +func (a NumberAttribute) IsOptional() bool { + return a.Optional +} + +// IsRequired returns the Required field value. +func (a NumberAttribute) IsRequired() bool { + return a.Required +} + +// IsSensitive returns false because it does not apply to ListResource schemas. +func (a NumberAttribute) IsSensitive() bool { + return false +} + +// IsWriteOnly returns false because it does not apply to ListResource schemas. +func (a NumberAttribute) IsWriteOnly() bool { + return false +} + +// IsRequiredForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a NumberAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a NumberAttribute) IsOptionalForImport() bool { + return false +} + +// NumberValidators returns the Validators field value. +func (a NumberAttribute) NumberValidators() []validator.Number { + return a.Validators +} diff --git a/list/schema/number_attribute_test.go b/list/schema/number_attribute_test.go new file mode 100644 index 000000000..dbeee3ebb --- /dev/null +++ b/list/schema/number_attribute_test.go @@ -0,0 +1,470 @@ +// 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/list/schema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func TestNumberAttributeApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.NumberAttribute + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + attribute: schema.NumberAttribute{}, + step: tftypes.AttributeName("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.AttributeName to basetypes.NumberType"), + }, + "ElementKeyInt": { + attribute: schema.NumberAttribute{}, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyInt to basetypes.NumberType"), + }, + "ElementKeyString": { + attribute: schema.NumberAttribute{}, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyString to basetypes.NumberType"), + }, + "ElementKeyValue": { + attribute: schema.NumberAttribute{}, + step: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyValue to basetypes.NumberType"), + }, + } + + for name, testCase := range testCases { + 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 TestNumberAttributeGetDeprecationMessage(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.NumberAttribute + expected string + }{ + "no-deprecation-message": { + attribute: schema.NumberAttribute{}, + expected: "", + }, + "deprecation-message": { + attribute: schema.NumberAttribute{ + DeprecationMessage: "test deprecation message", + }, + expected: "test deprecation message", + }, + } + + for name, testCase := range testCases { + 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 TestNumberAttributeEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.NumberAttribute + other fwschema.Attribute + expected bool + }{ + "different-type": { + attribute: schema.NumberAttribute{}, + other: testschema.AttributeWithNumberValidators{}, + expected: false, + }, + "equal": { + attribute: schema.NumberAttribute{}, + other: schema.NumberAttribute{}, + expected: true, + }, + } + + for name, testCase := range testCases { + 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 TestNumberAttributeGetDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.NumberAttribute + expected string + }{ + "no-description": { + attribute: schema.NumberAttribute{}, + expected: "", + }, + "description": { + attribute: schema.NumberAttribute{ + Description: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + 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 TestNumberAttributeGetMarkdownDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.NumberAttribute + expected string + }{ + "no-markdown-description": { + attribute: schema.NumberAttribute{}, + expected: "", + }, + "markdown-description": { + attribute: schema.NumberAttribute{ + MarkdownDescription: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + 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 TestNumberAttributeGetType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.NumberAttribute + expected attr.Type + }{ + "base": { + attribute: schema.NumberAttribute{}, + expected: types.NumberType, + }, + "custom-type": { + attribute: schema.NumberAttribute{ + CustomType: testtypes.NumberType{}, + }, + expected: testtypes.NumberType{}, + }, + } + + for name, testCase := range testCases { + 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 TestNumberAttributeIsComputed(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.NumberAttribute + expected bool + }{ + "not-computed": { + attribute: schema.NumberAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + 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 TestNumberAttributeIsOptional(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.NumberAttribute + expected bool + }{ + "not-optional": { + attribute: schema.NumberAttribute{}, + expected: false, + }, + "optional": { + attribute: schema.NumberAttribute{ + Optional: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + 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 TestNumberAttributeIsRequired(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.NumberAttribute + expected bool + }{ + "not-required": { + attribute: schema.NumberAttribute{}, + expected: false, + }, + "required": { + attribute: schema.NumberAttribute{ + Required: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + 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 TestNumberAttributeIsSensitive(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.NumberAttribute + expected bool + }{ + "not-sensitive": { + attribute: schema.NumberAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + 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 TestNumberAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.NumberAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.NumberAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNumberAttributeNumberValidators(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.NumberAttribute + expected []validator.Number + }{ + "no-validators": { + attribute: schema.NumberAttribute{}, + expected: nil, + }, + "validators": { + attribute: schema.NumberAttribute{ + Validators: []validator.Number{}, + }, + expected: []validator.Number{}, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.NumberValidators() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNumberAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.NumberAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.NumberAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNumberAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.NumberAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.NumberAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +}