Skip to content

Commit db12191

Browse files
committed
validation: Allow loose validation for sets
If a set (or associative lists keys) has duplicated elements, then this will not fail the validation. Everything is still the same.
1 parent 476102f commit db12191

File tree

4 files changed

+54
-18
lines changed

4 files changed

+54
-18
lines changed

typed/parser.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -93,13 +93,13 @@ func (p ParseableType) IsValid() bool {
9393

9494
// FromYAML parses a yaml string into an object with the current schema
9595
// and the type "typename" or an error if validation fails.
96-
func (p ParseableType) FromYAML(object YAMLObject) (*TypedValue, error) {
96+
func (p ParseableType) FromYAML(object YAMLObject, opts ...ValidationOptions) (*TypedValue, error) {
9797
var v interface{}
9898
err := yaml.Unmarshal([]byte(object), &v)
9999
if err != nil {
100100
return nil, err
101101
}
102-
return AsTyped(value.NewValueInterface(v), p.Schema, p.TypeRef)
102+
return AsTyped(value.NewValueInterface(v), p.Schema, p.TypeRef, opts...)
103103
}
104104

105105
// FromUnstructured converts a go "interface{}" type, typically an
@@ -108,21 +108,21 @@ func (p ParseableType) FromYAML(object YAMLObject) (*TypedValue, error) {
108108
// The provided interface{} must be one of: map[string]interface{},
109109
// map[interface{}]interface{}, []interface{}, int types, float types,
110110
// string or boolean. Nested interface{} must also be one of these types.
111-
func (p ParseableType) FromUnstructured(in interface{}) (*TypedValue, error) {
112-
return AsTyped(value.NewValueInterface(in), p.Schema, p.TypeRef)
111+
func (p ParseableType) FromUnstructured(in interface{}, opts ...ValidationOptions) (*TypedValue, error) {
112+
return AsTyped(value.NewValueInterface(in), p.Schema, p.TypeRef, opts...)
113113
}
114114

115115
// FromStructured converts a go "interface{}" type, typically an structured object in
116116
// Kubernetes, to a TypedValue. It will return an error if the resulting object fails
117117
// schema validation. The provided "interface{}" value must be a pointer so that the
118118
// value can be modified via reflection. The provided "interface{}" may contain structs
119119
// and types that are converted to Values by the jsonMarshaler interface.
120-
func (p ParseableType) FromStructured(in interface{}) (*TypedValue, error) {
120+
func (p ParseableType) FromStructured(in interface{}, opts ...ValidationOptions) (*TypedValue, error) {
121121
v, err := value.NewValueReflect(in)
122122
if err != nil {
123123
return nil, fmt.Errorf("error creating struct value reflector: %v", err)
124124
}
125-
return AsTyped(v, p.Schema, p.TypeRef)
125+
return AsTyped(v, p.Schema, p.TypeRef, opts...)
126126
}
127127

128128
// DeducedParseableType is a ParseableType that deduces the type from

typed/typed.go

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,16 +24,24 @@ import (
2424
"sigs.k8s.io/structured-merge-diff/v4/value"
2525
)
2626

27+
// ValidationOptions is the list of all the options available when running the validation.
28+
type ValidationOptions int
29+
30+
const (
31+
// AllowDuplicates means that sets and associative lists can have duplicate similar items.
32+
AllowDuplicates ValidationOptions = iota
33+
)
34+
2735
// AsTyped accepts a value and a type and returns a TypedValue. 'v' must have
2836
// type 'typeName' in the schema. An error is returned if the v doesn't conform
2937
// to the schema.
30-
func AsTyped(v value.Value, s *schema.Schema, typeRef schema.TypeRef) (*TypedValue, error) {
38+
func AsTyped(v value.Value, s *schema.Schema, typeRef schema.TypeRef, opts ...ValidationOptions) (*TypedValue, error) {
3139
tv := &TypedValue{
3240
value: v,
3341
typeRef: typeRef,
3442
schema: s,
3543
}
36-
if err := tv.Validate(); err != nil {
44+
if err := tv.Validate(opts...); err != nil {
3745
return nil, err
3846
}
3947
return tv, nil
@@ -79,8 +87,14 @@ func (tv TypedValue) Schema() *schema.Schema {
7987
}
8088

8189
// Validate returns an error with a list of every spec violation.
82-
func (tv TypedValue) Validate() error {
90+
func (tv TypedValue) Validate(opts ...ValidationOptions) error {
8391
w := tv.walker()
92+
for _, opt := range opts {
93+
switch opt {
94+
case AllowDuplicates:
95+
w.allowDuplicates = true
96+
}
97+
}
8498
defer w.finished()
8599
if errs := w.validate(nil); len(errs) != 0 {
86100
return errs

typed/validate.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ func (tv TypedValue) walker() *validatingObjectWalker {
3333
v.value = tv.value
3434
v.schema = tv.schema
3535
v.typeRef = tv.typeRef
36+
v.allowDuplicates = false
3637
if v.allocator == nil {
3738
v.allocator = value.NewFreelistAllocator()
3839
}
@@ -49,6 +50,9 @@ type validatingObjectWalker struct {
4950
value value.Value
5051
schema *schema.Schema
5152
typeRef schema.TypeRef
53+
// If set to true, duplicates will be allowed in
54+
// associativeLists/sets.
55+
allowDuplicates bool
5256

5357
// Allocate only as many walkers as needed for the depth by storing them here.
5458
spareWalkers *[]*validatingObjectWalker
@@ -137,7 +141,7 @@ func (v *validatingObjectWalker) visitListItems(t *schema.List, list value.List)
137141
// this element.
138142
return
139143
}
140-
if observedKeys.Has(pe) {
144+
if observedKeys.Has(pe) && !v.allowDuplicates {
141145
errs = append(errs, errorf("duplicate entries for key %v", pe.String())...)
142146
}
143147
observedKeys.Insert(pe)

typed/validate_test.go

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ type validationTestCase struct {
3131
schema typed.YAMLObject
3232
validObjects []typed.YAMLObject
3333
invalidObjects []typed.YAMLObject
34+
// duplicatesObjects are valid with AllowDuplicates validation, invalid otherwise.
35+
duplicatesObjects []typed.YAMLObject
3436
}
3537

3638
var validationCases = []validationTestCase{{
@@ -63,7 +65,6 @@ var validationCases = []validationTestCase{{
6365
`{"key":"foo","value":null}`,
6466
`{"key":"foo"}`,
6567
`{"key":"foo","value":true}`,
66-
`{"key":"foo","value":true}`,
6768
`{"key":null}`,
6869
},
6970
invalidObjects: []typed.YAMLObject{
@@ -136,28 +137,27 @@ var validationCases = []validationTestCase{{
136137
`{"bool":"aoeu"}`,
137138
`{"bool":{"a":1}}`,
138139
`{"bool":["foo"]}`,
139-
`{"setStr":["a","a"]}`,
140-
`{"setBool":[true,false,true]}`,
141-
`{"setNumeric":[1,2,3,3.14159,1]}`,
142140
`{"setStr":[1]}`,
143141
`{"setStr":[true]}`,
144142
`{"setStr":[1.5]}`,
145143
`{"setStr":[null]}`,
146144
`{"setStr":[{}]}`,
147145
`{"setStr":[[]]}`,
148-
`{"setBool":[true,false,true]}`,
149146
`{"setBool":[1]}`,
150147
`{"setBool":[1.5]}`,
151148
`{"setBool":[null]}`,
152149
`{"setBool":[{}]}`,
153150
`{"setBool":[[]]}`,
154151
`{"setBool":["a"]}`,
155-
`{"setNumeric":[1,2,3,3.14159,1]}`,
156152
`{"setNumeric":[null]}`,
157153
`{"setNumeric":[true]}`,
158154
`{"setNumeric":["a"]}`,
159155
`{"setNumeric":[[]]}`,
160156
`{"setNumeric":[{}]}`,
157+
}, duplicatesObjects: []typed.YAMLObject{
158+
`{"setStr":["a","a"]}`,
159+
`{"setBool":[true,false,true]}`,
160+
`{"setNumeric":[1,2,3,3.14159,1]}`,
161161
},
162162
}, {
163163
name: "associative list",
@@ -232,9 +232,10 @@ var validationCases = []validationTestCase{{
232232
`{"list":[{}]}`,
233233
`{"list":[{"value":{"a":"a"},"bv":true,"nv":3.14}]}`,
234234
`{"list":[{"key":"a","id":1,"value":{"a":1}}]}`,
235-
`{"list":[{"key":"a","id":1},{"key":"a","id":1}]}`,
236235
`{"list":[{"key":"a","id":1,"value":{"a":"a"},"bv":"true","nv":3.14}]}`,
237236
`{"list":[{"key":"a","id":1,"value":{"a":"a"},"bv":true,"nv":false}]}`,
237+
}, duplicatesObjects: []typed.YAMLObject{
238+
`{"list":[{"key":"a","id":1},{"key":"a","id":1}]}`,
238239
},
239240
}}
240241

@@ -262,13 +263,30 @@ func (tt validationTestCase) test(t *testing.T) {
262263
t.Parallel()
263264
_, err := pt.FromYAML(iv)
264265
if err == nil {
265-
t.Errorf("Object should fail: %v\n%v", err, iv)
266+
t.Fatalf("Object should fail:\n%v", iv)
266267
}
267268
if strings.Contains(err.Error(), "invalid atom") {
268269
t.Errorf("Error should be useful, but got: %v\n%v", err, iv)
269270
}
270271
})
271272
}
273+
for i, iv := range tt.duplicatesObjects {
274+
iv := iv
275+
t.Run(fmt.Sprintf("%v-duplicates-%v", tt.name, i), func(t *testing.T) {
276+
t.Parallel()
277+
_, err := pt.FromYAML(iv)
278+
if err == nil {
279+
t.Fatalf("Object should fail:\n%v", iv)
280+
}
281+
if strings.Contains(err.Error(), "invalid atom") {
282+
t.Errorf("Error should be useful, but got: %v\n%v", err, iv)
283+
}
284+
_, err = pt.FromYAML(iv, typed.AllowDuplicates)
285+
if err != nil {
286+
t.Errorf("failed to parse/validate yaml: %v\n%v", err, iv)
287+
}
288+
})
289+
}
272290
}
273291

274292
func TestSchemaValidation(t *testing.T) {

0 commit comments

Comments
 (0)