Skip to content

Commit

Permalink
feat: support form array in three notations
Browse files Browse the repository at this point in the history
Signed-off-by: kevin <[email protected]>
  • Loading branch information
kevwan committed Dec 20, 2024
1 parent 8f9ba3e commit d977d7b
Show file tree
Hide file tree
Showing 6 changed files with 162 additions and 21 deletions.
66 changes: 55 additions & 11 deletions core/mapping/unmarshaler.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
)

const (
comma = ","
defaultKeyName = "key"
delimiter = '.'
ignoreKey = "-"
Expand All @@ -36,6 +37,7 @@ var (
defaultCacheLock sync.Mutex
emptyMap = map[string]any{}
emptyValue = reflect.ValueOf(lang.Placeholder)
stringSliceType = reflect.TypeOf([]string{})
)

type (
Expand Down Expand Up @@ -173,13 +175,18 @@ func (u *Unmarshaler) fillSlice(fieldType reflect.Type, value reflect.Value,
baseType := fieldType.Elem()
dereffedBaseType := Deref(baseType)
dereffedBaseKind := dereffedBaseType.Kind()
conv := reflect.MakeSlice(reflect.SliceOf(baseType), refValue.Len(), refValue.Cap())
if refValue.Len() == 0 {
value.Set(conv)
value.Set(reflect.MakeSlice(reflect.SliceOf(baseType), 0, 0))
return nil
}

if u.opts.fromArray {
refValue = makeStringSlice(refValue)
}

var valid bool
conv := reflect.MakeSlice(reflect.SliceOf(baseType), refValue.Len(), refValue.Cap())

for i := 0; i < refValue.Len(); i++ {
ithValue := refValue.Index(i).Interface()
if ithValue == nil {
Expand All @@ -191,17 +198,9 @@ func (u *Unmarshaler) fillSlice(fieldType reflect.Type, value reflect.Value,

switch dereffedBaseKind {
case reflect.Struct:
target := reflect.New(dereffedBaseType)
val, ok := ithValue.(map[string]any)
if !ok {
return errTypeMismatch
}

if err := u.unmarshal(val, target.Interface(), sliceFullName); err != nil {
if err := u.fillStructElement(baseType, conv.Index(i), ithValue, sliceFullName); err != nil {
return err
}

SetValue(fieldType.Elem(), conv.Index(i), target.Elem())
case reflect.Slice:
if err := u.fillSlice(dereffedBaseType, conv.Index(i), ithValue, sliceFullName); err != nil {
return err
Expand Down Expand Up @@ -310,6 +309,23 @@ func (u *Unmarshaler) fillSliceWithDefault(derefedType reflect.Type, value refle
return u.fillSlice(derefedType, value, slice, fullName)
}

func (u *Unmarshaler) fillStructElement(baseType reflect.Type, target reflect.Value,
value any, fullName string) error {
val, ok := value.(map[string]any)
if !ok {
return errTypeMismatch
}

// use Deref(baseType) to get the base type in case the type is a pointer type.
ptr := reflect.New(Deref(baseType))
if err := u.unmarshal(val, ptr.Interface(), fullName); err != nil {
return err
}

SetValue(baseType, target, ptr.Elem())
return nil
}

func (u *Unmarshaler) fillUnmarshalerStruct(fieldType reflect.Type,
value reflect.Value, targetValue string) error {
if !value.CanSet() {
Expand Down Expand Up @@ -1146,6 +1162,34 @@ func join(elem ...string) string {
return builder.String()
}

func makeStringSlice(refValue reflect.Value) reflect.Value {
if refValue.Len() != 1 {
return refValue
}

element := refValue.Index(0)
if element.Kind() != reflect.String {
return refValue
}

val, ok := element.Interface().(string)
if !ok {
return refValue
}

splits := strings.Split(val, comma)
if len(splits) <= 1 {
return refValue
}

slice := reflect.MakeSlice(stringSliceType, len(splits), len(splits))
for i, split := range splits {
slice.Index(i).Set(reflect.ValueOf(split))
}

return slice
}

func newInitError(name string) error {
return fmt.Errorf("field %q is not set", name)
}
Expand Down
17 changes: 16 additions & 1 deletion core/mapping/unmarshaler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -351,7 +351,7 @@ func TestUnmarshalIntSliceOfPtr(t *testing.T) {
assert.Error(t, UnmarshalKey(m, &in))
})

t.Run("int slice with nil", func(t *testing.T) {
t.Run("int slice with nil element", func(t *testing.T) {
type inner struct {
Ints []int `key:"ints"`
}
Expand All @@ -365,6 +365,21 @@ func TestUnmarshalIntSliceOfPtr(t *testing.T) {
assert.Empty(t, in.Ints)
}
})

t.Run("int slice with nil", func(t *testing.T) {
type inner struct {
Ints []int `key:"ints"`
}

m := map[string]any{
"ints": []any(nil),
}

var in inner
if assert.NoError(t, UnmarshalKey(m, &in)) {
assert.Empty(t, in.Ints)
}
})
}

func TestUnmarshalIntWithDefault(t *testing.T) {
Expand Down
75 changes: 75 additions & 0 deletions rest/httpx/requests_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,21 @@ func TestParseFormArray(t *testing.T) {
}
})

t.Run("slice with empty", func(t *testing.T) {
var v struct {
Name []string `form:"name,optional"`
}

r, err := http.NewRequest(
http.MethodGet,
"/a?name=",
http.NoBody)
assert.NoError(t, err)
if assert.NoError(t, Parse(r, &v)) {
assert.ElementsMatch(t, []string{}, v.Name)
}
})

t.Run("slice with empty and non-empty", func(t *testing.T) {
var v struct {
Name []string `form:"name"`
Expand All @@ -102,6 +117,66 @@ func TestParseFormArray(t *testing.T) {
assert.ElementsMatch(t, []string{"1"}, v.Name)
}
})

t.Run("slice with one value on array format", func(t *testing.T) {
var v struct {
Names []string `form:"names"`
}

r, err := http.NewRequest(
http.MethodGet,
"/a?names=1,2,3",
http.NoBody)
assert.NoError(t, err)
if assert.NoError(t, Parse(r, &v)) {
assert.ElementsMatch(t, []string{"1", "2", "3"}, v.Names)
}
})

t.Run("slice with one value on combined array format", func(t *testing.T) {
var v struct {
Names []string `form:"names"`
}

r, err := http.NewRequest(
http.MethodGet,
"/a?names=[1,2,3]&names=4",
http.NoBody)
assert.NoError(t, err)
if assert.NoError(t, Parse(r, &v)) {
assert.ElementsMatch(t, []string{"[1,2,3]", "4"}, v.Names)
}
})

t.Run("slice with one value on integer array format", func(t *testing.T) {
var v struct {
Numbers []int `form:"numbers"`
}

r, err := http.NewRequest(
http.MethodGet,
"/a?numbers=1,2,3",
http.NoBody)
assert.NoError(t, err)
if assert.NoError(t, Parse(r, &v)) {
assert.ElementsMatch(t, []int{1, 2, 3}, v.Numbers)
}
})

t.Run("slice with one value on array format brackets", func(t *testing.T) {
var v struct {
Names []string `form:"names"`
}

r, err := http.NewRequest(
http.MethodGet,
"/a?names[]=1&names[]=2&names[]=3",
http.NoBody)
assert.NoError(t, err)
if assert.NoError(t, Parse(r, &v)) {
assert.ElementsMatch(t, []string{"1", "2", "3"}, v.Names)
}
})
}

func TestParseForm_Error(t *testing.T) {
Expand Down
9 changes: 8 additions & 1 deletion rest/httpx/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,13 @@ package httpx
import (
"errors"
"net/http"
"strings"
)

const xForwardedFor = "X-Forwarded-For"
const (
xForwardedFor = "X-Forwarded-For"
arraySuffix = "[]"
)

// GetFormValues returns the form values.
func GetFormValues(r *http.Request) (map[string]any, error) {
Expand All @@ -29,6 +33,9 @@ func GetFormValues(r *http.Request) (map[string]any, error) {
}

if len(filtered) > 0 {
if strings.HasSuffix(name, arraySuffix) {
name = name[:len(name)-2]
}
params[name] = filtered
}
}
Expand Down
14 changes: 7 additions & 7 deletions tools/goctl/pkg/parser/api/parser/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@ import (
)

const (
idAPI = "api"
groupKeyText = "group"
infoTitleKey = "Title"
infoDescKey = "Desc"
infoVersionKey = "Version"
infoAuthorKey = "Author"
infoEmailKey = "Email"
idAPI = "api"
groupKeyText = "group"
infoTitleKey = "Title"
infoDescKey = "Desc"
infoVersionKey = "Version"
infoAuthorKey = "Author"
infoEmailKey = "Email"
)

// Parser is the parser for api file.
Expand Down
2 changes: 1 addition & 1 deletion tools/goctl/pkg/parser/api/parser/parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,7 @@ func TestParser_Parse_atServerStmt(t *testing.T) {
"prefix3:": "v1/v2_",
"prefix4:": "a-b-c",
"summary:": `"test"`,
"key:": `"bar"`,
"key:": `"bar"`,
}

p := New("foo.api", atServerTestAPI)
Expand Down

0 comments on commit d977d7b

Please sign in to comment.