Skip to content

Commit

Permalink
feat(context): methods to get nested map from query string
Browse files Browse the repository at this point in the history
  • Loading branch information
dorzepowski committed Aug 14, 2024
1 parent cc4e114 commit 256e3f8
Show file tree
Hide file tree
Showing 4 changed files with 274 additions and 0 deletions.
16 changes: 16 additions & 0 deletions context.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (

"github.com/gin-contrib/sse"
"github.com/gin-gonic/gin/binding"
"github.com/gin-gonic/gin/internal/query"
"github.com/gin-gonic/gin/render"
)

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

// QueryNestedMap returns a map for a given query key.
// In contrast to QueryMap it handles nesting in query maps like key[foo][bar]=value.
func (c *Context) QueryNestedMap(key string) (dicts map[string]interface{}) {
dicts, _ = c.GetQueryNestedMap(key)
return
}

// GetQueryNestedMap returns a map for a given query key, plus a boolean value
// whether at least one value exists for the given key.
// In contrast to GetQueryMap it handles nesting in query maps like key[foo][bar]=value.
func (c *Context) GetQueryNestedMap(key string) (map[string]interface{}, bool) {
c.initQueryCache()
return query.GetMap(c.queryCache, key)
}

// PostForm returns the specified key from a POST urlencoded form or multipart form
// when it exists, otherwise it returns an empty string `("")`.
func (c *Context) PostForm(key string) (value string) {
Expand Down
146 changes: 146 additions & 0 deletions context_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -574,6 +574,152 @@ func TestContextQueryAndPostForm(t *testing.T) {
assert.Empty(t, dicts)
}

func TestContextQueryNestedMap(t *testing.T) {
var emptyQueryMap map[string]interface{}

tests := map[string]struct {
url string
expectedResult map[string]interface{}
exists bool
}{
"no searched map key in query string": {
url: "?foo=bar",
expectedResult: emptyQueryMap,
exists: false,
},
"searched map key is not a map": {
url: "?mapkey=value",
expectedResult: emptyQueryMap,
exists: false,
},
"searched map key is array": {
url: "?mapkey[]=value1&mapkey[]=value2",
expectedResult: emptyQueryMap,
exists: false,
},
"searched map key with invalid map access": {
url: "?mapkey[key]nested=value",
expectedResult: emptyQueryMap,
exists: false,
},
"searched map key with valid and invalid map access": {
url: "?mapkey[key]invalidNested=value&mapkey[key][nested]=value1",
expectedResult: map[string]interface{}{
"key": map[string]interface{}{
"nested": "value1",
},
},
exists: true,
},
"searched map key after other query params": {
url: "?foo=bar&mapkey[key]=value",
expectedResult: map[string]interface{}{
"key": "value",
},
exists: true,
},
"searched map key before other query params": {
url: "?mapkey[key]=value&foo=bar",
expectedResult: map[string]interface{}{
"key": "value",
},
exists: true,
},
"single key in searched map key": {
url: "?mapkey[key]=value",
expectedResult: map[string]interface{}{
"key": "value",
},
exists: true,
},
"multiple keys in searched map key": {
url: "?mapkey[key1]=value1&mapkey[key2]=value2&mapkey[key3]=value3",
expectedResult: map[string]interface{}{
"key1": "value1",
"key2": "value2",
"key3": "value3",
},
exists: true,
},
"nested key in searched map key": {
url: "?mapkey[foo][nested]=value1",
expectedResult: map[string]interface{}{
"foo": map[string]interface{}{
"nested": "value1",
},
},
exists: true,
},
"multiple nested keys in single key of searched map key": {
url: "?mapkey[foo][nested1]=value1&mapkey[foo][nested2]=value2",
expectedResult: map[string]interface{}{
"foo": map[string]interface{}{
"nested1": "value1",
"nested2": "value2",
},
},
exists: true,
},
"multiple keys with nested keys of searched map key": {
url: "?mapkey[key1][nested]=value1&mapkey[key2][nested]=value2",
expectedResult: map[string]interface{}{
"key1": map[string]interface{}{
"nested": "value1",
},
"key2": map[string]interface{}{
"nested": "value2",
},
},
exists: true,
},
"multiple levels of nesting in searched map key": {
url: "?mapkey[key][nested][moreNested]=value1",
expectedResult: map[string]interface{}{
"key": map[string]interface{}{
"nested": map[string]interface{}{
"moreNested": "value1",
},
},
},
exists: true,
},
"query keys similar to searched map key": {
url: "?mapkey[key]=value&mapkeys[key1]=value1&mapkey1=foo",
expectedResult: map[string]interface{}{
"key": "value",
},
exists: true,
},
}
for name, test := range tests {
t.Run("getQueryMap: "+name, func(t *testing.T) {
u, err := url.Parse(test.url)
require.NoError(t, err)

c := Context{
Request: &http.Request{
URL: u,
},
}
dicts, exists := c.GetQueryNestedMap("mapkey")
require.Equal(t, test.expectedResult, dicts)
require.Equal(t, test.exists, exists)
})
t.Run("queryMap: "+name, func(t *testing.T) {
u, err := url.Parse(test.url)
require.NoError(t, err)

c := Context{
Request: &http.Request{
URL: u,
},
}
dicts := c.QueryNestedMap("mapkey")
require.Equal(t, test.expectedResult, dicts)
})
}
}

func TestContextPostFormMultipart(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
c.Request = createMultipartRequest()
Expand Down
27 changes: 27 additions & 0 deletions docs/doc.md
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,33 @@ func main() {
ids: map[b:hello a:1234]; names: map[second:tianou first:thinkerou]
```

### Query string param as nested map

```sh
GET /get?page[number]=1&page[size]=50&page[sort][by]=id&page[sort][order]=asc HTTP/1.1
```

```go
func main() {
router := gin.Default()

router.GET("/get", func(c *gin.Context) {

paging := c.QueryNestedMap("page")

fmt.Printf("paging: %v\n", paging)
c.JSON(200, paging)
})

router.Run(":8080")
}
```

```sh
paging: map[number:1 size:50 sort:map[by:id order:asc]]
```
### Upload files
#### Single file
Expand Down
85 changes: 85 additions & 0 deletions internal/query/map.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package query

import (
"fmt"
"strings"
)

// GetMap returns a map, which satisfies conditions.
func GetMap(query map[string][]string, key string) (dicts map[string]interface{}, exists bool) {
result := make(map[string]interface{})
for qk, value := range query {
if isKey(qk, key) {
exists = true
path, err := parsePath(qk, key)
if err != nil {
exists = false
continue
}
setValueOnPath(result, path, value)
}
}
if !exists {
return nil, exists
}
return result, exists

}

// isKey is an internal function to check if a k is a map key.
func isKey(k string, key string) bool {
i := strings.IndexByte(k, '[')
return i >= 1 && k[0:i] == key
}

// parsePath is an internal function to parse key access path.
// For example, key[foo][bar] will be parsed to ["foo", "bar"].
func parsePath(k string, key string) ([]string, error) {
rawPath := strings.TrimPrefix(k, key)
if rawPath == "" {
return nil, fmt.Errorf("expect %s to be a map but got value", key)
}
splitted := strings.Split(rawPath, "]")
paths := make([]string, 0)
for _, p := range splitted {
if p == "" {
continue
}
if strings.HasPrefix(p, "[") {
p = p[1:]
} else {
return nil, fmt.Errorf("invalid access to map key %s", p)
}
if p == "" {
return nil, fmt.Errorf("expect %s to be a map but got array", key)
}
paths = append(paths, p)
}
return paths, nil
}

// setValueOnPath is an internal function to set value a path on dicts.
func setValueOnPath(dicts map[string]interface{}, paths []string, value []string) {
nesting := len(paths)
currentLevel := dicts
for i, p := range paths {
if isLast(i, nesting) {
currentLevel[p] = value[0]
} else {
initNestingIfNotExists(currentLevel, p)
currentLevel = currentLevel[p].(map[string]interface{})
}
}
}

// initNestingIfNotExists is an internal function to initialize a nested map if not exists.
func initNestingIfNotExists(currentLevel map[string]interface{}, p string) {
if _, ok := currentLevel[p]; !ok {
currentLevel[p] = make(map[string]interface{})
}
}

// isLast is an internal function to check if the current level is the last level.
func isLast(i int, nesting int) bool {
return i == nesting-1
}

0 comments on commit 256e3f8

Please sign in to comment.