diff --git a/apis/field_error.go b/apis/field_error.go index 50a9a2512..45b3e6bf5 100644 --- a/apis/field_error.go +++ b/apis/field_error.go @@ -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 == "" { diff --git a/apis/field_error_test.go b/apis/field_error_test.go index 677cc119b..67c49314e 100644 --- a/apis/field_error_test.go +++ b/apis/field_error_test.go @@ -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] +}