Skip to content

Commit

Permalink
WIP: binder: add slice nested binding support1
Browse files Browse the repository at this point in the history
  • Loading branch information
efectn committed Sep 3, 2024
1 parent 9fbf830 commit b851f71
Show file tree
Hide file tree
Showing 4 changed files with 309 additions and 0 deletions.
1 change: 1 addition & 0 deletions bind.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ type fieldTextDecoder struct {
get func(c Ctx, key string, defaultValue ...string) string
subFieldDecoders []decoder
isTextMarshaler bool
fragments []requestKeyFragment
}

func (d *fieldTextDecoder) Decode(ctx Ctx, reqValue reflect.Value) error {
Expand Down
140 changes: 140 additions & 0 deletions bind_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,146 @@ func Test_Bind_NestedStruct(t *testing.T) {
}, u)
}

func Test_Bind_Slice_NestedStruct(t *testing.T) {
t.Parallel()

app := New()
c := app.AcquireCtx(&fasthttp.RequestCtx{})

type Person struct {
Name string `query:"name"`
Age int `query:"age"`
}

type CollectionQuery struct {
Data []Person `query:"data"`
}

c.Request().URI().SetQueryString("data.0.name=john&data.0.age=10&data.1.name=doe&data.1.age=12")

var cq CollectionQuery

require.NoError(t, c.Bind().Req(&cq).Err())

require.Equal(t, CollectionQuery{
Data: []Person{
{Name: "john", Age: 10},
{Name: "doe", Age: 12},
},
}, cq)
}

func Benchmark_Bind_Slice_NestedStruct(b *testing.B) {
app := New()
c := app.AcquireCtx(&fasthttp.RequestCtx{})

type Person struct {
Name string `query:"name"`
Age int `query:"age"`
}

type CollectionQuery struct {
Data []Person `query:"data"`
}

c.Request().URI().SetQueryString("data.0.name=john&data.0.age=10&data.1.name=doe&data.1.age=12")

var cq CollectionQuery

for i := 0; i < b.N; i++ {
_ = c.Bind().Req(&cq)
}

require.NoError(b, c.Bind().Req(&cq).Err())

require.Equal(b, CollectionQuery{
Data: []Person{
{Name: "john", Age: 10},
{Name: "doe", Age: 12},
},
}, cq)
}

func Test_Bind_Slice_NestedStruct2(t *testing.T) {
t.Parallel()

app := New()
c := app.AcquireCtx(&fasthttp.RequestCtx{})

type Person struct {
Name string `query:"name"`
Age int `query:"age"`
}

type Family struct {
Name string `query:"name"`
Members []Person `query:"members"`
}

type CollectionQuery struct {
Data []Family `query:"data"`
}

c.Request().URI().SetQueryString("data.0.name=doe&data.0.members.0.name=john&data.0.members.0.age=10&data.0.members.1.name=doe&data.0.members.1.age=12&data.0.members.2.name=doe&data.0.members.2.age=12")

var cq CollectionQuery

require.NoError(t, c.Bind().Req(&cq).Err())

require.Equal(t, CollectionQuery{
Data: []Family{
{
Name: "doe",
Members: []Person{
{Name: "john", Age: 10},
{Name: "doe", Age: 12},
},
},
},
}, cq)
}

func Test_Bind_Slice_NestedStruct3(t *testing.T) {
t.Parallel()

app := New()
c := app.AcquireCtx(&fasthttp.RequestCtx{})

type Test2 struct {
Name string `query:"name"`
Age int `query:"age"`
}

type Person struct {
Name string `query:"name"`
Age int `query:"age"`
Test Test2 `query:"test"`
}

type CollectionQuery struct {
Data []Person `query:"data"`
}

c.Request().URI().SetQueryString("data.0.name=john&data.0.age=10&data.0.test.name=doe&data.0.test.age=12")

var cq CollectionQuery

require.NoError(t, c.Bind().Req(&cq).Err())

require.Equal(t, CollectionQuery{
Data: []Person{
{
Name: "john",
Age: 10,
Test: Test2{
Name: "doe",
Age: 12,
},
},
},
}, cq)
}

// go test -run Test_Bind_Query -v
func Test_Bind_Query(t *testing.T) {
t.Parallel()
Expand Down
40 changes: 40 additions & 0 deletions binder_compile.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"fmt"
"reflect"
"strconv"
"strings"

"github.com/gofiber/fiber/v3/internal/bind"
"github.com/gofiber/utils/v2"
Expand All @@ -31,6 +32,13 @@ type bindCompileOption struct {
reqDecoder bool // to parse header/cookie/param/query/header/respHeader
}

type requestKeyFragment struct {
key string
num int
index int
isNum bool
}

func compileReqParser(rt reflect.Type, opt bindCompileOption) (Decoder, error) {
var decoders []decoder

Expand Down Expand Up @@ -169,6 +177,22 @@ func compileTextBasedDecoder(field reflect.StructField, index int, tagScope, tag
get: get,
}

// append fragments
if strings.Contains(tagContent, ".") {
pieces := strings.Split(tagContent, ".")
frags := make([]requestKeyFragment, 0, len(pieces))

for _, piece := range pieces {
if piece == "NUM" {
frags = append(frags, requestKeyFragment{num: -1, isNum: true})
continue
}

frags = append(frags, requestKeyFragment{key: piece})
}
fieldDecoder.fragments = frags
}

// Check if the field implements encoding.TextUnmarshaler
if len(isTextMarshaler) > 0 && isTextMarshaler[0] {
fieldDecoder.isTextMarshaler = true
Expand Down Expand Up @@ -248,6 +272,22 @@ func compileSliceFieldTextBasedDecoder(field reflect.StructField, index int, tag
elementType: et,
}

// append fragments
if strings.Contains(tagContent, ".") {
pieces := strings.Split(tagContent, ".")
frags := make([]requestKeyFragment, 0, len(pieces))

for _, piece := range pieces {
if piece == "NUM" {
frags = append(frags, requestKeyFragment{num: -1, isNum: true})
continue
}

frags = append(frags, requestKeyFragment{key: piece})
}
sliceDecoder.fragments = frags
}

// support struct slices
if et.Kind() == reflect.Struct {
var decoders []decoder
Expand Down
128 changes: 128 additions & 0 deletions binder_slice.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package fiber

import (
"bytes"
"reflect"
"strconv"
"strings"

"github.com/gofiber/fiber/v3/internal/bind"
"github.com/gofiber/utils/v2"
Expand All @@ -20,9 +23,20 @@ type fieldSliceDecoder struct {
elementDecoder bind.TextDecoder
visitAll func(Ctx, func(key []byte, value []byte))
subFieldDecoders []decoder
fragments []requestKeyFragment
}

func (d *fieldSliceDecoder) Decode(ctx Ctx, reqValue reflect.Value) error {
if len(d.subFieldDecoders) > 0 {
rv, err := d.decodeSubFields(ctx, reqValue)
if err != nil {
return err
}

reqValue.Field(d.fieldIndex).Set(rv)
return nil
}

count := 0
d.visitAll(ctx, func(key, value []byte) {
if d.eqBytes(key, d.reqKey) {
Expand Down Expand Up @@ -60,6 +74,120 @@ func (d *fieldSliceDecoder) Decode(ctx Ctx, reqValue reflect.Value) error {
return nil
}

func (d *fieldSliceDecoder) decodeSubFields(ctx Ctx, reqValue reflect.Value) (reflect.Value, error) {
rv := reflect.New(d.fieldType).Elem()

// reqValue => ana struct
for _, subFieldDecoder := range d.subFieldDecoders {
if subFieldDecoder.Kind() == "text" {
textDec, ok := subFieldDecoder.(*fieldTextDecoder)
if !ok {
continue
}

test := make(map[string]int)

count := 0
maxIndex := 0
d.visitAll(ctx, func(key, value []byte) {
var num int
if !bytes.Contains(key, []byte(".")) {
return
}

frag := prepareFragments(utils.UnsafeString(key))

if textDec.subFieldDecoders == nil && len(frag) != len(textDec.fragments) {
return
}

if textDec.subFieldDecoders != nil && len(frag) > len(textDec.fragments) {

}

for i, f := range frag {
if textDec.fragments[i].isNum && f.isNum {
if f.num > maxIndex {
maxIndex = f.num
}
num = f.num
} else if textDec.fragments[i].key != f.key {
return
}
}
count++
test[utils.UnsafeString(key)] = num
})

if count == 0 {
reqValue.Field(d.fieldIndex).Set(reflect.MakeSlice(d.fieldType, 0, 0))
continue
}

if rv.Len() < maxIndex+1 {
rv = reflect.MakeSlice(d.fieldType, maxIndex+1, maxIndex+1)
}

d.visitAll(ctx, func(key, value []byte) {
if index, ok := test[utils.UnsafeString(key)]; ok {
textDec.dec.UnmarshalString(utils.UnsafeString(value), rv.Index(index).Field(textDec.fieldIndex))
}
})
} else {
sliceDec, ok := subFieldDecoder.(*fieldSliceDecoder)
if !ok {
continue
}

var count int
var maxIndex int

d.visitAll(ctx, func(key, value []byte) {
if !bytes.Contains(key, []byte(".")) {
return
}

frag := prepareFragments(utils.UnsafeString(key))

if len(frag) < len(sliceDec.fragments)+1 {
return
}
for i := 0; i < len(sliceDec.fragments)+1; i++ {
if i == len(sliceDec.fragments) && frag[i].isNum {
count++
if frag[i].num > maxIndex {
maxIndex = frag[i].num
}
continue
}

if frag[i].key != sliceDec.fragments[i].key && !frag[i].isNum {
return
}
}
})
//sliceDec.decodeSubFields(ctx, rv)
}
}

return rv, nil
}

func prepareFragments(key string) []requestKeyFragment {
split := strings.Split(key, ".")
fragments := make([]requestKeyFragment, 0, len(split))
for _, fragment := range split {
num, err := strconv.Atoi(fragment)
fragments = append(fragments, requestKeyFragment{
key: fragment,
num: num,
isNum: err == nil,
})
}

return fragments
}

func (d *fieldSliceDecoder) Kind() string {
return "slice"
}
Expand Down

0 comments on commit b851f71

Please sign in to comment.