Skip to content

Commit 256e3f8

Browse files
committed
feat(context): methods to get nested map from query string
1 parent cc4e114 commit 256e3f8

File tree

4 files changed

+274
-0
lines changed

4 files changed

+274
-0
lines changed

context.go

+16
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121

2222
"github.com/gin-contrib/sse"
2323
"github.com/gin-gonic/gin/binding"
24+
"github.com/gin-gonic/gin/internal/query"
2425
"github.com/gin-gonic/gin/render"
2526
)
2627

@@ -504,6 +505,21 @@ func (c *Context) GetQueryMap(key string) (map[string]string, bool) {
504505
return c.get(c.queryCache, key)
505506
}
506507

508+
// QueryNestedMap returns a map for a given query key.
509+
// In contrast to QueryMap it handles nesting in query maps like key[foo][bar]=value.
510+
func (c *Context) QueryNestedMap(key string) (dicts map[string]interface{}) {
511+
dicts, _ = c.GetQueryNestedMap(key)
512+
return
513+
}
514+
515+
// GetQueryNestedMap returns a map for a given query key, plus a boolean value
516+
// whether at least one value exists for the given key.
517+
// In contrast to GetQueryMap it handles nesting in query maps like key[foo][bar]=value.
518+
func (c *Context) GetQueryNestedMap(key string) (map[string]interface{}, bool) {
519+
c.initQueryCache()
520+
return query.GetMap(c.queryCache, key)
521+
}
522+
507523
// PostForm returns the specified key from a POST urlencoded form or multipart form
508524
// when it exists, otherwise it returns an empty string `("")`.
509525
func (c *Context) PostForm(key string) (value string) {

context_test.go

+146
Original file line numberDiff line numberDiff line change
@@ -574,6 +574,152 @@ func TestContextQueryAndPostForm(t *testing.T) {
574574
assert.Empty(t, dicts)
575575
}
576576

577+
func TestContextQueryNestedMap(t *testing.T) {
578+
var emptyQueryMap map[string]interface{}
579+
580+
tests := map[string]struct {
581+
url string
582+
expectedResult map[string]interface{}
583+
exists bool
584+
}{
585+
"no searched map key in query string": {
586+
url: "?foo=bar",
587+
expectedResult: emptyQueryMap,
588+
exists: false,
589+
},
590+
"searched map key is not a map": {
591+
url: "?mapkey=value",
592+
expectedResult: emptyQueryMap,
593+
exists: false,
594+
},
595+
"searched map key is array": {
596+
url: "?mapkey[]=value1&mapkey[]=value2",
597+
expectedResult: emptyQueryMap,
598+
exists: false,
599+
},
600+
"searched map key with invalid map access": {
601+
url: "?mapkey[key]nested=value",
602+
expectedResult: emptyQueryMap,
603+
exists: false,
604+
},
605+
"searched map key with valid and invalid map access": {
606+
url: "?mapkey[key]invalidNested=value&mapkey[key][nested]=value1",
607+
expectedResult: map[string]interface{}{
608+
"key": map[string]interface{}{
609+
"nested": "value1",
610+
},
611+
},
612+
exists: true,
613+
},
614+
"searched map key after other query params": {
615+
url: "?foo=bar&mapkey[key]=value",
616+
expectedResult: map[string]interface{}{
617+
"key": "value",
618+
},
619+
exists: true,
620+
},
621+
"searched map key before other query params": {
622+
url: "?mapkey[key]=value&foo=bar",
623+
expectedResult: map[string]interface{}{
624+
"key": "value",
625+
},
626+
exists: true,
627+
},
628+
"single key in searched map key": {
629+
url: "?mapkey[key]=value",
630+
expectedResult: map[string]interface{}{
631+
"key": "value",
632+
},
633+
exists: true,
634+
},
635+
"multiple keys in searched map key": {
636+
url: "?mapkey[key1]=value1&mapkey[key2]=value2&mapkey[key3]=value3",
637+
expectedResult: map[string]interface{}{
638+
"key1": "value1",
639+
"key2": "value2",
640+
"key3": "value3",
641+
},
642+
exists: true,
643+
},
644+
"nested key in searched map key": {
645+
url: "?mapkey[foo][nested]=value1",
646+
expectedResult: map[string]interface{}{
647+
"foo": map[string]interface{}{
648+
"nested": "value1",
649+
},
650+
},
651+
exists: true,
652+
},
653+
"multiple nested keys in single key of searched map key": {
654+
url: "?mapkey[foo][nested1]=value1&mapkey[foo][nested2]=value2",
655+
expectedResult: map[string]interface{}{
656+
"foo": map[string]interface{}{
657+
"nested1": "value1",
658+
"nested2": "value2",
659+
},
660+
},
661+
exists: true,
662+
},
663+
"multiple keys with nested keys of searched map key": {
664+
url: "?mapkey[key1][nested]=value1&mapkey[key2][nested]=value2",
665+
expectedResult: map[string]interface{}{
666+
"key1": map[string]interface{}{
667+
"nested": "value1",
668+
},
669+
"key2": map[string]interface{}{
670+
"nested": "value2",
671+
},
672+
},
673+
exists: true,
674+
},
675+
"multiple levels of nesting in searched map key": {
676+
url: "?mapkey[key][nested][moreNested]=value1",
677+
expectedResult: map[string]interface{}{
678+
"key": map[string]interface{}{
679+
"nested": map[string]interface{}{
680+
"moreNested": "value1",
681+
},
682+
},
683+
},
684+
exists: true,
685+
},
686+
"query keys similar to searched map key": {
687+
url: "?mapkey[key]=value&mapkeys[key1]=value1&mapkey1=foo",
688+
expectedResult: map[string]interface{}{
689+
"key": "value",
690+
},
691+
exists: true,
692+
},
693+
}
694+
for name, test := range tests {
695+
t.Run("getQueryMap: "+name, func(t *testing.T) {
696+
u, err := url.Parse(test.url)
697+
require.NoError(t, err)
698+
699+
c := Context{
700+
Request: &http.Request{
701+
URL: u,
702+
},
703+
}
704+
dicts, exists := c.GetQueryNestedMap("mapkey")
705+
require.Equal(t, test.expectedResult, dicts)
706+
require.Equal(t, test.exists, exists)
707+
})
708+
t.Run("queryMap: "+name, func(t *testing.T) {
709+
u, err := url.Parse(test.url)
710+
require.NoError(t, err)
711+
712+
c := Context{
713+
Request: &http.Request{
714+
URL: u,
715+
},
716+
}
717+
dicts := c.QueryNestedMap("mapkey")
718+
require.Equal(t, test.expectedResult, dicts)
719+
})
720+
}
721+
}
722+
577723
func TestContextPostFormMultipart(t *testing.T) {
578724
c, _ := CreateTestContext(httptest.NewRecorder())
579725
c.Request = createMultipartRequest()

docs/doc.md

+27
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,33 @@ func main() {
259259
ids: map[b:hello a:1234]; names: map[second:tianou first:thinkerou]
260260
```
261261

262+
### Query string param as nested map
263+
264+
```sh
265+
GET /get?page[number]=1&page[size]=50&page[sort][by]=id&page[sort][order]=asc HTTP/1.1
266+
```
267+
268+
```go
269+
func main() {
270+
router := gin.Default()
271+
272+
router.GET("/get", func(c *gin.Context) {
273+
274+
paging := c.QueryNestedMap("page")
275+
276+
fmt.Printf("paging: %v\n", paging)
277+
c.JSON(200, paging)
278+
})
279+
280+
router.Run(":8080")
281+
}
282+
```
283+
284+
```sh
285+
paging: map[number:1 size:50 sort:map[by:id order:asc]]
286+
```
287+
288+
262289
### Upload files
263290
264291
#### Single file

internal/query/map.go

+85
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package query
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
)
7+
8+
// GetMap returns a map, which satisfies conditions.
9+
func GetMap(query map[string][]string, key string) (dicts map[string]interface{}, exists bool) {
10+
result := make(map[string]interface{})
11+
for qk, value := range query {
12+
if isKey(qk, key) {
13+
exists = true
14+
path, err := parsePath(qk, key)
15+
if err != nil {
16+
exists = false
17+
continue
18+
}
19+
setValueOnPath(result, path, value)
20+
}
21+
}
22+
if !exists {
23+
return nil, exists
24+
}
25+
return result, exists
26+
27+
}
28+
29+
// isKey is an internal function to check if a k is a map key.
30+
func isKey(k string, key string) bool {
31+
i := strings.IndexByte(k, '[')
32+
return i >= 1 && k[0:i] == key
33+
}
34+
35+
// parsePath is an internal function to parse key access path.
36+
// For example, key[foo][bar] will be parsed to ["foo", "bar"].
37+
func parsePath(k string, key string) ([]string, error) {
38+
rawPath := strings.TrimPrefix(k, key)
39+
if rawPath == "" {
40+
return nil, fmt.Errorf("expect %s to be a map but got value", key)
41+
}
42+
splitted := strings.Split(rawPath, "]")
43+
paths := make([]string, 0)
44+
for _, p := range splitted {
45+
if p == "" {
46+
continue
47+
}
48+
if strings.HasPrefix(p, "[") {
49+
p = p[1:]
50+
} else {
51+
return nil, fmt.Errorf("invalid access to map key %s", p)
52+
}
53+
if p == "" {
54+
return nil, fmt.Errorf("expect %s to be a map but got array", key)
55+
}
56+
paths = append(paths, p)
57+
}
58+
return paths, nil
59+
}
60+
61+
// setValueOnPath is an internal function to set value a path on dicts.
62+
func setValueOnPath(dicts map[string]interface{}, paths []string, value []string) {
63+
nesting := len(paths)
64+
currentLevel := dicts
65+
for i, p := range paths {
66+
if isLast(i, nesting) {
67+
currentLevel[p] = value[0]
68+
} else {
69+
initNestingIfNotExists(currentLevel, p)
70+
currentLevel = currentLevel[p].(map[string]interface{})
71+
}
72+
}
73+
}
74+
75+
// initNestingIfNotExists is an internal function to initialize a nested map if not exists.
76+
func initNestingIfNotExists(currentLevel map[string]interface{}, p string) {
77+
if _, ok := currentLevel[p]; !ok {
78+
currentLevel[p] = make(map[string]interface{})
79+
}
80+
}
81+
82+
// isLast is an internal function to check if the current level is the last level.
83+
func isLast(i int, nesting int) bool {
84+
return i == nesting-1
85+
}

0 commit comments

Comments
 (0)