Adding ViaIndex, ViaKey, ViaFieldIndex, ViaFieldKey to FieldErrors (#55)

* Adding .ViaIndex(1). to make spec.param[1] easier to do.

* Adding ViaKey

* fix order of bag example.

* Add code coverage to missed line.

* Adding ViaFieldIndex helper

* cleaning up.

* compress more.

* more cleanup

* test name
This commit is contained in:
Scott Nichols 2018-09-14 21:07:17 -07:00 committed by Knative Prow Robot
parent 2a7e950c4e
commit f0ec8c5ac7
2 changed files with 335 additions and 14 deletions

View File

@ -52,17 +52,76 @@ func (fe *FieldError) ViaField(prefix ...string) *FieldError {
}
var newPaths []string
for _, oldPath := range fe.Paths {
if oldPath == CurrentField {
newPaths = append(newPaths, strings.Join(prefix, "."))
} else {
newPaths = append(newPaths,
strings.Join(append(prefix, oldPath), "."))
}
newPaths = append(newPaths, flatten(append(prefix, oldPath)))
}
fe.Paths = newPaths
return fe
}
// ViaIndex is used to attach an index to the next ViaField provided.
// For example, if a type recursively validates a parameter that has a collection:
// for i, c := range spec.Collection {
// if err := doValidation(c); err != nil {
// return err.ViaIndex(i).ViaField("collection")
// }
// }
func (fe *FieldError) ViaIndex(index int) *FieldError {
if fe == nil {
return nil
}
return fe.ViaField(fmt.Sprintf("[%d]", index))
}
// ViaFieldIndex is the short way to chain: err.ViaIndex(bar).ViaField(foo)
func (fe *FieldError) ViaFieldIndex(field string, index int) *FieldError {
return fe.ViaIndex(index).ViaField(field)
}
// ViaKey is used to attach a key to the next ViaField provided.
// For example, if a type recursively validates a parameter that has a collection:
// for k, v := range spec.Bag. {
// if err := doValidation(v); err != nil {
// return err.ViaKey(k).ViaField("bag")
// }
// }
func (fe *FieldError) ViaKey(key string) *FieldError {
if fe == nil {
return nil
}
return fe.ViaField(fmt.Sprintf("[%s]", key))
}
// ViaFieldKey is the short way to chain: err.ViaKey(bar).ViaField(foo)
func (fe *FieldError) ViaFieldKey(field string, key string) *FieldError {
return fe.ViaKey(key).ViaField(field)
}
// flatten takes in a array of path components and looks for chances to flatten
// objects that have index prefixes, examples:
// err([0]).ViaField(bar).ViaField(foo) -> foo.bar.[0] converts to foo.bar[0]
// err(bar).ViaIndex(0).ViaField(foo) -> foo.[0].bar converts to foo[0].bar
// err(bar).ViaField(foo).ViaIndex(0) -> [0].foo.bar converts to [0].foo.bar
// err(bar).ViaIndex(0).ViaIndex[1].ViaField(foo) -> foo.[1].[0].bar converts to foo[1][0].bar
func flatten(path []string) string {
var newPath []string
for _, part := range path {
for _, p := range strings.Split(part, ".") {
if p == CurrentField {
continue
} else if len(newPath) > 0 && isIndex(p) {
newPath[len(newPath)-1] = fmt.Sprintf("%s%s", newPath[len(newPath)-1], p)
} else {
newPath = append(newPath, p)
}
}
}
return strings.Join(newPath, ".")
}
func isIndex(part string) bool {
return strings.HasPrefix(part, "[") && strings.HasSuffix(part, "]")
}
// Error implements error
func (fe *FieldError) Error() string {
if fe.Details == "" {

View File

@ -17,6 +17,8 @@ limitations under the License.
package apis
import (
"strconv"
"strings"
"testing"
)
@ -111,19 +113,19 @@ Body.`,
err: ErrMultipleOneOf("foo", "bar"),
prefixes: [][]string{{"baz"}},
want: `expected exactly one, got both: baz.foo, baz.bar`,
},{
name: "invalid key name",
err: ErrInvalidKeyName("b@r", "foo[0].name",
}, {
name: "invalid key name",
err: ErrInvalidKeyName("b@r", "foo[0].name",
"can not use @", "do not try"),
prefixes: [][]string{{"baz"}},
want: `invalid key name "b@r": baz.foo[0].name
want: `invalid key name "b@r": baz.foo[0].name
can not use @, do not try`,
},{
name: "invalid key name with details array",
err: ErrInvalidKeyName("b@r", "foo[0].name",
}, {
name: "invalid key name with details array",
err: ErrInvalidKeyName("b@r", "foo[0].name",
[]string{"can not use @", "do not try"}...),
prefixes: [][]string{{"baz"}},
want: `invalid key name "b@r": baz.foo[0].name
want: `invalid key name "b@r": baz.foo[0].name
can not use @, do not try`,
}}
@ -145,3 +147,263 @@ can not use @, do not try`,
})
}
}
func TestViaIndexOrKeyFieldError(t *testing.T) {
tests := []struct {
name string
err *FieldError
prefixes [][]string
want string
}{{
name: "simple single no propagation",
err: &FieldError{
Message: "hear me roar",
Paths: []string{"bar"},
},
prefixes: [][]string{{"INDEX:3", "INDEX:2", "INDEX:1", "foo"}},
want: "hear me roar: foo[1][2][3].bar",
}, {
name: "simple key",
err: &FieldError{
Message: "hear me roar",
Paths: []string{"bar"},
},
prefixes: [][]string{{"KEY:C", "KEY:B", "KEY:A", "foo"}},
want: "hear me roar: foo[A][B][C].bar",
}, {
name: "missing field propagation",
err: ErrMissingField("foo", "bar"),
prefixes: [][]string{{"[2]", "baz"}},
want: "missing field(s): baz[2].foo, baz[2].bar",
}, {
name: "invalid key name",
err: ErrInvalidKeyName("b@r", "name",
"can not use @", "do not try"),
prefixes: [][]string{{"baz", "INDEX:0", "foo"}},
want: `invalid key name "b@r": foo[0].baz.name
can not use @, do not try`,
}, {
name: "invalid key name with keys",
err: ErrInvalidKeyName("b@r", "name",
"can not use @", "do not try"),
prefixes: [][]string{{"baz", "INDEX:0", "foo"}, {"bar", "KEY:A", "boo"}},
want: `invalid key name "b@r": boo[A].bar.foo[0].baz.name
can not use @, do not try`,
}, {
name: "multi prefixes provided",
err: &FieldError{
Message: "invalid field(s)",
Paths: []string{"foo"},
},
prefixes: [][]string{{"INDEX:2"}, {"bee"}, {"INDEX:0"}, {"baa", "baz", "ugh"}},
want: "invalid field(s): ugh.baz.baa[0].bee[2].foo",
}, {
name: "use helper viaFieldIndex",
err: &FieldError{
Message: "invalid field(s)",
Paths: []string{"foo"},
},
prefixes: [][]string{{"FIELDINDEX:bee,2"}, {"FIELDINDEX:baa,0"}, {"baz", "ugh"}},
want: "invalid field(s): ugh.baz.baa[0].bee[2].foo",
}, {
name: "use helper viaFieldKey",
err: &FieldError{
Message: "invalid field(s)",
Paths: []string{"foo"},
},
prefixes: [][]string{{"FIELDKEY:bee,AAA"}, {"FIELDKEY:baa,BBB"}, {"baz", "ugh"}},
want: "invalid field(s): ugh.baz.baa[BBB].bee[AAA].foo",
}, {
name: "bypass helpers",
err: &FieldError{
Message: "invalid field(s)",
Paths: []string{"foo"},
},
prefixes: [][]string{{"[2]"}, {"[1]"}, {"bar"}},
want: "invalid field(s): bar[1][2].foo",
}, {
name: "multi paths provided",
err: &FieldError{
Message: "invalid field(s)",
Paths: []string{"foo", "bar"},
},
prefixes: [][]string{{"INDEX:0"}, {"index"}, {"KEY:A"}, {"map"}},
want: "invalid field(s): map[A].index[0].foo, map[A].index[0].bar",
}, {
name: "manual index",
err: func() *FieldError {
// Example, return an error in a loop:
// for i, item := spec.myList {
// err := item.validate().ViaIndex(i).ViaField("myList")
// if err != nil {
// return err
// }
// }
// --> I expect path to be myList[i].foo
err := &FieldError{
Message: "invalid field(s)",
Paths: []string{"foo"},
}
err = err.ViaIndex(0).ViaField("bar")
err = err.ViaIndex(2).ViaIndex(1).ViaField("baz")
err = err.ViaIndex(3).ViaIndex(4).ViaField("boof")
return err
}(),
want: "invalid field(s): boof[4][3].baz[1][2].bar[0].foo",
}, {
name: "manual multiple index",
err: func() *FieldError {
err := &FieldError{
Message: "invalid field(s)",
Paths: []string{"foo"},
}
err = err.ViaField("bear", "[1]", "[2]", "[3]", "baz", "]xxx[").ViaField("bar")
return err
}(),
want: "invalid field(s): bar.bear[1][2][3].baz.]xxx[.foo",
}, {
name: "manual keys",
err: func() *FieldError {
err := &FieldError{
Message: "invalid field(s)",
Paths: []string{"foo"},
}
err = err.ViaKey("A").ViaField("bar")
err = err.ViaKey("CCC").ViaKey("BB").ViaField("baz")
err = err.ViaKey("E").ViaKey("F").ViaField("jar")
return err
}(),
want: "invalid field(s): jar[F][E].baz[BB][CCC].bar[A].foo",
}, {
name: "manual index and keys",
err: func() *FieldError {
err := &FieldError{
Message: "invalid field(s)",
Paths: []string{"foo", "faa"},
}
err = err.ViaKey("A").ViaField("bar")
err = err.ViaIndex(1).ViaField("baz")
err = err.ViaKey("E").ViaIndex(0).ViaField("jar")
return err
}(),
want: "invalid field(s): jar[0][E].baz[1].bar[A].foo, jar[0][E].baz[1].bar[A].faa",
}, {
name: "nil propagation",
err: nil,
prefixes: [][]string{{"baz", "ugh", "INDEX:0", "KEY:A"}},
}}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
fe := test.err
// Simulate propagation up a call stack.
for _, prefix := range test.prefixes {
for _, p := range prefix {
if strings.HasPrefix(p, "INDEX") {
index := strings.Split(p, ":")
fe = fe.ViaIndex(makeIndex(index[1]))
} else if strings.HasPrefix(p, "FIELDINDEX") {
index := strings.Split(p, ":")
fe = fe.ViaFieldIndex(makeFieldIndex(index[1]))
} else if strings.HasPrefix(p, "KEY") {
key := strings.Split(p, ":")
fe = fe.ViaKey(makeKey(key[1]))
} else if strings.HasPrefix(p, "FIELDKEY") {
index := strings.Split(p, ":")
fe = fe.ViaFieldKey(makeFieldKey(index[1]))
} else {
fe = fe.ViaField(p)
}
}
}
if test.want != "" {
got := fe.Error()
if got != test.want {
t.Errorf("Error() = %v, wanted %v", got, test.want)
}
} else if fe != nil {
t.Errorf("ViaField() = %v, wanted nil", fe)
}
})
}
}
func TestFlatten(t *testing.T) {
tests := []struct {
name string
indices []string
want string
}{{
name: "simple",
indices: strings.Split("foo.[1]", "."),
want: "foo[1]",
}, {
name: "no brackets",
indices: strings.Split("foo.bar", "."),
want: "foo.bar",
}, {
name: "err([0]).ViaField(bar).ViaField(foo)",
indices: strings.Split("foo.bar.[0]", "."),
want: "foo.bar[0]",
}, {
name: "err(bar).ViaIndex(0).ViaField(foo)",
indices: strings.Split("foo.[0].bar", "."),
want: "foo[0].bar",
}, {
name: "err(bar).ViaField(foo).ViaIndex(0)",
indices: strings.Split("[0].foo.bar", "."),
want: "[0].foo.bar",
}, {
name: "err(bar).ViaIndex(0).ViaIndex[1].ViaField(foo)",
indices: strings.Split("foo.[1].[0].bar", "."),
want: "foo[1][0].bar",
}, {
name: "err(bar).ViaIndex(0).ViaIndex[1].ViaField(foo)",
indices: []string{"foo", "bar.[0].baz"},
want: "foo.bar[0].baz",
}}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
got := flatten(test.indices)
if got != test.want {
t.Errorf("got: %q, want %q", got, test.want)
}
})
}
}
func makeIndex(index string) int {
all := strings.Split(index, ",")
if i, err := strconv.Atoi(all[0]); err == nil {
return i
}
return -1
}
func makeFieldIndex(fi string) (string, int) {
all := strings.Split(fi, ",")
if i, err := strconv.Atoi(all[1]); err == nil {
return all[0], i
}
return "error", -1
}
func makeKey(key string) string {
all := strings.Split(key, ",")
return all[0]
}
func makeFieldKey(fk string) (string, string) {
all := strings.Split(fk, ",")
return all[0], all[1]
}