Skip to content

Commit cc8ec27

Browse files
author
Ivan De Marino
committed
Introducing OneOf and NoneOf validator
Those check that the value(s) in an attribute is(are) one (or none) of the given `accptableValues`. Those 2 act "symmetrically" to each other.
1 parent e5e117e commit cc8ec27

File tree

5 files changed

+648
-0
lines changed

5 files changed

+648
-0
lines changed
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package genericvalidator
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag"
8+
"github.com/hashicorp/terraform-plugin-framework/attr"
9+
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
10+
"github.com/hashicorp/terraform-plugin-framework/types"
11+
)
12+
13+
// acceptableValuesAttributeValidator is the underlying struct implementing OneOf and NoneOf.
14+
type acceptableValuesAttributeValidator struct {
15+
acceptableValues []attr.Value
16+
shouldMatch bool
17+
}
18+
19+
var _ tfsdk.AttributeValidator = (*acceptableValuesAttributeValidator)(nil)
20+
21+
func (av *acceptableValuesAttributeValidator) Description(ctx context.Context) string {
22+
return av.MarkdownDescription(ctx)
23+
}
24+
25+
func (av *acceptableValuesAttributeValidator) MarkdownDescription(_ context.Context) string {
26+
if av.shouldMatch {
27+
return fmt.Sprintf("Value(s) must match one of: %q", av.acceptableValues)
28+
} else {
29+
return fmt.Sprintf("Value(s) must match none of: %q", av.acceptableValues)
30+
}
31+
32+
}
33+
34+
func (av *acceptableValuesAttributeValidator) Validate(ctx context.Context, req tfsdk.ValidateAttributeRequest, res *tfsdk.ValidateAttributeResponse) {
35+
if req.AttributeConfig.IsNull() || req.AttributeConfig.IsUnknown() {
36+
return
37+
}
38+
39+
// Gather the `values` to validate:
40+
// singleton slice for primitives,
41+
// a collection for the other possible `types.*`.
42+
var values []attr.Value
43+
switch typedAttributeConfig := req.AttributeConfig.(type) {
44+
case types.List:
45+
values = typedAttributeConfig.Elems
46+
case types.Map:
47+
values = make([]attr.Value, 0, len(typedAttributeConfig.Elems))
48+
for _, v := range typedAttributeConfig.Elems {
49+
values = append(values, v)
50+
}
51+
case types.Set:
52+
values = typedAttributeConfig.Elems
53+
case types.Object:
54+
values = make([]attr.Value, 0, len(typedAttributeConfig.Attrs))
55+
for _, v := range typedAttributeConfig.Attrs {
56+
values = append(values, v)
57+
}
58+
default:
59+
values = []attr.Value{typedAttributeConfig}
60+
}
61+
62+
for _, v := range values {
63+
if av.shouldMatch && !av.isAcceptableValue(v) || //< EITHER should match but it does not
64+
!av.shouldMatch && av.isAcceptableValue(v) { //< OR should not match but it does
65+
res.Diagnostics.Append(validatordiag.InvalidValueMatchDiagnostic(
66+
req.AttributePath,
67+
av.Description(ctx),
68+
v.String(),
69+
))
70+
}
71+
}
72+
}
73+
74+
func (av *acceptableValuesAttributeValidator) isAcceptableValue(v attr.Value) bool {
75+
for _, acceptableV := range av.acceptableValues {
76+
if v.Equal(acceptableV) {
77+
return true
78+
}
79+
}
80+
81+
return false
82+
}

genericvalidator/none_of.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package genericvalidator
2+
3+
import (
4+
"github.com/hashicorp/terraform-plugin-framework/attr"
5+
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
6+
)
7+
8+
// NoneOf checks that value(s) held in the attribute
9+
// is (are) none of the given `acceptableValues`.
10+
//
11+
// This validator can be used with all primitive `types.*`, as well as
12+
// collections (`types.List`, `types.Set`, `types.Map` and `types.Object`):
13+
// for key/value collections, the validator will be applied only to the values.
14+
func NoneOf(acceptableValues ...attr.Value) tfsdk.AttributeValidator {
15+
return &acceptableValuesAttributeValidator{
16+
acceptableValues: acceptableValues,
17+
shouldMatch: false,
18+
}
19+
}

genericvalidator/none_of_test.go

Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
package genericvalidator_test
2+
3+
import (
4+
"context"
5+
"math"
6+
"math/big"
7+
"testing"
8+
9+
"github.com/hashicorp/terraform-plugin-framework-validators/genericvalidator"
10+
"github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag"
11+
"github.com/hashicorp/terraform-plugin-framework/attr"
12+
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
13+
"github.com/hashicorp/terraform-plugin-framework/types"
14+
)
15+
16+
func TestNoneOfValidator(t *testing.T) {
17+
t.Parallel()
18+
19+
type testCase struct {
20+
in attr.Value
21+
validator tfsdk.AttributeValidator
22+
expErrors int
23+
}
24+
25+
objPersonAttrTypes := map[string]attr.Type{
26+
"Name": types.StringType,
27+
"Age": types.Int64Type,
28+
}
29+
objAttrTypes := map[string]attr.Type{
30+
"Person": types.ObjectType{
31+
AttrTypes: objPersonAttrTypes,
32+
},
33+
"Address": types.StringType,
34+
}
35+
36+
testCases := map[string]testCase{
37+
"simple-match": {
38+
in: types.String{Value: "foo"},
39+
validator: genericvalidator.NoneOf(
40+
types.String{Value: "foo"},
41+
types.String{Value: "bar"},
42+
types.String{Value: "baz"},
43+
),
44+
expErrors: 1,
45+
},
46+
"simple-mismatch-match": {
47+
in: types.String{Value: "foz"},
48+
validator: genericvalidator.NoneOf(
49+
types.String{Value: "foo"},
50+
types.String{Value: "bar"},
51+
types.String{Value: "baz"},
52+
),
53+
},
54+
"mixed": {
55+
in: types.Float64{Value: 1.234},
56+
validator: genericvalidator.NoneOf(
57+
types.String{Value: "foo"},
58+
types.Int64{Value: 567},
59+
types.Float64{Value: 1.234},
60+
),
61+
expErrors: 1,
62+
},
63+
"list": {
64+
in: types.List{
65+
ElemType: types.Int64Type,
66+
Elems: []attr.Value{
67+
types.Int64{Value: 10},
68+
types.Int64{Value: 20},
69+
types.Int64{Value: 30},
70+
},
71+
},
72+
validator: genericvalidator.NoneOf(
73+
types.Int64{Value: 10},
74+
types.Int64{Value: 20},
75+
types.Int64{Value: 30},
76+
types.Int64{Value: 40},
77+
types.Int64{Value: 50},
78+
),
79+
expErrors: 3,
80+
},
81+
"list-mismatch": {
82+
in: types.List{
83+
ElemType: types.Int64Type,
84+
Elems: []attr.Value{
85+
types.Int64{Value: 11},
86+
types.Int64{Value: 20},
87+
types.Int64{Value: 32},
88+
},
89+
},
90+
validator: genericvalidator.NoneOf(
91+
types.Int64{Value: 10},
92+
types.Int64{Value: 30},
93+
types.Int64{Value: 40},
94+
types.Int64{Value: 50},
95+
),
96+
},
97+
"set": {
98+
in: types.Set{
99+
ElemType: types.StringType,
100+
Elems: []attr.Value{
101+
types.String{Value: "foo"},
102+
types.String{Value: "bar"},
103+
types.String{Value: "baz"},
104+
},
105+
},
106+
validator: genericvalidator.NoneOf(
107+
types.String{Value: "bob"},
108+
types.String{Value: "alice"},
109+
types.String{Value: "john"},
110+
types.String{Value: "foo"},
111+
types.String{Value: "bar"},
112+
types.String{Value: "baz"},
113+
),
114+
expErrors: 3,
115+
},
116+
"set-mismatch": {
117+
in: types.Set{
118+
ElemType: types.StringType,
119+
Elems: []attr.Value{
120+
types.String{Value: "foo"},
121+
types.String{Value: "bar"},
122+
types.String{Value: "baz"},
123+
},
124+
},
125+
validator: genericvalidator.NoneOf(
126+
types.String{Value: "terraform"},
127+
types.String{Value: "packer"},
128+
types.String{Value: "vault"},
129+
types.String{Value: "boundary"},
130+
types.String{Value: "nomad"},
131+
types.String{Value: "vagrant"},
132+
types.String{Value: "waypoint"},
133+
types.String{Value: "consul"},
134+
),
135+
},
136+
"map": {
137+
in: types.Map{
138+
ElemType: types.NumberType,
139+
Elems: map[string]attr.Value{
140+
"one.one": types.Number{Value: big.NewFloat(1.1)},
141+
"ten.twenty": types.Number{Value: big.NewFloat(10.20)},
142+
"five.four": types.Number{Value: big.NewFloat(5.4)},
143+
},
144+
},
145+
validator: genericvalidator.NoneOf(
146+
types.Number{Value: big.NewFloat(1.1)},
147+
types.Number{Value: big.NewFloat(math.MaxFloat64)},
148+
types.Number{Value: big.NewFloat(math.SmallestNonzeroFloat64)},
149+
types.Number{Value: big.NewFloat(10.20)},
150+
types.Number{Value: big.NewFloat(5.4)},
151+
),
152+
expErrors: 3,
153+
},
154+
"map-mismatch": {
155+
in: types.Map{
156+
ElemType: types.NumberType,
157+
Elems: map[string]attr.Value{
158+
"one.one": types.Number{Value: big.NewFloat(1.1)},
159+
"ten.twenty": types.Number{Value: big.NewFloat(10.20)},
160+
"five.four": types.Number{Value: big.NewFloat(5.4)},
161+
},
162+
},
163+
validator: genericvalidator.NoneOf(
164+
types.Number{Value: big.NewFloat(math.MaxFloat64)},
165+
types.Number{Value: big.NewFloat(math.SmallestNonzeroFloat64)},
166+
),
167+
},
168+
"object": {
169+
in: types.Object{
170+
AttrTypes: objAttrTypes,
171+
Attrs: map[string]attr.Value{
172+
"Person": types.Object{
173+
AttrTypes: objPersonAttrTypes,
174+
Attrs: map[string]attr.Value{
175+
"Name": types.String{Value: "Bob Parr"},
176+
"Age": types.Int64{Value: 40},
177+
},
178+
},
179+
"Address": types.String{Value: "1200 Park Avenue Emeryville"},
180+
},
181+
},
182+
validator: genericvalidator.NoneOf(
183+
types.Object{
184+
AttrTypes: map[string]attr.Type{},
185+
Attrs: map[string]attr.Value{},
186+
},
187+
types.Object{
188+
AttrTypes: objPersonAttrTypes,
189+
Attrs: map[string]attr.Value{
190+
"Name": types.String{Value: "Bob Parr"},
191+
"Age": types.Int64{Value: 40},
192+
},
193+
},
194+
types.String{Value: "1200 Park Avenue Emeryville"},
195+
types.Int64{Value: 123},
196+
types.String{Value: "Bob Parr"},
197+
),
198+
expErrors: 2,
199+
},
200+
"object-mismatch": {
201+
in: types.Object{
202+
AttrTypes: objAttrTypes,
203+
Attrs: map[string]attr.Value{
204+
"Person": types.Object{
205+
AttrTypes: objPersonAttrTypes,
206+
Attrs: map[string]attr.Value{
207+
"Name": types.String{Value: "Bob Parr"},
208+
"Age": types.Int64{Value: 40},
209+
},
210+
},
211+
"Address": types.String{Value: "1200 Park Avenue Emeryville"},
212+
},
213+
},
214+
validator: genericvalidator.NoneOf(
215+
types.Object{
216+
AttrTypes: map[string]attr.Type{},
217+
Attrs: map[string]attr.Value{},
218+
},
219+
types.Int64{Value: 123},
220+
types.String{Value: "Bob Parr"},
221+
),
222+
},
223+
"skip-validation-on-null": {
224+
in: types.String{Null: true},
225+
validator: genericvalidator.NoneOf(
226+
types.String{Value: "foo"},
227+
types.String{Value: "bar"},
228+
types.String{Value: "baz"},
229+
),
230+
},
231+
"skip-validation-on-unknown": {
232+
in: types.String{Unknown: true},
233+
validator: genericvalidator.NoneOf(
234+
types.String{Value: "foo"},
235+
types.String{Value: "bar"},
236+
types.String{Value: "baz"},
237+
),
238+
},
239+
}
240+
241+
for name, test := range testCases {
242+
name, test := name, test
243+
t.Run(name, func(t *testing.T) {
244+
req := tfsdk.ValidateAttributeRequest{
245+
AttributeConfig: test.in,
246+
}
247+
res := tfsdk.ValidateAttributeResponse{}
248+
test.validator.Validate(context.TODO(), req, &res)
249+
250+
if test.expErrors > 0 && !res.Diagnostics.HasError() {
251+
t.Fatalf("expected %d error(s), got none", test.expErrors)
252+
}
253+
254+
if test.expErrors > 0 && test.expErrors != validatordiag.ErrorsCount(res.Diagnostics) {
255+
t.Fatalf("expected %d error(s), got %d: %v", test.expErrors, validatordiag.ErrorsCount(res.Diagnostics), res.Diagnostics)
256+
}
257+
258+
if test.expErrors == 0 && res.Diagnostics.HasError() {
259+
t.Fatalf("expected no error(s), got %d: %v", validatordiag.ErrorsCount(res.Diagnostics), res.Diagnostics)
260+
}
261+
})
262+
}
263+
}

genericvalidator/one_of.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package genericvalidator
2+
3+
import (
4+
"github.com/hashicorp/terraform-plugin-framework/attr"
5+
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
6+
)
7+
8+
// OneOf checks that value(s) held in the attribute
9+
// is (are) one of the given `acceptableValues`.
10+
//
11+
// This validator can be used with all primitive `types.*`, as well as
12+
// collections (`types.List`, `types.Set`, `types.Map` and `types.Object`):
13+
// for key/value collections, the validator will be applied only to the values.
14+
func OneOf(acceptableValues ...attr.Value) tfsdk.AttributeValidator {
15+
return &acceptableValuesAttributeValidator{
16+
acceptableValues: acceptableValues,
17+
shouldMatch: true,
18+
}
19+
}

0 commit comments

Comments
 (0)