Skip to content

Commit 58d15ef

Browse files
author
Ivan De Marino
committed
New schemavalidator package
1 parent d43cf99 commit 58d15ef

9 files changed

+1283
-0
lines changed

schemavalidator/at_least_one_of.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package schemavalidator
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/hashicorp/terraform-plugin-framework-validators/helpers/attributepath"
8+
"github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag"
9+
"github.com/hashicorp/terraform-plugin-framework/attr"
10+
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
11+
"github.com/hashicorp/terraform-plugin-go/tftypes"
12+
)
13+
14+
// atLeastOneOfAttributeValidator is the underlying struct implementing AtLeastOneOf.
15+
type atLeastOneOfAttributeValidator struct {
16+
attrPaths []*tftypes.AttributePath
17+
}
18+
19+
// AtLeastOneOf checks that of a set of *tftypes.AttributePath,
20+
// including the attribute it's applied to, at least one attribute out of all specified is configured.
21+
//
22+
// The provided tftypes.AttributePath must be "absolute",
23+
// and starting with top level attribute names.
24+
func AtLeastOneOf(attributePaths ...*tftypes.AttributePath) tfsdk.AttributeValidator {
25+
return &atLeastOneOfAttributeValidator{attributePaths}
26+
}
27+
28+
var _ tfsdk.AttributeValidator = (*atLeastOneOfAttributeValidator)(nil)
29+
30+
func (av atLeastOneOfAttributeValidator) Description(ctx context.Context) string {
31+
return av.MarkdownDescription(ctx)
32+
}
33+
34+
func (av atLeastOneOfAttributeValidator) MarkdownDescription(ctx context.Context) string {
35+
return fmt.Sprintf("Ensure that at least one attribute from this collection is set: %q", av.attrPaths)
36+
}
37+
38+
func (av atLeastOneOfAttributeValidator) Validate(ctx context.Context, req tfsdk.ValidateAttributeRequest, res *tfsdk.ValidateAttributeResponse) {
39+
// Assemble a slice of paths, ensuring we don't repeat the attribute this validator is applied to
40+
var paths []*tftypes.AttributePath
41+
if attributepath.Contains(req.AttributePath, av.attrPaths...) {
42+
paths = av.attrPaths
43+
} else {
44+
paths = append(av.attrPaths, req.AttributePath)
45+
}
46+
47+
for _, path := range paths {
48+
var v attr.Value
49+
diags := req.Config.GetAttribute(ctx, path, &v)
50+
res.Diagnostics.Append(diags...)
51+
if diags.HasError() {
52+
return
53+
}
54+
55+
if !v.IsNull() {
56+
return
57+
}
58+
}
59+
60+
res.Diagnostics.Append(validatordiag.InvalidSchemaDiagnostic(
61+
req.AttributePath,
62+
fmt.Sprintf("At least one attribute out of %q must be specified", attributepath.JoinToString(paths...)),
63+
))
64+
}
Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
package schemavalidator_test
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag"
8+
"github.com/hashicorp/terraform-plugin-framework-validators/schemavalidator"
9+
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
10+
"github.com/hashicorp/terraform-plugin-framework/types"
11+
"github.com/hashicorp/terraform-plugin-go/tftypes"
12+
)
13+
14+
func TestAtLeastOneOfValidator(t *testing.T) {
15+
t.Parallel()
16+
17+
type testCase struct {
18+
req tfsdk.ValidateAttributeRequest
19+
in []*tftypes.AttributePath
20+
expErrors int
21+
}
22+
23+
testCases := map[string]testCase{
24+
"base": {
25+
req: tfsdk.ValidateAttributeRequest{
26+
AttributeConfig: types.String{Value: "bar value"},
27+
AttributePath: tftypes.NewAttributePath().WithAttributeName("bar"),
28+
Config: tfsdk.Config{
29+
Schema: tfsdk.Schema{
30+
Attributes: map[string]tfsdk.Attribute{
31+
"foo": {
32+
Type: types.Int64Type,
33+
},
34+
"bar": {
35+
Type: types.StringType,
36+
},
37+
},
38+
},
39+
Raw: tftypes.NewValue(tftypes.Object{
40+
AttributeTypes: map[string]tftypes.Type{
41+
"foo": tftypes.Number,
42+
"bar": tftypes.String,
43+
},
44+
}, map[string]tftypes.Value{
45+
"foo": tftypes.NewValue(tftypes.Number, 42),
46+
"bar": tftypes.NewValue(tftypes.String, "bar value"),
47+
}),
48+
},
49+
},
50+
in: []*tftypes.AttributePath{
51+
tftypes.NewAttributePath().WithAttributeName("foo"),
52+
},
53+
},
54+
"self-is-null": {
55+
req: tfsdk.ValidateAttributeRequest{
56+
AttributeConfig: types.String{Null: true},
57+
AttributePath: tftypes.NewAttributePath().WithAttributeName("bar"),
58+
Config: tfsdk.Config{
59+
Schema: tfsdk.Schema{
60+
Attributes: map[string]tfsdk.Attribute{
61+
"foo": {
62+
Type: types.Int64Type,
63+
},
64+
"bar": {
65+
Type: types.StringType,
66+
},
67+
},
68+
},
69+
Raw: tftypes.NewValue(tftypes.Object{
70+
AttributeTypes: map[string]tftypes.Type{
71+
"foo": tftypes.Number,
72+
"bar": tftypes.String,
73+
},
74+
}, map[string]tftypes.Value{
75+
"foo": tftypes.NewValue(tftypes.Number, 42),
76+
"bar": tftypes.NewValue(tftypes.String, nil),
77+
}),
78+
},
79+
},
80+
in: []*tftypes.AttributePath{
81+
tftypes.NewAttributePath().WithAttributeName("foo"),
82+
},
83+
},
84+
"error_none-set": {
85+
req: tfsdk.ValidateAttributeRequest{
86+
AttributeConfig: types.String{Value: "bar value"},
87+
AttributePath: tftypes.NewAttributePath().WithAttributeName("bar"),
88+
Config: tfsdk.Config{
89+
Schema: tfsdk.Schema{
90+
Attributes: map[string]tfsdk.Attribute{
91+
"foo": {
92+
Type: types.Int64Type,
93+
},
94+
"bar": {
95+
Type: types.StringType,
96+
},
97+
"baz": {
98+
Type: types.Int64Type,
99+
},
100+
},
101+
},
102+
Raw: tftypes.NewValue(tftypes.Object{
103+
AttributeTypes: map[string]tftypes.Type{
104+
"foo": tftypes.Number,
105+
"bar": tftypes.String,
106+
"baz": tftypes.Number,
107+
},
108+
}, map[string]tftypes.Value{
109+
"foo": tftypes.NewValue(tftypes.Number, nil),
110+
"bar": tftypes.NewValue(tftypes.String, nil),
111+
"baz": tftypes.NewValue(tftypes.Number, nil),
112+
}),
113+
},
114+
},
115+
in: []*tftypes.AttributePath{
116+
tftypes.NewAttributePath().WithAttributeName("foo"),
117+
tftypes.NewAttributePath().WithAttributeName("baz"),
118+
},
119+
expErrors: 1,
120+
},
121+
"multiple-set": {
122+
req: tfsdk.ValidateAttributeRequest{
123+
AttributeConfig: types.String{Value: "bar value"},
124+
AttributePath: tftypes.NewAttributePath().WithAttributeName("bar"),
125+
Config: tfsdk.Config{
126+
Schema: tfsdk.Schema{
127+
Attributes: map[string]tfsdk.Attribute{
128+
"foo": {
129+
Type: types.Int64Type,
130+
},
131+
"bar": {
132+
Type: types.StringType,
133+
},
134+
"baz": {
135+
Type: types.Float64Type,
136+
},
137+
},
138+
},
139+
Raw: tftypes.NewValue(tftypes.Object{
140+
AttributeTypes: map[string]tftypes.Type{
141+
"foo": tftypes.Number,
142+
"bar": tftypes.String,
143+
"baz": tftypes.Number,
144+
},
145+
}, map[string]tftypes.Value{
146+
"foo": tftypes.NewValue(tftypes.Number, 42),
147+
"bar": tftypes.NewValue(tftypes.String, "bar value"),
148+
"baz": tftypes.NewValue(tftypes.Number, 4.2),
149+
}),
150+
},
151+
},
152+
in: []*tftypes.AttributePath{
153+
tftypes.NewAttributePath().WithAttributeName("foo"),
154+
tftypes.NewAttributePath().WithAttributeName("baz"),
155+
},
156+
},
157+
"allow-duplicate-input": {
158+
req: tfsdk.ValidateAttributeRequest{
159+
AttributeConfig: types.String{Value: "bar value"},
160+
AttributePath: tftypes.NewAttributePath().WithAttributeName("bar"),
161+
Config: tfsdk.Config{
162+
Schema: tfsdk.Schema{
163+
Attributes: map[string]tfsdk.Attribute{
164+
"foo": {
165+
Type: types.Int64Type,
166+
},
167+
"bar": {
168+
Type: types.StringType,
169+
},
170+
"baz": {
171+
Type: types.Int64Type,
172+
},
173+
},
174+
},
175+
Raw: tftypes.NewValue(tftypes.Object{
176+
AttributeTypes: map[string]tftypes.Type{
177+
"foo": tftypes.Number,
178+
"bar": tftypes.String,
179+
"baz": tftypes.Number,
180+
},
181+
}, map[string]tftypes.Value{
182+
"foo": tftypes.NewValue(tftypes.Number, nil),
183+
"bar": tftypes.NewValue(tftypes.String, "bar value"),
184+
"baz": tftypes.NewValue(tftypes.Number, nil),
185+
}),
186+
},
187+
},
188+
in: []*tftypes.AttributePath{
189+
tftypes.NewAttributePath().WithAttributeName("foo"),
190+
tftypes.NewAttributePath().WithAttributeName("bar"),
191+
tftypes.NewAttributePath().WithAttributeName("baz"),
192+
},
193+
},
194+
"unknowns": {
195+
req: tfsdk.ValidateAttributeRequest{
196+
AttributeConfig: types.String{Value: "bar value"},
197+
AttributePath: tftypes.NewAttributePath().WithAttributeName("bar"),
198+
Config: tfsdk.Config{
199+
Schema: tfsdk.Schema{
200+
Attributes: map[string]tfsdk.Attribute{
201+
"foo": {
202+
Type: types.Int64Type,
203+
},
204+
"bar": {
205+
Type: types.StringType,
206+
},
207+
"baz": {
208+
Type: types.Int64Type,
209+
},
210+
},
211+
},
212+
Raw: tftypes.NewValue(tftypes.Object{
213+
AttributeTypes: map[string]tftypes.Type{
214+
"foo": tftypes.Number,
215+
"bar": tftypes.String,
216+
"baz": tftypes.Number,
217+
},
218+
}, map[string]tftypes.Value{
219+
"foo": tftypes.NewValue(tftypes.Number, tftypes.UnknownValue),
220+
"bar": tftypes.NewValue(tftypes.String, "bar value"),
221+
"baz": tftypes.NewValue(tftypes.Number, tftypes.UnknownValue),
222+
}),
223+
},
224+
},
225+
in: []*tftypes.AttributePath{
226+
tftypes.NewAttributePath().WithAttributeName("foo"),
227+
tftypes.NewAttributePath().WithAttributeName("baz"),
228+
},
229+
},
230+
}
231+
232+
for name, test := range testCases {
233+
t.Run(name, func(t *testing.T) {
234+
res := tfsdk.ValidateAttributeResponse{}
235+
236+
schemavalidator.AtLeastOneOf(test.in...).Validate(context.TODO(), test.req, &res)
237+
238+
if test.expErrors > 0 && !res.Diagnostics.HasError() {
239+
t.Fatal("expected error(s), got none")
240+
}
241+
242+
if test.expErrors > 0 && test.expErrors != validatordiag.ErrorsCount(res.Diagnostics) {
243+
t.Fatalf("expected %d error(s), got %d: %v", test.expErrors, validatordiag.ErrorsCount(res.Diagnostics), res.Diagnostics)
244+
}
245+
246+
if test.expErrors == 0 && res.Diagnostics.HasError() {
247+
t.Fatalf("expected no error(s), got %d: %v", validatordiag.ErrorsCount(res.Diagnostics), res.Diagnostics)
248+
}
249+
})
250+
}
251+
}

schemavalidator/conflicts_with.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package schemavalidator
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/hashicorp/terraform-plugin-framework-validators/helpers/attributepath"
8+
"github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag"
9+
"github.com/hashicorp/terraform-plugin-framework/attr"
10+
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
11+
"github.com/hashicorp/terraform-plugin-go/tftypes"
12+
)
13+
14+
// conflictsWithAttributeValidator is the underlying struct implementing ConflictsWith.
15+
type conflictsWithAttributeValidator struct {
16+
attrPaths []*tftypes.AttributePath
17+
}
18+
19+
// ConflictsWith checks that a set of *tftypes.AttributePath,
20+
// including the attribute it's applied to, are not set simultaneously.
21+
// This implements the validation logic declaratively within the tfsdk.Schema.
22+
//
23+
// The provided tftypes.AttributePath must be "absolute",
24+
// and starting with top level attribute names.
25+
func ConflictsWith(attributePaths ...*tftypes.AttributePath) tfsdk.AttributeValidator {
26+
return &conflictsWithAttributeValidator{attributePaths}
27+
}
28+
29+
var _ tfsdk.AttributeValidator = (*conflictsWithAttributeValidator)(nil)
30+
31+
func (av conflictsWithAttributeValidator) Description(ctx context.Context) string {
32+
return av.MarkdownDescription(ctx)
33+
}
34+
35+
func (av conflictsWithAttributeValidator) MarkdownDescription(_ context.Context) string {
36+
return fmt.Sprintf("Ensure that if an attribute is set, these are not set: %q", av.attrPaths)
37+
}
38+
39+
func (av conflictsWithAttributeValidator) Validate(ctx context.Context, req tfsdk.ValidateAttributeRequest, res *tfsdk.ValidateAttributeResponse) {
40+
var v attr.Value
41+
res.Diagnostics.Append(tfsdk.ValueAs(ctx, req.AttributeConfig, &v)...)
42+
if res.Diagnostics.HasError() {
43+
return
44+
}
45+
46+
for _, path := range av.attrPaths {
47+
// If the user specifies the same attribute this validator is applied to,
48+
// also as part of the input, skip it.
49+
if req.AttributePath.Equal(path) {
50+
continue
51+
}
52+
53+
var o attr.Value
54+
diags := req.Config.GetAttribute(ctx, path, &o)
55+
res.Diagnostics.Append(diags...)
56+
if diags.HasError() {
57+
return
58+
}
59+
60+
if !v.IsNull() && !o.IsNull() {
61+
res.Diagnostics.Append(validatordiag.InvalidSchemaDiagnostic(
62+
req.AttributePath,
63+
fmt.Sprintf("Attribute %q cannot be specified when %q is specified", attributepath.ToString(path), attributepath.ToString(req.AttributePath)),
64+
))
65+
}
66+
}
67+
}

0 commit comments

Comments
 (0)