Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
- Resolve string and integer map keys using their declared type during nested
struct validation and filter/default write-back, including defined key types.
Invalid or overflowing integer path segments are treated as missing entries.
- Treat out-of-range slice and array indexes in struct field paths as missing instead of panicking.

## v2.0.0

Expand Down
6 changes: 6 additions & 0 deletions data_source.go
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,9 @@ func (d *StructData) tryGetRV(field string) (fv reflect.Value, exist, zero bool)
switch kind {
case reflect.Array, reflect.Slice:
index, _ := strconv.Atoi(fieldNode)
if index < 0 || index >= fv.Len() {
return reflect.Value{}, false, false
}
fv = fv.Index(index)
case reflect.Map:
fv = structMapValue(fv, fieldNode)
Expand Down Expand Up @@ -614,6 +617,9 @@ func (d *StructData) Set(field string, val any) (newVal any, err error) {
if err != nil {
return nil, ErrInvalidData
}
if index >= fv.Len() {
return nil, ErrNoField
}

fv = fv.Index(index)
case reflect.Map:
Expand Down
46 changes: 46 additions & 0 deletions data_source_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,52 @@ func TestStructData_Get_ptrVal(t *testing.T) {
assert.Equal(t, 0, *val.(*int))
}

func TestStructData_sliceIndexOutOfRange(t *testing.T) {
type Item struct {
Name string
}
form := &struct {
Items []Item
Fixed [1]Item
}{Items: []Item{{Name: "Ada"}}, Fixed: [1]Item{{Name: "Ada"}}}

d, err := FromStruct(form)
assert.NoErr(t, err)
for _, path := range []string{"Items.0.Name", "Fixed.0.Name"} {
value, exist, zero := d.TryGet(path)
assert.True(t, exist)
assert.False(t, zero)
assert.Eq(t, "Ada", value)
}
for _, path := range []string{"Items.1.Name", "Items.-1.Name", "Fixed.1.Name"} {
value, exist, zero := d.TryGet(path)
assert.False(t, exist)
assert.False(t, zero)
assert.Nil(t, value)
}

// a resolved index that no longer exists when written back
d, err = FromStruct(form)
assert.NoErr(t, err)
_, exist := d.Get("Items.0.Name")
assert.True(t, exist)
form.Items = nil
_, err = d.Set("Items.0.Name", "Grace")
assert.Eq(t, ErrNoField, err)

v := Struct(&struct{ Items []Item }{})
v.StringRule("Items.0.Name", "required")
assert.False(t, v.Validate())
assert.NotEmpty(t, v.Errors.Field("Items.0.Name"))

type Order struct {
Items []Item
Kind string `validate:"requiredIf:Items.0.Name,gift"`
}
assert.True(t, Struct(&Order{}).Validate())
assert.False(t, Struct(&Order{Items: []Item{{Name: "gift"}}}).Validate())
}

func TestValidatePrivateFieldsWhenTrue(t *testing.T) {
type foo struct {
Field1 int `validate:"required|min:5|max:20" message:"Field1 outside of range"`
Expand Down
Loading