From 73c6990dd6c67c3034e1bfcd2abd2a35f7836633 Mon Sep 17 00:00:00 2001 From: "hashicorp-tsccr[bot]" <129506189+hashicorp-tsccr[bot]@users.noreply.github.com> Date: Fri, 19 Jul 2024 10:31:16 -0400 Subject: [PATCH 01/18] Result of tsccr-helper -log-level=info gha update -latest . (#224) Co-authored-by: hashicorp-tsccr[bot] --- .github/workflows/ci-go.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-go.yml b/.github/workflows/ci-go.yml index 5ca47368..f098e053 100644 --- a/.github/workflows/ci-go.yml +++ b/.github/workflows/ci-go.yml @@ -37,7 +37,7 @@ jobs: - 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@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4 with: name: go-${{ matrix.go-version }}-coverage path: coverage.html From a400df62570d303a02c1cd09ed7a9f6cfcb457aa Mon Sep 17 00:00:00 2001 From: "hashicorp-tsccr[bot]" <129506189+hashicorp-tsccr[bot]@users.noreply.github.com> Date: Tue, 23 Jul 2024 12:59:20 -0400 Subject: [PATCH 02/18] Result of tsccr-helper -log-level=info gha update -latest . (#225) Co-authored-by: hashicorp-tsccr[bot] --- .github/workflows/ci-github-actions.yml | 2 +- .github/workflows/ci-go.yml | 4 ++-- .github/workflows/ci-goreleaser.yml | 2 +- .github/workflows/release.yml | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci-github-actions.yml b/.github/workflows/ci-github-actions.yml index cf96ddbd..ce2d45e5 100644 --- a/.github/workflows/ci-github-actions.yml +++ b/.github/workflows/ci-github-actions.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - - uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1 + - 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 f098e053..988dddf9 100644 --- a/.github/workflows/ci-go.yml +++ b/.github/workflows/ci-go.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - - uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1 + - uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 with: go-version-file: 'go.mod' - run: go mod download @@ -31,7 +31,7 @@ jobs: go-version: [ '1.22', '1.21' ] steps: - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - - uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1 + - uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 with: go-version: ${{ matrix.go-version }} - run: go mod download diff --git a/.github/workflows/ci-goreleaser.yml b/.github/workflows/ci-goreleaser.yml index 398bd155..0603e6b3 100644 --- a/.github/workflows/ci-goreleaser.yml +++ b/.github/workflows/ci-goreleaser.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - - uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1 + - 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/release.yml b/.github/workflows/release.yml index 3a6ec9c8..05421746 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -84,7 +84,7 @@ jobs: 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' From 1d6d280f4de8910fea4d67c445107e9189442b8d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 7 Aug 2024 09:23:21 -0400 Subject: [PATCH 03/18] build(deps): bump github.com/hashicorp/terraform-plugin-framework (#226) Bumps [github.com/hashicorp/terraform-plugin-framework](https://github.com/hashicorp/terraform-plugin-framework) from 1.10.0 to 1.11.0. - [Release notes](https://github.com/hashicorp/terraform-plugin-framework/releases) - [Changelog](https://github.com/hashicorp/terraform-plugin-framework/blob/main/CHANGELOG.md) - [Commits](https://github.com/hashicorp/terraform-plugin-framework/compare/v1.10.0...v1.11.0) --- updated-dependencies: - dependency-name: github.com/hashicorp/terraform-plugin-framework dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 8969b4e6..ca22a56f 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ toolchain go1.21.6 require ( github.com/google/go-cmp v0.6.0 - github.com/hashicorp/terraform-plugin-framework v1.10.0 + github.com/hashicorp/terraform-plugin-framework v1.11.0 github.com/hashicorp/terraform-plugin-go v0.23.0 ) diff --git a/go.sum b/go.sum index e7471426..bc50ac2c 100644 --- a/go.sum +++ b/go.sum @@ -7,8 +7,8 @@ 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-framework v1.11.0 h1:M7+9zBArexHFXDx/pKTxjE6n/2UCXY6b8FIq9ZYhwfE= +github.com/hashicorp/terraform-plugin-framework v1.11.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-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0= From 98f5260014d2f9d018e40d16a37b253d1fa472c5 Mon Sep 17 00:00:00 2001 From: "hashicorp-tsccr[bot]" <129506189+hashicorp-tsccr[bot]@users.noreply.github.com> Date: Tue, 13 Aug 2024 10:25:18 -0400 Subject: [PATCH 04/18] Result of tsccr-helper -log-level=info gha update -latest . (#227) Co-authored-by: hashicorp-tsccr[bot] --- .github/workflows/ci-go.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci-go.yml b/.github/workflows/ci-go.yml index 988dddf9..c080b794 100644 --- a/.github/workflows/ci-go.yml +++ b/.github/workflows/ci-go.yml @@ -21,7 +21,7 @@ jobs: with: go-version-file: 'go.mod' - run: go mod download - - uses: golangci/golangci-lint-action@a4f60bb28d35aeee14e6880718e0c85ff1882e64 # v6.0.1 + - uses: golangci/golangci-lint-action@aaa42aa0628b4ae2578232a66b541047968fac86 # v6.1.0 test: name: test (Go v${{ matrix.go-version }}) @@ -37,7 +37,7 @@ jobs: - run: go mod download - run: go test -coverprofile=coverage.out ./... - run: go tool cover -html=coverage.out -o coverage.html - - uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4 + - uses: actions/upload-artifact@89ef406dd8d7e03cfd12d9e0a4a378f454709029 # v4.3.5 with: name: go-${{ matrix.go-version }}-coverage path: coverage.html From 673fbef7d9e24ed3480a6f1084dde888177933a9 Mon Sep 17 00:00:00 2001 From: "hashicorp-tsccr[bot]" <129506189+hashicorp-tsccr[bot]@users.noreply.github.com> Date: Tue, 20 Aug 2024 16:00:25 -0400 Subject: [PATCH 05/18] SEC-090: Automated trusted workflow pinning (2024-08-19) (#228) * Result of tsccr-helper -log-level=info gha update -latest . * Resolve linter errors and warnings --------- Co-authored-by: hashicorp-tsccr[bot] Co-authored-by: Selena Goods --- .github/workflows/ci-go.yml | 2 +- .golangci.yml | 2 +- float32validator/at_least.go | 4 ++-- float32validator/at_most.go | 4 ++-- float32validator/between.go | 8 ++++---- float64validator/at_least.go | 4 ++-- float64validator/at_most.go | 4 ++-- float64validator/between.go | 8 ++++---- int32validator/at_least.go | 4 ++-- int32validator/at_most.go | 4 ++-- int32validator/between.go | 8 ++++---- int64validator/at_least.go | 4 ++-- int64validator/at_most.go | 4 ++-- int64validator/between.go | 8 ++++---- listvalidator/size_at_least.go | 7 ++++--- listvalidator/size_at_most.go | 7 ++++--- listvalidator/size_between.go | 9 +++++---- mapvalidator/size_at_least.go | 7 ++++--- mapvalidator/size_at_most.go | 7 ++++--- mapvalidator/size_between.go | 9 +++++---- setvalidator/size_at_least.go | 7 ++++--- setvalidator/size_at_most.go | 7 ++++--- setvalidator/size_between.go | 9 +++++---- 23 files changed, 73 insertions(+), 64 deletions(-) diff --git a/.github/workflows/ci-go.yml b/.github/workflows/ci-go.yml index c080b794..b19ef1a7 100644 --- a/.github/workflows/ci-go.yml +++ b/.github/workflows/ci-go.yml @@ -37,7 +37,7 @@ jobs: - run: go mod download - run: go test -coverprofile=coverage.out ./... - run: go tool cover -html=coverage.out -o coverage.html - - uses: actions/upload-artifact@89ef406dd8d7e03cfd12d9e0a4a378f454709029 # v4.3.5 + - uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 with: name: go-${{ matrix.go-version }}-coverage path: coverage.html 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/float32validator/at_least.go b/float32validator/at_least.go index db5192b8..548d2866 100644 --- a/float32validator/at_least.go +++ b/float32validator/at_least.go @@ -53,8 +53,8 @@ func (validator atLeastValidator) ValidateFloat32(ctx context.Context, request v // - 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) validator.Float32 { return atLeastValidator{ - min: min, + min: minVal, } } diff --git a/float32validator/at_most.go b/float32validator/at_most.go index 0d5e7785..e34e55cb 100644 --- a/float32validator/at_most.go +++ b/float32validator/at_most.go @@ -53,8 +53,8 @@ func (v atMostValidator) ValidateFloat32(ctx context.Context, request validator. // - 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) validator.Float32 { return atMostValidator{ - max: max, + max: maxVal, } } diff --git a/float32validator/between.go b/float32validator/between.go index cbc8a7d7..306d1cd2 100644 --- a/float32validator/between.go +++ b/float32validator/between.go @@ -53,13 +53,13 @@ func (v betweenValidator) ValidateFloat32(ctx context.Context, request validator // - 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 { +func Between(minVal, maxVal float32) validator.Float32 { + if minVal > maxVal { return nil } return betweenValidator{ - min: min, - max: max, + min: minVal, + max: maxVal, } } diff --git a/float64validator/at_least.go b/float64validator/at_least.go index 06cd02c9..a3d78c46 100644 --- a/float64validator/at_least.go +++ b/float64validator/at_least.go @@ -53,8 +53,8 @@ func (validator atLeastValidator) ValidateFloat64(ctx context.Context, request v // - 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) validator.Float64 { return atLeastValidator{ - min: min, + min: minVal, } } diff --git a/float64validator/at_most.go b/float64validator/at_most.go index cec57f78..fe1d63df 100644 --- a/float64validator/at_most.go +++ b/float64validator/at_most.go @@ -53,8 +53,8 @@ func (v atMostValidator) ValidateFloat64(ctx context.Context, request validator. // - 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) validator.Float64 { return atMostValidator{ - max: max, + max: maxVal, } } diff --git a/float64validator/between.go b/float64validator/between.go index ca1772e0..f08f18ce 100644 --- a/float64validator/between.go +++ b/float64validator/between.go @@ -53,13 +53,13 @@ func (v betweenValidator) ValidateFloat64(ctx context.Context, request validator // - 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 { +func Between(minVal, maxVal float64) validator.Float64 { + if minVal > maxVal { return nil } return betweenValidator{ - min: min, - max: max, + min: minVal, + max: maxVal, } } diff --git a/int32validator/at_least.go b/int32validator/at_least.go index 5aa839ea..681c1a88 100644 --- a/int32validator/at_least.go +++ b/int32validator/at_least.go @@ -51,8 +51,8 @@ func (v atLeastValidator) ValidateInt32(ctx context.Context, request validator.I // - 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) validator.Int32 { return atLeastValidator{ - min: min, + min: minVal, } } diff --git a/int32validator/at_most.go b/int32validator/at_most.go index 59ad7a57..afc1d309 100644 --- a/int32validator/at_most.go +++ b/int32validator/at_most.go @@ -51,8 +51,8 @@ func (v atMostValidator) ValidateInt32(ctx context.Context, request validator.In // - 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) validator.Int32 { return atMostValidator{ - max: max, + max: maxVal, } } diff --git a/int32validator/between.go b/int32validator/between.go index f17f37fd..18f0b041 100644 --- a/int32validator/between.go +++ b/int32validator/between.go @@ -51,13 +51,13 @@ func (v betweenValidator) ValidateInt32(ctx context.Context, request validator.I // - 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 { +func Between(minVal, maxVal int32) validator.Int32 { + if minVal > maxVal { return nil } return betweenValidator{ - min: min, - max: max, + min: minVal, + max: maxVal, } } diff --git a/int64validator/at_least.go b/int64validator/at_least.go index 092a9479..18fade05 100644 --- a/int64validator/at_least.go +++ b/int64validator/at_least.go @@ -51,8 +51,8 @@ func (v atLeastValidator) ValidateInt64(ctx context.Context, request validator.I // - 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) validator.Int64 { return atLeastValidator{ - min: min, + min: minVal, } } diff --git a/int64validator/at_most.go b/int64validator/at_most.go index b564a6e5..a7650502 100644 --- a/int64validator/at_most.go +++ b/int64validator/at_most.go @@ -51,8 +51,8 @@ func (v atMostValidator) ValidateInt64(ctx context.Context, request validator.In // - 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) validator.Int64 { return atMostValidator{ - max: max, + max: maxVal, } } diff --git a/int64validator/between.go b/int64validator/between.go index 879aeff0..f018bb93 100644 --- a/int64validator/between.go +++ b/int64validator/between.go @@ -51,13 +51,13 @@ func (v betweenValidator) ValidateInt64(ctx context.Context, request validator.I // - 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 { +func Between(minVal, maxVal int64) validator.Int64 { + if minVal > maxVal { return nil } return betweenValidator{ - min: min, - max: max, + min: minVal, + max: maxVal, } } diff --git a/listvalidator/size_at_least.go b/listvalidator/size_at_least.go index bfe35e7d..b346e185 100644 --- a/listvalidator/size_at_least.go +++ b/listvalidator/size_at_least.go @@ -7,8 +7,9 @@ import ( "context" "fmt" - "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" "github.com/hashicorp/terraform-plugin-framework/schema/validator" + + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" ) var _ validator.List = sizeAtLeastValidator{} @@ -52,8 +53,8 @@ func (v sizeAtLeastValidator) ValidateList(ctx context.Context, req validator.Li // - 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) validator.List { return sizeAtLeastValidator{ - min: min, + min: minVal, } } diff --git a/listvalidator/size_at_most.go b/listvalidator/size_at_most.go index f3e7b36d..4e7ea27f 100644 --- a/listvalidator/size_at_most.go +++ b/listvalidator/size_at_most.go @@ -7,8 +7,9 @@ import ( "context" "fmt" - "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" "github.com/hashicorp/terraform-plugin-framework/schema/validator" + + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" ) var _ validator.List = sizeAtMostValidator{} @@ -52,8 +53,8 @@ func (v sizeAtMostValidator) ValidateList(ctx context.Context, req validator.Lis // - 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) validator.List { return sizeAtMostValidator{ - max: max, + max: maxVal, } } diff --git a/listvalidator/size_between.go b/listvalidator/size_between.go index 32c34d9e..6bd4b892 100644 --- a/listvalidator/size_between.go +++ b/listvalidator/size_between.go @@ -7,8 +7,9 @@ import ( "context" "fmt" - "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" "github.com/hashicorp/terraform-plugin-framework/schema/validator" + + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" ) var _ validator.List = sizeBetweenValidator{} @@ -54,9 +55,9 @@ func (v sizeBetweenValidator) ValidateList(ctx context.Context, req validator.Li // - 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) validator.List { return sizeBetweenValidator{ - min: min, - max: max, + min: minVal, + max: maxVal, } } diff --git a/mapvalidator/size_at_least.go b/mapvalidator/size_at_least.go index d6a96cd3..2ddc9800 100644 --- a/mapvalidator/size_at_least.go +++ b/mapvalidator/size_at_least.go @@ -7,8 +7,9 @@ import ( "context" "fmt" - "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" "github.com/hashicorp/terraform-plugin-framework/schema/validator" + + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" ) var _ validator.Map = sizeAtLeastValidator{} @@ -52,8 +53,8 @@ func (v sizeAtLeastValidator) ValidateMap(ctx context.Context, req validator.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) validator.Map { return sizeAtLeastValidator{ - min: min, + min: minVal, } } diff --git a/mapvalidator/size_at_most.go b/mapvalidator/size_at_most.go index 5a926187..f41529e6 100644 --- a/mapvalidator/size_at_most.go +++ b/mapvalidator/size_at_most.go @@ -7,8 +7,9 @@ import ( "context" "fmt" - "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" "github.com/hashicorp/terraform-plugin-framework/schema/validator" + + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" ) var _ validator.Map = sizeAtMostValidator{} @@ -52,8 +53,8 @@ func (v sizeAtMostValidator) ValidateMap(ctx context.Context, req validator.MapR // - 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) validator.Map { return sizeAtMostValidator{ - max: max, + max: maxVal, } } diff --git a/mapvalidator/size_between.go b/mapvalidator/size_between.go index c7255c99..9d6e761f 100644 --- a/mapvalidator/size_between.go +++ b/mapvalidator/size_between.go @@ -7,8 +7,9 @@ import ( "context" "fmt" - "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" "github.com/hashicorp/terraform-plugin-framework/schema/validator" + + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" ) var _ validator.Map = sizeBetweenValidator{} @@ -54,9 +55,9 @@ func (v sizeBetweenValidator) ValidateMap(ctx context.Context, req validator.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) validator.Map { return sizeBetweenValidator{ - min: min, - max: max, + min: minVal, + max: maxVal, } } diff --git a/setvalidator/size_at_least.go b/setvalidator/size_at_least.go index d010b858..c27dc14a 100644 --- a/setvalidator/size_at_least.go +++ b/setvalidator/size_at_least.go @@ -7,8 +7,9 @@ import ( "context" "fmt" - "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" "github.com/hashicorp/terraform-plugin-framework/schema/validator" + + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" ) var _ validator.Set = sizeAtLeastValidator{} @@ -52,8 +53,8 @@ func (v sizeAtLeastValidator) ValidateSet(ctx context.Context, req validator.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) validator.Set { return sizeAtLeastValidator{ - min: min, + min: minVal, } } diff --git a/setvalidator/size_at_most.go b/setvalidator/size_at_most.go index 4479f597..1f1be7de 100644 --- a/setvalidator/size_at_most.go +++ b/setvalidator/size_at_most.go @@ -7,8 +7,9 @@ import ( "context" "fmt" - "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" "github.com/hashicorp/terraform-plugin-framework/schema/validator" + + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" ) var _ validator.Set = sizeAtMostValidator{} @@ -52,8 +53,8 @@ func (v sizeAtMostValidator) ValidateSet(ctx context.Context, req validator.SetR // - 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) validator.Set { return sizeAtMostValidator{ - max: max, + max: maxVal, } } diff --git a/setvalidator/size_between.go b/setvalidator/size_between.go index 15945a7b..7e717895 100644 --- a/setvalidator/size_between.go +++ b/setvalidator/size_between.go @@ -7,8 +7,9 @@ import ( "context" "fmt" - "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" "github.com/hashicorp/terraform-plugin-framework/schema/validator" + + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" ) var _ validator.Set = sizeBetweenValidator{} @@ -54,9 +55,9 @@ func (v sizeBetweenValidator) ValidateSet(ctx context.Context, req validator.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) validator.Set { return sizeBetweenValidator{ - min: min, - max: max, + min: minVal, + max: maxVal, } } From ab5bb62479ed7d2af9ba88d9435f90748cab17a0 Mon Sep 17 00:00:00 2001 From: "hashicorp-tsccr[bot]" <129506189+hashicorp-tsccr[bot]@users.noreply.github.com> Date: Mon, 9 Sep 2024 09:45:38 -0400 Subject: [PATCH 06/18] Result of tsccr-helper -log-level=info gha update -latest . (#230) Co-authored-by: hashicorp-tsccr[bot] --- .github/workflows/ci-go.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-go.yml b/.github/workflows/ci-go.yml index b19ef1a7..f3fbe598 100644 --- a/.github/workflows/ci-go.yml +++ b/.github/workflows/ci-go.yml @@ -37,7 +37,7 @@ jobs: - run: go mod download - run: go test -coverprofile=coverage.out ./... - run: go tool cover -html=coverage.out -o coverage.html - - uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 + - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: go-${{ matrix.go-version }}-coverage path: coverage.html From be3fc06d30143dda1dc7dd0daeac6bd75d878705 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Mon, 9 Sep 2024 10:37:36 -0400 Subject: [PATCH 07/18] all: Bump minimum Go module version to 1.22 (#229) * all: Bump minimum Go module version to 1.22 * add changelog --- .changes/unreleased/NOTES-20240906-162141.yaml | 7 +++++++ .github/workflows/ci-go.yml | 2 +- README.md | 2 +- go.mod | 4 ++-- tools/go.mod | 2 +- 5 files changed, 12 insertions(+), 5 deletions(-) create mode 100644 .changes/unreleased/NOTES-20240906-162141.yaml diff --git a/.changes/unreleased/NOTES-20240906-162141.yaml b/.changes/unreleased/NOTES-20240906-162141.yaml new file mode 100644 index 00000000..79d84861 --- /dev/null +++ b/.changes/unreleased/NOTES-20240906-162141.yaml @@ -0,0 +1,7 @@ +kind: NOTES +body: '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.' +time: 2024-09-06T16:21:41.853159-04:00 +custom: + Issue: "229" diff --git a/.github/workflows/ci-go.yml b/.github/workflows/ci-go.yml index f3fbe598..3ab68dd3 100644 --- a/.github/workflows/ci-go.yml +++ b/.github/workflows/ci-go.yml @@ -28,7 +28,7 @@ jobs: 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@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 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/go.mod b/go.mod index ca22a56f..24c29448 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,8 @@ 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 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 From 609df6fe2e4664af29559c6fe5768b69b1b18d9f Mon Sep 17 00:00:00 2001 From: Service Account - Terraform Provider DevEx <100357958+hc-github-team-tf-provider-devex@users.noreply.github.com> Date: Mon, 16 Sep 2024 09:32:22 -0400 Subject: [PATCH 08/18] [CI] Update issue comment triage workflow file From bb7349875a5cae637e22ab2860c81843c8f608e6 Mon Sep 17 00:00:00 2001 From: Service Account - Terraform Provider DevEx <100357958+hc-github-team-tf-provider-devex@users.noreply.github.com> Date: Mon, 16 Sep 2024 09:33:44 -0400 Subject: [PATCH 09/18] [CI] Update lock workflow file From 08bf4f2437483edaaf595d26ae505dcba2e2bf20 Mon Sep 17 00:00:00 2001 From: Service Account - Terraform Provider DevEx <100357958+hc-github-team-tf-provider-devex@users.noreply.github.com> Date: Mon, 16 Sep 2024 09:34:24 -0400 Subject: [PATCH 10/18] [CI] terraform-devex-repos automation From 9ec018a0e3f8836bbdd1ec1442c29043953c885a Mon Sep 17 00:00:00 2001 From: Service Account - Terraform Provider DevEx <100357958+hc-github-team-tf-provider-devex@users.noreply.github.com> Date: Mon, 16 Sep 2024 09:34:43 -0400 Subject: [PATCH 11/18] [CI] terraform-devex-repos automation From 7cdde0b96c8d6d284b3214a89767ae00ff5cf7cc Mon Sep 17 00:00:00 2001 From: Service Account - Terraform Provider DevEx <100357958+hc-github-team-tf-provider-devex@users.noreply.github.com> Date: Mon, 16 Sep 2024 09:35:34 -0400 Subject: [PATCH 12/18] [CI] terraform-devex-repos automation From 7979126db46cf5c36670d7e6fceda7d1ea573409 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 19 Sep 2024 09:22:51 -0400 Subject: [PATCH 13/18] build(deps): bump github.com/hashicorp/terraform-plugin-framework (#233) Bumps [github.com/hashicorp/terraform-plugin-framework](https://github.com/hashicorp/terraform-plugin-framework) from 1.11.0 to 1.12.0. - [Release notes](https://github.com/hashicorp/terraform-plugin-framework/releases) - [Changelog](https://github.com/hashicorp/terraform-plugin-framework/blob/main/CHANGELOG.md) - [Commits](https://github.com/hashicorp/terraform-plugin-framework/compare/v1.11.0...v1.12.0) --- updated-dependencies: - dependency-name: github.com/hashicorp/terraform-plugin-framework dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 6 +++--- go.sum | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index 24c29448..1ebd9191 100644 --- a/go.mod +++ b/go.mod @@ -6,8 +6,8 @@ toolchain go1.22.7 require ( github.com/google/go-cmp v0.6.0 - github.com/hashicorp/terraform-plugin-framework v1.11.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 bc50ac2c..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.11.0 h1:M7+9zBArexHFXDx/pKTxjE6n/2UCXY6b8FIq9ZYhwfE= -github.com/hashicorp/terraform-plugin-framework v1.11.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= From 974015b9c8f707214829fe59c41c97a3d3b8ca66 Mon Sep 17 00:00:00 2001 From: Jared Baker Date: Fri, 20 Sep 2024 17:29:35 -0400 Subject: [PATCH 14/18] boolvalidator: add `Equals` validator (#232) * boolvalidator: add Equals validator This validator can be used in cases where non-null boolean value should be exactly `true` or exactly `false`. ```console % go test -count=1 ./boolvalidator/... ok github.com/hashicorp/terraform-plugin-framework-validators/boolvalidator 0.251s ``` * Update boolvalidator/equals.go Co-authored-by: Austin Valle * chore: changelog --- .../unreleased/FEATURES-20240920-164852.yaml | 5 ++ boolvalidator/equals.go | 51 ++++++++++++++ boolvalidator/equals_test.go | 69 +++++++++++++++++++ 3 files changed, 125 insertions(+) create mode 100644 .changes/unreleased/FEATURES-20240920-164852.yaml create mode 100644 boolvalidator/equals.go create mode 100644 boolvalidator/equals_test.go diff --git a/.changes/unreleased/FEATURES-20240920-164852.yaml b/.changes/unreleased/FEATURES-20240920-164852.yaml new file mode 100644 index 00000000..0a90941d --- /dev/null +++ b/.changes/unreleased/FEATURES-20240920-164852.yaml @@ -0,0 +1,5 @@ +kind: FEATURES +body: 'boolvalidator: Added `Equals` validator' +time: 2024-09-20T16:48:52.562758-04:00 +custom: + Issue: "232" diff --git a/boolvalidator/equals.go b/boolvalidator/equals.go new file mode 100644 index 00000000..b63da2a4 --- /dev/null +++ b/boolvalidator/equals.go @@ -0,0 +1,51 @@ +// 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/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +var _ validator.Bool = 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(), + )) + } +} + +// Equals returns an AttributeValidator which ensures that the configured boolean attribute +// matches the given `value`. Null (unconfigured) and unknown (known after apply) values are skipped. +func Equals(value bool) validator.Bool { + return equalsValidator{ + value: types.BoolValue(value), + } +} diff --git a/boolvalidator/equals_test.go b/boolvalidator/equals_test.go new file mode 100644 index 00000000..8d87af96 --- /dev/null +++ b/boolvalidator/equals_test.go @@ -0,0 +1,69 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package boolvalidator_test + +import ( + "context" + "testing" + + "github.com/hashicorp/terraform-plugin-framework-validators/boolvalidator" + "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 + validator validator.Bool + expErrors int + } + + testCases := map[string]testCase{ + "simple-match": { + in: types.BoolValue(true), + validator: boolvalidator.Equals(true), + expErrors: 0, + }, + "simple-mismatch": { + in: types.BoolValue(false), + validator: boolvalidator.Equals(true), + expErrors: 1, + }, + "skip-validation-on-null": { + in: types.BoolNull(), + validator: boolvalidator.Equals(true), + expErrors: 0, + }, + "skip-validation-on-unknown": { + in: types.BoolUnknown(), + validator: boolvalidator.Equals(true), + expErrors: 0, + }, + } + + for name, test := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + req := validator.BoolRequest{ + ConfigValue: test.in, + } + res := validator.BoolResponse{} + test.validator.ValidateBool(context.TODO(), req, &res) + + if test.expErrors > 0 && !res.Diagnostics.HasError() { + t.Fatalf("expected %d error(s), got none", test.expErrors) + } + + 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 test.expErrors == 0 && res.Diagnostics.HasError() { + t.Fatalf("expected no error(s), got %d: %v", res.Diagnostics.ErrorsCount(), res.Diagnostics) + } + }) + } +} From 8beb21856fdd36e5f84825e78fe10fa49212c41d Mon Sep 17 00:00:00 2001 From: "hashicorp-tsccr[bot]" <129506189+hashicorp-tsccr[bot]@users.noreply.github.com> Date: Mon, 7 Oct 2024 09:56:54 -0400 Subject: [PATCH 15/18] Result of tsccr-helper -log-level=info gha update -latest . (#236) Co-authored-by: hashicorp-tsccr[bot] --- .github/workflows/ci-changie.yml | 2 +- .github/workflows/ci-github-actions.yml | 2 +- .github/workflows/ci-go.yml | 4 ++-- .github/workflows/ci-goreleaser.yml | 2 +- .github/workflows/compliance.yml | 2 +- .github/workflows/release.yml | 6 +++--- 6 files changed, 9 insertions(+), 9 deletions(-) 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 ce2d45e5..2358e4ad 100644 --- a/.github/workflows/ci-github-actions.yml +++ b/.github/workflows/ci-github-actions.yml @@ -13,7 +13,7 @@ jobs: actionlint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 with: go-version-file: 'go.mod' diff --git a/.github/workflows/ci-go.yml b/.github/workflows/ci-go.yml index 3ab68dd3..d607bf7a 100644 --- a/.github/workflows/ci-go.yml +++ b/.github/workflows/ci-go.yml @@ -16,7 +16,7 @@ jobs: golangci-lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 with: go-version-file: 'go.mod' @@ -30,7 +30,7 @@ jobs: matrix: go-version: [ '1.23', '1.22' ] steps: - - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 with: go-version: ${{ matrix.go-version }} diff --git a/.github/workflows/ci-goreleaser.yml b/.github/workflows/ci-goreleaser.yml index 0603e6b3..d978baa4 100644 --- a/.github/workflows/ci-goreleaser.yml +++ b/.github/workflows/ci-goreleaser.yml @@ -14,7 +14,7 @@ jobs: check: runs-on: ubuntu-latest steps: - - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 with: go-version-file: 'go.mod' 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 05421746..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,7 +79,7 @@ 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 From 6acd967836e5ebb35848d35ca6812b38a5978084 Mon Sep 17 00:00:00 2001 From: "hashicorp-tsccr[bot]" <129506189+hashicorp-tsccr[bot]@users.noreply.github.com> Date: Mon, 14 Oct 2024 09:51:05 -0400 Subject: [PATCH 16/18] Result of tsccr-helper -log-level=info gha update -latest . (#237) Co-authored-by: hashicorp-tsccr[bot] --- .github/workflows/ci-go.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-go.yml b/.github/workflows/ci-go.yml index d607bf7a..60928e9b 100644 --- a/.github/workflows/ci-go.yml +++ b/.github/workflows/ci-go.yml @@ -21,7 +21,7 @@ jobs: with: go-version-file: 'go.mod' - run: go mod download - - uses: golangci/golangci-lint-action@aaa42aa0628b4ae2578232a66b541047968fac86 # v6.1.0 + - uses: golangci/golangci-lint-action@971e284b6050e8a5849b72094c50ab08da042db8 # v6.1.1 test: name: test (Go v${{ matrix.go-version }}) From aa6a2de506f8f9bcc7ed44cdbdb32280039c8232 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Thu, 17 Oct 2024 15:04:05 -0400 Subject: [PATCH 17/18] all: Implement function parameter validation interfaces (#238) * stringvalidator: implement parameter interface * boolvalidator: implement parameter interface * float32validator: implement parameter interface * float64validator: implement parameter interface * numbervalidator: implement parameter interface * int32validator: implement parameter interface * int64validator: implement parameter interface * listvalidator: implement parameter interface validation * setvalidator: implement parameter interface * mapvalidator: implement parameter interface * add changelogs --- .../ENHANCEMENTS-20241014-121220.yaml | 6 ++ .../unreleased/NOTES-20241014-121711.yaml | 7 ++ boolvalidator/doc.go | 2 +- boolvalidator/equals.go | 23 ++++++- boolvalidator/equals_test.go | 60 ++++++++++------- float32validator/at_least.go | 27 ++++++-- float32validator/at_least_example_test.go | 15 +++++ float32validator/at_least_test.go | 22 +++++- float32validator/at_most.go | 27 ++++++-- float32validator/at_most_example_test.go | 15 +++++ float32validator/at_most_test.go | 22 +++++- float32validator/between.go | 62 ++++++++++++++--- float32validator/between_example_test.go | 15 +++++ float32validator/between_test.go | 29 +++++++- float32validator/doc.go | 2 +- float32validator/none_of.go | 30 ++++++++- float32validator/none_of_example_test.go | 15 +++++ float32validator/none_of_test.go | 59 +++++++++------- float32validator/one_of.go | 28 +++++++- float32validator/one_of_example_test.go | 15 +++++ float32validator/one_of_test.go | 59 +++++++++------- float64validator/at_least.go | 27 ++++++-- float64validator/at_least_example_test.go | 15 +++++ float64validator/at_least_test.go | 22 +++++- float64validator/at_most.go | 27 ++++++-- float64validator/at_most_example_test.go | 15 +++++ float64validator/at_most_test.go | 22 +++++- float64validator/between.go | 62 ++++++++++++++--- float64validator/between_example_test.go | 15 +++++ float64validator/between_test.go | 32 ++++++++- float64validator/doc.go | 2 +- float64validator/none_of.go | 30 ++++++++- float64validator/none_of_example_test.go | 15 +++++ float64validator/none_of_test.go | 59 +++++++++------- float64validator/one_of.go | 28 +++++++- float64validator/one_of_example_test.go | 15 +++++ float64validator/one_of_test.go | 59 +++++++++------- helpers/validatordiag/diag.go | 13 ++++ helpers/validatorfuncerr/doc.go | 5 ++ helpers/validatorfuncerr/funcerr.go | 45 +++++++++++++ int32validator/at_least.go | 25 +++++-- int32validator/at_least_example_test.go | 15 +++++ int32validator/at_least_test.go | 22 +++++- int32validator/at_most.go | 25 +++++-- int32validator/at_most_example_test.go | 15 +++++ int32validator/at_most_test.go | 22 +++++- int32validator/between.go | 60 ++++++++++++++--- int32validator/between_example_test.go | 15 +++++ int32validator/between_test.go | 29 +++++++- int32validator/doc.go | 2 +- int32validator/none_of.go | 30 ++++++++- int32validator/none_of_example_test.go | 15 +++++ int32validator/none_of_test.go | 59 +++++++++------- int32validator/one_of.go | 28 +++++++- int32validator/one_of_example_test.go | 15 +++++ int32validator/one_of_test.go | 62 ++++++++++------- int64validator/at_least.go | 25 +++++-- int64validator/at_least_example_test.go | 15 +++++ int64validator/at_least_test.go | 22 +++++- int64validator/at_most.go | 25 +++++-- int64validator/at_most_example_test.go | 15 +++++ int64validator/at_most_test.go | 22 +++++- int64validator/between.go | 60 ++++++++++++++--- int64validator/between_example_test.go | 15 +++++ int64validator/between_test.go | 29 +++++++- int64validator/doc.go | 2 +- int64validator/none_of.go | 30 ++++++++- int64validator/none_of_example_test.go | 15 +++++ int64validator/none_of_test.go | 59 +++++++++------- int64validator/one_of.go | 28 +++++++- int64validator/one_of_example_test.go | 15 +++++ int64validator/one_of_test.go | 59 +++++++++------- listvalidator/doc.go | 2 +- listvalidator/size_at_least.go | 27 ++++++-- listvalidator/size_at_least_example_test.go | 15 +++++ listvalidator/size_at_least_test.go | 23 ++++++- listvalidator/size_at_most.go | 27 ++++++-- listvalidator/size_at_most_example_test.go | 15 +++++ listvalidator/size_at_most_test.go | 23 ++++++- listvalidator/size_between.go | 28 ++++++-- listvalidator/size_between_example_test.go | 15 +++++ listvalidator/size_between_test.go | 23 ++++++- listvalidator/unique_values.go | 43 ++++++++++-- listvalidator/unique_values_example_test.go | 15 +++++ listvalidator/unique_values_test.go | 56 +++++++++++++++- mapvalidator/doc.go | 2 +- mapvalidator/size_at_least.go | 27 ++++++-- mapvalidator/size_at_least_example_test.go | 15 +++++ mapvalidator/size_at_least_test.go | 23 ++++++- mapvalidator/size_at_most.go | 27 ++++++-- mapvalidator/size_at_most_example_test.go | 15 +++++ mapvalidator/size_at_most_test.go | 23 ++++++- mapvalidator/size_between.go | 28 ++++++-- mapvalidator/size_between_example_test.go | 15 +++++ mapvalidator/size_between_test.go | 23 ++++++- numbervalidator/doc.go | 2 +- numbervalidator/none_of.go | 30 ++++++++- numbervalidator/none_of_example_test.go | 21 ++++++ numbervalidator/none_of_test.go | 59 +++++++++------- numbervalidator/one_of.go | 28 +++++++- numbervalidator/one_of_example_test.go | 21 ++++++ numbervalidator/one_of_test.go | 59 +++++++++------- setvalidator/doc.go | 2 +- setvalidator/size_at_least.go | 27 ++++++-- setvalidator/size_at_least_example_test.go | 15 +++++ setvalidator/size_at_least_test.go | 23 ++++++- setvalidator/size_at_most.go | 27 ++++++-- setvalidator/size_at_most_example_test.go | 15 +++++ setvalidator/size_at_most_test.go | 23 ++++++- setvalidator/size_between.go | 28 ++++++-- setvalidator/size_between_example_test.go | 15 +++++ setvalidator/size_between_test.go | 23 ++++++- stringvalidator/doc.go | 2 +- stringvalidator/length_at_least.go | 64 +++++++++++++++--- .../length_at_least_example_test.go | 15 +++++ stringvalidator/length_at_least_test.go | 28 +++++++- stringvalidator/length_at_most.go | 64 +++++++++++++++--- .../length_at_most_example_test.go | 15 +++++ stringvalidator/length_at_most_test.go | 28 +++++++- stringvalidator/length_between.go | 64 +++++++++++++++--- .../length_between_example_test.go | 15 +++++ stringvalidator/length_between_test.go | 35 +++++++++- stringvalidator/none_of.go | 30 ++++++++- stringvalidator/none_of_case_insensitive.go | 28 +++++++- .../none_of_case_insensitive_test.go | 65 +++++++++++------- stringvalidator/none_of_example_test.go | 15 +++++ stringvalidator/none_of_test.go | 64 +++++++++++------- stringvalidator/one_of.go | 28 +++++++- stringvalidator/one_of_case_insensitive.go | 28 +++++++- .../one_of_case_insensitive_test.go | 64 +++++++++++------- stringvalidator/one_of_example_test.go | 15 +++++ stringvalidator/one_of_test.go | 65 +++++++++++------- stringvalidator/regex_matches.go | 27 ++++++-- stringvalidator/regex_matches_example_test.go | 18 +++++ stringvalidator/regex_matches_test.go | 23 ++++++- stringvalidator/utf8_length_at_least.go | 66 +++++++++++++++--- .../utf8_length_at_least_example_test.go | 15 +++++ stringvalidator/utf8_length_at_least_test.go | 28 +++++++- stringvalidator/utf8_length_at_most.go | 66 +++++++++++++++--- .../utf8_length_at_most_example_test.go | 15 +++++ stringvalidator/utf8_length_at_most_test.go | 28 +++++++- stringvalidator/utf8_length_between.go | 67 ++++++++++++++++--- .../utf8_length_between_example_test.go | 16 +++++ stringvalidator/utf8_length_between_test.go | 41 +++++++++++- 144 files changed, 3333 insertions(+), 660 deletions(-) create mode 100644 .changes/unreleased/ENHANCEMENTS-20241014-121220.yaml create mode 100644 .changes/unreleased/NOTES-20241014-121711.yaml create mode 100644 helpers/validatorfuncerr/doc.go create mode 100644 helpers/validatorfuncerr/funcerr.go diff --git a/.changes/unreleased/ENHANCEMENTS-20241014-121220.yaml b/.changes/unreleased/ENHANCEMENTS-20241014-121220.yaml new file mode 100644 index 00000000..56a4511f --- /dev/null +++ b/.changes/unreleased/ENHANCEMENTS-20241014-121220.yaml @@ -0,0 +1,6 @@ +kind: ENHANCEMENTS +body: 'all: Implemented parameter interfaces for all value-based validators. This + allows these validators to be used with provider-defined functions.' +time: 2024-10-14T12:12:20.607373-04:00 +custom: + Issue: "235" diff --git a/.changes/unreleased/NOTES-20241014-121711.yaml b/.changes/unreleased/NOTES-20241014-121711.yaml new file mode 100644 index 00000000..392757d5 --- /dev/null +++ b/.changes/unreleased/NOTES-20241014-121711.yaml @@ -0,0 +1,7 @@ +kind: NOTES +body: 'all: Previously, creating validators with invalid data would result in a `nil` + value being returned and a panic from `terraform-plugin-framework`. This has been + updated to return an implementation diagnostic referencing the invalid data/validator during config validation.' +time: 2024-10-14T12:17:11.811926-04:00 +custom: + Issue: "235" diff --git a/boolvalidator/doc.go b/boolvalidator/doc.go index 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 index b63da2a4..755e9769 100644 --- a/boolvalidator/equals.go +++ b/boolvalidator/equals.go @@ -8,11 +8,14 @@ import ( "fmt" "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatorfuncerr" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" ) var _ validator.Bool = equalsValidator{} +var _ function.BoolParameterValidator = equalsValidator{} type equalsValidator struct { value types.Bool @@ -42,9 +45,25 @@ func (v equalsValidator) ValidateBool(ctx context.Context, req validator.BoolReq } } -// Equals returns an AttributeValidator which ensures that the configured boolean attribute +func (v equalsValidator) ValidateParameterBool(ctx context.Context, req function.BoolParameterValidatorRequest, resp *function.BoolParameterValidatorResponse) { + if req.Value.IsNull() || req.Value.IsUnknown() { + return + } + + value := req.Value + + if !value.Equal(v.value) { + resp.Error = validatorfuncerr.InvalidParameterValueMatchFuncError( + req.ArgumentPosition, + v.Description(ctx), + value.String(), + ) + } +} + +// Equals returns an AttributeValidator which ensures that the configured boolean attribute or function parameter // matches the given `value`. Null (unconfigured) and unknown (known after apply) values are skipped. -func Equals(value bool) validator.Bool { +func Equals(value bool) equalsValidator { return equalsValidator{ value: types.BoolValue(value), } diff --git a/boolvalidator/equals_test.go b/boolvalidator/equals_test.go index 8d87af96..ed2671e1 100644 --- a/boolvalidator/equals_test.go +++ b/boolvalidator/equals_test.go @@ -5,9 +5,11 @@ package boolvalidator_test import ( "context" + "fmt" "testing" "github.com/hashicorp/terraform-plugin-framework-validators/boolvalidator" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" ) @@ -16,53 +18,65 @@ func TestEqualsValidator(t *testing.T) { t.Parallel() type testCase struct { - in types.Bool - validator validator.Bool - expErrors int + in types.Bool + equalsValue bool + expectError bool } testCases := map[string]testCase{ "simple-match": { - in: types.BoolValue(true), - validator: boolvalidator.Equals(true), - expErrors: 0, + in: types.BoolValue(true), + equalsValue: true, }, "simple-mismatch": { - in: types.BoolValue(false), - validator: boolvalidator.Equals(true), - expErrors: 1, + in: types.BoolValue(false), + equalsValue: true, + expectError: true, }, "skip-validation-on-null": { - in: types.BoolNull(), - validator: boolvalidator.Equals(true), - expErrors: 0, + in: types.BoolNull(), + equalsValue: true, }, "skip-validation-on-unknown": { - in: types.BoolUnknown(), - validator: boolvalidator.Equals(true), - expErrors: 0, + in: types.BoolUnknown(), + equalsValue: true, }, } for name, test := range testCases { - t.Run(name, func(t *testing.T) { + name, test := name, test + + t.Run(fmt.Sprintf("ValidateBool - %s", name), func(t *testing.T) { t.Parallel() req := validator.BoolRequest{ ConfigValue: test.in, } res := validator.BoolResponse{} - test.validator.ValidateBool(context.TODO(), req, &res) + boolvalidator.Equals(test.equalsValue).ValidateBool(context.TODO(), req, &res) + + if !res.Diagnostics.HasError() && test.expectError { + t.Fatal("expected error, got no error") + } - if test.expErrors > 0 && !res.Diagnostics.HasError() { - t.Fatalf("expected %d error(s), got none", test.expErrors) + if res.Diagnostics.HasError() && !test.expectError { + t.Fatalf("got unexpected error: %s", res.Diagnostics) + } + }) + + t.Run(fmt.Sprintf("ValidateParameterBool - %s", name), func(t *testing.T) { + t.Parallel() + req := function.BoolParameterValidatorRequest{ + Value: test.in, } + res := function.BoolParameterValidatorResponse{} + boolvalidator.Equals(test.equalsValue).ValidateParameterBool(context.TODO(), req, &res) - if test.expErrors > 0 && test.expErrors != res.Diagnostics.ErrorsCount() { - t.Fatalf("expected %d error(s), got %d: %v", test.expErrors, res.Diagnostics.ErrorsCount(), res.Diagnostics) + if res.Error == nil && test.expectError { + t.Fatal("expected error, got no error") } - if test.expErrors == 0 && res.Diagnostics.HasError() { - t.Fatalf("expected no error(s), got %d: %v", res.Diagnostics.ErrorsCount(), res.Diagnostics) + if res.Error != nil && !test.expectError { + t.Fatalf("got unexpected error: %s", res.Error) } }) } diff --git a/float32validator/at_least.go b/float32validator/at_least.go index 548d2866..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,14 +45,30 @@ func (validator atLeastValidator) ValidateFloat32(ctx context.Context, request v } } +func (validator atLeastValidator) ValidateParameterFloat32(ctx context.Context, request function.Float32ParameterValidatorRequest, response *function.Float32ParameterValidatorResponse) { + if request.Value.IsNull() || request.Value.IsUnknown() { + return + } + + value := request.Value.ValueFloat32() + + if value < validator.min { + response.Error = validatorfuncerr.InvalidParameterValueFuncError( + request.ArgumentPosition, + validator.Description(ctx), + fmt.Sprintf("%f", value), + ) + } +} + // AtLeast returns an AttributeValidator which ensures that any configured -// attribute value: +// attribute or function parameter value: // // - Is a number, which can be represented by a 32-bit floating point. // - Is greater than or equal to the given minimum. // // Null (unconfigured) and unknown (known after apply) values are skipped. -func AtLeast(minVal float32) validator.Float32 { +func AtLeast(minVal float32) atLeastValidator { return atLeastValidator{ min: minVal, } diff --git a/float32validator/at_least_example_test.go b/float32validator/at_least_example_test.go index 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 e34e55cb..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,14 +45,30 @@ func (v atMostValidator) ValidateFloat32(ctx context.Context, request validator. } } +func (v atMostValidator) ValidateParameterFloat32(ctx context.Context, request function.Float32ParameterValidatorRequest, response *function.Float32ParameterValidatorResponse) { + if request.Value.IsNull() || request.Value.IsUnknown() { + return + } + + value := request.Value.ValueFloat32() + + if value > v.max { + response.Error = validatorfuncerr.InvalidParameterValueFuncError( + request.ArgumentPosition, + v.Description(ctx), + fmt.Sprintf("%f", value), + ) + } +} + // AtMost returns an AttributeValidator which ensures that any configured -// attribute value: +// attribute or function parameter value: // // - Is a number, which can be represented by a 32-bit floating point. // - Is less than or equal to the given maximum. // // Null (unconfigured) and unknown (known after apply) values are skipped. -func AtMost(maxVal float32) validator.Float32 { +func AtMost(maxVal float32) atMostValidator { return atMostValidator{ max: maxVal, } diff --git a/float32validator/at_most_example_test.go b/float32validator/at_most_example_test.go index 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 306d1cd2..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,18 +62,44 @@ func (v betweenValidator) ValidateFloat32(ctx context.Context, request validator } } +func (v betweenValidator) ValidateParameterFloat32(ctx context.Context, request function.Float32ParameterValidatorRequest, response *function.Float32ParameterValidatorResponse) { + // Return an error if the validator has been created in an invalid state + if v.min > v.max { + response.Error = validatorfuncerr.InvalidValidatorUsageFuncError( + request.ArgumentPosition, + "Between", + v.invalidUsageMessage(), + ) + + return + } + + if request.Value.IsNull() || request.Value.IsUnknown() { + return + } + + value := request.Value.ValueFloat32() + + if value < v.min || value > v.max { + response.Error = validatorfuncerr.InvalidParameterValueFuncError( + request.ArgumentPosition, + v.Description(ctx), + fmt.Sprintf("%f", value), + ) + } +} + // Between returns an AttributeValidator which ensures that any configured -// attribute value: +// attribute or function parameter value: // // - Is a number, which can be represented by a 32-bit floating point. // - Is greater than or equal to the given minimum and less than or equal to the given maximum. // // Null (unconfigured) and unknown (known after apply) values are skipped. -func Between(minVal, maxVal float32) validator.Float32 { - if minVal > maxVal { - return nil - } - +// +// minVal cannot be greater than maxVal. Invalid combinations of +// minVal and maxVal will result in an implementation error message during validation. +func Between(minVal, maxVal float32) betweenValidator { return betweenValidator{ min: minVal, max: maxVal, diff --git a/float32validator/between_example_test.go b/float32validator/between_example_test.go index 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 a3d78c46..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,14 +45,30 @@ func (validator atLeastValidator) ValidateFloat64(ctx context.Context, request v } } +func (validator atLeastValidator) ValidateParameterFloat64(ctx context.Context, request function.Float64ParameterValidatorRequest, response *function.Float64ParameterValidatorResponse) { + if request.Value.IsNull() || request.Value.IsUnknown() { + return + } + + value := request.Value.ValueFloat64() + + if value < validator.min { + response.Error = validatorfuncerr.InvalidParameterValueFuncError( + request.ArgumentPosition, + validator.Description(ctx), + fmt.Sprintf("%f", value), + ) + } +} + // AtLeast returns an AttributeValidator which ensures that any configured -// attribute value: +// attribute or function parameter value: // // - Is a number, which can be represented by a 64-bit floating point. // - Is greater than or equal to the given minimum. // // Null (unconfigured) and unknown (known after apply) values are skipped. -func AtLeast(minVal float64) validator.Float64 { +func AtLeast(minVal float64) atLeastValidator { return atLeastValidator{ min: minVal, } diff --git a/float64validator/at_least_example_test.go b/float64validator/at_least_example_test.go index 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 fe1d63df..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,14 +45,30 @@ func (v atMostValidator) ValidateFloat64(ctx context.Context, request validator. } } +func (v atMostValidator) ValidateParameterFloat64(ctx context.Context, request function.Float64ParameterValidatorRequest, response *function.Float64ParameterValidatorResponse) { + if request.Value.IsNull() || request.Value.IsUnknown() { + return + } + + value := request.Value.ValueFloat64() + + if value > v.max { + response.Error = validatorfuncerr.InvalidParameterValueFuncError( + request.ArgumentPosition, + v.Description(ctx), + fmt.Sprintf("%f", value), + ) + } +} + // AtMost returns an AttributeValidator which ensures that any configured -// attribute value: +// attribute or function parameter value: // // - Is a number, which can be represented by a 64-bit floating point. // - Is less than or equal to the given maximum. // // Null (unconfigured) and unknown (known after apply) values are skipped. -func AtMost(maxVal float64) validator.Float64 { +func AtMost(maxVal float64) atMostValidator { return atMostValidator{ max: maxVal, } diff --git a/float64validator/at_most_example_test.go b/float64validator/at_most_example_test.go index 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 f08f18ce..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,18 +62,44 @@ func (v betweenValidator) ValidateFloat64(ctx context.Context, request validator } } +func (v betweenValidator) ValidateParameterFloat64(ctx context.Context, request function.Float64ParameterValidatorRequest, response *function.Float64ParameterValidatorResponse) { + // Return an error if the validator has been created in an invalid state + if v.min > v.max { + response.Error = validatorfuncerr.InvalidValidatorUsageFuncError( + request.ArgumentPosition, + "Between", + v.invalidUsageMessage(), + ) + + return + } + + if request.Value.IsNull() || request.Value.IsUnknown() { + return + } + + value := request.Value.ValueFloat64() + + if value < v.min || value > v.max { + response.Error = validatorfuncerr.InvalidParameterValueFuncError( + request.ArgumentPosition, + v.Description(ctx), + fmt.Sprintf("%f", value), + ) + } +} + // Between returns an AttributeValidator which ensures that any configured -// attribute value: +// attribute or function parameter value: // // - Is a number, which can be represented by a 64-bit floating point. // - Is greater than or equal to the given minimum and less than or equal to the given maximum. // // Null (unconfigured) and unknown (known after apply) values are skipped. -func Between(minVal, maxVal float64) validator.Float64 { - if minVal > maxVal { - return nil - } - +// +// minVal cannot be greater than maxVal. Invalid combinations of +// minVal and maxVal will result in an implementation error message during validation. +func Between(minVal, maxVal float64) betweenValidator { return betweenValidator{ min: minVal, max: maxVal, diff --git a/float64validator/between_example_test.go b/float64validator/between_example_test.go index 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/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 681c1a88..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,14 +43,28 @@ func (v atLeastValidator) ValidateInt32(ctx context.Context, request validator.I } } +func (v atLeastValidator) ValidateParameterInt32(ctx context.Context, request function.Int32ParameterValidatorRequest, response *function.Int32ParameterValidatorResponse) { + if request.Value.IsNull() || request.Value.IsUnknown() { + return + } + + if request.Value.ValueInt32() < v.min { + response.Error = validatorfuncerr.InvalidParameterValueFuncError( + request.ArgumentPosition, + v.Description(ctx), + fmt.Sprintf("%d", request.Value.ValueInt32()), + ) + } +} + // AtLeast returns an AttributeValidator which ensures that any configured -// attribute value: +// attribute or function parameter value: // // - Is a number, which can be represented by a 32-bit integer. // - Is greater than or equal to the given minimum. // // Null (unconfigured) and unknown (known after apply) values are skipped. -func AtLeast(minVal int32) validator.Int32 { +func AtLeast(minVal int32) atLeastValidator { return atLeastValidator{ min: minVal, } diff --git a/int32validator/at_least_example_test.go b/int32validator/at_least_example_test.go index 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 afc1d309..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,14 +43,28 @@ func (v atMostValidator) ValidateInt32(ctx context.Context, request validator.In } } +func (v atMostValidator) ValidateParameterInt32(ctx context.Context, request function.Int32ParameterValidatorRequest, response *function.Int32ParameterValidatorResponse) { + if request.Value.IsNull() || request.Value.IsUnknown() { + return + } + + if request.Value.ValueInt32() > v.max { + response.Error = validatorfuncerr.InvalidParameterValueFuncError( + request.ArgumentPosition, + v.Description(ctx), + fmt.Sprintf("%d", request.Value.ValueInt32()), + ) + } +} + // AtMost returns an AttributeValidator which ensures that any configured -// attribute value: +// attribute or function parameter value: // // - Is a number, which can be represented by a 32-bit integer. // - Is less than or equal to the given maximum. // // Null (unconfigured) and unknown (known after apply) values are skipped. -func AtMost(maxVal int32) validator.Int32 { +func AtMost(maxVal int32) atMostValidator { return atMostValidator{ max: maxVal, } diff --git a/int32validator/at_most_example_test.go b/int32validator/at_most_example_test.go index 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 18f0b041..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,18 +60,42 @@ func (v betweenValidator) ValidateInt32(ctx context.Context, request validator.I } } +func (v betweenValidator) ValidateParameterInt32(ctx context.Context, request function.Int32ParameterValidatorRequest, response *function.Int32ParameterValidatorResponse) { + // Return an error if the validator has been created in an invalid state + if v.min > v.max { + response.Error = validatorfuncerr.InvalidValidatorUsageFuncError( + request.ArgumentPosition, + "Between", + v.invalidUsageMessage(), + ) + + return + } + + if request.Value.IsNull() || request.Value.IsUnknown() { + return + } + + if request.Value.ValueInt32() < v.min || request.Value.ValueInt32() > v.max { + response.Error = validatorfuncerr.InvalidParameterValueFuncError( + request.ArgumentPosition, + v.Description(ctx), + fmt.Sprintf("%d", request.Value.ValueInt32()), + ) + } +} + // Between returns an AttributeValidator which ensures that any configured -// attribute value: +// attribute or function parameter value: // // - Is a number, which can be represented by a 32-bit integer. // - Is greater than or equal to the given minimum and less than or equal to the given maximum. // // Null (unconfigured) and unknown (known after apply) values are skipped. -func Between(minVal, maxVal int32) validator.Int32 { - if minVal > maxVal { - return nil - } - +// +// minVal cannot be greater than maxVal. Invalid combinations of +// minVal and maxVal will result in an implementation error message during validation. +func Between(minVal, maxVal int32) betweenValidator { return betweenValidator{ min: minVal, max: maxVal, diff --git a/int32validator/between_example_test.go b/int32validator/between_example_test.go index 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 18fade05..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,14 +43,28 @@ func (v atLeastValidator) ValidateInt64(ctx context.Context, request validator.I } } +func (v atLeastValidator) ValidateParameterInt64(ctx context.Context, request function.Int64ParameterValidatorRequest, response *function.Int64ParameterValidatorResponse) { + if request.Value.IsNull() || request.Value.IsUnknown() { + return + } + + if request.Value.ValueInt64() < v.min { + response.Error = validatorfuncerr.InvalidParameterValueFuncError( + request.ArgumentPosition, + v.Description(ctx), + fmt.Sprintf("%d", request.Value.ValueInt64()), + ) + } +} + // AtLeast returns an AttributeValidator which ensures that any configured -// attribute value: +// attribute or function parameter value: // // - Is a number, which can be represented by a 64-bit integer. // - Is greater than or equal to the given minimum. // // Null (unconfigured) and unknown (known after apply) values are skipped. -func AtLeast(minVal int64) validator.Int64 { +func AtLeast(minVal int64) atLeastValidator { return atLeastValidator{ min: minVal, } diff --git a/int64validator/at_least_example_test.go b/int64validator/at_least_example_test.go index 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 a7650502..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,14 +43,28 @@ func (v atMostValidator) ValidateInt64(ctx context.Context, request validator.In } } +func (v atMostValidator) ValidateParameterInt64(ctx context.Context, request function.Int64ParameterValidatorRequest, response *function.Int64ParameterValidatorResponse) { + if request.Value.IsNull() || request.Value.IsUnknown() { + return + } + + if request.Value.ValueInt64() > v.max { + response.Error = validatorfuncerr.InvalidParameterValueFuncError( + request.ArgumentPosition, + v.Description(ctx), + fmt.Sprintf("%d", request.Value.ValueInt64()), + ) + } +} + // AtMost returns an AttributeValidator which ensures that any configured -// attribute value: +// attribute or function parameter value: // // - Is a number, which can be represented by a 64-bit integer. // - Is less than or equal to the given maximum. // // Null (unconfigured) and unknown (known after apply) values are skipped. -func AtMost(maxVal int64) validator.Int64 { +func AtMost(maxVal int64) atMostValidator { return atMostValidator{ max: maxVal, } diff --git a/int64validator/at_most_example_test.go b/int64validator/at_most_example_test.go index 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 f018bb93..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,18 +60,42 @@ func (v betweenValidator) ValidateInt64(ctx context.Context, request validator.I } } +func (v betweenValidator) ValidateParameterInt64(ctx context.Context, request function.Int64ParameterValidatorRequest, response *function.Int64ParameterValidatorResponse) { + // Return an error if the validator has been created in an invalid state + if v.min > v.max { + response.Error = validatorfuncerr.InvalidValidatorUsageFuncError( + request.ArgumentPosition, + "Between", + v.invalidUsageMessage(), + ) + + return + } + + if request.Value.IsNull() || request.Value.IsUnknown() { + return + } + + if request.Value.ValueInt64() < v.min || request.Value.ValueInt64() > v.max { + response.Error = validatorfuncerr.InvalidParameterValueFuncError( + request.ArgumentPosition, + v.Description(ctx), + fmt.Sprintf("%d", request.Value.ValueInt64()), + ) + } +} + // Between returns an AttributeValidator which ensures that any configured -// attribute value: +// attribute or function parameter value: // // - Is a number, which can be represented by a 64-bit integer. // - Is greater than or equal to the given minimum and less than or equal to the given maximum. // // Null (unconfigured) and unknown (known after apply) values are skipped. -func Between(minVal, maxVal int64) validator.Int64 { - if minVal > maxVal { - return nil - } - +// +// minVal cannot be greater than maxVal. Invalid combinations of +// minVal and maxVal will result in an implementation error message during validation. +func Between(minVal, maxVal int64) betweenValidator { return betweenValidator{ min: minVal, max: maxVal, diff --git a/int64validator/between_example_test.go b/int64validator/between_example_test.go index 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 b346e185..54f86bea 100644 --- a/listvalidator/size_at_least.go +++ b/listvalidator/size_at_least.go @@ -7,29 +7,28 @@ import ( "context" "fmt" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatorfuncerr" ) var _ validator.List = sizeAtLeastValidator{} +var _ function.ListParameterValidator = sizeAtLeastValidator{} -// sizeAtLeastValidator validates that list contains at least min elements. type sizeAtLeastValidator struct { min int } -// Description describes the validation in plain text formatting. func (v sizeAtLeastValidator) Description(_ context.Context) string { return fmt.Sprintf("list must contain at least %d elements", v.min) } -// MarkdownDescription describes the validation in Markdown formatting. func (v sizeAtLeastValidator) MarkdownDescription(ctx context.Context) string { return v.Description(ctx) } -// Validate performs the validation. func (v sizeAtLeastValidator) ValidateList(ctx context.Context, req validator.ListRequest, resp *validator.ListResponse) { if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() { return @@ -46,14 +45,30 @@ func (v sizeAtLeastValidator) ValidateList(ctx context.Context, req validator.Li } } +func (v sizeAtLeastValidator) ValidateParameterList(ctx context.Context, req function.ListParameterValidatorRequest, resp *function.ListParameterValidatorResponse) { + if req.Value.IsNull() || req.Value.IsUnknown() { + return + } + + elems := req.Value.Elements() + + if len(elems) < v.min { + resp.Error = validatorfuncerr.InvalidParameterValueFuncError( + req.ArgumentPosition, + v.Description(ctx), + fmt.Sprintf("%d", len(elems)), + ) + } +} + // SizeAtLeast returns an AttributeValidator which ensures that any configured -// attribute value: +// attribute or function parameter value: // // - Is a List. // - Contains at least min elements. // // Null (unconfigured) and unknown (known after apply) values are skipped. -func SizeAtLeast(minVal int) validator.List { +func SizeAtLeast(minVal int) sizeAtLeastValidator { return sizeAtLeastValidator{ min: minVal, } diff --git a/listvalidator/size_at_least_example_test.go b/listvalidator/size_at_least_example_test.go index 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 4e7ea27f..0ff5ed24 100644 --- a/listvalidator/size_at_most.go +++ b/listvalidator/size_at_most.go @@ -7,29 +7,28 @@ import ( "context" "fmt" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatorfuncerr" ) var _ validator.List = sizeAtMostValidator{} +var _ function.ListParameterValidator = sizeAtMostValidator{} -// sizeAtMostValidator validates that list contains at most max elements. type sizeAtMostValidator struct { max int } -// Description describes the validation in plain text formatting. func (v sizeAtMostValidator) Description(_ context.Context) string { return fmt.Sprintf("list must contain at most %d elements", v.max) } -// MarkdownDescription describes the validation in Markdown formatting. func (v sizeAtMostValidator) MarkdownDescription(ctx context.Context) string { return v.Description(ctx) } -// Validate performs the validation. func (v sizeAtMostValidator) ValidateList(ctx context.Context, req validator.ListRequest, resp *validator.ListResponse) { if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() { return @@ -46,14 +45,30 @@ func (v sizeAtMostValidator) ValidateList(ctx context.Context, req validator.Lis } } +func (v sizeAtMostValidator) ValidateParameterList(ctx context.Context, req function.ListParameterValidatorRequest, resp *function.ListParameterValidatorResponse) { + if req.Value.IsNull() || req.Value.IsUnknown() { + return + } + + elems := req.Value.Elements() + + if len(elems) > v.max { + resp.Error = validatorfuncerr.InvalidParameterValueFuncError( + req.ArgumentPosition, + v.Description(ctx), + fmt.Sprintf("%d", len(elems)), + ) + } +} + // SizeAtMost returns an AttributeValidator which ensures that any configured -// attribute value: +// attribute or function parameter value: // // - Is a List. // - Contains at most max elements. // // Null (unconfigured) and unknown (known after apply) values are skipped. -func SizeAtMost(maxVal int) validator.List { +func SizeAtMost(maxVal int) sizeAtMostValidator { return sizeAtMostValidator{ max: maxVal, } diff --git a/listvalidator/size_at_most_example_test.go b/listvalidator/size_at_most_example_test.go index 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 6bd4b892..cab9c9dc 100644 --- a/listvalidator/size_between.go +++ b/listvalidator/size_between.go @@ -7,31 +7,29 @@ import ( "context" "fmt" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatorfuncerr" ) var _ validator.List = sizeBetweenValidator{} +var _ function.ListParameterValidator = sizeBetweenValidator{} -// sizeBetweenValidator validates that list contains at least min elements -// and at most max elements. type sizeBetweenValidator struct { min int max int } -// Description describes the validation in plain text formatting. func (v sizeBetweenValidator) Description(_ context.Context) string { return fmt.Sprintf("list must contain at least %d elements and at most %d elements", v.min, v.max) } -// MarkdownDescription describes the validation in Markdown formatting. func (v sizeBetweenValidator) MarkdownDescription(ctx context.Context) string { return v.Description(ctx) } -// Validate performs the validation. func (v sizeBetweenValidator) ValidateList(ctx context.Context, req validator.ListRequest, resp *validator.ListResponse) { if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() { return @@ -48,14 +46,30 @@ func (v sizeBetweenValidator) ValidateList(ctx context.Context, req validator.Li } } +func (v sizeBetweenValidator) ValidateParameterList(ctx context.Context, req function.ListParameterValidatorRequest, resp *function.ListParameterValidatorResponse) { + if req.Value.IsNull() || req.Value.IsUnknown() { + return + } + + elems := req.Value.Elements() + + if len(elems) < v.min || len(elems) > v.max { + resp.Error = validatorfuncerr.InvalidParameterValueFuncError( + req.ArgumentPosition, + v.Description(ctx), + fmt.Sprintf("%d", len(elems)), + ) + } +} + // SizeBetween returns an AttributeValidator which ensures that any configured -// attribute value: +// attribute or function parameter value: // // - Is a List. // - Contains at least min elements and at most max elements. // // Null (unconfigured) and unknown (known after apply) values are skipped. -func SizeBetween(minVal, maxVal int) validator.List { +func SizeBetween(minVal, maxVal int) sizeBetweenValidator { return sizeBetweenValidator{ min: minVal, max: maxVal, diff --git a/listvalidator/size_between_example_test.go b/listvalidator/size_between_example_test.go index 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 2ddc9800..8c9ff5a8 100644 --- a/mapvalidator/size_at_least.go +++ b/mapvalidator/size_at_least.go @@ -7,29 +7,28 @@ import ( "context" "fmt" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatorfuncerr" ) var _ validator.Map = sizeAtLeastValidator{} +var _ function.MapParameterValidator = sizeAtLeastValidator{} -// sizeAtLeastValidator validates that map contains at least min elements. type sizeAtLeastValidator struct { min int } -// Description describes the validation in plain text formatting. func (v sizeAtLeastValidator) Description(_ context.Context) string { return fmt.Sprintf("map must contain at least %d elements", v.min) } -// MarkdownDescription describes the validation in Markdown formatting. func (v sizeAtLeastValidator) MarkdownDescription(ctx context.Context) string { return v.Description(ctx) } -// Validate performs the validation. func (v sizeAtLeastValidator) ValidateMap(ctx context.Context, req validator.MapRequest, resp *validator.MapResponse) { if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() { return @@ -46,14 +45,30 @@ func (v sizeAtLeastValidator) ValidateMap(ctx context.Context, req validator.Map } } +func (v sizeAtLeastValidator) ValidateParameterMap(ctx context.Context, req function.MapParameterValidatorRequest, resp *function.MapParameterValidatorResponse) { + if req.Value.IsNull() || req.Value.IsUnknown() { + return + } + + elems := req.Value.Elements() + + if len(elems) < v.min { + resp.Error = validatorfuncerr.InvalidParameterValueFuncError( + req.ArgumentPosition, + v.Description(ctx), + fmt.Sprintf("%d", len(elems)), + ) + } +} + // SizeAtLeast returns an AttributeValidator which ensures that any configured -// attribute value: +// attribute or function parameter value: // // - Is a Map. // - Contains at least min elements. // // Null (unconfigured) and unknown (known after apply) values are skipped. -func SizeAtLeast(minVal int) validator.Map { +func SizeAtLeast(minVal int) sizeAtLeastValidator { return sizeAtLeastValidator{ min: minVal, } diff --git a/mapvalidator/size_at_least_example_test.go b/mapvalidator/size_at_least_example_test.go index 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 f41529e6..82fe3ef1 100644 --- a/mapvalidator/size_at_most.go +++ b/mapvalidator/size_at_most.go @@ -7,29 +7,28 @@ import ( "context" "fmt" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatorfuncerr" ) var _ validator.Map = sizeAtMostValidator{} +var _ function.MapParameterValidator = sizeAtMostValidator{} -// sizeAtMostValidator validates that map contains at most max elements. type sizeAtMostValidator struct { max int } -// Description describes the validation in plain text formatting. func (v sizeAtMostValidator) Description(_ context.Context) string { return fmt.Sprintf("map must contain at most %d elements", v.max) } -// MarkdownDescription describes the validation in Markdown formatting. func (v sizeAtMostValidator) MarkdownDescription(ctx context.Context) string { return v.Description(ctx) } -// Validate performs the validation. func (v sizeAtMostValidator) ValidateMap(ctx context.Context, req validator.MapRequest, resp *validator.MapResponse) { if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() { return @@ -46,14 +45,30 @@ func (v sizeAtMostValidator) ValidateMap(ctx context.Context, req validator.MapR } } +func (v sizeAtMostValidator) ValidateParameterMap(ctx context.Context, req function.MapParameterValidatorRequest, resp *function.MapParameterValidatorResponse) { + if req.Value.IsNull() || req.Value.IsUnknown() { + return + } + + elems := req.Value.Elements() + + if len(elems) > v.max { + resp.Error = validatorfuncerr.InvalidParameterValueFuncError( + req.ArgumentPosition, + v.Description(ctx), + fmt.Sprintf("%d", len(elems)), + ) + } +} + // SizeAtMost returns an AttributeValidator which ensures that any configured -// attribute value: +// attribute or function parameter value: // // - Is a Map. // - Contains at most max elements. // // Null (unconfigured) and unknown (known after apply) values are skipped. -func SizeAtMost(maxVal int) validator.Map { +func SizeAtMost(maxVal int) sizeAtMostValidator { return sizeAtMostValidator{ max: maxVal, } diff --git a/mapvalidator/size_at_most_example_test.go b/mapvalidator/size_at_most_example_test.go index 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 9d6e761f..78e12fb3 100644 --- a/mapvalidator/size_between.go +++ b/mapvalidator/size_between.go @@ -7,31 +7,29 @@ import ( "context" "fmt" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatorfuncerr" ) var _ validator.Map = sizeBetweenValidator{} +var _ function.MapParameterValidator = sizeBetweenValidator{} -// sizeBetweenValidator validates that map contains at least min elements -// and at most max elements. type sizeBetweenValidator struct { min int max int } -// Description describes the validation in plain text formatting. func (v sizeBetweenValidator) Description(_ context.Context) string { return fmt.Sprintf("map must contain at least %d elements and at most %d elements", v.min, v.max) } -// MarkdownDescription describes the validation in Markdown formatting. func (v sizeBetweenValidator) MarkdownDescription(ctx context.Context) string { return v.Description(ctx) } -// Validate performs the validation. func (v sizeBetweenValidator) ValidateMap(ctx context.Context, req validator.MapRequest, resp *validator.MapResponse) { if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() { return @@ -48,14 +46,30 @@ func (v sizeBetweenValidator) ValidateMap(ctx context.Context, req validator.Map } } +func (v sizeBetweenValidator) ValidateParameterMap(ctx context.Context, req function.MapParameterValidatorRequest, resp *function.MapParameterValidatorResponse) { + if req.Value.IsNull() || req.Value.IsUnknown() { + return + } + + elems := req.Value.Elements() + + if len(elems) < v.min || len(elems) > v.max { + resp.Error = validatorfuncerr.InvalidParameterValueFuncError( + req.ArgumentPosition, + v.Description(ctx), + fmt.Sprintf("%d", len(elems)), + ) + } +} + // SizeBetween returns an AttributeValidator which ensures that any configured -// attribute value: +// attribute or function parameter value: // // - Is a Map. // - Contains at least min elements and at most max elements. // // Null (unconfigured) and unknown (known after apply) values are skipped. -func SizeBetween(minVal, maxVal int) validator.Map { +func SizeBetween(minVal, maxVal int) sizeBetweenValidator { return sizeBetweenValidator{ min: minVal, max: maxVal, diff --git a/mapvalidator/size_between_example_test.go b/mapvalidator/size_between_example_test.go index 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 c27dc14a..7af58b00 100644 --- a/setvalidator/size_at_least.go +++ b/setvalidator/size_at_least.go @@ -7,29 +7,28 @@ import ( "context" "fmt" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatorfuncerr" ) var _ validator.Set = sizeAtLeastValidator{} +var _ function.SetParameterValidator = sizeAtLeastValidator{} -// sizeAtLeastValidator validates that set contains at least min elements. type sizeAtLeastValidator struct { min int } -// Description describes the validation in plain text formatting. func (v sizeAtLeastValidator) Description(_ context.Context) string { return fmt.Sprintf("set must contain at least %d elements", v.min) } -// MarkdownDescription describes the validation in Markdown formatting. func (v sizeAtLeastValidator) MarkdownDescription(ctx context.Context) string { return v.Description(ctx) } -// Validate performs the validation. func (v sizeAtLeastValidator) ValidateSet(ctx context.Context, req validator.SetRequest, resp *validator.SetResponse) { if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() { return @@ -46,14 +45,30 @@ func (v sizeAtLeastValidator) ValidateSet(ctx context.Context, req validator.Set } } +func (v sizeAtLeastValidator) ValidateParameterSet(ctx context.Context, req function.SetParameterValidatorRequest, resp *function.SetParameterValidatorResponse) { + if req.Value.IsNull() || req.Value.IsUnknown() { + return + } + + elems := req.Value.Elements() + + if len(elems) < v.min { + resp.Error = validatorfuncerr.InvalidParameterValueFuncError( + req.ArgumentPosition, + v.Description(ctx), + fmt.Sprintf("%d", len(elems)), + ) + } +} + // SizeAtLeast returns an AttributeValidator which ensures that any configured -// attribute value: +// attribute or function parameter value: // // - Is a Set. // - Contains at least min elements. // // Null (unconfigured) and unknown (known after apply) values are skipped. -func SizeAtLeast(minVal int) validator.Set { +func SizeAtLeast(minVal int) sizeAtLeastValidator { return sizeAtLeastValidator{ min: minVal, } diff --git a/setvalidator/size_at_least_example_test.go b/setvalidator/size_at_least_example_test.go index 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 1f1be7de..e0762acb 100644 --- a/setvalidator/size_at_most.go +++ b/setvalidator/size_at_most.go @@ -7,29 +7,28 @@ import ( "context" "fmt" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatorfuncerr" ) var _ validator.Set = sizeAtMostValidator{} +var _ function.SetParameterValidator = sizeAtMostValidator{} -// sizeAtMostValidator validates that set contains at most max elements. type sizeAtMostValidator struct { max int } -// Description describes the validation in plain text formatting. func (v sizeAtMostValidator) Description(_ context.Context) string { return fmt.Sprintf("set must contain at most %d elements", v.max) } -// MarkdownDescription describes the validation in Markdown formatting. func (v sizeAtMostValidator) MarkdownDescription(ctx context.Context) string { return v.Description(ctx) } -// Validate performs the validation. func (v sizeAtMostValidator) ValidateSet(ctx context.Context, req validator.SetRequest, resp *validator.SetResponse) { if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() { return @@ -46,14 +45,30 @@ func (v sizeAtMostValidator) ValidateSet(ctx context.Context, req validator.SetR } } +func (v sizeAtMostValidator) ValidateParameterSet(ctx context.Context, req function.SetParameterValidatorRequest, resp *function.SetParameterValidatorResponse) { + if req.Value.IsNull() || req.Value.IsUnknown() { + return + } + + elems := req.Value.Elements() + + if len(elems) > v.max { + resp.Error = validatorfuncerr.InvalidParameterValueFuncError( + req.ArgumentPosition, + v.Description(ctx), + fmt.Sprintf("%d", len(elems)), + ) + } +} + // SizeAtMost returns an AttributeValidator which ensures that any configured -// attribute value: +// attribute or function parameter value: // // - Is a Set. // - Contains at most max elements. // // Null (unconfigured) and unknown (known after apply) values are skipped. -func SizeAtMost(maxVal int) validator.Set { +func SizeAtMost(maxVal int) sizeAtMostValidator { return sizeAtMostValidator{ max: maxVal, } diff --git a/setvalidator/size_at_most_example_test.go b/setvalidator/size_at_most_example_test.go index 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 7e717895..9dd87044 100644 --- a/setvalidator/size_between.go +++ b/setvalidator/size_between.go @@ -7,31 +7,29 @@ import ( "context" "fmt" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatorfuncerr" ) var _ validator.Set = sizeBetweenValidator{} +var _ function.SetParameterValidator = sizeBetweenValidator{} -// sizeBetweenValidator validates that set contains at least min elements -// and at most max elements. type sizeBetweenValidator struct { min int max int } -// Description describes the validation in plain text formatting. func (v sizeBetweenValidator) Description(_ context.Context) string { return fmt.Sprintf("set must contain at least %d elements and at most %d elements", v.min, v.max) } -// MarkdownDescription describes the validation in Markdown formatting. func (v sizeBetweenValidator) MarkdownDescription(ctx context.Context) string { return v.Description(ctx) } -// ValidateSet performs the validation. func (v sizeBetweenValidator) ValidateSet(ctx context.Context, req validator.SetRequest, resp *validator.SetResponse) { if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() { return @@ -48,14 +46,30 @@ func (v sizeBetweenValidator) ValidateSet(ctx context.Context, req validator.Set } } +func (v sizeBetweenValidator) ValidateParameterSet(ctx context.Context, req function.SetParameterValidatorRequest, resp *function.SetParameterValidatorResponse) { + if req.Value.IsNull() || req.Value.IsUnknown() { + return + } + + elems := req.Value.Elements() + + if len(elems) < v.min || len(elems) > v.max { + resp.Error = validatorfuncerr.InvalidParameterValueFuncError( + req.ArgumentPosition, + v.Description(ctx), + fmt.Sprintf("%d", len(elems)), + ) + } +} + // SizeBetween returns an AttributeValidator which ensures that any configured -// attribute value: +// attribute or function parameter value: // // - Is a Set. // - Contains at least min elements and at most max elements. // // Null (unconfigured) and unknown (known after apply) values are skipped. -func SizeBetween(minVal, maxVal int) validator.Set { +func SizeBetween(minVal, maxVal int) sizeBetweenValidator { return sizeBetweenValidator{ min: minVal, max: maxVal, diff --git a/setvalidator/size_between_example_test.go b/setvalidator/size_between_example_test.go index 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) + } + }) } } From 081db0f34444a077f3032ee8508fe85ca770ad0e Mon Sep 17 00:00:00 2001 From: hc-github-team-tf-provider-devex Date: Thu, 17 Oct 2024 19:07:09 +0000 Subject: [PATCH 18/18] Update changelog --- .changes/0.14.0.md | 15 +++++++++++++++ .../unreleased/ENHANCEMENTS-20241014-121220.yaml | 6 ------ .changes/unreleased/FEATURES-20240920-164852.yaml | 5 ----- .changes/unreleased/NOTES-20240906-162141.yaml | 7 ------- .changes/unreleased/NOTES-20241014-121711.yaml | 7 ------- CHANGELOG.md | 15 +++++++++++++++ 6 files changed, 30 insertions(+), 25 deletions(-) create mode 100644 .changes/0.14.0.md delete mode 100644 .changes/unreleased/ENHANCEMENTS-20241014-121220.yaml delete mode 100644 .changes/unreleased/FEATURES-20240920-164852.yaml delete mode 100644 .changes/unreleased/NOTES-20240906-162141.yaml delete mode 100644 .changes/unreleased/NOTES-20241014-121711.yaml 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/.changes/unreleased/ENHANCEMENTS-20241014-121220.yaml b/.changes/unreleased/ENHANCEMENTS-20241014-121220.yaml deleted file mode 100644 index 56a4511f..00000000 --- a/.changes/unreleased/ENHANCEMENTS-20241014-121220.yaml +++ /dev/null @@ -1,6 +0,0 @@ -kind: ENHANCEMENTS -body: 'all: Implemented parameter interfaces for all value-based validators. This - allows these validators to be used with provider-defined functions.' -time: 2024-10-14T12:12:20.607373-04:00 -custom: - Issue: "235" diff --git a/.changes/unreleased/FEATURES-20240920-164852.yaml b/.changes/unreleased/FEATURES-20240920-164852.yaml deleted file mode 100644 index 0a90941d..00000000 --- a/.changes/unreleased/FEATURES-20240920-164852.yaml +++ /dev/null @@ -1,5 +0,0 @@ -kind: FEATURES -body: 'boolvalidator: Added `Equals` validator' -time: 2024-09-20T16:48:52.562758-04:00 -custom: - Issue: "232" diff --git a/.changes/unreleased/NOTES-20240906-162141.yaml b/.changes/unreleased/NOTES-20240906-162141.yaml deleted file mode 100644 index 79d84861..00000000 --- a/.changes/unreleased/NOTES-20240906-162141.yaml +++ /dev/null @@ -1,7 +0,0 @@ -kind: NOTES -body: '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.' -time: 2024-09-06T16:21:41.853159-04:00 -custom: - Issue: "229" diff --git a/.changes/unreleased/NOTES-20241014-121711.yaml b/.changes/unreleased/NOTES-20241014-121711.yaml deleted file mode 100644 index 392757d5..00000000 --- a/.changes/unreleased/NOTES-20241014-121711.yaml +++ /dev/null @@ -1,7 +0,0 @@ -kind: NOTES -body: 'all: Previously, creating validators with invalid data would result in a `nil` - value being returned and a panic from `terraform-plugin-framework`. This has been - updated to return an implementation diagnostic referencing the invalid data/validator during config validation.' -time: 2024-10-14T12:17:11.811926-04:00 -custom: - Issue: "235" diff --git a/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: pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy