From aa6a2de506f8f9bcc7ed44cdbdb32280039c8232 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Thu, 17 Oct 2024 15:04:05 -0400 Subject: [PATCH] all: Implement function parameter validation interfaces (#238) * stringvalidator: implement parameter interface * boolvalidator: implement parameter interface * float32validator: implement parameter interface * float64validator: implement parameter interface * numbervalidator: implement parameter interface * int32validator: implement parameter interface * int64validator: implement parameter interface * listvalidator: implement parameter interface validation * setvalidator: implement parameter interface * mapvalidator: implement parameter interface * add changelogs --- .../ENHANCEMENTS-20241014-121220.yaml | 6 ++ .../unreleased/NOTES-20241014-121711.yaml | 7 ++ boolvalidator/doc.go | 2 +- boolvalidator/equals.go | 23 ++++++- boolvalidator/equals_test.go | 60 ++++++++++------- float32validator/at_least.go | 27 ++++++-- float32validator/at_least_example_test.go | 15 +++++ float32validator/at_least_test.go | 22 +++++- float32validator/at_most.go | 27 ++++++-- float32validator/at_most_example_test.go | 15 +++++ float32validator/at_most_test.go | 22 +++++- float32validator/between.go | 62 ++++++++++++++--- float32validator/between_example_test.go | 15 +++++ float32validator/between_test.go | 29 +++++++- float32validator/doc.go | 2 +- float32validator/none_of.go | 30 ++++++++- float32validator/none_of_example_test.go | 15 +++++ float32validator/none_of_test.go | 59 +++++++++------- float32validator/one_of.go | 28 +++++++- float32validator/one_of_example_test.go | 15 +++++ float32validator/one_of_test.go | 59 +++++++++------- float64validator/at_least.go | 27 ++++++-- float64validator/at_least_example_test.go | 15 +++++ float64validator/at_least_test.go | 22 +++++- float64validator/at_most.go | 27 ++++++-- float64validator/at_most_example_test.go | 15 +++++ float64validator/at_most_test.go | 22 +++++- float64validator/between.go | 62 ++++++++++++++--- float64validator/between_example_test.go | 15 +++++ float64validator/between_test.go | 32 ++++++++- float64validator/doc.go | 2 +- float64validator/none_of.go | 30 ++++++++- float64validator/none_of_example_test.go | 15 +++++ float64validator/none_of_test.go | 59 +++++++++------- float64validator/one_of.go | 28 +++++++- float64validator/one_of_example_test.go | 15 +++++ float64validator/one_of_test.go | 59 +++++++++------- helpers/validatordiag/diag.go | 13 ++++ helpers/validatorfuncerr/doc.go | 5 ++ helpers/validatorfuncerr/funcerr.go | 45 +++++++++++++ int32validator/at_least.go | 25 +++++-- int32validator/at_least_example_test.go | 15 +++++ int32validator/at_least_test.go | 22 +++++- int32validator/at_most.go | 25 +++++-- int32validator/at_most_example_test.go | 15 +++++ int32validator/at_most_test.go | 22 +++++- int32validator/between.go | 60 ++++++++++++++--- int32validator/between_example_test.go | 15 +++++ int32validator/between_test.go | 29 +++++++- int32validator/doc.go | 2 +- int32validator/none_of.go | 30 ++++++++- int32validator/none_of_example_test.go | 15 +++++ int32validator/none_of_test.go | 59 +++++++++------- int32validator/one_of.go | 28 +++++++- int32validator/one_of_example_test.go | 15 +++++ int32validator/one_of_test.go | 62 ++++++++++------- int64validator/at_least.go | 25 +++++-- int64validator/at_least_example_test.go | 15 +++++ int64validator/at_least_test.go | 22 +++++- int64validator/at_most.go | 25 +++++-- int64validator/at_most_example_test.go | 15 +++++ int64validator/at_most_test.go | 22 +++++- int64validator/between.go | 60 ++++++++++++++--- int64validator/between_example_test.go | 15 +++++ int64validator/between_test.go | 29 +++++++- int64validator/doc.go | 2 +- int64validator/none_of.go | 30 ++++++++- int64validator/none_of_example_test.go | 15 +++++ int64validator/none_of_test.go | 59 +++++++++------- int64validator/one_of.go | 28 +++++++- int64validator/one_of_example_test.go | 15 +++++ int64validator/one_of_test.go | 59 +++++++++------- listvalidator/doc.go | 2 +- listvalidator/size_at_least.go | 27 ++++++-- listvalidator/size_at_least_example_test.go | 15 +++++ listvalidator/size_at_least_test.go | 23 ++++++- listvalidator/size_at_most.go | 27 ++++++-- listvalidator/size_at_most_example_test.go | 15 +++++ listvalidator/size_at_most_test.go | 23 ++++++- listvalidator/size_between.go | 28 ++++++-- listvalidator/size_between_example_test.go | 15 +++++ listvalidator/size_between_test.go | 23 ++++++- listvalidator/unique_values.go | 43 ++++++++++-- listvalidator/unique_values_example_test.go | 15 +++++ listvalidator/unique_values_test.go | 56 +++++++++++++++- mapvalidator/doc.go | 2 +- mapvalidator/size_at_least.go | 27 ++++++-- mapvalidator/size_at_least_example_test.go | 15 +++++ mapvalidator/size_at_least_test.go | 23 ++++++- mapvalidator/size_at_most.go | 27 ++++++-- mapvalidator/size_at_most_example_test.go | 15 +++++ mapvalidator/size_at_most_test.go | 23 ++++++- mapvalidator/size_between.go | 28 ++++++-- mapvalidator/size_between_example_test.go | 15 +++++ mapvalidator/size_between_test.go | 23 ++++++- numbervalidator/doc.go | 2 +- numbervalidator/none_of.go | 30 ++++++++- numbervalidator/none_of_example_test.go | 21 ++++++ numbervalidator/none_of_test.go | 59 +++++++++------- numbervalidator/one_of.go | 28 +++++++- numbervalidator/one_of_example_test.go | 21 ++++++ numbervalidator/one_of_test.go | 59 +++++++++------- setvalidator/doc.go | 2 +- setvalidator/size_at_least.go | 27 ++++++-- setvalidator/size_at_least_example_test.go | 15 +++++ setvalidator/size_at_least_test.go | 23 ++++++- setvalidator/size_at_most.go | 27 ++++++-- setvalidator/size_at_most_example_test.go | 15 +++++ setvalidator/size_at_most_test.go | 23 ++++++- setvalidator/size_between.go | 28 ++++++-- setvalidator/size_between_example_test.go | 15 +++++ setvalidator/size_between_test.go | 23 ++++++- stringvalidator/doc.go | 2 +- stringvalidator/length_at_least.go | 64 +++++++++++++++--- .../length_at_least_example_test.go | 15 +++++ stringvalidator/length_at_least_test.go | 28 +++++++- stringvalidator/length_at_most.go | 64 +++++++++++++++--- .../length_at_most_example_test.go | 15 +++++ stringvalidator/length_at_most_test.go | 28 +++++++- stringvalidator/length_between.go | 64 +++++++++++++++--- .../length_between_example_test.go | 15 +++++ stringvalidator/length_between_test.go | 35 +++++++++- stringvalidator/none_of.go | 30 ++++++++- stringvalidator/none_of_case_insensitive.go | 28 +++++++- .../none_of_case_insensitive_test.go | 65 +++++++++++------- stringvalidator/none_of_example_test.go | 15 +++++ stringvalidator/none_of_test.go | 64 +++++++++++------- stringvalidator/one_of.go | 28 +++++++- stringvalidator/one_of_case_insensitive.go | 28 +++++++- .../one_of_case_insensitive_test.go | 64 +++++++++++------- stringvalidator/one_of_example_test.go | 15 +++++ stringvalidator/one_of_test.go | 65 +++++++++++------- stringvalidator/regex_matches.go | 27 ++++++-- stringvalidator/regex_matches_example_test.go | 18 +++++ stringvalidator/regex_matches_test.go | 23 ++++++- stringvalidator/utf8_length_at_least.go | 66 +++++++++++++++--- .../utf8_length_at_least_example_test.go | 15 +++++ stringvalidator/utf8_length_at_least_test.go | 28 +++++++- stringvalidator/utf8_length_at_most.go | 66 +++++++++++++++--- .../utf8_length_at_most_example_test.go | 15 +++++ stringvalidator/utf8_length_at_most_test.go | 28 +++++++- stringvalidator/utf8_length_between.go | 67 ++++++++++++++++--- .../utf8_length_between_example_test.go | 16 +++++ stringvalidator/utf8_length_between_test.go | 41 +++++++++++- 144 files changed, 3333 insertions(+), 660 deletions(-) create mode 100644 .changes/unreleased/ENHANCEMENTS-20241014-121220.yaml create mode 100644 .changes/unreleased/NOTES-20241014-121711.yaml create mode 100644 helpers/validatorfuncerr/doc.go create mode 100644 helpers/validatorfuncerr/funcerr.go diff --git a/.changes/unreleased/ENHANCEMENTS-20241014-121220.yaml b/.changes/unreleased/ENHANCEMENTS-20241014-121220.yaml new file mode 100644 index 0000000..56a4511 --- /dev/null +++ b/.changes/unreleased/ENHANCEMENTS-20241014-121220.yaml @@ -0,0 +1,6 @@ +kind: ENHANCEMENTS +body: 'all: Implemented parameter interfaces for all value-based validators. This + allows these validators to be used with provider-defined functions.' +time: 2024-10-14T12:12:20.607373-04:00 +custom: + Issue: "235" diff --git a/.changes/unreleased/NOTES-20241014-121711.yaml b/.changes/unreleased/NOTES-20241014-121711.yaml new file mode 100644 index 0000000..392757d --- /dev/null +++ b/.changes/unreleased/NOTES-20241014-121711.yaml @@ -0,0 +1,7 @@ +kind: NOTES +body: 'all: Previously, creating validators with invalid data would result in a `nil` + value being returned and a panic from `terraform-plugin-framework`. This has been + updated to return an implementation diagnostic referencing the invalid data/validator during config validation.' +time: 2024-10-14T12:17:11.811926-04:00 +custom: + Issue: "235" diff --git a/boolvalidator/doc.go b/boolvalidator/doc.go index 9fd64e3..beae445 100644 --- a/boolvalidator/doc.go +++ b/boolvalidator/doc.go @@ -1,5 +1,5 @@ // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 -// Package boolvalidator provides validators for types.Bool attributes. +// Package boolvalidator provides validators for types.Bool attributes or function parameters. package boolvalidator diff --git a/boolvalidator/equals.go b/boolvalidator/equals.go index b63da2a..755e976 100644 --- a/boolvalidator/equals.go +++ b/boolvalidator/equals.go @@ -8,11 +8,14 @@ import ( "fmt" "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatorfuncerr" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" ) var _ validator.Bool = equalsValidator{} +var _ function.BoolParameterValidator = equalsValidator{} type equalsValidator struct { value types.Bool @@ -42,9 +45,25 @@ func (v equalsValidator) ValidateBool(ctx context.Context, req validator.BoolReq } } -// Equals returns an AttributeValidator which ensures that the configured boolean attribute +func (v equalsValidator) ValidateParameterBool(ctx context.Context, req function.BoolParameterValidatorRequest, resp *function.BoolParameterValidatorResponse) { + if req.Value.IsNull() || req.Value.IsUnknown() { + return + } + + value := req.Value + + if !value.Equal(v.value) { + resp.Error = validatorfuncerr.InvalidParameterValueMatchFuncError( + req.ArgumentPosition, + v.Description(ctx), + value.String(), + ) + } +} + +// Equals returns an AttributeValidator which ensures that the configured boolean attribute or function parameter // matches the given `value`. Null (unconfigured) and unknown (known after apply) values are skipped. -func Equals(value bool) validator.Bool { +func Equals(value bool) equalsValidator { return equalsValidator{ value: types.BoolValue(value), } diff --git a/boolvalidator/equals_test.go b/boolvalidator/equals_test.go index 8d87af9..ed2671e 100644 --- a/boolvalidator/equals_test.go +++ b/boolvalidator/equals_test.go @@ -5,9 +5,11 @@ package boolvalidator_test import ( "context" + "fmt" "testing" "github.com/hashicorp/terraform-plugin-framework-validators/boolvalidator" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" ) @@ -16,53 +18,65 @@ func TestEqualsValidator(t *testing.T) { t.Parallel() type testCase struct { - in types.Bool - validator validator.Bool - expErrors int + in types.Bool + equalsValue bool + expectError bool } testCases := map[string]testCase{ "simple-match": { - in: types.BoolValue(true), - validator: boolvalidator.Equals(true), - expErrors: 0, + in: types.BoolValue(true), + equalsValue: true, }, "simple-mismatch": { - in: types.BoolValue(false), - validator: boolvalidator.Equals(true), - expErrors: 1, + in: types.BoolValue(false), + equalsValue: true, + expectError: true, }, "skip-validation-on-null": { - in: types.BoolNull(), - validator: boolvalidator.Equals(true), - expErrors: 0, + in: types.BoolNull(), + equalsValue: true, }, "skip-validation-on-unknown": { - in: types.BoolUnknown(), - validator: boolvalidator.Equals(true), - expErrors: 0, + in: types.BoolUnknown(), + equalsValue: true, }, } for name, test := range testCases { - t.Run(name, func(t *testing.T) { + name, test := name, test + + t.Run(fmt.Sprintf("ValidateBool - %s", name), func(t *testing.T) { t.Parallel() req := validator.BoolRequest{ ConfigValue: test.in, } res := validator.BoolResponse{} - test.validator.ValidateBool(context.TODO(), req, &res) + boolvalidator.Equals(test.equalsValue).ValidateBool(context.TODO(), req, &res) + + if !res.Diagnostics.HasError() && test.expectError { + t.Fatal("expected error, got no error") + } - if test.expErrors > 0 && !res.Diagnostics.HasError() { - t.Fatalf("expected %d error(s), got none", test.expErrors) + if res.Diagnostics.HasError() && !test.expectError { + t.Fatalf("got unexpected error: %s", res.Diagnostics) + } + }) + + t.Run(fmt.Sprintf("ValidateParameterBool - %s", name), func(t *testing.T) { + t.Parallel() + req := function.BoolParameterValidatorRequest{ + Value: test.in, } + res := function.BoolParameterValidatorResponse{} + boolvalidator.Equals(test.equalsValue).ValidateParameterBool(context.TODO(), req, &res) - if test.expErrors > 0 && test.expErrors != res.Diagnostics.ErrorsCount() { - t.Fatalf("expected %d error(s), got %d: %v", test.expErrors, res.Diagnostics.ErrorsCount(), res.Diagnostics) + if res.Error == nil && test.expectError { + t.Fatal("expected error, got no error") } - if test.expErrors == 0 && res.Diagnostics.HasError() { - t.Fatalf("expected no error(s), got %d: %v", res.Diagnostics.ErrorsCount(), res.Diagnostics) + if res.Error != nil && !test.expectError { + t.Fatalf("got unexpected error: %s", res.Error) } }) } diff --git a/float32validator/at_least.go b/float32validator/at_least.go index 548d286..9a8eb82 100644 --- a/float32validator/at_least.go +++ b/float32validator/at_least.go @@ -7,29 +7,28 @@ import ( "context" "fmt" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatorfuncerr" ) var _ validator.Float32 = atLeastValidator{} +var _ function.Float32ParameterValidator = atLeastValidator{} -// atLeastValidator validates that an float Attribute's value is at least a certain value. type atLeastValidator struct { min float32 } -// Description describes the validation in plain text formatting. func (validator atLeastValidator) Description(_ context.Context) string { return fmt.Sprintf("value must be at least %f", validator.min) } -// MarkdownDescription describes the validation in Markdown formatting. func (validator atLeastValidator) MarkdownDescription(ctx context.Context) string { return validator.Description(ctx) } -// ValidateFloat32 performs the validation. func (validator atLeastValidator) ValidateFloat32(ctx context.Context, request validator.Float32Request, response *validator.Float32Response) { if request.ConfigValue.IsNull() || request.ConfigValue.IsUnknown() { return @@ -46,14 +45,30 @@ func (validator atLeastValidator) ValidateFloat32(ctx context.Context, request v } } +func (validator atLeastValidator) ValidateParameterFloat32(ctx context.Context, request function.Float32ParameterValidatorRequest, response *function.Float32ParameterValidatorResponse) { + if request.Value.IsNull() || request.Value.IsUnknown() { + return + } + + value := request.Value.ValueFloat32() + + if value < validator.min { + response.Error = validatorfuncerr.InvalidParameterValueFuncError( + request.ArgumentPosition, + validator.Description(ctx), + fmt.Sprintf("%f", value), + ) + } +} + // AtLeast returns an AttributeValidator which ensures that any configured -// attribute value: +// attribute or function parameter value: // // - Is a number, which can be represented by a 32-bit floating point. // - Is greater than or equal to the given minimum. // // Null (unconfigured) and unknown (known after apply) values are skipped. -func AtLeast(minVal float32) validator.Float32 { +func AtLeast(minVal float32) atLeastValidator { return atLeastValidator{ min: minVal, } diff --git a/float32validator/at_least_example_test.go b/float32validator/at_least_example_test.go index bae9da7..1729b3a 100644 --- a/float32validator/at_least_example_test.go +++ b/float32validator/at_least_example_test.go @@ -5,6 +5,7 @@ package float32validator_test import ( "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework-validators/float32validator" @@ -24,3 +25,17 @@ func ExampleAtLeast() { }, } } + +func ExampleAtLeast_function() { + _ = function.Definition{ + Parameters: []function.Parameter{ + function.Float32Parameter{ + Name: "example_param", + Validators: []function.Float32ParameterValidator{ + // Validate floating point value must be at least 42.42 + float32validator.AtLeast(42.42), + }, + }, + }, + } +} diff --git a/float32validator/at_least_test.go b/float32validator/at_least_test.go index 6669900..ea76838 100644 --- a/float32validator/at_least_test.go +++ b/float32validator/at_least_test.go @@ -5,8 +5,10 @@ package float32validator_test import ( "context" + "fmt" "testing" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" @@ -52,7 +54,8 @@ func TestAtLeastValidator(t *testing.T) { for name, test := range tests { name, test := name, test - t.Run(name, func(t *testing.T) { + + t.Run(fmt.Sprintf("ValidateFloat32 - %s", name), func(t *testing.T) { t.Parallel() request := validator.Float32Request{ Path: path.Root("test"), @@ -70,5 +73,22 @@ func TestAtLeastValidator(t *testing.T) { t.Fatalf("got unexpected error: %s", response.Diagnostics) } }) + + t.Run(fmt.Sprintf("ValidateParameterFloat32 - %s", name), func(t *testing.T) { + t.Parallel() + request := function.Float32ParameterValidatorRequest{ + Value: test.val, + } + response := function.Float32ParameterValidatorResponse{} + float32validator.AtLeast(test.min).ValidateParameterFloat32(context.TODO(), request, &response) + + if response.Error == nil && test.expectError { + t.Fatal("expected error, got no error") + } + + if response.Error != nil && !test.expectError { + t.Fatalf("got unexpected error: %s", response.Error) + } + }) } } diff --git a/float32validator/at_most.go b/float32validator/at_most.go index e34e55c..0fdf981 100644 --- a/float32validator/at_most.go +++ b/float32validator/at_most.go @@ -7,29 +7,28 @@ import ( "context" "fmt" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatorfuncerr" ) var _ validator.Float32 = atMostValidator{} +var _ function.Float32ParameterValidator = atMostValidator{} -// atMostValidator validates that an float Attribute's value is at most a certain value. type atMostValidator struct { max float32 } -// Description describes the validation in plain text formatting. func (validator atMostValidator) Description(_ context.Context) string { return fmt.Sprintf("value must be at most %f", validator.max) } -// MarkdownDescription describes the validation in Markdown formatting. func (validator atMostValidator) MarkdownDescription(ctx context.Context) string { return validator.Description(ctx) } -// ValidateFloat32 performs the validation. func (v atMostValidator) ValidateFloat32(ctx context.Context, request validator.Float32Request, response *validator.Float32Response) { if request.ConfigValue.IsNull() || request.ConfigValue.IsUnknown() { return @@ -46,14 +45,30 @@ func (v atMostValidator) ValidateFloat32(ctx context.Context, request validator. } } +func (v atMostValidator) ValidateParameterFloat32(ctx context.Context, request function.Float32ParameterValidatorRequest, response *function.Float32ParameterValidatorResponse) { + if request.Value.IsNull() || request.Value.IsUnknown() { + return + } + + value := request.Value.ValueFloat32() + + if value > v.max { + response.Error = validatorfuncerr.InvalidParameterValueFuncError( + request.ArgumentPosition, + v.Description(ctx), + fmt.Sprintf("%f", value), + ) + } +} + // AtMost returns an AttributeValidator which ensures that any configured -// attribute value: +// attribute or function parameter value: // // - Is a number, which can be represented by a 32-bit floating point. // - Is less than or equal to the given maximum. // // Null (unconfigured) and unknown (known after apply) values are skipped. -func AtMost(maxVal float32) validator.Float32 { +func AtMost(maxVal float32) atMostValidator { return atMostValidator{ max: maxVal, } diff --git a/float32validator/at_most_example_test.go b/float32validator/at_most_example_test.go index 6d3bef3..8b1ac46 100644 --- a/float32validator/at_most_example_test.go +++ b/float32validator/at_most_example_test.go @@ -5,6 +5,7 @@ package float32validator_test import ( "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework-validators/float32validator" @@ -24,3 +25,17 @@ func ExampleAtMost() { }, } } + +func ExampleAtMost_function() { + _ = function.Definition{ + Parameters: []function.Parameter{ + function.Float32Parameter{ + Name: "example_param", + Validators: []function.Float32ParameterValidator{ + // Validate floating point value must be at most 42.42 + float32validator.AtMost(42.42), + }, + }, + }, + } +} diff --git a/float32validator/at_most_test.go b/float32validator/at_most_test.go index cf5d1e7..b3915037 100644 --- a/float32validator/at_most_test.go +++ b/float32validator/at_most_test.go @@ -5,8 +5,10 @@ package float32validator_test import ( "context" + "fmt" "testing" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" @@ -52,7 +54,8 @@ func TestAtMostValidator(t *testing.T) { for name, test := range tests { name, test := name, test - t.Run(name, func(t *testing.T) { + + t.Run(fmt.Sprintf("ValidateFloat32 - %s", name), func(t *testing.T) { t.Parallel() request := validator.Float32Request{ Path: path.Root("test"), @@ -70,5 +73,22 @@ func TestAtMostValidator(t *testing.T) { t.Fatalf("got unexpected error: %s", response.Diagnostics) } }) + + t.Run(fmt.Sprintf("ValidateParameterFloat32 - %s", name), func(t *testing.T) { + t.Parallel() + request := function.Float32ParameterValidatorRequest{ + Value: test.val, + } + response := function.Float32ParameterValidatorResponse{} + float32validator.AtMost(test.max).ValidateParameterFloat32(context.TODO(), request, &response) + + if response.Error == nil && test.expectError { + t.Fatal("expected error, got no error") + } + + if response.Error != nil && !test.expectError { + t.Fatalf("got unexpected error: %s", response.Error) + } + }) } } diff --git a/float32validator/between.go b/float32validator/between.go index 306d1cd..a70cd8c 100644 --- a/float32validator/between.go +++ b/float32validator/between.go @@ -7,30 +7,46 @@ import ( "context" "fmt" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatorfuncerr" ) var _ validator.Float32 = betweenValidator{} +var _ function.Float32ParameterValidator = betweenValidator{} -// betweenValidator validates that an float Attribute's value is in a range. type betweenValidator struct { min, max float32 } -// Description describes the validation in plain text formatting. +func (validator betweenValidator) invalidUsageMessage() string { + return fmt.Sprintf("minVal cannot be greater than maxVal - minVal: %f, maxVal: %f", validator.min, validator.max) +} + func (validator betweenValidator) Description(_ context.Context) string { return fmt.Sprintf("value must be between %f and %f", validator.min, validator.max) } -// MarkdownDescription describes the validation in Markdown formatting. func (validator betweenValidator) MarkdownDescription(ctx context.Context) string { return validator.Description(ctx) } -// ValidateFloat32 performs the validation. func (v betweenValidator) ValidateFloat32(ctx context.Context, request validator.Float32Request, response *validator.Float32Response) { + // Return an error if the validator has been created in an invalid state + if v.min > v.max { + response.Diagnostics.Append( + validatordiag.InvalidValidatorUsageDiagnostic( + request.Path, + "Between", + v.invalidUsageMessage(), + ), + ) + + return + } + if request.ConfigValue.IsNull() || request.ConfigValue.IsUnknown() { return } @@ -46,18 +62,44 @@ func (v betweenValidator) ValidateFloat32(ctx context.Context, request validator } } +func (v betweenValidator) ValidateParameterFloat32(ctx context.Context, request function.Float32ParameterValidatorRequest, response *function.Float32ParameterValidatorResponse) { + // Return an error if the validator has been created in an invalid state + if v.min > v.max { + response.Error = validatorfuncerr.InvalidValidatorUsageFuncError( + request.ArgumentPosition, + "Between", + v.invalidUsageMessage(), + ) + + return + } + + if request.Value.IsNull() || request.Value.IsUnknown() { + return + } + + value := request.Value.ValueFloat32() + + if value < v.min || value > v.max { + response.Error = validatorfuncerr.InvalidParameterValueFuncError( + request.ArgumentPosition, + v.Description(ctx), + fmt.Sprintf("%f", value), + ) + } +} + // Between returns an AttributeValidator which ensures that any configured -// attribute value: +// attribute or function parameter value: // // - Is a number, which can be represented by a 32-bit floating point. // - Is greater than or equal to the given minimum and less than or equal to the given maximum. // // Null (unconfigured) and unknown (known after apply) values are skipped. -func Between(minVal, maxVal float32) validator.Float32 { - if minVal > maxVal { - return nil - } - +// +// minVal cannot be greater than maxVal. Invalid combinations of +// minVal and maxVal will result in an implementation error message during validation. +func Between(minVal, maxVal float32) betweenValidator { return betweenValidator{ min: minVal, max: maxVal, diff --git a/float32validator/between_example_test.go b/float32validator/between_example_test.go index 25a4927..c934613 100644 --- a/float32validator/between_example_test.go +++ b/float32validator/between_example_test.go @@ -5,6 +5,7 @@ package float32validator_test import ( "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework-validators/float32validator" @@ -24,3 +25,17 @@ func ExampleBetween() { }, } } + +func ExampleBetween_function() { + _ = function.Definition{ + Parameters: []function.Parameter{ + function.Float32Parameter{ + Name: "example_param", + Validators: []function.Float32ParameterValidator{ + // Validate floating point value must be at least 0.0 and at most 1.0 + float32validator.Between(0.0, 1.0), + }, + }, + }, + } +} diff --git a/float32validator/between_test.go b/float32validator/between_test.go index 1d3b813..be0c11d 100644 --- a/float32validator/between_test.go +++ b/float32validator/between_test.go @@ -5,8 +5,10 @@ package float32validator_test import ( "context" + "fmt" "testing" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" @@ -66,11 +68,18 @@ func TestBetweenValidator(t *testing.T) { max: 3.10, expectError: true, }, + "invalid validator usage - minVal > maxVal": { + val: types.Float32Value(2), + min: 3.20, + max: 3.10, + expectError: true, + }, } for name, test := range tests { name, test := name, test - t.Run(name, func(t *testing.T) { + + t.Run(fmt.Sprintf("ValidateFloat32 - %s", name), func(t *testing.T) { t.Parallel() request := validator.Float32Request{ Path: path.Root("test"), @@ -88,5 +97,23 @@ func TestBetweenValidator(t *testing.T) { t.Fatalf("got unexpected error: %s", response.Diagnostics) } }) + + t.Run(fmt.Sprintf("ValidateParameterFloat32 - %s", name), func(t *testing.T) { + t.Parallel() + request := function.Float32ParameterValidatorRequest{ + ArgumentPosition: 0, + Value: test.val, + } + response := function.Float32ParameterValidatorResponse{} + float32validator.Between(test.min, test.max).ValidateParameterFloat32(context.TODO(), request, &response) + + if response.Error == nil && test.expectError { + t.Fatal("expected error, got no error") + } + + if response.Error != nil && !test.expectError { + t.Fatalf("got unexpected error: %s", response.Error) + } + }) } } diff --git a/float32validator/doc.go b/float32validator/doc.go index 3b0a7c1..e57b117 100644 --- a/float32validator/doc.go +++ b/float32validator/doc.go @@ -1,5 +1,5 @@ // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 -// Package float32validator provides validators for types.Float32 attributes. +// Package float32validator provides validators for types.Float32 attributes or function parameters. package float32validator diff --git a/float32validator/none_of.go b/float32validator/none_of.go index fcaef90..0e6b275 100644 --- a/float32validator/none_of.go +++ b/float32validator/none_of.go @@ -7,15 +7,17 @@ import ( "context" "fmt" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatorfuncerr" ) var _ validator.Float32 = noneOfValidator{} +var _ function.Float32ParameterValidator = noneOfValidator{} -// noneOfValidator validates that the value does not match one of the values. type noneOfValidator struct { values []types.Float32 } @@ -50,9 +52,31 @@ func (v noneOfValidator) ValidateFloat32(ctx context.Context, request validator. } } -// NoneOf checks that the float32 held in the attribute +func (v noneOfValidator) ValidateParameterFloat32(ctx context.Context, request function.Float32ParameterValidatorRequest, response *function.Float32ParameterValidatorResponse) { + if request.Value.IsNull() || request.Value.IsUnknown() { + return + } + + value := request.Value + + for _, otherValue := range v.values { + if !value.Equal(otherValue) { + continue + } + + response.Error = validatorfuncerr.InvalidParameterValueMatchFuncError( + request.ArgumentPosition, + v.Description(ctx), + value.String(), + ) + + break + } +} + +// NoneOf checks that the float32 held in the attribute or function parameter // is none of the given `values`. -func NoneOf(values ...float32) validator.Float32 { +func NoneOf(values ...float32) noneOfValidator { frameworkValues := make([]types.Float32, 0, len(values)) for _, value := range values { diff --git a/float32validator/none_of_example_test.go b/float32validator/none_of_example_test.go index 7da6010..e004856 100644 --- a/float32validator/none_of_example_test.go +++ b/float32validator/none_of_example_test.go @@ -5,6 +5,7 @@ package float32validator_test import ( "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework-validators/float32validator" @@ -24,3 +25,17 @@ func ExampleNoneOf() { }, } } + +func ExampleNoneOf_function() { + _ = function.Definition{ + Parameters: []function.Parameter{ + function.Float32Parameter{ + Name: "example_param", + Validators: []function.Float32ParameterValidator{ + // Validate floating point value must not be 1.2, 2.4, or 4.8 + float32validator.NoneOf([]float32{1.2, 2.4, 4.8}...), + }, + }, + }, + } +} diff --git a/float32validator/none_of_test.go b/float32validator/none_of_test.go index c2a3d5f..8fb8a35 100644 --- a/float32validator/none_of_test.go +++ b/float32validator/none_of_test.go @@ -5,8 +5,10 @@ package float32validator_test import ( "context" + "fmt" "testing" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" @@ -17,71 +19,82 @@ func TestNoneOfValidator(t *testing.T) { t.Parallel() type testCase struct { - in types.Float32 - validator validator.Float32 - expErrors int + in types.Float32 + noneOfValues []float32 + expectError bool } testCases := map[string]testCase{ "simple-match": { in: types.Float32Value(123.456), - validator: float32validator.NoneOf( + noneOfValues: []float32{ 123.456, 234.567, 8910.11, 1213.1415, - ), - expErrors: 1, + }, + expectError: true, }, "simple-mismatch": { in: types.Float32Value(123.456), - validator: float32validator.NoneOf( + noneOfValues: []float32{ 234.567, 8910.11, 1213.1415, - ), - expErrors: 0, + }, }, "skip-validation-on-null": { in: types.Float32Null(), - validator: float32validator.NoneOf( + noneOfValues: []float32{ 234.567, 8910.11, 1213.1415, - ), - expErrors: 0, + }, }, "skip-validation-on-unknown": { in: types.Float32Unknown(), - validator: float32validator.NoneOf( + noneOfValues: []float32{ 234.567, 8910.11, 1213.1415, - ), - expErrors: 0, + }, }, } for name, test := range testCases { name, test := name, test - t.Run(name, func(t *testing.T) { + + t.Run(fmt.Sprintf("ValidateFloat32 - %s", name), func(t *testing.T) { t.Parallel() req := validator.Float32Request{ ConfigValue: test.in, } res := validator.Float32Response{} - test.validator.ValidateFloat32(context.TODO(), req, &res) + float32validator.NoneOf(test.noneOfValues...).ValidateFloat32(context.TODO(), req, &res) + + if !res.Diagnostics.HasError() && test.expectError { + t.Fatal("expected error, got no error") + } - if test.expErrors > 0 && !res.Diagnostics.HasError() { - t.Fatalf("expected %d error(s), got none", test.expErrors) + if res.Diagnostics.HasError() && !test.expectError { + t.Fatalf("got unexpected error: %s", res.Diagnostics) + } + }) + + t.Run(fmt.Sprintf("ValidateParameterFloat32 - %s", name), func(t *testing.T) { + t.Parallel() + req := function.Float32ParameterValidatorRequest{ + Value: test.in, } + res := function.Float32ParameterValidatorResponse{} + float32validator.NoneOf(test.noneOfValues...).ValidateParameterFloat32(context.TODO(), req, &res) - if test.expErrors > 0 && test.expErrors != res.Diagnostics.ErrorsCount() { - t.Fatalf("expected %d error(s), got %d: %v", test.expErrors, res.Diagnostics.ErrorsCount(), res.Diagnostics) + if res.Error == nil && test.expectError { + t.Fatal("expected error, got no error") } - if test.expErrors == 0 && res.Diagnostics.HasError() { - t.Fatalf("expected no error(s), got %d: %v", res.Diagnostics.ErrorsCount(), res.Diagnostics) + if res.Error != nil && !test.expectError { + t.Fatalf("got unexpected error: %s", res.Error) } }) } diff --git a/float32validator/one_of.go b/float32validator/one_of.go index c09543b..963f4b1 100644 --- a/float32validator/one_of.go +++ b/float32validator/one_of.go @@ -7,15 +7,17 @@ import ( "context" "fmt" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatorfuncerr" ) var _ validator.Float32 = oneOfValidator{} +var _ function.Float32ParameterValidator = oneOfValidator{} -// oneOfValidator validates that the value matches one of expected values. type oneOfValidator struct { values []types.Float32 } @@ -48,9 +50,29 @@ func (v oneOfValidator) ValidateFloat32(ctx context.Context, request validator.F )) } -// OneOf checks that the float32 held in the attribute +func (v oneOfValidator) ValidateParameterFloat32(ctx context.Context, request function.Float32ParameterValidatorRequest, response *function.Float32ParameterValidatorResponse) { + if request.Value.IsNull() || request.Value.IsUnknown() { + return + } + + value := request.Value + + for _, otherValue := range v.values { + if value.Equal(otherValue) { + return + } + } + + response.Error = validatorfuncerr.InvalidParameterValueMatchFuncError( + request.ArgumentPosition, + v.Description(ctx), + value.String(), + ) +} + +// OneOf checks that the float32 held in the attribute or function parameter // is one of the given `values`. -func OneOf(values ...float32) validator.Float32 { +func OneOf(values ...float32) oneOfValidator { frameworkValues := make([]types.Float32, 0, len(values)) for _, value := range values { diff --git a/float32validator/one_of_example_test.go b/float32validator/one_of_example_test.go index 7c9e942..f4bea24 100644 --- a/float32validator/one_of_example_test.go +++ b/float32validator/one_of_example_test.go @@ -5,6 +5,7 @@ package float32validator_test import ( "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework-validators/float32validator" @@ -24,3 +25,17 @@ func ExampleOneOf() { }, } } + +func ExampleOneOf_function() { + _ = function.Definition{ + Parameters: []function.Parameter{ + function.Float32Parameter{ + Name: "example_param", + Validators: []function.Float32ParameterValidator{ + // Validate floating point value must be 1.2, 2.4, or 4.8 + float32validator.OneOf([]float32{1.2, 2.4, 4.8}...), + }, + }, + }, + } +} diff --git a/float32validator/one_of_test.go b/float32validator/one_of_test.go index cfe5961..e960a04 100644 --- a/float32validator/one_of_test.go +++ b/float32validator/one_of_test.go @@ -5,8 +5,10 @@ package float32validator_test import ( "context" + "fmt" "testing" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" @@ -17,71 +19,82 @@ func TestOneOfValidator(t *testing.T) { t.Parallel() type testCase struct { - in types.Float32 - validator validator.Float32 - expErrors int + in types.Float32 + oneOfValues []float32 + expectError bool } testCases := map[string]testCase{ "simple-match": { in: types.Float32Value(123.456), - validator: float32validator.OneOf( + oneOfValues: []float32{ 123.456, 234.567, 8910.11, 1213.1415, - ), - expErrors: 0, + }, }, "simple-mismatch": { in: types.Float32Value(123.456), - validator: float32validator.OneOf( + oneOfValues: []float32{ 234.567, 8910.11, 1213.1415, - ), - expErrors: 1, + }, + expectError: true, }, "skip-validation-on-null": { in: types.Float32Null(), - validator: float32validator.OneOf( + oneOfValues: []float32{ 234.567, 8910.11, 1213.1415, - ), - expErrors: 0, + }, }, "skip-validation-on-unknown": { in: types.Float32Unknown(), - validator: float32validator.OneOf( + oneOfValues: []float32{ 234.567, 8910.11, 1213.1415, - ), - expErrors: 0, + }, }, } for name, test := range testCases { name, test := name, test - t.Run(name, func(t *testing.T) { + + t.Run(fmt.Sprintf("ValidateFloat32 - %s", name), func(t *testing.T) { t.Parallel() req := validator.Float32Request{ ConfigValue: test.in, } res := validator.Float32Response{} - test.validator.ValidateFloat32(context.TODO(), req, &res) + float32validator.OneOf(test.oneOfValues...).ValidateFloat32(context.TODO(), req, &res) + + if !res.Diagnostics.HasError() && test.expectError { + t.Fatal("expected error, got no error") + } - if test.expErrors > 0 && !res.Diagnostics.HasError() { - t.Fatalf("expected %d error(s), got none", test.expErrors) + if res.Diagnostics.HasError() && !test.expectError { + t.Fatalf("got unexpected error: %s", res.Diagnostics) + } + }) + + t.Run(fmt.Sprintf("ValidateParameterFloat32 - %s", name), func(t *testing.T) { + t.Parallel() + req := function.Float32ParameterValidatorRequest{ + Value: test.in, } + res := function.Float32ParameterValidatorResponse{} + float32validator.OneOf(test.oneOfValues...).ValidateParameterFloat32(context.TODO(), req, &res) - if test.expErrors > 0 && test.expErrors != res.Diagnostics.ErrorsCount() { - t.Fatalf("expected %d error(s), got %d: %v", test.expErrors, res.Diagnostics.ErrorsCount(), res.Diagnostics) + if res.Error == nil && test.expectError { + t.Fatal("expected error, got no error") } - if test.expErrors == 0 && res.Diagnostics.HasError() { - t.Fatalf("expected no error(s), got %d: %v", res.Diagnostics.ErrorsCount(), res.Diagnostics) + if res.Error != nil && !test.expectError { + t.Fatalf("got unexpected error: %s", res.Error) } }) } diff --git a/float64validator/at_least.go b/float64validator/at_least.go index a3d78c4..7d334b5 100644 --- a/float64validator/at_least.go +++ b/float64validator/at_least.go @@ -7,29 +7,28 @@ import ( "context" "fmt" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatorfuncerr" ) var _ validator.Float64 = atLeastValidator{} +var _ function.Float64ParameterValidator = atLeastValidator{} -// atLeastValidator validates that an float Attribute's value is at least a certain value. type atLeastValidator struct { min float64 } -// Description describes the validation in plain text formatting. func (validator atLeastValidator) Description(_ context.Context) string { return fmt.Sprintf("value must be at least %f", validator.min) } -// MarkdownDescription describes the validation in Markdown formatting. func (validator atLeastValidator) MarkdownDescription(ctx context.Context) string { return validator.Description(ctx) } -// ValidateFloat64 performs the validation. func (validator atLeastValidator) ValidateFloat64(ctx context.Context, request validator.Float64Request, response *validator.Float64Response) { if request.ConfigValue.IsNull() || request.ConfigValue.IsUnknown() { return @@ -46,14 +45,30 @@ func (validator atLeastValidator) ValidateFloat64(ctx context.Context, request v } } +func (validator atLeastValidator) ValidateParameterFloat64(ctx context.Context, request function.Float64ParameterValidatorRequest, response *function.Float64ParameterValidatorResponse) { + if request.Value.IsNull() || request.Value.IsUnknown() { + return + } + + value := request.Value.ValueFloat64() + + if value < validator.min { + response.Error = validatorfuncerr.InvalidParameterValueFuncError( + request.ArgumentPosition, + validator.Description(ctx), + fmt.Sprintf("%f", value), + ) + } +} + // AtLeast returns an AttributeValidator which ensures that any configured -// attribute value: +// attribute or function parameter value: // // - Is a number, which can be represented by a 64-bit floating point. // - Is greater than or equal to the given minimum. // // Null (unconfigured) and unknown (known after apply) values are skipped. -func AtLeast(minVal float64) validator.Float64 { +func AtLeast(minVal float64) atLeastValidator { return atLeastValidator{ min: minVal, } diff --git a/float64validator/at_least_example_test.go b/float64validator/at_least_example_test.go index 39e667b..a5d1df2 100644 --- a/float64validator/at_least_example_test.go +++ b/float64validator/at_least_example_test.go @@ -6,6 +6,7 @@ package float64validator_test import ( "github.com/hashicorp/terraform-plugin-framework-validators/float64validator" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/schema/validator" ) @@ -23,3 +24,17 @@ func ExampleAtLeast() { }, } } + +func ExampleAtLeast_function() { + _ = function.Definition{ + Parameters: []function.Parameter{ + function.Float64Parameter{ + Name: "example_param", + Validators: []function.Float64ParameterValidator{ + // Validate floating point value must be at least 42.42 + float64validator.AtLeast(42.42), + }, + }, + }, + } +} diff --git a/float64validator/at_least_test.go b/float64validator/at_least_test.go index 20146ba..d24df49 100644 --- a/float64validator/at_least_test.go +++ b/float64validator/at_least_test.go @@ -5,8 +5,10 @@ package float64validator_test import ( "context" + "fmt" "testing" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" @@ -52,7 +54,8 @@ func TestAtLeastValidator(t *testing.T) { for name, test := range tests { name, test := name, test - t.Run(name, func(t *testing.T) { + + t.Run(fmt.Sprintf("ValidateFloat64 - %s", name), func(t *testing.T) { t.Parallel() request := validator.Float64Request{ Path: path.Root("test"), @@ -70,5 +73,22 @@ func TestAtLeastValidator(t *testing.T) { t.Fatalf("got unexpected error: %s", response.Diagnostics) } }) + + t.Run(fmt.Sprintf("ValidateParameterFloat64 - %s", name), func(t *testing.T) { + t.Parallel() + request := function.Float64ParameterValidatorRequest{ + Value: test.val, + } + response := function.Float64ParameterValidatorResponse{} + float64validator.AtLeast(test.min).ValidateParameterFloat64(context.TODO(), request, &response) + + if response.Error == nil && test.expectError { + t.Fatal("expected error, got no error") + } + + if response.Error != nil && !test.expectError { + t.Fatalf("got unexpected error: %s", response.Error) + } + }) } } diff --git a/float64validator/at_most.go b/float64validator/at_most.go index fe1d63d..786859b 100644 --- a/float64validator/at_most.go +++ b/float64validator/at_most.go @@ -7,29 +7,28 @@ import ( "context" "fmt" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatorfuncerr" ) var _ validator.Float64 = atMostValidator{} +var _ function.Float64ParameterValidator = atMostValidator{} -// atMostValidator validates that an float Attribute's value is at most a certain value. type atMostValidator struct { max float64 } -// Description describes the validation in plain text formatting. func (validator atMostValidator) Description(_ context.Context) string { return fmt.Sprintf("value must be at most %f", validator.max) } -// MarkdownDescription describes the validation in Markdown formatting. func (validator atMostValidator) MarkdownDescription(ctx context.Context) string { return validator.Description(ctx) } -// ValidateFloat64 performs the validation. func (v atMostValidator) ValidateFloat64(ctx context.Context, request validator.Float64Request, response *validator.Float64Response) { if request.ConfigValue.IsNull() || request.ConfigValue.IsUnknown() { return @@ -46,14 +45,30 @@ func (v atMostValidator) ValidateFloat64(ctx context.Context, request validator. } } +func (v atMostValidator) ValidateParameterFloat64(ctx context.Context, request function.Float64ParameterValidatorRequest, response *function.Float64ParameterValidatorResponse) { + if request.Value.IsNull() || request.Value.IsUnknown() { + return + } + + value := request.Value.ValueFloat64() + + if value > v.max { + response.Error = validatorfuncerr.InvalidParameterValueFuncError( + request.ArgumentPosition, + v.Description(ctx), + fmt.Sprintf("%f", value), + ) + } +} + // AtMost returns an AttributeValidator which ensures that any configured -// attribute value: +// attribute or function parameter value: // // - Is a number, which can be represented by a 64-bit floating point. // - Is less than or equal to the given maximum. // // Null (unconfigured) and unknown (known after apply) values are skipped. -func AtMost(maxVal float64) validator.Float64 { +func AtMost(maxVal float64) atMostValidator { return atMostValidator{ max: maxVal, } diff --git a/float64validator/at_most_example_test.go b/float64validator/at_most_example_test.go index f1c4eb5..106b4b3 100644 --- a/float64validator/at_most_example_test.go +++ b/float64validator/at_most_example_test.go @@ -6,6 +6,7 @@ package float64validator_test import ( "github.com/hashicorp/terraform-plugin-framework-validators/float64validator" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/schema/validator" ) @@ -23,3 +24,17 @@ func ExampleAtMost() { }, } } + +func ExampleAtMost_function() { + _ = function.Definition{ + Parameters: []function.Parameter{ + function.Float64Parameter{ + Name: "example_param", + Validators: []function.Float64ParameterValidator{ + // Validate floating point value must be at most 42.42 + float64validator.AtMost(42.42), + }, + }, + }, + } +} diff --git a/float64validator/at_most_test.go b/float64validator/at_most_test.go index 4b1f216..464ef94 100644 --- a/float64validator/at_most_test.go +++ b/float64validator/at_most_test.go @@ -5,8 +5,10 @@ package float64validator_test import ( "context" + "fmt" "testing" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" @@ -52,7 +54,8 @@ func TestAtMostValidator(t *testing.T) { for name, test := range tests { name, test := name, test - t.Run(name, func(t *testing.T) { + + t.Run(fmt.Sprintf("ValidateFloat64 - %s", name), func(t *testing.T) { t.Parallel() request := validator.Float64Request{ Path: path.Root("test"), @@ -70,5 +73,22 @@ func TestAtMostValidator(t *testing.T) { t.Fatalf("got unexpected error: %s", response.Diagnostics) } }) + + t.Run(fmt.Sprintf("ValidateParameterFloat64 - %s", name), func(t *testing.T) { + t.Parallel() + request := function.Float64ParameterValidatorRequest{ + Value: test.val, + } + response := function.Float64ParameterValidatorResponse{} + float64validator.AtMost(test.max).ValidateParameterFloat64(context.TODO(), request, &response) + + if response.Error == nil && test.expectError { + t.Fatal("expected error, got no error") + } + + if response.Error != nil && !test.expectError { + t.Fatalf("got unexpected error: %s", response.Error) + } + }) } } diff --git a/float64validator/between.go b/float64validator/between.go index f08f18c..5afdab3 100644 --- a/float64validator/between.go +++ b/float64validator/between.go @@ -7,30 +7,46 @@ import ( "context" "fmt" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatorfuncerr" ) var _ validator.Float64 = betweenValidator{} +var _ function.Float64ParameterValidator = betweenValidator{} -// betweenValidator validates that an float Attribute's value is in a range. type betweenValidator struct { min, max float64 } -// Description describes the validation in plain text formatting. +func (validator betweenValidator) invalidUsageMessage() string { + return fmt.Sprintf("minVal cannot be greater than maxVal - minVal: %f, maxVal: %f", validator.min, validator.max) +} + func (validator betweenValidator) Description(_ context.Context) string { return fmt.Sprintf("value must be between %f and %f", validator.min, validator.max) } -// MarkdownDescription describes the validation in Markdown formatting. func (validator betweenValidator) MarkdownDescription(ctx context.Context) string { return validator.Description(ctx) } -// ValidateFloat64 performs the validation. func (v betweenValidator) ValidateFloat64(ctx context.Context, request validator.Float64Request, response *validator.Float64Response) { + // Return an error if the validator has been created in an invalid state + if v.min > v.max { + response.Diagnostics.Append( + validatordiag.InvalidValidatorUsageDiagnostic( + request.Path, + "Between", + v.invalidUsageMessage(), + ), + ) + + return + } + if request.ConfigValue.IsNull() || request.ConfigValue.IsUnknown() { return } @@ -46,18 +62,44 @@ func (v betweenValidator) ValidateFloat64(ctx context.Context, request validator } } +func (v betweenValidator) ValidateParameterFloat64(ctx context.Context, request function.Float64ParameterValidatorRequest, response *function.Float64ParameterValidatorResponse) { + // Return an error if the validator has been created in an invalid state + if v.min > v.max { + response.Error = validatorfuncerr.InvalidValidatorUsageFuncError( + request.ArgumentPosition, + "Between", + v.invalidUsageMessage(), + ) + + return + } + + if request.Value.IsNull() || request.Value.IsUnknown() { + return + } + + value := request.Value.ValueFloat64() + + if value < v.min || value > v.max { + response.Error = validatorfuncerr.InvalidParameterValueFuncError( + request.ArgumentPosition, + v.Description(ctx), + fmt.Sprintf("%f", value), + ) + } +} + // Between returns an AttributeValidator which ensures that any configured -// attribute value: +// attribute or function parameter value: // // - Is a number, which can be represented by a 64-bit floating point. // - Is greater than or equal to the given minimum and less than or equal to the given maximum. // // Null (unconfigured) and unknown (known after apply) values are skipped. -func Between(minVal, maxVal float64) validator.Float64 { - if minVal > maxVal { - return nil - } - +// +// minVal cannot be greater than maxVal. Invalid combinations of +// minVal and maxVal will result in an implementation error message during validation. +func Between(minVal, maxVal float64) betweenValidator { return betweenValidator{ min: minVal, max: maxVal, diff --git a/float64validator/between_example_test.go b/float64validator/between_example_test.go index a8e45c6..9ee79fa 100644 --- a/float64validator/between_example_test.go +++ b/float64validator/between_example_test.go @@ -6,6 +6,7 @@ package float64validator_test import ( "github.com/hashicorp/terraform-plugin-framework-validators/float64validator" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/schema/validator" ) @@ -23,3 +24,17 @@ func ExampleBetween() { }, } } + +func ExampleBetween_function() { + _ = function.Definition{ + Parameters: []function.Parameter{ + function.Float64Parameter{ + Name: "example_param", + Validators: []function.Float64ParameterValidator{ + // Validate floating point value must be at least 0.0 and at most 1.0 + float64validator.Between(0.0, 1.0), + }, + }, + }, + } +} diff --git a/float64validator/between_test.go b/float64validator/between_test.go index b6f644c..d9dbd4b 100644 --- a/float64validator/between_test.go +++ b/float64validator/between_test.go @@ -5,13 +5,14 @@ package float64validator_test import ( "context" + "fmt" "testing" + "github.com/hashicorp/terraform-plugin-framework-validators/float64validator" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - - "github.com/hashicorp/terraform-plugin-framework-validators/float64validator" ) func TestBetweenValidator(t *testing.T) { @@ -66,11 +67,18 @@ func TestBetweenValidator(t *testing.T) { max: 3.10, expectError: true, }, + "invalid validator usage - minVal > maxVal": { + val: types.Float64Value(2), + min: 3.20, + max: 3.10, + expectError: true, + }, } for name, test := range tests { name, test := name, test - t.Run(name, func(t *testing.T) { + + t.Run(fmt.Sprintf("ValidateFloat64 - %s", name), func(t *testing.T) { t.Parallel() request := validator.Float64Request{ Path: path.Root("test"), @@ -88,5 +96,23 @@ func TestBetweenValidator(t *testing.T) { t.Fatalf("got unexpected error: %s", response.Diagnostics) } }) + + t.Run(fmt.Sprintf("ValidateParameterFloat64 - %s", name), func(t *testing.T) { + t.Parallel() + request := function.Float64ParameterValidatorRequest{ + ArgumentPosition: 0, + Value: test.val, + } + response := function.Float64ParameterValidatorResponse{} + float64validator.Between(test.min, test.max).ValidateParameterFloat64(context.TODO(), request, &response) + + if response.Error == nil && test.expectError { + t.Fatal("expected error, got no error") + } + + if response.Error != nil && !test.expectError { + t.Fatalf("got unexpected error: %s", response.Error) + } + }) } } diff --git a/float64validator/doc.go b/float64validator/doc.go index 0ceb43b..aa1967e 100644 --- a/float64validator/doc.go +++ b/float64validator/doc.go @@ -1,5 +1,5 @@ // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 -// Package float64validator provides validators for types.Float64 attributes. +// Package float64validator provides validators for types.Float64 attributes or function parameters. package float64validator diff --git a/float64validator/none_of.go b/float64validator/none_of.go index c356803..0dcc0db 100644 --- a/float64validator/none_of.go +++ b/float64validator/none_of.go @@ -7,15 +7,17 @@ import ( "context" "fmt" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatorfuncerr" ) var _ validator.Float64 = noneOfValidator{} +var _ function.Float64ParameterValidator = noneOfValidator{} -// noneOfValidator validates that the value does not match one of the values. type noneOfValidator struct { values []types.Float64 } @@ -50,9 +52,31 @@ func (v noneOfValidator) ValidateFloat64(ctx context.Context, request validator. } } -// NoneOf checks that the float64 held in the attribute +func (v noneOfValidator) ValidateParameterFloat64(ctx context.Context, request function.Float64ParameterValidatorRequest, response *function.Float64ParameterValidatorResponse) { + if request.Value.IsNull() || request.Value.IsUnknown() { + return + } + + value := request.Value + + for _, otherValue := range v.values { + if !value.Equal(otherValue) { + continue + } + + response.Error = validatorfuncerr.InvalidParameterValueMatchFuncError( + request.ArgumentPosition, + v.Description(ctx), + value.String(), + ) + + break + } +} + +// NoneOf checks that the float64 held in the attribute or function parameter // is none of the given `values`. -func NoneOf(values ...float64) validator.Float64 { +func NoneOf(values ...float64) noneOfValidator { frameworkValues := make([]types.Float64, 0, len(values)) for _, value := range values { diff --git a/float64validator/none_of_example_test.go b/float64validator/none_of_example_test.go index 075a494..2205ee4 100644 --- a/float64validator/none_of_example_test.go +++ b/float64validator/none_of_example_test.go @@ -6,6 +6,7 @@ package float64validator_test import ( "github.com/hashicorp/terraform-plugin-framework-validators/float64validator" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/schema/validator" ) @@ -23,3 +24,17 @@ func ExampleNoneOf() { }, } } + +func ExampleNoneOf_function() { + _ = function.Definition{ + Parameters: []function.Parameter{ + function.Float64Parameter{ + Name: "example_param", + Validators: []function.Float64ParameterValidator{ + // Validate floating point value must not be 1.2, 2.4, or 4.8 + float64validator.NoneOf([]float64{1.2, 2.4, 4.8}...), + }, + }, + }, + } +} diff --git a/float64validator/none_of_test.go b/float64validator/none_of_test.go index ce17daf..01ba012 100644 --- a/float64validator/none_of_test.go +++ b/float64validator/none_of_test.go @@ -5,8 +5,10 @@ package float64validator_test import ( "context" + "fmt" "testing" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" @@ -17,71 +19,82 @@ func TestNoneOfValidator(t *testing.T) { t.Parallel() type testCase struct { - in types.Float64 - validator validator.Float64 - expErrors int + in types.Float64 + noneOfValues []float64 + expectError bool } testCases := map[string]testCase{ "simple-match": { in: types.Float64Value(123.456), - validator: float64validator.NoneOf( + noneOfValues: []float64{ 123.456, 234.567, 8910.11, 1213.1415, - ), - expErrors: 1, + }, + expectError: true, }, "simple-mismatch": { in: types.Float64Value(123.456), - validator: float64validator.NoneOf( + noneOfValues: []float64{ 234.567, 8910.11, 1213.1415, - ), - expErrors: 0, + }, }, "skip-validation-on-null": { in: types.Float64Null(), - validator: float64validator.NoneOf( + noneOfValues: []float64{ 234.567, 8910.11, 1213.1415, - ), - expErrors: 0, + }, }, "skip-validation-on-unknown": { in: types.Float64Unknown(), - validator: float64validator.NoneOf( + noneOfValues: []float64{ 234.567, 8910.11, 1213.1415, - ), - expErrors: 0, + }, }, } for name, test := range testCases { name, test := name, test - t.Run(name, func(t *testing.T) { + + t.Run(fmt.Sprintf("ValidateFloat64 - %s", name), func(t *testing.T) { t.Parallel() req := validator.Float64Request{ ConfigValue: test.in, } res := validator.Float64Response{} - test.validator.ValidateFloat64(context.TODO(), req, &res) + float64validator.NoneOf(test.noneOfValues...).ValidateFloat64(context.TODO(), req, &res) + + if !res.Diagnostics.HasError() && test.expectError { + t.Fatal("expected error, got no error") + } - if test.expErrors > 0 && !res.Diagnostics.HasError() { - t.Fatalf("expected %d error(s), got none", test.expErrors) + if res.Diagnostics.HasError() && !test.expectError { + t.Fatalf("got unexpected error: %s", res.Diagnostics) + } + }) + + t.Run(fmt.Sprintf("ValidateParameterFloat64 - %s", name), func(t *testing.T) { + t.Parallel() + req := function.Float64ParameterValidatorRequest{ + Value: test.in, } + res := function.Float64ParameterValidatorResponse{} + float64validator.NoneOf(test.noneOfValues...).ValidateParameterFloat64(context.TODO(), req, &res) - if test.expErrors > 0 && test.expErrors != res.Diagnostics.ErrorsCount() { - t.Fatalf("expected %d error(s), got %d: %v", test.expErrors, res.Diagnostics.ErrorsCount(), res.Diagnostics) + if res.Error == nil && test.expectError { + t.Fatal("expected error, got no error") } - if test.expErrors == 0 && res.Diagnostics.HasError() { - t.Fatalf("expected no error(s), got %d: %v", res.Diagnostics.ErrorsCount(), res.Diagnostics) + if res.Error != nil && !test.expectError { + t.Fatalf("got unexpected error: %s", res.Error) } }) } diff --git a/float64validator/one_of.go b/float64validator/one_of.go index 7a4702a..59c2f79 100644 --- a/float64validator/one_of.go +++ b/float64validator/one_of.go @@ -7,15 +7,17 @@ import ( "context" "fmt" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatorfuncerr" ) var _ validator.Float64 = oneOfValidator{} +var _ function.Float64ParameterValidator = oneOfValidator{} -// oneOfValidator validates that the value matches one of expected values. type oneOfValidator struct { values []types.Float64 } @@ -48,9 +50,29 @@ func (v oneOfValidator) ValidateFloat64(ctx context.Context, request validator.F )) } -// OneOf checks that the float64 held in the attribute +func (v oneOfValidator) ValidateParameterFloat64(ctx context.Context, request function.Float64ParameterValidatorRequest, response *function.Float64ParameterValidatorResponse) { + if request.Value.IsNull() || request.Value.IsUnknown() { + return + } + + value := request.Value + + for _, otherValue := range v.values { + if value.Equal(otherValue) { + return + } + } + + response.Error = validatorfuncerr.InvalidParameterValueMatchFuncError( + request.ArgumentPosition, + v.Description(ctx), + value.String(), + ) +} + +// OneOf checks that the float64 held in the attribute or function parameter // is one of the given `values`. -func OneOf(values ...float64) validator.Float64 { +func OneOf(values ...float64) oneOfValidator { frameworkValues := make([]types.Float64, 0, len(values)) for _, value := range values { diff --git a/float64validator/one_of_example_test.go b/float64validator/one_of_example_test.go index c90a751..ae80053 100644 --- a/float64validator/one_of_example_test.go +++ b/float64validator/one_of_example_test.go @@ -6,6 +6,7 @@ package float64validator_test import ( "github.com/hashicorp/terraform-plugin-framework-validators/float64validator" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/schema/validator" ) @@ -23,3 +24,17 @@ func ExampleOneOf() { }, } } + +func ExampleOneOf_function() { + _ = function.Definition{ + Parameters: []function.Parameter{ + function.Float64Parameter{ + Name: "example_param", + Validators: []function.Float64ParameterValidator{ + // Validate floating point value must be 1.2, 2.4, or 4.8 + float64validator.OneOf([]float64{1.2, 2.4, 4.8}...), + }, + }, + }, + } +} diff --git a/float64validator/one_of_test.go b/float64validator/one_of_test.go index 045b558..3fb6d39 100644 --- a/float64validator/one_of_test.go +++ b/float64validator/one_of_test.go @@ -5,8 +5,10 @@ package float64validator_test import ( "context" + "fmt" "testing" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" @@ -17,71 +19,82 @@ func TestOneOfValidator(t *testing.T) { t.Parallel() type testCase struct { - in types.Float64 - validator validator.Float64 - expErrors int + in types.Float64 + oneOfValues []float64 + expectError bool } testCases := map[string]testCase{ "simple-match": { in: types.Float64Value(123.456), - validator: float64validator.OneOf( + oneOfValues: []float64{ 123.456, 234.567, 8910.11, 1213.1415, - ), - expErrors: 0, + }, }, "simple-mismatch": { in: types.Float64Value(123.456), - validator: float64validator.OneOf( + oneOfValues: []float64{ 234.567, 8910.11, 1213.1415, - ), - expErrors: 1, + }, + expectError: true, }, "skip-validation-on-null": { in: types.Float64Null(), - validator: float64validator.OneOf( + oneOfValues: []float64{ 234.567, 8910.11, 1213.1415, - ), - expErrors: 0, + }, }, "skip-validation-on-unknown": { in: types.Float64Unknown(), - validator: float64validator.OneOf( + oneOfValues: []float64{ 234.567, 8910.11, 1213.1415, - ), - expErrors: 0, + }, }, } for name, test := range testCases { name, test := name, test - t.Run(name, func(t *testing.T) { + + t.Run(fmt.Sprintf("ValidateFloat64 - %s", name), func(t *testing.T) { t.Parallel() req := validator.Float64Request{ ConfigValue: test.in, } res := validator.Float64Response{} - test.validator.ValidateFloat64(context.TODO(), req, &res) + float64validator.OneOf(test.oneOfValues...).ValidateFloat64(context.TODO(), req, &res) + + if !res.Diagnostics.HasError() && test.expectError { + t.Fatal("expected error, got no error") + } - if test.expErrors > 0 && !res.Diagnostics.HasError() { - t.Fatalf("expected %d error(s), got none", test.expErrors) + if res.Diagnostics.HasError() && !test.expectError { + t.Fatalf("got unexpected error: %s", res.Diagnostics) + } + }) + + t.Run(fmt.Sprintf("ValidateParameterFloat64 - %s", name), func(t *testing.T) { + t.Parallel() + req := function.Float64ParameterValidatorRequest{ + Value: test.in, } + res := function.Float64ParameterValidatorResponse{} + float64validator.OneOf(test.oneOfValues...).ValidateParameterFloat64(context.TODO(), req, &res) - if test.expErrors > 0 && test.expErrors != res.Diagnostics.ErrorsCount() { - t.Fatalf("expected %d error(s), got %d: %v", test.expErrors, res.Diagnostics.ErrorsCount(), res.Diagnostics) + if res.Error == nil && test.expectError { + t.Fatal("expected error, got no error") } - if test.expErrors == 0 && res.Diagnostics.HasError() { - t.Fatalf("expected no error(s), got %d: %v", res.Diagnostics.ErrorsCount(), res.Diagnostics) + if res.Error != nil && !test.expectError { + t.Fatalf("got unexpected error: %s", res.Error) } }) } diff --git a/helpers/validatordiag/diag.go b/helpers/validatordiag/diag.go index 09a65e6..e856373 100644 --- a/helpers/validatordiag/diag.go +++ b/helpers/validatordiag/diag.go @@ -72,6 +72,19 @@ func BugInProviderDiagnostic(summary string) diag.Diagnostic { ) } +func InvalidValidatorUsageDiagnostic(path path.Path, validatorName string, description string) diag.Diagnostic { + return diag.NewAttributeErrorDiagnostic( + path, + "Invalid Validator Usage", + fmt.Sprintf("When validating the schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "An invalid usage of the %q validator was found: %s", + validatorName, + description, + ), + ) +} + // capitalize will uppercase the first letter in a UTF-8 string. func capitalize(str string) string { if str == "" { diff --git a/helpers/validatorfuncerr/doc.go b/helpers/validatorfuncerr/doc.go new file mode 100644 index 0000000..39f1268 --- /dev/null +++ b/helpers/validatorfuncerr/doc.go @@ -0,0 +1,5 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +// Package validatorfuncerr provides error helpers for provider-defined function validators. +package validatorfuncerr diff --git a/helpers/validatorfuncerr/funcerr.go b/helpers/validatorfuncerr/funcerr.go new file mode 100644 index 0000000..94966c9 --- /dev/null +++ b/helpers/validatorfuncerr/funcerr.go @@ -0,0 +1,45 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package validatorfuncerr + +import ( + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/function" +) + +func InvalidParameterValueFuncError(argumentPosition int64, description string, value string) *function.FuncError { + return function.NewArgumentFuncError( + argumentPosition, + fmt.Sprintf("Invalid Parameter Value: %s, got: %s", description, value), + ) +} + +func InvalidParameterValueLengthFuncError(argumentPosition int64, description string, value string) *function.FuncError { + return function.NewArgumentFuncError( + argumentPosition, + fmt.Sprintf("Invalid Parameter Value Length: %s, got: %s", description, value), + ) +} + +func InvalidParameterValueMatchFuncError(argumentPosition int64, description string, value string) *function.FuncError { + return function.NewArgumentFuncError( + argumentPosition, + fmt.Sprintf("Invalid Parameter Value Match: %s, got: %s", description, value), + ) +} + +func InvalidValidatorUsageFuncError(argumentPosition int64, validatorName string, description string) *function.FuncError { + return function.NewArgumentFuncError( + argumentPosition, + fmt.Sprintf( + "Invalid Validator Usage: "+ + "When validating the function definition, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "An invalid usage of the %q validator was found: %s", + validatorName, + description, + ), + ) +} diff --git a/int32validator/at_least.go b/int32validator/at_least.go index 681c1a8..c33d635 100644 --- a/int32validator/at_least.go +++ b/int32validator/at_least.go @@ -7,29 +7,28 @@ import ( "context" "fmt" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatorfuncerr" ) var _ validator.Int32 = atLeastValidator{} +var _ function.Int32ParameterValidator = atLeastValidator{} -// atLeastValidator validates that an integer Attribute's value is at least a certain value. type atLeastValidator struct { min int32 } -// Description describes the validation in plain text formatting. func (validator atLeastValidator) Description(_ context.Context) string { return fmt.Sprintf("value must be at least %d", validator.min) } -// MarkdownDescription describes the validation in Markdown formatting. func (validator atLeastValidator) MarkdownDescription(ctx context.Context) string { return validator.Description(ctx) } -// ValidateInt32 performs the validation. func (v atLeastValidator) ValidateInt32(ctx context.Context, request validator.Int32Request, response *validator.Int32Response) { if request.ConfigValue.IsNull() || request.ConfigValue.IsUnknown() { return @@ -44,14 +43,28 @@ func (v atLeastValidator) ValidateInt32(ctx context.Context, request validator.I } } +func (v atLeastValidator) ValidateParameterInt32(ctx context.Context, request function.Int32ParameterValidatorRequest, response *function.Int32ParameterValidatorResponse) { + if request.Value.IsNull() || request.Value.IsUnknown() { + return + } + + if request.Value.ValueInt32() < v.min { + response.Error = validatorfuncerr.InvalidParameterValueFuncError( + request.ArgumentPosition, + v.Description(ctx), + fmt.Sprintf("%d", request.Value.ValueInt32()), + ) + } +} + // AtLeast returns an AttributeValidator which ensures that any configured -// attribute value: +// attribute or function parameter value: // // - Is a number, which can be represented by a 32-bit integer. // - Is greater than or equal to the given minimum. // // Null (unconfigured) and unknown (known after apply) values are skipped. -func AtLeast(minVal int32) validator.Int32 { +func AtLeast(minVal int32) atLeastValidator { return atLeastValidator{ min: minVal, } diff --git a/int32validator/at_least_example_test.go b/int32validator/at_least_example_test.go index d97197b..b0e9d38 100644 --- a/int32validator/at_least_example_test.go +++ b/int32validator/at_least_example_test.go @@ -5,6 +5,7 @@ package int32validator_test import ( "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework-validators/int32validator" @@ -24,3 +25,17 @@ func ExampleAtLeast() { }, } } + +func ExampleAtLeast_function() { + _ = function.Definition{ + Parameters: []function.Parameter{ + function.Int32Parameter{ + Name: "example_param", + Validators: []function.Int32ParameterValidator{ + // Validate integer value must be at least 42 + int32validator.AtLeast(42), + }, + }, + }, + } +} diff --git a/int32validator/at_least_test.go b/int32validator/at_least_test.go index d6e875b..9eeb036 100644 --- a/int32validator/at_least_test.go +++ b/int32validator/at_least_test.go @@ -5,8 +5,10 @@ package int32validator_test import ( "context" + "fmt" "testing" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" @@ -48,7 +50,8 @@ func TestAtLeastValidator(t *testing.T) { for name, test := range tests { name, test := name, test - t.Run(name, func(t *testing.T) { + + t.Run(fmt.Sprintf("ValidateInt32 - %s", name), func(t *testing.T) { t.Parallel() request := validator.Int32Request{ Path: path.Root("test"), @@ -66,5 +69,22 @@ func TestAtLeastValidator(t *testing.T) { t.Fatalf("got unexpected error: %s", response.Diagnostics) } }) + + t.Run(fmt.Sprintf("ValidateParameterInt32 - %s", name), func(t *testing.T) { + t.Parallel() + request := function.Int32ParameterValidatorRequest{ + Value: test.val, + } + response := function.Int32ParameterValidatorResponse{} + int32validator.AtLeast(test.min).ValidateParameterInt32(context.TODO(), request, &response) + + if response.Error == nil && test.expectError { + t.Fatal("expected error, got no error") + } + + if response.Error != nil && !test.expectError { + t.Fatalf("got unexpected error: %s", response.Error) + } + }) } } diff --git a/int32validator/at_most.go b/int32validator/at_most.go index afc1d30..38f15d6 100644 --- a/int32validator/at_most.go +++ b/int32validator/at_most.go @@ -7,29 +7,28 @@ import ( "context" "fmt" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatorfuncerr" ) var _ validator.Int32 = atMostValidator{} +var _ function.Int32ParameterValidator = atMostValidator{} -// atMostValidator validates that an integer Attribute's value is at most a certain value. type atMostValidator struct { max int32 } -// Description describes the validation in plain text formatting. func (validator atMostValidator) Description(_ context.Context) string { return fmt.Sprintf("value must be at most %d", validator.max) } -// MarkdownDescription describes the validation in Markdown formatting. func (validator atMostValidator) MarkdownDescription(ctx context.Context) string { return validator.Description(ctx) } -// ValidateInt32 performs the validation. func (v atMostValidator) ValidateInt32(ctx context.Context, request validator.Int32Request, response *validator.Int32Response) { if request.ConfigValue.IsNull() || request.ConfigValue.IsUnknown() { return @@ -44,14 +43,28 @@ func (v atMostValidator) ValidateInt32(ctx context.Context, request validator.In } } +func (v atMostValidator) ValidateParameterInt32(ctx context.Context, request function.Int32ParameterValidatorRequest, response *function.Int32ParameterValidatorResponse) { + if request.Value.IsNull() || request.Value.IsUnknown() { + return + } + + if request.Value.ValueInt32() > v.max { + response.Error = validatorfuncerr.InvalidParameterValueFuncError( + request.ArgumentPosition, + v.Description(ctx), + fmt.Sprintf("%d", request.Value.ValueInt32()), + ) + } +} + // AtMost returns an AttributeValidator which ensures that any configured -// attribute value: +// attribute or function parameter value: // // - Is a number, which can be represented by a 32-bit integer. // - Is less than or equal to the given maximum. // // Null (unconfigured) and unknown (known after apply) values are skipped. -func AtMost(maxVal int32) validator.Int32 { +func AtMost(maxVal int32) atMostValidator { return atMostValidator{ max: maxVal, } diff --git a/int32validator/at_most_example_test.go b/int32validator/at_most_example_test.go index af49a23..9a291c9 100644 --- a/int32validator/at_most_example_test.go +++ b/int32validator/at_most_example_test.go @@ -5,6 +5,7 @@ package int32validator_test import ( "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework-validators/int32validator" @@ -24,3 +25,17 @@ func ExampleAtMost() { }, } } + +func ExampleAtMost_function() { + _ = function.Definition{ + Parameters: []function.Parameter{ + function.Int32Parameter{ + Name: "example_param", + Validators: []function.Int32ParameterValidator{ + // Validate integer value must be at most 42 + int32validator.AtMost(42), + }, + }, + }, + } +} diff --git a/int32validator/at_most_test.go b/int32validator/at_most_test.go index 177b135..c28a118 100644 --- a/int32validator/at_most_test.go +++ b/int32validator/at_most_test.go @@ -5,8 +5,10 @@ package int32validator_test import ( "context" + "fmt" "testing" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" @@ -48,7 +50,8 @@ func TestAtMostValidator(t *testing.T) { for name, test := range tests { name, test := name, test - t.Run(name, func(t *testing.T) { + + t.Run(fmt.Sprintf("ValidateInt32 - %s", name), func(t *testing.T) { t.Parallel() request := validator.Int32Request{ Path: path.Root("test"), @@ -66,5 +69,22 @@ func TestAtMostValidator(t *testing.T) { t.Fatalf("got unexpected error: %s", response.Diagnostics) } }) + + t.Run(fmt.Sprintf("ValidateParameterInt32 - %s", name), func(t *testing.T) { + t.Parallel() + request := function.Int32ParameterValidatorRequest{ + Value: test.val, + } + response := function.Int32ParameterValidatorResponse{} + int32validator.AtMost(test.max).ValidateParameterInt32(context.TODO(), request, &response) + + if response.Error == nil && test.expectError { + t.Fatal("expected error, got no error") + } + + if response.Error != nil && !test.expectError { + t.Fatalf("got unexpected error: %s", response.Error) + } + }) } } diff --git a/int32validator/between.go b/int32validator/between.go index 18f0b04..ea7291d 100644 --- a/int32validator/between.go +++ b/int32validator/between.go @@ -7,30 +7,46 @@ import ( "context" "fmt" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatorfuncerr" ) var _ validator.Int32 = betweenValidator{} +var _ function.Int32ParameterValidator = betweenValidator{} -// betweenValidator validates that an integer Attribute's value is in a range. type betweenValidator struct { min, max int32 } -// Description describes the validation in plain text formatting. +func (validator betweenValidator) invalidUsageMessage() string { + return fmt.Sprintf("minVal cannot be greater than maxVal - minVal: %d, maxVal: %d", validator.min, validator.max) +} + func (validator betweenValidator) Description(_ context.Context) string { return fmt.Sprintf("value must be between %d and %d", validator.min, validator.max) } -// MarkdownDescription describes the validation in Markdown formatting. func (validator betweenValidator) MarkdownDescription(ctx context.Context) string { return validator.Description(ctx) } -// ValidateInt32 performs the validation. func (v betweenValidator) ValidateInt32(ctx context.Context, request validator.Int32Request, response *validator.Int32Response) { + // Return an error if the validator has been created in an invalid state + if v.min > v.max { + response.Diagnostics.Append( + validatordiag.InvalidValidatorUsageDiagnostic( + request.Path, + "Between", + v.invalidUsageMessage(), + ), + ) + + return + } + if request.ConfigValue.IsNull() || request.ConfigValue.IsUnknown() { return } @@ -44,18 +60,42 @@ func (v betweenValidator) ValidateInt32(ctx context.Context, request validator.I } } +func (v betweenValidator) ValidateParameterInt32(ctx context.Context, request function.Int32ParameterValidatorRequest, response *function.Int32ParameterValidatorResponse) { + // Return an error if the validator has been created in an invalid state + if v.min > v.max { + response.Error = validatorfuncerr.InvalidValidatorUsageFuncError( + request.ArgumentPosition, + "Between", + v.invalidUsageMessage(), + ) + + return + } + + if request.Value.IsNull() || request.Value.IsUnknown() { + return + } + + if request.Value.ValueInt32() < v.min || request.Value.ValueInt32() > v.max { + response.Error = validatorfuncerr.InvalidParameterValueFuncError( + request.ArgumentPosition, + v.Description(ctx), + fmt.Sprintf("%d", request.Value.ValueInt32()), + ) + } +} + // Between returns an AttributeValidator which ensures that any configured -// attribute value: +// attribute or function parameter value: // // - Is a number, which can be represented by a 32-bit integer. // - Is greater than or equal to the given minimum and less than or equal to the given maximum. // // Null (unconfigured) and unknown (known after apply) values are skipped. -func Between(minVal, maxVal int32) validator.Int32 { - if minVal > maxVal { - return nil - } - +// +// minVal cannot be greater than maxVal. Invalid combinations of +// minVal and maxVal will result in an implementation error message during validation. +func Between(minVal, maxVal int32) betweenValidator { return betweenValidator{ min: minVal, max: maxVal, diff --git a/int32validator/between_example_test.go b/int32validator/between_example_test.go index b0f8512..754c788 100644 --- a/int32validator/between_example_test.go +++ b/int32validator/between_example_test.go @@ -5,6 +5,7 @@ package int32validator_test import ( "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework-validators/int32validator" @@ -24,3 +25,17 @@ func ExampleBetween() { }, } } + +func ExampleBetween_function() { + _ = function.Definition{ + Parameters: []function.Parameter{ + function.Int32Parameter{ + Name: "example_param", + Validators: []function.Int32ParameterValidator{ + // Validate integer value must be at least 10 and at most 100 + int32validator.Between(10, 100), + }, + }, + }, + } +} diff --git a/int32validator/between_test.go b/int32validator/between_test.go index 8167ab4..b67f45b 100644 --- a/int32validator/between_test.go +++ b/int32validator/between_test.go @@ -5,8 +5,10 @@ package int32validator_test import ( "context" + "fmt" "testing" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" @@ -61,11 +63,18 @@ func TestBetweenValidator(t *testing.T) { max: 3, expectError: true, }, + "invalid validator usage - minVal > maxVal": { + val: types.Int32Value(2), + min: 3, + max: 1, + expectError: true, + }, } for name, test := range tests { name, test := name, test - t.Run(name, func(t *testing.T) { + + t.Run(fmt.Sprintf("ValidateInt32 - %s", name), func(t *testing.T) { t.Parallel() request := validator.Int32Request{ Path: path.Root("test"), @@ -83,5 +92,23 @@ func TestBetweenValidator(t *testing.T) { t.Fatalf("got unexpected error: %s", response.Diagnostics) } }) + + t.Run(fmt.Sprintf("ValidateParameterInt32 - %s", name), func(t *testing.T) { + t.Parallel() + request := function.Int32ParameterValidatorRequest{ + ArgumentPosition: 0, + Value: test.val, + } + response := function.Int32ParameterValidatorResponse{} + int32validator.Between(test.min, test.max).ValidateParameterInt32(context.TODO(), request, &response) + + if response.Error == nil && test.expectError { + t.Fatal("expected error, got no error") + } + + if response.Error != nil && !test.expectError { + t.Fatalf("got unexpected error: %s", response.Error) + } + }) } } diff --git a/int32validator/doc.go b/int32validator/doc.go index c6403fb..f91dd67 100644 --- a/int32validator/doc.go +++ b/int32validator/doc.go @@ -1,5 +1,5 @@ // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 -// Package int32validator provides validators for types.Int32 attributes. +// Package int32validator provides validators for types.Int32 attributes or function parameters. package int32validator diff --git a/int32validator/none_of.go b/int32validator/none_of.go index c22bba9..fe985e9 100644 --- a/int32validator/none_of.go +++ b/int32validator/none_of.go @@ -7,15 +7,17 @@ import ( "context" "fmt" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatorfuncerr" ) var _ validator.Int32 = noneOfValidator{} +var _ function.Int32ParameterValidator = noneOfValidator{} -// noneOfValidator validates that the value does not match one of the values. type noneOfValidator struct { values []types.Int32 } @@ -50,9 +52,31 @@ func (v noneOfValidator) ValidateInt32(ctx context.Context, request validator.In } } -// NoneOf checks that the Int32 held in the attribute +func (v noneOfValidator) ValidateParameterInt32(ctx context.Context, request function.Int32ParameterValidatorRequest, response *function.Int32ParameterValidatorResponse) { + if request.Value.IsNull() || request.Value.IsUnknown() { + return + } + + value := request.Value + + for _, otherValue := range v.values { + if !value.Equal(otherValue) { + continue + } + + response.Error = validatorfuncerr.InvalidParameterValueMatchFuncError( + request.ArgumentPosition, + v.Description(ctx), + value.String(), + ) + + break + } +} + +// NoneOf checks that the Int32 held in the attribute or function parameter // is none of the given `values`. -func NoneOf(values ...int32) validator.Int32 { +func NoneOf(values ...int32) noneOfValidator { frameworkValues := make([]types.Int32, 0, len(values)) for _, value := range values { diff --git a/int32validator/none_of_example_test.go b/int32validator/none_of_example_test.go index 4e7f232..e088005 100644 --- a/int32validator/none_of_example_test.go +++ b/int32validator/none_of_example_test.go @@ -5,6 +5,7 @@ package int32validator_test import ( "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework-validators/int32validator" @@ -24,3 +25,17 @@ func ExampleNoneOf() { }, } } + +func ExampleNoneOf_function() { + _ = function.Definition{ + Parameters: []function.Parameter{ + function.Int32Parameter{ + Name: "example_param", + Validators: []function.Int32ParameterValidator{ + // Validate integer value must not be 12, 24, or 48 + int32validator.NoneOf([]int32{12, 24, 48}...), + }, + }, + }, + } +} diff --git a/int32validator/none_of_test.go b/int32validator/none_of_test.go index d2b3769..2ad66c0 100644 --- a/int32validator/none_of_test.go +++ b/int32validator/none_of_test.go @@ -5,8 +5,10 @@ package int32validator_test import ( "context" + "fmt" "testing" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" @@ -17,71 +19,82 @@ func TestNoneOfValidator(t *testing.T) { t.Parallel() type testCase struct { - in types.Int32 - validator validator.Int32 - expErrors int + in types.Int32 + noneOfValues []int32 + expectError bool } testCases := map[string]testCase{ "simple-match": { in: types.Int32Value(123), - validator: int32validator.NoneOf( + noneOfValues: []int32{ 123, 234, 8910, 1213, - ), - expErrors: 1, + }, + expectError: true, }, "simple-mismatch": { in: types.Int32Value(123), - validator: int32validator.NoneOf( + noneOfValues: []int32{ 234, 8910, 1213, - ), - expErrors: 0, + }, }, "skip-validation-on-null": { in: types.Int32Null(), - validator: int32validator.NoneOf( + noneOfValues: []int32{ 234, 8910, 1213, - ), - expErrors: 0, + }, }, "skip-validation-on-unknown": { in: types.Int32Unknown(), - validator: int32validator.NoneOf( + noneOfValues: []int32{ 234, 8910, 1213, - ), - expErrors: 0, + }, }, } for name, test := range testCases { name, test := name, test - t.Run(name, func(t *testing.T) { + + t.Run(fmt.Sprintf("ValidateInt32 - %s", name), func(t *testing.T) { t.Parallel() req := validator.Int32Request{ ConfigValue: test.in, } res := validator.Int32Response{} - test.validator.ValidateInt32(context.TODO(), req, &res) + int32validator.NoneOf(test.noneOfValues...).ValidateInt32(context.TODO(), req, &res) + + if !res.Diagnostics.HasError() && test.expectError { + t.Fatal("expected error, got no error") + } - if test.expErrors > 0 && !res.Diagnostics.HasError() { - t.Fatalf("expected %d error(s), got none", test.expErrors) + if res.Diagnostics.HasError() && !test.expectError { + t.Fatalf("got unexpected error: %s", res.Diagnostics) + } + }) + + t.Run(fmt.Sprintf("ValidateParameterInt32 - %s", name), func(t *testing.T) { + t.Parallel() + req := function.Int32ParameterValidatorRequest{ + Value: test.in, } + res := function.Int32ParameterValidatorResponse{} + int32validator.NoneOf(test.noneOfValues...).ValidateParameterInt32(context.TODO(), req, &res) - if test.expErrors > 0 && test.expErrors != res.Diagnostics.ErrorsCount() { - t.Fatalf("expected %d error(s), got %d: %v", test.expErrors, res.Diagnostics.ErrorsCount(), res.Diagnostics) + if res.Error == nil && test.expectError { + t.Fatal("expected error, got no error") } - if test.expErrors == 0 && res.Diagnostics.HasError() { - t.Fatalf("expected no error(s), got %d: %v", res.Diagnostics.ErrorsCount(), res.Diagnostics) + if res.Error != nil && !test.expectError { + t.Fatalf("got unexpected error: %s", res.Error) } }) } diff --git a/int32validator/one_of.go b/int32validator/one_of.go index 04974b5..2877b94 100644 --- a/int32validator/one_of.go +++ b/int32validator/one_of.go @@ -7,15 +7,17 @@ import ( "context" "fmt" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatorfuncerr" ) var _ validator.Int32 = oneOfValidator{} +var _ function.Int32ParameterValidator = oneOfValidator{} -// oneOfValidator validates that the value matches one of expected values. type oneOfValidator struct { values []types.Int32 } @@ -48,9 +50,29 @@ func (v oneOfValidator) ValidateInt32(ctx context.Context, request validator.Int )) } -// OneOf checks that the Int32 held in the attribute +func (v oneOfValidator) ValidateParameterInt32(ctx context.Context, request function.Int32ParameterValidatorRequest, response *function.Int32ParameterValidatorResponse) { + if request.Value.IsNull() || request.Value.IsUnknown() { + return + } + + value := request.Value + + for _, otherValue := range v.values { + if value.Equal(otherValue) { + return + } + } + + response.Error = validatorfuncerr.InvalidParameterValueMatchFuncError( + request.ArgumentPosition, + v.Description(ctx), + value.String(), + ) +} + +// OneOf checks that the Int32 held in the attribute or function parameter // is one of the given `values`. -func OneOf(values ...int32) validator.Int32 { +func OneOf(values ...int32) oneOfValidator { frameworkValues := make([]types.Int32, 0, len(values)) for _, value := range values { diff --git a/int32validator/one_of_example_test.go b/int32validator/one_of_example_test.go index 90fa475..2023ae0 100644 --- a/int32validator/one_of_example_test.go +++ b/int32validator/one_of_example_test.go @@ -5,6 +5,7 @@ package int32validator_test import ( "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework-validators/int32validator" @@ -24,3 +25,17 @@ func ExampleOneOf() { }, } } + +func ExampleOneOf_function() { + _ = function.Definition{ + Parameters: []function.Parameter{ + function.Int32Parameter{ + Name: "example_param", + Validators: []function.Int32ParameterValidator{ + // Validate integer value must be 12, 24, or 48 + int32validator.OneOf([]int32{12, 24, 48}...), + }, + }, + }, + } +} diff --git a/int32validator/one_of_test.go b/int32validator/one_of_test.go index e808661..f04444e 100644 --- a/int32validator/one_of_test.go +++ b/int32validator/one_of_test.go @@ -5,83 +5,95 @@ package int32validator_test import ( "context" + "fmt" "testing" + "github.com/hashicorp/terraform-plugin-framework-validators/int32validator" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - - "github.com/hashicorp/terraform-plugin-framework-validators/int32validator" ) func TestOneOfValidator(t *testing.T) { t.Parallel() type testCase struct { - in types.Int32 - validator validator.Int32 - expErrors int + in types.Int32 + oneOfValues []int32 + expectError bool } testCases := map[string]testCase{ "simple-match": { in: types.Int32Value(123), - validator: int32validator.OneOf( + oneOfValues: []int32{ 123, 234, 8910, 1213, - ), - expErrors: 0, + }, }, "simple-mismatch": { in: types.Int32Value(123), - validator: int32validator.OneOf( + oneOfValues: []int32{ 234, 8910, 1213, - ), - expErrors: 1, + }, + expectError: true, }, "skip-validation-on-null": { in: types.Int32Null(), - validator: int32validator.OneOf( + oneOfValues: []int32{ 234, 8910, 1213, - ), - expErrors: 0, + }, }, "skip-validation-on-unknown": { in: types.Int32Unknown(), - validator: int32validator.OneOf( + oneOfValues: []int32{ 234, 8910, 1213, - ), - expErrors: 0, + }, }, } for name, test := range testCases { name, test := name, test - t.Run(name, func(t *testing.T) { + + t.Run(fmt.Sprintf("ValidateInt32 - %s", name), func(t *testing.T) { t.Parallel() req := validator.Int32Request{ ConfigValue: test.in, } res := validator.Int32Response{} - test.validator.ValidateInt32(context.TODO(), req, &res) + int32validator.OneOf(test.oneOfValues...).ValidateInt32(context.TODO(), req, &res) + + if !res.Diagnostics.HasError() && test.expectError { + t.Fatal("expected error, got no error") + } - if test.expErrors > 0 && !res.Diagnostics.HasError() { - t.Fatalf("expected %d error(s), got none", test.expErrors) + if res.Diagnostics.HasError() && !test.expectError { + t.Fatalf("got unexpected error: %s", res.Diagnostics) + } + }) + + t.Run(fmt.Sprintf("ValidateParameterInt32 - %s", name), func(t *testing.T) { + t.Parallel() + req := function.Int32ParameterValidatorRequest{ + Value: test.in, } + res := function.Int32ParameterValidatorResponse{} + int32validator.OneOf(test.oneOfValues...).ValidateParameterInt32(context.TODO(), req, &res) - if test.expErrors > 0 && test.expErrors != res.Diagnostics.ErrorsCount() { - t.Fatalf("expected %d error(s), got %d: %v", test.expErrors, res.Diagnostics.ErrorsCount(), res.Diagnostics) + if res.Error == nil && test.expectError { + t.Fatal("expected error, got no error") } - if test.expErrors == 0 && res.Diagnostics.HasError() { - t.Fatalf("expected no error(s), got %d: %v", res.Diagnostics.ErrorsCount(), res.Diagnostics) + if res.Error != nil && !test.expectError { + t.Fatalf("got unexpected error: %s", res.Error) } }) } diff --git a/int64validator/at_least.go b/int64validator/at_least.go index 18fade0..54f8958 100644 --- a/int64validator/at_least.go +++ b/int64validator/at_least.go @@ -7,29 +7,28 @@ import ( "context" "fmt" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatorfuncerr" ) var _ validator.Int64 = atLeastValidator{} +var _ function.Int64ParameterValidator = atLeastValidator{} -// atLeastValidator validates that an integer Attribute's value is at least a certain value. type atLeastValidator struct { min int64 } -// Description describes the validation in plain text formatting. func (validator atLeastValidator) Description(_ context.Context) string { return fmt.Sprintf("value must be at least %d", validator.min) } -// MarkdownDescription describes the validation in Markdown formatting. func (validator atLeastValidator) MarkdownDescription(ctx context.Context) string { return validator.Description(ctx) } -// ValidateInt64 performs the validation. func (v atLeastValidator) ValidateInt64(ctx context.Context, request validator.Int64Request, response *validator.Int64Response) { if request.ConfigValue.IsNull() || request.ConfigValue.IsUnknown() { return @@ -44,14 +43,28 @@ func (v atLeastValidator) ValidateInt64(ctx context.Context, request validator.I } } +func (v atLeastValidator) ValidateParameterInt64(ctx context.Context, request function.Int64ParameterValidatorRequest, response *function.Int64ParameterValidatorResponse) { + if request.Value.IsNull() || request.Value.IsUnknown() { + return + } + + if request.Value.ValueInt64() < v.min { + response.Error = validatorfuncerr.InvalidParameterValueFuncError( + request.ArgumentPosition, + v.Description(ctx), + fmt.Sprintf("%d", request.Value.ValueInt64()), + ) + } +} + // AtLeast returns an AttributeValidator which ensures that any configured -// attribute value: +// attribute or function parameter value: // // - Is a number, which can be represented by a 64-bit integer. // - Is greater than or equal to the given minimum. // // Null (unconfigured) and unknown (known after apply) values are skipped. -func AtLeast(minVal int64) validator.Int64 { +func AtLeast(minVal int64) atLeastValidator { return atLeastValidator{ min: minVal, } diff --git a/int64validator/at_least_example_test.go b/int64validator/at_least_example_test.go index a2e1d4d..55d206d 100644 --- a/int64validator/at_least_example_test.go +++ b/int64validator/at_least_example_test.go @@ -6,6 +6,7 @@ package int64validator_test import ( "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/schema/validator" ) @@ -23,3 +24,17 @@ func ExampleAtLeast() { }, } } + +func ExampleAtLeast_function() { + _ = function.Definition{ + Parameters: []function.Parameter{ + function.Int64Parameter{ + Name: "example_param", + Validators: []function.Int64ParameterValidator{ + // Validate integer value must be at least 42 + int64validator.AtLeast(42), + }, + }, + }, + } +} diff --git a/int64validator/at_least_test.go b/int64validator/at_least_test.go index ddac371..d224a5d 100644 --- a/int64validator/at_least_test.go +++ b/int64validator/at_least_test.go @@ -5,8 +5,10 @@ package int64validator_test import ( "context" + "fmt" "testing" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" @@ -48,7 +50,8 @@ func TestAtLeastValidator(t *testing.T) { for name, test := range tests { name, test := name, test - t.Run(name, func(t *testing.T) { + + t.Run(fmt.Sprintf("ValidateInt64 - %s", name), func(t *testing.T) { t.Parallel() request := validator.Int64Request{ Path: path.Root("test"), @@ -66,5 +69,22 @@ func TestAtLeastValidator(t *testing.T) { t.Fatalf("got unexpected error: %s", response.Diagnostics) } }) + + t.Run(fmt.Sprintf("ValidateParameterInt64 - %s", name), func(t *testing.T) { + t.Parallel() + request := function.Int64ParameterValidatorRequest{ + Value: test.val, + } + response := function.Int64ParameterValidatorResponse{} + int64validator.AtLeast(test.min).ValidateParameterInt64(context.TODO(), request, &response) + + if response.Error == nil && test.expectError { + t.Fatal("expected error, got no error") + } + + if response.Error != nil && !test.expectError { + t.Fatalf("got unexpected error: %s", response.Error) + } + }) } } diff --git a/int64validator/at_most.go b/int64validator/at_most.go index a765050..afc7dff 100644 --- a/int64validator/at_most.go +++ b/int64validator/at_most.go @@ -7,29 +7,28 @@ import ( "context" "fmt" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatorfuncerr" ) var _ validator.Int64 = atMostValidator{} +var _ function.Int64ParameterValidator = atMostValidator{} -// atMostValidator validates that an integer Attribute's value is at most a certain value. type atMostValidator struct { max int64 } -// Description describes the validation in plain text formatting. func (validator atMostValidator) Description(_ context.Context) string { return fmt.Sprintf("value must be at most %d", validator.max) } -// MarkdownDescription describes the validation in Markdown formatting. func (validator atMostValidator) MarkdownDescription(ctx context.Context) string { return validator.Description(ctx) } -// ValidateInt64 performs the validation. func (v atMostValidator) ValidateInt64(ctx context.Context, request validator.Int64Request, response *validator.Int64Response) { if request.ConfigValue.IsNull() || request.ConfigValue.IsUnknown() { return @@ -44,14 +43,28 @@ func (v atMostValidator) ValidateInt64(ctx context.Context, request validator.In } } +func (v atMostValidator) ValidateParameterInt64(ctx context.Context, request function.Int64ParameterValidatorRequest, response *function.Int64ParameterValidatorResponse) { + if request.Value.IsNull() || request.Value.IsUnknown() { + return + } + + if request.Value.ValueInt64() > v.max { + response.Error = validatorfuncerr.InvalidParameterValueFuncError( + request.ArgumentPosition, + v.Description(ctx), + fmt.Sprintf("%d", request.Value.ValueInt64()), + ) + } +} + // AtMost returns an AttributeValidator which ensures that any configured -// attribute value: +// attribute or function parameter value: // // - Is a number, which can be represented by a 64-bit integer. // - Is less than or equal to the given maximum. // // Null (unconfigured) and unknown (known after apply) values are skipped. -func AtMost(maxVal int64) validator.Int64 { +func AtMost(maxVal int64) atMostValidator { return atMostValidator{ max: maxVal, } diff --git a/int64validator/at_most_example_test.go b/int64validator/at_most_example_test.go index fb8546b..c25747b 100644 --- a/int64validator/at_most_example_test.go +++ b/int64validator/at_most_example_test.go @@ -6,6 +6,7 @@ package int64validator_test import ( "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/schema/validator" ) @@ -23,3 +24,17 @@ func ExampleAtMost() { }, } } + +func ExampleAtMost_function() { + _ = function.Definition{ + Parameters: []function.Parameter{ + function.Int64Parameter{ + Name: "example_param", + Validators: []function.Int64ParameterValidator{ + // Validate integer value must be at most 42 + int64validator.AtMost(42), + }, + }, + }, + } +} diff --git a/int64validator/at_most_test.go b/int64validator/at_most_test.go index 88be51c..a94908d 100644 --- a/int64validator/at_most_test.go +++ b/int64validator/at_most_test.go @@ -5,8 +5,10 @@ package int64validator_test import ( "context" + "fmt" "testing" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" @@ -48,7 +50,8 @@ func TestAtMostValidator(t *testing.T) { for name, test := range tests { name, test := name, test - t.Run(name, func(t *testing.T) { + + t.Run(fmt.Sprintf("ValidateInt64 - %s", name), func(t *testing.T) { t.Parallel() request := validator.Int64Request{ Path: path.Root("test"), @@ -66,5 +69,22 @@ func TestAtMostValidator(t *testing.T) { t.Fatalf("got unexpected error: %s", response.Diagnostics) } }) + + t.Run(fmt.Sprintf("ValidateParameterInt64 - %s", name), func(t *testing.T) { + t.Parallel() + request := function.Int64ParameterValidatorRequest{ + Value: test.val, + } + response := function.Int64ParameterValidatorResponse{} + int64validator.AtMost(test.max).ValidateParameterInt64(context.TODO(), request, &response) + + if response.Error == nil && test.expectError { + t.Fatal("expected error, got no error") + } + + if response.Error != nil && !test.expectError { + t.Fatalf("got unexpected error: %s", response.Error) + } + }) } } diff --git a/int64validator/between.go b/int64validator/between.go index f018bb9..e414c3c 100644 --- a/int64validator/between.go +++ b/int64validator/between.go @@ -7,30 +7,46 @@ import ( "context" "fmt" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatorfuncerr" ) var _ validator.Int64 = betweenValidator{} +var _ function.Int64ParameterValidator = betweenValidator{} -// betweenValidator validates that an integer Attribute's value is in a range. type betweenValidator struct { min, max int64 } -// Description describes the validation in plain text formatting. +func (validator betweenValidator) invalidUsageMessage() string { + return fmt.Sprintf("minVal cannot be greater than maxVal - minVal: %d, maxVal: %d", validator.min, validator.max) +} + func (validator betweenValidator) Description(_ context.Context) string { return fmt.Sprintf("value must be between %d and %d", validator.min, validator.max) } -// MarkdownDescription describes the validation in Markdown formatting. func (validator betweenValidator) MarkdownDescription(ctx context.Context) string { return validator.Description(ctx) } -// ValidateInt64 performs the validation. func (v betweenValidator) ValidateInt64(ctx context.Context, request validator.Int64Request, response *validator.Int64Response) { + // Return an error if the validator has been created in an invalid state + if v.min > v.max { + response.Diagnostics.Append( + validatordiag.InvalidValidatorUsageDiagnostic( + request.Path, + "Between", + v.invalidUsageMessage(), + ), + ) + + return + } + if request.ConfigValue.IsNull() || request.ConfigValue.IsUnknown() { return } @@ -44,18 +60,42 @@ func (v betweenValidator) ValidateInt64(ctx context.Context, request validator.I } } +func (v betweenValidator) ValidateParameterInt64(ctx context.Context, request function.Int64ParameterValidatorRequest, response *function.Int64ParameterValidatorResponse) { + // Return an error if the validator has been created in an invalid state + if v.min > v.max { + response.Error = validatorfuncerr.InvalidValidatorUsageFuncError( + request.ArgumentPosition, + "Between", + v.invalidUsageMessage(), + ) + + return + } + + if request.Value.IsNull() || request.Value.IsUnknown() { + return + } + + if request.Value.ValueInt64() < v.min || request.Value.ValueInt64() > v.max { + response.Error = validatorfuncerr.InvalidParameterValueFuncError( + request.ArgumentPosition, + v.Description(ctx), + fmt.Sprintf("%d", request.Value.ValueInt64()), + ) + } +} + // Between returns an AttributeValidator which ensures that any configured -// attribute value: +// attribute or function parameter value: // // - Is a number, which can be represented by a 64-bit integer. // - Is greater than or equal to the given minimum and less than or equal to the given maximum. // // Null (unconfigured) and unknown (known after apply) values are skipped. -func Between(minVal, maxVal int64) validator.Int64 { - if minVal > maxVal { - return nil - } - +// +// minVal cannot be greater than maxVal. Invalid combinations of +// minVal and maxVal will result in an implementation error message during validation. +func Between(minVal, maxVal int64) betweenValidator { return betweenValidator{ min: minVal, max: maxVal, diff --git a/int64validator/between_example_test.go b/int64validator/between_example_test.go index 45274ce..076b15f 100644 --- a/int64validator/between_example_test.go +++ b/int64validator/between_example_test.go @@ -6,6 +6,7 @@ package int64validator_test import ( "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/schema/validator" ) @@ -23,3 +24,17 @@ func ExampleBetween() { }, } } + +func ExampleBetween_function() { + _ = function.Definition{ + Parameters: []function.Parameter{ + function.Int64Parameter{ + Name: "example_param", + Validators: []function.Int64ParameterValidator{ + // Validate integer value must be at least 10 and at most 100 + int64validator.Between(10, 100), + }, + }, + }, + } +} diff --git a/int64validator/between_test.go b/int64validator/between_test.go index 7170a3a..a7f428c 100644 --- a/int64validator/between_test.go +++ b/int64validator/between_test.go @@ -5,8 +5,10 @@ package int64validator_test import ( "context" + "fmt" "testing" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" @@ -61,11 +63,18 @@ func TestBetweenValidator(t *testing.T) { max: 3, expectError: true, }, + "invalid validator usage - minVal > maxVal": { + val: types.Int64Value(2), + min: 3, + max: 1, + expectError: true, + }, } for name, test := range tests { name, test := name, test - t.Run(name, func(t *testing.T) { + + t.Run(fmt.Sprintf("ValidateInt64 - %s", name), func(t *testing.T) { t.Parallel() request := validator.Int64Request{ Path: path.Root("test"), @@ -83,5 +92,23 @@ func TestBetweenValidator(t *testing.T) { t.Fatalf("got unexpected error: %s", response.Diagnostics) } }) + + t.Run(fmt.Sprintf("ValidateParameterInt64 - %s", name), func(t *testing.T) { + t.Parallel() + request := function.Int64ParameterValidatorRequest{ + ArgumentPosition: 0, + Value: test.val, + } + response := function.Int64ParameterValidatorResponse{} + int64validator.Between(test.min, test.max).ValidateParameterInt64(context.TODO(), request, &response) + + if response.Error == nil && test.expectError { + t.Fatal("expected error, got no error") + } + + if response.Error != nil && !test.expectError { + t.Fatalf("got unexpected error: %s", response.Error) + } + }) } } diff --git a/int64validator/doc.go b/int64validator/doc.go index 0e65c17..b35fd42 100644 --- a/int64validator/doc.go +++ b/int64validator/doc.go @@ -1,5 +1,5 @@ // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 -// Package int64validator provides validators for types.Int64 attributes. +// Package int64validator provides validators for types.Int64 attributes or function parameters. package int64validator diff --git a/int64validator/none_of.go b/int64validator/none_of.go index 749fe55..42bcb0f 100644 --- a/int64validator/none_of.go +++ b/int64validator/none_of.go @@ -7,15 +7,17 @@ import ( "context" "fmt" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatorfuncerr" ) var _ validator.Int64 = noneOfValidator{} +var _ function.Int64ParameterValidator = noneOfValidator{} -// noneOfValidator validates that the value does not match one of the values. type noneOfValidator struct { values []types.Int64 } @@ -50,9 +52,31 @@ func (v noneOfValidator) ValidateInt64(ctx context.Context, request validator.In } } -// NoneOf checks that the Int64 held in the attribute +func (v noneOfValidator) ValidateParameterInt64(ctx context.Context, request function.Int64ParameterValidatorRequest, response *function.Int64ParameterValidatorResponse) { + if request.Value.IsNull() || request.Value.IsUnknown() { + return + } + + value := request.Value + + for _, otherValue := range v.values { + if !value.Equal(otherValue) { + continue + } + + response.Error = validatorfuncerr.InvalidParameterValueMatchFuncError( + request.ArgumentPosition, + v.Description(ctx), + value.String(), + ) + + break + } +} + +// NoneOf checks that the Int64 held in the attribute or function parameter // is none of the given `values`. -func NoneOf(values ...int64) validator.Int64 { +func NoneOf(values ...int64) noneOfValidator { frameworkValues := make([]types.Int64, 0, len(values)) for _, value := range values { diff --git a/int64validator/none_of_example_test.go b/int64validator/none_of_example_test.go index f683587..4be6262 100644 --- a/int64validator/none_of_example_test.go +++ b/int64validator/none_of_example_test.go @@ -6,6 +6,7 @@ package int64validator_test import ( "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/schema/validator" ) @@ -23,3 +24,17 @@ func ExampleNoneOf() { }, } } + +func ExampleNoneOf_function() { + _ = function.Definition{ + Parameters: []function.Parameter{ + function.Int64Parameter{ + Name: "example_param", + Validators: []function.Int64ParameterValidator{ + // Validate integer value must not be 12, 24, or 48 + int64validator.NoneOf([]int64{12, 24, 48}...), + }, + }, + }, + } +} diff --git a/int64validator/none_of_test.go b/int64validator/none_of_test.go index 955002e..53726ba 100644 --- a/int64validator/none_of_test.go +++ b/int64validator/none_of_test.go @@ -5,8 +5,10 @@ package int64validator_test import ( "context" + "fmt" "testing" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" @@ -17,71 +19,82 @@ func TestNoneOfValidator(t *testing.T) { t.Parallel() type testCase struct { - in types.Int64 - validator validator.Int64 - expErrors int + in types.Int64 + noneOfValues []int64 + expectError bool } testCases := map[string]testCase{ "simple-match": { in: types.Int64Value(123), - validator: int64validator.NoneOf( + noneOfValues: []int64{ 123, 234, 8910, 1213, - ), - expErrors: 1, + }, + expectError: true, }, "simple-mismatch": { in: types.Int64Value(123), - validator: int64validator.NoneOf( + noneOfValues: []int64{ 234, 8910, 1213, - ), - expErrors: 0, + }, }, "skip-validation-on-null": { in: types.Int64Null(), - validator: int64validator.NoneOf( + noneOfValues: []int64{ 234, 8910, 1213, - ), - expErrors: 0, + }, }, "skip-validation-on-unknown": { in: types.Int64Unknown(), - validator: int64validator.NoneOf( + noneOfValues: []int64{ 234, 8910, 1213, - ), - expErrors: 0, + }, }, } for name, test := range testCases { name, test := name, test - t.Run(name, func(t *testing.T) { + + t.Run(fmt.Sprintf("ValidateInt64 - %s", name), func(t *testing.T) { t.Parallel() req := validator.Int64Request{ ConfigValue: test.in, } res := validator.Int64Response{} - test.validator.ValidateInt64(context.TODO(), req, &res) + int64validator.NoneOf(test.noneOfValues...).ValidateInt64(context.TODO(), req, &res) + + if !res.Diagnostics.HasError() && test.expectError { + t.Fatal("expected error, got no error") + } - if test.expErrors > 0 && !res.Diagnostics.HasError() { - t.Fatalf("expected %d error(s), got none", test.expErrors) + if res.Diagnostics.HasError() && !test.expectError { + t.Fatalf("got unexpected error: %s", res.Diagnostics) + } + }) + + t.Run(fmt.Sprintf("ValidateParameterInt64 - %s", name), func(t *testing.T) { + t.Parallel() + req := function.Int64ParameterValidatorRequest{ + Value: test.in, } + res := function.Int64ParameterValidatorResponse{} + int64validator.NoneOf(test.noneOfValues...).ValidateParameterInt64(context.TODO(), req, &res) - if test.expErrors > 0 && test.expErrors != res.Diagnostics.ErrorsCount() { - t.Fatalf("expected %d error(s), got %d: %v", test.expErrors, res.Diagnostics.ErrorsCount(), res.Diagnostics) + if res.Error == nil && test.expectError { + t.Fatal("expected error, got no error") } - if test.expErrors == 0 && res.Diagnostics.HasError() { - t.Fatalf("expected no error(s), got %d: %v", res.Diagnostics.ErrorsCount(), res.Diagnostics) + if res.Error != nil && !test.expectError { + t.Fatalf("got unexpected error: %s", res.Error) } }) } diff --git a/int64validator/one_of.go b/int64validator/one_of.go index 3a1e1db..264bbe3 100644 --- a/int64validator/one_of.go +++ b/int64validator/one_of.go @@ -7,15 +7,17 @@ import ( "context" "fmt" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatorfuncerr" ) var _ validator.Int64 = oneOfValidator{} +var _ function.Int64ParameterValidator = oneOfValidator{} -// oneOfValidator validates that the value matches one of expected values. type oneOfValidator struct { values []types.Int64 } @@ -48,9 +50,29 @@ func (v oneOfValidator) ValidateInt64(ctx context.Context, request validator.Int )) } -// OneOf checks that the Int64 held in the attribute +func (v oneOfValidator) ValidateParameterInt64(ctx context.Context, request function.Int64ParameterValidatorRequest, response *function.Int64ParameterValidatorResponse) { + if request.Value.IsNull() || request.Value.IsUnknown() { + return + } + + value := request.Value + + for _, otherValue := range v.values { + if value.Equal(otherValue) { + return + } + } + + response.Error = validatorfuncerr.InvalidParameterValueMatchFuncError( + request.ArgumentPosition, + v.Description(ctx), + value.String(), + ) +} + +// OneOf checks that the Int64 held in the attribute or function parameter // is one of the given `values`. -func OneOf(values ...int64) validator.Int64 { +func OneOf(values ...int64) oneOfValidator { frameworkValues := make([]types.Int64, 0, len(values)) for _, value := range values { diff --git a/int64validator/one_of_example_test.go b/int64validator/one_of_example_test.go index 06ba479..d67df85 100644 --- a/int64validator/one_of_example_test.go +++ b/int64validator/one_of_example_test.go @@ -6,6 +6,7 @@ package int64validator_test import ( "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/schema/validator" ) @@ -23,3 +24,17 @@ func ExampleOneOf() { }, } } + +func ExampleOneOf_function() { + _ = function.Definition{ + Parameters: []function.Parameter{ + function.Int64Parameter{ + Name: "example_param", + Validators: []function.Int64ParameterValidator{ + // Validate integer value must be 12, 24, or 48 + int64validator.OneOf([]int64{12, 24, 48}...), + }, + }, + }, + } +} diff --git a/int64validator/one_of_test.go b/int64validator/one_of_test.go index 63b58f1..37f23cc 100644 --- a/int64validator/one_of_test.go +++ b/int64validator/one_of_test.go @@ -5,8 +5,10 @@ package int64validator_test import ( "context" + "fmt" "testing" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" @@ -17,71 +19,82 @@ func TestOneOfValidator(t *testing.T) { t.Parallel() type testCase struct { - in types.Int64 - validator validator.Int64 - expErrors int + in types.Int64 + oneOfValues []int64 + expectError bool } testCases := map[string]testCase{ "simple-match": { in: types.Int64Value(123), - validator: int64validator.OneOf( + oneOfValues: []int64{ 123, 234, 8910, 1213, - ), - expErrors: 0, + }, }, "simple-mismatch": { in: types.Int64Value(123), - validator: int64validator.OneOf( + oneOfValues: []int64{ 234, 8910, 1213, - ), - expErrors: 1, + }, + expectError: true, }, "skip-validation-on-null": { in: types.Int64Null(), - validator: int64validator.OneOf( + oneOfValues: []int64{ 234, 8910, 1213, - ), - expErrors: 0, + }, }, "skip-validation-on-unknown": { in: types.Int64Unknown(), - validator: int64validator.OneOf( + oneOfValues: []int64{ 234, 8910, 1213, - ), - expErrors: 0, + }, }, } for name, test := range testCases { name, test := name, test - t.Run(name, func(t *testing.T) { + + t.Run(fmt.Sprintf("ValidateInt64 - %s", name), func(t *testing.T) { t.Parallel() req := validator.Int64Request{ ConfigValue: test.in, } res := validator.Int64Response{} - test.validator.ValidateInt64(context.TODO(), req, &res) + int64validator.OneOf(test.oneOfValues...).ValidateInt64(context.TODO(), req, &res) + + if !res.Diagnostics.HasError() && test.expectError { + t.Fatal("expected error, got no error") + } - if test.expErrors > 0 && !res.Diagnostics.HasError() { - t.Fatalf("expected %d error(s), got none", test.expErrors) + if res.Diagnostics.HasError() && !test.expectError { + t.Fatalf("got unexpected error: %s", res.Diagnostics) + } + }) + + t.Run(fmt.Sprintf("ValidateParameterInt64 - %s", name), func(t *testing.T) { + t.Parallel() + req := function.Int64ParameterValidatorRequest{ + Value: test.in, } + res := function.Int64ParameterValidatorResponse{} + int64validator.OneOf(test.oneOfValues...).ValidateParameterInt64(context.TODO(), req, &res) - if test.expErrors > 0 && test.expErrors != res.Diagnostics.ErrorsCount() { - t.Fatalf("expected %d error(s), got %d: %v", test.expErrors, res.Diagnostics.ErrorsCount(), res.Diagnostics) + if res.Error == nil && test.expectError { + t.Fatal("expected error, got no error") } - if test.expErrors == 0 && res.Diagnostics.HasError() { - t.Fatalf("expected no error(s), got %d: %v", res.Diagnostics.ErrorsCount(), res.Diagnostics) + if res.Error != nil && !test.expectError { + t.Fatalf("got unexpected error: %s", res.Error) } }) } diff --git a/listvalidator/doc.go b/listvalidator/doc.go index a13b376..ee0a5e8 100644 --- a/listvalidator/doc.go +++ b/listvalidator/doc.go @@ -1,5 +1,5 @@ // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 -// Package listvalidator provides validators for types.List attributes. +// Package listvalidator provides validators for types.List attributes and function parameters. package listvalidator diff --git a/listvalidator/size_at_least.go b/listvalidator/size_at_least.go index b346e18..54f86be 100644 --- a/listvalidator/size_at_least.go +++ b/listvalidator/size_at_least.go @@ -7,29 +7,28 @@ import ( "context" "fmt" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatorfuncerr" ) var _ validator.List = sizeAtLeastValidator{} +var _ function.ListParameterValidator = sizeAtLeastValidator{} -// sizeAtLeastValidator validates that list contains at least min elements. type sizeAtLeastValidator struct { min int } -// Description describes the validation in plain text formatting. func (v sizeAtLeastValidator) Description(_ context.Context) string { return fmt.Sprintf("list must contain at least %d elements", v.min) } -// MarkdownDescription describes the validation in Markdown formatting. func (v sizeAtLeastValidator) MarkdownDescription(ctx context.Context) string { return v.Description(ctx) } -// Validate performs the validation. func (v sizeAtLeastValidator) ValidateList(ctx context.Context, req validator.ListRequest, resp *validator.ListResponse) { if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() { return @@ -46,14 +45,30 @@ func (v sizeAtLeastValidator) ValidateList(ctx context.Context, req validator.Li } } +func (v sizeAtLeastValidator) ValidateParameterList(ctx context.Context, req function.ListParameterValidatorRequest, resp *function.ListParameterValidatorResponse) { + if req.Value.IsNull() || req.Value.IsUnknown() { + return + } + + elems := req.Value.Elements() + + if len(elems) < v.min { + resp.Error = validatorfuncerr.InvalidParameterValueFuncError( + req.ArgumentPosition, + v.Description(ctx), + fmt.Sprintf("%d", len(elems)), + ) + } +} + // SizeAtLeast returns an AttributeValidator which ensures that any configured -// attribute value: +// attribute or function parameter value: // // - Is a List. // - Contains at least min elements. // // Null (unconfigured) and unknown (known after apply) values are skipped. -func SizeAtLeast(minVal int) validator.List { +func SizeAtLeast(minVal int) sizeAtLeastValidator { return sizeAtLeastValidator{ min: minVal, } diff --git a/listvalidator/size_at_least_example_test.go b/listvalidator/size_at_least_example_test.go index 8aa7e54..5c04485 100644 --- a/listvalidator/size_at_least_example_test.go +++ b/listvalidator/size_at_least_example_test.go @@ -6,6 +6,7 @@ package listvalidator_test import ( "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" ) @@ -25,3 +26,17 @@ func ExampleSizeAtLeast() { }, } } + +func ExampleSizeAtLeast_function() { + _ = function.Definition{ + Parameters: []function.Parameter{ + function.ListParameter{ + Name: "example_param", + Validators: []function.ListParameterValidator{ + // Validate this list must contain at least 2 elements. + listvalidator.SizeAtLeast(2), + }, + }, + }, + } +} diff --git a/listvalidator/size_at_least_test.go b/listvalidator/size_at_least_test.go index 2ace7cc..ffb1760 100644 --- a/listvalidator/size_at_least_test.go +++ b/listvalidator/size_at_least_test.go @@ -5,9 +5,11 @@ package listvalidator import ( "context" + "fmt" "testing" "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" @@ -67,7 +69,8 @@ func TestSizeAtLeastValidator(t *testing.T) { for name, test := range tests { name, test := name, test - t.Run(name, func(t *testing.T) { + + t.Run(fmt.Sprintf("ValidateList - %s", name), func(t *testing.T) { t.Parallel() request := validator.ListRequest{ Path: path.Root("test"), @@ -85,5 +88,23 @@ func TestSizeAtLeastValidator(t *testing.T) { t.Fatalf("got unexpected error: %s", response.Diagnostics) } }) + + t.Run(fmt.Sprintf("ValidateParameterList - %s", name), func(t *testing.T) { + t.Parallel() + request := function.ListParameterValidatorRequest{ + ArgumentPosition: 0, + Value: test.val, + } + response := function.ListParameterValidatorResponse{} + SizeAtLeast(test.min).ValidateParameterList(context.TODO(), request, &response) + + if response.Error == nil && test.expectError { + t.Fatal("expected error, got no error") + } + + if response.Error != nil && !test.expectError { + t.Fatalf("got unexpected error: %s", response.Error) + } + }) } } diff --git a/listvalidator/size_at_most.go b/listvalidator/size_at_most.go index 4e7ea27..0ff5ed2 100644 --- a/listvalidator/size_at_most.go +++ b/listvalidator/size_at_most.go @@ -7,29 +7,28 @@ import ( "context" "fmt" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatorfuncerr" ) var _ validator.List = sizeAtMostValidator{} +var _ function.ListParameterValidator = sizeAtMostValidator{} -// sizeAtMostValidator validates that list contains at most max elements. type sizeAtMostValidator struct { max int } -// Description describes the validation in plain text formatting. func (v sizeAtMostValidator) Description(_ context.Context) string { return fmt.Sprintf("list must contain at most %d elements", v.max) } -// MarkdownDescription describes the validation in Markdown formatting. func (v sizeAtMostValidator) MarkdownDescription(ctx context.Context) string { return v.Description(ctx) } -// Validate performs the validation. func (v sizeAtMostValidator) ValidateList(ctx context.Context, req validator.ListRequest, resp *validator.ListResponse) { if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() { return @@ -46,14 +45,30 @@ func (v sizeAtMostValidator) ValidateList(ctx context.Context, req validator.Lis } } +func (v sizeAtMostValidator) ValidateParameterList(ctx context.Context, req function.ListParameterValidatorRequest, resp *function.ListParameterValidatorResponse) { + if req.Value.IsNull() || req.Value.IsUnknown() { + return + } + + elems := req.Value.Elements() + + if len(elems) > v.max { + resp.Error = validatorfuncerr.InvalidParameterValueFuncError( + req.ArgumentPosition, + v.Description(ctx), + fmt.Sprintf("%d", len(elems)), + ) + } +} + // SizeAtMost returns an AttributeValidator which ensures that any configured -// attribute value: +// attribute or function parameter value: // // - Is a List. // - Contains at most max elements. // // Null (unconfigured) and unknown (known after apply) values are skipped. -func SizeAtMost(maxVal int) validator.List { +func SizeAtMost(maxVal int) sizeAtMostValidator { return sizeAtMostValidator{ max: maxVal, } diff --git a/listvalidator/size_at_most_example_test.go b/listvalidator/size_at_most_example_test.go index d5d51a0..e2cefa6 100644 --- a/listvalidator/size_at_most_example_test.go +++ b/listvalidator/size_at_most_example_test.go @@ -6,6 +6,7 @@ package listvalidator_test import ( "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" ) @@ -25,3 +26,17 @@ func ExampleSizeAtMost() { }, } } + +func ExampleSizeAtMost_function() { + _ = function.Definition{ + Parameters: []function.Parameter{ + function.ListParameter{ + Name: "example_param", + Validators: []function.ListParameterValidator{ + // Validate this list must contain at most 2 elements. + listvalidator.SizeAtMost(2), + }, + }, + }, + } +} diff --git a/listvalidator/size_at_most_test.go b/listvalidator/size_at_most_test.go index 6d5bbcf..a8cad9a 100644 --- a/listvalidator/size_at_most_test.go +++ b/listvalidator/size_at_most_test.go @@ -5,9 +5,11 @@ package listvalidator import ( "context" + "fmt" "testing" "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" @@ -71,7 +73,8 @@ func TestSizeAtMostValidator(t *testing.T) { for name, test := range tests { name, test := name, test - t.Run(name, func(t *testing.T) { + + t.Run(fmt.Sprintf("ValidateList - %s", name), func(t *testing.T) { t.Parallel() request := validator.ListRequest{ Path: path.Root("test"), @@ -89,5 +92,23 @@ func TestSizeAtMostValidator(t *testing.T) { t.Fatalf("got unexpected error: %s", response.Diagnostics) } }) + + t.Run(fmt.Sprintf("ValidateParameterList - %s", name), func(t *testing.T) { + t.Parallel() + request := function.ListParameterValidatorRequest{ + ArgumentPosition: 0, + Value: test.val, + } + response := function.ListParameterValidatorResponse{} + SizeAtMost(test.max).ValidateParameterList(context.TODO(), request, &response) + + if response.Error == nil && test.expectError { + t.Fatal("expected error, got no error") + } + + if response.Error != nil && !test.expectError { + t.Fatalf("got unexpected error: %s", response.Error) + } + }) } } diff --git a/listvalidator/size_between.go b/listvalidator/size_between.go index 6bd4b89..cab9c9d 100644 --- a/listvalidator/size_between.go +++ b/listvalidator/size_between.go @@ -7,31 +7,29 @@ import ( "context" "fmt" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatorfuncerr" ) var _ validator.List = sizeBetweenValidator{} +var _ function.ListParameterValidator = sizeBetweenValidator{} -// sizeBetweenValidator validates that list contains at least min elements -// and at most max elements. type sizeBetweenValidator struct { min int max int } -// Description describes the validation in plain text formatting. func (v sizeBetweenValidator) Description(_ context.Context) string { return fmt.Sprintf("list must contain at least %d elements and at most %d elements", v.min, v.max) } -// MarkdownDescription describes the validation in Markdown formatting. func (v sizeBetweenValidator) MarkdownDescription(ctx context.Context) string { return v.Description(ctx) } -// Validate performs the validation. func (v sizeBetweenValidator) ValidateList(ctx context.Context, req validator.ListRequest, resp *validator.ListResponse) { if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() { return @@ -48,14 +46,30 @@ func (v sizeBetweenValidator) ValidateList(ctx context.Context, req validator.Li } } +func (v sizeBetweenValidator) ValidateParameterList(ctx context.Context, req function.ListParameterValidatorRequest, resp *function.ListParameterValidatorResponse) { + if req.Value.IsNull() || req.Value.IsUnknown() { + return + } + + elems := req.Value.Elements() + + if len(elems) < v.min || len(elems) > v.max { + resp.Error = validatorfuncerr.InvalidParameterValueFuncError( + req.ArgumentPosition, + v.Description(ctx), + fmt.Sprintf("%d", len(elems)), + ) + } +} + // SizeBetween returns an AttributeValidator which ensures that any configured -// attribute value: +// attribute or function parameter value: // // - Is a List. // - Contains at least min elements and at most max elements. // // Null (unconfigured) and unknown (known after apply) values are skipped. -func SizeBetween(minVal, maxVal int) validator.List { +func SizeBetween(minVal, maxVal int) sizeBetweenValidator { return sizeBetweenValidator{ min: minVal, max: maxVal, diff --git a/listvalidator/size_between_example_test.go b/listvalidator/size_between_example_test.go index a4bbf80..3c15464 100644 --- a/listvalidator/size_between_example_test.go +++ b/listvalidator/size_between_example_test.go @@ -6,6 +6,7 @@ package listvalidator_test import ( "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" ) @@ -25,3 +26,17 @@ func ExampleSizeBetween() { }, } } + +func ExampleSizeBetween_function() { + _ = function.Definition{ + Parameters: []function.Parameter{ + function.ListParameter{ + Name: "example_param", + Validators: []function.ListParameterValidator{ + // Validate this list must contain at least 2 and at most 4 elements. + listvalidator.SizeBetween(2, 4), + }, + }, + }, + } +} diff --git a/listvalidator/size_between_test.go b/listvalidator/size_between_test.go index d2253d2..1b35c9d 100644 --- a/listvalidator/size_between_test.go +++ b/listvalidator/size_between_test.go @@ -5,9 +5,11 @@ package listvalidator import ( "context" + "fmt" "testing" "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" @@ -110,7 +112,8 @@ func TestSizeBetweenValidator(t *testing.T) { for name, test := range tests { name, test := name, test - t.Run(name, func(t *testing.T) { + + t.Run(fmt.Sprintf("ValidateList - %s", name), func(t *testing.T) { t.Parallel() request := validator.ListRequest{ Path: path.Root("test"), @@ -128,5 +131,23 @@ func TestSizeBetweenValidator(t *testing.T) { t.Fatalf("got unexpected error: %s", response.Diagnostics) } }) + + t.Run(fmt.Sprintf("ValidateParameterList - %s", name), func(t *testing.T) { + t.Parallel() + request := function.ListParameterValidatorRequest{ + ArgumentPosition: 0, + Value: test.val, + } + response := function.ListParameterValidatorResponse{} + SizeBetween(test.min, test.max).ValidateParameterList(context.TODO(), request, &response) + + if response.Error == nil && test.expectError { + t.Fatal("expected error, got no error") + } + + if response.Error != nil && !test.expectError { + t.Fatalf("got unexpected error: %s", response.Error) + } + }) } } diff --git a/listvalidator/unique_values.go b/listvalidator/unique_values.go index 6cfc3b7..cb9932c 100644 --- a/listvalidator/unique_values.go +++ b/listvalidator/unique_values.go @@ -7,25 +7,23 @@ import ( "context" "fmt" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/schema/validator" ) var _ validator.List = uniqueValuesValidator{} +var _ function.ListParameterValidator = uniqueValuesValidator{} -// uniqueValuesValidator implements the validator. type uniqueValuesValidator struct{} -// Description returns the plaintext description of the validator. func (v uniqueValuesValidator) Description(_ context.Context) string { return "all values must be unique" } -// MarkdownDescription returns the Markdown description of the validator. func (v uniqueValuesValidator) MarkdownDescription(ctx context.Context) string { return v.Description(ctx) } -// ValidateList implements the validation logic. func (v uniqueValuesValidator) ValidateList(_ context.Context, req validator.ListRequest, resp *validator.ListResponse) { if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() { return @@ -59,10 +57,45 @@ func (v uniqueValuesValidator) ValidateList(_ context.Context, req validator.Lis } } +func (v uniqueValuesValidator) ValidateParameterList(ctx context.Context, req function.ListParameterValidatorRequest, resp *function.ListParameterValidatorResponse) { + if req.Value.IsNull() || req.Value.IsUnknown() { + return + } + + elements := req.Value.Elements() + + for indexOuter, elementOuter := range elements { + // Only evaluate known values for duplicates. + if elementOuter.IsUnknown() { + continue + } + + for indexInner := indexOuter + 1; indexInner < len(elements); indexInner++ { + elementInner := elements[indexInner] + + if elementInner.IsUnknown() { + continue + } + + if !elementInner.Equal(elementOuter) { + continue + } + + resp.Error = function.ConcatFuncErrors( + resp.Error, + function.NewArgumentFuncError( + req.ArgumentPosition, + fmt.Sprintf("Duplicate List Value: This attribute contains duplicate values of: %s", elementInner), + ), + ) + } + } +} + // UniqueValues returns a validator which ensures that any configured list // only contains unique values. This is similar to using a set attribute type // which inherently validates unique values, but with list ordering semantics. // Null (unconfigured) and unknown (known after apply) values are skipped. -func UniqueValues() validator.List { +func UniqueValues() uniqueValuesValidator { return uniqueValuesValidator{} } diff --git a/listvalidator/unique_values_example_test.go b/listvalidator/unique_values_example_test.go index 06f0966..8b00b4d 100644 --- a/listvalidator/unique_values_example_test.go +++ b/listvalidator/unique_values_example_test.go @@ -6,6 +6,7 @@ package listvalidator_test import ( "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" ) @@ -25,3 +26,17 @@ func ExampleUniqueValues() { }, } } + +func ExampleUniqueValues_function() { + _ = function.Definition{ + Parameters: []function.Parameter{ + function.ListParameter{ + Name: "example_param", + Validators: []function.ListParameterValidator{ + // Validate this list must contain only unique values. + listvalidator.UniqueValues(), + }, + }, + }, + } +} diff --git a/listvalidator/unique_values_test.go b/listvalidator/unique_values_test.go index 3127be1..51b49ad 100644 --- a/listvalidator/unique_values_test.go +++ b/listvalidator/unique_values_test.go @@ -5,12 +5,14 @@ package listvalidator_test import ( "context" + "fmt" "testing" "github.com/google/go-cmp/cmp" "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" @@ -22,6 +24,7 @@ func TestUniqueValues(t *testing.T) { testCases := map[string]struct { list types.List expectedDiagnostics diag.Diagnostics + expectedFuncError *function.FuncError }{ "null-list": { list: types.ListNull(types.StringType), @@ -50,6 +53,10 @@ func TestUniqueValues(t *testing.T) { "This attribute contains duplicate values of: ", ), }, + expectedFuncError: function.NewArgumentFuncError( + 0, + "Duplicate List Value: This attribute contains duplicate values of: ", + ), }, "null-values-valid": { list: types.ListValueMust( @@ -98,6 +105,38 @@ func TestUniqueValues(t *testing.T) { "This attribute contains duplicate values of: \"test\"", ), }, + expectedFuncError: function.NewArgumentFuncError( + 0, + "Duplicate List Value: This attribute contains duplicate values of: \"test\"", + ), + }, + "multiple-known-values-duplicate": { + list: types.ListValueMust( + types.StringType, + []attr.Value{ + types.StringValue("test-val-1"), + types.StringValue("test-val-1"), + types.StringValue("test-val-2"), + types.StringValue("test-val-2"), + }, + ), + expectedDiagnostics: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("test"), + "Duplicate List Value", + "This attribute contains duplicate values of: \"test-val-1\"", + ), + diag.NewAttributeErrorDiagnostic( + path.Root("test"), + "Duplicate List Value", + "This attribute contains duplicate values of: \"test-val-2\"", + ), + }, + expectedFuncError: function.NewArgumentFuncError( + 0, + "Duplicate List Value: This attribute contains duplicate values of: \"test-val-1\"\n"+ + "Duplicate List Value: This attribute contains duplicate values of: \"test-val-2\"", + ), }, "known-values-valid": { list: types.ListValueMust( @@ -111,7 +150,7 @@ func TestUniqueValues(t *testing.T) { for name, testCase := range testCases { name, testCase := name, testCase - t.Run(name, func(t *testing.T) { + t.Run(fmt.Sprintf("ValidateList - %s", name), func(t *testing.T) { t.Parallel() request := validator.ListRequest{ @@ -126,5 +165,20 @@ func TestUniqueValues(t *testing.T) { t.Errorf("unexpected diagnostics difference: %s", diff) } }) + + t.Run(fmt.Sprintf("ValidateParameterList - %s", name), func(t *testing.T) { + t.Parallel() + + request := function.ListParameterValidatorRequest{ + ArgumentPosition: 0, + Value: testCase.list, + } + response := function.ListParameterValidatorResponse{} + listvalidator.UniqueValues().ValidateParameterList(context.Background(), request, &response) + + if diff := cmp.Diff(response.Error, testCase.expectedFuncError); diff != "" { + t.Errorf("unexpected function error difference: %s", diff) + } + }) } } diff --git a/mapvalidator/doc.go b/mapvalidator/doc.go index 529d054..64f596a 100644 --- a/mapvalidator/doc.go +++ b/mapvalidator/doc.go @@ -1,5 +1,5 @@ // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 -// Package mapvalidator provides validators for types.Map attributes. +// Package mapvalidator provides validators for types.Map attributes and function parameters. package mapvalidator diff --git a/mapvalidator/size_at_least.go b/mapvalidator/size_at_least.go index 2ddc980..8c9ff5a 100644 --- a/mapvalidator/size_at_least.go +++ b/mapvalidator/size_at_least.go @@ -7,29 +7,28 @@ import ( "context" "fmt" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatorfuncerr" ) var _ validator.Map = sizeAtLeastValidator{} +var _ function.MapParameterValidator = sizeAtLeastValidator{} -// sizeAtLeastValidator validates that map contains at least min elements. type sizeAtLeastValidator struct { min int } -// Description describes the validation in plain text formatting. func (v sizeAtLeastValidator) Description(_ context.Context) string { return fmt.Sprintf("map must contain at least %d elements", v.min) } -// MarkdownDescription describes the validation in Markdown formatting. func (v sizeAtLeastValidator) MarkdownDescription(ctx context.Context) string { return v.Description(ctx) } -// Validate performs the validation. func (v sizeAtLeastValidator) ValidateMap(ctx context.Context, req validator.MapRequest, resp *validator.MapResponse) { if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() { return @@ -46,14 +45,30 @@ func (v sizeAtLeastValidator) ValidateMap(ctx context.Context, req validator.Map } } +func (v sizeAtLeastValidator) ValidateParameterMap(ctx context.Context, req function.MapParameterValidatorRequest, resp *function.MapParameterValidatorResponse) { + if req.Value.IsNull() || req.Value.IsUnknown() { + return + } + + elems := req.Value.Elements() + + if len(elems) < v.min { + resp.Error = validatorfuncerr.InvalidParameterValueFuncError( + req.ArgumentPosition, + v.Description(ctx), + fmt.Sprintf("%d", len(elems)), + ) + } +} + // SizeAtLeast returns an AttributeValidator which ensures that any configured -// attribute value: +// attribute or function parameter value: // // - Is a Map. // - Contains at least min elements. // // Null (unconfigured) and unknown (known after apply) values are skipped. -func SizeAtLeast(minVal int) validator.Map { +func SizeAtLeast(minVal int) sizeAtLeastValidator { return sizeAtLeastValidator{ min: minVal, } diff --git a/mapvalidator/size_at_least_example_test.go b/mapvalidator/size_at_least_example_test.go index 6730778..a3fbd71 100644 --- a/mapvalidator/size_at_least_example_test.go +++ b/mapvalidator/size_at_least_example_test.go @@ -6,6 +6,7 @@ package mapvalidator_test import ( "github.com/hashicorp/terraform-plugin-framework-validators/mapvalidator" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" ) @@ -25,3 +26,17 @@ func ExampleSizeAtLeast() { }, } } + +func ExampleSizeAtLeast_function() { + _ = function.Definition{ + Parameters: []function.Parameter{ + function.MapParameter{ + Name: "example_param", + Validators: []function.MapParameterValidator{ + // Validate this map must contain at least 2 elements. + mapvalidator.SizeAtLeast(2), + }, + }, + }, + } +} diff --git a/mapvalidator/size_at_least_test.go b/mapvalidator/size_at_least_test.go index e7a0345..969b3c8 100644 --- a/mapvalidator/size_at_least_test.go +++ b/mapvalidator/size_at_least_test.go @@ -5,9 +5,11 @@ package mapvalidator import ( "context" + "fmt" "testing" "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" @@ -67,7 +69,8 @@ func TestSizeAtLeastValidator(t *testing.T) { for name, test := range tests { name, test := name, test - t.Run(name, func(t *testing.T) { + + t.Run(fmt.Sprintf("ValidateMap - %s", name), func(t *testing.T) { t.Parallel() request := validator.MapRequest{ Path: path.Root("test"), @@ -85,5 +88,23 @@ func TestSizeAtLeastValidator(t *testing.T) { t.Fatalf("got unexpected error: %s", response.Diagnostics) } }) + + t.Run(fmt.Sprintf("ValidateParameterMap - %s", name), func(t *testing.T) { + t.Parallel() + request := function.MapParameterValidatorRequest{ + ArgumentPosition: 0, + Value: test.val, + } + response := function.MapParameterValidatorResponse{} + SizeAtLeast(test.min).ValidateParameterMap(context.TODO(), request, &response) + + if response.Error == nil && test.expectError { + t.Fatal("expected error, got no error") + } + + if response.Error != nil && !test.expectError { + t.Fatalf("got unexpected error: %s", response.Error) + } + }) } } diff --git a/mapvalidator/size_at_most.go b/mapvalidator/size_at_most.go index f41529e..82fe3ef 100644 --- a/mapvalidator/size_at_most.go +++ b/mapvalidator/size_at_most.go @@ -7,29 +7,28 @@ import ( "context" "fmt" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatorfuncerr" ) var _ validator.Map = sizeAtMostValidator{} +var _ function.MapParameterValidator = sizeAtMostValidator{} -// sizeAtMostValidator validates that map contains at most max elements. type sizeAtMostValidator struct { max int } -// Description describes the validation in plain text formatting. func (v sizeAtMostValidator) Description(_ context.Context) string { return fmt.Sprintf("map must contain at most %d elements", v.max) } -// MarkdownDescription describes the validation in Markdown formatting. func (v sizeAtMostValidator) MarkdownDescription(ctx context.Context) string { return v.Description(ctx) } -// Validate performs the validation. func (v sizeAtMostValidator) ValidateMap(ctx context.Context, req validator.MapRequest, resp *validator.MapResponse) { if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() { return @@ -46,14 +45,30 @@ func (v sizeAtMostValidator) ValidateMap(ctx context.Context, req validator.MapR } } +func (v sizeAtMostValidator) ValidateParameterMap(ctx context.Context, req function.MapParameterValidatorRequest, resp *function.MapParameterValidatorResponse) { + if req.Value.IsNull() || req.Value.IsUnknown() { + return + } + + elems := req.Value.Elements() + + if len(elems) > v.max { + resp.Error = validatorfuncerr.InvalidParameterValueFuncError( + req.ArgumentPosition, + v.Description(ctx), + fmt.Sprintf("%d", len(elems)), + ) + } +} + // SizeAtMost returns an AttributeValidator which ensures that any configured -// attribute value: +// attribute or function parameter value: // // - Is a Map. // - Contains at most max elements. // // Null (unconfigured) and unknown (known after apply) values are skipped. -func SizeAtMost(maxVal int) validator.Map { +func SizeAtMost(maxVal int) sizeAtMostValidator { return sizeAtMostValidator{ max: maxVal, } diff --git a/mapvalidator/size_at_most_example_test.go b/mapvalidator/size_at_most_example_test.go index 898beeb..21bdc59 100644 --- a/mapvalidator/size_at_most_example_test.go +++ b/mapvalidator/size_at_most_example_test.go @@ -6,6 +6,7 @@ package mapvalidator_test import ( "github.com/hashicorp/terraform-plugin-framework-validators/mapvalidator" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" ) @@ -25,3 +26,17 @@ func ExampleSizeAtMost() { }, } } + +func ExampleSizeAtMost_function() { + _ = function.Definition{ + Parameters: []function.Parameter{ + function.MapParameter{ + Name: "example_param", + Validators: []function.MapParameterValidator{ + // Validate this map must contain at most 2 elements. + mapvalidator.SizeAtMost(2), + }, + }, + }, + } +} diff --git a/mapvalidator/size_at_most_test.go b/mapvalidator/size_at_most_test.go index 7fe6cc1..5886bae 100644 --- a/mapvalidator/size_at_most_test.go +++ b/mapvalidator/size_at_most_test.go @@ -5,9 +5,11 @@ package mapvalidator import ( "context" + "fmt" "testing" "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" @@ -71,7 +73,8 @@ func TestSizeAtMostValidator(t *testing.T) { for name, test := range tests { name, test := name, test - t.Run(name, func(t *testing.T) { + + t.Run(fmt.Sprintf("ValidateMap - %s", name), func(t *testing.T) { t.Parallel() request := validator.MapRequest{ Path: path.Root("test"), @@ -89,5 +92,23 @@ func TestSizeAtMostValidator(t *testing.T) { t.Fatalf("got unexpected error: %s", response.Diagnostics) } }) + + t.Run(fmt.Sprintf("ValidateParameterMap - %s", name), func(t *testing.T) { + t.Parallel() + request := function.MapParameterValidatorRequest{ + ArgumentPosition: 0, + Value: test.val, + } + response := function.MapParameterValidatorResponse{} + SizeAtMost(test.max).ValidateParameterMap(context.TODO(), request, &response) + + if response.Error == nil && test.expectError { + t.Fatal("expected error, got no error") + } + + if response.Error != nil && !test.expectError { + t.Fatalf("got unexpected error: %s", response.Error) + } + }) } } diff --git a/mapvalidator/size_between.go b/mapvalidator/size_between.go index 9d6e761..78e12fb 100644 --- a/mapvalidator/size_between.go +++ b/mapvalidator/size_between.go @@ -7,31 +7,29 @@ import ( "context" "fmt" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatorfuncerr" ) var _ validator.Map = sizeBetweenValidator{} +var _ function.MapParameterValidator = sizeBetweenValidator{} -// sizeBetweenValidator validates that map contains at least min elements -// and at most max elements. type sizeBetweenValidator struct { min int max int } -// Description describes the validation in plain text formatting. func (v sizeBetweenValidator) Description(_ context.Context) string { return fmt.Sprintf("map must contain at least %d elements and at most %d elements", v.min, v.max) } -// MarkdownDescription describes the validation in Markdown formatting. func (v sizeBetweenValidator) MarkdownDescription(ctx context.Context) string { return v.Description(ctx) } -// Validate performs the validation. func (v sizeBetweenValidator) ValidateMap(ctx context.Context, req validator.MapRequest, resp *validator.MapResponse) { if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() { return @@ -48,14 +46,30 @@ func (v sizeBetweenValidator) ValidateMap(ctx context.Context, req validator.Map } } +func (v sizeBetweenValidator) ValidateParameterMap(ctx context.Context, req function.MapParameterValidatorRequest, resp *function.MapParameterValidatorResponse) { + if req.Value.IsNull() || req.Value.IsUnknown() { + return + } + + elems := req.Value.Elements() + + if len(elems) < v.min || len(elems) > v.max { + resp.Error = validatorfuncerr.InvalidParameterValueFuncError( + req.ArgumentPosition, + v.Description(ctx), + fmt.Sprintf("%d", len(elems)), + ) + } +} + // SizeBetween returns an AttributeValidator which ensures that any configured -// attribute value: +// attribute or function parameter value: // // - Is a Map. // - Contains at least min elements and at most max elements. // // Null (unconfigured) and unknown (known after apply) values are skipped. -func SizeBetween(minVal, maxVal int) validator.Map { +func SizeBetween(minVal, maxVal int) sizeBetweenValidator { return sizeBetweenValidator{ min: minVal, max: maxVal, diff --git a/mapvalidator/size_between_example_test.go b/mapvalidator/size_between_example_test.go index a748212..b2be083 100644 --- a/mapvalidator/size_between_example_test.go +++ b/mapvalidator/size_between_example_test.go @@ -6,6 +6,7 @@ package mapvalidator_test import ( "github.com/hashicorp/terraform-plugin-framework-validators/mapvalidator" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" ) @@ -25,3 +26,17 @@ func ExampleSizeBetween() { }, } } + +func ExampleSizeBetween_function() { + _ = function.Definition{ + Parameters: []function.Parameter{ + function.MapParameter{ + Name: "example_param", + Validators: []function.MapParameterValidator{ + // Validate this map must contain at least 2 and at most 4 elements. + mapvalidator.SizeBetween(2, 4), + }, + }, + }, + } +} diff --git a/mapvalidator/size_between_test.go b/mapvalidator/size_between_test.go index 0a3a6e2..d95634c 100644 --- a/mapvalidator/size_between_test.go +++ b/mapvalidator/size_between_test.go @@ -5,9 +5,11 @@ package mapvalidator import ( "context" + "fmt" "testing" "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" @@ -110,7 +112,8 @@ func TestSizeBetweenValidator(t *testing.T) { for name, test := range tests { name, test := name, test - t.Run(name, func(t *testing.T) { + + t.Run(fmt.Sprintf("ValidateMap - %s", name), func(t *testing.T) { t.Parallel() request := validator.MapRequest{ Path: path.Root("test"), @@ -128,5 +131,23 @@ func TestSizeBetweenValidator(t *testing.T) { t.Fatalf("got unexpected error: %s", response.Diagnostics) } }) + + t.Run(fmt.Sprintf("ValidateParameterMap - %s", name), func(t *testing.T) { + t.Parallel() + request := function.MapParameterValidatorRequest{ + ArgumentPosition: 0, + Value: test.val, + } + response := function.MapParameterValidatorResponse{} + SizeBetween(test.min, test.max).ValidateParameterMap(context.TODO(), request, &response) + + if response.Error == nil && test.expectError { + t.Fatal("expected error, got no error") + } + + if response.Error != nil && !test.expectError { + t.Fatalf("got unexpected error: %s", response.Error) + } + }) } } diff --git a/numbervalidator/doc.go b/numbervalidator/doc.go index 2d6f879..175ce24 100644 --- a/numbervalidator/doc.go +++ b/numbervalidator/doc.go @@ -1,5 +1,5 @@ // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 -// Package numbervalidator provides validators for types.Number attributes. +// Package numbervalidator provides validators for types.Number attributes or function parameters. package numbervalidator diff --git a/numbervalidator/none_of.go b/numbervalidator/none_of.go index bb96b32..43fb5f4 100644 --- a/numbervalidator/none_of.go +++ b/numbervalidator/none_of.go @@ -8,15 +8,17 @@ import ( "fmt" "math/big" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatorfuncerr" ) var _ validator.Number = noneOfValidator{} +var _ function.NumberParameterValidator = noneOfValidator{} -// noneOfValidator validates that the value does not match one of the values. type noneOfValidator struct { values []types.Number } @@ -51,9 +53,31 @@ func (v noneOfValidator) ValidateNumber(ctx context.Context, request validator.N } } -// NoneOf checks that the Number held in the attribute +func (v noneOfValidator) ValidateParameterNumber(ctx context.Context, request function.NumberParameterValidatorRequest, response *function.NumberParameterValidatorResponse) { + if request.Value.IsNull() || request.Value.IsUnknown() { + return + } + + value := request.Value + + for _, otherValue := range v.values { + if !value.Equal(otherValue) { + continue + } + + response.Error = validatorfuncerr.InvalidParameterValueMatchFuncError( + request.ArgumentPosition, + v.Description(ctx), + value.String(), + ) + + break + } +} + +// NoneOf checks that the Number held in the attribute or function parameter // is none of the given `values`. -func NoneOf(values ...*big.Float) validator.Number { +func NoneOf(values ...*big.Float) noneOfValidator { frameworkValues := make([]types.Number, 0, len(values)) for _, value := range values { diff --git a/numbervalidator/none_of_example_test.go b/numbervalidator/none_of_example_test.go index 8251ea7..9016bc6 100644 --- a/numbervalidator/none_of_example_test.go +++ b/numbervalidator/none_of_example_test.go @@ -8,6 +8,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework-validators/numbervalidator" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/schema/validator" ) @@ -31,3 +32,23 @@ func ExampleNoneOf() { }, } } + +func ExampleNoneOf_function() { + _ = function.Definition{ + Parameters: []function.Parameter{ + function.NumberParameter{ + Name: "example_param", + Validators: []function.NumberParameterValidator{ + // Validate number value must not be 1.2, 2.4, or 4.8 + numbervalidator.NoneOf( + []*big.Float{ + big.NewFloat(1.2), + big.NewFloat(2.4), + big.NewFloat(4.8), + }..., + ), + }, + }, + }, + } +} diff --git a/numbervalidator/none_of_test.go b/numbervalidator/none_of_test.go index 5c0fc3e..a381ffa 100644 --- a/numbervalidator/none_of_test.go +++ b/numbervalidator/none_of_test.go @@ -5,9 +5,11 @@ package numbervalidator_test import ( "context" + "fmt" "math/big" "testing" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" @@ -18,71 +20,82 @@ func TestNoneOfValidator(t *testing.T) { t.Parallel() type testCase struct { - in types.Number - validator validator.Number - expErrors int + in types.Number + noneOfValues []*big.Float + expectError bool } testCases := map[string]testCase{ "simple-match": { in: types.NumberValue(big.NewFloat(123.456)), - validator: numbervalidator.NoneOf( + noneOfValues: []*big.Float{ big.NewFloat(123.456), big.NewFloat(234.567), big.NewFloat(8910.11), big.NewFloat(1213.1415), - ), - expErrors: 1, + }, + expectError: true, }, "simple-mismatch": { in: types.NumberValue(big.NewFloat(123.456)), - validator: numbervalidator.NoneOf( + noneOfValues: []*big.Float{ big.NewFloat(234.567), big.NewFloat(8910.11), big.NewFloat(1213.1415), - ), - expErrors: 0, + }, }, "skip-validation-on-null": { in: types.NumberNull(), - validator: numbervalidator.NoneOf( + noneOfValues: []*big.Float{ big.NewFloat(234.567), big.NewFloat(8910.11), big.NewFloat(1213.1415), - ), - expErrors: 0, + }, }, "skip-validation-on-unknown": { in: types.NumberUnknown(), - validator: numbervalidator.NoneOf( + noneOfValues: []*big.Float{ big.NewFloat(234.567), big.NewFloat(8910.11), big.NewFloat(1213.1415), - ), - expErrors: 0, + }, }, } for name, test := range testCases { name, test := name, test - t.Run(name, func(t *testing.T) { + + t.Run(fmt.Sprintf("ValidateNumber - %s", name), func(t *testing.T) { t.Parallel() req := validator.NumberRequest{ ConfigValue: test.in, } res := validator.NumberResponse{} - test.validator.ValidateNumber(context.TODO(), req, &res) + numbervalidator.NoneOf(test.noneOfValues...).ValidateNumber(context.TODO(), req, &res) + + if !res.Diagnostics.HasError() && test.expectError { + t.Fatal("expected error, got no error") + } - if test.expErrors > 0 && !res.Diagnostics.HasError() { - t.Fatalf("expected %d error(s), got none", test.expErrors) + if res.Diagnostics.HasError() && !test.expectError { + t.Fatalf("got unexpected error: %s", res.Diagnostics) + } + }) + + t.Run(fmt.Sprintf("ValidateParameterNumber - %s", name), func(t *testing.T) { + t.Parallel() + req := function.NumberParameterValidatorRequest{ + Value: test.in, } + res := function.NumberParameterValidatorResponse{} + numbervalidator.NoneOf(test.noneOfValues...).ValidateParameterNumber(context.TODO(), req, &res) - if test.expErrors > 0 && test.expErrors != res.Diagnostics.ErrorsCount() { - t.Fatalf("expected %d error(s), got %d: %v", test.expErrors, res.Diagnostics.ErrorsCount(), res.Diagnostics) + if res.Error == nil && test.expectError { + t.Fatal("expected error, got no error") } - if test.expErrors == 0 && res.Diagnostics.HasError() { - t.Fatalf("expected no error(s), got %d: %v", res.Diagnostics.ErrorsCount(), res.Diagnostics) + if res.Error != nil && !test.expectError { + t.Fatalf("got unexpected error: %s", res.Error) } }) } diff --git a/numbervalidator/one_of.go b/numbervalidator/one_of.go index eee3869..9bbb3ac 100644 --- a/numbervalidator/one_of.go +++ b/numbervalidator/one_of.go @@ -8,15 +8,17 @@ import ( "fmt" "math/big" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatorfuncerr" ) var _ validator.Number = oneOfValidator{} +var _ function.NumberParameterValidator = oneOfValidator{} -// oneOfValidator validates that the value matches one of expected values. type oneOfValidator struct { values []types.Number } @@ -49,9 +51,29 @@ func (v oneOfValidator) ValidateNumber(ctx context.Context, request validator.Nu )) } -// OneOf checks that the Number held in the attribute +func (v oneOfValidator) ValidateParameterNumber(ctx context.Context, request function.NumberParameterValidatorRequest, response *function.NumberParameterValidatorResponse) { + if request.Value.IsNull() || request.Value.IsUnknown() { + return + } + + value := request.Value + + for _, otherValue := range v.values { + if value.Equal(otherValue) { + return + } + } + + response.Error = validatorfuncerr.InvalidParameterValueMatchFuncError( + request.ArgumentPosition, + v.Description(ctx), + value.String(), + ) +} + +// OneOf checks that the Number held in the attribute or function parameter // is one of the given `values`. -func OneOf(values ...*big.Float) validator.Number { +func OneOf(values ...*big.Float) oneOfValidator { frameworkValues := make([]types.Number, 0, len(values)) for _, value := range values { diff --git a/numbervalidator/one_of_example_test.go b/numbervalidator/one_of_example_test.go index a66c217..956bbeb 100644 --- a/numbervalidator/one_of_example_test.go +++ b/numbervalidator/one_of_example_test.go @@ -8,6 +8,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework-validators/numbervalidator" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/schema/validator" ) @@ -31,3 +32,23 @@ func ExampleOneOf() { }, } } + +func ExampleOneOf_function() { + _ = function.Definition{ + Parameters: []function.Parameter{ + function.NumberParameter{ + Name: "example_param", + Validators: []function.NumberParameterValidator{ + // Validate number value must be 1.2, 2.4, or 4.8 + numbervalidator.OneOf( + []*big.Float{ + big.NewFloat(1.2), + big.NewFloat(2.4), + big.NewFloat(4.8), + }..., + ), + }, + }, + }, + } +} diff --git a/numbervalidator/one_of_test.go b/numbervalidator/one_of_test.go index f55c2f1..a79aed1 100644 --- a/numbervalidator/one_of_test.go +++ b/numbervalidator/one_of_test.go @@ -5,9 +5,11 @@ package numbervalidator_test import ( "context" + "fmt" "math/big" "testing" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" @@ -18,71 +20,82 @@ func TestOneOfValidator(t *testing.T) { t.Parallel() type testCase struct { - in types.Number - validator validator.Number - expErrors int + in types.Number + oneOfValues []*big.Float + expectError bool } testCases := map[string]testCase{ "simple-match": { in: types.NumberValue(big.NewFloat(123.456)), - validator: numbervalidator.OneOf( + oneOfValues: []*big.Float{ big.NewFloat(123.456), big.NewFloat(234.567), big.NewFloat(8910.11), big.NewFloat(1213.1415), - ), - expErrors: 0, + }, }, "simple-mismatch": { in: types.NumberValue(big.NewFloat(123.456)), - validator: numbervalidator.OneOf( + oneOfValues: []*big.Float{ big.NewFloat(234.567), big.NewFloat(8910.11), big.NewFloat(1213.1415), - ), - expErrors: 1, + }, + expectError: true, }, "skip-validation-on-null": { in: types.NumberNull(), - validator: numbervalidator.OneOf( + oneOfValues: []*big.Float{ big.NewFloat(234.567), big.NewFloat(8910.11), big.NewFloat(1213.1415), - ), - expErrors: 0, + }, }, "skip-validation-on-unknown": { in: types.NumberUnknown(), - validator: numbervalidator.OneOf( + oneOfValues: []*big.Float{ big.NewFloat(234.567), big.NewFloat(8910.11), big.NewFloat(1213.1415), - ), - expErrors: 0, + }, }, } for name, test := range testCases { name, test := name, test - t.Run(name, func(t *testing.T) { + + t.Run(fmt.Sprintf("ValidateNumber - %s", name), func(t *testing.T) { t.Parallel() req := validator.NumberRequest{ ConfigValue: test.in, } res := validator.NumberResponse{} - test.validator.ValidateNumber(context.TODO(), req, &res) + numbervalidator.OneOf(test.oneOfValues...).ValidateNumber(context.TODO(), req, &res) + + if !res.Diagnostics.HasError() && test.expectError { + t.Fatal("expected error, got no error") + } - if test.expErrors > 0 && !res.Diagnostics.HasError() { - t.Fatalf("expected %d error(s), got none", test.expErrors) + if res.Diagnostics.HasError() && !test.expectError { + t.Fatalf("got unexpected error: %s", res.Diagnostics) + } + }) + + t.Run(fmt.Sprintf("ValidateParameterNumber - %s", name), func(t *testing.T) { + t.Parallel() + req := function.NumberParameterValidatorRequest{ + Value: test.in, } + res := function.NumberParameterValidatorResponse{} + numbervalidator.OneOf(test.oneOfValues...).ValidateParameterNumber(context.TODO(), req, &res) - if test.expErrors > 0 && test.expErrors != res.Diagnostics.ErrorsCount() { - t.Fatalf("expected %d error(s), got %d: %v", test.expErrors, res.Diagnostics.ErrorsCount(), res.Diagnostics) + if res.Error == nil && test.expectError { + t.Fatal("expected error, got no error") } - if test.expErrors == 0 && res.Diagnostics.HasError() { - t.Fatalf("expected no error(s), got %d: %v", res.Diagnostics.ErrorsCount(), res.Diagnostics) + if res.Error != nil && !test.expectError { + t.Fatalf("got unexpected error: %s", res.Error) } }) } diff --git a/setvalidator/doc.go b/setvalidator/doc.go index 258a0db..ed5b822 100644 --- a/setvalidator/doc.go +++ b/setvalidator/doc.go @@ -1,5 +1,5 @@ // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 -// Package setvalidator provides validators for types.Set attributes. +// Package setvalidator provides validators for types.Set attributes and function parameters. package setvalidator diff --git a/setvalidator/size_at_least.go b/setvalidator/size_at_least.go index c27dc14..7af58b0 100644 --- a/setvalidator/size_at_least.go +++ b/setvalidator/size_at_least.go @@ -7,29 +7,28 @@ import ( "context" "fmt" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatorfuncerr" ) var _ validator.Set = sizeAtLeastValidator{} +var _ function.SetParameterValidator = sizeAtLeastValidator{} -// sizeAtLeastValidator validates that set contains at least min elements. type sizeAtLeastValidator struct { min int } -// Description describes the validation in plain text formatting. func (v sizeAtLeastValidator) Description(_ context.Context) string { return fmt.Sprintf("set must contain at least %d elements", v.min) } -// MarkdownDescription describes the validation in Markdown formatting. func (v sizeAtLeastValidator) MarkdownDescription(ctx context.Context) string { return v.Description(ctx) } -// Validate performs the validation. func (v sizeAtLeastValidator) ValidateSet(ctx context.Context, req validator.SetRequest, resp *validator.SetResponse) { if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() { return @@ -46,14 +45,30 @@ func (v sizeAtLeastValidator) ValidateSet(ctx context.Context, req validator.Set } } +func (v sizeAtLeastValidator) ValidateParameterSet(ctx context.Context, req function.SetParameterValidatorRequest, resp *function.SetParameterValidatorResponse) { + if req.Value.IsNull() || req.Value.IsUnknown() { + return + } + + elems := req.Value.Elements() + + if len(elems) < v.min { + resp.Error = validatorfuncerr.InvalidParameterValueFuncError( + req.ArgumentPosition, + v.Description(ctx), + fmt.Sprintf("%d", len(elems)), + ) + } +} + // SizeAtLeast returns an AttributeValidator which ensures that any configured -// attribute value: +// attribute or function parameter value: // // - Is a Set. // - Contains at least min elements. // // Null (unconfigured) and unknown (known after apply) values are skipped. -func SizeAtLeast(minVal int) validator.Set { +func SizeAtLeast(minVal int) sizeAtLeastValidator { return sizeAtLeastValidator{ min: minVal, } diff --git a/setvalidator/size_at_least_example_test.go b/setvalidator/size_at_least_example_test.go index f31cdea..97061f7 100644 --- a/setvalidator/size_at_least_example_test.go +++ b/setvalidator/size_at_least_example_test.go @@ -6,6 +6,7 @@ package setvalidator_test import ( "github.com/hashicorp/terraform-plugin-framework-validators/setvalidator" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" ) @@ -25,3 +26,17 @@ func ExampleSizeAtLeast() { }, } } + +func ExampleSizeAtLeast_function() { + _ = function.Definition{ + Parameters: []function.Parameter{ + function.SetParameter{ + Name: "example_param", + Validators: []function.SetParameterValidator{ + // Validate this set must contain at least 2 elements. + setvalidator.SizeAtLeast(2), + }, + }, + }, + } +} diff --git a/setvalidator/size_at_least_test.go b/setvalidator/size_at_least_test.go index 3a59ba2..e76da98 100644 --- a/setvalidator/size_at_least_test.go +++ b/setvalidator/size_at_least_test.go @@ -5,9 +5,11 @@ package setvalidator import ( "context" + "fmt" "testing" "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" @@ -67,7 +69,8 @@ func TestSizeAtLeastValidator(t *testing.T) { for name, test := range tests { name, test := name, test - t.Run(name, func(t *testing.T) { + + t.Run(fmt.Sprintf("ValidateSet - %s", name), func(t *testing.T) { t.Parallel() request := validator.SetRequest{ Path: path.Root("test"), @@ -85,5 +88,23 @@ func TestSizeAtLeastValidator(t *testing.T) { t.Fatalf("got unexpected error: %s", response.Diagnostics) } }) + + t.Run(fmt.Sprintf("ValidateParameterSet - %s", name), func(t *testing.T) { + t.Parallel() + request := function.SetParameterValidatorRequest{ + ArgumentPosition: 0, + Value: test.val, + } + response := function.SetParameterValidatorResponse{} + SizeAtLeast(test.min).ValidateParameterSet(context.TODO(), request, &response) + + if response.Error == nil && test.expectError { + t.Fatal("expected error, got no error") + } + + if response.Error != nil && !test.expectError { + t.Fatalf("got unexpected error: %s", response.Error) + } + }) } } diff --git a/setvalidator/size_at_most.go b/setvalidator/size_at_most.go index 1f1be7d..e0762ac 100644 --- a/setvalidator/size_at_most.go +++ b/setvalidator/size_at_most.go @@ -7,29 +7,28 @@ import ( "context" "fmt" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatorfuncerr" ) var _ validator.Set = sizeAtMostValidator{} +var _ function.SetParameterValidator = sizeAtMostValidator{} -// sizeAtMostValidator validates that set contains at most max elements. type sizeAtMostValidator struct { max int } -// Description describes the validation in plain text formatting. func (v sizeAtMostValidator) Description(_ context.Context) string { return fmt.Sprintf("set must contain at most %d elements", v.max) } -// MarkdownDescription describes the validation in Markdown formatting. func (v sizeAtMostValidator) MarkdownDescription(ctx context.Context) string { return v.Description(ctx) } -// Validate performs the validation. func (v sizeAtMostValidator) ValidateSet(ctx context.Context, req validator.SetRequest, resp *validator.SetResponse) { if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() { return @@ -46,14 +45,30 @@ func (v sizeAtMostValidator) ValidateSet(ctx context.Context, req validator.SetR } } +func (v sizeAtMostValidator) ValidateParameterSet(ctx context.Context, req function.SetParameterValidatorRequest, resp *function.SetParameterValidatorResponse) { + if req.Value.IsNull() || req.Value.IsUnknown() { + return + } + + elems := req.Value.Elements() + + if len(elems) > v.max { + resp.Error = validatorfuncerr.InvalidParameterValueFuncError( + req.ArgumentPosition, + v.Description(ctx), + fmt.Sprintf("%d", len(elems)), + ) + } +} + // SizeAtMost returns an AttributeValidator which ensures that any configured -// attribute value: +// attribute or function parameter value: // // - Is a Set. // - Contains at most max elements. // // Null (unconfigured) and unknown (known after apply) values are skipped. -func SizeAtMost(maxVal int) validator.Set { +func SizeAtMost(maxVal int) sizeAtMostValidator { return sizeAtMostValidator{ max: maxVal, } diff --git a/setvalidator/size_at_most_example_test.go b/setvalidator/size_at_most_example_test.go index 2386ecf..2304096 100644 --- a/setvalidator/size_at_most_example_test.go +++ b/setvalidator/size_at_most_example_test.go @@ -6,6 +6,7 @@ package setvalidator_test import ( "github.com/hashicorp/terraform-plugin-framework-validators/setvalidator" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" ) @@ -25,3 +26,17 @@ func ExampleSizeAtMost() { }, } } + +func ExampleSizeAtMost_function() { + _ = function.Definition{ + Parameters: []function.Parameter{ + function.SetParameter{ + Name: "example_param", + Validators: []function.SetParameterValidator{ + // Validate this set must contain at most 2 elements. + setvalidator.SizeAtMost(2), + }, + }, + }, + } +} diff --git a/setvalidator/size_at_most_test.go b/setvalidator/size_at_most_test.go index d8849f6..0cebc4d 100644 --- a/setvalidator/size_at_most_test.go +++ b/setvalidator/size_at_most_test.go @@ -5,9 +5,11 @@ package setvalidator import ( "context" + "fmt" "testing" "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" @@ -71,7 +73,8 @@ func TestSizeAtMostValidator(t *testing.T) { for name, test := range tests { name, test := name, test - t.Run(name, func(t *testing.T) { + + t.Run(fmt.Sprintf("ValidateSet - %s", name), func(t *testing.T) { t.Parallel() request := validator.SetRequest{ Path: path.Root("test"), @@ -89,5 +92,23 @@ func TestSizeAtMostValidator(t *testing.T) { t.Fatalf("got unexpected error: %s", response.Diagnostics) } }) + + t.Run(fmt.Sprintf("ValidateParameterSet - %s", name), func(t *testing.T) { + t.Parallel() + request := function.SetParameterValidatorRequest{ + ArgumentPosition: 0, + Value: test.val, + } + response := function.SetParameterValidatorResponse{} + SizeAtMost(test.max).ValidateParameterSet(context.TODO(), request, &response) + + if response.Error == nil && test.expectError { + t.Fatal("expected error, got no error") + } + + if response.Error != nil && !test.expectError { + t.Fatalf("got unexpected error: %s", response.Error) + } + }) } } diff --git a/setvalidator/size_between.go b/setvalidator/size_between.go index 7e71789..9dd8704 100644 --- a/setvalidator/size_between.go +++ b/setvalidator/size_between.go @@ -7,31 +7,29 @@ import ( "context" "fmt" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatorfuncerr" ) var _ validator.Set = sizeBetweenValidator{} +var _ function.SetParameterValidator = sizeBetweenValidator{} -// sizeBetweenValidator validates that set contains at least min elements -// and at most max elements. type sizeBetweenValidator struct { min int max int } -// Description describes the validation in plain text formatting. func (v sizeBetweenValidator) Description(_ context.Context) string { return fmt.Sprintf("set must contain at least %d elements and at most %d elements", v.min, v.max) } -// MarkdownDescription describes the validation in Markdown formatting. func (v sizeBetweenValidator) MarkdownDescription(ctx context.Context) string { return v.Description(ctx) } -// ValidateSet performs the validation. func (v sizeBetweenValidator) ValidateSet(ctx context.Context, req validator.SetRequest, resp *validator.SetResponse) { if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() { return @@ -48,14 +46,30 @@ func (v sizeBetweenValidator) ValidateSet(ctx context.Context, req validator.Set } } +func (v sizeBetweenValidator) ValidateParameterSet(ctx context.Context, req function.SetParameterValidatorRequest, resp *function.SetParameterValidatorResponse) { + if req.Value.IsNull() || req.Value.IsUnknown() { + return + } + + elems := req.Value.Elements() + + if len(elems) < v.min || len(elems) > v.max { + resp.Error = validatorfuncerr.InvalidParameterValueFuncError( + req.ArgumentPosition, + v.Description(ctx), + fmt.Sprintf("%d", len(elems)), + ) + } +} + // SizeBetween returns an AttributeValidator which ensures that any configured -// attribute value: +// attribute or function parameter value: // // - Is a Set. // - Contains at least min elements and at most max elements. // // Null (unconfigured) and unknown (known after apply) values are skipped. -func SizeBetween(minVal, maxVal int) validator.Set { +func SizeBetween(minVal, maxVal int) sizeBetweenValidator { return sizeBetweenValidator{ min: minVal, max: maxVal, diff --git a/setvalidator/size_between_example_test.go b/setvalidator/size_between_example_test.go index 4a4bdec..303d82e 100644 --- a/setvalidator/size_between_example_test.go +++ b/setvalidator/size_between_example_test.go @@ -6,6 +6,7 @@ package setvalidator_test import ( "github.com/hashicorp/terraform-plugin-framework-validators/setvalidator" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" ) @@ -25,3 +26,17 @@ func ExampleSizeBetween() { }, } } + +func ExampleSizeBetween_function() { + _ = function.Definition{ + Parameters: []function.Parameter{ + function.SetParameter{ + Name: "example_param", + Validators: []function.SetParameterValidator{ + // Validate this set must contain at least 2 and at most 4 elements. + setvalidator.SizeBetween(2, 4), + }, + }, + }, + } +} diff --git a/setvalidator/size_between_test.go b/setvalidator/size_between_test.go index 5a92b5a..0b24e7e 100644 --- a/setvalidator/size_between_test.go +++ b/setvalidator/size_between_test.go @@ -5,9 +5,11 @@ package setvalidator import ( "context" + "fmt" "testing" "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" @@ -110,7 +112,8 @@ func TestSizeBetweenValidator(t *testing.T) { for name, test := range tests { name, test := name, test - t.Run(name, func(t *testing.T) { + + t.Run(fmt.Sprintf("ValidateSet - %s", name), func(t *testing.T) { t.Parallel() request := validator.SetRequest{ Path: path.Root("test"), @@ -128,5 +131,23 @@ func TestSizeBetweenValidator(t *testing.T) { t.Fatalf("got unexpected error: %s", response.Diagnostics) } }) + + t.Run(fmt.Sprintf("ValidateParameterSet - %s", name), func(t *testing.T) { + t.Parallel() + request := function.SetParameterValidatorRequest{ + ArgumentPosition: 0, + Value: test.val, + } + response := function.SetParameterValidatorResponse{} + SizeBetween(test.min, test.max).ValidateParameterSet(context.TODO(), request, &response) + + if response.Error == nil && test.expectError { + t.Fatal("expected error, got no error") + } + + if response.Error != nil && !test.expectError { + t.Fatalf("got unexpected error: %s", response.Error) + } + }) } } diff --git a/stringvalidator/doc.go b/stringvalidator/doc.go index 67e1b7c..ce0a9bf 100644 --- a/stringvalidator/doc.go +++ b/stringvalidator/doc.go @@ -1,7 +1,7 @@ // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 -// Package stringvalidator provides validators for types.String attributes. +// Package stringvalidator provides validators for types.String attributes and function parameters. // // There are also HashiCorp-supported custom string types available for specific // use cases, including but not limited to: diff --git a/stringvalidator/length_at_least.go b/stringvalidator/length_at_least.go index 0ebaffa..38ae80a 100644 --- a/stringvalidator/length_at_least.go +++ b/stringvalidator/length_at_least.go @@ -7,30 +7,46 @@ import ( "context" "fmt" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatorfuncerr" ) var _ validator.String = lengthAtLeastValidator{} +var _ function.StringParameterValidator = lengthAtLeastValidator{} -// stringLenAtLeastValidator validates that a string Attribute's length is at least a certain value. type lengthAtLeastValidator struct { minLength int } -// Description describes the validation in plain text formatting. +func (validator lengthAtLeastValidator) invalidUsageMessage() string { + return fmt.Sprintf("minLength cannot be less than zero - minLength: %d", validator.minLength) +} + func (validator lengthAtLeastValidator) Description(_ context.Context) string { return fmt.Sprintf("string length must be at least %d", validator.minLength) } -// MarkdownDescription describes the validation in Markdown formatting. func (validator lengthAtLeastValidator) MarkdownDescription(ctx context.Context) string { return validator.Description(ctx) } -// Validate performs the validation. func (v lengthAtLeastValidator) ValidateString(ctx context.Context, request validator.StringRequest, response *validator.StringResponse) { + // Return an error if the validator has been created in an invalid state + if v.minLength < 0 { + response.Diagnostics.Append( + validatordiag.InvalidValidatorUsageDiagnostic( + request.Path, + "LengthAtLeast", + v.invalidUsageMessage(), + ), + ) + + return + } + if request.ConfigValue.IsNull() || request.ConfigValue.IsUnknown() { return } @@ -48,17 +64,45 @@ func (v lengthAtLeastValidator) ValidateString(ctx context.Context, request vali } } +func (v lengthAtLeastValidator) ValidateParameterString(ctx context.Context, request function.StringParameterValidatorRequest, response *function.StringParameterValidatorResponse) { + // Return an error if the validator has been created in an invalid state + if v.minLength < 0 { + response.Error = validatorfuncerr.InvalidValidatorUsageFuncError( + request.ArgumentPosition, + "LengthAtLeast", + v.invalidUsageMessage(), + ) + + return + } + + if request.Value.IsNull() || request.Value.IsUnknown() { + return + } + + value := request.Value.ValueString() + + if l := len(value); l < v.minLength { + response.Error = validatorfuncerr.InvalidParameterValueLengthFuncError( + request.ArgumentPosition, + v.Description(ctx), + fmt.Sprintf("%d", l), + ) + + return + } +} + // LengthAtLeast returns an validator which ensures that any configured -// attribute value is of single-byte character length greater than or equal +// attribute or function parameter value is of single-byte character length greater than or equal // to the given minimum. Null (unconfigured) and unknown (known after apply) // values are skipped. // +// minLength cannot be less than zero. Invalid input for minLength will result in an +// implementation error message during validation. +// // Use UTF8LengthAtLeast for checking multiple-byte characters. -func LengthAtLeast(minLength int) validator.String { - if minLength < 0 { - return nil - } - +func LengthAtLeast(minLength int) lengthAtLeastValidator { return lengthAtLeastValidator{ minLength: minLength, } diff --git a/stringvalidator/length_at_least_example_test.go b/stringvalidator/length_at_least_example_test.go index 90a13b6..4c5811e 100644 --- a/stringvalidator/length_at_least_example_test.go +++ b/stringvalidator/length_at_least_example_test.go @@ -6,6 +6,7 @@ package stringvalidator_test import ( "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/schema/validator" ) @@ -23,3 +24,17 @@ func ExampleLengthAtLeast() { }, } } + +func ExampleLengthAtLeast_function() { + _ = function.Definition{ + Parameters: []function.Parameter{ + function.StringParameter{ + Name: "example_param", + Validators: []function.StringParameterValidator{ + // Validate string value length must be at least 3 characters. + stringvalidator.LengthAtLeast(3), + }, + }, + }, + } +} diff --git a/stringvalidator/length_at_least_test.go b/stringvalidator/length_at_least_test.go index 68422a9..5ec51cb 100644 --- a/stringvalidator/length_at_least_test.go +++ b/stringvalidator/length_at_least_test.go @@ -5,8 +5,10 @@ package stringvalidator_test import ( "context" + "fmt" "testing" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" @@ -45,11 +47,17 @@ func TestLengthAtLeastValidator(t *testing.T) { val: types.StringValue("⇄"), minLength: 2, }, + "invalid validator usage - minLength < 0": { + val: types.StringValue("ok"), + minLength: -1, + expectError: true, + }, } for name, test := range tests { name, test := name, test - t.Run(name, func(t *testing.T) { + + t.Run(fmt.Sprintf("ValidateString - %s", name), func(t *testing.T) { t.Parallel() request := validator.StringRequest{ Path: path.Root("test"), @@ -67,5 +75,23 @@ func TestLengthAtLeastValidator(t *testing.T) { t.Fatalf("got unexpected error: %s", response.Diagnostics) } }) + + t.Run(fmt.Sprintf("ValidateParameterString - %s", name), func(t *testing.T) { + t.Parallel() + request := function.StringParameterValidatorRequest{ + ArgumentPosition: 0, + Value: test.val, + } + response := function.StringParameterValidatorResponse{} + stringvalidator.LengthAtLeast(test.minLength).ValidateParameterString(context.TODO(), request, &response) + + if response.Error == nil && test.expectError { + t.Fatal("expected error, got no error") + } + + if response.Error != nil && !test.expectError { + t.Fatalf("got unexpected error: %s", response.Error) + } + }) } } diff --git a/stringvalidator/length_at_most.go b/stringvalidator/length_at_most.go index a793a0b..2c32696 100644 --- a/stringvalidator/length_at_most.go +++ b/stringvalidator/length_at_most.go @@ -8,28 +8,44 @@ import ( "fmt" "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatorfuncerr" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/schema/validator" ) var _ validator.String = lengthAtMostValidator{} +var _ function.StringParameterValidator = lengthAtMostValidator{} -// lengthAtMostValidator validates that a string Attribute's length is at most a certain value. type lengthAtMostValidator struct { maxLength int } -// Description describes the validation in plain text formatting. +func (validator lengthAtMostValidator) invalidUsageMessage() string { + return fmt.Sprintf("maxLength cannot be less than zero - maxLength: %d", validator.maxLength) +} + func (validator lengthAtMostValidator) Description(_ context.Context) string { return fmt.Sprintf("string length must be at most %d", validator.maxLength) } -// MarkdownDescription describes the validation in Markdown formatting. func (validator lengthAtMostValidator) MarkdownDescription(ctx context.Context) string { return validator.Description(ctx) } -// Validate performs the validation. func (v lengthAtMostValidator) ValidateString(ctx context.Context, request validator.StringRequest, response *validator.StringResponse) { + // Return an error if the validator has been created in an invalid state + if v.maxLength < 0 { + response.Diagnostics.Append( + validatordiag.InvalidValidatorUsageDiagnostic( + request.Path, + "LengthAtMost", + v.invalidUsageMessage(), + ), + ) + + return + } + if request.ConfigValue.IsNull() || request.ConfigValue.IsUnknown() { return } @@ -47,17 +63,45 @@ func (v lengthAtMostValidator) ValidateString(ctx context.Context, request valid } } +func (v lengthAtMostValidator) ValidateParameterString(ctx context.Context, request function.StringParameterValidatorRequest, response *function.StringParameterValidatorResponse) { + // Return an error if the validator has been created in an invalid state + if v.maxLength < 0 { + response.Error = validatorfuncerr.InvalidValidatorUsageFuncError( + request.ArgumentPosition, + "LengthAtMost", + v.invalidUsageMessage(), + ) + + return + } + + if request.Value.IsNull() || request.Value.IsUnknown() { + return + } + + value := request.Value.ValueString() + + if l := len(value); l > v.maxLength { + response.Error = validatorfuncerr.InvalidParameterValueLengthFuncError( + request.ArgumentPosition, + v.Description(ctx), + fmt.Sprintf("%d", l), + ) + + return + } +} + // LengthAtMost returns an validator which ensures that any configured -// attribute value is of single-byte character length less than or equal +// attribute or function parameter value is of single-byte character length less than or equal // to the given maximum. Null (unconfigured) and unknown (known after apply) // values are skipped. // +// maxLength cannot be less than zero. Invalid input for maxLength will result in an +// implementation error message during validation. +// // Use UTF8LengthAtMost for checking multiple-byte characters. -func LengthAtMost(maxLength int) validator.String { - if maxLength < 0 { - return nil - } - +func LengthAtMost(maxLength int) lengthAtMostValidator { return lengthAtMostValidator{ maxLength: maxLength, } diff --git a/stringvalidator/length_at_most_example_test.go b/stringvalidator/length_at_most_example_test.go index 6435d25..3789344 100644 --- a/stringvalidator/length_at_most_example_test.go +++ b/stringvalidator/length_at_most_example_test.go @@ -6,6 +6,7 @@ package stringvalidator_test import ( "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/schema/validator" ) @@ -23,3 +24,17 @@ func ExampleLengthAtMost() { }, } } + +func ExampleLengthAtMost_function() { + _ = function.Definition{ + Parameters: []function.Parameter{ + function.StringParameter{ + Name: "example_param", + Validators: []function.StringParameterValidator{ + // Validate string value length must be at most 256 characters. + stringvalidator.LengthAtMost(256), + }, + }, + }, + } +} diff --git a/stringvalidator/length_at_most_test.go b/stringvalidator/length_at_most_test.go index 7f48bd5..6eeca55 100644 --- a/stringvalidator/length_at_most_test.go +++ b/stringvalidator/length_at_most_test.go @@ -5,8 +5,10 @@ package stringvalidator_test import ( "context" + "fmt" "testing" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" @@ -46,11 +48,17 @@ func TestLengthAtMostValidator(t *testing.T) { maxLength: 2, expectError: true, }, + "invalid validator usage - maxLength < 0": { + val: types.StringValue("ok"), + maxLength: -1, + expectError: true, + }, } for name, test := range tests { name, test := name, test - t.Run(name, func(t *testing.T) { + + t.Run(fmt.Sprintf("ValidateString - %s", name), func(t *testing.T) { t.Parallel() request := validator.StringRequest{ Path: path.Root("test"), @@ -68,5 +76,23 @@ func TestLengthAtMostValidator(t *testing.T) { t.Fatalf("got unexpected error: %s", response.Diagnostics) } }) + + t.Run(fmt.Sprintf("ValidateParameterString - %s", name), func(t *testing.T) { + t.Parallel() + request := function.StringParameterValidatorRequest{ + ArgumentPosition: 0, + Value: test.val, + } + response := function.StringParameterValidatorResponse{} + stringvalidator.LengthAtMost(test.maxLength).ValidateParameterString(context.TODO(), request, &response) + + if response.Error == nil && test.expectError { + t.Fatal("expected error, got no error") + } + + if response.Error != nil && !test.expectError { + t.Fatalf("got unexpected error: %s", response.Error) + } + }) } } diff --git a/stringvalidator/length_between.go b/stringvalidator/length_between.go index c70f4c0..f02c5fd 100644 --- a/stringvalidator/length_between.go +++ b/stringvalidator/length_between.go @@ -8,28 +8,44 @@ import ( "fmt" "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatorfuncerr" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/schema/validator" ) var _ validator.String = lengthBetweenValidator{} +var _ function.StringParameterValidator = lengthBetweenValidator{} -// stringLenBetweenValidator validates that a string Attribute's length is in a range. type lengthBetweenValidator struct { minLength, maxLength int } -// Description describes the validation in plain text formatting. +func (validator lengthBetweenValidator) invalidUsageMessage() string { + return fmt.Sprintf("minLength cannot be less than zero or greater than maxLength - minLength: %d, maxLength: %d", validator.minLength, validator.maxLength) +} + func (validator lengthBetweenValidator) Description(_ context.Context) string { return fmt.Sprintf("string length must be between %d and %d", validator.minLength, validator.maxLength) } -// MarkdownDescription describes the validation in Markdown formatting. func (validator lengthBetweenValidator) MarkdownDescription(ctx context.Context) string { return validator.Description(ctx) } -// Validate performs the validation. func (v lengthBetweenValidator) ValidateString(ctx context.Context, request validator.StringRequest, response *validator.StringResponse) { + // Return an error if the validator has been created in an invalid state + if v.minLength < 0 || v.minLength > v.maxLength { + response.Diagnostics.Append( + validatordiag.InvalidValidatorUsageDiagnostic( + request.Path, + "LengthBetween", + v.invalidUsageMessage(), + ), + ) + + return + } + if request.ConfigValue.IsNull() || request.ConfigValue.IsUnknown() { return } @@ -47,17 +63,45 @@ func (v lengthBetweenValidator) ValidateString(ctx context.Context, request vali } } +func (v lengthBetweenValidator) ValidateParameterString(ctx context.Context, request function.StringParameterValidatorRequest, response *function.StringParameterValidatorResponse) { + // Return an error if the validator has been created in an invalid state + if v.minLength < 0 || v.minLength > v.maxLength { + response.Error = validatorfuncerr.InvalidValidatorUsageFuncError( + request.ArgumentPosition, + "LengthBetween", + v.invalidUsageMessage(), + ) + + return + } + + if request.Value.IsNull() || request.Value.IsUnknown() { + return + } + + value := request.Value.ValueString() + + if l := len(value); l < v.minLength || l > v.maxLength { + response.Error = validatorfuncerr.InvalidParameterValueLengthFuncError( + request.ArgumentPosition, + v.Description(ctx), + fmt.Sprintf("%d", l), + ) + + return + } +} + // LengthBetween returns a validator which ensures that any configured -// attribute value is of single-byte character length greater than or equal +// attribute or function parameter value is of single-byte character length greater than or equal // to the given minimum and less than or equal to the given maximum. Null // (unconfigured) and unknown (known after apply) values are skipped. // +// minLength cannot be less than zero or greater than maxLength. Invalid combinations of +// minLength and maxLength will result in an implementation error message during validation. +// // Use UTF8LengthBetween for checking multiple-byte characters. -func LengthBetween(minLength, maxLength int) validator.String { - if minLength < 0 || minLength > maxLength { - return nil - } - +func LengthBetween(minLength, maxLength int) lengthBetweenValidator { return lengthBetweenValidator{ minLength: minLength, maxLength: maxLength, diff --git a/stringvalidator/length_between_example_test.go b/stringvalidator/length_between_example_test.go index aa17b39..f38cdf3 100644 --- a/stringvalidator/length_between_example_test.go +++ b/stringvalidator/length_between_example_test.go @@ -6,6 +6,7 @@ package stringvalidator_test import ( "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/schema/validator" ) @@ -23,3 +24,17 @@ func ExampleLengthBetween() { }, } } + +func ExampleLengthBetween_function() { + _ = function.Definition{ + Parameters: []function.Parameter{ + function.StringParameter{ + Name: "example_param", + Validators: []function.StringParameterValidator{ + // Validate string value length must be at least 3 and at most 256 characters. + stringvalidator.LengthBetween(3, 256), + }, + }, + }, + } +} diff --git a/stringvalidator/length_between_test.go b/stringvalidator/length_between_test.go index eb4f2d3..cf3db9b 100644 --- a/stringvalidator/length_between_test.go +++ b/stringvalidator/length_between_test.go @@ -5,8 +5,10 @@ package stringvalidator_test import ( "context" + "fmt" "testing" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" @@ -77,11 +79,24 @@ func TestLengthBetweenValidator(t *testing.T) { minLength: 2, maxLength: 4, }, + "invalid validator usage - minLength < 0": { + val: types.StringValue("ok"), + minLength: -1, + maxLength: 3, + expectError: true, + }, + "invalid validator usage - minLength > maxLength": { + val: types.StringValue("ok"), + minLength: 2, + maxLength: 1, + expectError: true, + }, } for name, test := range tests { name, test := name, test - t.Run(name, func(t *testing.T) { + + t.Run(fmt.Sprintf("ValidateString - %s", name), func(t *testing.T) { t.Parallel() request := validator.StringRequest{ Path: path.Root("test"), @@ -99,5 +114,23 @@ func TestLengthBetweenValidator(t *testing.T) { t.Fatalf("got unexpected error: %s", response.Diagnostics) } }) + + t.Run(fmt.Sprintf("ValidateParameterString - %s", name), func(t *testing.T) { + t.Parallel() + request := function.StringParameterValidatorRequest{ + ArgumentPosition: 0, + Value: test.val, + } + response := function.StringParameterValidatorResponse{} + stringvalidator.LengthBetween(test.minLength, test.maxLength).ValidateParameterString(context.TODO(), request, &response) + + if response.Error == nil && test.expectError { + t.Fatal("expected error, got no error") + } + + if response.Error != nil && !test.expectError { + t.Fatalf("got unexpected error: %s", response.Error) + } + }) } } diff --git a/stringvalidator/none_of.go b/stringvalidator/none_of.go index 6bf7dce..9639763 100644 --- a/stringvalidator/none_of.go +++ b/stringvalidator/none_of.go @@ -7,15 +7,17 @@ import ( "context" "fmt" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatorfuncerr" ) var _ validator.String = noneOfValidator{} +var _ function.StringParameterValidator = noneOfValidator{} -// noneOfValidator validates that the value does not match one of the values. type noneOfValidator struct { values []types.String } @@ -50,9 +52,31 @@ func (v noneOfValidator) ValidateString(ctx context.Context, request validator.S } } -// NoneOf checks that the String held in the attribute +func (v noneOfValidator) ValidateParameterString(ctx context.Context, request function.StringParameterValidatorRequest, response *function.StringParameterValidatorResponse) { + if request.Value.IsNull() || request.Value.IsUnknown() { + return + } + + value := request.Value + + for _, otherValue := range v.values { + if !value.Equal(otherValue) { + continue + } + + response.Error = validatorfuncerr.InvalidParameterValueMatchFuncError( + request.ArgumentPosition, + v.Description(ctx), + value.String(), + ) + + break + } +} + +// NoneOf checks that the String held in the attribute or function parameter // is none of the given `values`. -func NoneOf(values ...string) validator.String { +func NoneOf(values ...string) noneOfValidator { frameworkValues := make([]types.String, 0, len(values)) for _, value := range values { diff --git a/stringvalidator/none_of_case_insensitive.go b/stringvalidator/none_of_case_insensitive.go index aedb094..f6a541d 100644 --- a/stringvalidator/none_of_case_insensitive.go +++ b/stringvalidator/none_of_case_insensitive.go @@ -8,15 +8,17 @@ import ( "fmt" "strings" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatorfuncerr" ) var _ validator.String = noneOfCaseInsensitiveValidator{} +var _ function.StringParameterValidator = noneOfCaseInsensitiveValidator{} -// noneOfCaseInsensitiveValidator validates that the value matches one of expected values. type noneOfCaseInsensitiveValidator struct { values []types.String } @@ -49,9 +51,29 @@ func (v noneOfCaseInsensitiveValidator) ValidateString(ctx context.Context, requ } } -// NoneOfCaseInsensitive checks that the String held in the attribute +func (v noneOfCaseInsensitiveValidator) ValidateParameterString(ctx context.Context, request function.StringParameterValidatorRequest, response *function.StringParameterValidatorResponse) { + if request.Value.IsNull() || request.Value.IsUnknown() { + return + } + + value := request.Value + + for _, otherValue := range v.values { + if strings.EqualFold(value.ValueString(), otherValue.ValueString()) { + response.Error = validatorfuncerr.InvalidParameterValueMatchFuncError( + request.ArgumentPosition, + v.Description(ctx), + value.String(), + ) + + return + } + } +} + +// NoneOfCaseInsensitive checks that the String held in the attribute or function parameter // is none of the given `values`. -func NoneOfCaseInsensitive(values ...string) validator.String { +func NoneOfCaseInsensitive(values ...string) noneOfCaseInsensitiveValidator { frameworkValues := make([]types.String, 0, len(values)) for _, value := range values { diff --git a/stringvalidator/none_of_case_insensitive_test.go b/stringvalidator/none_of_case_insensitive_test.go index ca97f50..6f921f6 100644 --- a/stringvalidator/none_of_case_insensitive_test.go +++ b/stringvalidator/none_of_case_insensitive_test.go @@ -5,9 +5,11 @@ package stringvalidator_test import ( "context" + "fmt" "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" @@ -18,79 +20,90 @@ func TestNoneOfCaseInsensitiveValidator(t *testing.T) { t.Parallel() type testCase struct { - in types.String - validator validator.String - expErrors int + in types.String + noneOfValues []string + expectError bool } testCases := map[string]testCase{ "simple-match": { in: types.StringValue("foo"), - validator: stringvalidator.NoneOfCaseInsensitive( + noneOfValues: []string{ "foo", "bar", "baz", - ), - expErrors: 1, + }, + expectError: true, }, "simple-match-case-insensitive": { in: types.StringValue("foo"), - validator: stringvalidator.NoneOfCaseInsensitive( + noneOfValues: []string{ "FOO", "bar", "baz", - ), - expErrors: 1, + }, + expectError: true, }, "simple-mismatch": { in: types.StringValue("foz"), - validator: stringvalidator.NoneOfCaseInsensitive( + noneOfValues: []string{ "foo", "bar", "baz", - ), - expErrors: 0, + }, }, "skip-validation-on-null": { in: types.StringNull(), - validator: stringvalidator.NoneOfCaseInsensitive( + noneOfValues: []string{ "foo", "bar", "baz", - ), - expErrors: 0, + }, }, "skip-validation-on-unknown": { in: types.StringUnknown(), - validator: stringvalidator.NoneOfCaseInsensitive( + noneOfValues: []string{ "foo", "bar", "baz", - ), - expErrors: 0, + }, }, } for name, test := range testCases { name, test := name, test - t.Run(name, func(t *testing.T) { + + t.Run(fmt.Sprintf("ValidateString - %s", name), func(t *testing.T) { t.Parallel() req := validator.StringRequest{ ConfigValue: test.in, } res := validator.StringResponse{} - test.validator.ValidateString(context.TODO(), req, &res) + stringvalidator.NoneOfCaseInsensitive(test.noneOfValues...).ValidateString(context.TODO(), req, &res) + + if !res.Diagnostics.HasError() && test.expectError { + t.Fatal("expected error, got no error") + } - if test.expErrors > 0 && !res.Diagnostics.HasError() { - t.Fatalf("expected %d error(s), got none", test.expErrors) + if res.Diagnostics.HasError() && !test.expectError { + t.Fatalf("got unexpected error: %s", res.Diagnostics) + } + }) + + t.Run(fmt.Sprintf("ValidateParameterString - %s", name), func(t *testing.T) { + t.Parallel() + req := function.StringParameterValidatorRequest{ + Value: test.in, } + res := function.StringParameterValidatorResponse{} + stringvalidator.NoneOfCaseInsensitive(test.noneOfValues...).ValidateParameterString(context.TODO(), req, &res) - if test.expErrors > 0 && test.expErrors != res.Diagnostics.ErrorsCount() { - t.Fatalf("expected %d error(s), got %d: %v", test.expErrors, res.Diagnostics.ErrorsCount(), res.Diagnostics) + if res.Error == nil && test.expectError { + t.Fatal("expected error, got no error") } - if test.expErrors == 0 && res.Diagnostics.HasError() { - t.Fatalf("expected no error(s), got %d: %v", res.Diagnostics.ErrorsCount(), res.Diagnostics) + if res.Error != nil && !test.expectError { + t.Fatalf("got unexpected error: %s", res.Error) } }) } diff --git a/stringvalidator/none_of_example_test.go b/stringvalidator/none_of_example_test.go index c952461..6e92913 100644 --- a/stringvalidator/none_of_example_test.go +++ b/stringvalidator/none_of_example_test.go @@ -6,6 +6,7 @@ package stringvalidator_test import ( "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/schema/validator" ) @@ -23,3 +24,17 @@ func ExampleNoneOf() { }, } } + +func ExampleNoneOf_function() { + _ = function.Definition{ + Parameters: []function.Parameter{ + function.StringParameter{ + Name: "example_param", + Validators: []function.StringParameterValidator{ + // Validate string value must not be "one", "two", or "three" + stringvalidator.NoneOf([]string{"one", "two", "three"}...), + }, + }, + }, + } +} diff --git a/stringvalidator/none_of_test.go b/stringvalidator/none_of_test.go index c0c77a0..72ce028 100644 --- a/stringvalidator/none_of_test.go +++ b/stringvalidator/none_of_test.go @@ -5,9 +5,11 @@ package stringvalidator_test import ( "context" + "fmt" "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" @@ -18,79 +20,89 @@ func TestNoneOfValidator(t *testing.T) { t.Parallel() type testCase struct { - in types.String - validator validator.String - expErrors int + in types.String + noneOfValues []string + expectError bool } testCases := map[string]testCase{ "simple-match": { in: types.StringValue("foo"), - validator: stringvalidator.NoneOf( + noneOfValues: []string{ "foo", "bar", "baz", - ), - expErrors: 1, + }, + expectError: true, }, "simple-mismatch-case-insensitive": { in: types.StringValue("foo"), - validator: stringvalidator.NoneOf( + noneOfValues: []string{ "FOO", "bar", "baz", - ), - expErrors: 0, + }, }, "simple-mismatch": { in: types.StringValue("foz"), - validator: stringvalidator.NoneOf( + noneOfValues: []string{ "foo", "bar", "baz", - ), - expErrors: 0, + }, }, "skip-validation-on-null": { in: types.StringNull(), - validator: stringvalidator.NoneOf( + noneOfValues: []string{ "foo", "bar", "baz", - ), - expErrors: 0, + }, }, "skip-validation-on-unknown": { in: types.StringUnknown(), - validator: stringvalidator.NoneOf( + noneOfValues: []string{ "foo", "bar", "baz", - ), - expErrors: 0, + }, }, } for name, test := range testCases { name, test := name, test - t.Run(name, func(t *testing.T) { + + t.Run(fmt.Sprintf("ValidateString - %s", name), func(t *testing.T) { t.Parallel() req := validator.StringRequest{ ConfigValue: test.in, } res := validator.StringResponse{} - test.validator.ValidateString(context.TODO(), req, &res) + stringvalidator.NoneOf(test.noneOfValues...).ValidateString(context.TODO(), req, &res) + + if !res.Diagnostics.HasError() && test.expectError { + t.Fatal("expected error, got no error") + } - if test.expErrors > 0 && !res.Diagnostics.HasError() { - t.Fatalf("expected %d error(s), got none", test.expErrors) + if res.Diagnostics.HasError() && !test.expectError { + t.Fatalf("got unexpected error: %s", res.Diagnostics) + } + }) + + t.Run(fmt.Sprintf("ValidateParameterString - %s", name), func(t *testing.T) { + t.Parallel() + req := function.StringParameterValidatorRequest{ + Value: test.in, } + res := function.StringParameterValidatorResponse{} + stringvalidator.NoneOf(test.noneOfValues...).ValidateParameterString(context.TODO(), req, &res) - if test.expErrors > 0 && test.expErrors != res.Diagnostics.ErrorsCount() { - t.Fatalf("expected %d error(s), got %d: %v", test.expErrors, res.Diagnostics.ErrorsCount(), res.Diagnostics) + if res.Error == nil && test.expectError { + t.Fatal("expected error, got no error") } - if test.expErrors == 0 && res.Diagnostics.HasError() { - t.Fatalf("expected no error(s), got %d: %v", res.Diagnostics.ErrorsCount(), res.Diagnostics) + if res.Error != nil && !test.expectError { + t.Fatalf("got unexpected error: %s", res.Error) } }) } diff --git a/stringvalidator/one_of.go b/stringvalidator/one_of.go index c3ae055..5790235 100644 --- a/stringvalidator/one_of.go +++ b/stringvalidator/one_of.go @@ -7,15 +7,17 @@ import ( "context" "fmt" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatorfuncerr" ) var _ validator.String = oneOfValidator{} +var _ function.StringParameterValidator = oneOfValidator{} -// oneOfValidator validates that the value matches one of expected values. type oneOfValidator struct { values []types.String } @@ -48,9 +50,29 @@ func (v oneOfValidator) ValidateString(ctx context.Context, request validator.St )) } -// OneOf checks that the String held in the attribute +func (v oneOfValidator) ValidateParameterString(ctx context.Context, request function.StringParameterValidatorRequest, response *function.StringParameterValidatorResponse) { + if request.Value.IsNull() || request.Value.IsUnknown() { + return + } + + value := request.Value + + for _, otherValue := range v.values { + if value.Equal(otherValue) { + return + } + } + + response.Error = validatorfuncerr.InvalidParameterValueMatchFuncError( + request.ArgumentPosition, + v.Description(ctx), + value.String(), + ) +} + +// OneOf checks that the String held in the attribute or function parameter // is one of the given `values`. -func OneOf(values ...string) validator.String { +func OneOf(values ...string) oneOfValidator { frameworkValues := make([]types.String, 0, len(values)) for _, value := range values { diff --git a/stringvalidator/one_of_case_insensitive.go b/stringvalidator/one_of_case_insensitive.go index 7e5912a..74efc12 100644 --- a/stringvalidator/one_of_case_insensitive.go +++ b/stringvalidator/one_of_case_insensitive.go @@ -8,15 +8,17 @@ import ( "fmt" "strings" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatorfuncerr" ) var _ validator.String = oneOfCaseInsensitiveValidator{} +var _ function.StringParameterValidator = oneOfCaseInsensitiveValidator{} -// oneOfCaseInsensitiveValidator validates that the value matches one of expected values. type oneOfCaseInsensitiveValidator struct { values []types.String } @@ -49,9 +51,29 @@ func (v oneOfCaseInsensitiveValidator) ValidateString(ctx context.Context, reque )) } -// OneOfCaseInsensitive checks that the String held in the attribute +func (v oneOfCaseInsensitiveValidator) ValidateParameterString(ctx context.Context, request function.StringParameterValidatorRequest, response *function.StringParameterValidatorResponse) { + if request.Value.IsNull() || request.Value.IsUnknown() { + return + } + + value := request.Value + + for _, otherValue := range v.values { + if strings.EqualFold(value.ValueString(), otherValue.ValueString()) { + return + } + } + + response.Error = validatorfuncerr.InvalidParameterValueMatchFuncError( + request.ArgumentPosition, + v.Description(ctx), + value.String(), + ) +} + +// OneOfCaseInsensitive checks that the String held in the attribute or function parameter // is one of the given `values`. -func OneOfCaseInsensitive(values ...string) validator.String { +func OneOfCaseInsensitive(values ...string) oneOfCaseInsensitiveValidator { frameworkValues := make([]types.String, 0, len(values)) for _, value := range values { diff --git a/stringvalidator/one_of_case_insensitive_test.go b/stringvalidator/one_of_case_insensitive_test.go index 03d61d8..aa6be52 100644 --- a/stringvalidator/one_of_case_insensitive_test.go +++ b/stringvalidator/one_of_case_insensitive_test.go @@ -5,9 +5,11 @@ package stringvalidator_test import ( "context" + "fmt" "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" @@ -18,79 +20,89 @@ func TestOneOfCaseInsensitiveValidator(t *testing.T) { t.Parallel() type testCase struct { - in types.String - validator validator.String - expErrors int + in types.String + oneOfValues []string + expectError bool } testCases := map[string]testCase{ "simple-match": { in: types.StringValue("foo"), - validator: stringvalidator.OneOfCaseInsensitive( + oneOfValues: []string{ "foo", "bar", "baz", - ), - expErrors: 0, + }, }, "simple-match-case-insensitive": { in: types.StringValue("foo"), - validator: stringvalidator.OneOfCaseInsensitive( + oneOfValues: []string{ "FOO", "bar", "baz", - ), - expErrors: 0, + }, }, "simple-mismatch": { in: types.StringValue("foz"), - validator: stringvalidator.OneOfCaseInsensitive( + oneOfValues: []string{ "foo", "bar", "baz", - ), - expErrors: 1, + }, + expectError: true, }, "skip-validation-on-null": { in: types.StringNull(), - validator: stringvalidator.OneOfCaseInsensitive( + oneOfValues: []string{ "foo", "bar", "baz", - ), - expErrors: 0, + }, }, "skip-validation-on-unknown": { in: types.StringUnknown(), - validator: stringvalidator.OneOfCaseInsensitive( + oneOfValues: []string{ "foo", "bar", "baz", - ), - expErrors: 0, + }, }, } for name, test := range testCases { name, test := name, test - t.Run(name, func(t *testing.T) { + + t.Run(fmt.Sprintf("ValidateString - %s", name), func(t *testing.T) { t.Parallel() req := validator.StringRequest{ ConfigValue: test.in, } res := validator.StringResponse{} - test.validator.ValidateString(context.TODO(), req, &res) + stringvalidator.OneOfCaseInsensitive(test.oneOfValues...).ValidateString(context.TODO(), req, &res) + + if !res.Diagnostics.HasError() && test.expectError { + t.Fatal("expected error, got no error") + } - if test.expErrors > 0 && !res.Diagnostics.HasError() { - t.Fatalf("expected %d error(s), got none", test.expErrors) + if res.Diagnostics.HasError() && !test.expectError { + t.Fatalf("got unexpected error: %s", res.Diagnostics) + } + }) + + t.Run(fmt.Sprintf("ValidateParameterString - %s", name), func(t *testing.T) { + t.Parallel() + req := function.StringParameterValidatorRequest{ + Value: test.in, } + res := function.StringParameterValidatorResponse{} + stringvalidator.OneOfCaseInsensitive(test.oneOfValues...).ValidateParameterString(context.TODO(), req, &res) - if test.expErrors > 0 && test.expErrors != res.Diagnostics.ErrorsCount() { - t.Fatalf("expected %d error(s), got %d: %v", test.expErrors, res.Diagnostics.ErrorsCount(), res.Diagnostics) + if res.Error == nil && test.expectError { + t.Fatal("expected error, got no error") } - if test.expErrors == 0 && res.Diagnostics.HasError() { - t.Fatalf("expected no error(s), got %d: %v", res.Diagnostics.ErrorsCount(), res.Diagnostics) + if res.Error != nil && !test.expectError { + t.Fatalf("got unexpected error: %s", res.Error) } }) } diff --git a/stringvalidator/one_of_example_test.go b/stringvalidator/one_of_example_test.go index 39f820f..2eede95 100644 --- a/stringvalidator/one_of_example_test.go +++ b/stringvalidator/one_of_example_test.go @@ -6,6 +6,7 @@ package stringvalidator_test import ( "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/schema/validator" ) @@ -23,3 +24,17 @@ func ExampleOneOf() { }, } } + +func ExampleOneOf_function() { + _ = function.Definition{ + Parameters: []function.Parameter{ + function.StringParameter{ + Name: "example_param", + Validators: []function.StringParameterValidator{ + // Validate string value must be "one", "two", or "three" + stringvalidator.OneOf([]string{"one", "two", "three"}...), + }, + }, + }, + } +} diff --git a/stringvalidator/one_of_test.go b/stringvalidator/one_of_test.go index a3a99dc..e293563 100644 --- a/stringvalidator/one_of_test.go +++ b/stringvalidator/one_of_test.go @@ -5,9 +5,11 @@ package stringvalidator_test import ( "context" + "fmt" "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" @@ -18,79 +20,90 @@ func TestOneOfValidator(t *testing.T) { t.Parallel() type testCase struct { - in types.String - validator validator.String - expErrors int + in types.String + oneOfValues []string + expectError bool } testCases := map[string]testCase{ "simple-match": { in: types.StringValue("foo"), - validator: stringvalidator.OneOf( + oneOfValues: []string{ "foo", "bar", "baz", - ), - expErrors: 0, + }, }, "simple-mismatch-case-insensitive": { in: types.StringValue("foo"), - validator: stringvalidator.OneOf( + oneOfValues: []string{ "FOO", "bar", "baz", - ), - expErrors: 1, + }, + expectError: true, }, "simple-mismatch": { in: types.StringValue("foz"), - validator: stringvalidator.OneOf( + oneOfValues: []string{ "foo", "bar", "baz", - ), - expErrors: 1, + }, + expectError: true, }, "skip-validation-on-null": { in: types.StringNull(), - validator: stringvalidator.OneOf( + oneOfValues: []string{ "foo", "bar", "baz", - ), - expErrors: 0, + }, }, "skip-validation-on-unknown": { in: types.StringUnknown(), - validator: stringvalidator.OneOf( + oneOfValues: []string{ "foo", "bar", "baz", - ), - expErrors: 0, + }, }, } for name, test := range testCases { name, test := name, test - t.Run(name, func(t *testing.T) { + + t.Run(fmt.Sprintf("ValidateString - %s", name), func(t *testing.T) { t.Parallel() req := validator.StringRequest{ ConfigValue: test.in, } res := validator.StringResponse{} - test.validator.ValidateString(context.TODO(), req, &res) + stringvalidator.OneOf(test.oneOfValues...).ValidateString(context.TODO(), req, &res) + + if !res.Diagnostics.HasError() && test.expectError { + t.Fatal("expected error, got no error") + } - if test.expErrors > 0 && !res.Diagnostics.HasError() { - t.Fatalf("expected %d error(s), got none", test.expErrors) + if res.Diagnostics.HasError() && !test.expectError { + t.Fatalf("got unexpected error: %s", res.Diagnostics) + } + }) + + t.Run(fmt.Sprintf("ValidateParameterString - %s", name), func(t *testing.T) { + t.Parallel() + req := function.StringParameterValidatorRequest{ + Value: test.in, } + res := function.StringParameterValidatorResponse{} + stringvalidator.OneOf(test.oneOfValues...).ValidateParameterString(context.TODO(), req, &res) - if test.expErrors > 0 && test.expErrors != res.Diagnostics.ErrorsCount() { - t.Fatalf("expected %d error(s), got %d: %v", test.expErrors, res.Diagnostics.ErrorsCount(), res.Diagnostics) + if res.Error == nil && test.expectError { + t.Fatal("expected error, got no error") } - if test.expErrors == 0 && res.Diagnostics.HasError() { - t.Fatalf("expected no error(s), got %d: %v", res.Diagnostics.ErrorsCount(), res.Diagnostics) + if res.Error != nil && !test.expectError { + t.Fatalf("got unexpected error: %s", res.Error) } }) } diff --git a/stringvalidator/regex_matches.go b/stringvalidator/regex_matches.go index 4cab997..756f9bb 100644 --- a/stringvalidator/regex_matches.go +++ b/stringvalidator/regex_matches.go @@ -9,18 +9,19 @@ import ( "regexp" "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatorfuncerr" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/schema/validator" ) var _ validator.String = regexMatchesValidator{} +var _ function.StringParameterValidator = regexMatchesValidator{} -// regexMatchesValidator validates that a string Attribute's value matches the specified regular expression. type regexMatchesValidator struct { regexp *regexp.Regexp message string } -// Description describes the validation in plain text formatting. func (validator regexMatchesValidator) Description(_ context.Context) string { if validator.message != "" { return validator.message @@ -28,12 +29,10 @@ func (validator regexMatchesValidator) Description(_ context.Context) string { return fmt.Sprintf("value must match regular expression '%s'", validator.regexp) } -// MarkdownDescription describes the validation in Markdown formatting. func (validator regexMatchesValidator) MarkdownDescription(ctx context.Context) string { return validator.Description(ctx) } -// Validate performs the validation. func (v regexMatchesValidator) ValidateString(ctx context.Context, request validator.StringRequest, response *validator.StringResponse) { if request.ConfigValue.IsNull() || request.ConfigValue.IsUnknown() { return @@ -50,8 +49,24 @@ func (v regexMatchesValidator) ValidateString(ctx context.Context, request valid } } +func (v regexMatchesValidator) ValidateParameterString(ctx context.Context, request function.StringParameterValidatorRequest, response *function.StringParameterValidatorResponse) { + if request.Value.IsNull() || request.Value.IsUnknown() { + return + } + + value := request.Value.ValueString() + + if !v.regexp.MatchString(value) { + response.Error = validatorfuncerr.InvalidParameterValueMatchFuncError( + request.ArgumentPosition, + v.Description(ctx), + value, + ) + } +} + // RegexMatches returns an AttributeValidator which ensures that any configured -// attribute value: +// attribute or function parameter value: // // - Is a string. // - Matches the given regular expression https://github.com/google/re2/wiki/Syntax. @@ -59,7 +74,7 @@ func (v regexMatchesValidator) ValidateString(ctx context.Context, request valid // Null (unconfigured) and unknown (known after apply) values are skipped. // Optionally an error message can be provided to return something friendlier // than "value must match regular expression 'regexp'". -func RegexMatches(regexp *regexp.Regexp, message string) validator.String { +func RegexMatches(regexp *regexp.Regexp, message string) regexMatchesValidator { return regexMatchesValidator{ regexp: regexp, message: message, diff --git a/stringvalidator/regex_matches_example_test.go b/stringvalidator/regex_matches_example_test.go index 6616c13..3a190e9 100644 --- a/stringvalidator/regex_matches_example_test.go +++ b/stringvalidator/regex_matches_example_test.go @@ -8,6 +8,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/schema/validator" ) @@ -28,3 +29,20 @@ func ExampleRegexMatches() { }, } } + +func ExampleRegexMatches_function() { + _ = function.Definition{ + Parameters: []function.Parameter{ + function.StringParameter{ + Name: "example_param", + Validators: []function.StringParameterValidator{ + // Validate string value satisfies the regular expression for alphanumeric characters + stringvalidator.RegexMatches( + regexp.MustCompile(`^[a-zA-Z0-9]*$`), + "must only contain only alphanumeric characters", + ), + }, + }, + }, + } +} diff --git a/stringvalidator/regex_matches_test.go b/stringvalidator/regex_matches_test.go index 94cf76f..f591b7f 100644 --- a/stringvalidator/regex_matches_test.go +++ b/stringvalidator/regex_matches_test.go @@ -5,9 +5,11 @@ package stringvalidator_test import ( "context" + "fmt" "regexp" "testing" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" @@ -45,7 +47,8 @@ func TestRegexMatchesValidator(t *testing.T) { for name, test := range tests { name, test := name, test - t.Run(name, func(t *testing.T) { + + t.Run(fmt.Sprintf("ValidateString - %s", name), func(t *testing.T) { t.Parallel() request := validator.StringRequest{ Path: path.Root("test"), @@ -63,5 +66,23 @@ func TestRegexMatchesValidator(t *testing.T) { t.Fatalf("got unexpected error: %s", response.Diagnostics) } }) + + t.Run(fmt.Sprintf("ValidateParameterString - %s", name), func(t *testing.T) { + t.Parallel() + request := function.StringParameterValidatorRequest{ + ArgumentPosition: 0, + Value: test.val, + } + response := function.StringParameterValidatorResponse{} + stringvalidator.RegexMatches(test.regexp, "").ValidateParameterString(context.TODO(), request, &response) + + if response.Error == nil && test.expectError { + t.Fatal("expected error, got no error") + } + + if response.Error != nil && !test.expectError { + t.Fatalf("got unexpected error: %s", response.Error) + } + }) } } diff --git a/stringvalidator/utf8_length_at_least.go b/stringvalidator/utf8_length_at_least.go index 6159eab..a0f9931 100644 --- a/stringvalidator/utf8_length_at_least.go +++ b/stringvalidator/utf8_length_at_least.go @@ -8,30 +8,46 @@ import ( "fmt" "unicode/utf8" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatorfuncerr" ) var _ validator.String = utf8LengthAtLeastValidator{} +var _ function.StringParameterValidator = utf8LengthAtLeastValidator{} -// utf8LengthAtLeastValidator implements the validator. type utf8LengthAtLeastValidator struct { minLength int } -// Description describes the validation in plain text formatting. +func (validator utf8LengthAtLeastValidator) invalidUsageMessage() string { + return fmt.Sprintf("minLength cannot be less than zero - minLength: %d", validator.minLength) +} + func (validator utf8LengthAtLeastValidator) Description(_ context.Context) string { return fmt.Sprintf("UTF-8 character count must be at least %d", validator.minLength) } -// MarkdownDescription describes the validation in Markdown formatting. func (validator utf8LengthAtLeastValidator) MarkdownDescription(ctx context.Context) string { return validator.Description(ctx) } -// Validate performs the validation. func (v utf8LengthAtLeastValidator) ValidateString(ctx context.Context, request validator.StringRequest, response *validator.StringResponse) { + // Return an error if the validator has been created in an invalid state + if v.minLength < 0 { + response.Diagnostics.Append( + validatordiag.InvalidValidatorUsageDiagnostic( + request.Path, + "UTF8LengthAtLeast", + v.invalidUsageMessage(), + ), + ) + + return + } + if request.ConfigValue.IsNull() || request.ConfigValue.IsUnknown() { return } @@ -51,17 +67,47 @@ func (v utf8LengthAtLeastValidator) ValidateString(ctx context.Context, request } } +func (v utf8LengthAtLeastValidator) ValidateParameterString(ctx context.Context, request function.StringParameterValidatorRequest, response *function.StringParameterValidatorResponse) { + // Return an error if the validator has been created in an invalid state + if v.minLength < 0 { + response.Error = validatorfuncerr.InvalidValidatorUsageFuncError( + request.ArgumentPosition, + "UTF8LengthAtLeast", + v.invalidUsageMessage(), + ) + + return + } + + if request.Value.IsNull() || request.Value.IsUnknown() { + return + } + + value := request.Value.ValueString() + + count := utf8.RuneCountInString(value) + + if count < v.minLength { + response.Error = validatorfuncerr.InvalidParameterValueLengthFuncError( + request.ArgumentPosition, + v.Description(ctx), + fmt.Sprintf("%d", count), + ) + + return + } +} + // UTF8LengthAtLeast returns an validator which ensures that any configured -// attribute value is of UTF-8 character count greater than or equal to the +// attribute or function parameter value is of UTF-8 character count greater than or equal to the // given minimum. Null (unconfigured) and unknown (known after apply) values // are skipped. // +// minLength cannot be less than zero. Invalid input for minLength will result in an +// implementation error message during validation. +// // Use LengthAtLeast for checking single-byte character counts. -func UTF8LengthAtLeast(minLength int) validator.String { - if minLength < 0 { - return nil - } - +func UTF8LengthAtLeast(minLength int) utf8LengthAtLeastValidator { return utf8LengthAtLeastValidator{ minLength: minLength, } diff --git a/stringvalidator/utf8_length_at_least_example_test.go b/stringvalidator/utf8_length_at_least_example_test.go index 1db4a75..5ef990e 100644 --- a/stringvalidator/utf8_length_at_least_example_test.go +++ b/stringvalidator/utf8_length_at_least_example_test.go @@ -6,6 +6,7 @@ package stringvalidator_test import ( "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/schema/validator" ) @@ -23,3 +24,17 @@ func ExampleUTF8LengthAtLeast() { }, } } + +func ExampleUTF8LengthAtLeast_function() { + _ = function.Definition{ + Parameters: []function.Parameter{ + function.StringParameter{ + Name: "example_param", + Validators: []function.StringParameterValidator{ + // Validate UTF-8 character count must be at least 3 characters. + stringvalidator.UTF8LengthAtLeast(3), + }, + }, + }, + } +} diff --git a/stringvalidator/utf8_length_at_least_test.go b/stringvalidator/utf8_length_at_least_test.go index f1d4b01..27f0072 100644 --- a/stringvalidator/utf8_length_at_least_test.go +++ b/stringvalidator/utf8_length_at_least_test.go @@ -5,8 +5,10 @@ package stringvalidator_test import ( "context" + "fmt" "testing" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" @@ -62,11 +64,17 @@ func TestUTF8LengthAtLeastValidator(t *testing.T) { minLength: 2, expectError: true, }, + "invalid validator usage - minLength < 0": { + val: types.StringValue("ok"), + minLength: -1, + expectError: true, + }, } for name, test := range tests { name, test := name, test - t.Run(name, func(t *testing.T) { + + t.Run(fmt.Sprintf("ValidateString - %s", name), func(t *testing.T) { t.Parallel() request := validator.StringRequest{ Path: path.Root("test"), @@ -84,5 +92,23 @@ func TestUTF8LengthAtLeastValidator(t *testing.T) { t.Fatalf("got unexpected error: %s", response.Diagnostics) } }) + + t.Run(fmt.Sprintf("ValidateParameterString - %s", name), func(t *testing.T) { + t.Parallel() + request := function.StringParameterValidatorRequest{ + ArgumentPosition: 0, + Value: test.val, + } + response := function.StringParameterValidatorResponse{} + stringvalidator.UTF8LengthAtLeast(test.minLength).ValidateParameterString(context.Background(), request, &response) + + if response.Error == nil && test.expectError { + t.Fatal("expected error, got no error") + } + + if response.Error != nil && !test.expectError { + t.Fatalf("got unexpected error: %s", response.Error) + } + }) } } diff --git a/stringvalidator/utf8_length_at_most.go b/stringvalidator/utf8_length_at_most.go index 1653d5f..e02db80 100644 --- a/stringvalidator/utf8_length_at_most.go +++ b/stringvalidator/utf8_length_at_most.go @@ -8,30 +8,46 @@ import ( "fmt" "unicode/utf8" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatorfuncerr" ) var _ validator.String = utf8LengthAtMostValidator{} +var _ function.StringParameterValidator = utf8LengthAtMostValidator{} -// utf8LengthAtMostValidator implements the validator. type utf8LengthAtMostValidator struct { maxLength int } -// Description describes the validation in plain text formatting. +func (validator utf8LengthAtMostValidator) invalidUsageMessage() string { + return fmt.Sprintf("maxLength cannot be less than zero - maxLength: %d", validator.maxLength) +} + func (validator utf8LengthAtMostValidator) Description(_ context.Context) string { return fmt.Sprintf("UTF-8 character count must be at most %d", validator.maxLength) } -// MarkdownDescription describes the validation in Markdown formatting. func (validator utf8LengthAtMostValidator) MarkdownDescription(ctx context.Context) string { return validator.Description(ctx) } -// Validate performs the validation. func (v utf8LengthAtMostValidator) ValidateString(ctx context.Context, request validator.StringRequest, response *validator.StringResponse) { + // Return an error if the validator has been created in an invalid state + if v.maxLength < 0 { + response.Diagnostics.Append( + validatordiag.InvalidValidatorUsageDiagnostic( + request.Path, + "UTF8LengthAtMost", + v.invalidUsageMessage(), + ), + ) + + return + } + if request.ConfigValue.IsNull() || request.ConfigValue.IsUnknown() { return } @@ -51,17 +67,47 @@ func (v utf8LengthAtMostValidator) ValidateString(ctx context.Context, request v } } +func (v utf8LengthAtMostValidator) ValidateParameterString(ctx context.Context, request function.StringParameterValidatorRequest, response *function.StringParameterValidatorResponse) { + // Return an error if the validator has been created in an invalid state + if v.maxLength < 0 { + response.Error = validatorfuncerr.InvalidValidatorUsageFuncError( + request.ArgumentPosition, + "UTF8LengthAtMost", + v.invalidUsageMessage(), + ) + + return + } + + if request.Value.IsNull() || request.Value.IsUnknown() { + return + } + + value := request.Value.ValueString() + + count := utf8.RuneCountInString(value) + + if count > v.maxLength { + response.Error = validatorfuncerr.InvalidParameterValueLengthFuncError( + request.ArgumentPosition, + v.Description(ctx), + fmt.Sprintf("%d", count), + ) + + return + } +} + // UTF8LengthAtMost returns an validator which ensures that any configured -// attribute value is of UTF-8 character count less than or equal to the +// attribute or function parameter value is of UTF-8 character count less than or equal to the // given maximum. Null (unconfigured) and unknown (known after apply) values // are skipped. // +// maxLength cannot be less than zero. Invalid input for maxLength will result in an +// implementation error message during validation. +// // Use LengthAtMost for checking single-byte character counts. -func UTF8LengthAtMost(maxLength int) validator.String { - if maxLength < 0 { - return nil - } - +func UTF8LengthAtMost(maxLength int) utf8LengthAtMostValidator { return utf8LengthAtMostValidator{ maxLength: maxLength, } diff --git a/stringvalidator/utf8_length_at_most_example_test.go b/stringvalidator/utf8_length_at_most_example_test.go index 215f685..fb18881 100644 --- a/stringvalidator/utf8_length_at_most_example_test.go +++ b/stringvalidator/utf8_length_at_most_example_test.go @@ -6,6 +6,7 @@ package stringvalidator_test import ( "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/schema/validator" ) @@ -23,3 +24,17 @@ func ExampleUTF8LengthAtMost() { }, } } + +func ExampleUTF8LengthAtMost_function() { + _ = function.Definition{ + Parameters: []function.Parameter{ + function.StringParameter{ + Name: "example_param", + Validators: []function.StringParameterValidator{ + // Validate UTF-8 character count must be at most 255 characters. + stringvalidator.UTF8LengthAtMost(255), + }, + }, + }, + } +} diff --git a/stringvalidator/utf8_length_at_most_test.go b/stringvalidator/utf8_length_at_most_test.go index f34590c..ce70d88 100644 --- a/stringvalidator/utf8_length_at_most_test.go +++ b/stringvalidator/utf8_length_at_most_test.go @@ -5,8 +5,10 @@ package stringvalidator_test import ( "context" + "fmt" "testing" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" @@ -62,11 +64,17 @@ func TestUTF8LengthAtMostValidator(t *testing.T) { maxLength: 1, expectError: true, }, + "invalid validator usage - maxLength < 0": { + val: types.StringValue("ok"), + maxLength: -1, + expectError: true, + }, } for name, test := range tests { name, test := name, test - t.Run(name, func(t *testing.T) { + + t.Run(fmt.Sprintf("ValidateString - %s", name), func(t *testing.T) { t.Parallel() request := validator.StringRequest{ Path: path.Root("test"), @@ -84,5 +92,23 @@ func TestUTF8LengthAtMostValidator(t *testing.T) { t.Fatalf("got unexpected error: %s", response.Diagnostics) } }) + + t.Run(fmt.Sprintf("ValidateParameterString - %s", name), func(t *testing.T) { + t.Parallel() + request := function.StringParameterValidatorRequest{ + ArgumentPosition: 0, + Value: test.val, + } + response := function.StringParameterValidatorResponse{} + stringvalidator.UTF8LengthAtMost(test.maxLength).ValidateParameterString(context.Background(), request, &response) + + if response.Error == nil && test.expectError { + t.Fatal("expected error, got no error") + } + + if response.Error != nil && !test.expectError { + t.Fatalf("got unexpected error: %s", response.Error) + } + }) } } diff --git a/stringvalidator/utf8_length_between.go b/stringvalidator/utf8_length_between.go index 791b9a5..05e2215 100644 --- a/stringvalidator/utf8_length_between.go +++ b/stringvalidator/utf8_length_between.go @@ -8,31 +8,47 @@ import ( "fmt" "unicode/utf8" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatorfuncerr" ) var _ validator.String = utf8LengthBetweenValidator{} +var _ function.StringParameterValidator = utf8LengthBetweenValidator{} -// utf8LengthBetweenValidator implements the validator. type utf8LengthBetweenValidator struct { maxLength int minLength int } -// Description describes the validation in plain text formatting. +func (v utf8LengthBetweenValidator) invalidUsageMessage() string { + return fmt.Sprintf("minLength and maxLength cannot be less than zero and maxLength must be greater than or equal to minLength - minLength: %d, maxLength: %d", v.minLength, v.maxLength) +} + func (v utf8LengthBetweenValidator) Description(_ context.Context) string { return fmt.Sprintf("UTF-8 character count must be between %d and %d", v.minLength, v.maxLength) } -// MarkdownDescription describes the validation in Markdown formatting. func (v utf8LengthBetweenValidator) MarkdownDescription(ctx context.Context) string { return v.Description(ctx) } -// Validate performs the validation. func (v utf8LengthBetweenValidator) ValidateString(ctx context.Context, request validator.StringRequest, response *validator.StringResponse) { + // Return an error if the validator has been created in an invalid state + if v.minLength < 0 || v.maxLength < 0 || v.minLength > v.maxLength { + response.Diagnostics.Append( + validatordiag.InvalidValidatorUsageDiagnostic( + request.Path, + "UTF8LengthBetween", + v.invalidUsageMessage(), + ), + ) + + return + } + if request.ConfigValue.IsNull() || request.ConfigValue.IsUnknown() { return } @@ -52,17 +68,48 @@ func (v utf8LengthBetweenValidator) ValidateString(ctx context.Context, request } } +func (v utf8LengthBetweenValidator) ValidateParameterString(ctx context.Context, request function.StringParameterValidatorRequest, response *function.StringParameterValidatorResponse) { + // Return an error if the validator has been created in an invalid state + if v.minLength < 0 || v.maxLength < 0 || v.minLength > v.maxLength { + response.Error = validatorfuncerr.InvalidValidatorUsageFuncError( + request.ArgumentPosition, + "UTF8LengthBetween", + v.invalidUsageMessage(), + ) + + return + } + + if request.Value.IsNull() || request.Value.IsUnknown() { + return + } + + value := request.Value.ValueString() + + count := utf8.RuneCountInString(value) + + if count < v.minLength || count > v.maxLength { + response.Error = validatorfuncerr.InvalidParameterValueLengthFuncError( + request.ArgumentPosition, + v.Description(ctx), + fmt.Sprintf("%d", count), + ) + + return + } +} + // UTF8LengthBetween returns an validator which ensures that any configured -// attribute value is of UTF-8 character count greater than or equal to the +// attribute or function parameter value is of UTF-8 character count greater than or equal to the // given minimum and less than or equal to the given maximum. Null // (unconfigured) and unknown (known after apply) values are skipped. // +// minLength and maxLength cannot be less than zero and maxLength must be greater than or equal to minLength. +// Invalid combinations of minLength and maxLength will result in an implementation error message +// during validation. +// // Use LengthBetween for checking single-byte character counts. -func UTF8LengthBetween(minLength int, maxLength int) validator.String { - if minLength < 0 || maxLength < 0 || minLength > maxLength { - return nil - } - +func UTF8LengthBetween(minLength int, maxLength int) utf8LengthBetweenValidator { return utf8LengthBetweenValidator{ maxLength: maxLength, minLength: minLength, diff --git a/stringvalidator/utf8_length_between_example_test.go b/stringvalidator/utf8_length_between_example_test.go index fa38f79..139edf3 100644 --- a/stringvalidator/utf8_length_between_example_test.go +++ b/stringvalidator/utf8_length_between_example_test.go @@ -6,6 +6,7 @@ package stringvalidator_test import ( "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/schema/validator" ) @@ -24,3 +25,18 @@ func ExampleUTF8LengthBetween() { }, } } + +func ExampleUTF8LengthBetween_function() { + _ = function.Definition{ + Parameters: []function.Parameter{ + function.StringParameter{ + Name: "example_param", + Validators: []function.StringParameterValidator{ + // Validate UTF-8 character count must be at least 3 characters + // and at most 255 characters. + stringvalidator.UTF8LengthBetween(3, 255), + }, + }, + }, + } +} diff --git a/stringvalidator/utf8_length_between_test.go b/stringvalidator/utf8_length_between_test.go index 49e12c7..9a0b04e 100644 --- a/stringvalidator/utf8_length_between_test.go +++ b/stringvalidator/utf8_length_between_test.go @@ -5,8 +5,10 @@ package stringvalidator_test import ( "context" + "fmt" "testing" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" @@ -71,11 +73,30 @@ func TestUTF8LengthBetweenValidator(t *testing.T) { maxLength: 1, expectError: true, }, + "invalid validator usage - minLength < 0": { + val: types.StringValue("ok"), + minLength: -1, + maxLength: 3, + expectError: true, + }, + "invalid validator usage - maxLength < 0": { + val: types.StringValue("ok"), + minLength: 2, + maxLength: -1, + expectError: true, + }, + "invalid validator usage - minLength > maxLength": { + val: types.StringValue("ok"), + minLength: 2, + maxLength: 1, + expectError: true, + }, } for name, test := range tests { name, test := name, test - t.Run(name, func(t *testing.T) { + + t.Run(fmt.Sprintf("ValidateString - %s", name), func(t *testing.T) { t.Parallel() request := validator.StringRequest{ Path: path.Root("test"), @@ -93,5 +114,23 @@ func TestUTF8LengthBetweenValidator(t *testing.T) { t.Fatalf("got unexpected error: %s", response.Diagnostics) } }) + + t.Run(fmt.Sprintf("ValidateParameterString - %s", name), func(t *testing.T) { + t.Parallel() + request := function.StringParameterValidatorRequest{ + ArgumentPosition: 0, + Value: test.val, + } + response := function.StringParameterValidatorResponse{} + stringvalidator.UTF8LengthBetween(test.minLength, test.maxLength).ValidateParameterString(context.Background(), request, &response) + + if response.Error == nil && test.expectError { + t.Fatal("expected error, got no error") + } + + if response.Error != nil && !test.expectError { + t.Fatalf("got unexpected error: %s", response.Error) + } + }) } }