Skip to content

Commit 16687f3

Browse files
authored
New package dynamicvalidator (#249)
* Support dynamicvalidator * Pass tests * docs updates + changelog
1 parent 7d19faa commit 16687f3

20 files changed

+570
-0
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
kind: FEATURES
2+
body: 'dynamicvalidator: New package which contains `types.Dynamic` specific validators'
3+
time: 2024-12-12T14:42:10.189818-05:00
4+
custom:
5+
Issue: "249"

dynamicvalidator/all.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package dynamicvalidator
5+
6+
import (
7+
"context"
8+
"fmt"
9+
"strings"
10+
11+
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
12+
)
13+
14+
// All returns a validator which ensures that any configured attribute value
15+
// attribute value validates against all the given validators.
16+
//
17+
// Use of All is only necessary when used in conjunction with Any or AnyWithAllWarnings
18+
// as the Validators field automatically applies a logical AND.
19+
func All(validators ...validator.Dynamic) validator.Dynamic {
20+
return allValidator{
21+
validators: validators,
22+
}
23+
}
24+
25+
var _ validator.Dynamic = allValidator{}
26+
27+
// allValidator implements the validator.
28+
type allValidator struct {
29+
validators []validator.Dynamic
30+
}
31+
32+
// Description describes the validation in plain text formatting.
33+
func (v allValidator) Description(ctx context.Context) string {
34+
var descriptions []string
35+
36+
for _, subValidator := range v.validators {
37+
descriptions = append(descriptions, subValidator.Description(ctx))
38+
}
39+
40+
return fmt.Sprintf("Value must satisfy all of the validations: %s", strings.Join(descriptions, " + "))
41+
}
42+
43+
// MarkdownDescription describes the validation in Markdown formatting.
44+
func (v allValidator) MarkdownDescription(ctx context.Context) string {
45+
return v.Description(ctx)
46+
}
47+
48+
// ValidateDynamic performs the validation.
49+
func (v allValidator) ValidateDynamic(ctx context.Context, req validator.DynamicRequest, resp *validator.DynamicResponse) {
50+
for _, subValidator := range v.validators {
51+
validateResp := &validator.DynamicResponse{}
52+
53+
subValidator.ValidateDynamic(ctx, req, validateResp)
54+
55+
resp.Diagnostics.Append(validateResp.Diagnostics...)
56+
}
57+
}

dynamicvalidator/all_example_test.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package dynamicvalidator_test
5+
6+
import (
7+
"github.com/hashicorp/terraform-plugin-framework-validators/dynamicvalidator"
8+
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
9+
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
10+
)
11+
12+
func ExampleAll() {
13+
// Used within a Schema method of a DataSource, Provider, or Resource
14+
_ = schema.Schema{
15+
Attributes: map[string]schema.Attribute{
16+
"example_attr": schema.DynamicAttribute{
17+
Required: true,
18+
Validators: []validator.Dynamic{
19+
dynamicvalidator.Any(
20+
dynamicvalidator.Any( /* ... */ ),
21+
dynamicvalidator.All( /* ... */ ),
22+
),
23+
},
24+
},
25+
},
26+
}
27+
}

dynamicvalidator/also_requires.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package dynamicvalidator
5+
6+
import (
7+
"github.com/hashicorp/terraform-plugin-framework-validators/internal/schemavalidator"
8+
"github.com/hashicorp/terraform-plugin-framework/path"
9+
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
10+
)
11+
12+
// AlsoRequires checks that a set of path.Expression has a non-null value,
13+
// if the current attribute or block also has a non-null value.
14+
//
15+
// This implements the validation logic declaratively within the schema.
16+
// Refer to [datasourcevalidator.RequiredTogether],
17+
// [providervalidator.RequiredTogether], or [resourcevalidator.RequiredTogether]
18+
// for declaring this type of validation outside the schema definition.
19+
//
20+
// Relative path.Expression will be resolved using the attribute or block
21+
// being validated.
22+
func AlsoRequires(expressions ...path.Expression) validator.Dynamic {
23+
return schemavalidator.AlsoRequiresValidator{
24+
PathExpressions: expressions,
25+
}
26+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package dynamicvalidator_test
5+
6+
import (
7+
"github.com/hashicorp/terraform-plugin-framework-validators/dynamicvalidator"
8+
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
9+
"github.com/hashicorp/terraform-plugin-framework/path"
10+
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
11+
)
12+
13+
func ExampleAlsoRequires() {
14+
// Used within a Schema method of a DataSource, Provider, or Resource
15+
_ = schema.Schema{
16+
Attributes: map[string]schema.Attribute{
17+
"example_attr": schema.DynamicAttribute{
18+
Optional: true,
19+
Validators: []validator.Dynamic{
20+
// Validate this attribute must be configured with other_attr.
21+
dynamicvalidator.AlsoRequires(path.Expressions{
22+
path.MatchRoot("other_attr"),
23+
}...),
24+
},
25+
},
26+
"other_attr": schema.DynamicAttribute{
27+
Optional: true,
28+
},
29+
},
30+
}
31+
}

dynamicvalidator/any.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package dynamicvalidator
5+
6+
import (
7+
"context"
8+
"fmt"
9+
"strings"
10+
11+
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
12+
)
13+
14+
// Any returns a validator which ensures that any configured attribute value
15+
// passes at least one of the given validators.
16+
//
17+
// To prevent practitioner confusion should non-passing validators have
18+
// conflicting logic, only warnings from the passing validator are returned.
19+
// Use AnyWithAllWarnings() to return warnings from non-passing validators
20+
// as well.
21+
func Any(validators ...validator.Dynamic) validator.Dynamic {
22+
return anyValidator{
23+
validators: validators,
24+
}
25+
}
26+
27+
var _ validator.Dynamic = anyValidator{}
28+
29+
// anyValidator implements the validator.
30+
type anyValidator struct {
31+
validators []validator.Dynamic
32+
}
33+
34+
// Description describes the validation in plain text formatting.
35+
func (v anyValidator) Description(ctx context.Context) string {
36+
var descriptions []string
37+
38+
for _, subValidator := range v.validators {
39+
descriptions = append(descriptions, subValidator.Description(ctx))
40+
}
41+
42+
return fmt.Sprintf("Value must satisfy at least one of the validations: %s", strings.Join(descriptions, " + "))
43+
}
44+
45+
// MarkdownDescription describes the validation in Markdown formatting.
46+
func (v anyValidator) MarkdownDescription(ctx context.Context) string {
47+
return v.Description(ctx)
48+
}
49+
50+
// ValidateDynamic performs the validation.
51+
func (v anyValidator) ValidateDynamic(ctx context.Context, req validator.DynamicRequest, resp *validator.DynamicResponse) {
52+
for _, subValidator := range v.validators {
53+
validateResp := &validator.DynamicResponse{}
54+
55+
subValidator.ValidateDynamic(ctx, req, validateResp)
56+
57+
if !validateResp.Diagnostics.HasError() {
58+
resp.Diagnostics = validateResp.Diagnostics
59+
60+
return
61+
}
62+
63+
resp.Diagnostics.Append(validateResp.Diagnostics...)
64+
}
65+
}

dynamicvalidator/any_example_test.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package dynamicvalidator_test
5+
6+
import (
7+
"github.com/hashicorp/terraform-plugin-framework-validators/dynamicvalidator"
8+
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
9+
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
10+
)
11+
12+
func ExampleAny() {
13+
// Used within a Schema method of a DataSource, Provider, or Resource
14+
_ = schema.Schema{
15+
Attributes: map[string]schema.Attribute{
16+
"example_attr": schema.DynamicAttribute{
17+
Required: true,
18+
Validators: []validator.Dynamic{
19+
dynamicvalidator.Any(
20+
dynamicvalidator.Any( /* ... */ ),
21+
),
22+
},
23+
},
24+
},
25+
}
26+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package dynamicvalidator
5+
6+
import (
7+
"context"
8+
"fmt"
9+
"strings"
10+
11+
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
12+
)
13+
14+
// AnyWithAllWarnings returns a validator which ensures that any configured
15+
// attribute value passes at least one of the given validators. This validator
16+
// returns all warnings, including failed validators.
17+
//
18+
// Use Any() to return warnings only from the passing validator.
19+
func AnyWithAllWarnings(validators ...validator.Dynamic) validator.Dynamic {
20+
return anyWithAllWarningsValidator{
21+
validators: validators,
22+
}
23+
}
24+
25+
var _ validator.Dynamic = anyWithAllWarningsValidator{}
26+
27+
// anyWithAllWarningsValidator implements the validator.
28+
type anyWithAllWarningsValidator struct {
29+
validators []validator.Dynamic
30+
}
31+
32+
// Description describes the validation in plain text formatting.
33+
func (v anyWithAllWarningsValidator) Description(ctx context.Context) string {
34+
var descriptions []string
35+
36+
for _, subValidator := range v.validators {
37+
descriptions = append(descriptions, subValidator.Description(ctx))
38+
}
39+
40+
return fmt.Sprintf("Value must satisfy at least one of the validations: %s", strings.Join(descriptions, " + "))
41+
}
42+
43+
// MarkdownDescription describes the validation in Markdown formatting.
44+
func (v anyWithAllWarningsValidator) MarkdownDescription(ctx context.Context) string {
45+
return v.Description(ctx)
46+
}
47+
48+
// ValidateDynamic performs the validation.
49+
func (v anyWithAllWarningsValidator) ValidateDynamic(ctx context.Context, req validator.DynamicRequest, resp *validator.DynamicResponse) {
50+
anyValid := false
51+
52+
for _, subValidator := range v.validators {
53+
validateResp := &validator.DynamicResponse{}
54+
55+
subValidator.ValidateDynamic(ctx, req, validateResp)
56+
57+
if !validateResp.Diagnostics.HasError() {
58+
anyValid = true
59+
}
60+
61+
resp.Diagnostics.Append(validateResp.Diagnostics...)
62+
}
63+
64+
if anyValid {
65+
resp.Diagnostics = resp.Diagnostics.Warnings()
66+
}
67+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package dynamicvalidator_test
5+
6+
import (
7+
"github.com/hashicorp/terraform-plugin-framework-validators/dynamicvalidator"
8+
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
9+
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
10+
)
11+
12+
func ExampleAnyWithAllWarnings() {
13+
// Used within a Schema method of a DataSource, Provider, or Resource
14+
_ = schema.Schema{
15+
Attributes: map[string]schema.Attribute{
16+
"example_attr": schema.DynamicAttribute{
17+
Required: true,
18+
Validators: []validator.Dynamic{
19+
dynamicvalidator.AnyWithAllWarnings(
20+
dynamicvalidator.AnyWithAllWarnings( /* ... */ ),
21+
),
22+
},
23+
},
24+
},
25+
}
26+
}

dynamicvalidator/at_least_one_of.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package dynamicvalidator
5+
6+
import (
7+
"github.com/hashicorp/terraform-plugin-framework-validators/internal/schemavalidator"
8+
"github.com/hashicorp/terraform-plugin-framework/path"
9+
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
10+
)
11+
12+
// AtLeastOneOf checks that of a set of path.Expression,
13+
// including the attribute this validator is applied to,
14+
// at least one has a non-null value.
15+
//
16+
// This implements the validation logic declaratively within the tfsdk.Schema.
17+
// Refer to [datasourcevalidator.AtLeastOneOf],
18+
// [providervalidator.AtLeastOneOf], or [resourcevalidator.AtLeastOneOf]
19+
// for declaring this type of validation outside the schema definition.
20+
//
21+
// Any relative path.Expression will be resolved using the attribute being
22+
// validated.
23+
func AtLeastOneOf(expressions ...path.Expression) validator.Dynamic {
24+
return schemavalidator.AtLeastOneOfValidator{
25+
PathExpressions: expressions,
26+
}
27+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package dynamicvalidator_test
5+
6+
import (
7+
"github.com/hashicorp/terraform-plugin-framework-validators/dynamicvalidator"
8+
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
9+
"github.com/hashicorp/terraform-plugin-framework/path"
10+
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
11+
)
12+
13+
func ExampleAtLeastOneOf() {
14+
// Used within a Schema method of a DataSource, Provider, or Resource
15+
_ = schema.Schema{
16+
Attributes: map[string]schema.Attribute{
17+
"example_attr": schema.DynamicAttribute{
18+
Optional: true,
19+
Validators: []validator.Dynamic{
20+
// Validate at least this attribute or other_attr should be configured.
21+
dynamicvalidator.AtLeastOneOf(path.Expressions{
22+
path.MatchRoot("other_attr"),
23+
}...),
24+
},
25+
},
26+
"other_attr": schema.DynamicAttribute{
27+
Optional: true,
28+
},
29+
},
30+
}
31+
}

0 commit comments

Comments
 (0)