diff --git a/.changelog/32.txt b/.changelog/32.txt new file mode 100644 index 00000000..84062a84 --- /dev/null +++ b/.changelog/32.txt @@ -0,0 +1,3 @@ +```release-note:feature +Introduced `schemavalidator` package with 4 new validation functions: `RequiredWith()`, `ConflictsWith()`, `AtLeastOneOf()`, `ExactlyOneOf()` +``` diff --git a/float64validator/at_least_test.go b/float64validator/at_least_test.go index 1773b659..ab8c8e50 100644 --- a/float64validator/at_least_test.go +++ b/float64validator/at_least_test.go @@ -55,8 +55,9 @@ func TestAtLeastValidator(t *testing.T) { name, test := name, test t.Run(name, func(t *testing.T) { request := tfsdk.ValidateAttributeRequest{ - AttributePath: path.Root("test"), - AttributeConfig: test.val, + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), + AttributeConfig: test.val, } response := tfsdk.ValidateAttributeResponse{} float64validator.AtLeast(test.min).Validate(context.TODO(), request, &response) diff --git a/float64validator/at_most_test.go b/float64validator/at_most_test.go index e5182b50..2443c3b8 100644 --- a/float64validator/at_most_test.go +++ b/float64validator/at_most_test.go @@ -55,8 +55,9 @@ func TestAtMostValidator(t *testing.T) { name, test := name, test t.Run(name, func(t *testing.T) { request := tfsdk.ValidateAttributeRequest{ - AttributePath: path.Root("test"), - AttributeConfig: test.val, + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), + AttributeConfig: test.val, } response := tfsdk.ValidateAttributeResponse{} float64validator.AtMost(test.max).Validate(context.TODO(), request, &response) diff --git a/float64validator/between_test.go b/float64validator/between_test.go index 4add5502..74c2e608 100644 --- a/float64validator/between_test.go +++ b/float64validator/between_test.go @@ -73,8 +73,9 @@ func TestBetweenValidator(t *testing.T) { name, test := name, test t.Run(name, func(t *testing.T) { request := tfsdk.ValidateAttributeRequest{ - AttributePath: path.Root("test"), - AttributeConfig: test.val, + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), + AttributeConfig: test.val, } response := tfsdk.ValidateAttributeResponse{} float64validator.Between(test.min, test.max).Validate(context.TODO(), request, &response) diff --git a/float64validator/none_of_test.go b/float64validator/none_of_test.go index 102f779b..ceccda2c 100644 --- a/float64validator/none_of_test.go +++ b/float64validator/none_of_test.go @@ -5,7 +5,6 @@ import ( "testing" "github.com/hashicorp/terraform-plugin-framework-validators/float64validator" - "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" @@ -73,12 +72,12 @@ func TestNoneOfValidator(t *testing.T) { t.Fatalf("expected %d error(s), got none", test.expErrors) } - if test.expErrors > 0 && test.expErrors != validatordiag.ErrorsCount(res.Diagnostics) { - t.Fatalf("expected %d error(s), got %d: %v", test.expErrors, validatordiag.ErrorsCount(res.Diagnostics), res.Diagnostics) + 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", validatordiag.ErrorsCount(res.Diagnostics), res.Diagnostics) + t.Fatalf("expected no error(s), got %d: %v", res.Diagnostics.ErrorsCount(), res.Diagnostics) } }) } diff --git a/float64validator/one_of_test.go b/float64validator/one_of_test.go index afe79b3b..dc1b55a9 100644 --- a/float64validator/one_of_test.go +++ b/float64validator/one_of_test.go @@ -5,7 +5,6 @@ import ( "testing" "github.com/hashicorp/terraform-plugin-framework-validators/float64validator" - "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" @@ -73,12 +72,12 @@ func TestOneOfValidator(t *testing.T) { t.Fatalf("expected %d error(s), got none", test.expErrors) } - if test.expErrors > 0 && test.expErrors != validatordiag.ErrorsCount(res.Diagnostics) { - t.Fatalf("expected %d error(s), got %d: %v", test.expErrors, validatordiag.ErrorsCount(res.Diagnostics), res.Diagnostics) + 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", validatordiag.ErrorsCount(res.Diagnostics), res.Diagnostics) + t.Fatalf("expected no error(s), got %d: %v", res.Diagnostics.ErrorsCount(), res.Diagnostics) } }) } diff --git a/float64validator/type_validation_test.go b/float64validator/type_validation_test.go index 7f8f2eaa..614a3585 100644 --- a/float64validator/type_validation_test.go +++ b/float64validator/type_validation_test.go @@ -20,32 +20,36 @@ func TestValidateFloat(t *testing.T) { }{ "invalid-type": { request: tfsdk.ValidateAttributeRequest{ - AttributeConfig: types.Bool{Value: true}, - AttributePath: path.Root("test"), + AttributeConfig: types.Bool{Value: true}, + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), }, expectedFloat64: 0.0, expectedOk: false, }, "float64-null": { request: tfsdk.ValidateAttributeRequest{ - AttributeConfig: types.Float64{Null: true}, - AttributePath: path.Root("test"), + AttributeConfig: types.Float64{Null: true}, + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), }, expectedFloat64: 0.0, expectedOk: false, }, "float64-value": { request: tfsdk.ValidateAttributeRequest{ - AttributeConfig: types.Float64{Value: 1.2}, - AttributePath: path.Root("test"), + AttributeConfig: types.Float64{Value: 1.2}, + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), }, expectedFloat64: 1.2, expectedOk: true, }, "float64-unknown": { request: tfsdk.ValidateAttributeRequest{ - AttributeConfig: types.Float64{Unknown: true}, - AttributePath: path.Root("test"), + AttributeConfig: types.Float64{Unknown: true}, + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), }, expectedFloat64: 0.0, expectedOk: false, diff --git a/go.mod b/go.mod index e2c33057..dc60f2e9 100644 --- a/go.mod +++ b/go.mod @@ -4,14 +4,14 @@ go 1.17 require ( github.com/google/go-cmp v0.5.8 - github.com/hashicorp/terraform-plugin-framework v0.9.1-0.20220627174514-5a338a7dd906 + github.com/hashicorp/terraform-plugin-framework v0.9.1-0.20220711170800-89baaa204707 + github.com/hashicorp/terraform-plugin-go v0.11.0 ) require ( github.com/fatih/color v1.13.0 // indirect github.com/golang/protobuf v1.5.2 // indirect github.com/hashicorp/go-hclog v1.2.1 // indirect - github.com/hashicorp/terraform-plugin-go v0.10.0 // indirect github.com/hashicorp/terraform-plugin-log v0.4.1 // indirect github.com/mattn/go-colorable v0.1.12 // indirect github.com/mattn/go-isatty v0.0.14 // indirect diff --git a/go.sum b/go.sum index ba3d6efd..2265c03e 100644 --- a/go.sum +++ b/go.sum @@ -57,21 +57,17 @@ github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-hclog v0.14.1/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= -github.com/hashicorp/go-hclog v1.2.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= github.com/hashicorp/go-hclog v1.2.1 h1:YQsLlGDJgwhXFpucSPyVbCBviQtjlHv3jLTlp8YmtEw= github.com/hashicorp/go-hclog v1.2.1/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= github.com/hashicorp/go-plugin v1.4.4/go.mod h1:viDMjcLJuDui6pXb8U4HVfb8AamCWhHGUjr2IrTF67s= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= -github.com/hashicorp/terraform-plugin-framework v0.9.1-0.20220627174514-5a338a7dd906 h1:Y9JWkRpLhbJfdN85Dx+joLWRiwzLlZQkhf1qSbyPJI8= -github.com/hashicorp/terraform-plugin-framework v0.9.1-0.20220627174514-5a338a7dd906/go.mod h1:ActelD2V6yt2m0MwIX4jESGDYJ573rAvZswGjSGm1rY= -github.com/hashicorp/terraform-plugin-go v0.9.1/go.mod h1:ItjVSlQs70otlzcCwlPcU8FRXLdO973oYFRZwAOxy8M= -github.com/hashicorp/terraform-plugin-go v0.10.0 h1:FIQDt/AZDSOXnN+znBnLLZA9aFk4/GwL40rwMLnvuTk= -github.com/hashicorp/terraform-plugin-go v0.10.0/go.mod h1:aphXBG8qtQH0yF1waMRlaw/3G+ZFlR/6Artnvt1QEDE= -github.com/hashicorp/terraform-plugin-log v0.4.0/go.mod h1:9KclxdunFownr4pIm1jdmwKRmE4d6HVG2c9XDq47rpg= +github.com/hashicorp/terraform-plugin-framework v0.9.1-0.20220711170800-89baaa204707 h1:4wAdubsgZ/eAbCjW2At8w6UDeVCel76jWDHsWvbc8Pk= +github.com/hashicorp/terraform-plugin-framework v0.9.1-0.20220711170800-89baaa204707/go.mod h1:+H4ieVu7X4bfYlLB/zytek48e4CjcG+gjKdVOjVY1PU= +github.com/hashicorp/terraform-plugin-go v0.11.0 h1:YXsvSCx7GbQO5jIUQd77FesqmIBxgSvYAtAX1NqErTk= +github.com/hashicorp/terraform-plugin-go v0.11.0/go.mod h1:aphXBG8qtQH0yF1waMRlaw/3G+ZFlR/6Artnvt1QEDE= github.com/hashicorp/terraform-plugin-log v0.4.1 h1:xpbmVhvuU3mgHzLetOmx9pkOL2rmgpu302XxddON6eo= github.com/hashicorp/terraform-plugin-log v0.4.1/go.mod h1:p4R1jWBXRTvL4odmEkFfDdhUjHf9zcs/BCoNHAc7IK4= -github.com/hashicorp/terraform-registry-address v0.0.0-20210412075316-9b2996cce896/go.mod h1:bzBPnUIkI0RxauU8Dqo+2KrZZ28Cf48s8V6IHt3p4co= github.com/hashicorp/terraform-registry-address v0.0.0-20220623143253-7d51757b572c/go.mod h1:Wn3Na71knbXc1G8Lh+yu/dQWWJeFQEpDeJMtWMtlmNI= github.com/hashicorp/terraform-svchost v0.0.0-20200729002733-f050f53b9734/go.mod h1:kNDNcF7sN4DocDLBkQYz73HGKwN1ANB1blq4lIYLYvg= github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= @@ -191,7 +187,6 @@ google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8 google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.2.0/go.mod h1:DNq5QpG7LJqD2AamLZ7zvKE0DEpVl2BSEVjFycAAjRY= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= diff --git a/helpers/validatordiag/diag.go b/helpers/validatordiag/diag.go index 44fcc87f..ca323fc9 100644 --- a/helpers/validatordiag/diag.go +++ b/helpers/validatordiag/diag.go @@ -35,8 +35,8 @@ func InvalidAttributeValueMatchDiagnostic(path path.Path, description string, va ) } -// InvalidAttributeSchemaDiagnostic returns an error Diagnostic to be used when a schemavalidator of attributes is invalid. -func InvalidAttributeSchemaDiagnostic(path path.Path, description string) diag.Diagnostic { +// InvalidAttributeCombinationDiagnostic returns an error Diagnostic to be used when a schemavalidator of attributes is invalid. +func InvalidAttributeCombinationDiagnostic(path path.Path, description string) diag.Diagnostic { return diag.NewAttributeErrorDiagnostic( path, "Invalid Attribute Combination", @@ -53,30 +53,10 @@ func InvalidAttributeTypeDiagnostic(path path.Path, description string, value st ) } -// ErrorsCount returns the amount of diag.Diagnostic in diag.Diagnostics that are diag.SeverityError. -func ErrorsCount(diags diag.Diagnostics) int { - count := 0 - - for _, d := range diags { - if diag.SeverityError == d.Severity() { - count++ - } - } - - return count -} - -// WarningsCount returns the amount of diag.Diagnostic in diag.Diagnostics that are diag.SeverityWarning. -func WarningsCount(diags diag.Diagnostics) int { - count := 0 - - for _, d := range diags { - if diag.SeverityWarning == d.Severity() { - count++ - } - } - - return count +func BugInProviderDiagnostic(summary string) diag.Diagnostic { + return diag.NewErrorDiagnostic(summary, + "This is a bug in the provider, which should be reported in the provider's own issue tracker", + ) } // capitalize will uppercase the first letter in a UTF-8 string. diff --git a/int64validator/at_least_test.go b/int64validator/at_least_test.go index 63a808d9..b5294bdb 100644 --- a/int64validator/at_least_test.go +++ b/int64validator/at_least_test.go @@ -51,8 +51,9 @@ func TestAtLeastValidator(t *testing.T) { name, test := name, test t.Run(name, func(t *testing.T) { request := tfsdk.ValidateAttributeRequest{ - AttributePath: path.Root("test"), - AttributeConfig: test.val, + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), + AttributeConfig: test.val, } response := tfsdk.ValidateAttributeResponse{} int64validator.AtLeast(test.min).Validate(context.TODO(), request, &response) diff --git a/int64validator/at_most_test.go b/int64validator/at_most_test.go index c0b8ca33..72a2801c 100644 --- a/int64validator/at_most_test.go +++ b/int64validator/at_most_test.go @@ -51,8 +51,9 @@ func TestAtMostValidator(t *testing.T) { name, test := name, test t.Run(name, func(t *testing.T) { request := tfsdk.ValidateAttributeRequest{ - AttributePath: path.Root("test"), - AttributeConfig: test.val, + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), + AttributeConfig: test.val, } response := tfsdk.ValidateAttributeResponse{} int64validator.AtMost(test.max).Validate(context.TODO(), request, &response) diff --git a/int64validator/between_test.go b/int64validator/between_test.go index 659820a5..1d454b85 100644 --- a/int64validator/between_test.go +++ b/int64validator/between_test.go @@ -68,8 +68,9 @@ func TestBetweenValidator(t *testing.T) { name, test := name, test t.Run(name, func(t *testing.T) { request := tfsdk.ValidateAttributeRequest{ - AttributePath: path.Root("test"), - AttributeConfig: test.val, + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), + AttributeConfig: test.val, } response := tfsdk.ValidateAttributeResponse{} int64validator.Between(test.min, test.max).Validate(context.TODO(), request, &response) diff --git a/int64validator/none_of_test.go b/int64validator/none_of_test.go index 3d7dd0ee..fea7ddcb 100644 --- a/int64validator/none_of_test.go +++ b/int64validator/none_of_test.go @@ -4,7 +4,6 @@ import ( "context" "testing" - "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/tfsdk" @@ -73,12 +72,12 @@ func TestNoneOfValidator(t *testing.T) { t.Fatalf("expected %d error(s), got none", test.expErrors) } - if test.expErrors > 0 && test.expErrors != validatordiag.ErrorsCount(res.Diagnostics) { - t.Fatalf("expected %d error(s), got %d: %v", test.expErrors, validatordiag.ErrorsCount(res.Diagnostics), res.Diagnostics) + 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", validatordiag.ErrorsCount(res.Diagnostics), res.Diagnostics) + t.Fatalf("expected no error(s), got %d: %v", res.Diagnostics.ErrorsCount(), res.Diagnostics) } }) } diff --git a/int64validator/one_of_test.go b/int64validator/one_of_test.go index 95517cb1..de2607cf 100644 --- a/int64validator/one_of_test.go +++ b/int64validator/one_of_test.go @@ -4,7 +4,6 @@ import ( "context" "testing" - "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/tfsdk" @@ -73,12 +72,12 @@ func TestOneOfValidator(t *testing.T) { t.Fatalf("expected %d error(s), got none", test.expErrors) } - if test.expErrors > 0 && test.expErrors != validatordiag.ErrorsCount(res.Diagnostics) { - t.Fatalf("expected %d error(s), got %d: %v", test.expErrors, validatordiag.ErrorsCount(res.Diagnostics), res.Diagnostics) + 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", validatordiag.ErrorsCount(res.Diagnostics), res.Diagnostics) + t.Fatalf("expected no error(s), got %d: %v", res.Diagnostics.ErrorsCount(), res.Diagnostics) } }) } diff --git a/int64validator/type_validation_test.go b/int64validator/type_validation_test.go index 7c6dc11e..596b51e7 100644 --- a/int64validator/type_validation_test.go +++ b/int64validator/type_validation_test.go @@ -20,32 +20,36 @@ func TestValidateInt(t *testing.T) { }{ "invalid-type": { request: tfsdk.ValidateAttributeRequest{ - AttributeConfig: types.Bool{Value: true}, - AttributePath: path.Root("test"), + AttributeConfig: types.Bool{Value: true}, + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), }, expectedInt64: 0.0, expectedOk: false, }, "int64-null": { request: tfsdk.ValidateAttributeRequest{ - AttributeConfig: types.Int64{Null: true}, - AttributePath: path.Root("test"), + AttributeConfig: types.Int64{Null: true}, + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), }, expectedInt64: 0.0, expectedOk: false, }, "int64-value": { request: tfsdk.ValidateAttributeRequest{ - AttributeConfig: types.Int64{Value: 123}, - AttributePath: path.Root("test"), + AttributeConfig: types.Int64{Value: 123}, + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), }, expectedInt64: 123, expectedOk: true, }, "int64-unknown": { request: tfsdk.ValidateAttributeRequest{ - AttributeConfig: types.Int64{Unknown: true}, - AttributePath: path.Root("test"), + AttributeConfig: types.Int64{Unknown: true}, + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), }, expectedInt64: 0.0, expectedOk: false, diff --git a/internal/primitivevalidator/none_of_test.go b/internal/primitivevalidator/none_of_test.go index c0329660..91cebbbd 100644 --- a/internal/primitivevalidator/none_of_test.go +++ b/internal/primitivevalidator/none_of_test.go @@ -6,7 +6,6 @@ import ( "math/big" "testing" - "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" "github.com/hashicorp/terraform-plugin-framework-validators/internal/primitivevalidator" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/tfsdk" @@ -181,12 +180,12 @@ func TestNoneOfValidator(t *testing.T) { t.Fatalf("expected %d error(s), got none", test.expErrors) } - if test.expErrors > 0 && test.expErrors != validatordiag.ErrorsCount(res.Diagnostics) { - t.Fatalf("expected %d error(s), got %d: %v", test.expErrors, validatordiag.ErrorsCount(res.Diagnostics), res.Diagnostics) + 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", validatordiag.ErrorsCount(res.Diagnostics), res.Diagnostics) + t.Fatalf("expected no error(s), got %d: %v", res.Diagnostics.ErrorsCount(), res.Diagnostics) } }) } diff --git a/internal/primitivevalidator/one_of_test.go b/internal/primitivevalidator/one_of_test.go index d132aff0..f55a1571 100644 --- a/internal/primitivevalidator/one_of_test.go +++ b/internal/primitivevalidator/one_of_test.go @@ -6,7 +6,6 @@ import ( "math/big" "testing" - "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" "github.com/hashicorp/terraform-plugin-framework-validators/internal/primitivevalidator" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/tfsdk" @@ -181,12 +180,12 @@ func TestOneOfValidator(t *testing.T) { t.Fatalf("expected %d error(s), got none", test.expErrors) } - if test.expErrors > 0 && test.expErrors != validatordiag.ErrorsCount(res.Diagnostics) { - t.Fatalf("expected %d error(s), got %d: %v", test.expErrors, validatordiag.ErrorsCount(res.Diagnostics), res.Diagnostics) + 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", validatordiag.ErrorsCount(res.Diagnostics), res.Diagnostics) + t.Fatalf("expected no error(s), got %d: %v", res.Diagnostics.ErrorsCount(), res.Diagnostics) } }) } diff --git a/listvalidator/size_at_least.go b/listvalidator/size_at_least.go index f7724e2e..66c00cfe 100644 --- a/listvalidator/size_at_least.go +++ b/listvalidator/size_at_least.go @@ -16,7 +16,7 @@ type sizeAtLeastValidator struct { } // Description describes the validation in plain text formatting. -func (v sizeAtLeastValidator) Description(ctx context.Context) string { +func (v sizeAtLeastValidator) Description(_ context.Context) string { return fmt.Sprintf("list must contain at least %d elements", v.min) } diff --git a/listvalidator/size_at_least_test.go b/listvalidator/size_at_least_test.go index a03b283f..024f4007 100644 --- a/listvalidator/size_at_least_test.go +++ b/listvalidator/size_at_least_test.go @@ -72,8 +72,9 @@ func TestSizeAtLeastValidator(t *testing.T) { name, test := name, test t.Run(name, func(t *testing.T) { request := tfsdk.ValidateAttributeRequest{ - AttributePath: path.Root("test"), - AttributeConfig: test.val, + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), + AttributeConfig: test.val, } response := tfsdk.ValidateAttributeResponse{} SizeAtLeast(test.min).Validate(context.TODO(), request, &response) diff --git a/listvalidator/size_at_most.go b/listvalidator/size_at_most.go index eb767290..a4dea246 100644 --- a/listvalidator/size_at_most.go +++ b/listvalidator/size_at_most.go @@ -16,7 +16,7 @@ type sizeAtMostValidator struct { } // Description describes the validation in plain text formatting. -func (v sizeAtMostValidator) Description(ctx context.Context) string { +func (v sizeAtMostValidator) Description(_ context.Context) string { return fmt.Sprintf("list must contain at most %d elements", v.max) } diff --git a/listvalidator/size_at_most_test.go b/listvalidator/size_at_most_test.go index 82177922..af4d121a 100644 --- a/listvalidator/size_at_most_test.go +++ b/listvalidator/size_at_most_test.go @@ -75,8 +75,9 @@ func TestSizeAtMostValidator(t *testing.T) { name, test := name, test t.Run(name, func(t *testing.T) { request := tfsdk.ValidateAttributeRequest{ - AttributePath: path.Root("test"), - AttributeConfig: test.val, + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), + AttributeConfig: test.val, } response := tfsdk.ValidateAttributeResponse{} SizeAtMost(test.max).Validate(context.TODO(), request, &response) diff --git a/listvalidator/size_between.go b/listvalidator/size_between.go index 7b7e0672..9c3c5a12 100644 --- a/listvalidator/size_between.go +++ b/listvalidator/size_between.go @@ -18,7 +18,7 @@ type sizeBetweenValidator struct { } // Description describes the validation in plain text formatting. -func (v sizeBetweenValidator) Description(ctx context.Context) string { +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) } diff --git a/listvalidator/size_between_test.go b/listvalidator/size_between_test.go index 76a566ba..7276e66d 100644 --- a/listvalidator/size_between_test.go +++ b/listvalidator/size_between_test.go @@ -115,8 +115,9 @@ func TestSizeBetweenValidator(t *testing.T) { name, test := name, test t.Run(name, func(t *testing.T) { request := tfsdk.ValidateAttributeRequest{ - AttributePath: path.Root("test"), - AttributeConfig: test.val, + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), + AttributeConfig: test.val, } response := tfsdk.ValidateAttributeResponse{} SizeBetween(test.min, test.max).Validate(context.TODO(), request, &response) diff --git a/listvalidator/type_validation.go b/listvalidator/type_validation.go index 4ebeae23..462edf1c 100644 --- a/listvalidator/type_validation.go +++ b/listvalidator/type_validation.go @@ -15,7 +15,7 @@ func validateList(ctx context.Context, request tfsdk.ValidateAttributeRequest, r diags := tfsdk.ValueAs(ctx, request.AttributeConfig, &l) if diags.HasError() { - response.Diagnostics = append(response.Diagnostics, diags...) + response.Diagnostics.Append(diags...) return nil, false } diff --git a/listvalidator/type_validation_test.go b/listvalidator/type_validation_test.go index 29b05705..c2538a8c 100644 --- a/listvalidator/type_validation_test.go +++ b/listvalidator/type_validation_test.go @@ -21,24 +21,27 @@ func TestValidateList(t *testing.T) { }{ "invalid-type": { request: tfsdk.ValidateAttributeRequest{ - AttributeConfig: types.Bool{Value: true}, - AttributePath: path.Root("test"), + AttributeConfig: types.Bool{Value: true}, + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), }, expectedListElems: nil, expectedOk: false, }, "list-null": { request: tfsdk.ValidateAttributeRequest{ - AttributeConfig: types.List{Null: true}, - AttributePath: path.Root("test"), + AttributeConfig: types.List{Null: true}, + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), }, expectedListElems: nil, expectedOk: false, }, "list-unknown": { request: tfsdk.ValidateAttributeRequest{ - AttributeConfig: types.List{Unknown: true}, - AttributePath: path.Root("test"), + AttributeConfig: types.List{Unknown: true}, + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), }, expectedListElems: nil, expectedOk: false, @@ -52,7 +55,8 @@ func TestValidateList(t *testing.T) { types.String{Value: "second"}, }, }, - AttributePath: path.Root("test"), + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), }, expectedListElems: []attr.Value{ types.String{Value: "first"}, diff --git a/listvalidator/values_are.go b/listvalidator/values_are.go index e72c14d5..ac6cb84f 100644 --- a/listvalidator/values_are.go +++ b/listvalidator/values_are.go @@ -38,10 +38,12 @@ func (v valuesAreValidator) Validate(ctx context.Context, req tfsdk.ValidateAttr } for k, elem := range elems { + attrPath := req.AttributePath.AtListIndex(k) request := tfsdk.ValidateAttributeRequest{ - AttributePath: req.AttributePath.AtListIndex(k), - AttributeConfig: elem, - Config: req.Config, + AttributePath: attrPath, + AttributePathExpression: attrPath.Expression(), + AttributeConfig: elem, + Config: req.Config, } for _, validator := range v.valueValidators { diff --git a/listvalidator/values_are_test.go b/listvalidator/values_are_test.go index 1d12c6a1..6b9b1a50 100644 --- a/listvalidator/values_are_test.go +++ b/listvalidator/values_are_test.go @@ -101,8 +101,9 @@ func TestValuesAreValidator(t *testing.T) { name, test := name, test t.Run(name, func(t *testing.T) { request := tfsdk.ValidateAttributeRequest{ - AttributePath: path.Root("test"), - AttributeConfig: test.val, + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), + AttributeConfig: test.val, } response := tfsdk.ValidateAttributeResponse{} ValuesAre(test.valuesAreValidators...).Validate(context.TODO(), request, &response) diff --git a/mapvalidator/keys_are.go b/mapvalidator/keys_are.go index cf4ccf29..5bdf7bfa 100644 --- a/mapvalidator/keys_are.go +++ b/mapvalidator/keys_are.go @@ -42,10 +42,12 @@ func (v keysAreValidator) Validate(ctx context.Context, req tfsdk.ValidateAttrib } for k := range elems { + attrPath := req.AttributePath.AtMapKey(k) request := tfsdk.ValidateAttributeRequest{ - AttributePath: req.AttributePath.AtMapKey(k), - AttributeConfig: types.String{Value: k}, - Config: req.Config, + AttributePath: attrPath, + AttributePathExpression: attrPath.Expression(), + AttributeConfig: types.String{Value: k}, + Config: req.Config, } for _, validator := range v.keyValidators { diff --git a/mapvalidator/keys_are_test.go b/mapvalidator/keys_are_test.go index 61e5eac6..4004b5d5 100644 --- a/mapvalidator/keys_are_test.go +++ b/mapvalidator/keys_are_test.go @@ -101,8 +101,9 @@ func TestKeysAreValidator(t *testing.T) { name, test := name, test t.Run(name, func(t *testing.T) { request := tfsdk.ValidateAttributeRequest{ - AttributePath: path.Root("test"), - AttributeConfig: test.val, + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), + AttributeConfig: test.val, } response := tfsdk.ValidateAttributeResponse{} KeysAre(test.keysAreValidators...).Validate(context.TODO(), request, &response) diff --git a/mapvalidator/size_at_least.go b/mapvalidator/size_at_least.go index 04bb6417..c8cd3363 100644 --- a/mapvalidator/size_at_least.go +++ b/mapvalidator/size_at_least.go @@ -16,7 +16,7 @@ type sizeAtLeastValidator struct { } // Description describes the validation in plain text formatting. -func (v sizeAtLeastValidator) Description(ctx context.Context) string { +func (v sizeAtLeastValidator) Description(_ context.Context) string { return fmt.Sprintf("map must contain at least %d elements", v.min) } diff --git a/mapvalidator/size_at_least_test.go b/mapvalidator/size_at_least_test.go index 1d532042..26cd0b52 100644 --- a/mapvalidator/size_at_least_test.go +++ b/mapvalidator/size_at_least_test.go @@ -72,8 +72,9 @@ func TestSizeAtLeastValidator(t *testing.T) { name, test := name, test t.Run(name, func(t *testing.T) { request := tfsdk.ValidateAttributeRequest{ - AttributePath: path.Root("test"), - AttributeConfig: test.val, + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), + AttributeConfig: test.val, } response := tfsdk.ValidateAttributeResponse{} SizeAtLeast(test.min).Validate(context.TODO(), request, &response) diff --git a/mapvalidator/size_at_most.go b/mapvalidator/size_at_most.go index db6f6d75..cec24759 100644 --- a/mapvalidator/size_at_most.go +++ b/mapvalidator/size_at_most.go @@ -16,7 +16,7 @@ type sizeAtMostValidator struct { } // Description describes the validation in plain text formatting. -func (v sizeAtMostValidator) Description(ctx context.Context) string { +func (v sizeAtMostValidator) Description(_ context.Context) string { return fmt.Sprintf("map must contain at most %d elements", v.max) } diff --git a/mapvalidator/size_at_most_test.go b/mapvalidator/size_at_most_test.go index 881326ad..fe2c327e 100644 --- a/mapvalidator/size_at_most_test.go +++ b/mapvalidator/size_at_most_test.go @@ -75,8 +75,9 @@ func TestSizeAtMostValidator(t *testing.T) { name, test := name, test t.Run(name, func(t *testing.T) { request := tfsdk.ValidateAttributeRequest{ - AttributePath: path.Root("test"), - AttributeConfig: test.val, + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), + AttributeConfig: test.val, } response := tfsdk.ValidateAttributeResponse{} SizeAtMost(test.max).Validate(context.TODO(), request, &response) diff --git a/mapvalidator/size_between.go b/mapvalidator/size_between.go index e7d284e1..033bfc34 100644 --- a/mapvalidator/size_between.go +++ b/mapvalidator/size_between.go @@ -18,7 +18,7 @@ type sizeBetweenValidator struct { } // Description describes the validation in plain text formatting. -func (v sizeBetweenValidator) Description(ctx context.Context) string { +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) } diff --git a/mapvalidator/size_between_test.go b/mapvalidator/size_between_test.go index 27705210..d701ac76 100644 --- a/mapvalidator/size_between_test.go +++ b/mapvalidator/size_between_test.go @@ -115,8 +115,9 @@ func TestSizeBetweenValidator(t *testing.T) { name, test := name, test t.Run(name, func(t *testing.T) { request := tfsdk.ValidateAttributeRequest{ - AttributePath: path.Root("test"), - AttributeConfig: test.val, + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), + AttributeConfig: test.val, } response := tfsdk.ValidateAttributeResponse{} SizeBetween(test.min, test.max).Validate(context.TODO(), request, &response) diff --git a/mapvalidator/type_validation.go b/mapvalidator/type_validation.go index e6448f0c..32450618 100644 --- a/mapvalidator/type_validation.go +++ b/mapvalidator/type_validation.go @@ -15,7 +15,7 @@ func validateMap(ctx context.Context, request tfsdk.ValidateAttributeRequest, re diags := tfsdk.ValueAs(ctx, request.AttributeConfig, &m) if diags.HasError() { - response.Diagnostics = append(response.Diagnostics, diags...) + response.Diagnostics.Append(diags...) return nil, false } diff --git a/mapvalidator/type_validation_test.go b/mapvalidator/type_validation_test.go index 0c3fc084..0c4cf8ae 100644 --- a/mapvalidator/type_validation_test.go +++ b/mapvalidator/type_validation_test.go @@ -21,24 +21,27 @@ func TestValidateMap(t *testing.T) { }{ "invalid-type": { request: tfsdk.ValidateAttributeRequest{ - AttributeConfig: types.Bool{Value: true}, - AttributePath: path.Root("test"), + AttributeConfig: types.Bool{Value: true}, + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), }, expectedMap: nil, expectedOk: false, }, "map-null": { request: tfsdk.ValidateAttributeRequest{ - AttributeConfig: types.Map{Null: true}, - AttributePath: path.Root("test"), + AttributeConfig: types.Map{Null: true}, + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), }, expectedMap: nil, expectedOk: false, }, "map-unknown": { request: tfsdk.ValidateAttributeRequest{ - AttributeConfig: types.Map{Unknown: true}, - AttributePath: path.Root("test"), + AttributeConfig: types.Map{Unknown: true}, + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), }, expectedMap: nil, expectedOk: false, @@ -52,7 +55,8 @@ func TestValidateMap(t *testing.T) { "two": types.String{Value: "second"}, }, }, - AttributePath: path.Root("test"), + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), }, expectedMap: map[string]attr.Value{ "one": types.String{Value: "first"}, diff --git a/mapvalidator/values_are.go b/mapvalidator/values_are.go index 8a8ba0b9..cef36290 100644 --- a/mapvalidator/values_are.go +++ b/mapvalidator/values_are.go @@ -38,10 +38,12 @@ func (v valuesAreValidator) Validate(ctx context.Context, req tfsdk.ValidateAttr } for k, elem := range elems { + attrPath := req.AttributePath.AtMapKey(k) request := tfsdk.ValidateAttributeRequest{ - AttributePath: req.AttributePath.AtMapKey(k), - AttributeConfig: elem, - Config: req.Config, + AttributePath: attrPath, + AttributePathExpression: attrPath.Expression(), + AttributeConfig: elem, + Config: req.Config, } for _, validator := range v.valueValidators { diff --git a/mapvalidator/values_are_test.go b/mapvalidator/values_are_test.go index 91680b39..d4eda8a8 100644 --- a/mapvalidator/values_are_test.go +++ b/mapvalidator/values_are_test.go @@ -95,8 +95,9 @@ func TestValuesAreValidator(t *testing.T) { name, test := name, test t.Run(name, func(t *testing.T) { request := tfsdk.ValidateAttributeRequest{ - AttributePath: path.Root("test"), - AttributeConfig: test.val, + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), + AttributeConfig: test.val, } response := tfsdk.ValidateAttributeResponse{} ValuesAre(test.valuesAreValidators...).Validate(context.TODO(), request, &response) diff --git a/metavalidator/all_test.go b/metavalidator/all_test.go index 8a0836a9..fa0bef57 100644 --- a/metavalidator/all_test.go +++ b/metavalidator/all_test.go @@ -70,8 +70,9 @@ func TestAllValidator(t *testing.T) { name, test := name, test t.Run(name, func(t *testing.T) { request := tfsdk.ValidateAttributeRequest{ - AttributePath: path.Root("test"), - AttributeConfig: test.val, + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), + AttributeConfig: test.val, } response := tfsdk.ValidateAttributeResponse{} metavalidator.All(test.valueValidators...).Validate(context.TODO(), request, &response) diff --git a/metavalidator/any_test.go b/metavalidator/any_test.go index f4f9a6b4..29dc48e3 100644 --- a/metavalidator/any_test.go +++ b/metavalidator/any_test.go @@ -142,8 +142,9 @@ func TestAnyValidator(t *testing.T) { name, test := name, test t.Run(name, func(t *testing.T) { request := tfsdk.ValidateAttributeRequest{ - AttributePath: path.Root("test"), - AttributeConfig: test.val, + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), + AttributeConfig: test.val, } response := tfsdk.ValidateAttributeResponse{} metavalidator.Any(test.valueValidators...).Validate(context.TODO(), request, &response) diff --git a/metavalidator/any_with_all_warnings_test.go b/metavalidator/any_with_all_warnings_test.go index 20798728..1707a89b 100644 --- a/metavalidator/any_with_all_warnings_test.go +++ b/metavalidator/any_with_all_warnings_test.go @@ -164,8 +164,9 @@ func TestAnyWithAllWarningsValidator(t *testing.T) { name, test := name, test t.Run(name, func(t *testing.T) { request := tfsdk.ValidateAttributeRequest{ - AttributePath: path.Root("test"), - AttributeConfig: test.val, + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), + AttributeConfig: test.val, } response := tfsdk.ValidateAttributeResponse{} metavalidator.AnyWithAllWarnings(test.valueValidators...).Validate(context.TODO(), request, &response) diff --git a/numbervalidator/none_of_test.go b/numbervalidator/none_of_test.go index b9eb386f..53623a90 100644 --- a/numbervalidator/none_of_test.go +++ b/numbervalidator/none_of_test.go @@ -5,7 +5,6 @@ import ( "math/big" "testing" - "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" "github.com/hashicorp/terraform-plugin-framework-validators/numbervalidator" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/tfsdk" @@ -74,12 +73,12 @@ func TestNoneOfValidator(t *testing.T) { t.Fatalf("expected %d error(s), got none", test.expErrors) } - if test.expErrors > 0 && test.expErrors != validatordiag.ErrorsCount(res.Diagnostics) { - t.Fatalf("expected %d error(s), got %d: %v", test.expErrors, validatordiag.ErrorsCount(res.Diagnostics), res.Diagnostics) + 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", validatordiag.ErrorsCount(res.Diagnostics), res.Diagnostics) + t.Fatalf("expected no error(s), got %d: %v", res.Diagnostics.ErrorsCount(), res.Diagnostics) } }) } diff --git a/numbervalidator/one_of_test.go b/numbervalidator/one_of_test.go index f90da2f4..e9f76b17 100644 --- a/numbervalidator/one_of_test.go +++ b/numbervalidator/one_of_test.go @@ -5,7 +5,6 @@ import ( "math/big" "testing" - "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" "github.com/hashicorp/terraform-plugin-framework-validators/numbervalidator" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/tfsdk" @@ -74,12 +73,12 @@ func TestOneOfValidator(t *testing.T) { t.Fatalf("expected %d error(s), got none", test.expErrors) } - if test.expErrors > 0 && test.expErrors != validatordiag.ErrorsCount(res.Diagnostics) { - t.Fatalf("expected %d error(s), got %d: %v", test.expErrors, validatordiag.ErrorsCount(res.Diagnostics), res.Diagnostics) + 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", validatordiag.ErrorsCount(res.Diagnostics), res.Diagnostics) + t.Fatalf("expected no error(s), got %d: %v", res.Diagnostics.ErrorsCount(), res.Diagnostics) } }) } diff --git a/schemavalidator/also_requires.go b/schemavalidator/also_requires.go new file mode 100644 index 00000000..92452fce --- /dev/null +++ b/schemavalidator/also_requires.go @@ -0,0 +1,85 @@ +package schemavalidator + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" +) + +// alsoRequiresAttributeValidator is the underlying struct implementing AlsoRequires. +type alsoRequiresAttributeValidator struct { + pathExpressions path.Expressions +} + +// AlsoRequires checks that a set of path.Expression has a non-null value, +// if the current attribute also has a non-null value. +// +// This implements the validation logic declaratively within the tfsdk.Schema. +// +// Relative path.Expression will be resolved against the validated attribute. +func AlsoRequires(attributePaths ...path.Expression) tfsdk.AttributeValidator { + return &alsoRequiresAttributeValidator{attributePaths} +} + +var _ tfsdk.AttributeValidator = (*alsoRequiresAttributeValidator)(nil) + +func (av alsoRequiresAttributeValidator) Description(ctx context.Context) string { + return av.MarkdownDescription(ctx) +} + +func (av alsoRequiresAttributeValidator) MarkdownDescription(_ context.Context) string { + return fmt.Sprintf("Ensure that if an attribute is set, also these are set: %q", av.pathExpressions) +} + +func (av alsoRequiresAttributeValidator) Validate(ctx context.Context, req tfsdk.ValidateAttributeRequest, res *tfsdk.ValidateAttributeResponse) { + // If attribute configuration is null, there is nothing else to validate + if req.AttributeConfig.IsNull() { + return + } + + expressions := req.AttributePathExpression.MergeExpressions(av.pathExpressions...) + + for _, expression := range expressions { + matchedPaths, diags := req.Config.PathMatches(ctx, expression) + + res.Diagnostics.Append(diags...) + + // Collect all errors + if diags.HasError() { + continue + } + + for _, mp := range matchedPaths { + // If the user specifies the same attribute this validator is applied to, + // also as part of the input, skip it + if mp.Equal(req.AttributePath) { + continue + } + + var mpVal attr.Value + diags := req.Config.GetAttribute(ctx, mp, &mpVal) + res.Diagnostics.Append(diags...) + + // Collect all errors + if diags.HasError() { + continue + } + + // Delay validation until all involved attribute have a known value + if mpVal.IsUnknown() { + return + } + + if mpVal.IsNull() { + res.Diagnostics.Append(validatordiag.InvalidAttributeCombinationDiagnostic( + req.AttributePath, + fmt.Sprintf("Attribute %q must be specified when %q is specified", mp, req.AttributePath), + )) + } + } + } +} diff --git a/schemavalidator/also_requires_test.go b/schemavalidator/also_requires_test.go new file mode 100644 index 00000000..46bb89c6 --- /dev/null +++ b/schemavalidator/also_requires_test.go @@ -0,0 +1,290 @@ +package schemavalidator_test + +import ( + "context" + "testing" + + "github.com/hashicorp/terraform-plugin-framework-validators/schemavalidator" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestRequiredWithValidator(t *testing.T) { + t.Parallel() + + type testCase struct { + req tfsdk.ValidateAttributeRequest + in path.Expressions + expErrors int + } + + testCases := map[string]testCase{ + "base": { + req: tfsdk.ValidateAttributeRequest{ + AttributeConfig: types.String{Value: "bar value"}, + AttributePath: path.Root("bar"), + AttributePathExpression: path.MatchRoot("bar"), + Config: tfsdk.Config{ + Schema: tfsdk.Schema{ + Attributes: map[string]tfsdk.Attribute{ + "foo": { + Type: types.Int64Type, + }, + "bar": { + Type: types.StringType, + }, + }, + }, + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "foo": tftypes.Number, + "bar": tftypes.String, + }, + }, map[string]tftypes.Value{ + "foo": tftypes.NewValue(tftypes.Number, 42), + "bar": tftypes.NewValue(tftypes.String, "bar value"), + }), + }, + }, + in: path.Expressions{ + path.MatchRoot("foo"), + }, + }, + "self-is-null": { + req: tfsdk.ValidateAttributeRequest{ + AttributeConfig: types.String{Null: true}, + AttributePath: path.Root("bar"), + AttributePathExpression: path.MatchRoot("bar"), + Config: tfsdk.Config{ + Schema: tfsdk.Schema{ + Attributes: map[string]tfsdk.Attribute{ + "foo": { + Type: types.Int64Type, + }, + "bar": { + Type: types.StringType, + }, + }, + }, + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "foo": tftypes.Number, + "bar": tftypes.String, + }, + }, map[string]tftypes.Value{ + "foo": tftypes.NewValue(tftypes.Number, 42), + "bar": tftypes.NewValue(tftypes.String, nil), + }), + }, + }, + in: path.Expressions{ + path.MatchRoot("foo"), + }, + }, + "error_missing-one": { + req: tfsdk.ValidateAttributeRequest{ + AttributeConfig: types.String{Value: "bar value"}, + AttributePath: path.Root("bar"), + AttributePathExpression: path.MatchRoot("bar"), + Config: tfsdk.Config{ + Schema: tfsdk.Schema{ + Attributes: map[string]tfsdk.Attribute{ + "foo": { + Type: types.Int64Type, + }, + "bar": { + Type: types.StringType, + }, + "baz": { + Type: types.Int64Type, + }, + }, + }, + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "foo": tftypes.Number, + "bar": tftypes.String, + "baz": tftypes.Number, + }, + }, map[string]tftypes.Value{ + "foo": tftypes.NewValue(tftypes.Number, 42), + "bar": tftypes.NewValue(tftypes.String, "bar value"), + "baz": tftypes.NewValue(tftypes.Number, nil), + }), + }, + }, + in: path.Expressions{ + path.MatchRoot("foo"), + path.MatchRoot("baz"), + }, + expErrors: 1, + }, + "error_missing-two": { + req: tfsdk.ValidateAttributeRequest{ + AttributeConfig: types.String{Value: "bar value"}, + AttributePath: path.Root("bar"), + AttributePathExpression: path.MatchRoot("bar"), + Config: tfsdk.Config{ + Schema: tfsdk.Schema{ + Attributes: map[string]tfsdk.Attribute{ + "foo": { + Type: types.Int64Type, + }, + "bar": { + Type: types.StringType, + }, + "baz": { + Type: types.Int64Type, + }, + }, + }, + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "foo": tftypes.Number, + "bar": tftypes.String, + "baz": tftypes.Number, + }, + }, map[string]tftypes.Value{ + "foo": tftypes.NewValue(tftypes.Number, nil), + "bar": tftypes.NewValue(tftypes.String, "bar value"), + "baz": tftypes.NewValue(tftypes.Number, nil), + }), + }, + }, + in: path.Expressions{ + path.MatchRoot("foo"), + path.MatchRoot("baz"), + }, + expErrors: 2, + }, + "allow-duplicate-input": { + req: tfsdk.ValidateAttributeRequest{ + AttributeConfig: types.String{Value: "bar value"}, + AttributePath: path.Root("bar"), + AttributePathExpression: path.MatchRoot("bar"), + Config: tfsdk.Config{ + Schema: tfsdk.Schema{ + Attributes: map[string]tfsdk.Attribute{ + "foo": { + Type: types.Int64Type, + }, + "bar": { + Type: types.StringType, + }, + "baz": { + Type: types.Int64Type, + }, + }, + }, + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "foo": tftypes.Number, + "bar": tftypes.String, + "baz": tftypes.Number, + }, + }, map[string]tftypes.Value{ + "foo": tftypes.NewValue(tftypes.Number, 42), + "bar": tftypes.NewValue(tftypes.String, "bar value"), + "baz": tftypes.NewValue(tftypes.Number, 43), + }), + }, + }, + in: path.Expressions{ + path.MatchRoot("foo"), + path.MatchRoot("bar"), + path.MatchRoot("baz"), + }, + }, + "unknowns": { + req: tfsdk.ValidateAttributeRequest{ + AttributeConfig: types.String{Value: "bar value"}, + AttributePath: path.Root("bar"), + AttributePathExpression: path.MatchRoot("bar"), + Config: tfsdk.Config{ + Schema: tfsdk.Schema{ + Attributes: map[string]tfsdk.Attribute{ + "foo": { + Type: types.Int64Type, + }, + "bar": { + Type: types.StringType, + }, + "baz": { + Type: types.Int64Type, + }, + }, + }, + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "foo": tftypes.Number, + "bar": tftypes.String, + "baz": tftypes.Number, + }, + }, map[string]tftypes.Value{ + "foo": tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), + "bar": tftypes.NewValue(tftypes.String, "bar value"), + "baz": tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), + }), + }, + }, + in: path.Expressions{ + path.MatchRoot("foo"), + path.MatchRoot("baz"), + }, + }, + "matches-no-attribute-in-schema": { + req: tfsdk.ValidateAttributeRequest{ + AttributeConfig: types.String{Value: "bar value"}, + AttributePath: path.Root("bar"), + AttributePathExpression: path.MatchRoot("bar"), + Config: tfsdk.Config{ + Schema: tfsdk.Schema{ + Attributes: map[string]tfsdk.Attribute{ + "foo": { + Type: types.Int64Type, + }, + "bar": { + Type: types.StringType, + }, + }, + }, + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "foo": tftypes.Number, + "bar": tftypes.String, + }, + }, map[string]tftypes.Value{ + "foo": tftypes.NewValue(tftypes.Number, 42), + "bar": tftypes.NewValue(tftypes.String, "bar value"), + }), + }, + }, + in: path.Expressions{ + path.MatchRoot("fooz"), + }, + expErrors: 1, + }, + } + + for name, test := range testCases { + t.Run(name, func(t *testing.T) { + res := tfsdk.ValidateAttributeResponse{} + + schemavalidator.AlsoRequires(test.in...).Validate(context.TODO(), test.req, &res) + + if test.expErrors > 0 && !res.Diagnostics.HasError() { + t.Fatal("expected error(s), got none") + } + + 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) + } + }) + } +} diff --git a/schemavalidator/at_least_one_of.go b/schemavalidator/at_least_one_of.go new file mode 100644 index 00000000..c87aa981 --- /dev/null +++ b/schemavalidator/at_least_one_of.go @@ -0,0 +1,82 @@ +package schemavalidator + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" +) + +// atLeastOneOfAttributeValidator is the underlying struct implementing AtLeastOneOf. +type atLeastOneOfAttributeValidator struct { + pathExpressions path.Expressions +} + +// AtLeastOneOf checks that of a set of path.Expression, +// including the attribute it's applied to, +// at least one attribute out of all specified is has a non-null value. +// +// This implements the validation logic declaratively within the tfsdk.Schema. +// +// Any relative path.Expression will be resolved against the attribute with this validator. +func AtLeastOneOf(attributePaths ...path.Expression) tfsdk.AttributeValidator { + return &atLeastOneOfAttributeValidator{attributePaths} +} + +var _ tfsdk.AttributeValidator = (*atLeastOneOfAttributeValidator)(nil) + +func (av atLeastOneOfAttributeValidator) Description(ctx context.Context) string { + return av.MarkdownDescription(ctx) +} + +func (av atLeastOneOfAttributeValidator) MarkdownDescription(_ context.Context) string { + return fmt.Sprintf("Ensure that at least one attribute from this collection is set: %s", av.pathExpressions) +} + +func (av atLeastOneOfAttributeValidator) Validate(ctx context.Context, req tfsdk.ValidateAttributeRequest, res *tfsdk.ValidateAttributeResponse) { + // If attribute configuration is not null, validator already succeeded. + if !req.AttributeConfig.IsNull() { + return + } + + expressions := req.AttributePathExpression.MergeExpressions(av.pathExpressions...) + + for _, expression := range expressions { + matchedPaths, diags := req.Config.PathMatches(ctx, expression) + + res.Diagnostics.Append(diags...) + + // Collect all errors + if diags.HasError() { + continue + } + + for _, mp := range matchedPaths { + var mpVal attr.Value + diags := req.Config.GetAttribute(ctx, mp, &mpVal) + res.Diagnostics.Append(diags...) + + // Collect all errors + if diags.HasError() { + continue + } + + // Delay validation until all involved attribute have a known value + if mpVal.IsUnknown() { + return + } + + if !mpVal.IsNull() { + return + } + } + } + + res.Diagnostics.Append(validatordiag.InvalidAttributeCombinationDiagnostic( + req.AttributePath, + fmt.Sprintf("At least one attribute out of %s must be specified", expressions), + )) +} diff --git a/schemavalidator/at_least_one_of_test.go b/schemavalidator/at_least_one_of_test.go new file mode 100644 index 00000000..45a1dfd8 --- /dev/null +++ b/schemavalidator/at_least_one_of_test.go @@ -0,0 +1,289 @@ +package schemavalidator_test + +import ( + "context" + "testing" + + "github.com/hashicorp/terraform-plugin-framework-validators/schemavalidator" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestAtLeastOneOfValidator(t *testing.T) { + t.Parallel() + + type testCase struct { + req tfsdk.ValidateAttributeRequest + in path.Expressions + expErrors int + } + + testCases := map[string]testCase{ + "base": { + req: tfsdk.ValidateAttributeRequest{ + AttributeConfig: types.String{Value: "bar value"}, + AttributePath: path.Root("bar"), + AttributePathExpression: path.MatchRoot("bar"), + Config: tfsdk.Config{ + Schema: tfsdk.Schema{ + Attributes: map[string]tfsdk.Attribute{ + "foo": { + Type: types.Int64Type, + }, + "bar": { + Type: types.StringType, + }, + }, + }, + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "foo": tftypes.Number, + "bar": tftypes.String, + }, + }, map[string]tftypes.Value{ + "foo": tftypes.NewValue(tftypes.Number, 42), + "bar": tftypes.NewValue(tftypes.String, "bar value"), + }), + }, + }, + in: path.Expressions{ + path.MatchRoot("foo"), + }, + }, + "self-is-null": { + req: tfsdk.ValidateAttributeRequest{ + AttributeConfig: types.String{Null: true}, + AttributePath: path.Root("bar"), + AttributePathExpression: path.MatchRoot("bar"), + Config: tfsdk.Config{ + Schema: tfsdk.Schema{ + Attributes: map[string]tfsdk.Attribute{ + "foo": { + Type: types.Int64Type, + }, + "bar": { + Type: types.StringType, + }, + }, + }, + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "foo": tftypes.Number, + "bar": tftypes.String, + }, + }, map[string]tftypes.Value{ + "foo": tftypes.NewValue(tftypes.Number, 42), + "bar": tftypes.NewValue(tftypes.String, nil), + }), + }, + }, + in: path.Expressions{ + path.MatchRoot("foo"), + }, + }, + "error_none-set": { + req: tfsdk.ValidateAttributeRequest{ + AttributeConfig: types.String{Null: true}, + AttributePath: path.Root("bar"), + AttributePathExpression: path.MatchRoot("bar"), + Config: tfsdk.Config{ + Schema: tfsdk.Schema{ + Attributes: map[string]tfsdk.Attribute{ + "foo": { + Type: types.Int64Type, + }, + "bar": { + Type: types.StringType, + }, + "baz": { + Type: types.Int64Type, + }, + }, + }, + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "foo": tftypes.Number, + "bar": tftypes.String, + "baz": tftypes.Number, + }, + }, map[string]tftypes.Value{ + "foo": tftypes.NewValue(tftypes.Number, nil), + "bar": tftypes.NewValue(tftypes.String, nil), + "baz": tftypes.NewValue(tftypes.Number, nil), + }), + }, + }, + in: path.Expressions{ + path.MatchRoot("foo"), + path.MatchRoot("baz"), + }, + expErrors: 1, + }, + "multiple-set": { + req: tfsdk.ValidateAttributeRequest{ + AttributeConfig: types.String{Value: "bar value"}, + AttributePath: path.Root("bar"), + AttributePathExpression: path.MatchRoot("bar"), + Config: tfsdk.Config{ + Schema: tfsdk.Schema{ + Attributes: map[string]tfsdk.Attribute{ + "foo": { + Type: types.Int64Type, + }, + "bar": { + Type: types.StringType, + }, + "baz": { + Type: types.Float64Type, + }, + }, + }, + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "foo": tftypes.Number, + "bar": tftypes.String, + "baz": tftypes.Number, + }, + }, map[string]tftypes.Value{ + "foo": tftypes.NewValue(tftypes.Number, 42), + "bar": tftypes.NewValue(tftypes.String, "bar value"), + "baz": tftypes.NewValue(tftypes.Number, 4.2), + }), + }, + }, + in: path.Expressions{ + path.MatchRoot("foo"), + path.MatchRoot("baz"), + }, + }, + "allow-duplicate-input": { + req: tfsdk.ValidateAttributeRequest{ + AttributeConfig: types.String{Value: "bar value"}, + AttributePath: path.Root("bar"), + AttributePathExpression: path.MatchRoot("bar"), + Config: tfsdk.Config{ + Schema: tfsdk.Schema{ + Attributes: map[string]tfsdk.Attribute{ + "foo": { + Type: types.Int64Type, + }, + "bar": { + Type: types.StringType, + }, + "baz": { + Type: types.Int64Type, + }, + }, + }, + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "foo": tftypes.Number, + "bar": tftypes.String, + "baz": tftypes.Number, + }, + }, map[string]tftypes.Value{ + "foo": tftypes.NewValue(tftypes.Number, nil), + "bar": tftypes.NewValue(tftypes.String, "bar value"), + "baz": tftypes.NewValue(tftypes.Number, nil), + }), + }, + }, + in: path.Expressions{ + path.MatchRoot("foo"), + path.MatchRoot("bar"), + path.MatchRoot("baz"), + }, + }, + "unknowns": { + req: tfsdk.ValidateAttributeRequest{ + AttributeConfig: types.String{Value: "bar value"}, + AttributePath: path.Root("bar"), + AttributePathExpression: path.MatchRoot("bar"), + Config: tfsdk.Config{ + Schema: tfsdk.Schema{ + Attributes: map[string]tfsdk.Attribute{ + "foo": { + Type: types.Int64Type, + }, + "bar": { + Type: types.StringType, + }, + "baz": { + Type: types.Int64Type, + }, + }, + }, + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "foo": tftypes.Number, + "bar": tftypes.String, + "baz": tftypes.Number, + }, + }, map[string]tftypes.Value{ + "foo": tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), + "bar": tftypes.NewValue(tftypes.String, "bar value"), + "baz": tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), + }), + }, + }, + in: path.Expressions{ + path.MatchRoot("foo"), + path.MatchRoot("baz"), + }, + }, + "matches-no-attribute-in-schema": { + req: tfsdk.ValidateAttributeRequest{ + AttributeConfig: types.String{Null: true}, + AttributePath: path.Root("bar"), + AttributePathExpression: path.MatchRoot("bar"), + Config: tfsdk.Config{ + Schema: tfsdk.Schema{ + Attributes: map[string]tfsdk.Attribute{ + "foo": { + Type: types.Int64Type, + }, + "bar": { + Type: types.StringType, + }, + }, + }, + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "foo": tftypes.Number, + "bar": tftypes.String, + }, + }, map[string]tftypes.Value{ + "foo": tftypes.NewValue(tftypes.Number, 42), + "bar": tftypes.NewValue(tftypes.String, nil), + }), + }, + }, + in: path.Expressions{ + path.MatchRoot("fooz"), + }, + expErrors: 2, + }, + } + + for name, test := range testCases { + t.Run(name, func(t *testing.T) { + res := tfsdk.ValidateAttributeResponse{} + + schemavalidator.AtLeastOneOf(test.in...).Validate(context.TODO(), test.req, &res) + + if test.expErrors > 0 && !res.Diagnostics.HasError() { + t.Fatal("expected error(s), got none") + } + + 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) + } + }) + } +} diff --git a/schemavalidator/conflicts_with.go b/schemavalidator/conflicts_with.go new file mode 100644 index 00000000..306b5d58 --- /dev/null +++ b/schemavalidator/conflicts_with.go @@ -0,0 +1,85 @@ +package schemavalidator + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" +) + +// conflictsWithAttributeValidator is the underlying struct implementing ConflictsWith. +type conflictsWithAttributeValidator struct { + pathExpressions path.Expressions +} + +// ConflictsWith checks that a set of path.Expression, +// including the attribute it's applied to, do not have a value simultaneously. +// +// This implements the validation logic declaratively within the tfsdk.Schema. +// +// Relative path.Expression will be resolved against the validated attribute. +func ConflictsWith(attributePaths ...path.Expression) tfsdk.AttributeValidator { + return &conflictsWithAttributeValidator{attributePaths} +} + +var _ tfsdk.AttributeValidator = (*conflictsWithAttributeValidator)(nil) + +func (av conflictsWithAttributeValidator) Description(ctx context.Context) string { + return av.MarkdownDescription(ctx) +} + +func (av conflictsWithAttributeValidator) MarkdownDescription(_ context.Context) string { + return fmt.Sprintf("Ensure that if an attribute is set, these are not set: %q", av.pathExpressions) +} + +func (av conflictsWithAttributeValidator) Validate(ctx context.Context, req tfsdk.ValidateAttributeRequest, res *tfsdk.ValidateAttributeResponse) { + // If attribute configuration is null, it cannot conflict with others + if req.AttributeConfig.IsNull() { + return + } + + expressions := req.AttributePathExpression.MergeExpressions(av.pathExpressions...) + + for _, expression := range expressions { + matchedPaths, diags := req.Config.PathMatches(ctx, expression) + + res.Diagnostics.Append(diags...) + + // Collect all errors + if diags.HasError() { + continue + } + + for _, mp := range matchedPaths { + // If the user specifies the same attribute this validator is applied to, + // also as part of the input, skip it + if mp.Equal(req.AttributePath) { + continue + } + + var mpVal attr.Value + diags := req.Config.GetAttribute(ctx, mp, &mpVal) + res.Diagnostics.Append(diags...) + + // Collect all errors + if diags.HasError() { + continue + } + + // Delay validation until all involved attribute have a known value + if mpVal.IsUnknown() { + return + } + + if !mpVal.IsNull() { + res.Diagnostics.Append(validatordiag.InvalidAttributeCombinationDiagnostic( + req.AttributePath, + fmt.Sprintf("Attribute %q cannot be specified when %q is specified", mp, req.AttributePath), + )) + } + } + } +} diff --git a/schemavalidator/conflicts_with_test.go b/schemavalidator/conflicts_with_test.go new file mode 100644 index 00000000..bd151f8e --- /dev/null +++ b/schemavalidator/conflicts_with_test.go @@ -0,0 +1,291 @@ +package schemavalidator_test + +import ( + "context" + "testing" + + "github.com/hashicorp/terraform-plugin-framework-validators/schemavalidator" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestConflictsWithValidator(t *testing.T) { + t.Parallel() + + type testCase struct { + req tfsdk.ValidateAttributeRequest + in path.Expressions + expErrors int + } + + testCases := map[string]testCase{ + "base": { + req: tfsdk.ValidateAttributeRequest{ + AttributeConfig: types.String{Value: "bar value"}, + AttributePath: path.Root("bar"), + AttributePathExpression: path.MatchRoot("bar"), + Config: tfsdk.Config{ + Schema: tfsdk.Schema{ + Attributes: map[string]tfsdk.Attribute{ + "foo": { + Type: types.Int64Type, + }, + "bar": { + Type: types.StringType, + }, + "baz": { + Type: types.Int64Type, + }, + }, + }, + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "foo": tftypes.Number, + "bar": tftypes.String, + "baz": tftypes.Number, + }, + }, map[string]tftypes.Value{ + "foo": tftypes.NewValue(tftypes.Number, 42), + "bar": tftypes.NewValue(tftypes.String, "bar value"), + "baz": tftypes.NewValue(tftypes.Number, 43), + }), + }, + }, + in: path.Expressions{ + path.MatchRoot("foo"), + path.MatchRoot("baz"), + }, + expErrors: 2, + }, + "conflicting-is-nil": { + req: tfsdk.ValidateAttributeRequest{ + AttributeConfig: types.String{Value: "bar value"}, + AttributePath: path.Root("bar"), + AttributePathExpression: path.MatchRoot("bar"), + Config: tfsdk.Config{ + Schema: tfsdk.Schema{ + Attributes: map[string]tfsdk.Attribute{ + "foo": { + Type: types.Int64Type, + }, + "bar": { + Type: types.StringType, + }, + }, + }, + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "foo": tftypes.Number, + "bar": tftypes.String, + }, + }, map[string]tftypes.Value{ + "foo": tftypes.NewValue(tftypes.Number, nil), + "bar": tftypes.NewValue(tftypes.String, "bar value"), + }), + }, + }, + in: path.Expressions{ + path.MatchRoot("foo"), + }, + }, + "conflicting-is-unknown": { + req: tfsdk.ValidateAttributeRequest{ + AttributeConfig: types.String{Value: "bar value"}, + AttributePath: path.Root("bar"), + AttributePathExpression: path.MatchRoot("bar"), + Config: tfsdk.Config{ + Schema: tfsdk.Schema{ + Attributes: map[string]tfsdk.Attribute{ + "foo": { + Type: types.Int64Type, + }, + "bar": { + Type: types.StringType, + }, + }, + }, + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "foo": tftypes.Number, + "bar": tftypes.String, + }, + }, map[string]tftypes.Value{ + "foo": tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), + "bar": tftypes.NewValue(tftypes.String, "bar value"), + }), + }, + }, + in: path.Expressions{ + path.MatchRoot("foo"), + }, + }, + "self-is-null": { + req: tfsdk.ValidateAttributeRequest{ + AttributeConfig: types.String{Null: true}, + AttributePath: path.Root("bar"), + AttributePathExpression: path.MatchRoot("bar"), + Config: tfsdk.Config{ + Schema: tfsdk.Schema{ + Attributes: map[string]tfsdk.Attribute{ + "foo": { + Type: types.Int64Type, + }, + "bar": { + Type: types.StringType, + }, + }, + }, + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "foo": tftypes.Number, + "bar": tftypes.String, + }, + }, map[string]tftypes.Value{ + "foo": tftypes.NewValue(tftypes.Number, 42), + "bar": tftypes.NewValue(tftypes.String, nil), + }), + }, + }, + in: path.Expressions{ + path.MatchRoot("foo"), + }, + }, + "error_allow-duplicate-input": { + req: tfsdk.ValidateAttributeRequest{ + AttributeConfig: types.String{Value: "bar value"}, + AttributePath: path.Root("bar"), + AttributePathExpression: path.MatchRoot("bar"), + Config: tfsdk.Config{ + Schema: tfsdk.Schema{ + Attributes: map[string]tfsdk.Attribute{ + "foo": { + Type: types.Int64Type, + }, + "bar": { + Type: types.StringType, + }, + "baz": { + Type: types.Int64Type, + }, + }, + }, + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "foo": tftypes.Number, + "bar": tftypes.String, + "baz": tftypes.Number, + }, + }, map[string]tftypes.Value{ + "foo": tftypes.NewValue(tftypes.Number, 42), + "bar": tftypes.NewValue(tftypes.String, "bar value"), + "baz": tftypes.NewValue(tftypes.Number, 43), + }), + }, + }, + in: path.Expressions{ + path.MatchRoot("foo"), + path.MatchRoot("bar"), + path.MatchRoot("baz"), + }, + expErrors: 2, + }, + "error_unknowns": { + req: tfsdk.ValidateAttributeRequest{ + AttributeConfig: types.String{Value: "bar value"}, + AttributePath: path.Root("bar"), + AttributePathExpression: path.MatchRoot("bar"), + Config: tfsdk.Config{ + Schema: tfsdk.Schema{ + Attributes: map[string]tfsdk.Attribute{ + "foo": { + Type: types.Int64Type, + }, + "bar": { + Type: types.StringType, + }, + "baz": { + Type: types.Int64Type, + }, + }, + }, + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "foo": tftypes.Number, + "bar": tftypes.String, + "baz": tftypes.Number, + }, + }, map[string]tftypes.Value{ + "foo": tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), + "bar": tftypes.NewValue(tftypes.String, "bar value"), + "baz": tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), + }), + }, + }, + in: path.Expressions{ + path.MatchRoot("foo"), + path.MatchRoot("baz"), + }, + //expErrors: 2, + }, + "matches-no-attribute-in-schema": { + req: tfsdk.ValidateAttributeRequest{ + AttributeConfig: types.String{Value: "bar value"}, + AttributePath: path.Root("bar"), + AttributePathExpression: path.MatchRoot("bar"), + Config: tfsdk.Config{ + Schema: tfsdk.Schema{ + Attributes: map[string]tfsdk.Attribute{ + "foo": { + Type: types.Int64Type, + }, + "bar": { + Type: types.StringType, + }, + "baz": { + Type: types.Int64Type, + }, + }, + }, + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "foo": tftypes.Number, + "bar": tftypes.String, + "baz": tftypes.Number, + }, + }, map[string]tftypes.Value{ + "foo": tftypes.NewValue(tftypes.Number, 42), + "bar": tftypes.NewValue(tftypes.String, "bar value"), + "baz": tftypes.NewValue(tftypes.Number, 43), + }), + }, + }, + in: path.Expressions{ + path.MatchRoot("fooz"), + path.MatchRoot("barz"), + }, + expErrors: 2, + }, + } + + for name, test := range testCases { + t.Run(name, func(t *testing.T) { + res := tfsdk.ValidateAttributeResponse{} + + schemavalidator.ConflictsWith(test.in...).Validate(context.TODO(), test.req, &res) + + if test.expErrors > 0 && !res.Diagnostics.HasError() { + t.Fatal("expected error(s), got none") + } + + 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) + } + }) + } +} diff --git a/schemavalidator/doc.go b/schemavalidator/doc.go new file mode 100644 index 00000000..ddd097b8 --- /dev/null +++ b/schemavalidator/doc.go @@ -0,0 +1,4 @@ +// Package schemavalidator provides validators to express relationships between +// multiple attributes within the schema of a resource, data source, or provider. +// For example, checking that an attribute is present when another is present, or vice-versa. +package schemavalidator diff --git a/schemavalidator/exactly_one_of.go b/schemavalidator/exactly_one_of.go new file mode 100644 index 00000000..8b098bcb --- /dev/null +++ b/schemavalidator/exactly_one_of.go @@ -0,0 +1,107 @@ +package schemavalidator + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" +) + +// exactlyOneOfAttributeValidator is the underlying struct implementing ExactlyOneOf. +type exactlyOneOfAttributeValidator struct { + pathExpressions path.Expressions +} + +// ExactlyOneOf checks that of a set of path.Expression, +// including the attribute it's applied to, +// one and only one attribute out of all specified has a value. +// It will also cause a validation error if none are specified. +// +// This implements the validation logic declaratively within the tfsdk.Schema. +// +// Relative path.Expression will be resolved against the validated attribute. +func ExactlyOneOf(attributePaths ...path.Expression) tfsdk.AttributeValidator { + return &exactlyOneOfAttributeValidator{attributePaths} +} + +var _ tfsdk.AttributeValidator = (*exactlyOneOfAttributeValidator)(nil) + +func (av exactlyOneOfAttributeValidator) Description(ctx context.Context) string { + return av.MarkdownDescription(ctx) +} + +func (av exactlyOneOfAttributeValidator) MarkdownDescription(_ context.Context) string { + return fmt.Sprintf("Ensure that one and only one attribute from this collection is set: %q", av.pathExpressions) +} + +func (av exactlyOneOfAttributeValidator) Validate(ctx context.Context, req tfsdk.ValidateAttributeRequest, res *tfsdk.ValidateAttributeResponse) { + count := 0 + expressions := req.AttributePathExpression.MergeExpressions(av.pathExpressions...) + + // If current attribute is unknown, delay validation + if req.AttributeConfig.IsUnknown() { + return + } + + // Now that we know the current attribute is known, check whether it is + // null to determine if it should contribute to the count. Later logic + // will remove a duplicate matching path, should it be included in the + // given expressions. + if !req.AttributeConfig.IsNull() { + count++ + } + + for _, expression := range expressions { + matchedPaths, diags := req.Config.PathMatches(ctx, expression) + + res.Diagnostics.Append(diags...) + + // Collect all errors + if diags.HasError() { + continue + } + + for _, mp := range matchedPaths { + // If the user specifies the same attribute this validator is applied to, + // also as part of the input, skip it + if mp.Equal(req.AttributePath) { + continue + } + + var mpVal attr.Value + diags := req.Config.GetAttribute(ctx, mp, &mpVal) + res.Diagnostics.Append(diags...) + + // Collect all errors + if diags.HasError() { + continue + } + + // Delay validation until all involved attribute have a known value + if mpVal.IsUnknown() { + return + } + + if !mpVal.IsNull() { + count++ + } + } + } + + if count == 0 { + res.Diagnostics.Append(validatordiag.InvalidAttributeCombinationDiagnostic( + req.AttributePath, + fmt.Sprintf("No attribute specified when one (and only one) of %s is required", expressions), + )) + } + + if count > 1 { + res.Diagnostics.Append(validatordiag.InvalidAttributeCombinationDiagnostic( + req.AttributePath, + fmt.Sprintf("%d attributes specified when one (and only one) of %s is required", count, expressions), + )) + } +} diff --git a/schemavalidator/exactly_one_of_test.go b/schemavalidator/exactly_one_of_test.go new file mode 100644 index 00000000..d05fef5d --- /dev/null +++ b/schemavalidator/exactly_one_of_test.go @@ -0,0 +1,292 @@ +package schemavalidator_test + +import ( + "context" + "testing" + + "github.com/hashicorp/terraform-plugin-framework-validators/schemavalidator" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestExactlyOneOfValidator(t *testing.T) { + t.Parallel() + + type testCase struct { + req tfsdk.ValidateAttributeRequest + in path.Expressions + expErrors int + } + + testCases := map[string]testCase{ + "base": { + req: tfsdk.ValidateAttributeRequest{ + AttributeConfig: types.String{Value: "bar value"}, + AttributePath: path.Root("bar"), + AttributePathExpression: path.MatchRoot("bar"), + Config: tfsdk.Config{ + Schema: tfsdk.Schema{ + Attributes: map[string]tfsdk.Attribute{ + "foo": { + Type: types.Int64Type, + }, + "bar": { + Type: types.StringType, + }, + }, + }, + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "foo": tftypes.Number, + "bar": tftypes.String, + }, + }, map[string]tftypes.Value{ + "foo": tftypes.NewValue(tftypes.Number, 42), + "bar": tftypes.NewValue(tftypes.String, "bar value"), + }), + }, + }, + in: path.Expressions{ + path.MatchRoot("foo"), + }, + expErrors: 1, + }, + "self-is-null": { + req: tfsdk.ValidateAttributeRequest{ + AttributeConfig: types.String{Null: true}, + AttributePath: path.Root("bar"), + AttributePathExpression: path.MatchRoot("bar"), + Config: tfsdk.Config{ + Schema: tfsdk.Schema{ + Attributes: map[string]tfsdk.Attribute{ + "foo": { + Type: types.Int64Type, + }, + "bar": { + Type: types.StringType, + }, + }, + }, + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "foo": tftypes.Number, + "bar": tftypes.String, + }, + }, map[string]tftypes.Value{ + "foo": tftypes.NewValue(tftypes.Number, 42), + "bar": tftypes.NewValue(tftypes.String, nil), + }), + }, + }, + in: path.Expressions{ + path.MatchRoot("foo"), + }, + }, + "error_too-many": { + req: tfsdk.ValidateAttributeRequest{ + AttributeConfig: types.String{Value: "bar value"}, + AttributePath: path.Root("bar"), + AttributePathExpression: path.MatchRoot("bar"), + Config: tfsdk.Config{ + Schema: tfsdk.Schema{ + Attributes: map[string]tfsdk.Attribute{ + "foo": { + Type: types.Int64Type, + }, + "bar": { + Type: types.StringType, + }, + "baz": { + Type: types.Float64Type, + }, + }, + }, + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "foo": tftypes.Number, + "bar": tftypes.String, + "baz": tftypes.Number, + }, + }, map[string]tftypes.Value{ + "foo": tftypes.NewValue(tftypes.Number, 42), + "bar": tftypes.NewValue(tftypes.String, "bar value"), + "baz": tftypes.NewValue(tftypes.Number, 4.2), + }), + }, + }, + in: path.Expressions{ + path.MatchRoot("foo"), + path.MatchRoot("baz"), + }, + expErrors: 1, + }, + "error_too-few": { + req: tfsdk.ValidateAttributeRequest{ + AttributeConfig: types.String{Null: true}, + AttributePath: path.Root("bar"), + AttributePathExpression: path.MatchRoot("bar"), + Config: tfsdk.Config{ + Schema: tfsdk.Schema{ + Attributes: map[string]tfsdk.Attribute{ + "foo": { + Type: types.Int64Type, + }, + "bar": { + Type: types.StringType, + }, + "baz": { + Type: types.Int64Type, + }, + }, + }, + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "foo": tftypes.Number, + "bar": tftypes.String, + "baz": tftypes.Number, + }, + }, map[string]tftypes.Value{ + "foo": tftypes.NewValue(tftypes.Number, nil), + "bar": tftypes.NewValue(tftypes.String, nil), + "baz": tftypes.NewValue(tftypes.Number, nil), + }), + }, + }, + in: path.Expressions{ + path.MatchRoot("foo"), + path.MatchRoot("baz"), + }, + expErrors: 1, + }, + "allow-duplicate-input": { + req: tfsdk.ValidateAttributeRequest{ + AttributeConfig: types.String{Value: "bar value"}, + AttributePath: path.Root("bar"), + AttributePathExpression: path.MatchRoot("bar"), + Config: tfsdk.Config{ + Schema: tfsdk.Schema{ + Attributes: map[string]tfsdk.Attribute{ + "foo": { + Type: types.Int64Type, + }, + "bar": { + Type: types.StringType, + }, + "baz": { + Type: types.Int64Type, + }, + }, + }, + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "foo": tftypes.Number, + "bar": tftypes.String, + "baz": tftypes.Number, + }, + }, map[string]tftypes.Value{ + "foo": tftypes.NewValue(tftypes.Number, nil), + "bar": tftypes.NewValue(tftypes.String, "bar value"), + "baz": tftypes.NewValue(tftypes.Number, nil), + }), + }, + }, + in: path.Expressions{ + path.MatchRoot("foo"), + path.MatchRoot("bar"), + path.MatchRoot("baz"), + }, + }, + "other-attributes-are-unknown": { + req: tfsdk.ValidateAttributeRequest{ + AttributeConfig: types.String{Value: "bar value"}, + AttributePath: path.Root("bar"), + AttributePathExpression: path.MatchRoot("bar"), + Config: tfsdk.Config{ + Schema: tfsdk.Schema{ + Attributes: map[string]tfsdk.Attribute{ + "foo": { + Type: types.Int64Type, + }, + "bar": { + Type: types.StringType, + }, + "baz": { + Type: types.Int64Type, + }, + }, + }, + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "foo": tftypes.Number, + "bar": tftypes.String, + "baz": tftypes.Number, + }, + }, map[string]tftypes.Value{ + "foo": tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), + "bar": tftypes.NewValue(tftypes.String, "bar value"), + "baz": tftypes.NewValue(tftypes.Number, tftypes.UnknownValue), + }), + }, + }, + in: path.Expressions{ + path.MatchRoot("foo"), + path.MatchRoot("baz"), + }, + }, + "matches-no-attribute-in-schema": { + req: tfsdk.ValidateAttributeRequest{ + AttributeConfig: types.String{Value: "bar value"}, + AttributePath: path.Root("bar"), + AttributePathExpression: path.MatchRoot("bar"), + Config: tfsdk.Config{ + Schema: tfsdk.Schema{ + Attributes: map[string]tfsdk.Attribute{ + "foo": { + Type: types.Int64Type, + }, + "bar": { + Type: types.StringType, + }, + }, + }, + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "foo": tftypes.Number, + "bar": tftypes.String, + }, + }, map[string]tftypes.Value{ + "foo": tftypes.NewValue(tftypes.Number, 42), + "bar": tftypes.NewValue(tftypes.String, "bar value"), + }), + }, + }, + in: path.Expressions{ + path.MatchRoot("fooz"), + path.MatchRoot("barz"), + }, + expErrors: 2, + }, + } + + for name, test := range testCases { + t.Run(name, func(t *testing.T) { + res := tfsdk.ValidateAttributeResponse{} + + schemavalidator.ExactlyOneOf(test.in...).Validate(context.TODO(), test.req, &res) + + if test.expErrors > 0 && !res.Diagnostics.HasError() { + t.Fatal("expected error(s), got none") + } + + 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) + } + }) + } +} diff --git a/setvalidator/size_at_least.go b/setvalidator/size_at_least.go index 78caea18..491cf6db 100644 --- a/setvalidator/size_at_least.go +++ b/setvalidator/size_at_least.go @@ -16,7 +16,7 @@ type sizeAtLeastValidator struct { } // Description describes the validation in plain text formatting. -func (v sizeAtLeastValidator) Description(ctx context.Context) string { +func (v sizeAtLeastValidator) Description(_ context.Context) string { return fmt.Sprintf("set must contain at least %d elements", v.min) } diff --git a/setvalidator/size_at_least_test.go b/setvalidator/size_at_least_test.go index 7f45c774..aa5b9bc6 100644 --- a/setvalidator/size_at_least_test.go +++ b/setvalidator/size_at_least_test.go @@ -72,8 +72,9 @@ func TestSizeAtLeastValidator(t *testing.T) { name, test := name, test t.Run(name, func(t *testing.T) { request := tfsdk.ValidateAttributeRequest{ - AttributePath: path.Root("test"), - AttributeConfig: test.val, + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), + AttributeConfig: test.val, } response := tfsdk.ValidateAttributeResponse{} SizeAtLeast(test.min).Validate(context.TODO(), request, &response) diff --git a/setvalidator/size_at_most.go b/setvalidator/size_at_most.go index fe296bdb..e84e6daf 100644 --- a/setvalidator/size_at_most.go +++ b/setvalidator/size_at_most.go @@ -16,7 +16,7 @@ type sizeAtMostValidator struct { } // Description describes the validation in plain text formatting. -func (v sizeAtMostValidator) Description(ctx context.Context) string { +func (v sizeAtMostValidator) Description(_ context.Context) string { return fmt.Sprintf("set must contain at most %d elements", v.max) } diff --git a/setvalidator/size_at_most_test.go b/setvalidator/size_at_most_test.go index a44ee28f..26561f64 100644 --- a/setvalidator/size_at_most_test.go +++ b/setvalidator/size_at_most_test.go @@ -75,8 +75,9 @@ func TestSizeAtMostValidator(t *testing.T) { name, test := name, test t.Run(name, func(t *testing.T) { request := tfsdk.ValidateAttributeRequest{ - AttributePath: path.Root("test"), - AttributeConfig: test.val, + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), + AttributeConfig: test.val, } response := tfsdk.ValidateAttributeResponse{} SizeAtMost(test.max).Validate(context.TODO(), request, &response) diff --git a/setvalidator/size_between.go b/setvalidator/size_between.go index 79ea8065..e3b2240b 100644 --- a/setvalidator/size_between.go +++ b/setvalidator/size_between.go @@ -18,7 +18,7 @@ type sizeBetweenValidator struct { } // Description describes the validation in plain text formatting. -func (v sizeBetweenValidator) Description(ctx context.Context) string { +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) } diff --git a/setvalidator/size_between_test.go b/setvalidator/size_between_test.go index 5f3dd38d..273130e2 100644 --- a/setvalidator/size_between_test.go +++ b/setvalidator/size_between_test.go @@ -115,8 +115,9 @@ func TestSizeBetweenValidator(t *testing.T) { name, test := name, test t.Run(name, func(t *testing.T) { request := tfsdk.ValidateAttributeRequest{ - AttributePath: path.Root("test"), - AttributeConfig: test.val, + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), + AttributeConfig: test.val, } response := tfsdk.ValidateAttributeResponse{} SizeBetween(test.min, test.max).Validate(context.TODO(), request, &response) diff --git a/setvalidator/type_validation.go b/setvalidator/type_validation.go index 60ba6613..e261af2a 100644 --- a/setvalidator/type_validation.go +++ b/setvalidator/type_validation.go @@ -15,7 +15,7 @@ func validateSet(ctx context.Context, request tfsdk.ValidateAttributeRequest, re diags := tfsdk.ValueAs(ctx, request.AttributeConfig, &s) if diags.HasError() { - response.Diagnostics = append(response.Diagnostics, diags...) + response.Diagnostics.Append(diags...) return nil, false } diff --git a/setvalidator/type_validation_test.go b/setvalidator/type_validation_test.go index f198c738..db8f32be 100644 --- a/setvalidator/type_validation_test.go +++ b/setvalidator/type_validation_test.go @@ -21,24 +21,27 @@ func TestValidateSet(t *testing.T) { }{ "invalid-type": { request: tfsdk.ValidateAttributeRequest{ - AttributeConfig: types.Bool{Value: true}, - AttributePath: path.Root("test"), + AttributeConfig: types.Bool{Value: true}, + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), }, expectedSetElems: nil, expectedOk: false, }, "set-null": { request: tfsdk.ValidateAttributeRequest{ - AttributeConfig: types.Set{Null: true}, - AttributePath: path.Root("test"), + AttributeConfig: types.Set{Null: true}, + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), }, expectedSetElems: nil, expectedOk: false, }, "set-unknown": { request: tfsdk.ValidateAttributeRequest{ - AttributeConfig: types.Set{Unknown: true}, - AttributePath: path.Root("test"), + AttributeConfig: types.Set{Unknown: true}, + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), }, expectedSetElems: nil, expectedOk: false, @@ -52,7 +55,8 @@ func TestValidateSet(t *testing.T) { types.String{Value: "second"}, }, }, - AttributePath: path.Root("test"), + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), }, expectedSetElems: []attr.Value{ types.String{Value: "first"}, diff --git a/setvalidator/values_are.go b/setvalidator/values_are.go index ac769cb3..a9c971b2 100644 --- a/setvalidator/values_are.go +++ b/setvalidator/values_are.go @@ -38,10 +38,12 @@ func (v valuesAreValidator) Validate(ctx context.Context, req tfsdk.ValidateAttr } for _, elem := range elems { + attrPath := req.AttributePath.AtSetValue(elem) request := tfsdk.ValidateAttributeRequest{ - AttributePath: req.AttributePath.AtSetValue(elem), - AttributeConfig: elem, - Config: req.Config, + AttributePath: attrPath, + AttributePathExpression: attrPath.Expression(), + AttributeConfig: elem, + Config: req.Config, } for _, validator := range v.valueValidators { diff --git a/setvalidator/values_are_test.go b/setvalidator/values_are_test.go index a3d00c81..e5caa8e7 100644 --- a/setvalidator/values_are_test.go +++ b/setvalidator/values_are_test.go @@ -101,8 +101,9 @@ func TestValuesAreValidator(t *testing.T) { name, test := name, test t.Run(name, func(t *testing.T) { request := tfsdk.ValidateAttributeRequest{ - AttributePath: path.Root("test"), - AttributeConfig: test.val, + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), + AttributeConfig: test.val, } response := tfsdk.ValidateAttributeResponse{} ValuesAre(test.valuesAreValidators...).Validate(context.TODO(), request, &response) diff --git a/stringvalidator/length_at_least_test.go b/stringvalidator/length_at_least_test.go index 3e0fb649..0a3bbbc6 100644 --- a/stringvalidator/length_at_least_test.go +++ b/stringvalidator/length_at_least_test.go @@ -47,8 +47,9 @@ func TestLengthAtLeastValidator(t *testing.T) { name, test := name, test t.Run(name, func(t *testing.T) { request := tfsdk.ValidateAttributeRequest{ - AttributePath: path.Root("test"), - AttributeConfig: test.val, + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), + AttributeConfig: test.val, } response := tfsdk.ValidateAttributeResponse{} stringvalidator.LengthAtLeast(test.minLength).Validate(context.TODO(), request, &response) diff --git a/stringvalidator/length_at_most_test.go b/stringvalidator/length_at_most_test.go index 18666a85..39d315d0 100644 --- a/stringvalidator/length_at_most_test.go +++ b/stringvalidator/length_at_most_test.go @@ -47,8 +47,9 @@ func TestLengthAtMostValidator(t *testing.T) { name, test := name, test t.Run(name, func(t *testing.T) { request := tfsdk.ValidateAttributeRequest{ - AttributePath: path.Root("test"), - AttributeConfig: test.val, + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), + AttributeConfig: test.val, } response := tfsdk.ValidateAttributeResponse{} stringvalidator.LengthAtMost(test.maxLength).Validate(context.TODO(), request, &response) diff --git a/stringvalidator/length_between_test.go b/stringvalidator/length_between_test.go index 6301974d..ce8cf560 100644 --- a/stringvalidator/length_between_test.go +++ b/stringvalidator/length_between_test.go @@ -58,8 +58,9 @@ func TestLengthBetweenValidator(t *testing.T) { name, test := name, test t.Run(name, func(t *testing.T) { request := tfsdk.ValidateAttributeRequest{ - AttributePath: path.Root("test"), - AttributeConfig: test.val, + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), + AttributeConfig: test.val, } response := tfsdk.ValidateAttributeResponse{} stringvalidator.LengthBetween(test.minLength, test.maxLength).Validate(context.TODO(), request, &response) diff --git a/stringvalidator/none_of_test.go b/stringvalidator/none_of_test.go index 78446222..e4361e58 100644 --- a/stringvalidator/none_of_test.go +++ b/stringvalidator/none_of_test.go @@ -4,7 +4,6 @@ import ( "context" "testing" - "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/tfsdk" @@ -159,12 +158,12 @@ func TestNoneOfValidator(t *testing.T) { t.Fatalf("expected %d error(s), got none", test.expErrors) } - if test.expErrors > 0 && test.expErrors != validatordiag.ErrorsCount(res.Diagnostics) { - t.Fatalf("expected %d error(s), got %d: %v", test.expErrors, validatordiag.ErrorsCount(res.Diagnostics), res.Diagnostics) + 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", validatordiag.ErrorsCount(res.Diagnostics), res.Diagnostics) + t.Fatalf("expected no error(s), got %d: %v", res.Diagnostics.ErrorsCount(), res.Diagnostics) } }) } @@ -318,12 +317,12 @@ func TestNoneOfCaseInsensitiveValidator(t *testing.T) { t.Fatalf("expected %d error(s), got none", test.expErrors) } - if test.expErrors > 0 && test.expErrors != validatordiag.ErrorsCount(res.Diagnostics) { - t.Fatalf("expected %d error(s), got %d: %v", test.expErrors, validatordiag.ErrorsCount(res.Diagnostics), res.Diagnostics) + 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", validatordiag.ErrorsCount(res.Diagnostics), res.Diagnostics) + t.Fatalf("expected no error(s), got %d: %v", res.Diagnostics.ErrorsCount(), res.Diagnostics) } }) } diff --git a/stringvalidator/one_of_test.go b/stringvalidator/one_of_test.go index a536d3d8..568dd120 100644 --- a/stringvalidator/one_of_test.go +++ b/stringvalidator/one_of_test.go @@ -4,7 +4,6 @@ import ( "context" "testing" - "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/tfsdk" @@ -159,12 +158,12 @@ func TestOneOfValidator(t *testing.T) { t.Fatalf("expected %d error(s), got none", test.expErrors) } - if test.expErrors > 0 && test.expErrors != validatordiag.ErrorsCount(res.Diagnostics) { - t.Fatalf("expected %d error(s), got %d: %v", test.expErrors, validatordiag.ErrorsCount(res.Diagnostics), res.Diagnostics) + 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", validatordiag.ErrorsCount(res.Diagnostics), res.Diagnostics) + t.Fatalf("expected no error(s), got %d: %v", res.Diagnostics.ErrorsCount(), res.Diagnostics) } }) } @@ -318,12 +317,12 @@ func TestOneOfCaseInsensitiveValidator(t *testing.T) { t.Fatalf("expected %d error(s), got none", test.expErrors) } - if test.expErrors > 0 && test.expErrors != validatordiag.ErrorsCount(res.Diagnostics) { - t.Fatalf("expected %d error(s), got %d: %v", test.expErrors, validatordiag.ErrorsCount(res.Diagnostics), res.Diagnostics) + 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", validatordiag.ErrorsCount(res.Diagnostics), res.Diagnostics) + t.Fatalf("expected no error(s), got %d: %v", res.Diagnostics.ErrorsCount(), res.Diagnostics) } }) } diff --git a/stringvalidator/regex_matches_test.go b/stringvalidator/regex_matches_test.go index 92239b96..76f04306 100644 --- a/stringvalidator/regex_matches_test.go +++ b/stringvalidator/regex_matches_test.go @@ -49,8 +49,9 @@ func TestRegexMatchesValidator(t *testing.T) { name, test := name, test t.Run(name, func(t *testing.T) { request := tfsdk.ValidateAttributeRequest{ - AttributePath: path.Root("test"), - AttributeConfig: test.val, + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), + AttributeConfig: test.val, } response := tfsdk.ValidateAttributeResponse{} stringvalidator.RegexMatches(test.regexp, "").Validate(context.TODO(), request, &response) diff --git a/stringvalidator/type_validation_test.go b/stringvalidator/type_validation_test.go index 003cb628..b02ed6f5 100644 --- a/stringvalidator/type_validation_test.go +++ b/stringvalidator/type_validation_test.go @@ -20,32 +20,36 @@ func TestValidateString(t *testing.T) { }{ "invalid-type": { request: tfsdk.ValidateAttributeRequest{ - AttributeConfig: types.Bool{Value: true}, - AttributePath: path.Root("test"), + AttributeConfig: types.Bool{Value: true}, + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), }, expectedString: "", expectedOk: false, }, "string-null": { request: tfsdk.ValidateAttributeRequest{ - AttributeConfig: types.Int64{Null: true}, - AttributePath: path.Root("test"), + AttributeConfig: types.Int64{Null: true}, + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), }, expectedString: "", expectedOk: false, }, "string-value": { request: tfsdk.ValidateAttributeRequest{ - AttributeConfig: types.String{Value: "test-value"}, - AttributePath: path.Root("test"), + AttributeConfig: types.String{Value: "test-value"}, + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), }, expectedString: "test-value", expectedOk: true, }, "string-unknown": { request: tfsdk.ValidateAttributeRequest{ - AttributeConfig: types.Int64{Unknown: true}, - AttributePath: path.Root("test"), + AttributeConfig: types.Int64{Unknown: true}, + AttributePath: path.Root("test"), + AttributePathExpression: path.MatchRoot("test"), }, expectedString: "", expectedOk: false,