Skip to content

Commit 505fb07

Browse files
committed
schema: better errors for AnyOf/AllOf + cleanups
AnyOf and AllOf have received better error messages and doc-strings. They have also been changed so that they will accept any value if there are no members. Clean up mergeFieldErrors to be easer to read; Go allows appending to nil slice and expansion of slices in append, so there is no need for the if branch and nested for-loop that was there before. Simplify error handling in Object.Validate slightly. Moves the ErrorMap type from object.go to errors.go and add some helper methods.
1 parent 3155f2e commit 505fb07

File tree

7 files changed

+98
-51
lines changed

7 files changed

+98
-51
lines changed

schema/alloff.go renamed to schema/allof.go

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package schema
22

3-
// AllOf validates that all the sub field validators validates.
3+
// AllOf validates that all the sub field validators validates. Be aware that
4+
// the order of the validators matter, as the result of one successful
5+
// validation is passed as input to the next.
46
type AllOf []FieldValidator
57

68
// Compile implements the ReferenceCompiler interface.
@@ -15,12 +17,10 @@ func (v AllOf) Compile(rc ReferenceChecker) (err error) {
1517
return
1618
}
1719

18-
// ValidateQuery implements schema.FieldQueryValidator interface
20+
// ValidateQuery implements schema.FieldQueryValidator interface. Note the
21+
// result of one successful validation is passed as input to the next. The
22+
// result of the first error or last successful validation is returned.
1923
func (v AllOf) ValidateQuery(value interface{}) (interface{}, error) {
20-
// This works like this:
21-
// 1. only the first validator gets passed the original value.
22-
// 2. after that, each validator gets passed the value from the previous validator.
23-
// 3. finally, the value returned from the last validator is returned for further use.
2424
for _, validator := range v {
2525
var err error
2626
if validatorQuery, ok := validator.(FieldQueryValidator); ok {
@@ -35,12 +35,10 @@ func (v AllOf) ValidateQuery(value interface{}) (interface{}, error) {
3535
return value, nil
3636
}
3737

38-
// Validate ensures that all sub-validators validates.
38+
// Validate ensures that all sub-validators validates. Note the result of one
39+
// successful validation is passed as input to the next. The result of the first
40+
// error or last successful validation is returned.
3941
func (v AllOf) Validate(value interface{}) (interface{}, error) {
40-
// This works like this:
41-
// 1. only the first validator gets passed the original value.
42-
// 2. after that, each validator gets passed the value from the previous validator.
43-
// 3. finally, the value returned from the last validator is returned for further use.
4442
for _, validator := range v {
4543
var err error
4644
if value, err = validator.Validate(value); err != nil {
File renamed without changes.

schema/anyof.go

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
package schema
22

3-
import "errors"
4-
53
// AnyOf validates if any of the sub field validators validates. If any of the
64
// sub field validators implements the FieldSerializer interface, the *first*
75
// implementation which does not error will be used.
@@ -20,8 +18,10 @@ func (v AnyOf) Compile(rc ReferenceChecker) error {
2018
return nil
2119
}
2220

23-
// ValidateQuery implements schema.FieldQueryValidator interface
21+
// ValidateQuery implements schema.FieldQueryValidator interface.
2422
func (v AnyOf) ValidateQuery(value interface{}) (interface{}, error) {
23+
var errs ErrorSlice
24+
2525
for _, validator := range v {
2626
var err error
2727
var val interface{}
@@ -33,20 +33,31 @@ func (v AnyOf) ValidateQuery(value interface{}) (interface{}, error) {
3333
if err == nil {
3434
return val, nil
3535
}
36+
errs = errs.Append(err)
37+
}
38+
39+
if len(errs) > 0 {
40+
return nil, errs
3641
}
37-
// TODO: combine errors.
38-
return nil, errors.New("invalid")
42+
return nil, nil
3943
}
4044

4145
// Validate ensures that at least one sub-validator validates.
4246
func (v AnyOf) Validate(value interface{}) (interface{}, error) {
47+
var errs ErrorSlice
48+
4349
for _, validator := range v {
44-
if value, err := validator.Validate(value); err == nil {
50+
value, err := validator.Validate(value)
51+
if err == nil {
4552
return value, nil
4653
}
54+
errs = errs.Append(err)
55+
}
56+
57+
if len(errs) > 0 {
58+
return nil, errs
4759
}
48-
// TODO: combine errors.
49-
return nil, errors.New("invalid")
60+
return nil, nil
5061
}
5162

5263
// Serialize attempts to serialize the value using the first available

schema/anyof_test.go

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ import (
1111
// hexByteArray implements the FieldSerializer interface.
1212
type hexByteArray struct{}
1313

14-
// Validate is a dummy implemetation of the FieldValidator interface implemented
15-
// to allow inclusion in AnyOf.
14+
// Validate is a dummy implementation of the FieldValidator interface
15+
// implemented to allow inclusion in AnyOf.
1616
func (h hexByteArray) Validate(value interface{}) (interface{}, error) {
1717
return nil, nil
1818
}
@@ -75,7 +75,13 @@ func TestAnyOfValidate(t *testing.T) {
7575
Name: `{Bool,Bool}.Validate("")`,
7676
Validator: schema.AnyOf{&schema.Bool{}, &schema.Bool{}},
7777
Input: "",
78-
Error: "invalid",
78+
Error: "not a Boolean, not a Boolean",
79+
},
80+
{
81+
Name: "{Bool,String}.Validate(42)",
82+
Validator: schema.AnyOf{&schema.Bool{}, &schema.String{}},
83+
Input: 42,
84+
Error: "not a Boolean, not a string",
7985
},
8086
{
8187
Name: "{Bool,String}.Validate(true)",
@@ -128,7 +134,7 @@ func TestAnyOfQueryValidate(t *testing.T) {
128134
Name: `{Bool,Bool}.Validate("")`,
129135
Validator: schema.AnyOf{&schema.Bool{}, &schema.Bool{}},
130136
Input: "",
131-
Error: "invalid",
137+
Error: "not a Boolean, not a Boolean",
132138
},
133139
{
134140
Name: "{Bool,String}.Validate(true)",

schema/error.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package schema
2+
3+
import (
4+
"fmt"
5+
"sort"
6+
"strings"
7+
)
8+
9+
// ErrorMap contains a map of errors by field name.
10+
type ErrorMap map[string][]interface{}
11+
12+
// Error implements the built-in error interface.
13+
func (err ErrorMap) Error() string {
14+
errs := make([]string, 0, len(err))
15+
for key := range err {
16+
errs = append(errs, key)
17+
}
18+
sort.Strings(errs)
19+
for i, key := range errs {
20+
errs[i] = fmt.Sprintf("%s is %s", key, err[key])
21+
}
22+
return strings.Join(errs, ", ")
23+
}
24+
25+
// Merge copies all errors from other into err.
26+
func (err ErrorMap) Merge(other ErrorMap) {
27+
for k, v := range other {
28+
err[k] = append(err[k], v...)
29+
}
30+
}
31+
32+
// ErrorSlice contains a concatenation of several errors.
33+
type ErrorSlice []error
34+
35+
// Append adds an error to err and returns a new slice if others is not nil. If
36+
// other is another ErrorSlice it is extended so that all elements are appended.
37+
func (err ErrorSlice) Append(other error) ErrorSlice {
38+
switch et := other.(type) {
39+
case nil:
40+
// don't append nil errors.
41+
case ErrorSlice:
42+
// Merge error slices.
43+
err = append(err, et...)
44+
default:
45+
err = append(err, et)
46+
}
47+
return err
48+
}
49+
50+
func (err ErrorSlice) Error() string {
51+
sl := make([]string, 0, len(err))
52+
for _, err := range err {
53+
sl = append(sl, err.Error())
54+
}
55+
return strings.Join(sl, ", ")
56+
}

schema/object.go

Lines changed: 3 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,6 @@ package schema
22

33
import (
44
"errors"
5-
"fmt"
6-
"sort"
7-
"strings"
85
)
96

107
// Object validates objects which are defined by Schemas.
@@ -31,9 +28,9 @@ func (v Object) Validate(value interface{}) (interface{}, error) {
3128
}
3229
dest, errs := v.Schema.Validate(nil, obj)
3330
if len(errs) > 0 {
34-
var errMap ErrorMap
35-
errMap = errs
36-
return nil, errMap
31+
// Currently, tests expect FieldValidators to always return a nil value
32+
// on validation errors.
33+
return nil, ErrorMap(errs)
3734
}
3835
return dest, nil
3936
}
@@ -42,18 +39,3 @@ func (v Object) Validate(value interface{}) (interface{}, error) {
4239
func (v Object) GetField(name string) *Field {
4340
return v.Schema.GetField(name)
4441
}
45-
46-
// ErrorMap contains a map of errors by field name.
47-
type ErrorMap map[string][]interface{}
48-
49-
func (e ErrorMap) Error() string {
50-
errs := make([]string, 0, len(e))
51-
for key := range e {
52-
errs = append(errs, key)
53-
}
54-
sort.Strings(errs)
55-
for i, key := range errs {
56-
errs[i] = fmt.Sprintf("%s is %s", key, e[key])
57-
}
58-
return strings.Join(errs, ", ")
59-
}

schema/schema.go

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -344,12 +344,6 @@ func addFieldError(errs map[string][]interface{}, field string, err interface{})
344344
func mergeFieldErrors(errs map[string][]interface{}, mergeErrs map[string][]interface{}) {
345345
// TODO recursive merge
346346
for field, values := range mergeErrs {
347-
if dest, found := errs[field]; found {
348-
for _, value := range values {
349-
dest = append(dest, value)
350-
}
351-
} else {
352-
errs[field] = values
353-
}
347+
errs[field] = append(errs[field], values...)
354348
}
355349
}

0 commit comments

Comments
 (0)