Skip to content

Commit 2275e7b

Browse files
committed
feat: support form array in three notations
Signed-off-by: kevin <[email protected]>
1 parent 8f9ba3e commit 2275e7b

File tree

6 files changed

+237
-34
lines changed

6 files changed

+237
-34
lines changed

core/mapping/unmarshaler.go

+55-11
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
)
1919

2020
const (
21+
comma = ","
2122
defaultKeyName = "key"
2223
delimiter = '.'
2324
ignoreKey = "-"
@@ -36,6 +37,7 @@ var (
3637
defaultCacheLock sync.Mutex
3738
emptyMap = map[string]any{}
3839
emptyValue = reflect.ValueOf(lang.Placeholder)
40+
stringSliceType = reflect.TypeOf([]string{})
3941
)
4042

4143
type (
@@ -173,13 +175,18 @@ func (u *Unmarshaler) fillSlice(fieldType reflect.Type, value reflect.Value,
173175
baseType := fieldType.Elem()
174176
dereffedBaseType := Deref(baseType)
175177
dereffedBaseKind := dereffedBaseType.Kind()
176-
conv := reflect.MakeSlice(reflect.SliceOf(baseType), refValue.Len(), refValue.Cap())
177178
if refValue.Len() == 0 {
178-
value.Set(conv)
179+
value.Set(reflect.MakeSlice(reflect.SliceOf(baseType), 0, 0))
179180
return nil
180181
}
181182

183+
if u.opts.fromArray {
184+
refValue = makeStringSlice(refValue)
185+
}
186+
182187
var valid bool
188+
conv := reflect.MakeSlice(reflect.SliceOf(baseType), refValue.Len(), refValue.Cap())
189+
183190
for i := 0; i < refValue.Len(); i++ {
184191
ithValue := refValue.Index(i).Interface()
185192
if ithValue == nil {
@@ -191,17 +198,9 @@ func (u *Unmarshaler) fillSlice(fieldType reflect.Type, value reflect.Value,
191198

192199
switch dereffedBaseKind {
193200
case reflect.Struct:
194-
target := reflect.New(dereffedBaseType)
195-
val, ok := ithValue.(map[string]any)
196-
if !ok {
197-
return errTypeMismatch
198-
}
199-
200-
if err := u.unmarshal(val, target.Interface(), sliceFullName); err != nil {
201+
if err := u.fillStructElement(baseType, conv.Index(i), ithValue, sliceFullName); err != nil {
201202
return err
202203
}
203-
204-
SetValue(fieldType.Elem(), conv.Index(i), target.Elem())
205204
case reflect.Slice:
206205
if err := u.fillSlice(dereffedBaseType, conv.Index(i), ithValue, sliceFullName); err != nil {
207206
return err
@@ -310,6 +309,23 @@ func (u *Unmarshaler) fillSliceWithDefault(derefedType reflect.Type, value refle
310309
return u.fillSlice(derefedType, value, slice, fullName)
311310
}
312311

312+
func (u *Unmarshaler) fillStructElement(baseType reflect.Type, target reflect.Value,
313+
value any, fullName string) error {
314+
val, ok := value.(map[string]any)
315+
if !ok {
316+
return errTypeMismatch
317+
}
318+
319+
// use Deref(baseType) to get the base type in case the type is a pointer type.
320+
ptr := reflect.New(Deref(baseType))
321+
if err := u.unmarshal(val, ptr.Interface(), fullName); err != nil {
322+
return err
323+
}
324+
325+
SetValue(baseType, target, ptr.Elem())
326+
return nil
327+
}
328+
313329
func (u *Unmarshaler) fillUnmarshalerStruct(fieldType reflect.Type,
314330
value reflect.Value, targetValue string) error {
315331
if !value.CanSet() {
@@ -1146,6 +1162,34 @@ func join(elem ...string) string {
11461162
return builder.String()
11471163
}
11481164

1165+
func makeStringSlice(refValue reflect.Value) reflect.Value {
1166+
if refValue.Len() != 1 {
1167+
return refValue
1168+
}
1169+
1170+
element := refValue.Index(0)
1171+
if element.Kind() != reflect.String {
1172+
return refValue
1173+
}
1174+
1175+
val, ok := element.Interface().(string)
1176+
if !ok {
1177+
return refValue
1178+
}
1179+
1180+
splits := strings.Split(val, comma)
1181+
if len(splits) <= 1 {
1182+
return refValue
1183+
}
1184+
1185+
slice := reflect.MakeSlice(stringSliceType, len(splits), len(splits))
1186+
for i, split := range splits {
1187+
slice.Index(i).Set(reflect.ValueOf(split))
1188+
}
1189+
1190+
return slice
1191+
}
1192+
11491193
func newInitError(name string) error {
11501194
return fmt.Errorf("field %q is not set", name)
11511195
}

core/mapping/unmarshaler_test.go

+91-14
Original file line numberDiff line numberDiff line change
@@ -351,7 +351,7 @@ func TestUnmarshalIntSliceOfPtr(t *testing.T) {
351351
assert.Error(t, UnmarshalKey(m, &in))
352352
})
353353

354-
t.Run("int slice with nil", func(t *testing.T) {
354+
t.Run("int slice with nil element", func(t *testing.T) {
355355
type inner struct {
356356
Ints []int `key:"ints"`
357357
}
@@ -365,6 +365,21 @@ func TestUnmarshalIntSliceOfPtr(t *testing.T) {
365365
assert.Empty(t, in.Ints)
366366
}
367367
})
368+
369+
t.Run("int slice with nil", func(t *testing.T) {
370+
type inner struct {
371+
Ints []int `key:"ints"`
372+
}
373+
374+
m := map[string]any{
375+
"ints": []any(nil),
376+
}
377+
378+
var in inner
379+
if assert.NoError(t, UnmarshalKey(m, &in)) {
380+
assert.Empty(t, in.Ints)
381+
}
382+
})
368383
}
369384

370385
func TestUnmarshalIntWithDefault(t *testing.T) {
@@ -1374,20 +1389,82 @@ func TestUnmarshalWithFloatPtr(t *testing.T) {
13741389
}
13751390

13761391
func TestUnmarshalIntSlice(t *testing.T) {
1377-
var v struct {
1378-
Ages []int `key:"ages"`
1379-
Slice []int `key:"slice"`
1380-
}
1381-
m := map[string]any{
1382-
"ages": []int{1, 2},
1383-
"slice": []any{},
1384-
}
1392+
t.Run("int slice from int", func(t *testing.T) {
1393+
var v struct {
1394+
Ages []int `key:"ages"`
1395+
Slice []int `key:"slice"`
1396+
}
1397+
m := map[string]any{
1398+
"ages": []int{1, 2},
1399+
"slice": []any{},
1400+
}
13851401

1386-
ast := assert.New(t)
1387-
if ast.NoError(UnmarshalKey(m, &v)) {
1388-
ast.ElementsMatch([]int{1, 2}, v.Ages)
1389-
ast.Equal([]int{}, v.Slice)
1390-
}
1402+
ast := assert.New(t)
1403+
if ast.NoError(UnmarshalKey(m, &v)) {
1404+
ast.ElementsMatch([]int{1, 2}, v.Ages)
1405+
ast.Equal([]int{}, v.Slice)
1406+
}
1407+
})
1408+
1409+
t.Run("int slice from one int", func(t *testing.T) {
1410+
var v struct {
1411+
Ages []int `key:"ages"`
1412+
}
1413+
m := map[string]any{
1414+
"ages": []int{2},
1415+
}
1416+
1417+
ast := assert.New(t)
1418+
unmarshaler := NewUnmarshaler(defaultKeyName, WithFromArray())
1419+
if ast.NoError(unmarshaler.Unmarshal(m, &v)) {
1420+
ast.ElementsMatch([]int{2}, v.Ages)
1421+
}
1422+
})
1423+
1424+
t.Run("int slice from one int string", func(t *testing.T) {
1425+
var v struct {
1426+
Ages []int `key:"ages"`
1427+
}
1428+
m := map[string]any{
1429+
"ages": []string{"2"},
1430+
}
1431+
1432+
ast := assert.New(t)
1433+
unmarshaler := NewUnmarshaler(defaultKeyName, WithFromArray())
1434+
if ast.NoError(unmarshaler.Unmarshal(m, &v)) {
1435+
ast.ElementsMatch([]int{2}, v.Ages)
1436+
}
1437+
})
1438+
1439+
t.Run("int slice from one json.Number", func(t *testing.T) {
1440+
var v struct {
1441+
Ages []int `key:"ages"`
1442+
}
1443+
m := map[string]any{
1444+
"ages": []json.Number{"2"},
1445+
}
1446+
1447+
ast := assert.New(t)
1448+
unmarshaler := NewUnmarshaler(defaultKeyName, WithFromArray())
1449+
if ast.NoError(unmarshaler.Unmarshal(m, &v)) {
1450+
ast.ElementsMatch([]int{2}, v.Ages)
1451+
}
1452+
})
1453+
1454+
t.Run("int slice from one int strings", func(t *testing.T) {
1455+
var v struct {
1456+
Ages []int `key:"ages"`
1457+
}
1458+
m := map[string]any{
1459+
"ages": []string{"1,2"},
1460+
}
1461+
1462+
ast := assert.New(t)
1463+
unmarshaler := NewUnmarshaler(defaultKeyName, WithFromArray())
1464+
if ast.NoError(unmarshaler.Unmarshal(m, &v)) {
1465+
ast.ElementsMatch([]int{1, 2}, v.Ages)
1466+
}
1467+
})
13911468
}
13921469

13931470
func TestUnmarshalString(t *testing.T) {

rest/httpx/requests_test.go

+75
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,21 @@ func TestParseFormArray(t *testing.T) {
8888
}
8989
})
9090

91+
t.Run("slice with empty", func(t *testing.T) {
92+
var v struct {
93+
Name []string `form:"name,optional"`
94+
}
95+
96+
r, err := http.NewRequest(
97+
http.MethodGet,
98+
"/a?name=",
99+
http.NoBody)
100+
assert.NoError(t, err)
101+
if assert.NoError(t, Parse(r, &v)) {
102+
assert.ElementsMatch(t, []string{}, v.Name)
103+
}
104+
})
105+
91106
t.Run("slice with empty and non-empty", func(t *testing.T) {
92107
var v struct {
93108
Name []string `form:"name"`
@@ -102,6 +117,66 @@ func TestParseFormArray(t *testing.T) {
102117
assert.ElementsMatch(t, []string{"1"}, v.Name)
103118
}
104119
})
120+
121+
t.Run("slice with one value on array format", func(t *testing.T) {
122+
var v struct {
123+
Names []string `form:"names"`
124+
}
125+
126+
r, err := http.NewRequest(
127+
http.MethodGet,
128+
"/a?names=1,2,3",
129+
http.NoBody)
130+
assert.NoError(t, err)
131+
if assert.NoError(t, Parse(r, &v)) {
132+
assert.ElementsMatch(t, []string{"1", "2", "3"}, v.Names)
133+
}
134+
})
135+
136+
t.Run("slice with one value on combined array format", func(t *testing.T) {
137+
var v struct {
138+
Names []string `form:"names"`
139+
}
140+
141+
r, err := http.NewRequest(
142+
http.MethodGet,
143+
"/a?names=[1,2,3]&names=4",
144+
http.NoBody)
145+
assert.NoError(t, err)
146+
if assert.NoError(t, Parse(r, &v)) {
147+
assert.ElementsMatch(t, []string{"[1,2,3]", "4"}, v.Names)
148+
}
149+
})
150+
151+
t.Run("slice with one value on integer array format", func(t *testing.T) {
152+
var v struct {
153+
Numbers []int `form:"numbers"`
154+
}
155+
156+
r, err := http.NewRequest(
157+
http.MethodGet,
158+
"/a?numbers=1,2,3",
159+
http.NoBody)
160+
assert.NoError(t, err)
161+
if assert.NoError(t, Parse(r, &v)) {
162+
assert.ElementsMatch(t, []int{1, 2, 3}, v.Numbers)
163+
}
164+
})
165+
166+
t.Run("slice with one value on array format brackets", func(t *testing.T) {
167+
var v struct {
168+
Names []string `form:"names"`
169+
}
170+
171+
r, err := http.NewRequest(
172+
http.MethodGet,
173+
"/a?names[]=1&names[]=2&names[]=3",
174+
http.NoBody)
175+
assert.NoError(t, err)
176+
if assert.NoError(t, Parse(r, &v)) {
177+
assert.ElementsMatch(t, []string{"1", "2", "3"}, v.Names)
178+
}
179+
})
105180
}
106181

107182
func TestParseForm_Error(t *testing.T) {

rest/httpx/util.go

+8-1
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,13 @@ package httpx
33
import (
44
"errors"
55
"net/http"
6+
"strings"
67
)
78

8-
const xForwardedFor = "X-Forwarded-For"
9+
const (
10+
xForwardedFor = "X-Forwarded-For"
11+
arraySuffix = "[]"
12+
)
913

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

3135
if len(filtered) > 0 {
36+
if strings.HasSuffix(name, arraySuffix) {
37+
name = name[:len(name)-2]
38+
}
3239
params[name] = filtered
3340
}
3441
}

tools/goctl/pkg/parser/api/parser/parser.go

+7-7
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,13 @@ import (
1313
)
1414

1515
const (
16-
idAPI = "api"
17-
groupKeyText = "group"
18-
infoTitleKey = "Title"
19-
infoDescKey = "Desc"
20-
infoVersionKey = "Version"
21-
infoAuthorKey = "Author"
22-
infoEmailKey = "Email"
16+
idAPI = "api"
17+
groupKeyText = "group"
18+
infoTitleKey = "Title"
19+
infoDescKey = "Desc"
20+
infoVersionKey = "Version"
21+
infoAuthorKey = "Author"
22+
infoEmailKey = "Email"
2323
)
2424

2525
// Parser is the parser for api file.

tools/goctl/pkg/parser/api/parser/parser_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -305,7 +305,7 @@ func TestParser_Parse_atServerStmt(t *testing.T) {
305305
"prefix3:": "v1/v2_",
306306
"prefix4:": "a-b-c",
307307
"summary:": `"test"`,
308-
"key:": `"bar"`,
308+
"key:": `"bar"`,
309309
}
310310

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

0 commit comments

Comments
 (0)