-
Notifications
You must be signed in to change notification settings - Fork 335
/
Copy pathfield_error.go
394 lines (348 loc) · 12 KB
/
field_error.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
/*
Copyright 2017 The Knative Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package apis
import (
"fmt"
"sort"
"strings"
"knative.dev/pkg/kmp"
)
// CurrentField is a constant to supply as a fieldPath for when there is
// a problem with the current field itself.
const CurrentField = ""
// FieldError is used to propagate the context of errors pertaining to
// 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=true
type FieldError struct {
Message string
Paths []string
// Details contains an optional longer payload.
// +optional
Details string
errors []FieldError
}
// FieldError implements error
var _ error = (*FieldError)(nil)
// ViaField is used to propagate a validation error along a field access.
// For example, if a type recursively validates its "spec" via:
// if err := foo.Spec.Validate(); err != nil {
// // Augment any field paths with the context that they were accessed
// // via "spec".
// return err.ViaField("spec")
// }
func (fe *FieldError) ViaField(prefix ...string) *FieldError {
if fe == nil {
return nil
}
// Copy over message and details, paths will be updated and errors come
// along using .Also().
newErr := &FieldError{
Message: fe.Message,
Details: fe.Details,
}
// 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
}
// 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 {
return fe.ViaField(asIndex(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 {
return fe.ViaField(asKey(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)
}
// Also collects errors, returns a new collection of existing errors and new errors.
func (fe *FieldError) Also(errs ...*FieldError) *FieldError {
// Avoid doing any work, if we don't have to.
if l := len(errs); l == 0 || l == 1 && errs[0].isEmpty() {
return fe
}
var newErr *FieldError
// collect the current objects errors, if it has any
if !fe.isEmpty() {
newErr = fe.DeepCopy()
} else {
newErr = &FieldError{}
}
// and then collect the passed in errors
for _, e := range errs {
if !e.isEmpty() {
newErr.errors = append(newErr.errors, *e)
}
}
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
}
// normalized returns a flattened copy of all the errors.
func (fe *FieldError) normalized() []*FieldError {
// In case we call normalized on a nil object, return just an empty
// list. This can happen when .Error() is called on a nil object.
if fe == nil {
return []*FieldError(nil)
}
// Allocate errors with at least as many objects as we'll get on the first pass.
errors := make([]*FieldError, 0, len(fe.errors)+1)
// If this FieldError is a leaf, add it.
if fe.Message != "" {
errors = append(errors, &FieldError{
Message: fe.Message,
Paths: fe.Paths,
Details: fe.Details,
})
}
// And then collect all other errors recursively.
for _, e := range fe.errors {
errors = append(errors, e.normalized()...)
}
return errors
}
// Error implements error
func (fe *FieldError) Error() string {
// Get the list of errors as a flat merged list.
normedErrors := merge(fe.normalized())
errs := make([]string, 0, len(normedErrors))
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)
}
// 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, ".") {
switch {
case p == CurrentField:
continue
case len(newPath) > 0 && isIndex(p):
newPath[len(newPath)-1] += p
default:
newPath = append(newPath, p)
}
}
}
return strings.Join(newPath, ".")
}
// 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 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
// FieldErrors. 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)
} 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 {
return &FieldError{
Message: "missing field(s)",
Paths: fieldPaths,
}
}
// ErrDisallowedFields is a variadic helper method for constructing a FieldError
// for a set of disallowed fields.
func ErrDisallowedFields(fieldPaths ...string) *FieldError {
return &FieldError{
Message: "must not set the field(s)",
Paths: fieldPaths,
}
}
// ErrDisallowedUpdateDeprecatedFields is a variadic helper method for
// constructing a FieldError for updating of deprecated fields.
func ErrDisallowedUpdateDeprecatedFields(fieldPaths ...string) *FieldError {
return &FieldError{
Message: "must not update deprecated field(s)",
Paths: fieldPaths,
}
}
// ErrInvalidArrayValue constructs a FieldError for a repetetive `field`
// at `index` that has received an invalid value.
func ErrInvalidArrayValue(value interface{}, field string, index int) *FieldError {
return ErrInvalidValue(value, CurrentField).ViaFieldIndex(field, index)
}
// ErrInvalidValue constructs a FieldError for a field that has received an
// invalid value.
func ErrInvalidValue(value interface{}, fieldPath string) *FieldError {
return &FieldError{
Message: fmt.Sprintf("invalid value: %v", value),
Paths: []string{fieldPath},
}
}
// ErrGeneric constructs a FieldError to allow for the different error strings for the
// the different cases.
func ErrGeneric(diagnostic string, fieldPaths ...string) *FieldError {
return &FieldError{
Message: diagnostic,
Paths: fieldPaths,
}
}
// ErrMissingOneOf is a variadic helper method for constructing a FieldError for
// not having at least one field in a mutually exclusive field group.
func ErrMissingOneOf(fieldPaths ...string) *FieldError {
return &FieldError{
Message: "expected exactly one, got neither",
Paths: fieldPaths,
}
}
// ErrMultipleOneOf is a variadic helper method for constructing a FieldError
// for having more than one field set in a mutually exclusive field group.
func ErrMultipleOneOf(fieldPaths ...string) *FieldError {
return &FieldError{
Message: "expected exactly one, got both",
Paths: fieldPaths,
}
}
// ErrInvalidKeyName is a variadic helper method for constructing a FieldError
// that specifies a key name that is invalid.
func ErrInvalidKeyName(key, fieldPath string, details ...string) *FieldError {
return &FieldError{
Message: fmt.Sprintf("invalid key name %q", key),
Paths: []string{fieldPath},
Details: strings.Join(details, ", "),
}
}
// ErrOutOfBoundsValue constructs a FieldError for a field that has received an
// out of bound value.
func ErrOutOfBoundsValue(value, lower, upper interface{}, fieldPath string) *FieldError {
return &FieldError{
Message: fmt.Sprintf("expected %v <= %v <= %v", lower, value, upper),
Paths: []string{fieldPath},
}
}
// CheckDisallowedFields compares the request object against a masked request object. Fields
// that are set in the request object that are unset in the mask are reported back as disallowed fields. If
// there is an error comparing the two objects FieldError of "Internal Error" is returned.
func CheckDisallowedFields(request, maskedRequest interface{}) *FieldError {
if disallowed, err := kmp.CompareSetFields(request, maskedRequest); err != nil {
return &FieldError{
Message: "Internal Error",
Paths: []string{CurrentField},
}
} else if len(disallowed) > 0 {
return ErrDisallowedFields(disallowed...)
}
return nil
}