mirror of https://github.com/knative/pkg.git
Simplify FieldError merging. (#99)
* checkpoint. * Tried a new way. * Some more slimplifying based on feedback. * use bang vs == false. * not sure why I did not use go style on this forloop, fixed.
This commit is contained in:
parent
d83605ef7b
commit
5a67e38d13
|
@ -30,7 +30,7 @@ const CurrentField = ""
|
|||
// specific fields in a manner suitable for use in a recursive walk, so
|
||||
// that errors contain the appropriate field context.
|
||||
// FieldError methods are non-mutating.
|
||||
// +k8s:deepcopy-gen=false
|
||||
// +k8s:deepcopy-gen=true
|
||||
type FieldError struct {
|
||||
Message string
|
||||
Paths []string
|
||||
|
@ -54,18 +54,21 @@ func (fe *FieldError) ViaField(prefix ...string) *FieldError {
|
|||
if fe == nil {
|
||||
return nil
|
||||
}
|
||||
newErr := &FieldError{}
|
||||
for _, e := range fe.getNormalizedErrors() {
|
||||
// Prepend the Prefix to existing errors.
|
||||
newPaths := make([]string, 0, len(e.Paths))
|
||||
for _, oldPath := range e.Paths {
|
||||
newPaths = append(newPaths, flatten(append(prefix, oldPath)))
|
||||
}
|
||||
sort.Slice(newPaths, func(i, j int) bool { return newPaths[i] < newPaths[j] })
|
||||
e.Paths = newPaths
|
||||
// Copy over message and details, paths will be updated and errors come
|
||||
// along using .Also().
|
||||
newErr := &FieldError{
|
||||
Message: fe.Message,
|
||||
Details: fe.Details,
|
||||
}
|
||||
|
||||
// Append the mutated error to the errors list.
|
||||
newErr = newErr.Also(&e)
|
||||
// Prepend the Prefix to existing errors.
|
||||
newPaths := make([]string, 0, len(fe.Paths))
|
||||
for _, oldPath := range fe.Paths {
|
||||
newPaths = append(newPaths, flatten(append(prefix, oldPath)))
|
||||
}
|
||||
newErr.Paths = newPaths
|
||||
for _, e := range fe.errors {
|
||||
newErr = newErr.Also(e.ViaField(prefix...))
|
||||
}
|
||||
return newErr
|
||||
}
|
||||
|
@ -104,21 +107,32 @@ func (fe *FieldError) ViaFieldKey(field string, key string) *FieldError {
|
|||
|
||||
// Also collects errors, returns a new collection of existing errors and new errors.
|
||||
func (fe *FieldError) Also(errs ...*FieldError) *FieldError {
|
||||
newErr := &FieldError{}
|
||||
var newErr *FieldError
|
||||
// collect the current objects errors, if it has any
|
||||
if fe != nil {
|
||||
newErr.errors = fe.getNormalizedErrors()
|
||||
if !fe.isEmpty() {
|
||||
newErr = fe.DeepCopy()
|
||||
} else {
|
||||
newErr = &FieldError{}
|
||||
}
|
||||
// and then collect the passed in errors
|
||||
for _, e := range errs {
|
||||
newErr.errors = append(newErr.errors, e.getNormalizedErrors()...)
|
||||
if !e.isEmpty() {
|
||||
newErr.errors = append(newErr.errors, *e)
|
||||
}
|
||||
}
|
||||
if len(newErr.errors) == 0 {
|
||||
if newErr.isEmpty() {
|
||||
return nil
|
||||
}
|
||||
return newErr
|
||||
}
|
||||
|
||||
func (fe *FieldError) isEmpty() bool {
|
||||
if fe == nil {
|
||||
return true
|
||||
}
|
||||
return fe.Message == "" && fe.Details == "" && len(fe.errors) == 0 && len(fe.Paths) == 0
|
||||
}
|
||||
|
||||
func (fe *FieldError) getNormalizedErrors() []FieldError {
|
||||
// in case we call getNormalizedErrors on a nil object, return just an empty
|
||||
// list. This can happen when .Error() is called on a nil object.
|
||||
|
@ -139,14 +153,34 @@ func (fe *FieldError) getNormalizedErrors() []FieldError {
|
|||
for _, e := range fe.errors {
|
||||
errors = append(errors, e.getNormalizedErrors()...)
|
||||
}
|
||||
sort.Slice(errors, func(i, j int) bool { return errors[i].Message < errors[j].Message })
|
||||
return errors
|
||||
}
|
||||
|
||||
// Error implements error
|
||||
func (fe *FieldError) Error() string {
|
||||
var errs []string
|
||||
// Get the list of errors as a flat merged list.
|
||||
normedErrors := merge(fe.getNormalizedErrors())
|
||||
for _, e := range normedErrors {
|
||||
if e.Details == "" {
|
||||
errs = append(errs, fmt.Sprintf("%v: %v", e.Message, strings.Join(e.Paths, ", ")))
|
||||
} else {
|
||||
errs = append(errs, fmt.Sprintf("%v: %v\n%v", e.Message, strings.Join(e.Paths, ", "), e.Details))
|
||||
}
|
||||
}
|
||||
return strings.Join(errs, "\n")
|
||||
}
|
||||
|
||||
// Helpers ---
|
||||
|
||||
func asIndex(index int) string {
|
||||
return fmt.Sprintf("[%d]", index)
|
||||
}
|
||||
|
||||
func isIndex(part string) bool {
|
||||
return strings.HasPrefix(part, "[") && strings.HasSuffix(part, "]")
|
||||
}
|
||||
|
||||
func asKey(key string) string {
|
||||
return fmt.Sprintf("[%s]", key)
|
||||
}
|
||||
|
@ -173,23 +207,80 @@ func flatten(path []string) string {
|
|||
return strings.Join(newPath, ".")
|
||||
}
|
||||
|
||||
func isIndex(part string) bool {
|
||||
return strings.HasPrefix(part, "[") && strings.HasSuffix(part, "]")
|
||||
}
|
||||
|
||||
// Error implements error
|
||||
func (fe *FieldError) Error() string {
|
||||
var errs []string
|
||||
for _, e := range fe.getNormalizedErrors() {
|
||||
if e.Details == "" {
|
||||
errs = append(errs, fmt.Sprintf("%v: %v", e.Message, strings.Join(e.Paths, ", ")))
|
||||
} else {
|
||||
errs = append(errs, fmt.Sprintf("%v: %v\n%v", e.Message, strings.Join(e.Paths, ", "), e.Details))
|
||||
// mergePaths takes in two string slices and returns the combination of them
|
||||
// without any duplicate entries.
|
||||
func mergePaths(a, b []string) []string {
|
||||
newPaths := make([]string, 0, len(a)+len(b))
|
||||
newPaths = append(newPaths, a...)
|
||||
for _, bi := range b {
|
||||
if !containsString(newPaths, bi) {
|
||||
newPaths = append(newPaths, bi)
|
||||
}
|
||||
}
|
||||
return strings.Join(errs, "\n")
|
||||
return newPaths
|
||||
}
|
||||
|
||||
// containsString takes in a string slice and looks for the provided string
|
||||
// within the slice.
|
||||
func containsString(slice []string, s string) bool {
|
||||
for _, item := range slice {
|
||||
if item == s {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// merge takes in a flat list of FieldErrors and returns back a merged list of
|
||||
// FiledErrors. FieldErrors have their Paths combined (and de-duped) if their
|
||||
// Message and Details are the same. Merge will not inspect FieldError.errors.
|
||||
// Merge will also sort the .Path slice, and the errors slice before returning.
|
||||
func merge(errs []FieldError) []FieldError {
|
||||
// make a map big enough for all the errors.
|
||||
m := make(map[string]FieldError, len(errs))
|
||||
|
||||
// Convert errs to a map where the key is <message>-<details> and the value
|
||||
// is the error. If an error already exists in the map with the same key,
|
||||
// then the paths will be merged.
|
||||
for _, e := range errs {
|
||||
k := key(&e)
|
||||
if v, ok := m[k]; ok {
|
||||
// Found a match, merge the keys.
|
||||
v.Paths = mergePaths(v.Paths, e.Paths)
|
||||
m[k] = v
|
||||
} else {
|
||||
// Does not exist in the map, save the error.
|
||||
m[k] = e
|
||||
}
|
||||
}
|
||||
|
||||
// Take the map made previously and flatten it back out again.
|
||||
newErrs := make([]FieldError, 0, len(m))
|
||||
for _, v := range m {
|
||||
// While we have access to the merged paths, sort them too.
|
||||
sort.Slice(v.Paths, func(i, j int) bool { return v.Paths[i] < v.Paths[j] })
|
||||
newErrs = append(newErrs, v)
|
||||
}
|
||||
|
||||
// Sort the flattened map.
|
||||
sort.Slice(newErrs, func(i, j int) bool {
|
||||
if newErrs[i].Message == newErrs[j].Message {
|
||||
return newErrs[i].Details < newErrs[j].Details
|
||||
}
|
||||
return newErrs[i].Message < newErrs[j].Message
|
||||
})
|
||||
|
||||
// return back the merged list of sorted errors.
|
||||
return newErrs
|
||||
}
|
||||
|
||||
// key returns the key using the fields .Message and .Details.
|
||||
func key(err *FieldError) string {
|
||||
return fmt.Sprintf("%s-%s", err.Message, err.Details)
|
||||
}
|
||||
|
||||
// Public helpers ---
|
||||
|
||||
// ErrMissingField is a variadic helper method for constructing a FieldError for
|
||||
// a set of missing fields.
|
||||
func ErrMissingField(fieldPaths ...string) *FieldError {
|
||||
|
|
|
@ -17,6 +17,7 @@ limitations under the License.
|
|||
package apis
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
@ -127,6 +128,89 @@ can not use @, do not try`,
|
|||
prefixes: [][]string{{"baz"}},
|
||||
want: `invalid key name "b@r": baz.foo[0].name
|
||||
can not use @, do not try`,
|
||||
}, {
|
||||
name: "very complex to simple",
|
||||
err: func() *FieldError {
|
||||
fe := &FieldError{
|
||||
Message: "First",
|
||||
Paths: []string{"A", "B", "C"},
|
||||
}
|
||||
|
||||
fe = fe.Also(fe).Also(fe).Also(fe).Also(fe)
|
||||
|
||||
fe = fe.Also(&FieldError{
|
||||
Message: "Second",
|
||||
Paths: []string{"Z", "X", "Y"},
|
||||
})
|
||||
|
||||
fe = fe.Also(fe).Also(fe).Also(fe).Also(fe)
|
||||
|
||||
return fe
|
||||
}(),
|
||||
want: `First: A, B, C
|
||||
Second: X, Y, Z`,
|
||||
}, {
|
||||
name: "exponentially grows",
|
||||
err: func() *FieldError {
|
||||
fe := &FieldError{
|
||||
Message: "Top",
|
||||
Paths: []string{"A", "B", "C"},
|
||||
}
|
||||
|
||||
for _, p := range []string{"3", "2", "1"} {
|
||||
for i := 0; i < 3; i++ {
|
||||
fe = fe.Also(fe)
|
||||
}
|
||||
fe = fe.ViaField(p)
|
||||
}
|
||||
|
||||
return fe
|
||||
}(),
|
||||
want: `Top: 1.2.3.A, 1.2.3.B, 1.2.3.C`,
|
||||
}, {
|
||||
name: "path grows but details are different",
|
||||
err: func() *FieldError {
|
||||
fe := &FieldError{
|
||||
Message: "Top",
|
||||
Paths: []string{"A", "B", "C"},
|
||||
}
|
||||
|
||||
for _, p := range []string{"3", "2", "1"} {
|
||||
e := fe.ViaField(p)
|
||||
e.Details = fmt.Sprintf("here at %s", p)
|
||||
for i := 0; i < 3; i++ {
|
||||
fe = fe.Also(e)
|
||||
}
|
||||
}
|
||||
|
||||
return fe
|
||||
}(),
|
||||
want: `Top: A, B, C
|
||||
Top: 1.A, 1.B, 1.C
|
||||
here at 1
|
||||
Top: 1.2.A, 1.2.B, 1.2.C, 2.A, 2.B, 2.C
|
||||
here at 2
|
||||
Top: 1.2.3.A, 1.2.3.B, 1.2.3.C, 1.3.A, 1.3.B, 1.3.C, 2.3.A, 2.3.B, 2.3.C, 3.A, 3.B, 3.C
|
||||
here at 3`,
|
||||
}, {
|
||||
name: "very complex to complex",
|
||||
err: func() *FieldError {
|
||||
fe := &FieldError{
|
||||
Message: "First",
|
||||
Paths: []string{"A", "B", "C"},
|
||||
}
|
||||
|
||||
fe = fe.ViaField("one").Also(fe).ViaField("two").Also(fe).ViaField("three").Also(fe)
|
||||
|
||||
fe = fe.Also(&FieldError{
|
||||
Message: "Second",
|
||||
Paths: []string{"Z", "X", "Y"},
|
||||
})
|
||||
|
||||
return fe
|
||||
}(),
|
||||
want: `First: A, B, C, three.A, three.B, three.C, three.two.A, three.two.B, three.two.C, three.two.one.A, three.two.one.B, three.two.one.C
|
||||
Second: X, Y, Z`,
|
||||
}}
|
||||
|
||||
for _, test := range tests {
|
||||
|
@ -434,6 +518,111 @@ not without this: bar.C`,
|
|||
}
|
||||
}
|
||||
|
||||
func TestMergeFieldErrors(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
err *FieldError
|
||||
also []FieldError
|
||||
prefixes [][]string
|
||||
want string
|
||||
}{{
|
||||
name: "simple",
|
||||
err: &FieldError{
|
||||
Message: "A simple error message",
|
||||
Paths: []string{"bar"},
|
||||
},
|
||||
also: []FieldError{{
|
||||
Message: "A simple error message",
|
||||
Paths: []string{"foo"},
|
||||
}},
|
||||
want: `A simple error message: bar, foo`,
|
||||
}, {
|
||||
name: "conflict",
|
||||
err: &FieldError{
|
||||
Message: "A simple error message",
|
||||
Paths: []string{"bar", "foo"},
|
||||
},
|
||||
also: []FieldError{{
|
||||
Message: "A simple error message",
|
||||
Paths: []string{"foo"},
|
||||
}},
|
||||
want: `A simple error message: bar, foo`,
|
||||
}, {
|
||||
name: "lots of also",
|
||||
err: (&FieldError{
|
||||
Message: "this error",
|
||||
Paths: []string{"bar", "foo"},
|
||||
}).Also(&FieldError{
|
||||
Message: "another",
|
||||
Paths: []string{"right", "left"},
|
||||
}).ViaField("head"),
|
||||
also: []FieldError{{
|
||||
Message: "An alpha error message",
|
||||
Paths: []string{"A"},
|
||||
}, {
|
||||
Message: "An alpha error message",
|
||||
Paths: []string{"B"},
|
||||
}, {
|
||||
Message: "An alpha error message",
|
||||
Paths: []string{"B"},
|
||||
}, {
|
||||
Message: "An alpha error message",
|
||||
Paths: []string{"B"},
|
||||
}, {
|
||||
Message: "An alpha error message",
|
||||
Paths: []string{"B"},
|
||||
}, {
|
||||
Message: "An alpha error message",
|
||||
Paths: []string{"B"},
|
||||
}, {
|
||||
Message: "An alpha error message",
|
||||
Paths: []string{"B"},
|
||||
}, {
|
||||
Message: "An alpha error message",
|
||||
Paths: []string{"C"},
|
||||
}, {
|
||||
Message: "An alpha error message",
|
||||
Paths: []string{"D"},
|
||||
}, {
|
||||
Message: "this error",
|
||||
Paths: []string{"foo"},
|
||||
Details: "devil is in the details",
|
||||
}, {
|
||||
Message: "this error",
|
||||
Paths: []string{"foo"},
|
||||
Details: "more details",
|
||||
}},
|
||||
prefixes: [][]string{{"this"}},
|
||||
want: `An alpha error message: this.A, this.B, this.C, this.D
|
||||
another: this.head.left, this.head.right
|
||||
this error: this.head.bar, this.head.foo
|
||||
this error: this.foo
|
||||
devil is in the details
|
||||
this error: this.foo
|
||||
more details`,
|
||||
}}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
fe := test.err
|
||||
for _, err := range test.also {
|
||||
fe = fe.Also(&err)
|
||||
}
|
||||
// Simulate propagation up a call stack.
|
||||
for _, prefix := range test.prefixes {
|
||||
fe = fe.ViaField(prefix...)
|
||||
}
|
||||
if test.want != "" {
|
||||
got := fe.Error()
|
||||
if got != test.want {
|
||||
t.Errorf("%s: Error() = %v, wanted %v", test.name, got, test.want)
|
||||
}
|
||||
} else if fe != nil {
|
||||
t.Errorf("%s: ViaField() = %v, wanted nil", test.name, fe)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAlsoStaysNil(t *testing.T) {
|
||||
var err *FieldError
|
||||
if err != nil {
|
||||
|
|
|
@ -20,6 +20,34 @@ limitations under the License.
|
|||
|
||||
package apis
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *FieldError) DeepCopyInto(out *FieldError) {
|
||||
*out = *in
|
||||
if in.Paths != nil {
|
||||
in, out := &in.Paths, &out.Paths
|
||||
*out = make([]string, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
if in.errors != nil {
|
||||
in, out := &in.errors, &out.errors
|
||||
*out = make([]FieldError, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FieldError.
|
||||
func (in *FieldError) DeepCopy() *FieldError {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(FieldError)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *VolatileTime) DeepCopyInto(out *VolatileTime) {
|
||||
*out = *in
|
||||
|
|
Loading…
Reference in New Issue