diff --git a/.changes/0.14.0.md b/.changes/0.14.0.md new file mode 100644 index 00000000..b52df76b --- /dev/null +++ b/.changes/0.14.0.md @@ -0,0 +1,15 @@ +## 0.14.0 (October 17, 2024) + +NOTES: + +* all: This Go module has been updated to Go 1.22 per the [Go support policy](https://go.dev/doc/devel/release#policy). It is recommended to review the [Go 1.22 release notes](https://go.dev/doc/go1.22) before upgrading. Any consumers building on earlier Go versions may experience errors. ([#229](https://github.com/hashicorp/terraform-plugin-framework-validators/issues/229)) +* 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. ([#235](https://github.com/hashicorp/terraform-plugin-framework-validators/issues/235)) + +FEATURES: + +* boolvalidator: Added `Equals` validator ([#232](https://github.com/hashicorp/terraform-plugin-framework-validators/issues/232)) + +ENHANCEMENTS: + +* all: Implemented parameter interfaces for all value-based validators. This allows these validators to be used with provider-defined functions. ([#235](https://github.com/hashicorp/terraform-plugin-framework-validators/issues/235)) + diff --git a/.github/workflows/ci-changie.yml b/.github/workflows/ci-changie.yml index de5554dd..60a514c0 100644 --- a/.github/workflows/ci-changie.yml +++ b/.github/workflows/ci-changie.yml @@ -19,7 +19,7 @@ jobs: runs-on: ubuntu-latest steps: # Ensure terraform-devex-repos is updated on version changes. - - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 # Ensure terraform-devex-repos is updated on version changes. - uses: miniscruff/changie-action@6dcc2533cac0495148ed4046c438487e4dceaa23 # v2.0.0 with: diff --git a/.github/workflows/ci-github-actions.yml b/.github/workflows/ci-github-actions.yml index cf96ddbd..2358e4ad 100644 --- a/.github/workflows/ci-github-actions.yml +++ b/.github/workflows/ci-github-actions.yml @@ -13,8 +13,8 @@ jobs: actionlint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - - uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1 + - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + - uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 with: go-version-file: 'go.mod' - run: go install github.com/rhysd/actionlint/cmd/actionlint@latest diff --git a/.github/workflows/ci-go.yml b/.github/workflows/ci-go.yml index 5ca47368..60928e9b 100644 --- a/.github/workflows/ci-go.yml +++ b/.github/workflows/ci-go.yml @@ -16,28 +16,28 @@ jobs: golangci-lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - - uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1 + - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + - uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 with: go-version-file: 'go.mod' - run: go mod download - - uses: golangci/golangci-lint-action@a4f60bb28d35aeee14e6880718e0c85ff1882e64 # v6.0.1 + - uses: golangci/golangci-lint-action@971e284b6050e8a5849b72094c50ab08da042db8 # v6.1.1 test: name: test (Go v${{ matrix.go-version }}) runs-on: ubuntu-latest strategy: matrix: - go-version: [ '1.22', '1.21' ] + go-version: [ '1.23', '1.22' ] steps: - - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - - uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1 + - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + - uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 with: go-version: ${{ matrix.go-version }} - run: go mod download - run: go test -coverprofile=coverage.out ./... - run: go tool cover -html=coverage.out -o coverage.html - - uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 + - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: go-${{ matrix.go-version }}-coverage path: coverage.html diff --git a/.github/workflows/ci-goreleaser.yml b/.github/workflows/ci-goreleaser.yml index 398bd155..d978baa4 100644 --- a/.github/workflows/ci-goreleaser.yml +++ b/.github/workflows/ci-goreleaser.yml @@ -14,8 +14,8 @@ jobs: check: runs-on: ubuntu-latest steps: - - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - - uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1 + - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + - uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 with: go-version-file: 'go.mod' - uses: goreleaser/goreleaser-action@286f3b13b1b49da4ac219696163fb8c1c93e1200 # v6.0.0 diff --git a/.github/workflows/compliance.yml b/.github/workflows/compliance.yml index 51d71992..ddd4d72a 100644 --- a/.github/workflows/compliance.yml +++ b/.github/workflows/compliance.yml @@ -11,7 +11,7 @@ jobs: copywrite: runs-on: ubuntu-latest steps: - - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - uses: hashicorp/setup-copywrite@32638da2d4e81d56a0764aa1547882fc4d209636 # v1.1.3 - run: copywrite headers --plan - run: copywrite license --plan diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3a6ec9c8..e253c38d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -25,7 +25,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 with: fetch-depth: 0 # Avoid persisting GITHUB_TOKEN credentials as they take priority over our service account PAT for `git push` operations @@ -54,7 +54,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 with: fetch-depth: 0 # Default input is the SHA that initially triggered the workflow. As we created a new commit in the previous job, @@ -79,12 +79,12 @@ jobs: contents: write # Needed for goreleaser to create GitHub release issues: write # Needed for goreleaser to close associated milestone steps: - - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 with: ref: ${{ inputs.versionNumber }} fetch-depth: 0 - - uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1 + - uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 with: go-version-file: 'go.mod' diff --git a/.golangci.yml b/.golangci.yml index b5e4abef..f8175fa9 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -22,4 +22,4 @@ linters: - unconvert - unparam - unused - - vet + - govet diff --git a/CHANGELOG.md b/CHANGELOG.md index 762e2137..82768140 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,18 @@ +## 0.14.0 (October 17, 2024) + +NOTES: + +* all: This Go module has been updated to Go 1.22 per the [Go support policy](https://go.dev/doc/devel/release#policy). It is recommended to review the [Go 1.22 release notes](https://go.dev/doc/go1.22) before upgrading. Any consumers building on earlier Go versions may experience errors. ([#229](https://github.com/hashicorp/terraform-plugin-framework-validators/issues/229)) +* 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. ([#235](https://github.com/hashicorp/terraform-plugin-framework-validators/issues/235)) + +FEATURES: + +* boolvalidator: Added `Equals` validator ([#232](https://github.com/hashicorp/terraform-plugin-framework-validators/issues/232)) + +ENHANCEMENTS: + +* all: Implemented parameter interfaces for all value-based validators. This allows these validators to be used with provider-defined functions. ([#235](https://github.com/hashicorp/terraform-plugin-framework-validators/issues/235)) + ## 0.13.0 (July 09, 2024) NOTES: diff --git a/README.md b/README.md index 668dc484..79d263b6 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ This Go module is typically kept up to date with the latest `terraform-plugin-fr This Go module follows `terraform-plugin-framework` Go compatibility. -Currently that means Go **1.21** must be used when developing and testing code. +Currently that means Go **1.22** must be used when developing and testing code. ## Contributing diff --git a/boolvalidator/doc.go b/boolvalidator/doc.go index 9fd64e3e..beae4455 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 new file mode 100644 index 00000000..755e9769 --- /dev/null +++ b/boolvalidator/equals.go @@ -0,0 +1,70 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package boolvalidator + +import ( + "context" + "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 +} + +func (v equalsValidator) Description(ctx context.Context) string { + return fmt.Sprintf("Value must be %q", v.value) +} + +func (v equalsValidator) MarkdownDescription(ctx context.Context) string { + return v.Description(ctx) +} + +func (v equalsValidator) ValidateBool(ctx context.Context, req validator.BoolRequest, resp *validator.BoolResponse) { + if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() { + return + } + + configValue := req.ConfigValue + + if !configValue.Equal(v.value) { + resp.Diagnostics.Append(validatordiag.InvalidAttributeValueMatchDiagnostic( + req.Path, + v.Description(ctx), + configValue.String(), + )) + } +} + +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) equalsValidator { + return equalsValidator{ + value: types.BoolValue(value), + } +} diff --git a/boolvalidator/equals_test.go b/boolvalidator/equals_test.go new file mode 100644 index 00000000..ed2671e1 --- /dev/null +++ b/boolvalidator/equals_test.go @@ -0,0 +1,83 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +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" +) + +func TestEqualsValidator(t *testing.T) { + t.Parallel() + + type testCase struct { + in types.Bool + equalsValue bool + expectError bool + } + + testCases := map[string]testCase{ + "simple-match": { + in: types.BoolValue(true), + equalsValue: true, + }, + "simple-mismatch": { + in: types.BoolValue(false), + equalsValue: true, + expectError: true, + }, + "skip-validation-on-null": { + in: types.BoolNull(), + equalsValue: true, + }, + "skip-validation-on-unknown": { + in: types.BoolUnknown(), + equalsValue: true, + }, + } + + for name, test := range testCases { + 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{} + boolvalidator.Equals(test.equalsValue).ValidateBool(context.TODO(), req, &res) + + if !res.Diagnostics.HasError() && test.expectError { + t.Fatal("expected error, got no error") + } + + 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 res.Error == nil && test.expectError { + t.Fatal("expected error, got no error") + } + + 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 db5192b8..9a8eb82e 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,15 +45,31 @@ 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(min float32) validator.Float32 { +func AtLeast(minVal float32) atLeastValidator { return atLeastValidator{ - min: min, + min: minVal, } } diff --git a/float32validator/at_least_example_test.go b/float32validator/at_least_example_test.go index bae9da74..1729b3ab 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 6669900c..ea768381 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 0d5e7785..0fdf981b 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,15 +45,31 @@ 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(max float32) validator.Float32 { +func AtMost(maxVal float32) atMostValidator { return atMostValidator{ - max: max, + max: maxVal, } } diff --git a/float32validator/at_most_example_test.go b/float32validator/at_most_example_test.go index 6d3bef3a..8b1ac46c 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 cf5d1e71..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 cbc8a7d7..a70cd8ca 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,20 +62,46 @@ 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(min, max float32) validator.Float32 { - if min > max { - 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: min, - max: max, + min: minVal, + max: maxVal, } } diff --git a/float32validator/between_example_test.go b/float32validator/between_example_test.go index 25a49272..c9346133 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 1d3b813f..be0c11d4 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 3b0a7c18..e57b117a 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 fcaef909..0e6b2750 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 7da60106..e004856a 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 c2a3d5f2..8fb8a353 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 c09543b6..963f4b10 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 7c9e9426..f4bea247 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 cfe59612..e960a040 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 06cd02c9..7d334b5e 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,15 +45,31 @@ 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(min float64) validator.Float64 { +func AtLeast(minVal float64) atLeastValidator { return atLeastValidator{ - min: min, + min: minVal, } } diff --git a/float64validator/at_least_example_test.go b/float64validator/at_least_example_test.go index 39e667b3..a5d1df22 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 20146ba5..d24df495 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 cec57f78..786859b2 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,15 +45,31 @@ 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(max float64) validator.Float64 { +func AtMost(maxVal float64) atMostValidator { return atMostValidator{ - max: max, + max: maxVal, } } diff --git a/float64validator/at_most_example_test.go b/float64validator/at_most_example_test.go index f1c4eb53..106b4b3f 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 4b1f2169..464ef945 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 ca1772e0..5afdab37 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,20 +62,46 @@ 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(min, max float64) validator.Float64 { - if min > max { - 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: min, - max: max, + min: minVal, + max: maxVal, } } diff --git a/float64validator/between_example_test.go b/float64validator/between_example_test.go index a8e45c67..9ee79fab 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 b6f644cc..d9dbd4bc 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 0ceb43b0..aa1967ef 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 c3568037..0dcc0db1 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 075a4948..2205ee49 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 ce17daf7..01ba012a 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 7a4702a2..59c2f79d 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 c90a751b..ae800535 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 045b5582..3fb6d391 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/go.mod b/go.mod index 8969b4e6..1ebd9191 100644 --- a/go.mod +++ b/go.mod @@ -1,13 +1,13 @@ module github.com/hashicorp/terraform-plugin-framework-validators -go 1.21 +go 1.22.0 -toolchain go1.21.6 +toolchain go1.22.7 require ( github.com/google/go-cmp v0.6.0 - github.com/hashicorp/terraform-plugin-framework v1.10.0 - github.com/hashicorp/terraform-plugin-go v0.23.0 + github.com/hashicorp/terraform-plugin-framework v1.12.0 + github.com/hashicorp/terraform-plugin-go v0.24.0 ) require ( @@ -19,5 +19,5 @@ require ( github.com/mitchellh/go-testing-interface v1.14.1 // indirect github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect - golang.org/x/sys v0.18.0 // indirect + golang.org/x/sys v0.21.0 // indirect ) diff --git a/go.sum b/go.sum index e7471426..3521a4c7 100644 --- a/go.sum +++ b/go.sum @@ -7,10 +7,10 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/hashicorp/go-hclog v1.5.0 h1:bI2ocEMgcVlz55Oj1xZNBsVi900c7II+fWDyV9o+13c= github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= -github.com/hashicorp/terraform-plugin-framework v1.10.0 h1:xXhICE2Fns1RYZxEQebwkB2+kXouLC932Li9qelozrc= -github.com/hashicorp/terraform-plugin-framework v1.10.0/go.mod h1:qBXLDn69kM97NNVi/MQ9qgd1uWWsVftGSnygYG1tImM= -github.com/hashicorp/terraform-plugin-go v0.23.0 h1:AALVuU1gD1kPb48aPQUjug9Ir/125t+AAurhqphJ2Co= -github.com/hashicorp/terraform-plugin-go v0.23.0/go.mod h1:1E3Cr9h2vMlahWMbsSEcNrOCxovCZhOOIXjFHbjc/lQ= +github.com/hashicorp/terraform-plugin-framework v1.12.0 h1:7HKaueHPaikX5/7cbC1r9d1m12iYHY+FlNZEGxQ42CQ= +github.com/hashicorp/terraform-plugin-framework v1.12.0/go.mod h1:N/IOQ2uYjW60Jp39Cp3mw7I/OpC/GfZ0385R0YibmkE= +github.com/hashicorp/terraform-plugin-go v0.24.0 h1:2WpHhginCdVhFIrWHxDEg6RBn3YaWzR2o6qUeIEat2U= +github.com/hashicorp/terraform-plugin-go v0.24.0/go.mod h1:tUQ53lAsOyYSckFGEefGC5C8BAaO0ENqzFd3bQeuYQg= github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0= github.com/hashicorp/terraform-plugin-log v0.9.0/go.mod h1:rKL8egZQ/eXSyDqzLUuwUYLVdlYeamldAHSxjUFADow= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= @@ -35,8 +35,8 @@ golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= -golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/helpers/validatordiag/diag.go b/helpers/validatordiag/diag.go index 09a65e6b..e8563739 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 00000000..39f12681 --- /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 00000000..94966c95 --- /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 5aa839ea..c33d635b 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,15 +43,29 @@ 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(min int32) validator.Int32 { +func AtLeast(minVal int32) atLeastValidator { return atLeastValidator{ - min: min, + min: minVal, } } diff --git a/int32validator/at_least_example_test.go b/int32validator/at_least_example_test.go index d97197be..b0e9d385 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 d6e875bf..9eeb036c 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 59ad7a57..38f15d6f 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,15 +43,29 @@ 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(max int32) validator.Int32 { +func AtMost(maxVal int32) atMostValidator { return atMostValidator{ - max: max, + max: maxVal, } } diff --git a/int32validator/at_most_example_test.go b/int32validator/at_most_example_test.go index af49a23e..9a291c99 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 177b1354..c28a1186 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 f17f37fd..ea7291d1 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,20 +60,44 @@ 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(min, max int32) validator.Int32 { - if min > max { - 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: min, - max: max, + min: minVal, + max: maxVal, } } diff --git a/int32validator/between_example_test.go b/int32validator/between_example_test.go index b0f8512e..754c788a 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 8167ab47..b67f45bf 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 c6403fbf..f91dd671 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 c22bba99..fe985e91 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 4e7f2320..e0880055 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 d2b37691..2ad66c07 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 04974b58..2877b944 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 90fa4758..2023ae0e 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 e8086612..f04444e3 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 092a9479..54f89584 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,15 +43,29 @@ 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(min int64) validator.Int64 { +func AtLeast(minVal int64) atLeastValidator { return atLeastValidator{ - min: min, + min: minVal, } } diff --git a/int64validator/at_least_example_test.go b/int64validator/at_least_example_test.go index a2e1d4de..55d206d4 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 ddac371d..d224a5dc 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 b564a6e5..afc7dffa 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,15 +43,29 @@ 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(max int64) validator.Int64 { +func AtMost(maxVal int64) atMostValidator { return atMostValidator{ - max: max, + max: maxVal, } } diff --git a/int64validator/at_most_example_test.go b/int64validator/at_most_example_test.go index fb8546b5..c25747bd 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 88be51c7..a94908dc 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 879aeff0..e414c3c8 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,20 +60,44 @@ 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(min, max int64) validator.Int64 { - if min > max { - 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: min, - max: max, + min: minVal, + max: maxVal, } } diff --git a/int64validator/between_example_test.go b/int64validator/between_example_test.go index 45274ce9..076b15f3 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 7170a3aa..a7f428c8 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 0e65c174..b35fd42c 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 749fe554..42bcb0f0 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 f6835879..4be62628 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 955002e4..53726ba6 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 3a1e1db4..264bbe35 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 06ba479b..d67df85d 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 63b58f11..37f23cc7 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 a13b3761..ee0a5e8d 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 bfe35e7d..54f86bea 100644 --- a/listvalidator/size_at_least.go +++ b/listvalidator/size_at_least.go @@ -7,28 +7,28 @@ import ( "context" "fmt" - "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" + "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 @@ -45,15 +45,31 @@ 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(min int) validator.List { +func SizeAtLeast(minVal int) sizeAtLeastValidator { return sizeAtLeastValidator{ - min: min, + min: minVal, } } diff --git a/listvalidator/size_at_least_example_test.go b/listvalidator/size_at_least_example_test.go index 8aa7e54b..5c044859 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 2ace7cc7..ffb17606 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 f3e7b36d..0ff5ed24 100644 --- a/listvalidator/size_at_most.go +++ b/listvalidator/size_at_most.go @@ -7,28 +7,28 @@ import ( "context" "fmt" - "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" + "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 @@ -45,15 +45,31 @@ 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(max int) validator.List { +func SizeAtMost(maxVal int) sizeAtMostValidator { return sizeAtMostValidator{ - max: max, + max: maxVal, } } diff --git a/listvalidator/size_at_most_example_test.go b/listvalidator/size_at_most_example_test.go index d5d51a02..e2cefa6b 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 6d5bbcf4..a8cad9ac 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 32c34d9e..cab9c9dc 100644 --- a/listvalidator/size_between.go +++ b/listvalidator/size_between.go @@ -7,30 +7,29 @@ import ( "context" "fmt" - "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" + "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 @@ -47,16 +46,32 @@ 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(min, max int) validator.List { +func SizeBetween(minVal, maxVal int) sizeBetweenValidator { return sizeBetweenValidator{ - min: min, - max: max, + min: minVal, + max: maxVal, } } diff --git a/listvalidator/size_between_example_test.go b/listvalidator/size_between_example_test.go index a4bbf80d..3c15464c 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 d2253d20..1b35c9d8 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 6cfc3b73..cb9932c2 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 06f0966f..8b00b4d2 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 3127be19..51b49ad6 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 529d054e..64f596a9 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 d6a96cd3..8c9ff5a8 100644 --- a/mapvalidator/size_at_least.go +++ b/mapvalidator/size_at_least.go @@ -7,28 +7,28 @@ import ( "context" "fmt" - "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" + "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 @@ -45,15 +45,31 @@ 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(min int) validator.Map { +func SizeAtLeast(minVal int) sizeAtLeastValidator { return sizeAtLeastValidator{ - min: min, + min: minVal, } } diff --git a/mapvalidator/size_at_least_example_test.go b/mapvalidator/size_at_least_example_test.go index 67307784..a3fbd71c 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 e7a0345c..969b3c8a 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 5a926187..82fe3ef1 100644 --- a/mapvalidator/size_at_most.go +++ b/mapvalidator/size_at_most.go @@ -7,28 +7,28 @@ import ( "context" "fmt" - "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" + "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 @@ -45,15 +45,31 @@ 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(max int) validator.Map { +func SizeAtMost(maxVal int) sizeAtMostValidator { return sizeAtMostValidator{ - max: max, + max: maxVal, } } diff --git a/mapvalidator/size_at_most_example_test.go b/mapvalidator/size_at_most_example_test.go index 898beeb8..21bdc599 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 7fe6cc1c..5886bae6 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 c7255c99..78e12fb3 100644 --- a/mapvalidator/size_between.go +++ b/mapvalidator/size_between.go @@ -7,30 +7,29 @@ import ( "context" "fmt" - "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" + "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 @@ -47,16 +46,32 @@ 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(min, max int) validator.Map { +func SizeBetween(minVal, maxVal int) sizeBetweenValidator { return sizeBetweenValidator{ - min: min, - max: max, + min: minVal, + max: maxVal, } } diff --git a/mapvalidator/size_between_example_test.go b/mapvalidator/size_between_example_test.go index a7482120..b2be0839 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 0a3a6e21..d95634c7 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 2d6f8798..175ce24e 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 bb96b326..43fb5f46 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 8251ea78..9016bc6d 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 5c0fc3e6..a381ffa7 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 eee38692..9bbb3ac9 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 a66c2177..956bbeb4 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 f55c2f14..a79aed16 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 258a0db5..ed5b8227 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 d010b858..7af58b00 100644 --- a/setvalidator/size_at_least.go +++ b/setvalidator/size_at_least.go @@ -7,28 +7,28 @@ import ( "context" "fmt" - "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" + "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 @@ -45,15 +45,31 @@ 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(min int) validator.Set { +func SizeAtLeast(minVal int) sizeAtLeastValidator { return sizeAtLeastValidator{ - min: min, + min: minVal, } } diff --git a/setvalidator/size_at_least_example_test.go b/setvalidator/size_at_least_example_test.go index f31cdea1..97061f7a 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 3a59ba2e..e76da98f 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 4479f597..e0762acb 100644 --- a/setvalidator/size_at_most.go +++ b/setvalidator/size_at_most.go @@ -7,28 +7,28 @@ import ( "context" "fmt" - "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" + "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 @@ -45,15 +45,31 @@ 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(max int) validator.Set { +func SizeAtMost(maxVal int) sizeAtMostValidator { return sizeAtMostValidator{ - max: max, + max: maxVal, } } diff --git a/setvalidator/size_at_most_example_test.go b/setvalidator/size_at_most_example_test.go index 2386ecfe..23040965 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 d8849f65..0cebc4d7 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 15945a7b..9dd87044 100644 --- a/setvalidator/size_between.go +++ b/setvalidator/size_between.go @@ -7,30 +7,29 @@ import ( "context" "fmt" - "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" + "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 @@ -47,16 +46,32 @@ 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(min, max int) validator.Set { +func SizeBetween(minVal, maxVal int) sizeBetweenValidator { return sizeBetweenValidator{ - min: min, - max: max, + min: minVal, + max: maxVal, } } diff --git a/setvalidator/size_between_example_test.go b/setvalidator/size_between_example_test.go index 4a4bdec1..303d82ef 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 5a92b5af..0b24e7e4 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 67e1b7ca..ce0a9bf3 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 0ebaffae..38ae80a5 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 90a13b6a..4c5811eb 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 68422a9e..5ec51cb2 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 a793a0b0..2c326968 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 6435d253..3789344d 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 7f48bd5d..6eeca554 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 c70f4c05..f02c5fdd 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 aa17b39c..f38cdf30 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 eb4f2d3b..cf3db9b2 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 6bf7dce8..9639763b 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 aedb0949..f6a541dc 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 ca97f506..6f921f6c 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 c9524615..6e929136 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 c0c77a04..72ce0285 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 c3ae055b..57902355 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 7e5912ab..74efc12c 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 03d61d83..aa6be52a 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 39f820fc..2eede957 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 a3a99dc9..e2935631 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 4cab9975..756f9bbb 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 6616c13a..3a190e9d 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 94cf76f9..f591b7fe 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 6159eab5..a0f9931b 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 1db4a755..5ef990e1 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 f1d4b015..27f00724 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 1653d5f8..e02db80b 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 215f685f..fb188811 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 f34590c2..ce70d88d 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 791b9a56..05e22159 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 fa38f796..139edf38 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 49e12c74..9a0b04e5 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) + } + }) } } diff --git a/tools/go.mod b/tools/go.mod index 4a4affd2..9938d4ca 100644 --- a/tools/go.mod +++ b/tools/go.mod @@ -1,6 +1,6 @@ module tools -go 1.21 +go 1.22.7 require github.com/hashicorp/copywrite v0.19.0