Skip to content

Commit

Permalink
Validation: add merge errors
Browse files Browse the repository at this point in the history
  • Loading branch information
System-Glitch committed Mar 26, 2024
1 parent 19fc0ba commit af00483
Show file tree
Hide file tree
Showing 4 changed files with 518 additions and 21 deletions.
69 changes: 69 additions & 0 deletions validation/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,55 @@ func (e *Errors) Add(path *walk.Path, message string) {
}
}

// Merge the given errors into this bag of errors at the given path.
// This can be used when a validator uses nested validation and wants
// to add the results in the higher-level validation errors.
//
// Missing path segments will be added automatically.
// Elements from the given errors are NOT cloned. Therefore there can
// be side-effects if you modify them after the call of `Merge`.
func (e *Errors) Merge(path *walk.Path, errors *Errors) {
switch path.Type {
case walk.PathTypeElement:
if len(errors.Fields) > 0 && e.Fields == nil {
e.Fields = make(FieldsErrors, len(errors.Fields))
}
for k, v := range errors.Fields {
if fields, ok := e.Fields[k]; ok {
fields.Merge(path, v)
} else {
e.Fields[k] = v
}
}
if len(errors.Elements) > 0 && e.Elements == nil {
e.Elements = make(ArrayErrors, len(errors.Elements))
}
for i, v := range errors.Elements {
if elements, ok := e.Elements[i]; ok {
elements.Merge(path, v)
} else {
e.Elements[i] = v
}
}
e.Errors = append(e.Errors, errors.Errors...)
case walk.PathTypeArray:
if e.Elements == nil {
e.Elements = make(ArrayErrors)
}

index := -1
if path.Index != nil {
index = *path.Index
}
e.Elements.Merge(path.Next, index, errors)
case walk.PathTypeObject:
if e.Fields == nil {
e.Fields = make(FieldsErrors)
}
e.Fields.Merge(path.Next, errors)
}
}

// Add an error message to the element identified by the given path.
// Creates all missing elements in the path.
func (e FieldsErrors) Add(path *walk.Path, message string) {
Expand All @@ -66,6 +115,16 @@ func (e FieldsErrors) Add(path *walk.Path, message string) {
errs.Add(path, message)
}

// Merge the given errors into this bag of errors at the given path.
func (e FieldsErrors) Merge(path *walk.Path, errors *Errors) {
errs, ok := e[*path.Name]
if !ok {
errs = &Errors{}
e[*path.Name] = errs
}
errs.Merge(path, errors)
}

// Add an error message to the element identified by the given path in the array,
// at the given index. "-1" index is accepted to identify non-existing elements.
// Creates all missing elements in the path.
Expand All @@ -77,3 +136,13 @@ func (e ArrayErrors) Add(path *walk.Path, index int, message string) {
}
errs.Add(path, message)
}

// Merge the given errors into this bag of errors at the given path.
func (e ArrayErrors) Merge(path *walk.Path, index int, errors *Errors) {
errs, ok := e[index]
if !ok {
errs = &Errors{}
e[index] = errs
}
errs.Merge(path, errors)
}
290 changes: 290 additions & 0 deletions validation/errors_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,4 +148,294 @@ func TestErrors(t *testing.T) {
assert.Equal(t, expected, errs)
})

t.Run("Merge", func(t *testing.T) {

mergeErrs := func() *Errors {
return &Errors{
Fields: FieldsErrors{
"mergeField": &Errors{
Fields: FieldsErrors{
"nested": &Errors{Errors: []string{"nested err"}},
},
Errors: []string{"merge mergeField err"},
},
"field": &Errors{
Errors: []string{"merge field err"},
},
},
Elements: ArrayErrors{
1: &Errors{
Errors: []string{"element err"},
},
3: &Errors{
Fields: FieldsErrors{
"elementField": &Errors{Errors: []string{"element merge err"}},
},
},
},
Errors: []string{"merge err 1", "merge err 2"},
}
}

cases := []struct {
base *Errors
mergeErrs *Errors
path *walk.Path
want *Errors
desc string
}{
{
desc: "root",
base: &Errors{
Fields: FieldsErrors{
"field": &Errors{
Errors: []string{"field err"},
},
},
Elements: ArrayErrors{
3: &Errors{},
},
Errors: []string{"error 1"},
},
mergeErrs: mergeErrs(),
path: walk.MustParse(""),
want: &Errors{
Fields: FieldsErrors{
"mergeField": &Errors{
Fields: FieldsErrors{
"nested": &Errors{Errors: []string{"nested err"}},
},
Errors: []string{"merge mergeField err"},
},
"field": &Errors{
Errors: []string{"field err", "merge field err"},
},
},
Elements: ArrayErrors{
1: &Errors{
Errors: []string{"element err"},
},
3: &Errors{
Fields: FieldsErrors{
"elementField": &Errors{Errors: []string{"element merge err"}},
},
},
},
Errors: []string{"error 1", "merge err 1", "merge err 2"},
},
},
{
desc: "in_array_element",
base: &Errors{
Fields: FieldsErrors{
"field": &Errors{
Errors: []string{"field err"},
},
},
Elements: ArrayErrors{
3: &Errors{},
},
Errors: []string{"error 1"},
},
mergeErrs: mergeErrs(),
path: &walk.Path{
Type: walk.PathTypeArray,
Index: lo.ToPtr(3),
Next: &walk.Path{Type: walk.PathTypeElement},
},
want: &Errors{
Fields: FieldsErrors{
"field": &Errors{
Errors: []string{"field err"},
},
},
Elements: ArrayErrors{
3: mergeErrs(),
},
Errors: []string{"error 1"},
},
},
{
desc: "in_new_array_element",
base: &Errors{
Fields: FieldsErrors{
"field": &Errors{
Errors: []string{"field err"},
},
},
Elements: ArrayErrors{
3: &Errors{},
},
Errors: []string{"error 1"},
},
mergeErrs: mergeErrs(),
path: &walk.Path{
Type: walk.PathTypeArray,
Index: lo.ToPtr(2),
Next: &walk.Path{Type: walk.PathTypeObject, Next: &walk.Path{Type: walk.PathTypeElement, Name: lo.ToPtr("property")}},
},
want: &Errors{
Fields: FieldsErrors{
"field": &Errors{
Errors: []string{"field err"},
},
},
Elements: ArrayErrors{
3: &Errors{},
2: &Errors{
Fields: FieldsErrors{
"property": mergeErrs(),
},
},
},
Errors: []string{"error 1"},
},
},
{
desc: "in_field",
base: &Errors{
Fields: FieldsErrors{
"field": &Errors{
Errors: []string{"field err"},
},
},
Elements: ArrayErrors{
3: &Errors{},
},
Errors: []string{"error 1"},
},
mergeErrs: mergeErrs(),
path: &walk.Path{
Type: walk.PathTypeObject,
Next: &walk.Path{
Type: walk.PathTypeElement,
Name: lo.ToPtr("field"),
},
},
want: &Errors{
Fields: FieldsErrors{
"field": &Errors{
Fields: FieldsErrors{
"mergeField": &Errors{
Fields: FieldsErrors{
"nested": &Errors{Errors: []string{"nested err"}},
},
Errors: []string{"merge mergeField err"},
},
"field": &Errors{
Errors: []string{"merge field err"},
},
},
Elements: ArrayErrors{
1: &Errors{
Errors: []string{"element err"},
},
3: &Errors{
Fields: FieldsErrors{
"elementField": &Errors{Errors: []string{"element merge err"}},
},
},
},
Errors: []string{"field err", "merge err 1", "merge err 2"},
},
},
Elements: ArrayErrors{
3: &Errors{},
},
Errors: []string{"error 1"},
},
},
{
desc: "in_new_field",
base: &Errors{
Fields: FieldsErrors{
"field": &Errors{
Errors: []string{"field err"},
},
},
Elements: ArrayErrors{
3: &Errors{},
},
Errors: []string{"error 1"},
},
mergeErrs: mergeErrs(),
path: &walk.Path{
Type: walk.PathTypeObject,
Next: &walk.Path{
Type: walk.PathTypeObject,
Name: lo.ToPtr("mergeObject"),
Next: &walk.Path{
Type: walk.PathTypeElement,
Name: lo.ToPtr("mergeProp"),
},
},
},
want: &Errors{
Fields: FieldsErrors{
"field": &Errors{
Errors: []string{"field err"},
},
"mergeObject": &Errors{
Fields: FieldsErrors{
"mergeProp": mergeErrs(),
},
},
},
Elements: ArrayErrors{
3: &Errors{},
},
Errors: []string{"error 1"},
},
},
{
desc: "in_new_field_elements",
base: &Errors{
Fields: FieldsErrors{
"field": &Errors{
Errors: []string{"field err"},
},
},
Elements: ArrayErrors{
3: &Errors{},
},
Errors: []string{"error 1"},
},
mergeErrs: mergeErrs(),
path: &walk.Path{
Type: walk.PathTypeObject,
Next: &walk.Path{
Type: walk.PathTypeArray,
Name: lo.ToPtr("mergeArray"),
Index: lo.ToPtr(4),
Next: &walk.Path{Type: walk.PathTypeElement},
},
},
want: &Errors{
Fields: FieldsErrors{
"field": &Errors{
Errors: []string{"field err"},
},
"mergeArray": &Errors{
Elements: ArrayErrors{
4: mergeErrs(),
},
},
},
Elements: ArrayErrors{
3: &Errors{},
},
Errors: []string{"error 1"},
},
},
}

for _, c := range cases {
c := c
t.Run(c.desc, func(t *testing.T) {
errs := c.base
errs.Merge(c.path, c.mergeErrs)
assert.Equal(t, c.want, errs)
})
}
})
}
Loading

0 comments on commit af00483

Please sign in to comment.