Skip to content

Commit 02f7dc5

Browse files
yongruilinthockin
andcommitted
feat: Add Origin field to Error and related methods
This change introducing a new field in Error. It would be used in testing to compare the expected errors without matching the detail strings. Co-authored-by: Tim Hockin <[email protected]>
1 parent de7708f commit 02f7dc5

File tree

2 files changed

+76
-10
lines changed

2 files changed

+76
-10
lines changed

staging/src/k8s.io/apimachinery/pkg/util/validation/field/errors.go

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,20 @@ type Error struct {
3333
Field string
3434
BadValue interface{}
3535
Detail string
36+
37+
// Origin uniquely identifies where this error was generated from. It is used in testing to
38+
// compare expected errors against actual errors without relying on exact detail string matching.
39+
// This allows tests to verify the correct validation logic triggered the error
40+
// regardless of how the error message might be formatted or localized.
41+
//
42+
// The value should be either:
43+
// - A simple camelCase identifier (e.g., "maximum", "maxItems")
44+
// - A structured format using "format=<dash-style-identifier>" for validation errors related to specific formats
45+
// (e.g., "format=dns-label", "format=qualified-name")
46+
//
47+
// Origin should be set in the most deeply nested validation function that
48+
// can still identify the unique source of the error.
49+
Origin string
3650
}
3751

3852
var _ error = &Error{}
@@ -96,6 +110,12 @@ func (v *Error) ErrorBody() string {
96110
return s
97111
}
98112

113+
// WithOrigin adds origin information to the FieldError
114+
func (v *Error) WithOrigin(o string) *Error {
115+
v.Origin = o
116+
return v
117+
}
118+
99119
// ErrorType is a machine readable value providing more detail about why
100120
// a field is invalid. These values are expected to match 1-1 with
101121
// CauseType in api/types.go.
@@ -169,32 +189,32 @@ func (t ErrorType) String() string {
169189

170190
// TypeInvalid returns a *Error indicating "type is invalid"
171191
func TypeInvalid(field *Path, value interface{}, detail string) *Error {
172-
return &Error{ErrorTypeTypeInvalid, field.String(), value, detail}
192+
return &Error{ErrorTypeTypeInvalid, field.String(), value, detail, ""}
173193
}
174194

175195
// NotFound returns a *Error indicating "value not found". This is
176196
// used to report failure to find a requested value (e.g. looking up an ID).
177197
func NotFound(field *Path, value interface{}) *Error {
178-
return &Error{ErrorTypeNotFound, field.String(), value, ""}
198+
return &Error{ErrorTypeNotFound, field.String(), value, "", ""}
179199
}
180200

181201
// Required returns a *Error indicating "value required". This is used
182202
// to report required values that are not provided (e.g. empty strings, null
183203
// values, or empty arrays).
184204
func Required(field *Path, detail string) *Error {
185-
return &Error{ErrorTypeRequired, field.String(), "", detail}
205+
return &Error{ErrorTypeRequired, field.String(), "", detail, ""}
186206
}
187207

188208
// Duplicate returns a *Error indicating "duplicate value". This is
189209
// used to report collisions of values that must be unique (e.g. names or IDs).
190210
func Duplicate(field *Path, value interface{}) *Error {
191-
return &Error{ErrorTypeDuplicate, field.String(), value, ""}
211+
return &Error{ErrorTypeDuplicate, field.String(), value, "", ""}
192212
}
193213

194214
// Invalid returns a *Error indicating "invalid value". This is used
195215
// to report malformed values (e.g. failed regex match, too long, out of bounds).
196216
func Invalid(field *Path, value interface{}, detail string) *Error {
197-
return &Error{ErrorTypeInvalid, field.String(), value, detail}
217+
return &Error{ErrorTypeInvalid, field.String(), value, detail, ""}
198218
}
199219

200220
// NotSupported returns a *Error indicating "unsupported value".
@@ -209,15 +229,15 @@ func NotSupported[T ~string](field *Path, value interface{}, validValues []T) *E
209229
}
210230
detail = "supported values: " + strings.Join(quotedValues, ", ")
211231
}
212-
return &Error{ErrorTypeNotSupported, field.String(), value, detail}
232+
return &Error{ErrorTypeNotSupported, field.String(), value, detail, ""}
213233
}
214234

215235
// Forbidden returns a *Error indicating "forbidden". This is used to
216236
// report valid (as per formatting rules) values which would be accepted under
217237
// some conditions, but which are not permitted by current conditions (e.g.
218238
// security policy).
219239
func Forbidden(field *Path, detail string) *Error {
220-
return &Error{ErrorTypeForbidden, field.String(), "", detail}
240+
return &Error{ErrorTypeForbidden, field.String(), "", detail, ""}
221241
}
222242

223243
// TooLong returns a *Error indicating "too long". This is used to report that
@@ -231,7 +251,7 @@ func TooLong(field *Path, value interface{}, maxLength int) *Error {
231251
} else {
232252
msg = "value is too long"
233253
}
234-
return &Error{ErrorTypeTooLong, field.String(), "<value omitted>", msg}
254+
return &Error{ErrorTypeTooLong, field.String(), "<value omitted>", msg, ""}
235255
}
236256

237257
// TooLongMaxLength returns a *Error indicating "too long".
@@ -259,14 +279,14 @@ func TooMany(field *Path, actualQuantity, maxQuantity int) *Error {
259279
actual = omitValue
260280
}
261281

262-
return &Error{ErrorTypeTooMany, field.String(), actual, msg}
282+
return &Error{ErrorTypeTooMany, field.String(), actual, msg, ""}
263283
}
264284

265285
// InternalError returns a *Error indicating "internal error". This is used
266286
// to signal that an error was found that was not directly related to user
267287
// input. The err argument must be non-nil.
268288
func InternalError(field *Path, err error) *Error {
269-
return &Error{ErrorTypeInternal, field.String(), nil, err.Error()}
289+
return &Error{ErrorTypeInternal, field.String(), nil, err.Error(), ""}
270290
}
271291

272292
// ErrorList holds a set of Errors. It is plausible that we might one day have
@@ -285,6 +305,14 @@ func NewErrorTypeMatcher(t ErrorType) utilerrors.Matcher {
285305
}
286306
}
287307

308+
// WithOrigin sets the origin for all errors in the list and returns the updated list.
309+
func (list ErrorList) WithOrigin(origin string) ErrorList {
310+
for _, err := range list {
311+
err.Origin = origin
312+
}
313+
return list
314+
}
315+
288316
// ToAggregate converts the ErrorList into an errors.Aggregate.
289317
func (list ErrorList) ToAggregate() utilerrors.Aggregate {
290318
if len(list) == 0 {

staging/src/k8s.io/apimachinery/pkg/util/validation/field/errors_test.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,3 +173,41 @@ func TestNotSupported(t *testing.T) {
173173
t.Errorf("Expected: %s\n, but got: %s\n", expected, notSupported.ErrorBody())
174174
}
175175
}
176+
177+
func TestErrorOrigin(t *testing.T) {
178+
err := Invalid(NewPath("field"), "value", "detail")
179+
180+
// Test WithOrigin
181+
newErr := err.WithOrigin("origin1")
182+
if newErr.Origin != "origin1" {
183+
t.Errorf("Expected Origin to be 'origin1', got '%s'", newErr.Origin)
184+
}
185+
if err.Origin != "origin1" {
186+
t.Errorf("Expected Origin to be 'origin1', got '%s'", err.Origin)
187+
}
188+
}
189+
190+
func TestErrorListOrigin(t *testing.T) {
191+
// Create an ErrorList with multiple errors
192+
list := ErrorList{
193+
Invalid(NewPath("field1"), "value1", "detail1"),
194+
Invalid(NewPath("field2"), "value2", "detail2"),
195+
Required(NewPath("field3"), "detail3"),
196+
}
197+
198+
// Test WithOrigin
199+
newList := list.WithOrigin("origin1")
200+
// Check that WithOrigin returns the modified list
201+
for i, err := range newList {
202+
if err.Origin != "origin1" {
203+
t.Errorf("Error %d: Expected Origin to be 'origin2', got '%s'", i, err.Origin)
204+
}
205+
}
206+
207+
// Check that the original list was also modified (WithOrigin modifies and returns the same list)
208+
for i, err := range list {
209+
if err.Origin != "origin1" {
210+
t.Errorf("Error %d: Expected original list Origin to be 'origin2', got '%s'", i, err.Origin)
211+
}
212+
}
213+
}

0 commit comments

Comments
 (0)