Skip to content

Commit 7425c06

Browse files
authored
Introduce Data Source, Provider, and Resource Level Validators (#60)
Reference: #48 These configuration validators are intended for usage outside the schema definition, which may be easier to comprehend or introduce with code generation. For example: ```go func (r exampleResource) ConfigValidators(ctx context.Context) []resource.ConfigValidator { return []resource.ConfigValidator{ // Schema defined attributes named attr1 and attr2 must have one or more configured resourcevalidator.AtLeastOneOf( path.MatchRoot("attr1"), path.MatchRoot("attr2"), ), // Schema defined attributes named attr3 and attr4 cannot be configured together resourcevalidator.Conflicting( path.MatchRoot("attr3"), path.MatchRoot("attr4"), ), // Schema defined attributes named attr5 and attr6 must only have one configured resourcevalidator.ExactlyOneOf( path.MatchRoot("attr5"), path.MatchRoot("attr6"), ), // Schema defined attributes named attr7 and attr8 must be configured together resourcevalidator.RequiredTogether( path.MatchRoot("attr7"), path.MatchRoot("attr8"), ), } } ```
1 parent 6edbdcf commit 7425c06

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

54 files changed

+6372
-0
lines changed

.changelog/60.txt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
```release-note:feature
2+
Introduced `datasourcevalidator` package with `AtLeastOneOf()`, `Conflicting()`, `ExactlyOneOf()`, and `RequiredTogether()` validation functions
3+
```
4+
5+
```release-note:feature
6+
Introduced `providervalidator` package with `AtLeastOneOf()`, `Conflicting()`, `ExactlyOneOf()`, and `RequiredTogether()` validation functions
7+
```
8+
9+
```release-note:feature
10+
Introduced `resourcevalidator` package with `AtLeastOneOf()`, `Conflicting()`, `ExactlyOneOf()`, and `RequiredTogether()` validation functions
11+
```
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package datasourcevalidator
2+
3+
import (
4+
"github.com/hashicorp/terraform-plugin-framework-validators/internal/configvalidator"
5+
"github.com/hashicorp/terraform-plugin-framework/datasource"
6+
"github.com/hashicorp/terraform-plugin-framework/path"
7+
)
8+
9+
// AtLeastOneOf checks that a set of path.Expression has at least one non-null
10+
// or unknown value.
11+
func AtLeastOneOf(expressions ...path.Expression) datasource.ConfigValidator {
12+
return &configvalidator.AtLeastOneOfValidator{
13+
PathExpressions: expressions,
14+
}
15+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package datasourcevalidator_test
2+
3+
import (
4+
"github.com/hashicorp/terraform-plugin-framework-validators/datasourcevalidator"
5+
"github.com/hashicorp/terraform-plugin-framework/datasource"
6+
"github.com/hashicorp/terraform-plugin-framework/path"
7+
)
8+
9+
func ExampleAtLeastOneOf() {
10+
// Used inside a datasource.DataSource type ConfigValidators method
11+
_ = []datasource.ConfigValidator{
12+
// Validate at least one of the schema defined attributes named attr1
13+
// and attr2 has a known, non-null value.
14+
datasourcevalidator.AtLeastOneOf(
15+
path.MatchRoot("attr1"),
16+
path.MatchRoot("attr2"),
17+
),
18+
}
19+
}
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
package datasourcevalidator_test
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/google/go-cmp/cmp"
8+
"github.com/hashicorp/terraform-plugin-framework-validators/datasourcevalidator"
9+
"github.com/hashicorp/terraform-plugin-framework/datasource"
10+
"github.com/hashicorp/terraform-plugin-framework/diag"
11+
"github.com/hashicorp/terraform-plugin-framework/path"
12+
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
13+
"github.com/hashicorp/terraform-plugin-framework/types"
14+
"github.com/hashicorp/terraform-plugin-go/tftypes"
15+
)
16+
17+
func TestAtLeastOneOf(t *testing.T) {
18+
t.Parallel()
19+
20+
testCases := map[string]struct {
21+
pathExpressions path.Expressions
22+
req datasource.ValidateConfigRequest
23+
expected *datasource.ValidateConfigResponse
24+
}{
25+
"no-diagnostics": {
26+
pathExpressions: path.Expressions{
27+
path.MatchRoot("test"),
28+
},
29+
req: datasource.ValidateConfigRequest{
30+
Config: tfsdk.Config{
31+
Schema: tfsdk.Schema{
32+
Attributes: map[string]tfsdk.Attribute{
33+
"test": {
34+
Optional: true,
35+
Type: types.StringType,
36+
},
37+
"other": {
38+
Optional: true,
39+
Type: types.StringType,
40+
},
41+
},
42+
},
43+
Raw: tftypes.NewValue(
44+
tftypes.Object{
45+
AttributeTypes: map[string]tftypes.Type{
46+
"test": tftypes.String,
47+
"other": tftypes.String,
48+
},
49+
},
50+
map[string]tftypes.Value{
51+
"test": tftypes.NewValue(tftypes.String, "test-value"),
52+
"other": tftypes.NewValue(tftypes.String, "test-value"),
53+
},
54+
),
55+
},
56+
},
57+
expected: &datasource.ValidateConfigResponse{},
58+
},
59+
"diagnostics": {
60+
pathExpressions: path.Expressions{
61+
path.MatchRoot("test1"),
62+
path.MatchRoot("test2"),
63+
},
64+
req: datasource.ValidateConfigRequest{
65+
Config: tfsdk.Config{
66+
Schema: tfsdk.Schema{
67+
Attributes: map[string]tfsdk.Attribute{
68+
"test1": {
69+
Optional: true,
70+
Type: types.StringType,
71+
},
72+
"test2": {
73+
Optional: true,
74+
Type: types.StringType,
75+
},
76+
"other": {
77+
Optional: true,
78+
Type: types.StringType,
79+
},
80+
},
81+
},
82+
Raw: tftypes.NewValue(
83+
tftypes.Object{
84+
AttributeTypes: map[string]tftypes.Type{
85+
"test1": tftypes.String,
86+
"test2": tftypes.String,
87+
"other": tftypes.String,
88+
},
89+
},
90+
map[string]tftypes.Value{
91+
"test1": tftypes.NewValue(tftypes.String, nil),
92+
"test2": tftypes.NewValue(tftypes.String, nil),
93+
"other": tftypes.NewValue(tftypes.String, "test-value"),
94+
},
95+
),
96+
},
97+
},
98+
expected: &datasource.ValidateConfigResponse{
99+
Diagnostics: diag.Diagnostics{
100+
diag.NewErrorDiagnostic(
101+
"Missing Attribute Configuration",
102+
"At least one of these attributes must be configured: [test1,test2]",
103+
),
104+
},
105+
},
106+
},
107+
}
108+
109+
for name, testCase := range testCases {
110+
name, testCase := name, testCase
111+
112+
t.Run(name, func(t *testing.T) {
113+
t.Parallel()
114+
115+
validator := datasourcevalidator.AtLeastOneOf(testCase.pathExpressions...)
116+
got := &datasource.ValidateConfigResponse{}
117+
118+
validator.ValidateDataSource(context.Background(), testCase.req, got)
119+
120+
if diff := cmp.Diff(got, testCase.expected); diff != "" {
121+
t.Errorf("unexpected difference: %s", diff)
122+
}
123+
})
124+
}
125+
}

datasourcevalidator/conflicting.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package datasourcevalidator
2+
3+
import (
4+
"github.com/hashicorp/terraform-plugin-framework-validators/internal/configvalidator"
5+
"github.com/hashicorp/terraform-plugin-framework/datasource"
6+
"github.com/hashicorp/terraform-plugin-framework/path"
7+
)
8+
9+
// Conflicting checks that a set of path.Expression, are not configured
10+
// simultaneously.
11+
func Conflicting(expressions ...path.Expression) datasource.ConfigValidator {
12+
return &configvalidator.ConflictingValidator{
13+
PathExpressions: expressions,
14+
}
15+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package datasourcevalidator_test
2+
3+
import (
4+
"github.com/hashicorp/terraform-plugin-framework-validators/datasourcevalidator"
5+
"github.com/hashicorp/terraform-plugin-framework/datasource"
6+
"github.com/hashicorp/terraform-plugin-framework/path"
7+
)
8+
9+
func ExampleConflicting() {
10+
// Used inside a datasource.DataSource type ConfigValidators method
11+
_ = []datasource.ConfigValidator{
12+
// Validate that schema defined attributes named attr1 and attr2 are not
13+
// both configured with known, non-null values.
14+
datasourcevalidator.Conflicting(
15+
path.MatchRoot("attr1"),
16+
path.MatchRoot("attr2"),
17+
),
18+
}
19+
}
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
package datasourcevalidator_test
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/google/go-cmp/cmp"
8+
"github.com/hashicorp/terraform-plugin-framework-validators/datasourcevalidator"
9+
"github.com/hashicorp/terraform-plugin-framework/datasource"
10+
"github.com/hashicorp/terraform-plugin-framework/diag"
11+
"github.com/hashicorp/terraform-plugin-framework/path"
12+
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
13+
"github.com/hashicorp/terraform-plugin-framework/types"
14+
"github.com/hashicorp/terraform-plugin-go/tftypes"
15+
)
16+
17+
func TestConflicting(t *testing.T) {
18+
t.Parallel()
19+
20+
testCases := map[string]struct {
21+
pathExpressions path.Expressions
22+
req datasource.ValidateConfigRequest
23+
expected *datasource.ValidateConfigResponse
24+
}{
25+
"no-diagnostics": {
26+
pathExpressions: path.Expressions{
27+
path.MatchRoot("test"),
28+
},
29+
req: datasource.ValidateConfigRequest{
30+
Config: tfsdk.Config{
31+
Schema: tfsdk.Schema{
32+
Attributes: map[string]tfsdk.Attribute{
33+
"test": {
34+
Optional: true,
35+
Type: types.StringType,
36+
},
37+
"other": {
38+
Optional: true,
39+
Type: types.StringType,
40+
},
41+
},
42+
},
43+
Raw: tftypes.NewValue(
44+
tftypes.Object{
45+
AttributeTypes: map[string]tftypes.Type{
46+
"test": tftypes.String,
47+
"other": tftypes.String,
48+
},
49+
},
50+
map[string]tftypes.Value{
51+
"test": tftypes.NewValue(tftypes.String, "test-value"),
52+
"other": tftypes.NewValue(tftypes.String, "test-value"),
53+
},
54+
),
55+
},
56+
},
57+
expected: &datasource.ValidateConfigResponse{},
58+
},
59+
"diagnostics": {
60+
pathExpressions: path.Expressions{
61+
path.MatchRoot("test1"),
62+
path.MatchRoot("test2"),
63+
},
64+
req: datasource.ValidateConfigRequest{
65+
Config: tfsdk.Config{
66+
Schema: tfsdk.Schema{
67+
Attributes: map[string]tfsdk.Attribute{
68+
"test1": {
69+
Optional: true,
70+
Type: types.StringType,
71+
},
72+
"test2": {
73+
Optional: true,
74+
Type: types.StringType,
75+
},
76+
"other": {
77+
Optional: true,
78+
Type: types.StringType,
79+
},
80+
},
81+
},
82+
Raw: tftypes.NewValue(
83+
tftypes.Object{
84+
AttributeTypes: map[string]tftypes.Type{
85+
"test1": tftypes.String,
86+
"test2": tftypes.String,
87+
"other": tftypes.String,
88+
},
89+
},
90+
map[string]tftypes.Value{
91+
"test1": tftypes.NewValue(tftypes.String, "test-value"),
92+
"test2": tftypes.NewValue(tftypes.String, "test-value"),
93+
"other": tftypes.NewValue(tftypes.String, "test-value"),
94+
},
95+
),
96+
},
97+
},
98+
expected: &datasource.ValidateConfigResponse{
99+
Diagnostics: diag.Diagnostics{
100+
diag.NewAttributeErrorDiagnostic(
101+
path.Root("test1"),
102+
"Invalid Attribute Combination",
103+
"These attributes cannot be configured together: [test1,test2]",
104+
),
105+
},
106+
},
107+
},
108+
}
109+
110+
for name, testCase := range testCases {
111+
name, testCase := name, testCase
112+
113+
t.Run(name, func(t *testing.T) {
114+
t.Parallel()
115+
116+
validator := datasourcevalidator.Conflicting(testCase.pathExpressions...)
117+
got := &datasource.ValidateConfigResponse{}
118+
119+
validator.ValidateDataSource(context.Background(), testCase.req, got)
120+
121+
if diff := cmp.Diff(got, testCase.expected); diff != "" {
122+
t.Errorf("unexpected difference: %s", diff)
123+
}
124+
})
125+
}
126+
}

datasourcevalidator/doc.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// Package datasourcevalidator provides validators to express relationships
2+
// between multiple attributes of a data source. For example, checking that
3+
// multiple attributes are not configured at the same time.
4+
//
5+
// These validators are implemented outside the schema, which may be easier to
6+
// implement in provider code generation situations or suit provider code
7+
// preferences differently than those in the schemavalidator package. Those
8+
// validators start on a starting attribute, where relationships can be
9+
// expressed as absolute paths to others or relative to the starting attribute.
10+
package datasourcevalidator
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package datasourcevalidator
2+
3+
import (
4+
"github.com/hashicorp/terraform-plugin-framework-validators/internal/configvalidator"
5+
"github.com/hashicorp/terraform-plugin-framework/datasource"
6+
"github.com/hashicorp/terraform-plugin-framework/path"
7+
)
8+
9+
// ExactlyOneOf checks that a set of path.Expression does not have more than
10+
// one known value.
11+
func ExactlyOneOf(expressions ...path.Expression) datasource.ConfigValidator {
12+
return &configvalidator.ExactlyOneOfValidator{
13+
PathExpressions: expressions,
14+
}
15+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package datasourcevalidator_test
2+
3+
import (
4+
"github.com/hashicorp/terraform-plugin-framework-validators/datasourcevalidator"
5+
"github.com/hashicorp/terraform-plugin-framework/datasource"
6+
"github.com/hashicorp/terraform-plugin-framework/path"
7+
)
8+
9+
func ExampleExactlyOneOf() {
10+
// Used inside a datasource.DataSource type ConfigValidators method
11+
_ = []datasource.ConfigValidator{
12+
// Validate only one of the schema defined attributes named attr1
13+
// and attr2 has a known, non-null value.
14+
datasourcevalidator.ExactlyOneOf(
15+
path.MatchRoot("attr1"),
16+
path.MatchRoot("attr2"),
17+
),
18+
}
19+
}

0 commit comments

Comments
 (0)