diff --git a/.changelog/542.txt b/.changelog/542.txt new file mode 100644 index 000000000..afc190ca2 --- /dev/null +++ b/.changelog/542.txt @@ -0,0 +1,3 @@ +```release-note:feature +schema/validator: New package which contains type-specific schema validator interfaces +``` diff --git a/internal/fwschema/attribute.go b/internal/fwschema/attribute.go index 81cbad5fc..1de0820ef 100644 --- a/internal/fwschema/attribute.go +++ b/internal/fwschema/attribute.go @@ -74,3 +74,42 @@ type Attribute interface { // conflict with the tfsdk.Attribute field name. IsSensitive() bool } + +// AttributesEqual is a helper function to perform equality testing on two +// Attribute. Attribute Equal implementations should still compare the concrete +// types in addition to using this helper. +func AttributesEqual(a, b Attribute) bool { + if !a.GetType().Equal(b.GetType()) { + return false + } + + if a.GetDeprecationMessage() != b.GetDeprecationMessage() { + return false + } + + if a.GetDescription() != b.GetDescription() { + return false + } + + if a.GetMarkdownDescription() != b.GetMarkdownDescription() { + return false + } + + if a.IsRequired() != b.IsRequired() { + return false + } + + if a.IsOptional() != b.IsOptional() { + return false + } + + if a.IsComputed() != b.IsComputed() { + return false + } + + if a.IsSensitive() != b.IsSensitive() { + return false + } + + return true +} diff --git a/internal/fwschema/block.go b/internal/fwschema/block.go index 0ee4e8672..1841a8785 100644 --- a/internal/fwschema/block.go +++ b/internal/fwschema/block.go @@ -65,3 +65,34 @@ type Block interface { // Type should return the framework type of a block. Type() attr.Type } + +// BlocksEqual is a helper function to perform equality testing on two +// Block. Attribute Equal implementations should still compare the concrete +// types in addition to using this helper. +func BlocksEqual(a, b Block) bool { + if !a.Type().Equal(b.Type()) { + return false + } + + if a.GetDeprecationMessage() != b.GetDeprecationMessage() { + return false + } + + if a.GetDescription() != b.GetDescription() { + return false + } + + if a.GetMarkdownDescription() != b.GetMarkdownDescription() { + return false + } + + if a.GetMaxItems() != b.GetMaxItems() { + return false + } + + if a.GetMinItems() != b.GetMinItems() { + return false + } + + return true +} diff --git a/internal/fwschema/fwxschema/attribute_validation.go b/internal/fwschema/fwxschema/attribute_validation.go index f75b76773..ff04d0b99 100644 --- a/internal/fwschema/fwxschema/attribute_validation.go +++ b/internal/fwschema/fwxschema/attribute_validation.go @@ -2,6 +2,7 @@ package fwxschema import ( "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/tfsdk" ) @@ -17,3 +18,84 @@ type AttributeWithValidators interface { // tfsdk.Attribute field name. GetValidators() []tfsdk.AttributeValidator } + +// AttributeWithBoolValidators is an optional interface on Attribute which +// enables Bool validation support. +type AttributeWithBoolValidators interface { + fwschema.Attribute + + // BoolValidators should return a list of Bool validators. + BoolValidators() []validator.Bool +} + +// AttributeWithFloat64Validators is an optional interface on Attribute which +// enables Float64 validation support. +type AttributeWithFloat64Validators interface { + fwschema.Attribute + + // Float64Validators should return a list of Float64 validators. + Float64Validators() []validator.Float64 +} + +// AttributeWithInt64Validators is an optional interface on Attribute which +// enables Int64 validation support. +type AttributeWithInt64Validators interface { + fwschema.Attribute + + // Int64Validators should return a list of Int64 validators. + Int64Validators() []validator.Int64 +} + +// AttributeWithListValidators is an optional interface on Attribute which +// enables List validation support. +type AttributeWithListValidators interface { + fwschema.Attribute + + // ListValidators should return a list of List validators. + ListValidators() []validator.List +} + +// AttributeWithMapValidators is an optional interface on Attribute which +// enables Map validation support. +type AttributeWithMapValidators interface { + fwschema.Attribute + + // MapValidators should return a list of Map validators. + MapValidators() []validator.Map +} + +// AttributeWithNumberValidators is an optional interface on Attribute which +// enables Number validation support. +type AttributeWithNumberValidators interface { + fwschema.Attribute + + // NumberValidators should return a list of Number validators. + NumberValidators() []validator.Number +} + +// AttributeWithObjectValidators is an optional interface on Attribute which +// enables Object validation support. +type AttributeWithObjectValidators interface { + fwschema.Attribute + + // ObjectValidators should return a list of Object validators. + ObjectValidators() []validator.Object +} + +// AttributeWithSetValidators is an optional interface on Attribute which +// enables Set validation support. +type AttributeWithSetValidators interface { + fwschema.Attribute + + // SetValidators should return a list of Set validators. + SetValidators() []validator.Set +} + +// AttributeWithStringValidators is an optional interface on Attribute which +// enables String validation support. +type AttributeWithStringValidators interface { + fwschema.Attribute + + // StringValidators should return a list of String validators. + StringValidators() []validator.String +} diff --git a/internal/fwschema/fwxschema/block_validation.go b/internal/fwschema/fwxschema/block_validation.go index 7dfd8a250..a967468e6 100644 --- a/internal/fwschema/fwxschema/block_validation.go +++ b/internal/fwschema/fwxschema/block_validation.go @@ -2,6 +2,7 @@ package fwxschema import ( "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/tfsdk" ) @@ -17,3 +18,30 @@ type BlockWithValidators interface { // tfsdk.Block field name. GetValidators() []tfsdk.AttributeValidator } + +// BlockWithListValidators is an optional interface on Block which +// enables List validation support. +type BlockWithListValidators interface { + fwschema.Block + + // ListValidators should return a list of List validators. + ListValidators() []validator.List +} + +// BlockWithObjectValidators is an optional interface on Block which +// enables Object validation support. +type BlockWithObjectValidators interface { + fwschema.Block + + // ObjectValidators should return a list of Object validators. + ObjectValidators() []validator.Object +} + +// BlockWithSetValidators is an optional interface on Block which +// enables Set validation support. +type BlockWithSetValidators interface { + fwschema.Block + + // SetValidators should return a list of Set validators. + SetValidators() []validator.Set +} diff --git a/internal/fwserver/attribute_validation.go b/internal/fwserver/attribute_validation.go index 8639ea188..c8f9a6328 100644 --- a/internal/fwserver/attribute_validation.go +++ b/internal/fwserver/attribute_validation.go @@ -8,6 +8,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschemadata" "github.com/hashicorp/terraform-plugin-framework/internal/logging" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" ) @@ -94,7 +95,9 @@ func AttributeValidate(ctx context.Context, a fwschema.Attribute, req tfsdk.Vali req.AttributeConfig = attributeConfig - if attributeWithValidators, ok := a.(fwxschema.AttributeWithValidators); ok { + switch attributeWithValidators := a.(type) { + // Legacy tfsdk.AttributeValidator handling + case fwxschema.AttributeWithValidators: for _, validator := range attributeWithValidators.GetValidators() { logging.FrameworkDebug( ctx, @@ -112,6 +115,24 @@ func AttributeValidate(ctx context.Context, a fwschema.Attribute, req tfsdk.Vali }, ) } + case fwxschema.AttributeWithBoolValidators: + AttributeValidateBool(ctx, attributeWithValidators, req, resp) + case fwxschema.AttributeWithFloat64Validators: + AttributeValidateFloat64(ctx, attributeWithValidators, req, resp) + case fwxschema.AttributeWithInt64Validators: + AttributeValidateInt64(ctx, attributeWithValidators, req, resp) + case fwxschema.AttributeWithListValidators: + AttributeValidateList(ctx, attributeWithValidators, req, resp) + case fwxschema.AttributeWithMapValidators: + AttributeValidateMap(ctx, attributeWithValidators, req, resp) + case fwxschema.AttributeWithNumberValidators: + AttributeValidateNumber(ctx, attributeWithValidators, req, resp) + case fwxschema.AttributeWithObjectValidators: + AttributeValidateObject(ctx, attributeWithValidators, req, resp) + case fwxschema.AttributeWithSetValidators: + AttributeValidateSet(ctx, attributeWithValidators, req, resp) + case fwxschema.AttributeWithStringValidators: + AttributeValidateString(ctx, attributeWithValidators, req, resp) } AttributeValidateNestedAttributes(ctx, a, req, resp) @@ -126,6 +147,591 @@ func AttributeValidate(ctx context.Context, a fwschema.Attribute, req tfsdk.Vali } } +// AttributeValidateBool performs all types.Bool validation. +func AttributeValidateBool(ctx context.Context, attribute fwxschema.AttributeWithBoolValidators, req tfsdk.ValidateAttributeRequest, resp *tfsdk.ValidateAttributeResponse) { + // Use types.BoolValuable until custom types cannot re-implement + // ValueFromTerraform. Until then, custom types are not technically + // required to implement this interface. This opts to enforce the + // requirement before compatibility promises would interfere. + configValuable, ok := req.AttributeConfig.(types.BoolValuable) + + if !ok { + resp.Diagnostics.AddAttributeError( + req.AttributePath, + "Invalid Bool Attribute Validator Value Type", + "An unexpected value type was encountered while attempting to perform Bool attribute validation. "+ + "The value type must implement the types.BoolValuable interface. "+ + "Please report this to the provider developers.\n\n"+ + fmt.Sprintf("Incoming Value Type: %T", req.AttributeConfig), + ) + + return + } + + configValue, diags := configValuable.ToBoolValue(ctx) + + resp.Diagnostics.Append(diags...) + + // Only return early on new errors as the resp.Diagnostics may have errors + // from other attributes. + if diags.HasError() { + return + } + + validateReq := validator.BoolRequest{ + Config: req.Config, + ConfigValue: configValue, + Path: req.AttributePath, + PathExpression: req.AttributePathExpression, + } + + for _, attributeValidator := range attribute.BoolValidators() { + // Instantiate a new response for each request to prevent validators + // from modifying or removing diagnostics. + validateResp := &validator.BoolResponse{} + + logging.FrameworkDebug( + ctx, + "Calling provider defined validator.Bool", + map[string]interface{}{ + logging.KeyDescription: attributeValidator.Description(ctx), + }, + ) + + attributeValidator.ValidateBool(ctx, validateReq, validateResp) + + logging.FrameworkDebug( + ctx, + "Called provider defined validator.Bool", + map[string]interface{}{ + logging.KeyDescription: attributeValidator.Description(ctx), + }, + ) + + resp.Diagnostics.Append(validateResp.Diagnostics...) + } +} + +// AttributeValidateFloat64 performs all types.Float64 validation. +func AttributeValidateFloat64(ctx context.Context, attribute fwxschema.AttributeWithFloat64Validators, req tfsdk.ValidateAttributeRequest, resp *tfsdk.ValidateAttributeResponse) { + // Use types.Float64Valuable until custom types cannot re-implement + // ValueFromTerraform. Until then, custom types are not technically + // required to implement this interface. This opts to enforce the + // requirement before compatibility promises would interfere. + configValuable, ok := req.AttributeConfig.(types.Float64Valuable) + + if !ok { + resp.Diagnostics.AddAttributeError( + req.AttributePath, + "Invalid Float64 Attribute Validator Value Type", + "An unexpected value type was encountered while attempting to perform Float64 attribute validation. "+ + "The value type must implement the types.Float64Valuable interface. "+ + "Please report this to the provider developers.\n\n"+ + fmt.Sprintf("Incoming Value Type: %T", req.AttributeConfig), + ) + + return + } + + configValue, diags := configValuable.ToFloat64Value(ctx) + + resp.Diagnostics.Append(diags...) + + // Only return early on new errors as the resp.Diagnostics may have errors + // from other attributes. + if diags.HasError() { + return + } + + validateReq := validator.Float64Request{ + Config: req.Config, + ConfigValue: configValue, + Path: req.AttributePath, + PathExpression: req.AttributePathExpression, + } + + for _, attributeValidator := range attribute.Float64Validators() { + // Instantiate a new response for each request to prevent validators + // from modifying or removing diagnostics. + validateResp := &validator.Float64Response{} + + logging.FrameworkDebug( + ctx, + "Calling provider defined validator.Float64", + map[string]interface{}{ + logging.KeyDescription: attributeValidator.Description(ctx), + }, + ) + + attributeValidator.ValidateFloat64(ctx, validateReq, validateResp) + + logging.FrameworkDebug( + ctx, + "Called provider defined validator.Float64", + map[string]interface{}{ + logging.KeyDescription: attributeValidator.Description(ctx), + }, + ) + + resp.Diagnostics.Append(validateResp.Diagnostics...) + } +} + +// AttributeValidateInt64 performs all types.Int64 validation. +func AttributeValidateInt64(ctx context.Context, attribute fwxschema.AttributeWithInt64Validators, req tfsdk.ValidateAttributeRequest, resp *tfsdk.ValidateAttributeResponse) { + // Use types.Int64Valuable until custom types cannot re-implement + // ValueFromTerraform. Until then, custom types are not technically + // required to implement this interface. This opts to enforce the + // requirement before compatibility promises would interfere. + configValuable, ok := req.AttributeConfig.(types.Int64Valuable) + + if !ok { + resp.Diagnostics.AddAttributeError( + req.AttributePath, + "Invalid Int64 Attribute Validator Value Type", + "An unexpected value type was encountered while attempting to perform Int64 attribute validation. "+ + "The value type must implement the types.Int64Valuable interface. "+ + "Please report this to the provider developers.\n\n"+ + fmt.Sprintf("Incoming Value Type: %T", req.AttributeConfig), + ) + + return + } + + configValue, diags := configValuable.ToInt64Value(ctx) + + resp.Diagnostics.Append(diags...) + + // Only return early on new errors as the resp.Diagnostics may have errors + // from other attributes. + if diags.HasError() { + return + } + + validateReq := validator.Int64Request{ + Config: req.Config, + ConfigValue: configValue, + Path: req.AttributePath, + PathExpression: req.AttributePathExpression, + } + + for _, attributeValidator := range attribute.Int64Validators() { + // Instantiate a new response for each request to prevent validators + // from modifying or removing diagnostics. + validateResp := &validator.Int64Response{} + + logging.FrameworkDebug( + ctx, + "Calling provider defined validator.Int64", + map[string]interface{}{ + logging.KeyDescription: attributeValidator.Description(ctx), + }, + ) + + attributeValidator.ValidateInt64(ctx, validateReq, validateResp) + + logging.FrameworkDebug( + ctx, + "Called provider defined validator.Int64", + map[string]interface{}{ + logging.KeyDescription: attributeValidator.Description(ctx), + }, + ) + + resp.Diagnostics.Append(validateResp.Diagnostics...) + } +} + +// AttributeValidateList performs all types.List validation. +func AttributeValidateList(ctx context.Context, attribute fwxschema.AttributeWithListValidators, req tfsdk.ValidateAttributeRequest, resp *tfsdk.ValidateAttributeResponse) { + // Use types.ListValuable until custom types cannot re-implement + // ValueFromTerraform. Until then, custom types are not technically + // required to implement this interface. This opts to enforce the + // requirement before compatibility promises would interfere. + configValuable, ok := req.AttributeConfig.(types.ListValuable) + + if !ok { + resp.Diagnostics.AddAttributeError( + req.AttributePath, + "Invalid List Attribute Validator Value Type", + "An unexpected value type was encountered while attempting to perform List attribute validation. "+ + "The value type must implement the types.ListValuable interface. "+ + "Please report this to the provider developers.\n\n"+ + fmt.Sprintf("Incoming Value Type: %T", req.AttributeConfig), + ) + + return + } + + configValue, diags := configValuable.ToListValue(ctx) + + resp.Diagnostics.Append(diags...) + + // Only return early on new errors as the resp.Diagnostics may have errors + // from other attributes. + if diags.HasError() { + return + } + + validateReq := validator.ListRequest{ + Config: req.Config, + ConfigValue: configValue, + Path: req.AttributePath, + PathExpression: req.AttributePathExpression, + } + + for _, attributeValidator := range attribute.ListValidators() { + // Instantiate a new response for each request to prevent validators + // from modifying or removing diagnostics. + validateResp := &validator.ListResponse{} + + logging.FrameworkDebug( + ctx, + "Calling provider defined validator.List", + map[string]interface{}{ + logging.KeyDescription: attributeValidator.Description(ctx), + }, + ) + + attributeValidator.ValidateList(ctx, validateReq, validateResp) + + logging.FrameworkDebug( + ctx, + "Called provider defined validator.List", + map[string]interface{}{ + logging.KeyDescription: attributeValidator.Description(ctx), + }, + ) + + resp.Diagnostics.Append(validateResp.Diagnostics...) + } +} + +// AttributeValidateMap performs all types.Map validation. +func AttributeValidateMap(ctx context.Context, attribute fwxschema.AttributeWithMapValidators, req tfsdk.ValidateAttributeRequest, resp *tfsdk.ValidateAttributeResponse) { + // Use types.MapValuable until custom types cannot re-implement + // ValueFromTerraform. Until then, custom types are not technically + // required to implement this interface. This opts to enforce the + // requirement before compatibility promises would interfere. + configValuable, ok := req.AttributeConfig.(types.MapValuable) + + if !ok { + resp.Diagnostics.AddAttributeError( + req.AttributePath, + "Invalid Map Attribute Validator Value Type", + "An unexpected value type was encountered while attempting to perform Map attribute validation. "+ + "The value type must implement the types.MapValuable interface. "+ + "Please report this to the provider developers.\n\n"+ + fmt.Sprintf("Incoming Value Type: %T", req.AttributeConfig), + ) + + return + } + + configValue, diags := configValuable.ToMapValue(ctx) + + resp.Diagnostics.Append(diags...) + + // Only return early on new errors as the resp.Diagnostics may have errors + // from other attributes. + if diags.HasError() { + return + } + + validateReq := validator.MapRequest{ + Config: req.Config, + ConfigValue: configValue, + Path: req.AttributePath, + PathExpression: req.AttributePathExpression, + } + + for _, attributeValidator := range attribute.MapValidators() { + // Instantiate a new response for each request to prevent validators + // from modifying or removing diagnostics. + validateResp := &validator.MapResponse{} + + logging.FrameworkDebug( + ctx, + "Calling provider defined validator.Map", + map[string]interface{}{ + logging.KeyDescription: attributeValidator.Description(ctx), + }, + ) + + attributeValidator.ValidateMap(ctx, validateReq, validateResp) + + logging.FrameworkDebug( + ctx, + "Called provider defined validator.Map", + map[string]interface{}{ + logging.KeyDescription: attributeValidator.Description(ctx), + }, + ) + + resp.Diagnostics.Append(validateResp.Diagnostics...) + } +} + +// AttributeValidateNumber performs all types.Number validation. +func AttributeValidateNumber(ctx context.Context, attribute fwxschema.AttributeWithNumberValidators, req tfsdk.ValidateAttributeRequest, resp *tfsdk.ValidateAttributeResponse) { + // Use types.NumberValuable until custom types cannot re-implement + // ValueFromTerraform. Until then, custom types are not technically + // required to implement this interface. This opts to enforce the + // requirement before compatibility promises would interfere. + configValuable, ok := req.AttributeConfig.(types.NumberValuable) + + if !ok { + resp.Diagnostics.AddAttributeError( + req.AttributePath, + "Invalid Number Attribute Validator Value Type", + "An unexpected value type was encountered while attempting to perform Number attribute validation. "+ + "The value type must implement the types.NumberValuable interface. "+ + "Please report this to the provider developers.\n\n"+ + fmt.Sprintf("Incoming Value Type: %T", req.AttributeConfig), + ) + + return + } + + configValue, diags := configValuable.ToNumberValue(ctx) + + resp.Diagnostics.Append(diags...) + + // Only return early on new errors as the resp.Diagnostics may have errors + // from other attributes. + if diags.HasError() { + return + } + + validateReq := validator.NumberRequest{ + Config: req.Config, + ConfigValue: configValue, + Path: req.AttributePath, + PathExpression: req.AttributePathExpression, + } + + for _, attributeValidator := range attribute.NumberValidators() { + // Instantiate a new response for each request to prevent validators + // from modifying or removing diagnostics. + validateResp := &validator.NumberResponse{} + + logging.FrameworkDebug( + ctx, + "Calling provider defined validator.Number", + map[string]interface{}{ + logging.KeyDescription: attributeValidator.Description(ctx), + }, + ) + + attributeValidator.ValidateNumber(ctx, validateReq, validateResp) + + logging.FrameworkDebug( + ctx, + "Called provider defined validator.Number", + map[string]interface{}{ + logging.KeyDescription: attributeValidator.Description(ctx), + }, + ) + + resp.Diagnostics.Append(validateResp.Diagnostics...) + } +} + +// AttributeValidateObject performs all types.Object validation. +func AttributeValidateObject(ctx context.Context, attribute fwxschema.AttributeWithObjectValidators, req tfsdk.ValidateAttributeRequest, resp *tfsdk.ValidateAttributeResponse) { + // Use types.ObjectValuable until custom types cannot re-implement + // ValueFromTerraform. Until then, custom types are not technically + // required to implement this interface. This opts to enforce the + // requirement before compatibility promises would interfere. + configValuable, ok := req.AttributeConfig.(types.ObjectValuable) + + if !ok { + resp.Diagnostics.AddAttributeError( + req.AttributePath, + "Invalid Object Attribute Validator Value Type", + "An unexpected value type was encountered while attempting to perform Object attribute validation. "+ + "The value type must implement the types.ObjectValuable interface. "+ + "Please report this to the provider developers.\n\n"+ + fmt.Sprintf("Incoming Value Type: %T", req.AttributeConfig), + ) + + return + } + + configValue, diags := configValuable.ToObjectValue(ctx) + + resp.Diagnostics.Append(diags...) + + // Only return early on new errors as the resp.Diagnostics may have errors + // from other attributes. + if diags.HasError() { + return + } + + validateReq := validator.ObjectRequest{ + Config: req.Config, + ConfigValue: configValue, + Path: req.AttributePath, + PathExpression: req.AttributePathExpression, + } + + for _, attributeValidator := range attribute.ObjectValidators() { + // Instantiate a new response for each request to prevent validators + // from modifying or removing diagnostics. + validateResp := &validator.ObjectResponse{} + + logging.FrameworkDebug( + ctx, + "Calling provider defined validator.Object", + map[string]interface{}{ + logging.KeyDescription: attributeValidator.Description(ctx), + }, + ) + + attributeValidator.ValidateObject(ctx, validateReq, validateResp) + + logging.FrameworkDebug( + ctx, + "Called provider defined validator.Object", + map[string]interface{}{ + logging.KeyDescription: attributeValidator.Description(ctx), + }, + ) + + resp.Diagnostics.Append(validateResp.Diagnostics...) + } +} + +// AttributeValidateSet performs all types.Set validation. +func AttributeValidateSet(ctx context.Context, attribute fwxschema.AttributeWithSetValidators, req tfsdk.ValidateAttributeRequest, resp *tfsdk.ValidateAttributeResponse) { + // Use types.SetValuable until custom types cannot re-implement + // ValueFromTerraform. Until then, custom types are not technically + // required to implement this interface. This opts to enforce the + // requirement before compatibility promises would interfere. + configValuable, ok := req.AttributeConfig.(types.SetValuable) + + if !ok { + resp.Diagnostics.AddAttributeError( + req.AttributePath, + "Invalid Set Attribute Validator Value Type", + "An unexpected value type was encountered while attempting to perform Set attribute validation. "+ + "The value type must implement the types.SetValuable interface. "+ + "Please report this to the provider developers.\n\n"+ + fmt.Sprintf("Incoming Value Type: %T", req.AttributeConfig), + ) + + return + } + + configValue, diags := configValuable.ToSetValue(ctx) + + resp.Diagnostics.Append(diags...) + + // Only return early on new errors as the resp.Diagnostics may have errors + // from other attributes. + if diags.HasError() { + return + } + + validateReq := validator.SetRequest{ + Config: req.Config, + ConfigValue: configValue, + Path: req.AttributePath, + PathExpression: req.AttributePathExpression, + } + + for _, attributeValidator := range attribute.SetValidators() { + // Instantiate a new response for each request to prevent validators + // from modifying or removing diagnostics. + validateResp := &validator.SetResponse{} + + logging.FrameworkDebug( + ctx, + "Calling provider defined validator.Set", + map[string]interface{}{ + logging.KeyDescription: attributeValidator.Description(ctx), + }, + ) + + attributeValidator.ValidateSet(ctx, validateReq, validateResp) + + logging.FrameworkDebug( + ctx, + "Called provider defined validator.Set", + map[string]interface{}{ + logging.KeyDescription: attributeValidator.Description(ctx), + }, + ) + + resp.Diagnostics.Append(validateResp.Diagnostics...) + } +} + +// AttributeValidateString performs all types.String validation. +func AttributeValidateString(ctx context.Context, attribute fwxschema.AttributeWithStringValidators, req tfsdk.ValidateAttributeRequest, resp *tfsdk.ValidateAttributeResponse) { + // Use types.StringValuable until custom types cannot re-implement + // ValueFromTerraform. Until then, custom types are not technically + // required to implement this interface. This opts to enforce the + // requirement before compatibility promises would interfere. + configValuable, ok := req.AttributeConfig.(types.StringValuable) + + if !ok { + resp.Diagnostics.AddAttributeError( + req.AttributePath, + "Invalid String Attribute Validator Value Type", + "An unexpected value type was encountered while attempting to perform String attribute validation. "+ + "The value type must implement the types.StringValuable interface. "+ + "Please report this to the provider developers.\n\n"+ + fmt.Sprintf("Incoming Value Type: %T", req.AttributeConfig), + ) + + return + } + + configValue, diags := configValuable.ToStringValue(ctx) + + resp.Diagnostics.Append(diags...) + + // Only return early on new errors as the resp.Diagnostics may have errors + // from other attributes. + if diags.HasError() { + return + } + + validateReq := validator.StringRequest{ + Config: req.Config, + ConfigValue: configValue, + Path: req.AttributePath, + PathExpression: req.AttributePathExpression, + } + + for _, attributeValidator := range attribute.StringValidators() { + // Instantiate a new response for each request to prevent validators + // from modifying or removing diagnostics. + validateResp := &validator.StringResponse{} + + logging.FrameworkDebug( + ctx, + "Calling provider defined validator.String", + map[string]interface{}{ + logging.KeyDescription: attributeValidator.Description(ctx), + }, + ) + + attributeValidator.ValidateString(ctx, validateReq, validateResp) + + logging.FrameworkDebug( + ctx, + "Called provider defined validator.String", + map[string]interface{}{ + logging.KeyDescription: attributeValidator.Description(ctx), + }, + ) + + resp.Diagnostics.Append(validateResp.Diagnostics...) + } +} + // AttributeValidateNestedAttributes performs all nested Attributes validation. // // TODO: Clean up this abstraction back into an internal Attribute type method. diff --git a/internal/fwserver/attribute_validation_test.go b/internal/fwserver/attribute_validation_test.go index 2ad7f03d2..8dbfc9431 100644 --- a/internal/fwserver/attribute_validation_test.go +++ b/internal/fwserver/attribute_validation_test.go @@ -2,14 +2,21 @@ package fwserver import ( "context" + "fmt" + "math/big" "testing" "github.com/google/go-cmp/cmp" "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testvalidator" testtypes "github.com/hashicorp/terraform-plugin-framework/internal/testing/types" "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" ) @@ -1564,6 +1571,1948 @@ func TestAttributeValidate(t *testing.T) { } } +func TestAttributeValidateBool(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute fwxschema.AttributeWithBoolValidators + request tfsdk.ValidateAttributeRequest + response *tfsdk.ValidateAttributeResponse + expected *tfsdk.ValidateAttributeResponse + }{ + "request-path": { + attribute: testschema.AttributeWithBoolValidators{ + Validators: []validator.Bool{ + testvalidator.Bool{ + ValidateBoolMethod: func(ctx context.Context, req validator.BoolRequest, resp *validator.BoolResponse) { + got := req.Path + expected := path.Root("test") + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected BoolRequest.Path", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: tfsdk.ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.BoolValue(true), + }, + response: &tfsdk.ValidateAttributeResponse{}, + expected: &tfsdk.ValidateAttributeResponse{}, + }, + "request-pathexpression": { + attribute: testschema.AttributeWithBoolValidators{ + Validators: []validator.Bool{ + testvalidator.Bool{ + ValidateBoolMethod: func(ctx context.Context, req validator.BoolRequest, resp *validator.BoolResponse) { + got := req.PathExpression + expected := path.MatchRoot("test") + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected BoolRequest.PathExpression", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: tfsdk.ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), + AttributeConfig: types.BoolValue(true), + }, + response: &tfsdk.ValidateAttributeResponse{}, + expected: &tfsdk.ValidateAttributeResponse{}, + }, + "request-config": { + attribute: testschema.AttributeWithBoolValidators{ + Validators: []validator.Bool{ + testvalidator.Bool{ + ValidateBoolMethod: func(ctx context.Context, req validator.BoolRequest, resp *validator.BoolResponse) { + got := req.Config + expected := tfsdk.Config{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Bool, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.Bool, true), + }, + ), + } + + if !got.Raw.Equal(expected.Raw) { + resp.Diagnostics.AddError( + "Unexpected BoolRequest.Config", + fmt.Sprintf("expected %s, got: %s", expected.Raw, got.Raw), + ) + } + }, + }, + }, + }, + request: tfsdk.ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.BoolValue(true), + Config: tfsdk.Config{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Bool, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.Bool, true), + }, + ), + }, + }, + response: &tfsdk.ValidateAttributeResponse{}, + expected: &tfsdk.ValidateAttributeResponse{}, + }, + "request-configvalue": { + attribute: testschema.AttributeWithBoolValidators{ + Validators: []validator.Bool{ + testvalidator.Bool{ + ValidateBoolMethod: func(ctx context.Context, req validator.BoolRequest, resp *validator.BoolResponse) { + got := req.ConfigValue + expected := types.BoolValue(true) + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected BoolRequest.ConfigValue", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: tfsdk.ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.BoolValue(true), + }, + response: &tfsdk.ValidateAttributeResponse{}, + expected: &tfsdk.ValidateAttributeResponse{}, + }, + "response-diagnostics": { + attribute: testschema.AttributeWithBoolValidators{ + Validators: []validator.Bool{ + testvalidator.Bool{ + ValidateBoolMethod: func(ctx context.Context, req validator.BoolRequest, resp *validator.BoolResponse) { + resp.Diagnostics.AddAttributeWarning(req.Path, "New Warning Summary", "New Warning Details") + resp.Diagnostics.AddAttributeError(req.Path, "New Error Summary", "New Error Details") + }, + }, + }, + }, + request: tfsdk.ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.BoolValue(true), + }, + response: &tfsdk.ValidateAttributeResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewAttributeWarningDiagnostic( + path.Root("other"), + "Existing Warning Summary", + "Existing Warning Details", + ), + diag.NewAttributeErrorDiagnostic( + path.Root("other"), + "Existing Error Summary", + "Existing Error Details", + ), + }, + }, + expected: &tfsdk.ValidateAttributeResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewAttributeWarningDiagnostic( + path.Root("other"), + "Existing Warning Summary", + "Existing Warning Details", + ), + diag.NewAttributeErrorDiagnostic( + path.Root("other"), + "Existing Error Summary", + "Existing Error Details", + ), + diag.NewAttributeWarningDiagnostic( + path.Root("test"), + "New Warning Summary", + "New Warning Details", + ), + diag.NewAttributeErrorDiagnostic( + path.Root("test"), + "New Error Summary", + "New Error Details", + ), + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + AttributeValidateBool(context.Background(), testCase.attribute, testCase.request, testCase.response) + + if diff := cmp.Diff(testCase.response, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestAttributeValidateFloat64(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute fwxschema.AttributeWithFloat64Validators + request tfsdk.ValidateAttributeRequest + response *tfsdk.ValidateAttributeResponse + expected *tfsdk.ValidateAttributeResponse + }{ + "request-path": { + attribute: testschema.AttributeWithFloat64Validators{ + Validators: []validator.Float64{ + testvalidator.Float64{ + ValidateFloat64Method: func(ctx context.Context, req validator.Float64Request, resp *validator.Float64Response) { + got := req.Path + expected := path.Root("test") + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected Float64Request.Path", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: tfsdk.ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.Float64Value(1.2), + }, + response: &tfsdk.ValidateAttributeResponse{}, + expected: &tfsdk.ValidateAttributeResponse{}, + }, + "request-pathexpression": { + attribute: testschema.AttributeWithFloat64Validators{ + Validators: []validator.Float64{ + testvalidator.Float64{ + ValidateFloat64Method: func(ctx context.Context, req validator.Float64Request, resp *validator.Float64Response) { + got := req.PathExpression + expected := path.MatchRoot("test") + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected Float64Request.PathExpression", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: tfsdk.ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), + AttributeConfig: types.Float64Value(1.2), + }, + response: &tfsdk.ValidateAttributeResponse{}, + expected: &tfsdk.ValidateAttributeResponse{}, + }, + "request-config": { + attribute: testschema.AttributeWithFloat64Validators{ + Validators: []validator.Float64{ + testvalidator.Float64{ + ValidateFloat64Method: func(ctx context.Context, req validator.Float64Request, resp *validator.Float64Response) { + got := req.Config + expected := tfsdk.Config{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.Number, 1.2), + }, + ), + } + + if !got.Raw.Equal(expected.Raw) { + resp.Diagnostics.AddError( + "Unexpected Float64Request.Config", + fmt.Sprintf("expected %s, got: %s", expected.Raw, got.Raw), + ) + } + }, + }, + }, + }, + request: tfsdk.ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.Float64Value(1.2), + Config: tfsdk.Config{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.Number, 1.2), + }, + ), + }, + }, + response: &tfsdk.ValidateAttributeResponse{}, + expected: &tfsdk.ValidateAttributeResponse{}, + }, + "request-configvalue": { + attribute: testschema.AttributeWithFloat64Validators{ + Validators: []validator.Float64{ + testvalidator.Float64{ + ValidateFloat64Method: func(ctx context.Context, req validator.Float64Request, resp *validator.Float64Response) { + got := req.ConfigValue + expected := types.Float64Value(1.2) + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected Float64Request.ConfigValue", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: tfsdk.ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.Float64Value(1.2), + }, + response: &tfsdk.ValidateAttributeResponse{}, + expected: &tfsdk.ValidateAttributeResponse{}, + }, + "response-diagnostics": { + attribute: testschema.AttributeWithFloat64Validators{ + Validators: []validator.Float64{ + testvalidator.Float64{ + ValidateFloat64Method: func(ctx context.Context, req validator.Float64Request, resp *validator.Float64Response) { + resp.Diagnostics.AddAttributeWarning(req.Path, "New Warning Summary", "New Warning Details") + resp.Diagnostics.AddAttributeError(req.Path, "New Error Summary", "New Error Details") + }, + }, + }, + }, + request: tfsdk.ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.Float64Value(1.2), + }, + response: &tfsdk.ValidateAttributeResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewAttributeWarningDiagnostic( + path.Root("other"), + "Existing Warning Summary", + "Existing Warning Details", + ), + diag.NewAttributeErrorDiagnostic( + path.Root("other"), + "Existing Error Summary", + "Existing Error Details", + ), + }, + }, + expected: &tfsdk.ValidateAttributeResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewAttributeWarningDiagnostic( + path.Root("other"), + "Existing Warning Summary", + "Existing Warning Details", + ), + diag.NewAttributeErrorDiagnostic( + path.Root("other"), + "Existing Error Summary", + "Existing Error Details", + ), + diag.NewAttributeWarningDiagnostic( + path.Root("test"), + "New Warning Summary", + "New Warning Details", + ), + diag.NewAttributeErrorDiagnostic( + path.Root("test"), + "New Error Summary", + "New Error Details", + ), + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + AttributeValidateFloat64(context.Background(), testCase.attribute, testCase.request, testCase.response) + + if diff := cmp.Diff(testCase.response, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestAttributeValidateInt64(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute fwxschema.AttributeWithInt64Validators + request tfsdk.ValidateAttributeRequest + response *tfsdk.ValidateAttributeResponse + expected *tfsdk.ValidateAttributeResponse + }{ + "request-path": { + attribute: testschema.AttributeWithInt64Validators{ + Validators: []validator.Int64{ + testvalidator.Int64{ + ValidateInt64Method: func(ctx context.Context, req validator.Int64Request, resp *validator.Int64Response) { + got := req.Path + expected := path.Root("test") + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected Int64Request.Path", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: tfsdk.ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.Int64Value(123), + }, + response: &tfsdk.ValidateAttributeResponse{}, + expected: &tfsdk.ValidateAttributeResponse{}, + }, + "request-pathexpression": { + attribute: testschema.AttributeWithInt64Validators{ + Validators: []validator.Int64{ + testvalidator.Int64{ + ValidateInt64Method: func(ctx context.Context, req validator.Int64Request, resp *validator.Int64Response) { + got := req.PathExpression + expected := path.MatchRoot("test") + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected Int64Request.PathExpression", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: tfsdk.ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), + AttributeConfig: types.Int64Value(123), + }, + response: &tfsdk.ValidateAttributeResponse{}, + expected: &tfsdk.ValidateAttributeResponse{}, + }, + "request-config": { + attribute: testschema.AttributeWithInt64Validators{ + Validators: []validator.Int64{ + testvalidator.Int64{ + ValidateInt64Method: func(ctx context.Context, req validator.Int64Request, resp *validator.Int64Response) { + got := req.Config + expected := tfsdk.Config{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.Number, 123), + }, + ), + } + + if !got.Raw.Equal(expected.Raw) { + resp.Diagnostics.AddError( + "Unexpected Int64Request.Config", + fmt.Sprintf("expected %s, got: %s", expected.Raw, got.Raw), + ) + } + }, + }, + }, + }, + request: tfsdk.ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.Int64Value(123), + Config: tfsdk.Config{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.Number, 123), + }, + ), + }, + }, + response: &tfsdk.ValidateAttributeResponse{}, + expected: &tfsdk.ValidateAttributeResponse{}, + }, + "request-configvalue": { + attribute: testschema.AttributeWithInt64Validators{ + Validators: []validator.Int64{ + testvalidator.Int64{ + ValidateInt64Method: func(ctx context.Context, req validator.Int64Request, resp *validator.Int64Response) { + got := req.ConfigValue + expected := types.Int64Value(123) + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected Int64Request.ConfigValue", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: tfsdk.ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.Int64Value(123), + }, + response: &tfsdk.ValidateAttributeResponse{}, + expected: &tfsdk.ValidateAttributeResponse{}, + }, + "response-diagnostics": { + attribute: testschema.AttributeWithInt64Validators{ + Validators: []validator.Int64{ + testvalidator.Int64{ + ValidateInt64Method: func(ctx context.Context, req validator.Int64Request, resp *validator.Int64Response) { + resp.Diagnostics.AddAttributeWarning(req.Path, "New Warning Summary", "New Warning Details") + resp.Diagnostics.AddAttributeError(req.Path, "New Error Summary", "New Error Details") + }, + }, + }, + }, + request: tfsdk.ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.Int64Value(123), + }, + response: &tfsdk.ValidateAttributeResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewAttributeWarningDiagnostic( + path.Root("other"), + "Existing Warning Summary", + "Existing Warning Details", + ), + diag.NewAttributeErrorDiagnostic( + path.Root("other"), + "Existing Error Summary", + "Existing Error Details", + ), + }, + }, + expected: &tfsdk.ValidateAttributeResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewAttributeWarningDiagnostic( + path.Root("other"), + "Existing Warning Summary", + "Existing Warning Details", + ), + diag.NewAttributeErrorDiagnostic( + path.Root("other"), + "Existing Error Summary", + "Existing Error Details", + ), + diag.NewAttributeWarningDiagnostic( + path.Root("test"), + "New Warning Summary", + "New Warning Details", + ), + diag.NewAttributeErrorDiagnostic( + path.Root("test"), + "New Error Summary", + "New Error Details", + ), + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + AttributeValidateInt64(context.Background(), testCase.attribute, testCase.request, testCase.response) + + if diff := cmp.Diff(testCase.response, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestAttributeValidateList(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute fwxschema.AttributeWithListValidators + request tfsdk.ValidateAttributeRequest + response *tfsdk.ValidateAttributeResponse + expected *tfsdk.ValidateAttributeResponse + }{ + "request-path": { + attribute: testschema.AttributeWithListValidators{ + ElementType: types.StringType, + Validators: []validator.List{ + testvalidator.List{ + ValidateListMethod: func(ctx context.Context, req validator.ListRequest, resp *validator.ListResponse) { + got := req.Path + expected := path.Root("test") + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected ListRequest.Path", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: tfsdk.ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("test")}), + }, + response: &tfsdk.ValidateAttributeResponse{}, + expected: &tfsdk.ValidateAttributeResponse{}, + }, + "request-pathexpression": { + attribute: testschema.AttributeWithListValidators{ + ElementType: types.StringType, + Validators: []validator.List{ + testvalidator.List{ + ValidateListMethod: func(ctx context.Context, req validator.ListRequest, resp *validator.ListResponse) { + got := req.PathExpression + expected := path.MatchRoot("test") + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected ListRequest.PathExpression", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: tfsdk.ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), + AttributeConfig: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("test")}), + }, + response: &tfsdk.ValidateAttributeResponse{}, + expected: &tfsdk.ValidateAttributeResponse{}, + }, + "request-config": { + attribute: testschema.AttributeWithListValidators{ + ElementType: types.StringType, + Validators: []validator.List{ + testvalidator.List{ + ValidateListMethod: func(ctx context.Context, req validator.ListRequest, resp *validator.ListResponse) { + got := req.Config + expected := tfsdk.Config{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.List{ElementType: tftypes.String}, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.List{ElementType: tftypes.String}, + []tftypes.Value{ + tftypes.NewValue(tftypes.String, "test"), + }, + ), + }, + ), + } + + if !got.Raw.Equal(expected.Raw) { + resp.Diagnostics.AddError( + "Unexpected ListRequest.Config", + fmt.Sprintf("expected %s, got: %s", expected.Raw, got.Raw), + ) + } + }, + }, + }, + }, + request: tfsdk.ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("test")}), + Config: tfsdk.Config{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.List{ElementType: tftypes.String}, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.List{ElementType: tftypes.String}, + []tftypes.Value{ + tftypes.NewValue(tftypes.String, "test"), + }, + ), + }, + ), + }, + }, + response: &tfsdk.ValidateAttributeResponse{}, + expected: &tfsdk.ValidateAttributeResponse{}, + }, + "request-configvalue": { + attribute: testschema.AttributeWithListValidators{ + ElementType: types.StringType, + Validators: []validator.List{ + testvalidator.List{ + ValidateListMethod: func(ctx context.Context, req validator.ListRequest, resp *validator.ListResponse) { + got := req.ConfigValue + expected := types.ListValueMust(types.StringType, []attr.Value{types.StringValue("test")}) + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected ListRequest.ConfigValue", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: tfsdk.ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("test")}), + }, + response: &tfsdk.ValidateAttributeResponse{}, + expected: &tfsdk.ValidateAttributeResponse{}, + }, + "response-diagnostics": { + attribute: testschema.AttributeWithListValidators{ + ElementType: types.StringType, + Validators: []validator.List{ + testvalidator.List{ + ValidateListMethod: func(ctx context.Context, req validator.ListRequest, resp *validator.ListResponse) { + resp.Diagnostics.AddAttributeWarning(req.Path, "New Warning Summary", "New Warning Details") + resp.Diagnostics.AddAttributeError(req.Path, "New Error Summary", "New Error Details") + }, + }, + }, + }, + request: tfsdk.ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("test")}), + }, + response: &tfsdk.ValidateAttributeResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewAttributeWarningDiagnostic( + path.Root("other"), + "Existing Warning Summary", + "Existing Warning Details", + ), + diag.NewAttributeErrorDiagnostic( + path.Root("other"), + "Existing Error Summary", + "Existing Error Details", + ), + }, + }, + expected: &tfsdk.ValidateAttributeResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewAttributeWarningDiagnostic( + path.Root("other"), + "Existing Warning Summary", + "Existing Warning Details", + ), + diag.NewAttributeErrorDiagnostic( + path.Root("other"), + "Existing Error Summary", + "Existing Error Details", + ), + diag.NewAttributeWarningDiagnostic( + path.Root("test"), + "New Warning Summary", + "New Warning Details", + ), + diag.NewAttributeErrorDiagnostic( + path.Root("test"), + "New Error Summary", + "New Error Details", + ), + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + AttributeValidateList(context.Background(), testCase.attribute, testCase.request, testCase.response) + + if diff := cmp.Diff(testCase.response, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestAttributeValidateMap(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute fwxschema.AttributeWithMapValidators + request tfsdk.ValidateAttributeRequest + response *tfsdk.ValidateAttributeResponse + expected *tfsdk.ValidateAttributeResponse + }{ + "request-path": { + attribute: testschema.AttributeWithMapValidators{ + ElementType: types.StringType, + Validators: []validator.Map{ + testvalidator.Map{ + ValidateMapMethod: func(ctx context.Context, req validator.MapRequest, resp *validator.MapResponse) { + got := req.Path + expected := path.Root("test") + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected MapRequest.Path", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: tfsdk.ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.MapValueMust( + types.StringType, + map[string]attr.Value{"testkey": types.StringValue("testvalue")}, + ), + }, + response: &tfsdk.ValidateAttributeResponse{}, + expected: &tfsdk.ValidateAttributeResponse{}, + }, + "request-pathexpression": { + attribute: testschema.AttributeWithMapValidators{ + ElementType: types.StringType, + Validators: []validator.Map{ + testvalidator.Map{ + ValidateMapMethod: func(ctx context.Context, req validator.MapRequest, resp *validator.MapResponse) { + got := req.PathExpression + expected := path.MatchRoot("test") + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected MapRequest.PathExpression", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: tfsdk.ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), + AttributeConfig: types.MapValueMust( + types.StringType, + map[string]attr.Value{"testkey": types.StringValue("testvalue")}, + ), + }, + response: &tfsdk.ValidateAttributeResponse{}, + expected: &tfsdk.ValidateAttributeResponse{}, + }, + "request-config": { + attribute: testschema.AttributeWithMapValidators{ + ElementType: types.StringType, + Validators: []validator.Map{ + testvalidator.Map{ + ValidateMapMethod: func(ctx context.Context, req validator.MapRequest, resp *validator.MapResponse) { + got := req.Config + expected := tfsdk.Config{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Map{ElementType: tftypes.String}, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.Map{ElementType: tftypes.String}, + map[string]tftypes.Value{ + "testkey": tftypes.NewValue(tftypes.String, "testvalue"), + }, + ), + }, + ), + } + + if !got.Raw.Equal(expected.Raw) { + resp.Diagnostics.AddError( + "Unexpected MapRequest.Config", + fmt.Sprintf("expected %s, got: %s", expected.Raw, got.Raw), + ) + } + }, + }, + }, + }, + request: tfsdk.ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.MapValueMust( + types.StringType, + map[string]attr.Value{"testkey": types.StringValue("testvalue")}, + ), + Config: tfsdk.Config{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Map{ElementType: tftypes.String}, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.Map{ElementType: tftypes.String}, + map[string]tftypes.Value{ + "testkey": tftypes.NewValue(tftypes.String, "testvalue"), + }, + ), + }, + ), + }, + }, + response: &tfsdk.ValidateAttributeResponse{}, + expected: &tfsdk.ValidateAttributeResponse{}, + }, + "request-configvalue": { + attribute: testschema.AttributeWithMapValidators{ + ElementType: types.StringType, + Validators: []validator.Map{ + testvalidator.Map{ + ValidateMapMethod: func(ctx context.Context, req validator.MapRequest, resp *validator.MapResponse) { + got := req.ConfigValue + expected := types.MapValueMust( + types.StringType, + map[string]attr.Value{"testkey": types.StringValue("testvalue")}, + ) + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected MapRequest.ConfigValue", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: tfsdk.ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.MapValueMust( + types.StringType, + map[string]attr.Value{"testkey": types.StringValue("testvalue")}, + ), + }, + response: &tfsdk.ValidateAttributeResponse{}, + expected: &tfsdk.ValidateAttributeResponse{}, + }, + "response-diagnostics": { + attribute: testschema.AttributeWithMapValidators{ + ElementType: types.StringType, + Validators: []validator.Map{ + testvalidator.Map{ + ValidateMapMethod: func(ctx context.Context, req validator.MapRequest, resp *validator.MapResponse) { + resp.Diagnostics.AddAttributeWarning(req.Path, "New Warning Summary", "New Warning Details") + resp.Diagnostics.AddAttributeError(req.Path, "New Error Summary", "New Error Details") + }, + }, + }, + }, + request: tfsdk.ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.MapValueMust( + types.StringType, + map[string]attr.Value{"testkey": types.StringValue("testvalue")}, + ), + }, + response: &tfsdk.ValidateAttributeResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewAttributeWarningDiagnostic( + path.Root("other"), + "Existing Warning Summary", + "Existing Warning Details", + ), + diag.NewAttributeErrorDiagnostic( + path.Root("other"), + "Existing Error Summary", + "Existing Error Details", + ), + }, + }, + expected: &tfsdk.ValidateAttributeResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewAttributeWarningDiagnostic( + path.Root("other"), + "Existing Warning Summary", + "Existing Warning Details", + ), + diag.NewAttributeErrorDiagnostic( + path.Root("other"), + "Existing Error Summary", + "Existing Error Details", + ), + diag.NewAttributeWarningDiagnostic( + path.Root("test"), + "New Warning Summary", + "New Warning Details", + ), + diag.NewAttributeErrorDiagnostic( + path.Root("test"), + "New Error Summary", + "New Error Details", + ), + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + AttributeValidateMap(context.Background(), testCase.attribute, testCase.request, testCase.response) + + if diff := cmp.Diff(testCase.response, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestAttributeValidateNumber(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute fwxschema.AttributeWithNumberValidators + request tfsdk.ValidateAttributeRequest + response *tfsdk.ValidateAttributeResponse + expected *tfsdk.ValidateAttributeResponse + }{ + "request-path": { + attribute: testschema.AttributeWithNumberValidators{ + Validators: []validator.Number{ + testvalidator.Number{ + ValidateNumberMethod: func(ctx context.Context, req validator.NumberRequest, resp *validator.NumberResponse) { + got := req.Path + expected := path.Root("test") + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected NumberRequest.Path", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: tfsdk.ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.NumberValue(big.NewFloat(1.2)), + }, + response: &tfsdk.ValidateAttributeResponse{}, + expected: &tfsdk.ValidateAttributeResponse{}, + }, + "request-pathexpression": { + attribute: testschema.AttributeWithNumberValidators{ + Validators: []validator.Number{ + testvalidator.Number{ + ValidateNumberMethod: func(ctx context.Context, req validator.NumberRequest, resp *validator.NumberResponse) { + got := req.PathExpression + expected := path.MatchRoot("test") + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected NumberRequest.PathExpression", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: tfsdk.ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), + AttributeConfig: types.NumberValue(big.NewFloat(1.2)), + }, + response: &tfsdk.ValidateAttributeResponse{}, + expected: &tfsdk.ValidateAttributeResponse{}, + }, + "request-config": { + attribute: testschema.AttributeWithNumberValidators{ + Validators: []validator.Number{ + testvalidator.Number{ + ValidateNumberMethod: func(ctx context.Context, req validator.NumberRequest, resp *validator.NumberResponse) { + got := req.Config + expected := tfsdk.Config{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.Number, 1.2), + }, + ), + } + + if !got.Raw.Equal(expected.Raw) { + resp.Diagnostics.AddError( + "Unexpected NumberRequest.Config", + fmt.Sprintf("expected %s, got: %s", expected.Raw, got.Raw), + ) + } + }, + }, + }, + }, + request: tfsdk.ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.NumberValue(big.NewFloat(1.2)), + Config: tfsdk.Config{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.Number, 1.2), + }, + ), + }, + }, + response: &tfsdk.ValidateAttributeResponse{}, + expected: &tfsdk.ValidateAttributeResponse{}, + }, + "request-configvalue": { + attribute: testschema.AttributeWithNumberValidators{ + Validators: []validator.Number{ + testvalidator.Number{ + ValidateNumberMethod: func(ctx context.Context, req validator.NumberRequest, resp *validator.NumberResponse) { + got := req.ConfigValue + expected := types.NumberValue(big.NewFloat(1.2)) + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected NumberRequest.ConfigValue", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: tfsdk.ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.NumberValue(big.NewFloat(1.2)), + }, + response: &tfsdk.ValidateAttributeResponse{}, + expected: &tfsdk.ValidateAttributeResponse{}, + }, + "response-diagnostics": { + attribute: testschema.AttributeWithNumberValidators{ + Validators: []validator.Number{ + testvalidator.Number{ + ValidateNumberMethod: func(ctx context.Context, req validator.NumberRequest, resp *validator.NumberResponse) { + resp.Diagnostics.AddAttributeWarning(req.Path, "New Warning Summary", "New Warning Details") + resp.Diagnostics.AddAttributeError(req.Path, "New Error Summary", "New Error Details") + }, + }, + }, + }, + request: tfsdk.ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.NumberValue(big.NewFloat(1.2)), + }, + response: &tfsdk.ValidateAttributeResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewAttributeWarningDiagnostic( + path.Root("other"), + "Existing Warning Summary", + "Existing Warning Details", + ), + diag.NewAttributeErrorDiagnostic( + path.Root("other"), + "Existing Error Summary", + "Existing Error Details", + ), + }, + }, + expected: &tfsdk.ValidateAttributeResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewAttributeWarningDiagnostic( + path.Root("other"), + "Existing Warning Summary", + "Existing Warning Details", + ), + diag.NewAttributeErrorDiagnostic( + path.Root("other"), + "Existing Error Summary", + "Existing Error Details", + ), + diag.NewAttributeWarningDiagnostic( + path.Root("test"), + "New Warning Summary", + "New Warning Details", + ), + diag.NewAttributeErrorDiagnostic( + path.Root("test"), + "New Error Summary", + "New Error Details", + ), + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + AttributeValidateNumber(context.Background(), testCase.attribute, testCase.request, testCase.response) + + if diff := cmp.Diff(testCase.response, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestAttributeValidateObject(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute fwxschema.AttributeWithObjectValidators + request tfsdk.ValidateAttributeRequest + response *tfsdk.ValidateAttributeResponse + expected *tfsdk.ValidateAttributeResponse + }{ + "request-path": { + attribute: testschema.AttributeWithObjectValidators{ + AttributeTypes: map[string]attr.Type{ + "testattr": types.StringType, + }, + Validators: []validator.Object{ + testvalidator.Object{ + ValidateObjectMethod: func(ctx context.Context, req validator.ObjectRequest, resp *validator.ObjectResponse) { + got := req.Path + expected := path.Root("test") + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected ObjectRequest.Path", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: tfsdk.ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.ObjectValueMust( + map[string]attr.Type{"testattr": types.StringType}, + map[string]attr.Value{"testattr": types.StringValue("testvalue")}, + ), + }, + response: &tfsdk.ValidateAttributeResponse{}, + expected: &tfsdk.ValidateAttributeResponse{}, + }, + "request-pathexpression": { + attribute: testschema.AttributeWithObjectValidators{ + AttributeTypes: map[string]attr.Type{ + "testattr": types.StringType, + }, + Validators: []validator.Object{ + testvalidator.Object{ + ValidateObjectMethod: func(ctx context.Context, req validator.ObjectRequest, resp *validator.ObjectResponse) { + got := req.PathExpression + expected := path.MatchRoot("test") + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected ObjectRequest.PathExpression", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: tfsdk.ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), + AttributeConfig: types.ObjectValueMust( + map[string]attr.Type{"testattr": types.StringType}, + map[string]attr.Value{"testattr": types.StringValue("testvalue")}, + ), + }, + response: &tfsdk.ValidateAttributeResponse{}, + expected: &tfsdk.ValidateAttributeResponse{}, + }, + "request-config": { + attribute: testschema.AttributeWithObjectValidators{ + AttributeTypes: map[string]attr.Type{ + "testattr": types.StringType, + }, + Validators: []validator.Object{ + testvalidator.Object{ + ValidateObjectMethod: func(ctx context.Context, req validator.ObjectRequest, resp *validator.ObjectResponse) { + got := req.Config + expected := tfsdk.Config{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Object{AttributeTypes: map[string]tftypes.Type{"testattr": tftypes.String}}, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.Object{AttributeTypes: map[string]tftypes.Type{"testattr": tftypes.String}}, + map[string]tftypes.Value{ + "testattr": tftypes.NewValue(tftypes.String, "testvalue"), + }, + ), + }, + ), + } + + if !got.Raw.Equal(expected.Raw) { + resp.Diagnostics.AddError( + "Unexpected ObjectRequest.Config", + fmt.Sprintf("expected %s, got: %s", expected.Raw, got.Raw), + ) + } + }, + }, + }, + }, + request: tfsdk.ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.ObjectValueMust( + map[string]attr.Type{"testattr": types.StringType}, + map[string]attr.Value{"testattr": types.StringValue("testvalue")}, + ), + Config: tfsdk.Config{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Object{AttributeTypes: map[string]tftypes.Type{"testattr": tftypes.String}}, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.Object{AttributeTypes: map[string]tftypes.Type{"testattr": tftypes.String}}, + map[string]tftypes.Value{ + "testattr": tftypes.NewValue(tftypes.String, "testvalue"), + }, + ), + }, + ), + }, + }, + response: &tfsdk.ValidateAttributeResponse{}, + expected: &tfsdk.ValidateAttributeResponse{}, + }, + "request-configvalue": { + attribute: testschema.AttributeWithObjectValidators{ + AttributeTypes: map[string]attr.Type{ + "testattr": types.StringType, + }, + Validators: []validator.Object{ + testvalidator.Object{ + ValidateObjectMethod: func(ctx context.Context, req validator.ObjectRequest, resp *validator.ObjectResponse) { + got := req.ConfigValue + expected := types.ObjectValueMust( + map[string]attr.Type{"testattr": types.StringType}, + map[string]attr.Value{"testattr": types.StringValue("testvalue")}, + ) + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected ObjectRequest.ConfigValue", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: tfsdk.ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.ObjectValueMust( + map[string]attr.Type{"testattr": types.StringType}, + map[string]attr.Value{"testattr": types.StringValue("testvalue")}, + ), + }, + response: &tfsdk.ValidateAttributeResponse{}, + expected: &tfsdk.ValidateAttributeResponse{}, + }, + "response-diagnostics": { + attribute: testschema.AttributeWithObjectValidators{ + AttributeTypes: map[string]attr.Type{ + "testattr": types.StringType, + }, + Validators: []validator.Object{ + testvalidator.Object{ + ValidateObjectMethod: func(ctx context.Context, req validator.ObjectRequest, resp *validator.ObjectResponse) { + resp.Diagnostics.AddAttributeWarning(req.Path, "New Warning Summary", "New Warning Details") + resp.Diagnostics.AddAttributeError(req.Path, "New Error Summary", "New Error Details") + }, + }, + }, + }, + request: tfsdk.ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.ObjectValueMust( + map[string]attr.Type{"testattr": types.StringType}, + map[string]attr.Value{"testattr": types.StringValue("testvalue")}, + ), + }, + response: &tfsdk.ValidateAttributeResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewAttributeWarningDiagnostic( + path.Root("other"), + "Existing Warning Summary", + "Existing Warning Details", + ), + diag.NewAttributeErrorDiagnostic( + path.Root("other"), + "Existing Error Summary", + "Existing Error Details", + ), + }, + }, + expected: &tfsdk.ValidateAttributeResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewAttributeWarningDiagnostic( + path.Root("other"), + "Existing Warning Summary", + "Existing Warning Details", + ), + diag.NewAttributeErrorDiagnostic( + path.Root("other"), + "Existing Error Summary", + "Existing Error Details", + ), + diag.NewAttributeWarningDiagnostic( + path.Root("test"), + "New Warning Summary", + "New Warning Details", + ), + diag.NewAttributeErrorDiagnostic( + path.Root("test"), + "New Error Summary", + "New Error Details", + ), + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + AttributeValidateObject(context.Background(), testCase.attribute, testCase.request, testCase.response) + + if diff := cmp.Diff(testCase.response, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestAttributeValidateSet(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute fwxschema.AttributeWithSetValidators + request tfsdk.ValidateAttributeRequest + response *tfsdk.ValidateAttributeResponse + expected *tfsdk.ValidateAttributeResponse + }{ + "request-path": { + attribute: testschema.AttributeWithSetValidators{ + ElementType: types.StringType, + Validators: []validator.Set{ + testvalidator.Set{ + ValidateSetMethod: func(ctx context.Context, req validator.SetRequest, resp *validator.SetResponse) { + got := req.Path + expected := path.Root("test") + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected SetRequest.Path", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: tfsdk.ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("test")}), + }, + response: &tfsdk.ValidateAttributeResponse{}, + expected: &tfsdk.ValidateAttributeResponse{}, + }, + "request-pathexpression": { + attribute: testschema.AttributeWithSetValidators{ + ElementType: types.StringType, + Validators: []validator.Set{ + testvalidator.Set{ + ValidateSetMethod: func(ctx context.Context, req validator.SetRequest, resp *validator.SetResponse) { + got := req.PathExpression + expected := path.MatchRoot("test") + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected SetRequest.PathExpression", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: tfsdk.ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), + AttributeConfig: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("test")}), + }, + response: &tfsdk.ValidateAttributeResponse{}, + expected: &tfsdk.ValidateAttributeResponse{}, + }, + "request-config": { + attribute: testschema.AttributeWithSetValidators{ + ElementType: types.StringType, + Validators: []validator.Set{ + testvalidator.Set{ + ValidateSetMethod: func(ctx context.Context, req validator.SetRequest, resp *validator.SetResponse) { + got := req.Config + expected := tfsdk.Config{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Set{ElementType: tftypes.String}, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.Set{ElementType: tftypes.String}, + []tftypes.Value{ + tftypes.NewValue(tftypes.String, "test"), + }, + ), + }, + ), + } + + if !got.Raw.Equal(expected.Raw) { + resp.Diagnostics.AddError( + "Unexpected SetRequest.Config", + fmt.Sprintf("expected %s, got: %s", expected.Raw, got.Raw), + ) + } + }, + }, + }, + }, + request: tfsdk.ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("test")}), + Config: tfsdk.Config{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Set{ElementType: tftypes.String}, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.Set{ElementType: tftypes.String}, + []tftypes.Value{ + tftypes.NewValue(tftypes.String, "test"), + }, + ), + }, + ), + }, + }, + response: &tfsdk.ValidateAttributeResponse{}, + expected: &tfsdk.ValidateAttributeResponse{}, + }, + "request-configvalue": { + attribute: testschema.AttributeWithSetValidators{ + ElementType: types.StringType, + Validators: []validator.Set{ + testvalidator.Set{ + ValidateSetMethod: func(ctx context.Context, req validator.SetRequest, resp *validator.SetResponse) { + got := req.ConfigValue + expected := types.SetValueMust(types.StringType, []attr.Value{types.StringValue("test")}) + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected SetRequest.ConfigValue", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: tfsdk.ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("test")}), + }, + response: &tfsdk.ValidateAttributeResponse{}, + expected: &tfsdk.ValidateAttributeResponse{}, + }, + "response-diagnostics": { + attribute: testschema.AttributeWithSetValidators{ + ElementType: types.StringType, + Validators: []validator.Set{ + testvalidator.Set{ + ValidateSetMethod: func(ctx context.Context, req validator.SetRequest, resp *validator.SetResponse) { + resp.Diagnostics.AddAttributeWarning(req.Path, "New Warning Summary", "New Warning Details") + resp.Diagnostics.AddAttributeError(req.Path, "New Error Summary", "New Error Details") + }, + }, + }, + }, + request: tfsdk.ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("test")}), + }, + response: &tfsdk.ValidateAttributeResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewAttributeWarningDiagnostic( + path.Root("other"), + "Existing Warning Summary", + "Existing Warning Details", + ), + diag.NewAttributeErrorDiagnostic( + path.Root("other"), + "Existing Error Summary", + "Existing Error Details", + ), + }, + }, + expected: &tfsdk.ValidateAttributeResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewAttributeWarningDiagnostic( + path.Root("other"), + "Existing Warning Summary", + "Existing Warning Details", + ), + diag.NewAttributeErrorDiagnostic( + path.Root("other"), + "Existing Error Summary", + "Existing Error Details", + ), + diag.NewAttributeWarningDiagnostic( + path.Root("test"), + "New Warning Summary", + "New Warning Details", + ), + diag.NewAttributeErrorDiagnostic( + path.Root("test"), + "New Error Summary", + "New Error Details", + ), + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + AttributeValidateSet(context.Background(), testCase.attribute, testCase.request, testCase.response) + + if diff := cmp.Diff(testCase.response, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestAttributeValidateString(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute fwxschema.AttributeWithStringValidators + request tfsdk.ValidateAttributeRequest + response *tfsdk.ValidateAttributeResponse + expected *tfsdk.ValidateAttributeResponse + }{ + "request-path": { + attribute: testschema.AttributeWithStringValidators{ + Validators: []validator.String{ + testvalidator.String{ + ValidateStringMethod: func(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) { + got := req.Path + expected := path.Root("test") + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected StringRequest.Path", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: tfsdk.ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.StringValue("test"), + }, + response: &tfsdk.ValidateAttributeResponse{}, + expected: &tfsdk.ValidateAttributeResponse{}, + }, + "request-pathexpression": { + attribute: testschema.AttributeWithStringValidators{ + Validators: []validator.String{ + testvalidator.String{ + ValidateStringMethod: func(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) { + got := req.PathExpression + expected := path.MatchRoot("test") + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected StringRequest.PathExpression", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: tfsdk.ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), + AttributeConfig: types.StringValue("test"), + }, + response: &tfsdk.ValidateAttributeResponse{}, + expected: &tfsdk.ValidateAttributeResponse{}, + }, + "request-config": { + attribute: testschema.AttributeWithStringValidators{ + Validators: []validator.String{ + testvalidator.String{ + ValidateStringMethod: func(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) { + got := req.Config + expected := tfsdk.Config{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.String, "test"), + }, + ), + } + + if !got.Raw.Equal(expected.Raw) { + resp.Diagnostics.AddError( + "Unexpected StringRequest.Config", + fmt.Sprintf("expected %s, got: %s", expected.Raw, got.Raw), + ) + } + }, + }, + }, + }, + request: tfsdk.ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.StringValue("test"), + Config: tfsdk.Config{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.String, "test"), + }, + ), + }, + }, + response: &tfsdk.ValidateAttributeResponse{}, + expected: &tfsdk.ValidateAttributeResponse{}, + }, + "request-configvalue": { + attribute: testschema.AttributeWithStringValidators{ + Validators: []validator.String{ + testvalidator.String{ + ValidateStringMethod: func(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) { + got := req.ConfigValue + expected := types.StringValue("test") + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected StringRequest.ConfigValue", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: tfsdk.ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.StringValue("test"), + }, + response: &tfsdk.ValidateAttributeResponse{}, + expected: &tfsdk.ValidateAttributeResponse{}, + }, + "response-diagnostics": { + attribute: testschema.AttributeWithStringValidators{ + Validators: []validator.String{ + testvalidator.String{ + ValidateStringMethod: func(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) { + resp.Diagnostics.AddAttributeWarning(req.Path, "New Warning Summary", "New Warning Details") + resp.Diagnostics.AddAttributeError(req.Path, "New Error Summary", "New Error Details") + }, + }, + }, + }, + request: tfsdk.ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.StringValue("test"), + }, + response: &tfsdk.ValidateAttributeResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewAttributeWarningDiagnostic( + path.Root("other"), + "Existing Warning Summary", + "Existing Warning Details", + ), + diag.NewAttributeErrorDiagnostic( + path.Root("other"), + "Existing Error Summary", + "Existing Error Details", + ), + }, + }, + expected: &tfsdk.ValidateAttributeResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewAttributeWarningDiagnostic( + path.Root("other"), + "Existing Warning Summary", + "Existing Warning Details", + ), + diag.NewAttributeErrorDiagnostic( + path.Root("other"), + "Existing Error Summary", + "Existing Error Details", + ), + diag.NewAttributeWarningDiagnostic( + path.Root("test"), + "New Warning Summary", + "New Warning Details", + ), + diag.NewAttributeErrorDiagnostic( + path.Root("test"), + "New Error Summary", + "New Error Details", + ), + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + AttributeValidateString(context.Background(), testCase.attribute, testCase.request, testCase.response) + + if diff := cmp.Diff(testCase.response, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + var ( testErrorDiagnostic1 = diag.NewErrorDiagnostic( "Error Diagnostic 1", diff --git a/internal/fwserver/block_validation.go b/internal/fwserver/block_validation.go index cb5add730..c62c8bbe9 100644 --- a/internal/fwserver/block_validation.go +++ b/internal/fwserver/block_validation.go @@ -9,7 +9,9 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschemadata" + "github.com/hashicorp/terraform-plugin-framework/internal/logging" "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" ) @@ -21,12 +23,6 @@ import ( // package from the tfsdk package and not wanting to export the method. // Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/365 func BlockValidate(ctx context.Context, b fwschema.Block, req tfsdk.ValidateAttributeRequest, resp *tfsdk.ValidateAttributeResponse) { - blockWithValidators, ok := b.(fwxschema.BlockWithValidators) - - if !ok { - return - } - configData := &fwschemadata.Data{ Description: fwschemadata.DataDescriptionConfiguration, Schema: req.Config.Schema, @@ -42,8 +38,32 @@ func BlockValidate(ctx context.Context, b fwschema.Block, req tfsdk.ValidateAttr req.AttributeConfig = attributeConfig - for _, validator := range blockWithValidators.GetValidators() { - validator.Validate(ctx, req, resp) + switch blockWithValidators := b.(type) { + // Legacy tfsdk.AttributeValidator handling + case fwxschema.BlockWithValidators: + for _, validator := range blockWithValidators.GetValidators() { + logging.FrameworkDebug( + ctx, + "Calling provider defined AttributeValidator", + map[string]interface{}{ + logging.KeyDescription: validator.Description(ctx), + }, + ) + validator.Validate(ctx, req, resp) + logging.FrameworkDebug( + ctx, + "Called provider defined AttributeValidator", + map[string]interface{}{ + logging.KeyDescription: validator.Description(ctx), + }, + ) + } + case fwxschema.BlockWithListValidators: + BlockValidateList(ctx, blockWithValidators, req, resp) + case fwxschema.BlockWithObjectValidators: + BlockValidateObject(ctx, blockWithValidators, req, resp) + case fwxschema.BlockWithSetValidators: + BlockValidateSet(ctx, blockWithValidators, req, resp) } nm := b.GetNestingMode() @@ -273,6 +293,201 @@ func BlockValidate(ctx context.Context, b fwschema.Block, req tfsdk.ValidateAttr } } +// BlockValidateList performs all types.List validation. +func BlockValidateList(ctx context.Context, block fwxschema.BlockWithListValidators, req tfsdk.ValidateAttributeRequest, resp *tfsdk.ValidateAttributeResponse) { + // Use types.ListValuable until custom types cannot re-implement + // ValueFromTerraform. Until then, custom types are not technically + // required to implement this interface. This opts to enforce the + // requirement before compatibility promises would interfere. + configValuable, ok := req.AttributeConfig.(types.ListValuable) + + if !ok { + resp.Diagnostics.AddAttributeError( + req.AttributePath, + "Invalid List Attribute Validator Value Type", + "An unexpected value type was encountered while attempting to perform List attribute validation. "+ + "The value type must implement the types.ListValuable interface. "+ + "Please report this to the provider developers.\n\n"+ + fmt.Sprintf("Incoming Value Type: %T", req.AttributeConfig), + ) + + return + } + + configValue, diags := configValuable.ToListValue(ctx) + + resp.Diagnostics.Append(diags...) + + // Only return early on new errors as the resp.Diagnostics may have errors + // from other attributes. + if diags.HasError() { + return + } + + validateReq := validator.ListRequest{ + Config: req.Config, + ConfigValue: configValue, + Path: req.AttributePath, + PathExpression: req.AttributePathExpression, + } + + for _, blockValidator := range block.ListValidators() { + // Instantiate a new response for each request to prevent validators + // from modifying or removing diagnostics. + validateResp := &validator.ListResponse{} + + logging.FrameworkDebug( + ctx, + "Calling provider defined validator.List", + map[string]interface{}{ + logging.KeyDescription: blockValidator.Description(ctx), + }, + ) + + blockValidator.ValidateList(ctx, validateReq, validateResp) + + logging.FrameworkDebug( + ctx, + "Called provider defined validator.List", + map[string]interface{}{ + logging.KeyDescription: blockValidator.Description(ctx), + }, + ) + + resp.Diagnostics.Append(validateResp.Diagnostics...) + } +} + +// BlockValidateObject performs all types.Object validation. +func BlockValidateObject(ctx context.Context, block fwxschema.BlockWithObjectValidators, req tfsdk.ValidateAttributeRequest, resp *tfsdk.ValidateAttributeResponse) { + // Use types.ObjectValuable until custom types cannot re-implement + // ValueFromTerraform. Until then, custom types are not technically + // required to implement this interface. This opts to enforce the + // requirement before compatibility promises would interfere. + configValuable, ok := req.AttributeConfig.(types.ObjectValuable) + + if !ok { + resp.Diagnostics.AddAttributeError( + req.AttributePath, + "Invalid Object Attribute Validator Value Type", + "An unexpected value type was encountered while attempting to perform Object attribute validation. "+ + "The value type must implement the types.ObjectValuable interface. "+ + "Please report this to the provider developers.\n\n"+ + fmt.Sprintf("Incoming Value Type: %T", req.AttributeConfig), + ) + + return + } + + configValue, diags := configValuable.ToObjectValue(ctx) + + resp.Diagnostics.Append(diags...) + + // Only return early on new errors as the resp.Diagnostics may have errors + // from other attributes. + if diags.HasError() { + return + } + + validateReq := validator.ObjectRequest{ + Config: req.Config, + ConfigValue: configValue, + Path: req.AttributePath, + PathExpression: req.AttributePathExpression, + } + + for _, blockValidator := range block.ObjectValidators() { + // Instantiate a new response for each request to prevent validators + // from modifying or removing diagnostics. + validateResp := &validator.ObjectResponse{} + + logging.FrameworkDebug( + ctx, + "Calling provider defined validator.Object", + map[string]interface{}{ + logging.KeyDescription: blockValidator.Description(ctx), + }, + ) + + blockValidator.ValidateObject(ctx, validateReq, validateResp) + + logging.FrameworkDebug( + ctx, + "Called provider defined validator.Object", + map[string]interface{}{ + logging.KeyDescription: blockValidator.Description(ctx), + }, + ) + + resp.Diagnostics.Append(validateResp.Diagnostics...) + } +} + +// BlockValidateSet performs all types.Set validation. +func BlockValidateSet(ctx context.Context, block fwxschema.BlockWithSetValidators, req tfsdk.ValidateAttributeRequest, resp *tfsdk.ValidateAttributeResponse) { + // Use types.SetValuable until custom types cannot re-implement + // ValueFromTerraform. Until then, custom types are not technically + // required to implement this interface. This opts to enforce the + // requirement before compatibility promises would interfere. + configValuable, ok := req.AttributeConfig.(types.SetValuable) + + if !ok { + resp.Diagnostics.AddAttributeError( + req.AttributePath, + "Invalid Set Attribute Validator Value Type", + "An unexpected value type was encountered while attempting to perform Set attribute validation. "+ + "The value type must implement the types.SetValuable interface. "+ + "Please report this to the provider developers.\n\n"+ + fmt.Sprintf("Incoming Value Type: %T", req.AttributeConfig), + ) + + return + } + + configValue, diags := configValuable.ToSetValue(ctx) + + resp.Diagnostics.Append(diags...) + + // Only return early on new errors as the resp.Diagnostics may have errors + // from other attributes. + if diags.HasError() { + return + } + + validateReq := validator.SetRequest{ + Config: req.Config, + ConfigValue: configValue, + Path: req.AttributePath, + PathExpression: req.AttributePathExpression, + } + + for _, blockValidator := range block.SetValidators() { + // Instantiate a new response for each request to prevent validators + // from modifying or removing diagnostics. + validateResp := &validator.SetResponse{} + + logging.FrameworkDebug( + ctx, + "Calling provider defined validator.Set", + map[string]interface{}{ + logging.KeyDescription: blockValidator.Description(ctx), + }, + ) + + blockValidator.ValidateSet(ctx, validateReq, validateResp) + + logging.FrameworkDebug( + ctx, + "Called provider defined validator.Set", + map[string]interface{}{ + logging.KeyDescription: blockValidator.Description(ctx), + }, + ) + + resp.Diagnostics.Append(validateResp.Diagnostics...) + } +} + func blockMaxItemsDiagnostic(attrPath path.Path, maxItems int64, elements int) diag.Diagnostic { var details strings.Builder diff --git a/internal/fwserver/block_validation_test.go b/internal/fwserver/block_validation_test.go index 72dd5f120..0ec65f2a4 100644 --- a/internal/fwserver/block_validation_test.go +++ b/internal/fwserver/block_validation_test.go @@ -2,12 +2,19 @@ package fwserver import ( "context" + "fmt" "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testvalidator" testtypes "github.com/hashicorp/terraform-plugin-framework/internal/testing/types" "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" @@ -2651,6 +2658,931 @@ func TestBlockValidate(t *testing.T) { } } +func TestBlockValidateList(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block fwxschema.BlockWithListValidators + request tfsdk.ValidateAttributeRequest + response *tfsdk.ValidateAttributeResponse + expected *tfsdk.ValidateAttributeResponse + }{ + "request-path": { + block: testschema.BlockWithListValidators{ + Attributes: map[string]fwschema.Attribute{ + "testattr": testschema.AttributeWithStringValidators{}, + }, + Validators: []validator.List{ + testvalidator.List{ + ValidateListMethod: func(ctx context.Context, req validator.ListRequest, resp *validator.ListResponse) { + got := req.Path + expected := path.Root("test") + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected ListRequest.Path", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: tfsdk.ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.ListValueMust( + types.ObjectType{ + AttrTypes: map[string]attr.Type{"testattr": types.StringType}, + }, + []attr.Value{ + types.ObjectValueMust( + map[string]attr.Type{"testattr": types.StringType}, + map[string]attr.Value{"testattr": types.StringValue("test")}, + ), + }, + ), + }, + response: &tfsdk.ValidateAttributeResponse{}, + expected: &tfsdk.ValidateAttributeResponse{}, + }, + "request-pathexpression": { + block: testschema.BlockWithListValidators{ + Attributes: map[string]fwschema.Attribute{ + "testattr": testschema.AttributeWithStringValidators{}, + }, + Validators: []validator.List{ + testvalidator.List{ + ValidateListMethod: func(ctx context.Context, req validator.ListRequest, resp *validator.ListResponse) { + got := req.PathExpression + expected := path.MatchRoot("test") + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected ListRequest.PathExpression", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: tfsdk.ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), + AttributeConfig: types.ListValueMust( + types.ObjectType{ + AttrTypes: map[string]attr.Type{"testattr": types.StringType}, + }, + []attr.Value{ + types.ObjectValueMust( + map[string]attr.Type{"testattr": types.StringType}, + map[string]attr.Value{"testattr": types.StringValue("test")}, + ), + }, + ), + }, + response: &tfsdk.ValidateAttributeResponse{}, + expected: &tfsdk.ValidateAttributeResponse{}, + }, + "request-config": { + block: testschema.BlockWithListValidators{ + Attributes: map[string]fwschema.Attribute{ + "testattr": testschema.AttributeWithStringValidators{}, + }, + Validators: []validator.List{ + testvalidator.List{ + ValidateListMethod: func(ctx context.Context, req validator.ListRequest, resp *validator.ListResponse) { + got := req.Config + expected := tfsdk.Config{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "testattr": tftypes.String, + }, + }, + }, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "testattr": tftypes.String, + }, + }, + }, + []tftypes.Value{ + tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "testattr": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "testattr": tftypes.NewValue(tftypes.String, "test"), + }, + ), + }, + ), + }, + ), + } + + if !got.Raw.Equal(expected.Raw) { + resp.Diagnostics.AddError( + "Unexpected ListRequest.Config", + fmt.Sprintf("expected %s, got: %s", expected.Raw, got.Raw), + ) + } + }, + }, + }, + }, + request: tfsdk.ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.ListValueMust( + types.ObjectType{ + AttrTypes: map[string]attr.Type{"testattr": types.StringType}, + }, + []attr.Value{ + types.ObjectValueMust( + map[string]attr.Type{"testattr": types.StringType}, + map[string]attr.Value{"testattr": types.StringValue("test")}, + ), + }, + ), + Config: tfsdk.Config{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "testattr": tftypes.String, + }, + }, + }, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "testattr": tftypes.String, + }, + }, + }, + []tftypes.Value{ + tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "testattr": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "testattr": tftypes.NewValue(tftypes.String, "test"), + }, + ), + }, + ), + }, + ), + }, + }, + response: &tfsdk.ValidateAttributeResponse{}, + expected: &tfsdk.ValidateAttributeResponse{}, + }, + "request-configvalue": { + block: testschema.BlockWithListValidators{ + Attributes: map[string]fwschema.Attribute{ + "testattr": testschema.AttributeWithStringValidators{}, + }, + Validators: []validator.List{ + testvalidator.List{ + ValidateListMethod: func(ctx context.Context, req validator.ListRequest, resp *validator.ListResponse) { + got := req.ConfigValue + expected := types.ListValueMust( + types.ObjectType{ + AttrTypes: map[string]attr.Type{"testattr": types.StringType}, + }, + []attr.Value{ + types.ObjectValueMust( + map[string]attr.Type{"testattr": types.StringType}, + map[string]attr.Value{"testattr": types.StringValue("test")}, + ), + }, + ) + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected ListRequest.ConfigValue", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: tfsdk.ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.ListValueMust( + types.ObjectType{ + AttrTypes: map[string]attr.Type{"testattr": types.StringType}, + }, + []attr.Value{ + types.ObjectValueMust( + map[string]attr.Type{"testattr": types.StringType}, + map[string]attr.Value{"testattr": types.StringValue("test")}, + ), + }, + ), + }, + response: &tfsdk.ValidateAttributeResponse{}, + expected: &tfsdk.ValidateAttributeResponse{}, + }, + "response-diagnostics": { + block: testschema.BlockWithListValidators{ + Attributes: map[string]fwschema.Attribute{ + "testattr": testschema.AttributeWithStringValidators{}, + }, + Validators: []validator.List{ + testvalidator.List{ + ValidateListMethod: func(ctx context.Context, req validator.ListRequest, resp *validator.ListResponse) { + resp.Diagnostics.AddAttributeWarning(req.Path, "New Warning Summary", "New Warning Details") + resp.Diagnostics.AddAttributeError(req.Path, "New Error Summary", "New Error Details") + }, + }, + }, + }, + request: tfsdk.ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.ListValueMust( + types.ObjectType{ + AttrTypes: map[string]attr.Type{"testattr": types.StringType}, + }, + []attr.Value{ + types.ObjectValueMust( + map[string]attr.Type{"testattr": types.StringType}, + map[string]attr.Value{"testattr": types.StringValue("test")}, + ), + }, + ), + }, + response: &tfsdk.ValidateAttributeResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewAttributeWarningDiagnostic( + path.Root("other"), + "Existing Warning Summary", + "Existing Warning Details", + ), + diag.NewAttributeErrorDiagnostic( + path.Root("other"), + "Existing Error Summary", + "Existing Error Details", + ), + }, + }, + expected: &tfsdk.ValidateAttributeResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewAttributeWarningDiagnostic( + path.Root("other"), + "Existing Warning Summary", + "Existing Warning Details", + ), + diag.NewAttributeErrorDiagnostic( + path.Root("other"), + "Existing Error Summary", + "Existing Error Details", + ), + diag.NewAttributeWarningDiagnostic( + path.Root("test"), + "New Warning Summary", + "New Warning Details", + ), + diag.NewAttributeErrorDiagnostic( + path.Root("test"), + "New Error Summary", + "New Error Details", + ), + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + BlockValidateList(context.Background(), testCase.block, testCase.request, testCase.response) + + if diff := cmp.Diff(testCase.response, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestBlockValidateObject(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block fwxschema.BlockWithObjectValidators + request tfsdk.ValidateAttributeRequest + response *tfsdk.ValidateAttributeResponse + expected *tfsdk.ValidateAttributeResponse + }{ + "request-path": { + block: testschema.BlockWithObjectValidators{ + Attributes: map[string]fwschema.Attribute{ + "testattr": testschema.AttributeWithStringValidators{}, + }, + Validators: []validator.Object{ + testvalidator.Object{ + ValidateObjectMethod: func(ctx context.Context, req validator.ObjectRequest, resp *validator.ObjectResponse) { + got := req.Path + expected := path.Root("test") + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected ObjectRequest.Path", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: tfsdk.ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.ObjectValueMust( + map[string]attr.Type{"testattr": types.StringType}, + map[string]attr.Value{"testattr": types.StringValue("test")}, + ), + }, + response: &tfsdk.ValidateAttributeResponse{}, + expected: &tfsdk.ValidateAttributeResponse{}, + }, + "request-pathexpression": { + block: testschema.BlockWithObjectValidators{ + Attributes: map[string]fwschema.Attribute{ + "testattr": testschema.AttributeWithStringValidators{}, + }, + Validators: []validator.Object{ + testvalidator.Object{ + ValidateObjectMethod: func(ctx context.Context, req validator.ObjectRequest, resp *validator.ObjectResponse) { + got := req.PathExpression + expected := path.MatchRoot("test") + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected ObjectRequest.PathExpression", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: tfsdk.ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), + AttributeConfig: types.ObjectValueMust( + map[string]attr.Type{"testattr": types.StringType}, + map[string]attr.Value{"testattr": types.StringValue("test")}, + ), + }, + response: &tfsdk.ValidateAttributeResponse{}, + expected: &tfsdk.ValidateAttributeResponse{}, + }, + "request-config": { + block: testschema.BlockWithObjectValidators{ + Attributes: map[string]fwschema.Attribute{ + "testattr": testschema.AttributeWithStringValidators{}, + }, + Validators: []validator.Object{ + testvalidator.Object{ + ValidateObjectMethod: func(ctx context.Context, req validator.ObjectRequest, resp *validator.ObjectResponse) { + got := req.Config + expected := tfsdk.Config{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "testattr": tftypes.String, + }, + }, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "testattr": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "testattr": tftypes.NewValue(tftypes.String, "test"), + }, + ), + }, + ), + } + + if !got.Raw.Equal(expected.Raw) { + resp.Diagnostics.AddError( + "Unexpected ObjectRequest.Config", + fmt.Sprintf("expected %s, got: %s", expected.Raw, got.Raw), + ) + } + }, + }, + }, + }, + request: tfsdk.ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.ObjectValueMust( + map[string]attr.Type{"testattr": types.StringType}, + map[string]attr.Value{"testattr": types.StringValue("test")}, + ), + Config: tfsdk.Config{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "testattr": tftypes.String, + }, + }, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "testattr": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "testattr": tftypes.NewValue(tftypes.String, "test"), + }, + ), + }, + ), + }, + }, + response: &tfsdk.ValidateAttributeResponse{}, + expected: &tfsdk.ValidateAttributeResponse{}, + }, + "request-configvalue": { + block: testschema.BlockWithObjectValidators{ + Attributes: map[string]fwschema.Attribute{ + "testattr": testschema.AttributeWithStringValidators{}, + }, + Validators: []validator.Object{ + testvalidator.Object{ + ValidateObjectMethod: func(ctx context.Context, req validator.ObjectRequest, resp *validator.ObjectResponse) { + got := req.ConfigValue + expected := types.ObjectValueMust( + map[string]attr.Type{"testattr": types.StringType}, + map[string]attr.Value{"testattr": types.StringValue("test")}, + ) + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected ObjectRequest.ConfigValue", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: tfsdk.ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.ObjectValueMust( + map[string]attr.Type{"testattr": types.StringType}, + map[string]attr.Value{"testattr": types.StringValue("test")}, + ), + }, + response: &tfsdk.ValidateAttributeResponse{}, + expected: &tfsdk.ValidateAttributeResponse{}, + }, + "response-diagnostics": { + block: testschema.BlockWithObjectValidators{ + Attributes: map[string]fwschema.Attribute{ + "testattr": testschema.AttributeWithStringValidators{}, + }, + Validators: []validator.Object{ + testvalidator.Object{ + ValidateObjectMethod: func(ctx context.Context, req validator.ObjectRequest, resp *validator.ObjectResponse) { + resp.Diagnostics.AddAttributeWarning(req.Path, "New Warning Summary", "New Warning Details") + resp.Diagnostics.AddAttributeError(req.Path, "New Error Summary", "New Error Details") + }, + }, + }, + }, + request: tfsdk.ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.ObjectValueMust( + map[string]attr.Type{"testattr": types.StringType}, + map[string]attr.Value{"testattr": types.StringValue("test")}, + ), + }, + response: &tfsdk.ValidateAttributeResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewAttributeWarningDiagnostic( + path.Root("other"), + "Existing Warning Summary", + "Existing Warning Details", + ), + diag.NewAttributeErrorDiagnostic( + path.Root("other"), + "Existing Error Summary", + "Existing Error Details", + ), + }, + }, + expected: &tfsdk.ValidateAttributeResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewAttributeWarningDiagnostic( + path.Root("other"), + "Existing Warning Summary", + "Existing Warning Details", + ), + diag.NewAttributeErrorDiagnostic( + path.Root("other"), + "Existing Error Summary", + "Existing Error Details", + ), + diag.NewAttributeWarningDiagnostic( + path.Root("test"), + "New Warning Summary", + "New Warning Details", + ), + diag.NewAttributeErrorDiagnostic( + path.Root("test"), + "New Error Summary", + "New Error Details", + ), + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + BlockValidateObject(context.Background(), testCase.block, testCase.request, testCase.response) + + if diff := cmp.Diff(testCase.response, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestBlockValidateSet(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block fwxschema.BlockWithSetValidators + request tfsdk.ValidateAttributeRequest + response *tfsdk.ValidateAttributeResponse + expected *tfsdk.ValidateAttributeResponse + }{ + "request-path": { + block: testschema.BlockWithSetValidators{ + Attributes: map[string]fwschema.Attribute{ + "testattr": testschema.AttributeWithStringValidators{}, + }, + Validators: []validator.Set{ + testvalidator.Set{ + ValidateSetMethod: func(ctx context.Context, req validator.SetRequest, resp *validator.SetResponse) { + got := req.Path + expected := path.Root("test") + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected SetRequest.Path", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: tfsdk.ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.SetValueMust( + types.ObjectType{ + AttrTypes: map[string]attr.Type{"testattr": types.StringType}, + }, + []attr.Value{ + types.ObjectValueMust( + map[string]attr.Type{"testattr": types.StringType}, + map[string]attr.Value{"testattr": types.StringValue("test")}, + ), + }, + ), + }, + response: &tfsdk.ValidateAttributeResponse{}, + expected: &tfsdk.ValidateAttributeResponse{}, + }, + "request-pathexpression": { + block: testschema.BlockWithSetValidators{ + Attributes: map[string]fwschema.Attribute{ + "testattr": testschema.AttributeWithStringValidators{}, + }, + Validators: []validator.Set{ + testvalidator.Set{ + ValidateSetMethod: func(ctx context.Context, req validator.SetRequest, resp *validator.SetResponse) { + got := req.PathExpression + expected := path.MatchRoot("test") + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected SetRequest.PathExpression", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: tfsdk.ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), + AttributeConfig: types.SetValueMust( + types.ObjectType{ + AttrTypes: map[string]attr.Type{"testattr": types.StringType}, + }, + []attr.Value{ + types.ObjectValueMust( + map[string]attr.Type{"testattr": types.StringType}, + map[string]attr.Value{"testattr": types.StringValue("test")}, + ), + }, + ), + }, + response: &tfsdk.ValidateAttributeResponse{}, + expected: &tfsdk.ValidateAttributeResponse{}, + }, + "request-config": { + block: testschema.BlockWithSetValidators{ + Attributes: map[string]fwschema.Attribute{ + "testattr": testschema.AttributeWithStringValidators{}, + }, + Validators: []validator.Set{ + testvalidator.Set{ + ValidateSetMethod: func(ctx context.Context, req validator.SetRequest, resp *validator.SetResponse) { + got := req.Config + expected := tfsdk.Config{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "testattr": tftypes.String, + }, + }, + }, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "testattr": tftypes.String, + }, + }, + }, + []tftypes.Value{ + tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "testattr": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "testattr": tftypes.NewValue(tftypes.String, "test"), + }, + ), + }, + ), + }, + ), + } + + if !got.Raw.Equal(expected.Raw) { + resp.Diagnostics.AddError( + "Unexpected SetRequest.Config", + fmt.Sprintf("expected %s, got: %s", expected.Raw, got.Raw), + ) + } + }, + }, + }, + }, + request: tfsdk.ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.SetValueMust( + types.ObjectType{ + AttrTypes: map[string]attr.Type{"testattr": types.StringType}, + }, + []attr.Value{ + types.ObjectValueMust( + map[string]attr.Type{"testattr": types.StringType}, + map[string]attr.Value{"testattr": types.StringValue("test")}, + ), + }, + ), + Config: tfsdk.Config{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "testattr": tftypes.String, + }, + }, + }, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "testattr": tftypes.String, + }, + }, + }, + []tftypes.Value{ + tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "testattr": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "testattr": tftypes.NewValue(tftypes.String, "test"), + }, + ), + }, + ), + }, + ), + }, + }, + response: &tfsdk.ValidateAttributeResponse{}, + expected: &tfsdk.ValidateAttributeResponse{}, + }, + "request-configvalue": { + block: testschema.BlockWithSetValidators{ + Attributes: map[string]fwschema.Attribute{ + "testattr": testschema.AttributeWithStringValidators{}, + }, + Validators: []validator.Set{ + testvalidator.Set{ + ValidateSetMethod: func(ctx context.Context, req validator.SetRequest, resp *validator.SetResponse) { + got := req.ConfigValue + expected := types.SetValueMust( + types.ObjectType{ + AttrTypes: map[string]attr.Type{"testattr": types.StringType}, + }, + []attr.Value{ + types.ObjectValueMust( + map[string]attr.Type{"testattr": types.StringType}, + map[string]attr.Value{"testattr": types.StringValue("test")}, + ), + }, + ) + + if !got.Equal(expected) { + resp.Diagnostics.AddError( + "Unexpected SetRequest.ConfigValue", + fmt.Sprintf("expected %s, got: %s", expected, got), + ) + } + }, + }, + }, + }, + request: tfsdk.ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.SetValueMust( + types.ObjectType{ + AttrTypes: map[string]attr.Type{"testattr": types.StringType}, + }, + []attr.Value{ + types.ObjectValueMust( + map[string]attr.Type{"testattr": types.StringType}, + map[string]attr.Value{"testattr": types.StringValue("test")}, + ), + }, + ), + }, + response: &tfsdk.ValidateAttributeResponse{}, + expected: &tfsdk.ValidateAttributeResponse{}, + }, + "response-diagnostics": { + block: testschema.BlockWithSetValidators{ + Attributes: map[string]fwschema.Attribute{ + "testattr": testschema.AttributeWithStringValidators{}, + }, + Validators: []validator.Set{ + testvalidator.Set{ + ValidateSetMethod: func(ctx context.Context, req validator.SetRequest, resp *validator.SetResponse) { + resp.Diagnostics.AddAttributeWarning(req.Path, "New Warning Summary", "New Warning Details") + resp.Diagnostics.AddAttributeError(req.Path, "New Error Summary", "New Error Details") + }, + }, + }, + }, + request: tfsdk.ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.SetValueMust( + types.ObjectType{ + AttrTypes: map[string]attr.Type{"testattr": types.StringType}, + }, + []attr.Value{ + types.ObjectValueMust( + map[string]attr.Type{"testattr": types.StringType}, + map[string]attr.Value{"testattr": types.StringValue("test")}, + ), + }, + ), + }, + response: &tfsdk.ValidateAttributeResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewAttributeWarningDiagnostic( + path.Root("other"), + "Existing Warning Summary", + "Existing Warning Details", + ), + diag.NewAttributeErrorDiagnostic( + path.Root("other"), + "Existing Error Summary", + "Existing Error Details", + ), + }, + }, + expected: &tfsdk.ValidateAttributeResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewAttributeWarningDiagnostic( + path.Root("other"), + "Existing Warning Summary", + "Existing Warning Details", + ), + diag.NewAttributeErrorDiagnostic( + path.Root("other"), + "Existing Error Summary", + "Existing Error Details", + ), + diag.NewAttributeWarningDiagnostic( + path.Root("test"), + "New Warning Summary", + "New Warning Details", + ), + diag.NewAttributeErrorDiagnostic( + path.Root("test"), + "New Error Summary", + "New Error Details", + ), + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + BlockValidateSet(context.Background(), testCase.block, testCase.request, testCase.response) + + if diff := cmp.Diff(testCase.response, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestBlockMaxItemsDiagnostic(t *testing.T) { t.Parallel() diff --git a/internal/testing/testschema/attributewithboolvalidators.go b/internal/testing/testschema/attributewithboolvalidators.go new file mode 100644 index 000000000..365f74d11 --- /dev/null +++ b/internal/testing/testschema/attributewithboolvalidators.go @@ -0,0 +1,94 @@ +package testschema + +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-go/tftypes" +) + +var _ fwxschema.AttributeWithBoolValidators = AttributeWithBoolValidators{} + +type AttributeWithBoolValidators struct { + Computed bool + DeprecationMessage string + Description string + MarkdownDescription string + Optional bool + Required bool + Sensitive bool + Validators []validator.Bool +} + +// ApplyTerraform5AttributePathStep satisfies the fwschema.Attribute interface. +func (a AttributeWithBoolValidators) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (any, error) { + return a.GetType().ApplyTerraform5AttributePathStep(step) +} + +// BoolValidators satisfies the fwxschema.AttributeWithBoolValidators interface. +func (a AttributeWithBoolValidators) BoolValidators() []validator.Bool { + return a.Validators +} + +// Equal satisfies the fwschema.Attribute interface. +func (a AttributeWithBoolValidators) Equal(o fwschema.Attribute) bool { + _, ok := o.(AttributeWithBoolValidators) + + if !ok { + return false + } + + return fwschema.AttributesEqual(a, o) +} + +// FrameworkType satisfies the fwschema.Attribute interface. +func (a AttributeWithBoolValidators) FrameworkType() attr.Type { + return a.GetType() +} + +// GetAttributes satisfies the fwschema.Attribute interface. +func (a AttributeWithBoolValidators) GetAttributes() fwschema.NestedAttributes { + return nil +} + +// GetDeprecationMessage satisfies the fwschema.Attribute interface. +func (a AttributeWithBoolValidators) GetDeprecationMessage() string { + return a.DeprecationMessage +} + +// GetDescription satisfies the fwschema.Attribute interface. +func (a AttributeWithBoolValidators) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription satisfies the fwschema.Attribute interface. +func (a AttributeWithBoolValidators) GetMarkdownDescription() string { + return a.MarkdownDescription +} + +// GetType satisfies the fwschema.Attribute interface. +func (a AttributeWithBoolValidators) GetType() attr.Type { + return types.BoolType +} + +// IsComputed satisfies the fwschema.Attribute interface. +func (a AttributeWithBoolValidators) IsComputed() bool { + return a.Computed +} + +// IsOptional satisfies the fwschema.Attribute interface. +func (a AttributeWithBoolValidators) IsOptional() bool { + return a.Optional +} + +// IsRequired satisfies the fwschema.Attribute interface. +func (a AttributeWithBoolValidators) IsRequired() bool { + return a.Required +} + +// IsSensitive satisfies the fwschema.Attribute interface. +func (a AttributeWithBoolValidators) IsSensitive() bool { + return a.Sensitive +} diff --git a/internal/testing/testschema/attributewithfloat64validators.go b/internal/testing/testschema/attributewithfloat64validators.go new file mode 100644 index 000000000..f4d5fdef3 --- /dev/null +++ b/internal/testing/testschema/attributewithfloat64validators.go @@ -0,0 +1,94 @@ +package testschema + +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-go/tftypes" +) + +var _ fwxschema.AttributeWithFloat64Validators = AttributeWithFloat64Validators{} + +type AttributeWithFloat64Validators struct { + Computed bool + DeprecationMessage string + Description string + MarkdownDescription string + Optional bool + Required bool + Sensitive bool + Validators []validator.Float64 +} + +// ApplyTerraform5AttributePathStep satisfies the fwschema.Attribute interface. +func (a AttributeWithFloat64Validators) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (any, error) { + return a.GetType().ApplyTerraform5AttributePathStep(step) +} + +// Equal satisfies the fwschema.Attribute interface. +func (a AttributeWithFloat64Validators) Equal(o fwschema.Attribute) bool { + _, ok := o.(AttributeWithFloat64Validators) + + if !ok { + return false + } + + return fwschema.AttributesEqual(a, o) +} + +// Float64Validators satisfies the fwxschema.AttributeWithFloat64Validators interface. +func (a AttributeWithFloat64Validators) Float64Validators() []validator.Float64 { + return a.Validators +} + +// FrameworkType satisfies the fwschema.Attribute interface. +func (a AttributeWithFloat64Validators) FrameworkType() attr.Type { + return a.GetType() +} + +// GetAttributes satisfies the fwschema.Attribute interface. +func (a AttributeWithFloat64Validators) GetAttributes() fwschema.NestedAttributes { + return nil +} + +// GetDeprecationMessage satisfies the fwschema.Attribute interface. +func (a AttributeWithFloat64Validators) GetDeprecationMessage() string { + return a.DeprecationMessage +} + +// GetDescription satisfies the fwschema.Attribute interface. +func (a AttributeWithFloat64Validators) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription satisfies the fwschema.Attribute interface. +func (a AttributeWithFloat64Validators) GetMarkdownDescription() string { + return a.MarkdownDescription +} + +// GetType satisfies the fwschema.Attribute interface. +func (a AttributeWithFloat64Validators) GetType() attr.Type { + return types.Float64Type +} + +// IsComputed satisfies the fwschema.Attribute interface. +func (a AttributeWithFloat64Validators) IsComputed() bool { + return a.Computed +} + +// IsOptional satisfies the fwschema.Attribute interface. +func (a AttributeWithFloat64Validators) IsOptional() bool { + return a.Optional +} + +// IsRequired satisfies the fwschema.Attribute interface. +func (a AttributeWithFloat64Validators) IsRequired() bool { + return a.Required +} + +// IsSensitive satisfies the fwschema.Attribute interface. +func (a AttributeWithFloat64Validators) IsSensitive() bool { + return a.Sensitive +} diff --git a/internal/testing/testschema/attributewithint64validators.go b/internal/testing/testschema/attributewithint64validators.go new file mode 100644 index 000000000..32b40edab --- /dev/null +++ b/internal/testing/testschema/attributewithint64validators.go @@ -0,0 +1,94 @@ +package testschema + +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-go/tftypes" +) + +var _ fwxschema.AttributeWithInt64Validators = AttributeWithInt64Validators{} + +type AttributeWithInt64Validators struct { + Computed bool + DeprecationMessage string + Description string + MarkdownDescription string + Optional bool + Required bool + Sensitive bool + Validators []validator.Int64 +} + +// ApplyTerraform5AttributePathStep satisfies the fwschema.Attribute interface. +func (a AttributeWithInt64Validators) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (any, error) { + return a.GetType().ApplyTerraform5AttributePathStep(step) +} + +// Equal satisfies the fwschema.Attribute interface. +func (a AttributeWithInt64Validators) Equal(o fwschema.Attribute) bool { + _, ok := o.(AttributeWithInt64Validators) + + if !ok { + return false + } + + return fwschema.AttributesEqual(a, o) +} + +// FrameworkType satisfies the fwschema.Attribute interface. +func (a AttributeWithInt64Validators) FrameworkType() attr.Type { + return a.GetType() +} + +// GetAttributes satisfies the fwschema.Attribute interface. +func (a AttributeWithInt64Validators) GetAttributes() fwschema.NestedAttributes { + return nil +} + +// GetDeprecationMessage satisfies the fwschema.Attribute interface. +func (a AttributeWithInt64Validators) GetDeprecationMessage() string { + return a.DeprecationMessage +} + +// GetDescription satisfies the fwschema.Attribute interface. +func (a AttributeWithInt64Validators) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription satisfies the fwschema.Attribute interface. +func (a AttributeWithInt64Validators) GetMarkdownDescription() string { + return a.MarkdownDescription +} + +// GetType satisfies the fwschema.Attribute interface. +func (a AttributeWithInt64Validators) GetType() attr.Type { + return types.Int64Type +} + +// Int64Validators satisfies the fwxschema.AttributeWithInt64Validators interface. +func (a AttributeWithInt64Validators) Int64Validators() []validator.Int64 { + return a.Validators +} + +// IsComputed satisfies the fwschema.Attribute interface. +func (a AttributeWithInt64Validators) IsComputed() bool { + return a.Computed +} + +// IsOptional satisfies the fwschema.Attribute interface. +func (a AttributeWithInt64Validators) IsOptional() bool { + return a.Optional +} + +// IsRequired satisfies the fwschema.Attribute interface. +func (a AttributeWithInt64Validators) IsRequired() bool { + return a.Required +} + +// IsSensitive satisfies the fwschema.Attribute interface. +func (a AttributeWithInt64Validators) IsSensitive() bool { + return a.Sensitive +} diff --git a/internal/testing/testschema/attributewithlistvalidators.go b/internal/testing/testschema/attributewithlistvalidators.go new file mode 100644 index 000000000..eb3a637c0 --- /dev/null +++ b/internal/testing/testschema/attributewithlistvalidators.go @@ -0,0 +1,97 @@ +package testschema + +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-go/tftypes" +) + +var _ fwxschema.AttributeWithListValidators = AttributeWithListValidators{} + +type AttributeWithListValidators struct { + Computed bool + DeprecationMessage string + Description string + ElementType attr.Type + MarkdownDescription string + Optional bool + Required bool + Sensitive bool + Validators []validator.List +} + +// ApplyTerraform5AttributePathStep satisfies the fwschema.Attribute interface. +func (a AttributeWithListValidators) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (any, error) { + return a.GetType().ApplyTerraform5AttributePathStep(step) +} + +// Equal satisfies the fwschema.Attribute interface. +func (a AttributeWithListValidators) Equal(o fwschema.Attribute) bool { + _, ok := o.(AttributeWithListValidators) + + if !ok { + return false + } + + return fwschema.AttributesEqual(a, o) +} + +// FrameworkType satisfies the fwschema.Attribute interface. +func (a AttributeWithListValidators) FrameworkType() attr.Type { + return a.GetType() +} + +// GetAttributes satisfies the fwschema.Attribute interface. +func (a AttributeWithListValidators) GetAttributes() fwschema.NestedAttributes { + return nil +} + +// GetDeprecationMessage satisfies the fwschema.Attribute interface. +func (a AttributeWithListValidators) GetDeprecationMessage() string { + return a.DeprecationMessage +} + +// GetDescription satisfies the fwschema.Attribute interface. +func (a AttributeWithListValidators) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription satisfies the fwschema.Attribute interface. +func (a AttributeWithListValidators) GetMarkdownDescription() string { + return a.MarkdownDescription +} + +// GetType satisfies the fwschema.Attribute interface. +func (a AttributeWithListValidators) GetType() attr.Type { + return types.ListType{ + ElemType: a.ElementType, + } +} + +// IsComputed satisfies the fwschema.Attribute interface. +func (a AttributeWithListValidators) IsComputed() bool { + return a.Computed +} + +// IsOptional satisfies the fwschema.Attribute interface. +func (a AttributeWithListValidators) IsOptional() bool { + return a.Optional +} + +// IsRequired satisfies the fwschema.Attribute interface. +func (a AttributeWithListValidators) IsRequired() bool { + return a.Required +} + +// IsSensitive satisfies the fwschema.Attribute interface. +func (a AttributeWithListValidators) IsSensitive() bool { + return a.Sensitive +} + +// ListValidators satisfies the fwxschema.AttributeWithListValidators interface. +func (a AttributeWithListValidators) ListValidators() []validator.List { + return a.Validators +} diff --git a/internal/testing/testschema/attributewithmapvalidators.go b/internal/testing/testschema/attributewithmapvalidators.go new file mode 100644 index 000000000..399999224 --- /dev/null +++ b/internal/testing/testschema/attributewithmapvalidators.go @@ -0,0 +1,97 @@ +package testschema + +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-go/tftypes" +) + +var _ fwxschema.AttributeWithMapValidators = AttributeWithMapValidators{} + +type AttributeWithMapValidators struct { + Computed bool + DeprecationMessage string + Description string + ElementType attr.Type + MarkdownDescription string + Optional bool + Required bool + Sensitive bool + Validators []validator.Map +} + +// ApplyTerraform5AttributePathStep satisfies the fwschema.Attribute interface. +func (a AttributeWithMapValidators) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (any, error) { + return a.GetType().ApplyTerraform5AttributePathStep(step) +} + +// Equal satisfies the fwschema.Attribute interface. +func (a AttributeWithMapValidators) Equal(o fwschema.Attribute) bool { + _, ok := o.(AttributeWithMapValidators) + + if !ok { + return false + } + + return fwschema.AttributesEqual(a, o) +} + +// FrameworkType satisfies the fwschema.Attribute interface. +func (a AttributeWithMapValidators) FrameworkType() attr.Type { + return a.GetType() +} + +// GetAttributes satisfies the fwschema.Attribute interface. +func (a AttributeWithMapValidators) GetAttributes() fwschema.NestedAttributes { + return nil +} + +// GetDeprecationMessage satisfies the fwschema.Attribute interface. +func (a AttributeWithMapValidators) GetDeprecationMessage() string { + return a.DeprecationMessage +} + +// GetDescription satisfies the fwschema.Attribute interface. +func (a AttributeWithMapValidators) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription satisfies the fwschema.Attribute interface. +func (a AttributeWithMapValidators) GetMarkdownDescription() string { + return a.MarkdownDescription +} + +// GetType satisfies the fwschema.Attribute interface. +func (a AttributeWithMapValidators) GetType() attr.Type { + return types.MapType{ + ElemType: a.ElementType, + } +} + +// IsComputed satisfies the fwschema.Attribute interface. +func (a AttributeWithMapValidators) IsComputed() bool { + return a.Computed +} + +// IsOptional satisfies the fwschema.Attribute interface. +func (a AttributeWithMapValidators) IsOptional() bool { + return a.Optional +} + +// IsRequired satisfies the fwschema.Attribute interface. +func (a AttributeWithMapValidators) IsRequired() bool { + return a.Required +} + +// IsSensitive satisfies the fwschema.Attribute interface. +func (a AttributeWithMapValidators) IsSensitive() bool { + return a.Sensitive +} + +// MapValidators satisfies the fwxschema.AttributeWithMapValidators interface. +func (a AttributeWithMapValidators) MapValidators() []validator.Map { + return a.Validators +} diff --git a/internal/testing/testschema/attributewithnumbervalidators.go b/internal/testing/testschema/attributewithnumbervalidators.go new file mode 100644 index 000000000..690038294 --- /dev/null +++ b/internal/testing/testschema/attributewithnumbervalidators.go @@ -0,0 +1,94 @@ +package testschema + +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-go/tftypes" +) + +var _ fwxschema.AttributeWithNumberValidators = AttributeWithNumberValidators{} + +type AttributeWithNumberValidators struct { + Computed bool + DeprecationMessage string + Description string + MarkdownDescription string + Optional bool + Required bool + Sensitive bool + Validators []validator.Number +} + +// ApplyTerraform5AttributePathStep satisfies the fwschema.Attribute interface. +func (a AttributeWithNumberValidators) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (any, error) { + return a.GetType().ApplyTerraform5AttributePathStep(step) +} + +// Equal satisfies the fwschema.Attribute interface. +func (a AttributeWithNumberValidators) Equal(o fwschema.Attribute) bool { + _, ok := o.(AttributeWithNumberValidators) + + if !ok { + return false + } + + return fwschema.AttributesEqual(a, o) +} + +// FrameworkType satisfies the fwschema.Attribute interface. +func (a AttributeWithNumberValidators) FrameworkType() attr.Type { + return a.GetType() +} + +// GetAttributes satisfies the fwschema.Attribute interface. +func (a AttributeWithNumberValidators) GetAttributes() fwschema.NestedAttributes { + return nil +} + +// GetDeprecationMessage satisfies the fwschema.Attribute interface. +func (a AttributeWithNumberValidators) GetDeprecationMessage() string { + return a.DeprecationMessage +} + +// GetDescription satisfies the fwschema.Attribute interface. +func (a AttributeWithNumberValidators) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription satisfies the fwschema.Attribute interface. +func (a AttributeWithNumberValidators) GetMarkdownDescription() string { + return a.MarkdownDescription +} + +// GetType satisfies the fwschema.Attribute interface. +func (a AttributeWithNumberValidators) GetType() attr.Type { + return types.NumberType +} + +// IsComputed satisfies the fwschema.Attribute interface. +func (a AttributeWithNumberValidators) IsComputed() bool { + return a.Computed +} + +// IsOptional satisfies the fwschema.Attribute interface. +func (a AttributeWithNumberValidators) IsOptional() bool { + return a.Optional +} + +// IsRequired satisfies the fwschema.Attribute interface. +func (a AttributeWithNumberValidators) IsRequired() bool { + return a.Required +} + +// IsSensitive satisfies the fwschema.Attribute interface. +func (a AttributeWithNumberValidators) IsSensitive() bool { + return a.Sensitive +} + +// NumberValidators satisfies the fwxschema.AttributeWithNumberValidators interface. +func (a AttributeWithNumberValidators) NumberValidators() []validator.Number { + return a.Validators +} diff --git a/internal/testing/testschema/attributewithobjectvalidators.go b/internal/testing/testschema/attributewithobjectvalidators.go new file mode 100644 index 000000000..a73ddbf52 --- /dev/null +++ b/internal/testing/testschema/attributewithobjectvalidators.go @@ -0,0 +1,97 @@ +package testschema + +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-go/tftypes" +) + +var _ fwxschema.AttributeWithObjectValidators = AttributeWithObjectValidators{} + +type AttributeWithObjectValidators struct { + AttributeTypes map[string]attr.Type + Computed bool + DeprecationMessage string + Description string + MarkdownDescription string + Optional bool + Required bool + Sensitive bool + Validators []validator.Object +} + +// ApplyTerraform5AttributePathStep satisfies the fwschema.Attribute interface. +func (a AttributeWithObjectValidators) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (any, error) { + return a.GetType().ApplyTerraform5AttributePathStep(step) +} + +// Equal satisfies the fwschema.Attribute interface. +func (a AttributeWithObjectValidators) Equal(o fwschema.Attribute) bool { + _, ok := o.(AttributeWithObjectValidators) + + if !ok { + return false + } + + return fwschema.AttributesEqual(a, o) +} + +// FrameworkType satisfies the fwschema.Attribute interface. +func (a AttributeWithObjectValidators) FrameworkType() attr.Type { + return a.GetType() +} + +// GetAttributes satisfies the fwschema.Attribute interface. +func (a AttributeWithObjectValidators) GetAttributes() fwschema.NestedAttributes { + return nil +} + +// GetDeprecationMessage satisfies the fwschema.Attribute interface. +func (a AttributeWithObjectValidators) GetDeprecationMessage() string { + return a.DeprecationMessage +} + +// GetDescription satisfies the fwschema.Attribute interface. +func (a AttributeWithObjectValidators) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription satisfies the fwschema.Attribute interface. +func (a AttributeWithObjectValidators) GetMarkdownDescription() string { + return a.MarkdownDescription +} + +// GetType satisfies the fwschema.Attribute interface. +func (a AttributeWithObjectValidators) GetType() attr.Type { + return types.ObjectType{ + AttrTypes: a.AttributeTypes, + } +} + +// IsComputed satisfies the fwschema.Attribute interface. +func (a AttributeWithObjectValidators) IsComputed() bool { + return a.Computed +} + +// IsOptional satisfies the fwschema.Attribute interface. +func (a AttributeWithObjectValidators) IsOptional() bool { + return a.Optional +} + +// IsRequired satisfies the fwschema.Attribute interface. +func (a AttributeWithObjectValidators) IsRequired() bool { + return a.Required +} + +// IsSensitive satisfies the fwschema.Attribute interface. +func (a AttributeWithObjectValidators) IsSensitive() bool { + return a.Sensitive +} + +// ObjectValidators satisfies the fwxschema.AttributeWithObjectValidators interface. +func (a AttributeWithObjectValidators) ObjectValidators() []validator.Object { + return a.Validators +} diff --git a/internal/testing/testschema/attributewithsetvalidators.go b/internal/testing/testschema/attributewithsetvalidators.go new file mode 100644 index 000000000..83f04e9f6 --- /dev/null +++ b/internal/testing/testschema/attributewithsetvalidators.go @@ -0,0 +1,97 @@ +package testschema + +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-go/tftypes" +) + +var _ fwxschema.AttributeWithSetValidators = AttributeWithSetValidators{} + +type AttributeWithSetValidators struct { + Computed bool + DeprecationMessage string + Description string + ElementType attr.Type + MarkdownDescription string + Optional bool + Required bool + Sensitive bool + Validators []validator.Set +} + +// ApplyTerraform5AttributePathStep satisfies the fwschema.Attribute interface. +func (a AttributeWithSetValidators) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (any, error) { + return a.GetType().ApplyTerraform5AttributePathStep(step) +} + +// Equal satisfies the fwschema.Attribute interface. +func (a AttributeWithSetValidators) Equal(o fwschema.Attribute) bool { + _, ok := o.(AttributeWithSetValidators) + + if !ok { + return false + } + + return fwschema.AttributesEqual(a, o) +} + +// FrameworkType satisfies the fwschema.Attribute interface. +func (a AttributeWithSetValidators) FrameworkType() attr.Type { + return a.GetType() +} + +// GetAttributes satisfies the fwschema.Attribute interface. +func (a AttributeWithSetValidators) GetAttributes() fwschema.NestedAttributes { + return nil +} + +// GetDeprecationMessage satisfies the fwschema.Attribute interface. +func (a AttributeWithSetValidators) GetDeprecationMessage() string { + return a.DeprecationMessage +} + +// GetDescription satisfies the fwschema.Attribute interface. +func (a AttributeWithSetValidators) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription satisfies the fwschema.Attribute interface. +func (a AttributeWithSetValidators) GetMarkdownDescription() string { + return a.MarkdownDescription +} + +// GetType satisfies the fwschema.Attribute interface. +func (a AttributeWithSetValidators) GetType() attr.Type { + return types.SetType{ + ElemType: a.ElementType, + } +} + +// IsComputed satisfies the fwschema.Attribute interface. +func (a AttributeWithSetValidators) IsComputed() bool { + return a.Computed +} + +// IsOptional satisfies the fwschema.Attribute interface. +func (a AttributeWithSetValidators) IsOptional() bool { + return a.Optional +} + +// IsRequired satisfies the fwschema.Attribute interface. +func (a AttributeWithSetValidators) IsRequired() bool { + return a.Required +} + +// IsSensitive satisfies the fwschema.Attribute interface. +func (a AttributeWithSetValidators) IsSensitive() bool { + return a.Sensitive +} + +// SetValidators satisfies the fwxschema.AttributeWithSetValidators interface. +func (a AttributeWithSetValidators) SetValidators() []validator.Set { + return a.Validators +} diff --git a/internal/testing/testschema/attributewithstringvalidators.go b/internal/testing/testschema/attributewithstringvalidators.go new file mode 100644 index 000000000..b96583a1d --- /dev/null +++ b/internal/testing/testschema/attributewithstringvalidators.go @@ -0,0 +1,94 @@ +package testschema + +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-go/tftypes" +) + +var _ fwxschema.AttributeWithStringValidators = AttributeWithStringValidators{} + +type AttributeWithStringValidators struct { + Computed bool + DeprecationMessage string + Description string + MarkdownDescription string + Optional bool + Required bool + Sensitive bool + Validators []validator.String +} + +// ApplyTerraform5AttributePathStep satisfies the fwschema.Attribute interface. +func (a AttributeWithStringValidators) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (any, error) { + return a.GetType().ApplyTerraform5AttributePathStep(step) +} + +// Equal satisfies the fwschema.Attribute interface. +func (a AttributeWithStringValidators) Equal(o fwschema.Attribute) bool { + _, ok := o.(AttributeWithStringValidators) + + if !ok { + return false + } + + return fwschema.AttributesEqual(a, o) +} + +// FrameworkType satisfies the fwschema.Attribute interface. +func (a AttributeWithStringValidators) FrameworkType() attr.Type { + return a.GetType() +} + +// GetAttributes satisfies the fwschema.Attribute interface. +func (a AttributeWithStringValidators) GetAttributes() fwschema.NestedAttributes { + return nil +} + +// GetDeprecationMessage satisfies the fwschema.Attribute interface. +func (a AttributeWithStringValidators) GetDeprecationMessage() string { + return a.DeprecationMessage +} + +// GetDescription satisfies the fwschema.Attribute interface. +func (a AttributeWithStringValidators) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription satisfies the fwschema.Attribute interface. +func (a AttributeWithStringValidators) GetMarkdownDescription() string { + return a.MarkdownDescription +} + +// GetType satisfies the fwschema.Attribute interface. +func (a AttributeWithStringValidators) GetType() attr.Type { + return types.StringType +} + +// IsComputed satisfies the fwschema.Attribute interface. +func (a AttributeWithStringValidators) IsComputed() bool { + return a.Computed +} + +// IsOptional satisfies the fwschema.Attribute interface. +func (a AttributeWithStringValidators) IsOptional() bool { + return a.Optional +} + +// IsRequired satisfies the fwschema.Attribute interface. +func (a AttributeWithStringValidators) IsRequired() bool { + return a.Required +} + +// IsSensitive satisfies the fwschema.Attribute interface. +func (a AttributeWithStringValidators) IsSensitive() bool { + return a.Sensitive +} + +// StringValidators satisfies the fwxschema.AttributeWithStringValidators interface. +func (a AttributeWithStringValidators) StringValidators() []validator.String { + return a.Validators +} diff --git a/internal/testing/testschema/blockwithlistvalidators.go b/internal/testing/testschema/blockwithlistvalidators.go new file mode 100644 index 000000000..7d891d0fe --- /dev/null +++ b/internal/testing/testschema/blockwithlistvalidators.go @@ -0,0 +1,103 @@ +package testschema + +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-go/tftypes" +) + +var _ fwxschema.BlockWithListValidators = BlockWithListValidators{} + +type BlockWithListValidators struct { + Attributes map[string]fwschema.Attribute + Blocks map[string]fwschema.Block + DeprecationMessage string + Description string + MarkdownDescription string + MaxItems int64 + MinItems int64 + Validators []validator.List +} + +// ApplyTerraform5AttributePathStep satisfies the fwschema.Block interface. +func (b BlockWithListValidators) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (any, error) { + return b.Type().ApplyTerraform5AttributePathStep(step) +} + +// Equal satisfies the fwschema.Block interface. +func (b BlockWithListValidators) Equal(o fwschema.Block) bool { + _, ok := o.(BlockWithListValidators) + + if !ok { + return false + } + + return fwschema.BlocksEqual(b, o) +} + +// GetAttributes satisfies the fwschema.Block interface. +func (b BlockWithListValidators) GetAttributes() map[string]fwschema.Attribute { + return nil +} + +// GetBlocks satisfies the fwschema.Block interface. +func (b BlockWithListValidators) GetBlocks() map[string]fwschema.Block { + return nil +} + +// GetDeprecationMessage satisfies the fwschema.Block interface. +func (b BlockWithListValidators) GetDeprecationMessage() string { + return b.DeprecationMessage +} + +// GetDescription satisfies the fwschema.Block interface. +func (b BlockWithListValidators) GetDescription() string { + return b.Description +} + +// GetMarkdownDescription satisfies the fwschema.Block interface. +func (b BlockWithListValidators) GetMarkdownDescription() string { + return b.MarkdownDescription +} + +// GetMaxItems satisfies the fwschema.Block interface. +func (b BlockWithListValidators) GetMaxItems() int64 { + return b.MaxItems +} + +// GetMinItems satisfies the fwschema.Block interface. +func (b BlockWithListValidators) GetMinItems() int64 { + return b.MinItems +} + +// GetNestingMode satisfies the fwschema.Block interface. +func (b BlockWithListValidators) GetNestingMode() fwschema.BlockNestingMode { + return fwschema.BlockNestingModeList +} + +// ListValidators satisfies the fwxschema.BlockWithListValidators interface. +func (b BlockWithListValidators) ListValidators() []validator.List { + return b.Validators +} + +// Type satisfies the fwschema.Block interface. +func (b BlockWithListValidators) Type() attr.Type { + attrType := types.ObjectType{ + AttrTypes: make(map[string]attr.Type, len(b.GetAttributes())+len(b.GetBlocks())), + } + + for attrName, attr := range b.GetAttributes() { + attrType.AttrTypes[attrName] = attr.FrameworkType() + } + + for blockName, block := range b.GetBlocks() { + attrType.AttrTypes[blockName] = block.Type() + } + + return types.ListType{ + ElemType: attrType, + } +} diff --git a/internal/testing/testschema/blockwithobjectvalidators.go b/internal/testing/testschema/blockwithobjectvalidators.go new file mode 100644 index 000000000..45deac000 --- /dev/null +++ b/internal/testing/testschema/blockwithobjectvalidators.go @@ -0,0 +1,101 @@ +package testschema + +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-go/tftypes" +) + +var _ fwxschema.BlockWithObjectValidators = BlockWithObjectValidators{} + +type BlockWithObjectValidators struct { + Attributes map[string]fwschema.Attribute + Blocks map[string]fwschema.Block + DeprecationMessage string + Description string + MarkdownDescription string + MaxItems int64 + MinItems int64 + Validators []validator.Object +} + +// ApplyTerraform5AttributePathStep satisfies the fwschema.Block interface. +func (b BlockWithObjectValidators) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (any, error) { + return b.Type().ApplyTerraform5AttributePathStep(step) +} + +// Equal satisfies the fwschema.Block interface. +func (b BlockWithObjectValidators) Equal(o fwschema.Block) bool { + _, ok := o.(BlockWithObjectValidators) + + if !ok { + return false + } + + return fwschema.BlocksEqual(b, o) +} + +// GetAttributes satisfies the fwschema.Block interface. +func (b BlockWithObjectValidators) GetAttributes() map[string]fwschema.Attribute { + return nil +} + +// GetBlocks satisfies the fwschema.Block interface. +func (b BlockWithObjectValidators) GetBlocks() map[string]fwschema.Block { + return nil +} + +// GetDeprecationMessage satisfies the fwschema.Block interface. +func (b BlockWithObjectValidators) GetDeprecationMessage() string { + return b.DeprecationMessage +} + +// GetDescription satisfies the fwschema.Block interface. +func (b BlockWithObjectValidators) GetDescription() string { + return b.Description +} + +// GetMarkdownDescription satisfies the fwschema.Block interface. +func (b BlockWithObjectValidators) GetMarkdownDescription() string { + return b.MarkdownDescription +} + +// GetMaxItems satisfies the fwschema.Block interface. +func (b BlockWithObjectValidators) GetMaxItems() int64 { + return b.MaxItems +} + +// GetMinItems satisfies the fwschema.Block interface. +func (b BlockWithObjectValidators) GetMinItems() int64 { + return b.MinItems +} + +// GetNestingMode satisfies the fwschema.Block interface. +func (b BlockWithObjectValidators) GetNestingMode() fwschema.BlockNestingMode { + return fwschema.BlockNestingModeSingle +} + +// ObjectValidators satisfies the fwxschema.BlockWithObjectValidators interface. +func (b BlockWithObjectValidators) ObjectValidators() []validator.Object { + return b.Validators +} + +// Type satisfies the fwschema.Block interface. +func (b BlockWithObjectValidators) Type() attr.Type { + attrType := types.ObjectType{ + AttrTypes: make(map[string]attr.Type, len(b.GetAttributes())+len(b.GetBlocks())), + } + + for attrName, attr := range b.GetAttributes() { + attrType.AttrTypes[attrName] = attr.FrameworkType() + } + + for blockName, block := range b.GetBlocks() { + attrType.AttrTypes[blockName] = block.Type() + } + + return attrType +} diff --git a/internal/testing/testschema/blockwithsetvalidators.go b/internal/testing/testschema/blockwithsetvalidators.go new file mode 100644 index 000000000..ec3333811 --- /dev/null +++ b/internal/testing/testschema/blockwithsetvalidators.go @@ -0,0 +1,103 @@ +package testschema + +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-go/tftypes" +) + +var _ fwxschema.BlockWithSetValidators = BlockWithSetValidators{} + +type BlockWithSetValidators struct { + Attributes map[string]fwschema.Attribute + Blocks map[string]fwschema.Block + DeprecationMessage string + Description string + MarkdownDescription string + MaxItems int64 + MinItems int64 + Validators []validator.Set +} + +// ApplyTerraform5AttributePathStep satisfies the fwschema.Block interface. +func (b BlockWithSetValidators) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (any, error) { + return b.Type().ApplyTerraform5AttributePathStep(step) +} + +// Equal satisfies the fwschema.Block interface. +func (b BlockWithSetValidators) Equal(o fwschema.Block) bool { + _, ok := o.(BlockWithSetValidators) + + if !ok { + return false + } + + return fwschema.BlocksEqual(b, o) +} + +// GetAttributes satisfies the fwschema.Block interface. +func (b BlockWithSetValidators) GetAttributes() map[string]fwschema.Attribute { + return nil +} + +// GetBlocks satisfies the fwschema.Block interface. +func (b BlockWithSetValidators) GetBlocks() map[string]fwschema.Block { + return nil +} + +// GetDeprecationMessage satisfies the fwschema.Block interface. +func (b BlockWithSetValidators) GetDeprecationMessage() string { + return b.DeprecationMessage +} + +// GetDescription satisfies the fwschema.Block interface. +func (b BlockWithSetValidators) GetDescription() string { + return b.Description +} + +// GetMarkdownDescription satisfies the fwschema.Block interface. +func (b BlockWithSetValidators) GetMarkdownDescription() string { + return b.MarkdownDescription +} + +// GetMaxItems satisfies the fwschema.Block interface. +func (b BlockWithSetValidators) GetMaxItems() int64 { + return b.MaxItems +} + +// GetMinItems satisfies the fwschema.Block interface. +func (b BlockWithSetValidators) GetMinItems() int64 { + return b.MinItems +} + +// GetNestingMode satisfies the fwschema.Block interface. +func (b BlockWithSetValidators) GetNestingMode() fwschema.BlockNestingMode { + return fwschema.BlockNestingModeSet +} + +// SetValidators satisfies the fwxschema.BlockWithSetValidators interface. +func (b BlockWithSetValidators) SetValidators() []validator.Set { + return b.Validators +} + +// Type satisfies the fwschema.Block interface. +func (b BlockWithSetValidators) Type() attr.Type { + attrType := types.ObjectType{ + AttrTypes: make(map[string]attr.Type, len(b.GetAttributes())+len(b.GetBlocks())), + } + + for attrName, attr := range b.GetAttributes() { + attrType.AttrTypes[attrName] = attr.FrameworkType() + } + + for blockName, block := range b.GetBlocks() { + attrType.AttrTypes[blockName] = block.Type() + } + + return types.SetType{ + ElemType: attrType, + } +} diff --git a/internal/testing/testschema/doc.go b/internal/testing/testschema/doc.go new file mode 100644 index 000000000..d2ddfbe64 --- /dev/null +++ b/internal/testing/testschema/doc.go @@ -0,0 +1,2 @@ +// Package testschema contains declarative schema types for unit testing. +package testschema diff --git a/internal/testing/testvalidator/bool.go b/internal/testing/testvalidator/bool.go new file mode 100644 index 000000000..1a29b7f85 --- /dev/null +++ b/internal/testing/testvalidator/bool.go @@ -0,0 +1,44 @@ +package testvalidator + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/schema/validator" +) + +var _ validator.Bool = &Bool{} + +// Declarative validator.Bool for unit testing. +type Bool struct { + // Bool interface methods + DescriptionMethod func(context.Context) string + MarkdownDescriptionMethod func(context.Context) string + ValidateBoolMethod func(context.Context, validator.BoolRequest, *validator.BoolResponse) +} + +// Description satisfies the validator.Bool interface. +func (v Bool) Description(ctx context.Context) string { + if v.DescriptionMethod == nil { + return "" + } + + return v.DescriptionMethod(ctx) +} + +// MarkdownDescription satisfies the validator.Bool interface. +func (v Bool) MarkdownDescription(ctx context.Context) string { + if v.MarkdownDescriptionMethod == nil { + return "" + } + + return v.MarkdownDescriptionMethod(ctx) +} + +// Validate satisfies the validator.Bool interface. +func (v Bool) ValidateBool(ctx context.Context, req validator.BoolRequest, resp *validator.BoolResponse) { + if v.ValidateBoolMethod == nil { + return + } + + v.ValidateBoolMethod(ctx, req, resp) +} diff --git a/internal/testing/testvalidator/doc.go b/internal/testing/testvalidator/doc.go new file mode 100644 index 000000000..4ddd3ac30 --- /dev/null +++ b/internal/testing/testvalidator/doc.go @@ -0,0 +1,3 @@ +// Package testvalidator contains declarative schema/validator implementations +// for unit testing. +package testvalidator diff --git a/internal/testing/testvalidator/float64.go b/internal/testing/testvalidator/float64.go new file mode 100644 index 000000000..51ea49fdc --- /dev/null +++ b/internal/testing/testvalidator/float64.go @@ -0,0 +1,44 @@ +package testvalidator + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/schema/validator" +) + +var _ validator.Float64 = &Float64{} + +// Declarative validator.Float64 for unit testing. +type Float64 struct { + // Float64 interface methods + DescriptionMethod func(context.Context) string + MarkdownDescriptionMethod func(context.Context) string + ValidateFloat64Method func(context.Context, validator.Float64Request, *validator.Float64Response) +} + +// Description satisfies the validator.Float64 interface. +func (v Float64) Description(ctx context.Context) string { + if v.DescriptionMethod == nil { + return "" + } + + return v.DescriptionMethod(ctx) +} + +// MarkdownDescription satisfies the validator.Float64 interface. +func (v Float64) MarkdownDescription(ctx context.Context) string { + if v.MarkdownDescriptionMethod == nil { + return "" + } + + return v.MarkdownDescriptionMethod(ctx) +} + +// Validate satisfies the validator.Float64 interface. +func (v Float64) ValidateFloat64(ctx context.Context, req validator.Float64Request, resp *validator.Float64Response) { + if v.ValidateFloat64Method == nil { + return + } + + v.ValidateFloat64Method(ctx, req, resp) +} diff --git a/internal/testing/testvalidator/int64.go b/internal/testing/testvalidator/int64.go new file mode 100644 index 000000000..ed65e9c0c --- /dev/null +++ b/internal/testing/testvalidator/int64.go @@ -0,0 +1,44 @@ +package testvalidator + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/schema/validator" +) + +var _ validator.Int64 = &Int64{} + +// Declarative validator.Int64 for unit testing. +type Int64 struct { + // Int64 interface methods + DescriptionMethod func(context.Context) string + MarkdownDescriptionMethod func(context.Context) string + ValidateInt64Method func(context.Context, validator.Int64Request, *validator.Int64Response) +} + +// Description satisfies the validator.Int64 interface. +func (v Int64) Description(ctx context.Context) string { + if v.DescriptionMethod == nil { + return "" + } + + return v.DescriptionMethod(ctx) +} + +// MarkdownDescription satisfies the validator.Int64 interface. +func (v Int64) MarkdownDescription(ctx context.Context) string { + if v.MarkdownDescriptionMethod == nil { + return "" + } + + return v.MarkdownDescriptionMethod(ctx) +} + +// Validate satisfies the validator.Int64 interface. +func (v Int64) ValidateInt64(ctx context.Context, req validator.Int64Request, resp *validator.Int64Response) { + if v.ValidateInt64Method == nil { + return + } + + v.ValidateInt64Method(ctx, req, resp) +} diff --git a/internal/testing/testvalidator/list.go b/internal/testing/testvalidator/list.go new file mode 100644 index 000000000..0cf218f51 --- /dev/null +++ b/internal/testing/testvalidator/list.go @@ -0,0 +1,44 @@ +package testvalidator + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/schema/validator" +) + +var _ validator.List = &List{} + +// Declarative validator.List for unit testing. +type List struct { + // List interface methods + DescriptionMethod func(context.Context) string + MarkdownDescriptionMethod func(context.Context) string + ValidateListMethod func(context.Context, validator.ListRequest, *validator.ListResponse) +} + +// Description satisfies the validator.List interface. +func (v List) Description(ctx context.Context) string { + if v.DescriptionMethod == nil { + return "" + } + + return v.DescriptionMethod(ctx) +} + +// MarkdownDescription satisfies the validator.List interface. +func (v List) MarkdownDescription(ctx context.Context) string { + if v.MarkdownDescriptionMethod == nil { + return "" + } + + return v.MarkdownDescriptionMethod(ctx) +} + +// Validate satisfies the validator.List interface. +func (v List) ValidateList(ctx context.Context, req validator.ListRequest, resp *validator.ListResponse) { + if v.ValidateListMethod == nil { + return + } + + v.ValidateListMethod(ctx, req, resp) +} diff --git a/internal/testing/testvalidator/map.go b/internal/testing/testvalidator/map.go new file mode 100644 index 000000000..4866fcfda --- /dev/null +++ b/internal/testing/testvalidator/map.go @@ -0,0 +1,44 @@ +package testvalidator + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/schema/validator" +) + +var _ validator.Map = &Map{} + +// Declarative validator.Map for unit testing. +type Map struct { + // Map interface methods + DescriptionMethod func(context.Context) string + MarkdownDescriptionMethod func(context.Context) string + ValidateMapMethod func(context.Context, validator.MapRequest, *validator.MapResponse) +} + +// Description satisfies the validator.Map interface. +func (v Map) Description(ctx context.Context) string { + if v.DescriptionMethod == nil { + return "" + } + + return v.DescriptionMethod(ctx) +} + +// MarkdownDescription satisfies the validator.Map interface. +func (v Map) MarkdownDescription(ctx context.Context) string { + if v.MarkdownDescriptionMethod == nil { + return "" + } + + return v.MarkdownDescriptionMethod(ctx) +} + +// Validate satisfies the validator.Map interface. +func (v Map) ValidateMap(ctx context.Context, req validator.MapRequest, resp *validator.MapResponse) { + if v.ValidateMapMethod == nil { + return + } + + v.ValidateMapMethod(ctx, req, resp) +} diff --git a/internal/testing/testvalidator/number.go b/internal/testing/testvalidator/number.go new file mode 100644 index 000000000..bdcd523c0 --- /dev/null +++ b/internal/testing/testvalidator/number.go @@ -0,0 +1,44 @@ +package testvalidator + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/schema/validator" +) + +var _ validator.Number = &Number{} + +// Declarative validator.Number for unit testing. +type Number struct { + // Number interface methods + DescriptionMethod func(context.Context) string + MarkdownDescriptionMethod func(context.Context) string + ValidateNumberMethod func(context.Context, validator.NumberRequest, *validator.NumberResponse) +} + +// Description satisfies the validator.Number interface. +func (v Number) Description(ctx context.Context) string { + if v.DescriptionMethod == nil { + return "" + } + + return v.DescriptionMethod(ctx) +} + +// MarkdownDescription satisfies the validator.Number interface. +func (v Number) MarkdownDescription(ctx context.Context) string { + if v.MarkdownDescriptionMethod == nil { + return "" + } + + return v.MarkdownDescriptionMethod(ctx) +} + +// Validate satisfies the validator.Number interface. +func (v Number) ValidateNumber(ctx context.Context, req validator.NumberRequest, resp *validator.NumberResponse) { + if v.ValidateNumberMethod == nil { + return + } + + v.ValidateNumberMethod(ctx, req, resp) +} diff --git a/internal/testing/testvalidator/object.go b/internal/testing/testvalidator/object.go new file mode 100644 index 000000000..cd7357839 --- /dev/null +++ b/internal/testing/testvalidator/object.go @@ -0,0 +1,44 @@ +package testvalidator + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/schema/validator" +) + +var _ validator.Object = &Object{} + +// Declarative validator.Object for unit testing. +type Object struct { + // Object interface methods + DescriptionMethod func(context.Context) string + MarkdownDescriptionMethod func(context.Context) string + ValidateObjectMethod func(context.Context, validator.ObjectRequest, *validator.ObjectResponse) +} + +// Description satisfies the validator.Object interface. +func (v Object) Description(ctx context.Context) string { + if v.DescriptionMethod == nil { + return "" + } + + return v.DescriptionMethod(ctx) +} + +// MarkdownDescription satisfies the validator.Object interface. +func (v Object) MarkdownDescription(ctx context.Context) string { + if v.MarkdownDescriptionMethod == nil { + return "" + } + + return v.MarkdownDescriptionMethod(ctx) +} + +// Validate satisfies the validator.Object interface. +func (v Object) ValidateObject(ctx context.Context, req validator.ObjectRequest, resp *validator.ObjectResponse) { + if v.ValidateObjectMethod == nil { + return + } + + v.ValidateObjectMethod(ctx, req, resp) +} diff --git a/internal/testing/testvalidator/set.go b/internal/testing/testvalidator/set.go new file mode 100644 index 000000000..36daf676b --- /dev/null +++ b/internal/testing/testvalidator/set.go @@ -0,0 +1,44 @@ +package testvalidator + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/schema/validator" +) + +var _ validator.Set = &Set{} + +// Declarative validator.Set for unit testing. +type Set struct { + // Set interface methods + DescriptionMethod func(context.Context) string + MarkdownDescriptionMethod func(context.Context) string + ValidateSetMethod func(context.Context, validator.SetRequest, *validator.SetResponse) +} + +// Description satisfies the validator.Set interface. +func (v Set) Description(ctx context.Context) string { + if v.DescriptionMethod == nil { + return "" + } + + return v.DescriptionMethod(ctx) +} + +// MarkdownDescription satisfies the validator.Set interface. +func (v Set) MarkdownDescription(ctx context.Context) string { + if v.MarkdownDescriptionMethod == nil { + return "" + } + + return v.MarkdownDescriptionMethod(ctx) +} + +// Validate satisfies the validator.Set interface. +func (v Set) ValidateSet(ctx context.Context, req validator.SetRequest, resp *validator.SetResponse) { + if v.ValidateSetMethod == nil { + return + } + + v.ValidateSetMethod(ctx, req, resp) +} diff --git a/internal/testing/testvalidator/string.go b/internal/testing/testvalidator/string.go new file mode 100644 index 000000000..f46d11fe9 --- /dev/null +++ b/internal/testing/testvalidator/string.go @@ -0,0 +1,44 @@ +package testvalidator + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/schema/validator" +) + +var _ validator.String = &String{} + +// Declarative validator.String for unit testing. +type String struct { + // String interface methods + DescriptionMethod func(context.Context) string + MarkdownDescriptionMethod func(context.Context) string + ValidateStringMethod func(context.Context, validator.StringRequest, *validator.StringResponse) +} + +// Description satisfies the validator.String interface. +func (v String) Description(ctx context.Context) string { + if v.DescriptionMethod == nil { + return "" + } + + return v.DescriptionMethod(ctx) +} + +// MarkdownDescription satisfies the validator.String interface. +func (v String) MarkdownDescription(ctx context.Context) string { + if v.MarkdownDescriptionMethod == nil { + return "" + } + + return v.MarkdownDescriptionMethod(ctx) +} + +// Validate satisfies the validator.String interface. +func (v String) ValidateString(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) { + if v.ValidateStringMethod == nil { + return + } + + v.ValidateStringMethod(ctx, req, resp) +} diff --git a/schema/doc.go b/schema/doc.go new file mode 100644 index 000000000..3f5ef1ba1 --- /dev/null +++ b/schema/doc.go @@ -0,0 +1,4 @@ +// Package schema contains functionality common to all schemas. Refer to the +// datasource/schema, provider/schema, and resource/schema packages for concept +// specific implementations. +package schema diff --git a/schema/validator/bool.go b/schema/validator/bool.go new file mode 100644 index 000000000..0a0f0d6dd --- /dev/null +++ b/schema/validator/bool.go @@ -0,0 +1,43 @@ +package validator + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Bool is a schema validator for types.Bool attributes. +type Bool interface { + Describer + + // ValidateBool should perform the validation. + ValidateBool(context.Context, BoolRequest, *BoolResponse) +} + +// BoolRequest is a request for types.Bool schema validation. +type BoolRequest struct { + // Path contains the path of the attribute for validation. Use this path + // for any response diagnostics. + Path path.Path + + // PathExpression contains the expression matching the exact path + // of the attribute for validation. + PathExpression path.Expression + + // Config contains the entire configuration of the data source, provider, or resource. + Config tfsdk.Config + + // ConfigValue contains the value of the attribute for validation from the configuration. + ConfigValue types.Bool +} + +// BoolResponse is a response to a BoolRequest. +type BoolResponse struct { + // Diagnostics report errors or warnings related to validating the data + // source configuration. An empty slice indicates success, with no warnings + // or errors generated. + Diagnostics diag.Diagnostics +} diff --git a/schema/validator/describer.go b/schema/validator/describer.go new file mode 100644 index 000000000..2c1b1e99d --- /dev/null +++ b/schema/validator/describer.go @@ -0,0 +1,37 @@ +package validator + +import ( + "context" +) + +// Describer is the common documentation interface for extensible schema +// validation functionality. +type Describer interface { + // Description should describe the validation in plain text formatting. + // This information is used by provider logging and provider tooling such + // as documentation generation. + // + // The description should: + // - Begin with a lowercase or other character suitable for the middle of + // a sentence. + // - End without punctuation. + // - Use actionable language, such as "must" or "cannot". + // - Avoid newlines. Prefer separate validators instead. + // + // For example, "size must be less than 50 elements". + Description(context.Context) string + + // MarkdownDescription should describe the validation in Markdown + // formatting. This information is used by provider logging and provider + // tooling such as documentation generation. + // + // The description should: + // - Begin with a lowercase or other character suitable for the middle of + // a sentence. + // - End without punctuation. + // - Use actionable language, such as "must" or "cannot". + // - Avoid newlines. Prefer separate validators instead. + // + // For example, "value must be `one` or `two`". + MarkdownDescription(context.Context) string +} diff --git a/schema/validator/doc.go b/schema/validator/doc.go new file mode 100644 index 000000000..ad33ed716 --- /dev/null +++ b/schema/validator/doc.go @@ -0,0 +1,33 @@ +// Package validator contains common schema validator interfaces and +// implementations. These validators are used by concept specific packages +// such as datasource/schema, provider/schema, and resource/schema. +// +// Each attr.Type has a corresponding {TYPE}Validator interface which +// implements concretely typed Validate{TYPE} methods, such as +// StringValidator and ValidateString. Custom attr.Type can also consider +// implementing native type validation via the attr/xattr.TypeWithValidate +// interface instead of schema validators. +// +// The framework has to choose between validator developers handling a concrete +// framework value type, such as types.Bool, or the framework interface for +// custom value types, such as types.BoolValuable. +// +// In the framework type model, the developer can immediately use the value. +// If the value was associated with a custom type and using the custom value +// type is desired, the developer must use the type's ValueFrom{TYPE} method. +// +// In the custom type model, the developer must always convert to a concreate +// type before using the value unless checking for null or unknown. Since any +// custom type may be passed due to the schema, it is possible, if not likely, +// that unknown concrete types will be passed to the validator. +// +// The framework chooses to pass the framework value type. This prevents the +// potential for unexpected runtime panics and simplifies development for +// easier use cases where the framework type is sufficient. More advanced +// developers can choose to implement native type validation for custom +// types or call the type's ValueFrom{TYPE} method to get the desired +// desired custom type in a validator. +// +// Validators that are not type dependent need to implement all interfaces, +// but can use shared logic to reduce implementation code. +package validator diff --git a/schema/validator/float64.go b/schema/validator/float64.go new file mode 100644 index 000000000..7097509e8 --- /dev/null +++ b/schema/validator/float64.go @@ -0,0 +1,43 @@ +package validator + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Float64 is a schema validator for types.Float64 attributes. +type Float64 interface { + Describer + + // ValidateFloat64 should perform the validation. + ValidateFloat64(context.Context, Float64Request, *Float64Response) +} + +// Float64Request is a request for types.Float64 schema validation. +type Float64Request struct { + // Path contains the path of the attribute for validation. Use this path + // for any response diagnostics. + Path path.Path + + // PathExpression contains the expression matching the exact path + // of the attribute for validation. + PathExpression path.Expression + + // Config contains the entire configuration of the data source, provider, or resource. + Config tfsdk.Config + + // ConfigValue contains the value of the attribute for validation from the configuration. + ConfigValue types.Float64 +} + +// Float64Response is a response to a Float64Request. +type Float64Response struct { + // Diagnostics report errors or warnings related to validating the data + // source configuration. An empty slice indicates success, with no warnings + // or errors generated. + Diagnostics diag.Diagnostics +} diff --git a/schema/validator/int64.go b/schema/validator/int64.go new file mode 100644 index 000000000..85b44d768 --- /dev/null +++ b/schema/validator/int64.go @@ -0,0 +1,43 @@ +package validator + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Int64 is a schema validator for types.Int64 attributes. +type Int64 interface { + Describer + + // ValidateInt64 should perform the validation. + ValidateInt64(context.Context, Int64Request, *Int64Response) +} + +// Int64Request is a request for types.Int64 schema validation. +type Int64Request struct { + // Path contains the path of the attribute for validation. Use this path + // for any response diagnostics. + Path path.Path + + // PathExpression contains the expression matching the exact path + // of the attribute for validation. + PathExpression path.Expression + + // Config contains the entire configuration of the data source, provider, or resource. + Config tfsdk.Config + + // ConfigValue contains the value of the attribute for validation from the configuration. + ConfigValue types.Int64 +} + +// Int64Response is a response to a Int64Request. +type Int64Response struct { + // Diagnostics report errors or warnings related to validating the data + // source configuration. An empty slice indicates success, with no warnings + // or errors generated. + Diagnostics diag.Diagnostics +} diff --git a/schema/validator/list.go b/schema/validator/list.go new file mode 100644 index 000000000..cd417317b --- /dev/null +++ b/schema/validator/list.go @@ -0,0 +1,43 @@ +package validator + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// List is a schema validator for types.List attributes. +type List interface { + Describer + + // ValidateList should perform the validation. + ValidateList(context.Context, ListRequest, *ListResponse) +} + +// ListRequest is a request for types.List schema validation. +type ListRequest struct { + // Path contains the path of the attribute for validation. Use this path + // for any response diagnostics. + Path path.Path + + // PathExpression contains the expression matching the exact path + // of the attribute for validation. + PathExpression path.Expression + + // Config contains the entire configuration of the data source, provider, or resource. + Config tfsdk.Config + + // ConfigValue contains the value of the attribute for validation from the configuration. + ConfigValue types.List +} + +// ListResponse is a response to a ListRequest. +type ListResponse struct { + // Diagnostics report errors or warnings related to validating the data + // source configuration. An empty slice indicates success, with no warnings + // or errors generated. + Diagnostics diag.Diagnostics +} diff --git a/schema/validator/map.go b/schema/validator/map.go new file mode 100644 index 000000000..bee8f3a70 --- /dev/null +++ b/schema/validator/map.go @@ -0,0 +1,43 @@ +package validator + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Map is a schema validator for types.Map attributes. +type Map interface { + Describer + + // ValidateMap should perform the validation. + ValidateMap(context.Context, MapRequest, *MapResponse) +} + +// MapRequest is a request for types.Map schema validation. +type MapRequest struct { + // Path contains the path of the attribute for validation. Use this path + // for any response diagnostics. + Path path.Path + + // PathExpression contains the expression matching the exact path + // of the attribute for validation. + PathExpression path.Expression + + // Config contains the entire configuration of the data source, provider, or resource. + Config tfsdk.Config + + // ConfigValue contains the value of the attribute for validation from the configuration. + ConfigValue types.Map +} + +// MapResponse is a response to a MapRequest. +type MapResponse struct { + // Diagnostics report errors or warnings related to validating the data + // source configuration. An empty slice indicates success, with no warnings + // or errors generated. + Diagnostics diag.Diagnostics +} diff --git a/schema/validator/number.go b/schema/validator/number.go new file mode 100644 index 000000000..5630f5d4b --- /dev/null +++ b/schema/validator/number.go @@ -0,0 +1,43 @@ +package validator + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Number is a schema validator for types.Number attributes. +type Number interface { + Describer + + // ValidateNumber should perform the validation. + ValidateNumber(context.Context, NumberRequest, *NumberResponse) +} + +// NumberRequest is a request for types.Number schema validation. +type NumberRequest struct { + // Path contains the path of the attribute for validation. Use this path + // for any response diagnostics. + Path path.Path + + // PathExpression contains the expression matching the exact path + // of the attribute for validation. + PathExpression path.Expression + + // Config contains the entire configuration of the data source, provider, or resource. + Config tfsdk.Config + + // ConfigValue contains the value of the attribute for validation from the configuration. + ConfigValue types.Number +} + +// NumberResponse is a response to a NumberRequest. +type NumberResponse struct { + // Diagnostics report errors or warnings related to validating the data + // source configuration. An empty slice indicates success, with no warnings + // or errors generated. + Diagnostics diag.Diagnostics +} diff --git a/schema/validator/object.go b/schema/validator/object.go new file mode 100644 index 000000000..bab1d3a68 --- /dev/null +++ b/schema/validator/object.go @@ -0,0 +1,43 @@ +package validator + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Object is a schema validator for types.Object attributes. +type Object interface { + Describer + + // ValidateObject should perform the validation. + ValidateObject(context.Context, ObjectRequest, *ObjectResponse) +} + +// ObjectRequest is a request for types.Object schema validation. +type ObjectRequest struct { + // Path contains the path of the attribute for validation. Use this path + // for any response diagnostics. + Path path.Path + + // PathExpression contains the expression matching the exact path + // of the attribute for validation. + PathExpression path.Expression + + // Config contains the entire configuration of the data source, provider, or resource. + Config tfsdk.Config + + // ConfigValue contains the value of the attribute for validation from the configuration. + ConfigValue types.Object +} + +// ObjectResponse is a response to a ObjectRequest. +type ObjectResponse struct { + // Diagnostics report errors or warnings related to validating the data + // source configuration. An empty slice indicates success, with no warnings + // or errors generated. + Diagnostics diag.Diagnostics +} diff --git a/schema/validator/set.go b/schema/validator/set.go new file mode 100644 index 000000000..d064659d4 --- /dev/null +++ b/schema/validator/set.go @@ -0,0 +1,43 @@ +package validator + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Set is a schema validator for types.Set attributes. +type Set interface { + Describer + + // ValidateSet should perform the validation. + ValidateSet(context.Context, SetRequest, *SetResponse) +} + +// SetRequest is a request for types.Set schema validation. +type SetRequest struct { + // Path contains the path of the attribute for validation. Use this path + // for any response diagnostics. + Path path.Path + + // PathExpression contains the expression matching the exact path + // of the attribute for validation. + PathExpression path.Expression + + // Config contains the entire configuration of the data source, provider, or resource. + Config tfsdk.Config + + // ConfigValue contains the value of the attribute for validation from the configuration. + ConfigValue types.Set +} + +// SetResponse is a response to a SetRequest. +type SetResponse struct { + // Diagnostics report errors or warnings related to validating the data + // source configuration. An empty slice indicates success, with no warnings + // or errors generated. + Diagnostics diag.Diagnostics +} diff --git a/schema/validator/string.go b/schema/validator/string.go new file mode 100644 index 000000000..725e2c3c0 --- /dev/null +++ b/schema/validator/string.go @@ -0,0 +1,43 @@ +package validator + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// String is a schema validator for types.String attributes. +type String interface { + Describer + + // ValidateString should perform the validation. + ValidateString(context.Context, StringRequest, *StringResponse) +} + +// StringRequest is a request for types.String schema validation. +type StringRequest struct { + // Path contains the path of the attribute for validation. Use this path + // for any response diagnostics. + Path path.Path + + // PathExpression contains the expression matching the exact path + // of the attribute for validation. + PathExpression path.Expression + + // Config contains the entire configuration of the data source, provider, or resource. + Config tfsdk.Config + + // ConfigValue contains the value of the attribute for validation from the configuration. + ConfigValue types.String +} + +// StringResponse is a response to a StringRequest. +type StringResponse struct { + // Diagnostics report errors or warnings related to validating the data + // source configuration. An empty slice indicates success, with no warnings + // or errors generated. + Diagnostics diag.Diagnostics +}