mirror of
https://github.com/gin-gonic/gin.git
synced 2025-05-22 20:41:24 +08:00
Merge 8f79761032f135a8635c2263a8842cb29825c55c into 674522db91d637d179c16c372d87756ea26fa089
This commit is contained in:
commit
af01c87948
15
context.go
15
context.go
@ -22,6 +22,7 @@ import (
|
|||||||
|
|
||||||
"github.com/gin-contrib/sse"
|
"github.com/gin-contrib/sse"
|
||||||
"github.com/gin-gonic/gin/binding"
|
"github.com/gin-gonic/gin/binding"
|
||||||
|
"github.com/gin-gonic/gin/internal/query"
|
||||||
"github.com/gin-gonic/gin/render"
|
"github.com/gin-gonic/gin/render"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -567,6 +568,20 @@ func (c *Context) GetQueryMap(key string) (map[string]string, bool) {
|
|||||||
return c.get(c.queryCache, key)
|
return c.get(c.queryCache, key)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ShouldGetQueryNestedMap returns a map from query params.
|
||||||
|
// In contrast to QueryMap it handles nesting in query maps like key[foo][bar]=value.
|
||||||
|
func (c *Context) ShouldGetQueryNestedMap() (dicts map[string]interface{}, err error) {
|
||||||
|
return c.ShouldGetQueryNestedMapForKey("")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ShouldGetQueryNestedMapForKey returns a map from query params for a given query key.
|
||||||
|
// In contrast to QueryMap it handles nesting in query maps like key[foo][bar]=value.
|
||||||
|
// Similar to ShouldGetQueryNestedMap but it returns only the map for the given key.
|
||||||
|
func (c *Context) ShouldGetQueryNestedMapForKey(key string) (dicts map[string]interface{}, err error) {
|
||||||
|
q := c.Request.URL.Query()
|
||||||
|
return query.GetMap(q, key)
|
||||||
|
}
|
||||||
|
|
||||||
// PostForm returns the specified key from a POST urlencoded form or multipart form
|
// PostForm returns the specified key from a POST urlencoded form or multipart form
|
||||||
// when it exists, otherwise it returns an empty string `("")`.
|
// when it exists, otherwise it returns an empty string `("")`.
|
||||||
func (c *Context) PostForm(key string) (value string) {
|
func (c *Context) PostForm(key string) (value string) {
|
||||||
|
418
context_test.go
418
context_test.go
@ -28,6 +28,7 @@ import (
|
|||||||
|
|
||||||
"github.com/gin-contrib/sse"
|
"github.com/gin-contrib/sse"
|
||||||
"github.com/gin-gonic/gin/binding"
|
"github.com/gin-gonic/gin/binding"
|
||||||
|
"github.com/gin-gonic/gin/internal/query"
|
||||||
testdata "github.com/gin-gonic/gin/testdata/protoexample"
|
testdata "github.com/gin-gonic/gin/testdata/protoexample"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
@ -767,6 +768,423 @@ func TestContextQueryAndPostForm(t *testing.T) {
|
|||||||
assert.Empty(t, dicts)
|
assert.Empty(t, dicts)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestContextShouldGetQueryNestedMapSuccessfulParsing(t *testing.T) {
|
||||||
|
var emptyQueryMap map[string]any
|
||||||
|
veryDeepNesting := ""
|
||||||
|
currentLv := make(map[string]any)
|
||||||
|
veryDeepNestingResult := currentLv
|
||||||
|
for i := 0; i < query.MaxNestedMapDepth; i++ {
|
||||||
|
currKey := "nested" + strconv.Itoa(i)
|
||||||
|
veryDeepNesting += "[" + currKey + "]"
|
||||||
|
if i == query.MaxNestedMapDepth-1 {
|
||||||
|
currentLv[currKey] = "value"
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
currentLv[currKey] = make(map[string]any)
|
||||||
|
currentLv = currentLv[currKey].(map[string]any)
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := map[string]struct {
|
||||||
|
url string
|
||||||
|
expectedResult map[string]any
|
||||||
|
}{
|
||||||
|
"no query params": {
|
||||||
|
url: "",
|
||||||
|
expectedResult: emptyQueryMap,
|
||||||
|
},
|
||||||
|
"single query param": {
|
||||||
|
url: "?foo=bar",
|
||||||
|
expectedResult: map[string]any{
|
||||||
|
"foo": "bar",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"empty key and value": {
|
||||||
|
url: "?=",
|
||||||
|
expectedResult: map[string]any{
|
||||||
|
"": "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"empty key with some value value": {
|
||||||
|
url: "?=value",
|
||||||
|
expectedResult: map[string]any{
|
||||||
|
"": "value",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"single key with empty value": {
|
||||||
|
url: "?key=",
|
||||||
|
expectedResult: map[string]any{
|
||||||
|
"key": "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"only keys": {
|
||||||
|
url: "?foo&bar",
|
||||||
|
expectedResult: map[string]any{
|
||||||
|
"foo": "",
|
||||||
|
"bar": "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"encoded & sign in value": {
|
||||||
|
url: "?foo=bar%26baz",
|
||||||
|
expectedResult: map[string]any{
|
||||||
|
"foo": "bar&baz",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"encoded = sign in value": {
|
||||||
|
url: "?foo=bar%3Dbaz",
|
||||||
|
expectedResult: map[string]any{
|
||||||
|
"foo": "bar=baz",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"multiple query param": {
|
||||||
|
url: "?foo=bar&mapkey=value1",
|
||||||
|
expectedResult: map[string]any{
|
||||||
|
"foo": "bar",
|
||||||
|
"mapkey": "value1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"map query param": {
|
||||||
|
url: "?mapkey[key]=value",
|
||||||
|
expectedResult: map[string]any{
|
||||||
|
"mapkey": map[string]any{
|
||||||
|
"key": "value",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"multiple different value types in map query param": {
|
||||||
|
url: "?mapkey[key1]=value1&mapkey[key2]=1&mapkey[key3]=true",
|
||||||
|
expectedResult: map[string]any{
|
||||||
|
"mapkey": map[string]any{
|
||||||
|
"key1": "value1",
|
||||||
|
"key2": "1",
|
||||||
|
"key3": "true",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"multiple different value types in array value of map query param": {
|
||||||
|
url: "?mapkey[key][]=value1&mapkey[key][]=1&mapkey[key][]=true",
|
||||||
|
expectedResult: map[string]any{
|
||||||
|
"mapkey": map[string]any{
|
||||||
|
"key": []string{"value1", "1", "true"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"nested map query param": {
|
||||||
|
url: "?mapkey[key][nested][moreNested]=value",
|
||||||
|
expectedResult: map[string]any{
|
||||||
|
"mapkey": map[string]any{
|
||||||
|
"key": map[string]any{
|
||||||
|
"nested": map[string]any{
|
||||||
|
"moreNested": "value",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"very deep nested map query param": {
|
||||||
|
url: "?mapkey" + veryDeepNesting + "=value",
|
||||||
|
expectedResult: map[string]any{
|
||||||
|
"mapkey": veryDeepNestingResult,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"map query param with explicit arrays accessors ([]) at the value level will return array": {
|
||||||
|
url: "?mapkey[key][]=value1&mapkey[key][]=value2",
|
||||||
|
expectedResult: map[string]any{
|
||||||
|
"mapkey": map[string]any{
|
||||||
|
"key": []string{"value1", "value2"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"map query param with implicit arrays (duplicated key) at the value level will return only first value": {
|
||||||
|
url: "?mapkey[key]=value1&mapkey[key]=value2",
|
||||||
|
expectedResult: map[string]any{
|
||||||
|
"mapkey": map[string]any{
|
||||||
|
"key": "value1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"array query param": {
|
||||||
|
url: "?mapkey[]=value1&mapkey[]=value2",
|
||||||
|
expectedResult: map[string]any{
|
||||||
|
"mapkey": []string{"value1", "value2"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for name, test := range tests {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
u, err := url.Parse(test.url)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
c := &Context{
|
||||||
|
Request: &http.Request{
|
||||||
|
URL: u,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
dicts, err := c.ShouldGetQueryNestedMap()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, test.expectedResult, dicts)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContextShouldGetQueryNestedMapParsingError(t *testing.T) {
|
||||||
|
tooDeepNesting := strings.Repeat("[nested]", query.MaxNestedMapDepth+1)
|
||||||
|
|
||||||
|
tests := map[string]struct {
|
||||||
|
url string
|
||||||
|
expectedResult map[string]any
|
||||||
|
error string
|
||||||
|
}{
|
||||||
|
"searched map key with invalid map access": {
|
||||||
|
url: "?mapkey[key]nested=value",
|
||||||
|
error: "invalid access to map key",
|
||||||
|
},
|
||||||
|
"searched map key with array accessor in the middle": {
|
||||||
|
url: "?mapkey[key][][nested]=value",
|
||||||
|
error: "unsupported array-like access to map key",
|
||||||
|
},
|
||||||
|
"too deep nesting of the map in query params": {
|
||||||
|
url: "?mapkey" + tooDeepNesting + "=value",
|
||||||
|
error: "maximum depth [100] of nesting in map exceeded",
|
||||||
|
},
|
||||||
|
"setting value and nested map at the same level": {
|
||||||
|
url: "?mapkey[key]=value&mapkey[key][nested]=value1",
|
||||||
|
error: "trying to set different types at the same key",
|
||||||
|
},
|
||||||
|
"setting array and nested map at the same level": {
|
||||||
|
url: "?mapkey[key][]=value&mapkey[key][nested]=value1",
|
||||||
|
error: "trying to set different types at the same key",
|
||||||
|
},
|
||||||
|
"setting nested map and array at the same level": {
|
||||||
|
url: "?mapkey[key][nested]=value1&mapkey[key][]=value",
|
||||||
|
error: "trying to set different types at the same key",
|
||||||
|
},
|
||||||
|
"setting array and value at the same level": {
|
||||||
|
url: "?key[]=value1&key=value2",
|
||||||
|
error: "trying to set different types at the same key",
|
||||||
|
},
|
||||||
|
"setting value and array at the same level": {
|
||||||
|
url: "?key=value1&key[]=value2",
|
||||||
|
error: "trying to set different types at the same key",
|
||||||
|
},
|
||||||
|
"setting array and nested map at same query param": {
|
||||||
|
url: "?mapkey[]=value1&mapkey[key]=value2",
|
||||||
|
error: "trying to set different types at the same key",
|
||||||
|
},
|
||||||
|
"setting nested map and array at same query param": {
|
||||||
|
url: "?mapkey[key]=value2&mapkey[]=value1",
|
||||||
|
error: "trying to set different types at the same key",
|
||||||
|
},
|
||||||
|
"] without [ in query param": {
|
||||||
|
url: "?mapkey]=value",
|
||||||
|
error: "invalid access to map key",
|
||||||
|
},
|
||||||
|
"[ without ] in query param": {
|
||||||
|
url: "?mapkey[key=value",
|
||||||
|
error: "invalid access to map key",
|
||||||
|
},
|
||||||
|
"[ without ] in query param with nested key": {
|
||||||
|
url: "?mapkey[key[nested]=value",
|
||||||
|
error: "invalid access to map key",
|
||||||
|
},
|
||||||
|
"[[key]] in query param": {
|
||||||
|
url: "?mapkey[[key]]=value",
|
||||||
|
error: "invalid access to map key",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for name, test := range tests {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
u, err := url.Parse(test.url)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
c := &Context{
|
||||||
|
Request: &http.Request{
|
||||||
|
URL: u,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
dicts, err := c.ShouldGetQueryNestedMap()
|
||||||
|
require.ErrorContains(t, err, test.error)
|
||||||
|
require.Nil(t, dicts)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContextShouldGetQueryNestedForKeySuccessfulParsing(t *testing.T) {
|
||||||
|
var emptyQueryMap map[string]any
|
||||||
|
|
||||||
|
tests := map[string]struct {
|
||||||
|
url string
|
||||||
|
key string
|
||||||
|
expectedResult map[string]any
|
||||||
|
}{
|
||||||
|
"no searched map key in query string": {
|
||||||
|
url: "?foo=bar",
|
||||||
|
key: "mapkey",
|
||||||
|
expectedResult: emptyQueryMap,
|
||||||
|
},
|
||||||
|
"searched map key after other query params": {
|
||||||
|
url: "?foo=bar&mapkey[key]=value",
|
||||||
|
key: "mapkey",
|
||||||
|
expectedResult: map[string]any{
|
||||||
|
"key": "value",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"searched map key before other query params": {
|
||||||
|
url: "?mapkey[key]=value&foo=bar",
|
||||||
|
key: "mapkey",
|
||||||
|
expectedResult: map[string]any{
|
||||||
|
"key": "value",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"single key in searched map key": {
|
||||||
|
url: "?mapkey[key]=value",
|
||||||
|
key: "mapkey",
|
||||||
|
expectedResult: map[string]any{
|
||||||
|
"key": "value",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"multiple keys in searched map key": {
|
||||||
|
url: "?mapkey[key1]=value1&mapkey[key2]=value2&mapkey[key3]=value3",
|
||||||
|
key: "mapkey",
|
||||||
|
expectedResult: map[string]any{
|
||||||
|
"key1": "value1",
|
||||||
|
"key2": "value2",
|
||||||
|
"key3": "value3",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"nested key in searched map key": {
|
||||||
|
url: "?mapkey[foo][nested]=value1",
|
||||||
|
key: "mapkey",
|
||||||
|
expectedResult: map[string]any{
|
||||||
|
"foo": map[string]any{
|
||||||
|
"nested": "value1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"multiple nested keys in single key of searched map key": {
|
||||||
|
url: "?mapkey[foo][nested1]=value1&mapkey[foo][nested2]=value2",
|
||||||
|
key: "mapkey",
|
||||||
|
expectedResult: map[string]any{
|
||||||
|
"foo": map[string]any{
|
||||||
|
"nested1": "value1",
|
||||||
|
"nested2": "value2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"multiple keys with nested keys of searched map key": {
|
||||||
|
url: "?mapkey[key1][nested]=value1&mapkey[key2][nested]=value2",
|
||||||
|
key: "mapkey",
|
||||||
|
expectedResult: map[string]any{
|
||||||
|
"key1": map[string]any{
|
||||||
|
"nested": "value1",
|
||||||
|
},
|
||||||
|
"key2": map[string]any{
|
||||||
|
"nested": "value2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"multiple levels of nesting in searched map key": {
|
||||||
|
url: "?mapkey[key][nested][moreNested]=value1",
|
||||||
|
key: "mapkey",
|
||||||
|
expectedResult: map[string]any{
|
||||||
|
"key": map[string]any{
|
||||||
|
"nested": map[string]any{
|
||||||
|
"moreNested": "value1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"query keys similar to searched map key": {
|
||||||
|
url: "?mapkey[key]=value&mapkeys[key1]=value1&mapkey1=foo",
|
||||||
|
key: "mapkey",
|
||||||
|
expectedResult: map[string]any{
|
||||||
|
"key": "value",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"handle explicit arrays accessors ([]) at the value level": {
|
||||||
|
url: "?mapkey[key][]=value1&mapkey[key][]=value2",
|
||||||
|
key: "mapkey",
|
||||||
|
expectedResult: map[string]any{
|
||||||
|
"key": []string{"value1", "value2"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"implicit arrays (duplicated key) at the value level will return only first value": {
|
||||||
|
url: "?mapkey[key]=value1&mapkey[key]=value2",
|
||||||
|
key: "mapkey",
|
||||||
|
expectedResult: map[string]any{
|
||||||
|
"key": "value1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for name, test := range tests {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
u, err := url.Parse(test.url)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
c := &Context{
|
||||||
|
Request: &http.Request{
|
||||||
|
URL: u,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
dicts, err := c.ShouldGetQueryNestedMapForKey(test.key)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, test.expectedResult, dicts)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContextShouldGetQueryNestedForKeyParsingError(t *testing.T) {
|
||||||
|
tests := map[string]struct {
|
||||||
|
url string
|
||||||
|
key string
|
||||||
|
error string
|
||||||
|
}{
|
||||||
|
|
||||||
|
"searched map key is value not a map": {
|
||||||
|
url: "?mapkey=value",
|
||||||
|
key: "mapkey",
|
||||||
|
error: "invalid access to map",
|
||||||
|
},
|
||||||
|
"searched map key is array": {
|
||||||
|
url: "?mapkey[]=value1&mapkey[]=value2",
|
||||||
|
key: "mapkey",
|
||||||
|
error: "invalid access to map",
|
||||||
|
},
|
||||||
|
"searched map key with invalid map access": {
|
||||||
|
url: "?mapkey[key]nested=value",
|
||||||
|
key: "mapkey",
|
||||||
|
error: "invalid access to map key",
|
||||||
|
},
|
||||||
|
"searched map key with valid and invalid map access": {
|
||||||
|
url: "?mapkey[key]invalidNested=value&mapkey[key][nested]=value1",
|
||||||
|
key: "mapkey",
|
||||||
|
error: "invalid access to map key",
|
||||||
|
},
|
||||||
|
"searched map key with valid before invalid map access": {
|
||||||
|
url: "?mapkey[key][nested]=value1&mapkey[key]invalidNested=value",
|
||||||
|
key: "mapkey",
|
||||||
|
error: "invalid access to map key",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for name, test := range tests {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
u, err := url.Parse(test.url)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
c := &Context{
|
||||||
|
Request: &http.Request{
|
||||||
|
URL: u,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
dicts, err := c.ShouldGetQueryNestedMapForKey(test.key)
|
||||||
|
require.ErrorContains(t, err, test.error)
|
||||||
|
require.Nil(t, dicts)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestContextPostFormMultipart(t *testing.T) {
|
func TestContextPostFormMultipart(t *testing.T) {
|
||||||
c, _ := CreateTestContext(httptest.NewRecorder())
|
c, _ := CreateTestContext(httptest.NewRecorder())
|
||||||
c.Request = createMultipartRequest()
|
c.Request = createMultipartRequest()
|
||||||
|
122
docs/doc.md
122
docs/doc.md
@ -261,6 +261,128 @@ func main() {
|
|||||||
ids: map[b:hello a:1234]; names: map[second:tianou first:thinkerou]
|
ids: map[b:hello a:1234]; names: map[second:tianou first:thinkerou]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Query string param as nested map
|
||||||
|
|
||||||
|
#### Parse query params to nested map
|
||||||
|
|
||||||
|
```sh
|
||||||
|
GET /get?name=alice&page[number]=1&page[sort][order]=asc&created[days][]=5&created[days][]=7 HTTP/1.1
|
||||||
|
```
|
||||||
|
|
||||||
|
Notice that:
|
||||||
|
- client can use standard name=value syntax and this will be mapped to the `key: value` in map
|
||||||
|
- client can use map access syntax (`key[nested][deepNested]=value`) and this will be mapped to the nested map `key:map[nested:map[deepNested:value]]`
|
||||||
|
- client can use array syntax even as value of map key (`key[nested][]=value1&key[nested][]=value2`) and this will be mapped to the array `key:map[nested:[value1 value2]]`
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
router := gin.Default()
|
||||||
|
|
||||||
|
router.GET("/get", func(c *gin.Context) {
|
||||||
|
|
||||||
|
paging, err := c.ShouldGetQueryNestedMap()
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(400, gin.H{ "error": err.Error() })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("query: %v\n", paging)
|
||||||
|
c.JSON(200, paging)
|
||||||
|
})
|
||||||
|
|
||||||
|
router.Run(":8080")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```sh
|
||||||
|
query: map[created:map[days:[5 7]] name:alice page:map[number:1 sort:map[order:asc]]]
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Extract key as nested map
|
||||||
|
|
||||||
|
```sh
|
||||||
|
GET /get?page[number]=1&page[size]=50&page[sort][by]=id&page[sort][order]=asc HTTP/1.1
|
||||||
|
```
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
router := gin.Default()
|
||||||
|
|
||||||
|
router.GET("/get", func(c *gin.Context) {
|
||||||
|
|
||||||
|
paging, err := c.ShouldGetQueryNestedMapForKey("page")
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(400, gin.H{ "error": err.Error() })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
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]]
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Extract key as nested map with array as values
|
||||||
|
|
||||||
|
It is possible to get the array values from the query string as well.
|
||||||
|
But the client need to use array syntax in the query string.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
GET /get?filter[names][]=alice&filter[names][]=bob&filter[status]=new&filter[status]=old HTTP/1.1
|
||||||
|
```
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
router := gin.Default()
|
||||||
|
|
||||||
|
router.GET("/get", func(c *gin.Context) {
|
||||||
|
|
||||||
|
filters, err := c.ShouldGetQueryNestedMapForKey("filter")
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(400, gin.H{ "error": err.Error() })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("filters: %v\n", filters)
|
||||||
|
c.JSON(200, filters)
|
||||||
|
})
|
||||||
|
|
||||||
|
router.Run(":8080")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```sh
|
||||||
|
filters: map[names:[alice bob] status:new]
|
||||||
|
```
|
||||||
|
Notice that status has only one value because it is not an explicit array.
|
||||||
|
|
||||||
### Upload files
|
### Upload files
|
||||||
|
|
||||||
#### Single file
|
#### Single file
|
||||||
|
214
internal/query/map.go
Normal file
214
internal/query/map.go
Normal file
@ -0,0 +1,214 @@
|
|||||||
|
package query
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MaxNestedMapDepth is the maximum depth of nesting of single map key in query params.
|
||||||
|
const MaxNestedMapDepth = 100
|
||||||
|
|
||||||
|
type queryKeyType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
filteredUnsupported queryKeyType = iota
|
||||||
|
filteredMap
|
||||||
|
filteredRejected
|
||||||
|
mapType
|
||||||
|
arrayType
|
||||||
|
emptyKeyValue
|
||||||
|
valueType
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetMap returns a map, which satisfies conditions.
|
||||||
|
func GetMap(query map[string][]string, filteredKey string) (map[string]interface{}, error) {
|
||||||
|
result := make(map[string]interface{})
|
||||||
|
getAll := filteredKey == ""
|
||||||
|
var allErrors = make([]error, 0)
|
||||||
|
for key, value := range query {
|
||||||
|
kType := getType(key, filteredKey, getAll)
|
||||||
|
switch kType {
|
||||||
|
case filteredUnsupported:
|
||||||
|
allErrors = append(allErrors, fmt.Errorf("invalid access to map %s", key))
|
||||||
|
continue
|
||||||
|
case filteredMap:
|
||||||
|
fallthrough
|
||||||
|
case mapType:
|
||||||
|
path, err := parsePath(key)
|
||||||
|
if err != nil {
|
||||||
|
allErrors = append(allErrors, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !getAll {
|
||||||
|
path = path[1:]
|
||||||
|
}
|
||||||
|
err = setValueOnPath(result, path, value)
|
||||||
|
if err != nil {
|
||||||
|
allErrors = append(allErrors, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
case arrayType:
|
||||||
|
err := setValueOnPath(result, []string{keyWithoutArraySymbol(key), ""}, value)
|
||||||
|
if err != nil {
|
||||||
|
allErrors = append(allErrors, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
case filteredRejected:
|
||||||
|
continue
|
||||||
|
case emptyKeyValue:
|
||||||
|
result[key] = value[0]
|
||||||
|
case valueType:
|
||||||
|
fallthrough
|
||||||
|
default:
|
||||||
|
err := setValueOnPath(result, []string{key}, value)
|
||||||
|
if err != nil {
|
||||||
|
allErrors = append(allErrors, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(allErrors) > 0 {
|
||||||
|
return nil, errors.Join(allErrors...)
|
||||||
|
}
|
||||||
|
if len(result) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getType is an internal function to get the type of query key.
|
||||||
|
func getType(key string, filteredKey string, getAll bool) queryKeyType {
|
||||||
|
if getAll {
|
||||||
|
if isMap(key) {
|
||||||
|
return mapType
|
||||||
|
}
|
||||||
|
if isArray(key) {
|
||||||
|
return arrayType
|
||||||
|
}
|
||||||
|
if key == "" {
|
||||||
|
return emptyKeyValue
|
||||||
|
}
|
||||||
|
return valueType
|
||||||
|
}
|
||||||
|
if isFilteredKey(key, filteredKey) {
|
||||||
|
if isMap(key) {
|
||||||
|
return filteredMap
|
||||||
|
}
|
||||||
|
return filteredUnsupported
|
||||||
|
}
|
||||||
|
return filteredRejected
|
||||||
|
}
|
||||||
|
|
||||||
|
// isFilteredKey is an internal function to check if k is accepted when searching for map with given key.
|
||||||
|
func isFilteredKey(k string, filteredKey string) bool {
|
||||||
|
return k == filteredKey || strings.HasPrefix(k, filteredKey+"[")
|
||||||
|
}
|
||||||
|
|
||||||
|
// isMap is an internal function to check if k is a map query key.
|
||||||
|
func isMap(k string) bool {
|
||||||
|
i := strings.IndexByte(k, '[')
|
||||||
|
j := strings.IndexByte(k, ']')
|
||||||
|
return j-i > 1 || (i >= 0 && j == -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// isArray is an internal function to check if k is an array query key.
|
||||||
|
func isArray(k string) bool {
|
||||||
|
i := strings.IndexByte(k, '[')
|
||||||
|
j := strings.IndexByte(k, ']')
|
||||||
|
return j-i == 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// keyWithoutArraySymbol is an internal function to remove array symbol from query key.
|
||||||
|
func keyWithoutArraySymbol(key string) string {
|
||||||
|
return key[:len(key)-2]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) ([]string, error) {
|
||||||
|
firstKeyEnd := strings.IndexByte(k, '[')
|
||||||
|
if firstKeyEnd == -1 {
|
||||||
|
return nil, fmt.Errorf("invalid access to map key %s", k)
|
||||||
|
}
|
||||||
|
first, rawPath := k[:firstKeyEnd], k[firstKeyEnd:]
|
||||||
|
|
||||||
|
split := strings.Split(rawPath, "]")
|
||||||
|
|
||||||
|
// Bear in mind that split of the valid map will always have "" as the last element.
|
||||||
|
if split[len(split)-1] != "" {
|
||||||
|
return nil, fmt.Errorf("invalid access to map key %s", k)
|
||||||
|
}
|
||||||
|
if len(split)-1 > MaxNestedMapDepth {
|
||||||
|
return nil, fmt.Errorf("maximum depth [%d] of nesting in map exceeded [%d]", MaxNestedMapDepth, len(split)-1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// -2 because after split the last element should be empty string.
|
||||||
|
last := len(split) - 2
|
||||||
|
|
||||||
|
paths := []string{first}
|
||||||
|
for i := 0; i <= last; i++ {
|
||||||
|
p := split[i]
|
||||||
|
|
||||||
|
// this way we can handle both error cases: foo] and [foo[bar
|
||||||
|
if strings.LastIndex(p, "[") == 0 {
|
||||||
|
p = p[1:]
|
||||||
|
} else {
|
||||||
|
return nil, fmt.Errorf("invalid access to map key %s", p)
|
||||||
|
}
|
||||||
|
if p == "" && i != last {
|
||||||
|
return nil, fmt.Errorf("unsupported array-like access to map key %s", k)
|
||||||
|
}
|
||||||
|
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) error {
|
||||||
|
nesting := len(paths)
|
||||||
|
previousLevel := dicts
|
||||||
|
currentLevel := dicts
|
||||||
|
for i, p := range paths {
|
||||||
|
if isLast(i, nesting) {
|
||||||
|
// handle setting value
|
||||||
|
if isArrayOnPath(p) {
|
||||||
|
previousLevel[paths[i-1]] = value
|
||||||
|
} else {
|
||||||
|
// if there was already a value set, then it is an error to set a different value.
|
||||||
|
if _, ok := currentLevel[p]; ok {
|
||||||
|
return fmt.Errorf("trying to set different types at the same key [%s]", p)
|
||||||
|
}
|
||||||
|
currentLevel[p] = value[0]
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// handle subpath of the map
|
||||||
|
switch currentLevel[p].(type) {
|
||||||
|
case map[string]any:
|
||||||
|
// if there was map, and we have to set array, then it is an error
|
||||||
|
if isArrayOnPath(paths[i+1]) {
|
||||||
|
return fmt.Errorf("trying to set different types at the same key [%s]", p)
|
||||||
|
}
|
||||||
|
case nil:
|
||||||
|
// initialize map if it is not set here yet
|
||||||
|
currentLevel[p] = make(map[string]any)
|
||||||
|
default:
|
||||||
|
// if there was different value then a map, then it is an error to set a map here.
|
||||||
|
return fmt.Errorf("trying to set different types at the same key [%s]", p)
|
||||||
|
}
|
||||||
|
previousLevel = currentLevel
|
||||||
|
currentLevel = currentLevel[p].(map[string]any)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// isArrayOnPath is an internal function to check if the current parsed map path is an array.
|
||||||
|
func isArrayOnPath(p string) bool {
|
||||||
|
return p == ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user