diff --git a/action/action.go b/action/action.go index fdec8e15b..a53b6d212 100644 --- a/action/action.go +++ b/action/action.go @@ -49,3 +49,33 @@ type ActionWithModifyPlan interface { // diagnostics to practitioners, such as validation errors. ModifyPlan(context.Context, ModifyPlanRequest, *ModifyPlanResponse) } + +// ActionWithConfigValidators is an interface type that extends Action to include declarative validations. +// +// Declaring validation using this methodology simplifies implementation of +// reusable functionality. These also include descriptions, which can be used +// for automating documentation. +// +// Validation will include ConfigValidators and ValidateConfig, if both are +// implemented, in addition to any Attribute or Type validation. +type ActionWithConfigValidators interface { + Action + + // ConfigValidators returns a list of functions which will all be performed during validation. + ConfigValidators(context.Context) []ConfigValidator +} + +// ActionWithValidateConfig is an interface type that extends Action to include imperative validation. +// +// Declaring validation using this methodology simplifies one-off +// functionality that typically applies to a single action. Any documentation +// of this functionality must be manually added into schema descriptions. +// +// Validation will include ConfigValidators and ValidateConfig, if both are +// implemented, in addition to any Attribute or Type validation. +type ActionWithValidateConfig interface { + Action + + // ValidateConfig performs the validation. + ValidateConfig(context.Context, ValidateConfigRequest, *ValidateConfigResponse) +} diff --git a/action/config_validator.go b/action/config_validator.go new file mode 100644 index 000000000..90ea1c87a --- /dev/null +++ b/action/config_validator.go @@ -0,0 +1,27 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package action + +import "context" + +// ConfigValidator describes reusable Action configuration validation functionality. +type ConfigValidator interface { + // Description describes the validation in plain text formatting. + // + // This information may be automatically added to action plain text + // descriptions by external tooling. + Description(context.Context) string + + // MarkdownDescription describes the validation in Markdown formatting. + // + // This information may be automatically added to action Markdown + // descriptions by external tooling. + MarkdownDescription(context.Context) string + + // ValidateAction performs the validation. + // + // This method name is separate from ConfigValidators in resource and other packages in + // order to allow generic validators. + ValidateAction(context.Context, ValidateConfigRequest, *ValidateConfigResponse) +} diff --git a/action/schema/bool_attribute.go b/action/schema/bool_attribute.go index 48c8f46db..c33070721 100644 --- a/action/schema/bool_attribute.go +++ b/action/schema/bool_attribute.go @@ -8,13 +8,16 @@ import ( "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" ) // Ensure the implementation satisfies the desired interfaces. var ( - _ Attribute = BoolAttribute{} + _ Attribute = BoolAttribute{} + _ fwxschema.AttributeWithBoolValidators = BoolAttribute{} ) // BoolAttribute represents a schema attribute that is a boolean. When @@ -89,6 +92,18 @@ type BoolAttribute struct { // - 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 @@ -167,3 +182,8 @@ func (a BoolAttribute) IsRequiredForImport() bool { func (a BoolAttribute) IsOptionalForImport() bool { return false } + +// BoolValidators returns the Validators field value. +func (a BoolAttribute) BoolValidators() []validator.Bool { + return a.Validators +} diff --git a/action/schema/bool_attribute_test.go b/action/schema/bool_attribute_test.go index 725f14c8b..a7b15890b 100644 --- a/action/schema/bool_attribute_test.go +++ b/action/schema/bool_attribute_test.go @@ -16,6 +16,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" ) @@ -435,3 +436,35 @@ func TestBoolAttributeIsOptionalForImport(t *testing.T) { }) } } + +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) + } + }) + } +} diff --git a/action/schema/dynamic_attribute.go b/action/schema/dynamic_attribute.go index e9178f8ff..28837ef94 100644 --- a/action/schema/dynamic_attribute.go +++ b/action/schema/dynamic_attribute.go @@ -8,13 +8,16 @@ import ( "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" ) // Ensure the implementation satisifies the desired interfaces. var ( - _ Attribute = DynamicAttribute{} + _ Attribute = DynamicAttribute{} + _ fwxschema.AttributeWithDynamicValidators = DynamicAttribute{} ) // DynamicAttribute represents a schema attribute that is a dynamic, rather @@ -90,6 +93,18 @@ type DynamicAttribute struct { // - 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.Dynamic } // ApplyTerraform5AttributePathStep always returns an error as it is not @@ -168,3 +183,8 @@ func (a DynamicAttribute) IsRequiredForImport() bool { func (a DynamicAttribute) IsOptionalForImport() bool { return false } + +// DynamicValidators returns the Validators field value. +func (a DynamicAttribute) DynamicValidators() []validator.Dynamic { + return a.Validators +} diff --git a/action/schema/dynamic_attribute_test.go b/action/schema/dynamic_attribute_test.go index 7f3bb5a78..d8d6b5b31 100644 --- a/action/schema/dynamic_attribute_test.go +++ b/action/schema/dynamic_attribute_test.go @@ -14,6 +14,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-go/tftypes" ) @@ -434,3 +435,35 @@ func TestDynamicAttributeIsOptionalForImport(t *testing.T) { }) } } + +func TestDynamicAttributeDynamicValidators(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.DynamicAttribute + expected []validator.Dynamic + }{ + "no-validators": { + attribute: schema.DynamicAttribute{}, + expected: nil, + }, + "validators": { + attribute: schema.DynamicAttribute{ + Validators: []validator.Dynamic{}, + }, + expected: []validator.Dynamic{}, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.DynamicValidators() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/action/schema/float32_attribute.go b/action/schema/float32_attribute.go index e357d1338..06f998a3d 100644 --- a/action/schema/float32_attribute.go +++ b/action/schema/float32_attribute.go @@ -8,13 +8,16 @@ import ( "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" ) // Ensure the implementation satisifies the desired interfaces. var ( - _ Attribute = Float32Attribute{} + _ Attribute = Float32Attribute{} + _ fwxschema.AttributeWithFloat32Validators = Float32Attribute{} ) // Float32Attribute represents a schema attribute that is a 32-bit floating @@ -92,6 +95,18 @@ type Float32Attribute struct { // - 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 @@ -170,3 +185,8 @@ func (a Float32Attribute) IsRequiredForImport() bool { func (a Float32Attribute) IsOptionalForImport() bool { return false } + +// Float32Validators returns the Validators field value. +func (a Float32Attribute) Float32Validators() []validator.Float32 { + return a.Validators +} diff --git a/action/schema/float32_attribute_test.go b/action/schema/float32_attribute_test.go index 1253d1611..78df1346c 100644 --- a/action/schema/float32_attribute_test.go +++ b/action/schema/float32_attribute_test.go @@ -16,6 +16,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" ) @@ -435,3 +436,35 @@ func TestFloat32AttributeIsOptionalForImport(t *testing.T) { }) } } + +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) + } + }) + } +} diff --git a/action/schema/float64_attribute.go b/action/schema/float64_attribute.go index 42ed95d0f..aad03bc59 100644 --- a/action/schema/float64_attribute.go +++ b/action/schema/float64_attribute.go @@ -8,13 +8,16 @@ import ( "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" ) // Ensure the implementation satisifies the desired interfaces. var ( - _ Attribute = Float64Attribute{} + _ Attribute = Float64Attribute{} + _ fwxschema.AttributeWithFloat64Validators = Float64Attribute{} ) // Float64Attribute represents a schema attribute that is a 64-bit floating @@ -92,6 +95,18 @@ type Float64Attribute struct { // - 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 @@ -170,3 +185,8 @@ func (a Float64Attribute) IsRequiredForImport() bool { func (a Float64Attribute) IsOptionalForImport() bool { return false } + +// Float64Validators returns the Validators field value. +func (a Float64Attribute) Float64Validators() []validator.Float64 { + return a.Validators +} diff --git a/action/schema/float64_attribute_test.go b/action/schema/float64_attribute_test.go index f413d37c1..631565ced 100644 --- a/action/schema/float64_attribute_test.go +++ b/action/schema/float64_attribute_test.go @@ -16,6 +16,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" ) @@ -435,3 +436,35 @@ func TestFloat64AttributeIsOptionalForImport(t *testing.T) { }) } } + +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) + } + }) + } +} diff --git a/action/schema/int32_attribute.go b/action/schema/int32_attribute.go index 4bae0215d..4b57d6196 100644 --- a/action/schema/int32_attribute.go +++ b/action/schema/int32_attribute.go @@ -8,13 +8,16 @@ import ( "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" ) // Ensure the implementation satisifies the desired interfaces. var ( - _ Attribute = Int32Attribute{} + _ Attribute = Int32Attribute{} + _ fwxschema.AttributeWithInt32Validators = Int32Attribute{} ) // Int32Attribute represents a schema attribute that is a 32-bit integer. @@ -92,6 +95,18 @@ type Int32Attribute struct { // - 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 @@ -170,3 +185,8 @@ func (a Int32Attribute) IsRequiredForImport() bool { func (a Int32Attribute) IsOptionalForImport() bool { return false } + +// Int32Validators returns the Validators field value. +func (a Int32Attribute) Int32Validators() []validator.Int32 { + return a.Validators +} diff --git a/action/schema/int32_attribute_test.go b/action/schema/int32_attribute_test.go index 9b2fac75e..88015bb5f 100644 --- a/action/schema/int32_attribute_test.go +++ b/action/schema/int32_attribute_test.go @@ -16,6 +16,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" ) @@ -435,3 +436,35 @@ func TestInt32AttributeIsOptionalForImport(t *testing.T) { }) } } + +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) + } + }) + } +} diff --git a/action/schema/int64_attribute.go b/action/schema/int64_attribute.go index 1b527eae4..e64f59447 100644 --- a/action/schema/int64_attribute.go +++ b/action/schema/int64_attribute.go @@ -8,13 +8,16 @@ import ( "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" ) // Ensure the implementation satisifies the desired interfaces. var ( - _ Attribute = Int64Attribute{} + _ Attribute = Int64Attribute{} + _ fwxschema.AttributeWithInt64Validators = Int64Attribute{} ) // Int64Attribute represents a schema attribute that is a 64-bit integer. @@ -92,6 +95,18 @@ type Int64Attribute struct { // - 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 @@ -170,3 +185,8 @@ func (a Int64Attribute) IsRequiredForImport() bool { func (a Int64Attribute) IsOptionalForImport() bool { return false } + +// Int64Validators returns the Validators field value. +func (a Int64Attribute) Int64Validators() []validator.Int64 { + return a.Validators +} diff --git a/action/schema/int64_attribute_test.go b/action/schema/int64_attribute_test.go index 30376be27..9bd6cc524 100644 --- a/action/schema/int64_attribute_test.go +++ b/action/schema/int64_attribute_test.go @@ -16,6 +16,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" ) @@ -435,3 +436,35 @@ func TestInt64AttributeIsOptionalForImport(t *testing.T) { }) } } + +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) + } + }) + } +} diff --git a/action/schema/list_attribute.go b/action/schema/list_attribute.go index 180664e9e..86086d53e 100644 --- a/action/schema/list_attribute.go +++ b/action/schema/list_attribute.go @@ -10,7 +10,9 @@ import ( "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" "github.com/hashicorp/terraform-plugin-framework/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" ) @@ -19,6 +21,7 @@ import ( var ( _ Attribute = ListAttribute{} _ fwschema.AttributeWithValidateImplementation = ListAttribute{} + _ fwxschema.AttributeWithListValidators = ListAttribute{} ) // ListAttribute represents a schema attribute that is a list with a single @@ -108,6 +111,18 @@ type ListAttribute struct { // - 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 @@ -189,6 +204,11 @@ 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 diff --git a/action/schema/list_attribute_test.go b/action/schema/list_attribute_test.go index 52d8ec503..69315448a 100644 --- a/action/schema/list_attribute_test.go +++ b/action/schema/list_attribute_test.go @@ -19,6 +19,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" ) @@ -392,6 +393,38 @@ func TestListAttributeIsWriteOnly(t *testing.T) { } } +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() diff --git a/action/schema/list_nested_attribute.go b/action/schema/list_nested_attribute.go index ce3157ded..3287270f5 100644 --- a/action/schema/list_nested_attribute.go +++ b/action/schema/list_nested_attribute.go @@ -11,7 +11,9 @@ import ( "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" "github.com/hashicorp/terraform-plugin-framework/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" ) @@ -20,6 +22,7 @@ import ( var ( _ NestedAttribute = ListNestedAttribute{} _ fwschema.AttributeWithValidateImplementation = ListNestedAttribute{} + _ fwxschema.AttributeWithListValidators = ListNestedAttribute{} ) // ListNestedAttribute represents an attribute that is a list of objects where @@ -118,6 +121,18 @@ type ListNestedAttribute struct { // - 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 Attributes field value if step @@ -217,6 +232,11 @@ func (a ListNestedAttribute) IsOptionalForImport() bool { return false } +// ListValidators returns the Validators field value. +func (a ListNestedAttribute) 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 diff --git a/action/schema/list_nested_attribute_test.go b/action/schema/list_nested_attribute_test.go index 0a53508ba..e863b12d2 100644 --- a/action/schema/list_nested_attribute_test.go +++ b/action/schema/list_nested_attribute_test.go @@ -17,6 +17,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-go/tftypes" ) @@ -560,6 +561,44 @@ func TestListNestedAttributeIsWriteOnly(t *testing.T) { } } +func TestListNestedAttributeListValidators(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListNestedAttribute + expected []validator.List + }{ + "no-validators": { + attribute: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: nil, + }, + "validators": { + attribute: schema.ListNestedAttribute{ + 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 TestListNestedAttributeValidateImplementation(t *testing.T) { t.Parallel() diff --git a/action/schema/list_nested_block.go b/action/schema/list_nested_block.go index d61b8d888..5f3b5ca60 100644 --- a/action/schema/list_nested_block.go +++ b/action/schema/list_nested_block.go @@ -9,7 +9,9 @@ import ( "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" "github.com/hashicorp/terraform-plugin-framework/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" "github.com/hashicorp/terraform-plugin-go/tftypes" @@ -19,6 +21,7 @@ import ( var ( _ Block = ListNestedBlock{} _ fwschema.BlockWithValidateImplementation = ListNestedBlock{} + _ fwxschema.BlockWithListValidators = ListNestedBlock{} ) // ListNestedBlock represents a block that is a list of objects where @@ -113,6 +116,18 @@ type ListNestedBlock struct { // - 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 NestedObject field value if step @@ -173,6 +188,11 @@ func (b ListNestedBlock) Type() attr.Type { } } +// ListValidators returns the Validators field value. +func (b ListNestedBlock) ListValidators() []validator.List { + return b.Validators +} + // ValidateImplementation contains logic for validating the // provider-defined implementation of the block to prevent unexpected // errors or panics. This logic runs during the GetProviderSchema RPC and diff --git a/action/schema/list_nested_block_test.go b/action/schema/list_nested_block_test.go index d23f2614e..c80ffed3d 100644 --- a/action/schema/list_nested_block_test.go +++ b/action/schema/list_nested_block_test.go @@ -17,6 +17,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-go/tftypes" ) @@ -435,6 +436,44 @@ func TestListNestedBlockType(t *testing.T) { } } +func TestListNestedBlockListValidators(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block schema.ListNestedBlock + expected []validator.List + }{ + "no-validators": { + block: schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: nil, + }, + "validators": { + block: schema.ListNestedBlock{ + Validators: []validator.List{}, + }, + expected: []validator.List{}, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.block.ListValidators() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestListNestedBlockValidateImplementation(t *testing.T) { t.Parallel() diff --git a/action/schema/map_attribute.go b/action/schema/map_attribute.go index 80687fa09..d8d97504f 100644 --- a/action/schema/map_attribute.go +++ b/action/schema/map_attribute.go @@ -10,7 +10,9 @@ import ( "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" "github.com/hashicorp/terraform-plugin-framework/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" ) @@ -19,6 +21,7 @@ import ( var ( _ Attribute = MapAttribute{} _ fwschema.AttributeWithValidateImplementation = MapAttribute{} + _ fwxschema.AttributeWithMapValidators = MapAttribute{} ) // MapAttribute represents a schema attribute that is a map with a single @@ -111,6 +114,18 @@ type MapAttribute struct { // - 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 @@ -192,6 +207,11 @@ 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 diff --git a/action/schema/map_attribute_test.go b/action/schema/map_attribute_test.go index 5e0d42947..855bf1673 100644 --- a/action/schema/map_attribute_test.go +++ b/action/schema/map_attribute_test.go @@ -17,6 +17,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-go/tftypes" ) @@ -391,6 +392,38 @@ func TestMapAttributeIsWriteOnly(t *testing.T) { } } +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() diff --git a/action/schema/map_nested_attribute.go b/action/schema/map_nested_attribute.go index 82598dc2f..a6c84303a 100644 --- a/action/schema/map_nested_attribute.go +++ b/action/schema/map_nested_attribute.go @@ -11,7 +11,9 @@ import ( "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" "github.com/hashicorp/terraform-plugin-framework/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" ) @@ -20,6 +22,7 @@ import ( var ( _ NestedAttribute = MapNestedAttribute{} _ fwschema.AttributeWithValidateImplementation = MapNestedAttribute{} + _ fwxschema.AttributeWithMapValidators = MapNestedAttribute{} ) // MapNestedAttribute represents an attribute that is a map of objects where @@ -118,6 +121,18 @@ type MapNestedAttribute struct { // - 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 Attributes field value if step @@ -217,6 +232,11 @@ func (a MapNestedAttribute) IsOptionalForImport() bool { return false } +// MapValidators returns the Validators field value. +func (a MapNestedAttribute) 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 diff --git a/action/schema/map_nested_attribute_test.go b/action/schema/map_nested_attribute_test.go index 0db21ede3..bcb64fa6f 100644 --- a/action/schema/map_nested_attribute_test.go +++ b/action/schema/map_nested_attribute_test.go @@ -17,6 +17,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-go/tftypes" ) @@ -560,6 +561,44 @@ func TestMapNestedAttributeIsWriteOnly(t *testing.T) { } } +func TestMapNestedAttributeMapNestedValidators(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapNestedAttribute + expected []validator.Map + }{ + "no-validators": { + attribute: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: nil, + }, + "validators": { + attribute: schema.MapNestedAttribute{ + 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 TestMapNestedAttributeValidateImplementation(t *testing.T) { t.Parallel() diff --git a/action/schema/nested_attribute_object.go b/action/schema/nested_attribute_object.go index b082a154f..3719a2398 100644 --- a/action/schema/nested_attribute_object.go +++ b/action/schema/nested_attribute_object.go @@ -5,14 +5,14 @@ package schema import ( "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/basetypes" "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. -var ( - _ fwschema.NestedAttributeObject = NestedAttributeObject{} -) +var _ fwxschema.NestedAttributeObjectWithValidators = NestedAttributeObject{} // NestedAttributeObject is the object containing the underlying attributes // for a ListNestedAttribute, MapNestedAttribute, SetNestedAttribute, or @@ -33,6 +33,18 @@ type NestedAttributeObject struct { // default basetypes.ObjectType. When retrieving data, the basetypes.ObjectValuable // associated with this custom type must be used in place of types.Object. CustomType basetypes.ObjectTypable + + // 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.Object } // ApplyTerraform5AttributePathStep performs an AttributeName step on the @@ -55,6 +67,11 @@ func (o NestedAttributeObject) GetAttributes() fwschema.UnderlyingAttributes { return schemaAttributes(o.Attributes) } +// ObjectValidators returns the Validators field value. +func (o NestedAttributeObject) ObjectValidators() []validator.Object { + return o.Validators +} + // Type returns the framework type of the NestedAttributeObject. func (o NestedAttributeObject) Type() basetypes.ObjectTypable { if o.CustomType != nil { diff --git a/action/schema/nested_attribute_object_test.go b/action/schema/nested_attribute_object_test.go index 8c39be9e5..bde996213 100644 --- a/action/schema/nested_attribute_object_test.go +++ b/action/schema/nested_attribute_object_test.go @@ -13,6 +13,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-go/tftypes" ) @@ -192,6 +193,42 @@ func TestNestedAttributeObjectGetAttributes(t *testing.T) { } } +func TestNestedAttributeObjectObjectValidators(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.NestedAttributeObject + expected []validator.Object + }{ + "no-validators": { + attribute: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expected: nil, + }, + "validators": { + attribute: schema.NestedAttributeObject{ + Validators: []validator.Object{}, + }, + expected: []validator.Object{}, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.ObjectValidators() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestNestedAttributeObjectType(t *testing.T) { t.Parallel() diff --git a/action/schema/nested_block_object.go b/action/schema/nested_block_object.go index 8193b6891..2b560b606 100644 --- a/action/schema/nested_block_object.go +++ b/action/schema/nested_block_object.go @@ -5,14 +5,14 @@ package schema import ( "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/basetypes" "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. -var ( - _ fwschema.NestedBlockObject = NestedBlockObject{} -) +var _ fwxschema.NestedBlockObjectWithValidators = NestedBlockObject{} // NestedBlockObject is the object containing the underlying attributes and // blocks for a ListNestedBlock or SetNestedBlock. When retrieving the value @@ -40,6 +40,18 @@ type NestedBlockObject struct { // default basetypes.ObjectType. When retrieving data, the basetypes.ObjectValuable // associated with this custom type must be used in place of types.Object. CustomType basetypes.ObjectTypable + + // 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.Object } // ApplyTerraform5AttributePathStep performs an AttributeName step on the @@ -67,6 +79,11 @@ func (o NestedBlockObject) GetBlocks() map[string]fwschema.Block { return schemaBlocks(o.Blocks) } +// ObjectValidators returns the Validators field value. +func (o NestedBlockObject) ObjectValidators() []validator.Object { + return o.Validators +} + // Type returns the framework type of the NestedBlockObject. func (o NestedBlockObject) Type() basetypes.ObjectTypable { if o.CustomType != nil { diff --git a/action/schema/nested_block_object_test.go b/action/schema/nested_block_object_test.go index 2139384e1..f1d73ad79 100644 --- a/action/schema/nested_block_object_test.go +++ b/action/schema/nested_block_object_test.go @@ -13,6 +13,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-go/tftypes" ) @@ -264,6 +265,42 @@ func TestNestedBlockObjectGetBlocks(t *testing.T) { } } +func TestNestedBlockObjectObjectValidators(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.NestedBlockObject + expected []validator.Object + }{ + "no-validators": { + attribute: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expected: nil, + }, + "validators": { + attribute: schema.NestedBlockObject{ + Validators: []validator.Object{}, + }, + expected: []validator.Object{}, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.ObjectValidators() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestNestedBlockObjectType(t *testing.T) { t.Parallel() diff --git a/action/schema/number_attribute.go b/action/schema/number_attribute.go index a4bd6cee6..672d4371d 100644 --- a/action/schema/number_attribute.go +++ b/action/schema/number_attribute.go @@ -8,13 +8,16 @@ import ( "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" ) // Ensure the implementation satisifies the desired interfaces. var ( - _ Attribute = NumberAttribute{} + _ Attribute = NumberAttribute{} + _ fwxschema.AttributeWithNumberValidators = NumberAttribute{} ) // NumberAttribute represents a schema attribute that is a generic number with @@ -93,6 +96,18 @@ type NumberAttribute struct { // - 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 @@ -171,3 +186,8 @@ func (a NumberAttribute) IsRequiredForImport() bool { 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/action/schema/number_attribute_test.go b/action/schema/number_attribute_test.go index 1ac07f668..5708b5e3f 100644 --- a/action/schema/number_attribute_test.go +++ b/action/schema/number_attribute_test.go @@ -16,6 +16,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" ) @@ -435,3 +436,35 @@ func TestNumberAttributeIsOptionalForImport(t *testing.T) { }) } } + +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) + } + }) + } +} diff --git a/action/schema/object_attribute.go b/action/schema/object_attribute.go index a8e54f96b..5e9c656f7 100644 --- a/action/schema/object_attribute.go +++ b/action/schema/object_attribute.go @@ -10,7 +10,9 @@ import ( "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" "github.com/hashicorp/terraform-plugin-framework/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" ) @@ -19,6 +21,7 @@ import ( var ( _ Attribute = ObjectAttribute{} _ fwschema.AttributeWithValidateImplementation = ObjectAttribute{} + _ fwxschema.AttributeWithObjectValidators = ObjectAttribute{} ) // ObjectAttribute represents a schema attribute that is an object with only @@ -110,6 +113,18 @@ type ObjectAttribute struct { // - 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.Object } // ApplyTerraform5AttributePathStep returns the result of stepping into an @@ -191,6 +206,11 @@ func (a ObjectAttribute) IsOptionalForImport() bool { return false } +// ObjectValidators returns the Validators field value. +func (a ObjectAttribute) ObjectValidators() []validator.Object { + 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 diff --git a/action/schema/object_attribute_test.go b/action/schema/object_attribute_test.go index 1e04fdb66..4064b8ae2 100644 --- a/action/schema/object_attribute_test.go +++ b/action/schema/object_attribute_test.go @@ -19,6 +19,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" ) @@ -398,6 +399,38 @@ func TestObjectAttributeIsWriteOnly(t *testing.T) { } } +func TestObjectAttributeObjectValidators(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ObjectAttribute + expected []validator.Object + }{ + "no-validators": { + attribute: schema.ObjectAttribute{AttributeTypes: map[string]attr.Type{"testattr": types.StringType}}, + expected: nil, + }, + "validators": { + attribute: schema.ObjectAttribute{ + Validators: []validator.Object{}, + }, + expected: []validator.Object{}, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.ObjectValidators() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestObjectAttributeValidateImplementation(t *testing.T) { t.Parallel() diff --git a/action/schema/set_attribute.go b/action/schema/set_attribute.go index 449af04f5..eecdb6215 100644 --- a/action/schema/set_attribute.go +++ b/action/schema/set_attribute.go @@ -10,7 +10,9 @@ import ( "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" "github.com/hashicorp/terraform-plugin-framework/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" ) @@ -19,6 +21,7 @@ import ( var ( _ Attribute = SetAttribute{} _ fwschema.AttributeWithValidateImplementation = SetAttribute{} + _ fwxschema.AttributeWithSetValidators = SetAttribute{} ) // SetAttribute represents a schema attribute that is a set with a single @@ -106,6 +109,18 @@ type SetAttribute struct { // - 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.Set } // ApplyTerraform5AttributePathStep returns the result of stepping into a set @@ -187,6 +202,11 @@ func (a SetAttribute) IsOptionalForImport() bool { return false } +// SetValidators returns the Validators field value. +func (a SetAttribute) SetValidators() []validator.Set { + 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 diff --git a/action/schema/set_attribute_test.go b/action/schema/set_attribute_test.go index 063c2b26a..73a0fa67f 100644 --- a/action/schema/set_attribute_test.go +++ b/action/schema/set_attribute_test.go @@ -17,6 +17,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-go/tftypes" ) @@ -391,6 +392,38 @@ func TestSetAttributeIsWriteOnly(t *testing.T) { } } +func TestSetAttributeSetValidators(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetAttribute + expected []validator.Set + }{ + "no-validators": { + attribute: schema.SetAttribute{ElementType: types.StringType}, + expected: nil, + }, + "validators": { + attribute: schema.SetAttribute{ + Validators: []validator.Set{}, + }, + expected: []validator.Set{}, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.SetValidators() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestSetAttributeValidateImplementation(t *testing.T) { t.Parallel() diff --git a/action/schema/set_nested_attribute.go b/action/schema/set_nested_attribute.go index 5569ee6aa..a183409d7 100644 --- a/action/schema/set_nested_attribute.go +++ b/action/schema/set_nested_attribute.go @@ -11,7 +11,9 @@ import ( "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" "github.com/hashicorp/terraform-plugin-framework/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" ) @@ -20,6 +22,7 @@ import ( var ( _ NestedAttribute = SetNestedAttribute{} _ fwschema.AttributeWithValidateImplementation = SetNestedAttribute{} + _ fwxschema.AttributeWithSetValidators = SetNestedAttribute{} ) // SetNestedAttribute represents an attribute that is a set of objects where @@ -113,6 +116,18 @@ type SetNestedAttribute struct { // - 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.Set } // ApplyTerraform5AttributePathStep returns the Attributes field value if step @@ -212,6 +227,11 @@ func (a SetNestedAttribute) IsOptionalForImport() bool { return false } +// SetValidators returns the Validators field value. +func (a SetNestedAttribute) SetValidators() []validator.Set { + 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 diff --git a/action/schema/set_nested_attribute_test.go b/action/schema/set_nested_attribute_test.go index 2fd2f7b44..0b5e59b28 100644 --- a/action/schema/set_nested_attribute_test.go +++ b/action/schema/set_nested_attribute_test.go @@ -19,6 +19,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" ) @@ -561,6 +562,44 @@ func TestSetNestedAttributeIsWriteOnly(t *testing.T) { } } +func TestSetNestedAttributeSetValidators(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetNestedAttribute + expected []validator.Set + }{ + "no-validators": { + attribute: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: nil, + }, + "validators": { + attribute: schema.SetNestedAttribute{ + Validators: []validator.Set{}, + }, + expected: []validator.Set{}, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.SetValidators() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestSetNestedAttributeValidateImplementation(t *testing.T) { t.Parallel() diff --git a/action/schema/set_nested_block.go b/action/schema/set_nested_block.go index 158e43be1..e998a28c1 100644 --- a/action/schema/set_nested_block.go +++ b/action/schema/set_nested_block.go @@ -9,7 +9,9 @@ import ( "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" "github.com/hashicorp/terraform-plugin-framework/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" "github.com/hashicorp/terraform-plugin-go/tftypes" @@ -19,6 +21,7 @@ import ( var ( _ Block = SetNestedBlock{} _ fwschema.BlockWithValidateImplementation = SetNestedBlock{} + _ fwxschema.BlockWithSetValidators = SetNestedBlock{} ) // SetNestedBlock represents a block that is a set of objects where @@ -113,6 +116,18 @@ type SetNestedBlock struct { // - 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.Set } // ApplyTerraform5AttributePathStep returns the NestedObject field value if step @@ -162,6 +177,11 @@ func (b SetNestedBlock) GetNestingMode() fwschema.BlockNestingMode { return fwschema.BlockNestingModeSet } +// SetValidators returns the Validators field value. +func (b SetNestedBlock) SetValidators() []validator.Set { + return b.Validators +} + // Type returns SetType of ObjectType or CustomType. func (b SetNestedBlock) Type() attr.Type { if b.CustomType != nil { diff --git a/action/schema/set_nested_block_test.go b/action/schema/set_nested_block_test.go index 5e95c6d1a..ec09d6fc9 100644 --- a/action/schema/set_nested_block_test.go +++ b/action/schema/set_nested_block_test.go @@ -17,6 +17,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-go/tftypes" ) @@ -379,6 +380,44 @@ func TestSetNestedBlockGetNestedObject(t *testing.T) { } } +func TestSetNestedBlockSetValidators(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block schema.SetNestedBlock + expected []validator.Set + }{ + "no-validators": { + block: schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: nil, + }, + "validators": { + block: schema.SetNestedBlock{ + Validators: []validator.Set{}, + }, + expected: []validator.Set{}, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.block.SetValidators() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestSetNestedBlockType(t *testing.T) { t.Parallel() diff --git a/action/schema/single_nested_attribute.go b/action/schema/single_nested_attribute.go index 48903ab02..fcf08c1d9 100644 --- a/action/schema/single_nested_attribute.go +++ b/action/schema/single_nested_attribute.go @@ -10,13 +10,16 @@ import ( "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" ) // Ensure the implementation satisifies the desired interfaces. var ( - _ NestedAttribute = SingleNestedAttribute{} + _ NestedAttribute = SingleNestedAttribute{} + _ fwxschema.AttributeWithObjectValidators = SingleNestedAttribute{} ) // SingleNestedAttribute represents an attribute that is a single object where @@ -105,6 +108,18 @@ type SingleNestedAttribute struct { // - 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.Object } // ApplyTerraform5AttributePathStep returns the Attributes field value if step @@ -163,6 +178,7 @@ func (a SingleNestedAttribute) GetNestedObject() fwschema.NestedAttributeObject return NestedAttributeObject{ Attributes: a.Attributes, CustomType: a.CustomType, + Validators: a.Validators, } } @@ -224,3 +240,8 @@ func (a SingleNestedAttribute) IsRequiredForImport() bool { func (a SingleNestedAttribute) IsOptionalForImport() bool { return false } + +// ObjectValidators returns the Validators field value. +func (a SingleNestedAttribute) ObjectValidators() []validator.Object { + return a.Validators +} diff --git a/action/schema/single_nested_attribute_test.go b/action/schema/single_nested_attribute_test.go index a2522611d..4ceea6ca2 100644 --- a/action/schema/single_nested_attribute_test.go +++ b/action/schema/single_nested_attribute_test.go @@ -16,6 +16,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" ) @@ -573,3 +574,39 @@ func TestSingleNestedAttributeIsOptionalForImport(t *testing.T) { }) } } + +func TestSingleNestedAttributeObjectValidators(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SingleNestedAttribute + expected []validator.Object + }{ + "no-validators": { + attribute: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expected: nil, + }, + "validators": { + attribute: schema.SingleNestedAttribute{ + Validators: []validator.Object{}, + }, + expected: []validator.Object{}, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.ObjectValidators() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/action/schema/single_nested_block.go b/action/schema/single_nested_block.go index feb6f63d8..af7be7939 100644 --- a/action/schema/single_nested_block.go +++ b/action/schema/single_nested_block.go @@ -8,6 +8,8 @@ import ( "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" "github.com/hashicorp/terraform-plugin-go/tftypes" @@ -15,7 +17,8 @@ import ( // Ensure the implementation satisifies the desired interfaces. var ( - _ Block = SingleNestedBlock{} + _ Block = SingleNestedBlock{} + _ fwxschema.BlockWithObjectValidators = SingleNestedBlock{} ) // SingleNestedBlock represents a block that is a single object where @@ -107,6 +110,18 @@ type SingleNestedBlock struct { // - 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.Object } // ApplyTerraform5AttributePathStep returns the Attributes field value if step @@ -161,6 +176,7 @@ func (b SingleNestedBlock) GetNestedObject() fwschema.NestedBlockObject { Attributes: b.Attributes, Blocks: b.Blocks, CustomType: b.CustomType, + Validators: b.Validators, } } @@ -169,6 +185,11 @@ func (b SingleNestedBlock) GetNestingMode() fwschema.BlockNestingMode { return fwschema.BlockNestingModeSingle } +// ObjectValidators returns the Validators field value. +func (b SingleNestedBlock) ObjectValidators() []validator.Object { + return b.Validators +} + // Type returns ObjectType or CustomType. func (b SingleNestedBlock) Type() attr.Type { if b.CustomType != nil { diff --git a/action/schema/single_nested_block_test.go b/action/schema/single_nested_block_test.go index a52addce1..3ef4d5367 100644 --- a/action/schema/single_nested_block_test.go +++ b/action/schema/single_nested_block_test.go @@ -14,6 +14,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-go/tftypes" ) @@ -380,6 +381,42 @@ func TestSingleNestedBlockGetNestedObject(t *testing.T) { } } +func TestSingleNestedBlockObjectValidators(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block schema.SingleNestedBlock + expected []validator.Object + }{ + "no-validators": { + block: schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expected: nil, + }, + "validators": { + block: schema.SingleNestedBlock{ + Validators: []validator.Object{}, + }, + expected: []validator.Object{}, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.block.ObjectValidators() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestSingleNestedBlockType(t *testing.T) { t.Parallel() diff --git a/action/schema/string_attribute.go b/action/schema/string_attribute.go index bbf03341a..cb284d1c6 100644 --- a/action/schema/string_attribute.go +++ b/action/schema/string_attribute.go @@ -8,13 +8,16 @@ import ( "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" ) // Ensure the implementation satisfies the desired interfaces. var ( - _ Attribute = StringAttribute{} + _ Attribute = StringAttribute{} + _ fwxschema.AttributeWithStringValidators = StringAttribute{} ) // StringAttribute represents a schema attribute that is a string. When @@ -89,6 +92,18 @@ type StringAttribute struct { // - 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.String } // ApplyTerraform5AttributePathStep always returns an error as it is not @@ -167,3 +182,8 @@ func (a StringAttribute) IsRequiredForImport() bool { func (a StringAttribute) IsOptionalForImport() bool { return false } + +// StringValidators returns the Validators field value. +func (a StringAttribute) StringValidators() []validator.String { + return a.Validators +} diff --git a/action/schema/string_attribute_test.go b/action/schema/string_attribute_test.go index bbfc3cfff..a930c3e16 100644 --- a/action/schema/string_attribute_test.go +++ b/action/schema/string_attribute_test.go @@ -16,6 +16,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" ) @@ -435,3 +436,35 @@ func TestStringAttributeIsOptionalForImport(t *testing.T) { }) } } + +func TestStringAttributeStringValidators(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.StringAttribute + expected []validator.String + }{ + "no-validators": { + attribute: schema.StringAttribute{}, + expected: nil, + }, + "validators": { + attribute: schema.StringAttribute{ + Validators: []validator.String{}, + }, + expected: []validator.String{}, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.StringValidators() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/action/validate_config.go b/action/validate_config.go new file mode 100644 index 000000000..773b16fa6 --- /dev/null +++ b/action/validate_config.go @@ -0,0 +1,33 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package action + +import ( + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" +) + +// ValidateConfigRequest represents a request to validate the +// configuration of an action. An instance of this request struct is +// supplied as an argument to the Action ValidateConfig receiver method +// or automatically passed through to each ConfigValidator. +type ValidateConfigRequest struct { + // Config is the configuration the user supplied for the action. + // + // This configuration may contain unknown values if a user uses + // interpolation or other functionality that would prevent Terraform + // from knowing the value at request time. + Config tfsdk.Config +} + +// ValidateConfigResponse represents a response to a +// ValidateConfigRequest. An instance of this response struct is +// supplied as an argument to the Action ValidateConfig receiver method +// or automatically passed through to each ConfigValidator. +type ValidateConfigResponse struct { + // Diagnostics report errors or warnings related to validating the action + // configuration. An empty slice indicates success, with no warnings or + // errors generated. + Diagnostics diag.Diagnostics +} diff --git a/datasource/config_validator.go b/datasource/config_validator.go index 0a153da49..29024bd6b 100644 --- a/datasource/config_validator.go +++ b/datasource/config_validator.go @@ -21,8 +21,7 @@ type ConfigValidator interface { // ValidateDataSource performs the validation. // - // This method name is separate from the provider.ConfigValidator - // interface ValidateProvider method name and resource.ConfigValidator - // interface ValidateResource method name to allow generic validators. + // This method name is separate from ConfigValidators in resource and other packages in + // order to allow generic validators. ValidateDataSource(context.Context, ValidateConfigRequest, *ValidateConfigResponse) } diff --git a/ephemeral/config_validator.go b/ephemeral/config_validator.go index 1e8b15c7a..eedbbb8fd 100644 --- a/ephemeral/config_validator.go +++ b/ephemeral/config_validator.go @@ -21,9 +21,7 @@ type ConfigValidator interface { // ValidateEphemeralResource performs the validation. // - // This method name is separate from the datasource.ConfigValidator - // interface ValidateDataSource method name, provider.ConfigValidator - // interface ValidateProvider method name, and resource.ConfigValidator - // interface ValidateResource method name to allow generic validators. + // This method name is separate from ConfigValidators in resource and other packages in + // order to allow generic validators. ValidateEphemeralResource(context.Context, ValidateConfigRequest, *ValidateConfigResponse) } diff --git a/go.mod b/go.mod index 3996d6843..eecbe544d 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ toolchain go1.23.7 require ( github.com/google/go-cmp v0.7.0 - github.com/hashicorp/terraform-plugin-go v0.29.0-alpha.1.0.20250709165734-a8477a15f806 + github.com/hashicorp/terraform-plugin-go v0.29.0-alpha.1.0.20250717133739-e33a5336fb19 github.com/hashicorp/terraform-plugin-log v0.9.0 ) diff --git a/go.sum b/go.sum index 58d2914b5..2b4b5e573 100644 --- a/go.sum +++ b/go.sum @@ -21,8 +21,8 @@ github.com/hashicorp/go-plugin v1.6.3 h1:xgHB+ZUSYeuJi96WtxEjzi23uh7YQpznjGh0U0U github.com/hashicorp/go-plugin v1.6.3/go.mod h1:MRobyh+Wc/nYy1V4KAXUiYfzxoYhs7V1mlH1Z7iY2h0= github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/terraform-plugin-go v0.29.0-alpha.1.0.20250709165734-a8477a15f806 h1:i3kA1sT/Fk8Ex+VVKdjf9sFOPwS7w3Q73pfbnxKwdjg= -github.com/hashicorp/terraform-plugin-go v0.29.0-alpha.1.0.20250709165734-a8477a15f806/go.mod h1:hL//wLEfYo0YVt0TC/VLzia/ADQQto3HEm4/jX2gkdY= +github.com/hashicorp/terraform-plugin-go v0.29.0-alpha.1.0.20250717133739-e33a5336fb19 h1:P/ZVGEGXt9xSiLz+CrP/JzV2V8rtlE7994AX4jzcGB8= +github.com/hashicorp/terraform-plugin-go v0.29.0-alpha.1.0.20250717133739-e33a5336fb19/go.mod h1:hL//wLEfYo0YVt0TC/VLzia/ADQQto3HEm4/jX2gkdY= github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0= github.com/hashicorp/terraform-plugin-log v0.9.0/go.mod h1:rKL8egZQ/eXSyDqzLUuwUYLVdlYeamldAHSxjUFADow= github.com/hashicorp/terraform-registry-address v0.3.0 h1:HMpK3nqaGFPS9VmgRXrJL/dzHNdheGVKk5k7VlFxzCo= diff --git a/internal/fromproto5/validateactionconfig.go b/internal/fromproto5/validateactionconfig.go new file mode 100644 index 000000000..d43a1115f --- /dev/null +++ b/internal/fromproto5/validateactionconfig.go @@ -0,0 +1,31 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fromproto5 + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/action" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" +) + +// ValidateActionConfigRequest returns the *fwserver.ValidateActionConfigRequest +// equivalent of a *tfprotov5.ValidateActionConfigRequest. +func ValidateActionConfigRequest(ctx context.Context, proto5 *tfprotov5.ValidateActionConfigRequest, reqAction action.Action, actionSchema fwschema.Schema) (*fwserver.ValidateActionConfigRequest, diag.Diagnostics) { + if proto5 == nil { + return nil, nil + } + + fw := &fwserver.ValidateActionConfigRequest{} + + config, diags := Config(ctx, proto5.Config, actionSchema) + + fw.Config = config + fw.Action = reqAction + + return fw, diags +} diff --git a/internal/fromproto5/validateactionconfig_test.go b/internal/fromproto5/validateactionconfig_test.go new file mode 100644 index 000000000..4ecfeedc2 --- /dev/null +++ b/internal/fromproto5/validateactionconfig_test.go @@ -0,0 +1,108 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fromproto5_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/action" + "github.com/hashicorp/terraform-plugin-framework/action/schema" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fromproto5" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestValidateActionConfigRequest(t *testing.T) { + t.Parallel() + + testProto5Type := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_attribute": tftypes.String, + }, + } + + testProto5Value := tftypes.NewValue(testProto5Type, map[string]tftypes.Value{ + "test_attribute": tftypes.NewValue(tftypes.String, "test-value"), + }) + + testProto5DynamicValue, err := tfprotov5.NewDynamicValue(testProto5Type, testProto5Value) + + if err != nil { + t.Fatalf("unexpected error calling tfprotov5.NewDynamicValue(): %s", err) + } + + testFwSchema := schema.UnlinkedSchema{ + Attributes: map[string]schema.Attribute{ + "test_attribute": schema.StringAttribute{ + Required: true, + }, + }, + } + + testCases := map[string]struct { + input *tfprotov5.ValidateActionConfigRequest + actionSchema fwschema.Schema + actionImpl action.Action + expected *fwserver.ValidateActionConfigRequest + expectedDiagnostics diag.Diagnostics + }{ + "nil": { + input: nil, + expected: nil, + }, + "empty": { + input: &tfprotov5.ValidateActionConfigRequest{}, + expected: &fwserver.ValidateActionConfigRequest{}, + }, + "config-missing-schema": { + input: &tfprotov5.ValidateActionConfigRequest{ + Config: &testProto5DynamicValue, + }, + expected: &fwserver.ValidateActionConfigRequest{}, + expectedDiagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Unable to Convert Configuration", + "An unexpected error was encountered when converting the configuration from the protocol type. "+ + "This is always an issue in terraform-plugin-framework used to implement the provider and should be reported to the provider developers.\n\n"+ + "Please report this to the provider developer:\n\n"+ + "Missing schema.", + ), + }, + }, + "config": { + input: &tfprotov5.ValidateActionConfigRequest{ + Config: &testProto5DynamicValue, + }, + actionSchema: testFwSchema, + expected: &fwserver.ValidateActionConfigRequest{ + Config: &tfsdk.Config{ + Raw: testProto5Value, + Schema: testFwSchema, + }, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, diags := fromproto5.ValidateActionConfigRequest(context.Background(), testCase.input, testCase.actionImpl, testCase.actionSchema) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + + if diff := cmp.Diff(diags, testCase.expectedDiagnostics); diff != "" { + t.Errorf("unexpected diagnostics difference: %s", diff) + } + }) + } +} diff --git a/internal/fromproto6/validateactionconfig.go b/internal/fromproto6/validateactionconfig.go new file mode 100644 index 000000000..898aabf8d --- /dev/null +++ b/internal/fromproto6/validateactionconfig.go @@ -0,0 +1,31 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fromproto6 + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/action" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" +) + +// ValidateActionConfigRequest returns the *fwserver.ValidateActionConfigRequest +// equivalent of a *tfprotov6.ValidateActionConfigRequest. +func ValidateActionConfigRequest(ctx context.Context, proto6 *tfprotov6.ValidateActionConfigRequest, reqAction action.Action, actionSchema fwschema.Schema) (*fwserver.ValidateActionConfigRequest, diag.Diagnostics) { + if proto6 == nil { + return nil, nil + } + + fw := &fwserver.ValidateActionConfigRequest{} + + config, diags := Config(ctx, proto6.Config, actionSchema) + + fw.Config = config + fw.Action = reqAction + + return fw, diags +} diff --git a/internal/fromproto6/validateactionconfig_test.go b/internal/fromproto6/validateactionconfig_test.go new file mode 100644 index 000000000..c782c2e2f --- /dev/null +++ b/internal/fromproto6/validateactionconfig_test.go @@ -0,0 +1,108 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fromproto6_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/action" + "github.com/hashicorp/terraform-plugin-framework/action/schema" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fromproto6" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestValidateActionConfigRequest(t *testing.T) { + t.Parallel() + + testProto6Type := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_attribute": tftypes.String, + }, + } + + testProto6Value := tftypes.NewValue(testProto6Type, map[string]tftypes.Value{ + "test_attribute": tftypes.NewValue(tftypes.String, "test-value"), + }) + + testProto6DynamicValue, err := tfprotov6.NewDynamicValue(testProto6Type, testProto6Value) + + if err != nil { + t.Fatalf("unexpected error calling tfprotov6.NewDynamicValue(): %s", err) + } + + testFwSchema := schema.UnlinkedSchema{ + Attributes: map[string]schema.Attribute{ + "test_attribute": schema.StringAttribute{ + Required: true, + }, + }, + } + + testCases := map[string]struct { + input *tfprotov6.ValidateActionConfigRequest + actionSchema fwschema.Schema + actionImpl action.Action + expected *fwserver.ValidateActionConfigRequest + expectedDiagnostics diag.Diagnostics + }{ + "nil": { + input: nil, + expected: nil, + }, + "empty": { + input: &tfprotov6.ValidateActionConfigRequest{}, + expected: &fwserver.ValidateActionConfigRequest{}, + }, + "config-missing-schema": { + input: &tfprotov6.ValidateActionConfigRequest{ + Config: &testProto6DynamicValue, + }, + expected: &fwserver.ValidateActionConfigRequest{}, + expectedDiagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Unable to Convert Configuration", + "An unexpected error was encountered when converting the configuration from the protocol type. "+ + "This is always an issue in terraform-plugin-framework used to implement the provider and should be reported to the provider developers.\n\n"+ + "Please report this to the provider developer:\n\n"+ + "Missing schema.", + ), + }, + }, + "config": { + input: &tfprotov6.ValidateActionConfigRequest{ + Config: &testProto6DynamicValue, + }, + actionSchema: testFwSchema, + expected: &fwserver.ValidateActionConfigRequest{ + Config: &tfsdk.Config{ + Raw: testProto6Value, + Schema: testFwSchema, + }, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, diags := fromproto6.ValidateActionConfigRequest(context.Background(), testCase.input, testCase.actionImpl, testCase.actionSchema) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + + if diff := cmp.Diff(diags, testCase.expectedDiagnostics); diff != "" { + t.Errorf("unexpected diagnostics difference: %s", diff) + } + }) + } +} diff --git a/internal/fwserver/server_validateactionconfig.go b/internal/fwserver/server_validateactionconfig.go new file mode 100644 index 000000000..5dd387681 --- /dev/null +++ b/internal/fwserver/server_validateactionconfig.go @@ -0,0 +1,119 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fwserver + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/action" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/logging" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" +) + +// ValidateActionConfigRequest is the framework server request for the +// ValidateActionConfig RPC. +type ValidateActionConfigRequest struct { + Config *tfsdk.Config + Action action.Action +} + +// ValidateActionConfigResponse is the framework server response for the +// ValidateActionConfig RPC. +type ValidateActionConfigResponse struct { + Diagnostics diag.Diagnostics +} + +// ValidateActionConfig implements the framework server ValidateActionConfig RPC. +func (s *Server) ValidateActionConfig(ctx context.Context, req *ValidateActionConfigRequest, resp *ValidateActionConfigResponse) { + if req == nil || req.Config == nil { + return + } + + if actionWithConfigure, ok := req.Action.(action.ActionWithConfigure); ok { + logging.FrameworkTrace(ctx, "Action implements ActionWithConfigure") + + configureReq := action.ConfigureRequest{ + ProviderData: s.ActionConfigureData, + } + configureResp := action.ConfigureResponse{} + + logging.FrameworkTrace(ctx, "Calling provider defined Action Configure") + actionWithConfigure.Configure(ctx, configureReq, &configureResp) + logging.FrameworkTrace(ctx, "Called provider defined Action Configure") + + resp.Diagnostics.Append(configureResp.Diagnostics...) + + if resp.Diagnostics.HasError() { + return + } + } + + vdscReq := action.ValidateConfigRequest{ + Config: *req.Config, + } + + if actionWithConfigValidators, ok := req.Action.(action.ActionWithConfigValidators); ok { + logging.FrameworkTrace(ctx, "Action implements ActionWithConfigValidators") + + for _, configValidator := range actionWithConfigValidators.ConfigValidators(ctx) { + // Instantiate a new response for each request to prevent validators + // from modifying or removing diagnostics. + vdscResp := &action.ValidateConfigResponse{} + + logging.FrameworkTrace( + ctx, + "Calling provider defined ActionConfigValidator", + map[string]interface{}{ + logging.KeyDescription: configValidator.Description(ctx), + }, + ) + configValidator.ValidateAction(ctx, vdscReq, vdscResp) + logging.FrameworkTrace( + ctx, + "Called provider defined ActionConfigValidator", + map[string]interface{}{ + logging.KeyDescription: configValidator.Description(ctx), + }, + ) + + resp.Diagnostics.Append(vdscResp.Diagnostics...) + } + } + + if actionWithValidateConfig, ok := req.Action.(action.ActionWithValidateConfig); ok { + logging.FrameworkTrace(ctx, "Action implements ActionWithValidateConfig") + + // Instantiate a new response for each request to prevent validators + // from modifying or removing diagnostics. + vdscResp := &action.ValidateConfigResponse{} + + logging.FrameworkTrace(ctx, "Calling provider defined Action ValidateConfig") + actionWithValidateConfig.ValidateConfig(ctx, vdscReq, vdscResp) + logging.FrameworkTrace(ctx, "Called provider defined Action ValidateConfig") + + resp.Diagnostics.Append(vdscResp.Diagnostics...) + } + + schemaCapabilities := validator.ValidateSchemaClientCapabilities{ + // The SchemaValidate function is shared between provider, resource, + // data source, ephemeral resource, and action schemas; however, WriteOnlyAttributesAllowed + // capability is only valid for resource schemas, so this is explicitly set to false + // for all other schema types. + WriteOnlyAttributesAllowed: false, + } + + validateSchemaReq := ValidateSchemaRequest{ + ClientCapabilities: schemaCapabilities, + Config: *req.Config, + } + // Instantiate a new response for each request to prevent validators + // from modifying or removing diagnostics. + validateSchemaResp := ValidateSchemaResponse{} + + SchemaValidate(ctx, req.Config.Schema, validateSchemaReq, &validateSchemaResp) + + resp.Diagnostics.Append(validateSchemaResp.Diagnostics...) +} diff --git a/internal/fwserver/server_validateactionconfig_test.go b/internal/fwserver/server_validateactionconfig_test.go new file mode 100644 index 000000000..0a9d30ce6 --- /dev/null +++ b/internal/fwserver/server_validateactionconfig_test.go @@ -0,0 +1,306 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fwserver_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/action" + "github.com/hashicorp/terraform-plugin-framework/action/schema" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testprovider" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testvalidator" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestServerValidateActionConfig(t *testing.T) { + t.Parallel() + + testType := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.String, + }, + } + + testValue := tftypes.NewValue(testType, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.String, "test-value"), + }) + + testSchema := schema.UnlinkedSchema{ + Attributes: map[string]schema.Attribute{ + "test": schema.StringAttribute{ + Required: true, + }, + }, + } + + testConfig := tfsdk.Config{ + Raw: testValue, + Schema: testSchema, + } + + testSchemaAttributeValidator := schema.UnlinkedSchema{ + Attributes: map[string]schema.Attribute{ + "test": schema.StringAttribute{ + Required: true, + Validators: []validator.String{ + testvalidator.String{ + ValidateStringMethod: func(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) { + if req.ConfigValue.ValueString() != "test-value" { + resp.Diagnostics.AddError("Incorrect req.AttributeConfig", "expected test-value, got "+req.ConfigValue.ValueString()) + } + }, + }, + }, + }, + }, + } + + testConfigAttributeValidator := tfsdk.Config{ + Raw: testValue, + Schema: testSchemaAttributeValidator, + } + + testSchemaAttributeValidatorError := schema.UnlinkedSchema{ + Attributes: map[string]schema.Attribute{ + "test": schema.StringAttribute{ + Required: true, + Validators: []validator.String{ + testvalidator.String{ + ValidateStringMethod: func(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) { + resp.Diagnostics.AddAttributeError(req.Path, "error summary", "error detail") + }, + }, + }, + }, + }, + } + + testConfigAttributeValidatorError := tfsdk.Config{ + Raw: testValue, + Schema: testSchemaAttributeValidatorError, + } + + testCases := map[string]struct { + server *fwserver.Server + request *fwserver.ValidateActionConfigRequest + expectedResponse *fwserver.ValidateActionConfigResponse + }{ + "nil": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + expectedResponse: &fwserver.ValidateActionConfigResponse{}, + }, + "request-config": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ValidateActionConfigRequest{ + Config: &testConfig, + Action: &testprovider.Action{ + SchemaMethod: func(_ context.Context, _ action.SchemaRequest, resp *action.SchemaResponse) { + resp.Schema = testSchema + }, + }, + }, + expectedResponse: &fwserver.ValidateActionConfigResponse{}, + }, + "request-config-AttributeValidator": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ValidateActionConfigRequest{ + Config: &testConfigAttributeValidator, + Action: &testprovider.Action{ + SchemaMethod: func(_ context.Context, _ action.SchemaRequest, resp *action.SchemaResponse) { + resp.Schema = testSchemaAttributeValidator + }, + }, + }, + expectedResponse: &fwserver.ValidateActionConfigResponse{}, + }, + "request-config-AttributeValidator-diagnostic": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ValidateActionConfigRequest{ + Config: &testConfigAttributeValidatorError, + Action: &testprovider.Action{ + SchemaMethod: func(_ context.Context, _ action.SchemaRequest, resp *action.SchemaResponse) { + resp.Schema = testSchemaAttributeValidatorError + }, + }, + }, + expectedResponse: &fwserver.ValidateActionConfigResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("test"), + "error summary", + "error detail", + ), + }, + }, + }, + "request-config-ActionWithConfigValidators": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ValidateActionConfigRequest{ + Config: &testConfig, + Action: &testprovider.ActionWithConfigValidators{ + Action: &testprovider.Action{ + SchemaMethod: func(_ context.Context, _ action.SchemaRequest, resp *action.SchemaResponse) { + resp.Schema = testSchema + }, + }, + ConfigValidatorsMethod: func(ctx context.Context) []action.ConfigValidator { + return []action.ConfigValidator{ + &testprovider.ActionConfigValidator{ + ValidateActionMethod: func(ctx context.Context, req action.ValidateConfigRequest, resp *action.ValidateConfigResponse) { + var got types.String + + resp.Diagnostics.Append(req.Config.GetAttribute(ctx, path.Root("test"), &got)...) + + if resp.Diagnostics.HasError() { + return + } + + if got.ValueString() != "test-value" { + resp.Diagnostics.AddError("Incorrect req.Config", "expected test-value, got "+got.ValueString()) + } + }, + }, + } + }, + }, + }, + expectedResponse: &fwserver.ValidateActionConfigResponse{}, + }, + "request-config-ActionWithConfigValidators-diagnostics": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ValidateActionConfigRequest{ + Config: &testConfig, + Action: &testprovider.ActionWithConfigValidators{ + Action: &testprovider.Action{ + SchemaMethod: func(_ context.Context, _ action.SchemaRequest, resp *action.SchemaResponse) { + resp.Schema = testSchema + }, + }, + ConfigValidatorsMethod: func(ctx context.Context) []action.ConfigValidator { + return []action.ConfigValidator{ + &testprovider.ActionConfigValidator{ + ValidateActionMethod: func(ctx context.Context, req action.ValidateConfigRequest, resp *action.ValidateConfigResponse) { + resp.Diagnostics.AddError("error summary 1", "error detail 1") + }, + }, + &testprovider.ActionConfigValidator{ + ValidateActionMethod: func(ctx context.Context, req action.ValidateConfigRequest, resp *action.ValidateConfigResponse) { + // Intentionally set diagnostics instead of add/append. + // The framework should not overwrite existing diagnostics. + // Reference: https://github.com/hashicorp/terraform-plugin-framework-validators/pull/94 + resp.Diagnostics = diag.Diagnostics{ + diag.NewErrorDiagnostic("error summary 2", "error detail 2"), + } + }, + }, + } + }, + }, + }, + expectedResponse: &fwserver.ValidateActionConfigResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "error summary 1", + "error detail 1", + ), + diag.NewErrorDiagnostic( + "error summary 2", + "error detail 2", + ), + }}, + }, + "request-config-ActionWithValidateConfig": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ValidateActionConfigRequest{ + Config: &testConfig, + Action: &testprovider.ActionWithValidateConfig{ + Action: &testprovider.Action{ + SchemaMethod: func(_ context.Context, _ action.SchemaRequest, resp *action.SchemaResponse) { + resp.Schema = testSchema + }, + }, + ValidateConfigMethod: func(ctx context.Context, req action.ValidateConfigRequest, resp *action.ValidateConfigResponse) { + var got types.String + + resp.Diagnostics.Append(req.Config.GetAttribute(ctx, path.Root("test"), &got)...) + + if resp.Diagnostics.HasError() { + return + } + + if got.ValueString() != "test-value" { + resp.Diagnostics.AddError("Incorrect req.Config", "expected test-value, got "+got.ValueString()) + } + }, + }, + }, + expectedResponse: &fwserver.ValidateActionConfigResponse{}, + }, + "request-config-ActionWithValidateConfig-diagnostic": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ValidateActionConfigRequest{ + Config: &testConfig, + Action: &testprovider.ActionWithValidateConfig{ + Action: &testprovider.Action{ + SchemaMethod: func(_ context.Context, _ action.SchemaRequest, resp *action.SchemaResponse) { + resp.Schema = testSchema + }, + }, + ValidateConfigMethod: func(ctx context.Context, req action.ValidateConfigRequest, resp *action.ValidateConfigResponse) { + resp.Diagnostics.AddWarning("warning summary", "warning detail") + resp.Diagnostics.AddError("error summary", "error detail") + }, + }, + }, + expectedResponse: &fwserver.ValidateActionConfigResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewWarningDiagnostic( + "warning summary", + "warning detail", + ), + diag.NewErrorDiagnostic( + "error summary", + "error detail", + ), + }}, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + response := &fwserver.ValidateActionConfigResponse{} + testCase.server.ValidateActionConfig(context.Background(), testCase.request, response) + + if diff := cmp.Diff(response, testCase.expectedResponse); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/internal/proto5server/server_validateactionconfig.go b/internal/proto5server/server_validateactionconfig.go new file mode 100644 index 000000000..296de791a --- /dev/null +++ b/internal/proto5server/server_validateactionconfig.go @@ -0,0 +1,50 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package proto5server + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/internal/fromproto5" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/logging" + "github.com/hashicorp/terraform-plugin-framework/internal/toproto5" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" +) + +// ValidateActionConfig satisfies the tfprotov5.ProviderServer interface. +func (s *Server) ValidateActionConfig(ctx context.Context, proto5Req *tfprotov5.ValidateActionConfigRequest) (*tfprotov5.ValidateActionConfigResponse, error) { + ctx = s.registerContext(ctx) + ctx = logging.InitContext(ctx) + + fwResp := &fwserver.ValidateActionConfigResponse{} + + action, diags := s.FrameworkServer.Action(ctx, proto5Req.ActionType) + + fwResp.Diagnostics.Append(diags...) + + if fwResp.Diagnostics.HasError() { + return toproto5.ValidateActionConfigResponse(ctx, fwResp), nil + } + + actionSchema, diags := s.FrameworkServer.ActionSchema(ctx, proto5Req.ActionType) + + fwResp.Diagnostics.Append(diags...) + + if fwResp.Diagnostics.HasError() { + return toproto5.ValidateActionConfigResponse(ctx, fwResp), nil + } + + fwReq, diags := fromproto5.ValidateActionConfigRequest(ctx, proto5Req, action, actionSchema) + + fwResp.Diagnostics.Append(diags...) + + if fwResp.Diagnostics.HasError() { + return toproto5.ValidateActionConfigResponse(ctx, fwResp), nil + } + + s.FrameworkServer.ValidateActionConfig(ctx, fwReq, fwResp) + + return toproto5.ValidateActionConfigResponse(ctx, fwResp), nil +} diff --git a/internal/proto5server/server_validateactionconfig_test.go b/internal/proto5server/server_validateactionconfig_test.go new file mode 100644 index 000000000..77349fecf --- /dev/null +++ b/internal/proto5server/server_validateactionconfig_test.go @@ -0,0 +1,166 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package proto5server + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/action" + "github.com/hashicorp/terraform-plugin-framework/action/schema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testprovider" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestServerValidateActionConfig(t *testing.T) { + t.Parallel() + + testType := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.String, + }, + } + + testValue := tftypes.NewValue(testType, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.String, "test-value"), + }) + + testDynamicValue, err := tfprotov5.NewDynamicValue(testType, testValue) + + if err != nil { + t.Fatalf("unexpected error calling tfprotov5.NewDynamicValue(): %s", err) + } + + testSchema := schema.UnlinkedSchema{ + Attributes: map[string]schema.Attribute{ + "test": schema.StringAttribute{ + Required: true, + }, + }, + } + + testCases := map[string]struct { + server *Server + request *tfprotov5.ValidateActionConfigRequest + expectedError error + expectedResponse *tfprotov5.ValidateActionConfigResponse + }{ + "no-schema": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + ActionsMethod: func(_ context.Context) []func() action.Action { + return []func() action.Action{ + func() action.Action { + return &testprovider.Action{ + SchemaMethod: func(_ context.Context, _ action.SchemaRequest, resp *action.SchemaResponse) {}, + MetadataMethod: func(_ context.Context, _ action.MetadataRequest, resp *action.MetadataResponse) { + resp.TypeName = "test_action" + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov5.ValidateActionConfigRequest{ + ActionType: "test_action", + }, + expectedResponse: &tfprotov5.ValidateActionConfigResponse{}, + }, + "request-config": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + ActionsMethod: func(_ context.Context) []func() action.Action { + return []func() action.Action{ + func() action.Action { + return &testprovider.Action{ + SchemaMethod: func(_ context.Context, _ action.SchemaRequest, resp *action.SchemaResponse) { + resp.Schema = testSchema + }, + MetadataMethod: func(_ context.Context, _ action.MetadataRequest, resp *action.MetadataResponse) { + resp.TypeName = "test_action" + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov5.ValidateActionConfigRequest{ + Config: &testDynamicValue, + ActionType: "test_action", + }, + expectedResponse: &tfprotov5.ValidateActionConfigResponse{}, + }, + "response-diagnostics": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + ActionsMethod: func(_ context.Context) []func() action.Action { + return []func() action.Action{ + func() action.Action { + return &testprovider.ActionWithValidateConfig{ + Action: &testprovider.Action{ + SchemaMethod: func(_ context.Context, _ action.SchemaRequest, resp *action.SchemaResponse) { + resp.Schema = testSchema + }, + MetadataMethod: func(_ context.Context, _ action.MetadataRequest, resp *action.MetadataResponse) { + resp.TypeName = "test_action" + }, + }, + ValidateConfigMethod: func(ctx context.Context, req action.ValidateConfigRequest, resp *action.ValidateConfigResponse) { + resp.Diagnostics.AddWarning("warning summary", "warning detail") + resp.Diagnostics.AddError("error summary", "error detail") + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov5.ValidateActionConfigRequest{ + Config: &testDynamicValue, + ActionType: "test_action", + }, + expectedResponse: &tfprotov5.ValidateActionConfigResponse{ + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityWarning, + Summary: "warning summary", + Detail: "warning detail", + }, + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "error summary", + Detail: "error detail", + }, + }, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.server.ValidateActionConfig(context.Background(), testCase.request) + + if diff := cmp.Diff(testCase.expectedError, err); diff != "" { + t.Errorf("unexpected error difference: %s", diff) + } + + if diff := cmp.Diff(testCase.expectedResponse, got); diff != "" { + t.Errorf("unexpected response difference: %s", diff) + } + }) + } +} diff --git a/internal/proto6server/server_validateactionconfig.go b/internal/proto6server/server_validateactionconfig.go new file mode 100644 index 000000000..994766e34 --- /dev/null +++ b/internal/proto6server/server_validateactionconfig.go @@ -0,0 +1,50 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package proto6server + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/internal/fromproto6" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/logging" + "github.com/hashicorp/terraform-plugin-framework/internal/toproto6" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" +) + +// ValidateActionConfig satisfies the tfprotov6.ProviderServer interface. +func (s *Server) ValidateActionConfig(ctx context.Context, proto6Req *tfprotov6.ValidateActionConfigRequest) (*tfprotov6.ValidateActionConfigResponse, error) { + ctx = s.registerContext(ctx) + ctx = logging.InitContext(ctx) + + fwResp := &fwserver.ValidateActionConfigResponse{} + + action, diags := s.FrameworkServer.Action(ctx, proto6Req.ActionType) + + fwResp.Diagnostics.Append(diags...) + + if fwResp.Diagnostics.HasError() { + return toproto6.ValidateActionConfigResponse(ctx, fwResp), nil + } + + actionSchema, diags := s.FrameworkServer.ActionSchema(ctx, proto6Req.ActionType) + + fwResp.Diagnostics.Append(diags...) + + if fwResp.Diagnostics.HasError() { + return toproto6.ValidateActionConfigResponse(ctx, fwResp), nil + } + + fwReq, diags := fromproto6.ValidateActionConfigRequest(ctx, proto6Req, action, actionSchema) + + fwResp.Diagnostics.Append(diags...) + + if fwResp.Diagnostics.HasError() { + return toproto6.ValidateActionConfigResponse(ctx, fwResp), nil + } + + s.FrameworkServer.ValidateActionConfig(ctx, fwReq, fwResp) + + return toproto6.ValidateActionConfigResponse(ctx, fwResp), nil +} diff --git a/internal/proto6server/server_validateactionconfig_test.go b/internal/proto6server/server_validateactionconfig_test.go new file mode 100644 index 000000000..4fd7ce2d5 --- /dev/null +++ b/internal/proto6server/server_validateactionconfig_test.go @@ -0,0 +1,166 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package proto6server + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/action" + "github.com/hashicorp/terraform-plugin-framework/action/schema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testprovider" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestServerValidateActionConfig(t *testing.T) { + t.Parallel() + + testType := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.String, + }, + } + + testValue := tftypes.NewValue(testType, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.String, "test-value"), + }) + + testDynamicValue, err := tfprotov6.NewDynamicValue(testType, testValue) + + if err != nil { + t.Fatalf("unexpected error calling tfprotov6.NewDynamicValue(): %s", err) + } + + testSchema := schema.UnlinkedSchema{ + Attributes: map[string]schema.Attribute{ + "test": schema.StringAttribute{ + Required: true, + }, + }, + } + + testCases := map[string]struct { + server *Server + request *tfprotov6.ValidateActionConfigRequest + expectedError error + expectedResponse *tfprotov6.ValidateActionConfigResponse + }{ + "no-schema": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + ActionsMethod: func(_ context.Context) []func() action.Action { + return []func() action.Action{ + func() action.Action { + return &testprovider.Action{ + SchemaMethod: func(_ context.Context, _ action.SchemaRequest, resp *action.SchemaResponse) {}, + MetadataMethod: func(_ context.Context, _ action.MetadataRequest, resp *action.MetadataResponse) { + resp.TypeName = "test_action" + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov6.ValidateActionConfigRequest{ + ActionType: "test_action", + }, + expectedResponse: &tfprotov6.ValidateActionConfigResponse{}, + }, + "request-config": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + ActionsMethod: func(_ context.Context) []func() action.Action { + return []func() action.Action{ + func() action.Action { + return &testprovider.Action{ + SchemaMethod: func(_ context.Context, _ action.SchemaRequest, resp *action.SchemaResponse) { + resp.Schema = testSchema + }, + MetadataMethod: func(_ context.Context, _ action.MetadataRequest, resp *action.MetadataResponse) { + resp.TypeName = "test_action" + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov6.ValidateActionConfigRequest{ + Config: &testDynamicValue, + ActionType: "test_action", + }, + expectedResponse: &tfprotov6.ValidateActionConfigResponse{}, + }, + "response-diagnostics": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + ActionsMethod: func(_ context.Context) []func() action.Action { + return []func() action.Action{ + func() action.Action { + return &testprovider.ActionWithValidateConfig{ + Action: &testprovider.Action{ + SchemaMethod: func(_ context.Context, _ action.SchemaRequest, resp *action.SchemaResponse) { + resp.Schema = testSchema + }, + MetadataMethod: func(_ context.Context, _ action.MetadataRequest, resp *action.MetadataResponse) { + resp.TypeName = "test_action" + }, + }, + ValidateConfigMethod: func(ctx context.Context, req action.ValidateConfigRequest, resp *action.ValidateConfigResponse) { + resp.Diagnostics.AddWarning("warning summary", "warning detail") + resp.Diagnostics.AddError("error summary", "error detail") + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov6.ValidateActionConfigRequest{ + Config: &testDynamicValue, + ActionType: "test_action", + }, + expectedResponse: &tfprotov6.ValidateActionConfigResponse{ + Diagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityWarning, + Summary: "warning summary", + Detail: "warning detail", + }, + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "error summary", + Detail: "error detail", + }, + }, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.server.ValidateActionConfig(context.Background(), testCase.request) + + if diff := cmp.Diff(testCase.expectedError, err); diff != "" { + t.Errorf("unexpected error difference: %s", diff) + } + + if diff := cmp.Diff(testCase.expectedResponse, got); diff != "" { + t.Errorf("unexpected response difference: %s", diff) + } + }) + } +} diff --git a/internal/testing/testprovider/actionconfigvalidator.go b/internal/testing/testprovider/actionconfigvalidator.go new file mode 100644 index 000000000..e6d3d4ade --- /dev/null +++ b/internal/testing/testprovider/actionconfigvalidator.go @@ -0,0 +1,47 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package testprovider + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/action" +) + +var _ action.ConfigValidator = &ActionConfigValidator{} + +// Declarative action.ConfigValidator for unit testing. +type ActionConfigValidator struct { + // ActionConfigValidator interface methods + DescriptionMethod func(context.Context) string + MarkdownDescriptionMethod func(context.Context) string + ValidateActionMethod func(context.Context, action.ValidateConfigRequest, *action.ValidateConfigResponse) +} + +// Description satisfies the action.ConfigValidator interface. +func (v *ActionConfigValidator) Description(ctx context.Context) string { + if v.DescriptionMethod == nil { + return "" + } + + return v.DescriptionMethod(ctx) +} + +// MarkdownDescription satisfies the action.ConfigValidator interface. +func (v *ActionConfigValidator) MarkdownDescription(ctx context.Context) string { + if v.MarkdownDescriptionMethod == nil { + return "" + } + + return v.MarkdownDescriptionMethod(ctx) +} + +// Validate satisfies the action.ConfigValidator interface. +func (v *ActionConfigValidator) ValidateAction(ctx context.Context, req action.ValidateConfigRequest, resp *action.ValidateConfigResponse) { + if v.ValidateActionMethod == nil { + return + } + + v.ValidateActionMethod(ctx, req, resp) +} diff --git a/internal/testing/testprovider/actionwithconfigvalidators.go b/internal/testing/testprovider/actionwithconfigvalidators.go new file mode 100644 index 000000000..c3ea00233 --- /dev/null +++ b/internal/testing/testprovider/actionwithconfigvalidators.go @@ -0,0 +1,30 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package testprovider + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/action" +) + +var _ action.Action = &ActionWithConfigValidators{} +var _ action.ActionWithConfigValidators = &ActionWithConfigValidators{} + +// Declarative action.ActionWithConfigValidators for unit testing. +type ActionWithConfigValidators struct { + *Action + + // ActionWithConfigValidators interface methods + ConfigValidatorsMethod func(context.Context) []action.ConfigValidator +} + +// ConfigValidators satisfies the action.ActionWithConfigValidators interface. +func (p *ActionWithConfigValidators) ConfigValidators(ctx context.Context) []action.ConfigValidator { + if p.ConfigValidatorsMethod == nil { + return nil + } + + return p.ConfigValidatorsMethod(ctx) +} diff --git a/internal/testing/testprovider/actionwithvalidateconfig.go b/internal/testing/testprovider/actionwithvalidateconfig.go new file mode 100644 index 000000000..62fa36850 --- /dev/null +++ b/internal/testing/testprovider/actionwithvalidateconfig.go @@ -0,0 +1,30 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package testprovider + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/action" +) + +var _ action.Action = &ActionWithValidateConfig{} +var _ action.ActionWithValidateConfig = &ActionWithValidateConfig{} + +// Declarative action.ActionWithValidateConfig for unit testing. +type ActionWithValidateConfig struct { + *Action + + // ActionWithValidateConfig interface methods + ValidateConfigMethod func(context.Context, action.ValidateConfigRequest, *action.ValidateConfigResponse) +} + +// ValidateConfig satisfies the action.ActionWithValidateConfig interface. +func (p *ActionWithValidateConfig) ValidateConfig(ctx context.Context, req action.ValidateConfigRequest, resp *action.ValidateConfigResponse) { + if p.ValidateConfigMethod == nil { + return + } + + p.ValidateConfigMethod(ctx, req, resp) +} diff --git a/internal/toproto5/validateactionconfig.go b/internal/toproto5/validateactionconfig.go new file mode 100644 index 000000000..2d0258c8c --- /dev/null +++ b/internal/toproto5/validateactionconfig.go @@ -0,0 +1,25 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto5 + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" +) + +// ValidateActionConfigResponse returns the *tfprotov5.ValidateActionConfigResponse +// equivalent of a *fwserver.ValidateActionConfigResponse. +func ValidateActionConfigResponse(ctx context.Context, fw *fwserver.ValidateActionConfigResponse) *tfprotov5.ValidateActionConfigResponse { + if fw == nil { + return nil + } + + proto5 := &tfprotov5.ValidateActionConfigResponse{ + Diagnostics: Diagnostics(ctx, fw.Diagnostics), + } + + return proto5 +} diff --git a/internal/toproto5/validateactionconfig_test.go b/internal/toproto5/validateactionconfig_test.go new file mode 100644 index 000000000..3f9ec5527 --- /dev/null +++ b/internal/toproto5/validateactionconfig_test.go @@ -0,0 +1,67 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto5_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/toproto5" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" +) + +func TestValidateActionConfigResponse(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + input *fwserver.ValidateActionConfigResponse + expected *tfprotov5.ValidateActionConfigResponse + }{ + "nil": { + input: nil, + expected: nil, + }, + "empty": { + input: &fwserver.ValidateActionConfigResponse{}, + expected: &tfprotov5.ValidateActionConfigResponse{}, + }, + "diagnostics": { + input: &fwserver.ValidateActionConfigResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewWarningDiagnostic("test warning summary", "test warning details"), + diag.NewErrorDiagnostic("test error summary", "test error details"), + }, + }, + expected: &tfprotov5.ValidateActionConfigResponse{ + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityWarning, + Summary: "test warning summary", + Detail: "test warning details", + }, + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "test error summary", + Detail: "test error details", + }, + }, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := toproto5.ValidateActionConfigResponse(context.Background(), testCase.input) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/internal/toproto6/validateactionconfig.go b/internal/toproto6/validateactionconfig.go new file mode 100644 index 000000000..179086cf6 --- /dev/null +++ b/internal/toproto6/validateactionconfig.go @@ -0,0 +1,25 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto6 + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" +) + +// ValidateActionConfigResponse returns the *tfprotov6.ValidateActionConfigResponse +// equivalent of a *fwserver.ValidateActionConfigResponse. +func ValidateActionConfigResponse(ctx context.Context, fw *fwserver.ValidateActionConfigResponse) *tfprotov6.ValidateActionConfigResponse { + if fw == nil { + return nil + } + + proto6 := &tfprotov6.ValidateActionConfigResponse{ + Diagnostics: Diagnostics(ctx, fw.Diagnostics), + } + + return proto6 +} diff --git a/internal/toproto6/validateactionconfig_test.go b/internal/toproto6/validateactionconfig_test.go new file mode 100644 index 000000000..e4d3a2319 --- /dev/null +++ b/internal/toproto6/validateactionconfig_test.go @@ -0,0 +1,67 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto6_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/toproto6" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" +) + +func TestValidateActionConfigResponse(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + input *fwserver.ValidateActionConfigResponse + expected *tfprotov6.ValidateActionConfigResponse + }{ + "nil": { + input: nil, + expected: nil, + }, + "empty": { + input: &fwserver.ValidateActionConfigResponse{}, + expected: &tfprotov6.ValidateActionConfigResponse{}, + }, + "diagnostics": { + input: &fwserver.ValidateActionConfigResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewWarningDiagnostic("test warning summary", "test warning details"), + diag.NewErrorDiagnostic("test error summary", "test error details"), + }, + }, + expected: &tfprotov6.ValidateActionConfigResponse{ + Diagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityWarning, + Summary: "test warning summary", + Detail: "test warning details", + }, + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "test error summary", + Detail: "test error details", + }, + }, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := toproto6.ValidateActionConfigResponse(context.Background(), testCase.input) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/provider/config_validator.go b/provider/config_validator.go index 11d5337da..f4aaa113c 100644 --- a/provider/config_validator.go +++ b/provider/config_validator.go @@ -21,8 +21,7 @@ type ConfigValidator interface { // ValidateProvider performs the validation. // - // This method name is separate from the ConfigValidator - // interface ValidateDataSource method name and ResourceConfigValidator - // interface ValidateResource method name to allow generic validators. + // This method name is separate from ConfigValidators in resource and other packages in + // order to allow generic validators. ValidateProvider(context.Context, ValidateConfigRequest, *ValidateConfigResponse) } diff --git a/resource/config_validator.go b/resource/config_validator.go index 775ca7fe1..02af82c57 100644 --- a/resource/config_validator.go +++ b/resource/config_validator.go @@ -21,8 +21,7 @@ type ConfigValidator interface { // ValidateResource performs the validation. // - // This method name is separate from the datasource.ConfigValidator - // interface ValidateDataSource method name and provider.ConfigValidator - // interface ValidateProvider method name to allow generic validators. + // This method name is separate from ConfigValidators in datasource and other packages in + // order to allow generic validators. ValidateResource(context.Context, ValidateConfigRequest, *ValidateConfigResponse) }