Skip to content

Commit 8a09e3e

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

File tree

4 files changed

+588
-0
lines changed

4 files changed

+588
-0
lines changed

context.go

Lines changed: 15 additions & 0 deletions
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,20 @@ func (c *Context) GetQueryMap(key string) (map[string]string, bool) {
504505
return c.get(c.queryCache, key)
505506
}
506507

508+
// ShouldGetQueryNestedMap returns a map from query params.
509+
// In contrast to QueryMap it handles nesting in query maps like key[foo][bar]=value.
510+
func (c *Context) ShouldGetQueryNestedMap() (dicts map[string]interface{}, err error) {
511+
return c.ShouldGetQueryNestedMapForKey("")
512+
}
513+
514+
// ShouldGetQueryNestedMapForKey returns a map from query params for a given query key.
515+
// In contrast to QueryMap it handles nesting in query maps like key[foo][bar]=value.
516+
// Similar to ShouldGetQueryNestedMap but it returns only the map for the given key.
517+
func (c *Context) ShouldGetQueryNestedMapForKey(key string) (dicts map[string]interface{}, err error) {
518+
q := c.Request.URL.Query()
519+
return query.GetMap(q, key)
520+
}
521+
507522
// PostForm returns the specified key from a POST urlencoded form or multipart form
508523
// when it exists, otherwise it returns an empty string `("")`.
509524
func (c *Context) PostForm(key string) (value string) {

context_test.go

Lines changed: 294 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -574,6 +574,300 @@ func TestContextQueryAndPostForm(t *testing.T) {
574574
assert.Empty(t, dicts)
575575
}
576576

577+
func TestContextShouldGetQueryNestedMapSuccessfulParsing(t *testing.T) {
578+
var emptyQueryMap map[string]interface{}
579+
580+
tests := map[string]struct {
581+
url string
582+
expectedResult map[string]interface{}
583+
}{
584+
"no query params": {
585+
url: "",
586+
expectedResult: emptyQueryMap,
587+
},
588+
"single query param": {
589+
url: "?foo=bar",
590+
expectedResult: map[string]interface{}{
591+
"foo": "bar",
592+
},
593+
},
594+
"multiple query param": {
595+
url: "?foo=bar&mapkey=value1",
596+
expectedResult: map[string]interface{}{
597+
"foo": "bar",
598+
"mapkey": "value1",
599+
},
600+
},
601+
"map query param": {
602+
url: "?mapkey[key]=value",
603+
expectedResult: map[string]interface{}{
604+
"mapkey": map[string]interface{}{
605+
"key": "value",
606+
},
607+
},
608+
},
609+
"nested map query param": {
610+
url: "?mapkey[key][nested][moreNested]=value",
611+
expectedResult: map[string]interface{}{
612+
"mapkey": map[string]interface{}{
613+
"key": map[string]interface{}{
614+
"nested": map[string]interface{}{
615+
"moreNested": "value",
616+
},
617+
},
618+
},
619+
},
620+
},
621+
"map query param with explicit arrays accessors ([]) at the value level will return array": {
622+
url: "?mapkey[key][]=value1&mapkey[key][]=value2",
623+
expectedResult: map[string]interface{}{
624+
"mapkey": map[string]interface{}{
625+
"key": []string{"value1", "value2"},
626+
},
627+
},
628+
},
629+
"map query param with implicit arrays (duplicated key) at the value level will return only first value": {
630+
url: "?mapkey[key]=value1&mapkey[key]=value2",
631+
expectedResult: map[string]interface{}{
632+
"mapkey": map[string]interface{}{
633+
"key": "value1",
634+
},
635+
},
636+
},
637+
"array query param": {
638+
url: "?mapkey[]=value1&mapkey[]=value2",
639+
expectedResult: map[string]interface{}{
640+
"mapkey": []string{"value1", "value2"},
641+
},
642+
},
643+
}
644+
for name, test := range tests {
645+
t.Run(name, func(t *testing.T) {
646+
u, err := url.Parse(test.url)
647+
require.NoError(t, err)
648+
649+
c := &Context{
650+
Request: &http.Request{
651+
URL: u,
652+
},
653+
}
654+
655+
dicts, err := c.ShouldGetQueryNestedMap()
656+
require.Equal(t, test.expectedResult, dicts)
657+
require.NoError(t, err)
658+
})
659+
}
660+
}
661+
662+
func TestContextShouldGetQueryNestedMapParsingError(t *testing.T) {
663+
tests := map[string]struct {
664+
url string
665+
expectedResult map[string]interface{}
666+
error string
667+
}{
668+
"searched map key with invalid map access": {
669+
url: "?mapkey[key]nested=value",
670+
error: "invalid access to map key",
671+
},
672+
"searched map key with array accessor in the middle": {
673+
url: "?mapkey[key][][nested]=value",
674+
error: "unsupported array-like access to map key",
675+
},
676+
}
677+
for name, test := range tests {
678+
t.Run(name, func(t *testing.T) {
679+
u, err := url.Parse(test.url)
680+
require.NoError(t, err)
681+
682+
c := &Context{
683+
Request: &http.Request{
684+
URL: u,
685+
},
686+
}
687+
688+
dicts, err := c.ShouldGetQueryNestedMap()
689+
require.Nil(t, dicts)
690+
require.ErrorContains(t, err, test.error)
691+
})
692+
}
693+
}
694+
695+
func TestContextShouldGetQueryNestedForKeySuccessfulParsing(t *testing.T) {
696+
var emptyQueryMap map[string]interface{}
697+
698+
tests := map[string]struct {
699+
url string
700+
key string
701+
expectedResult map[string]interface{}
702+
}{
703+
"no searched map key in query string": {
704+
url: "?foo=bar",
705+
key: "mapkey",
706+
expectedResult: emptyQueryMap,
707+
},
708+
"searched map key after other query params": {
709+
url: "?foo=bar&mapkey[key]=value",
710+
key: "mapkey",
711+
expectedResult: map[string]interface{}{
712+
"key": "value",
713+
},
714+
},
715+
"searched map key before other query params": {
716+
url: "?mapkey[key]=value&foo=bar",
717+
key: "mapkey",
718+
expectedResult: map[string]interface{}{
719+
"key": "value",
720+
},
721+
},
722+
"single key in searched map key": {
723+
url: "?mapkey[key]=value",
724+
key: "mapkey",
725+
expectedResult: map[string]interface{}{
726+
"key": "value",
727+
},
728+
},
729+
"multiple keys in searched map key": {
730+
url: "?mapkey[key1]=value1&mapkey[key2]=value2&mapkey[key3]=value3",
731+
key: "mapkey",
732+
expectedResult: map[string]interface{}{
733+
"key1": "value1",
734+
"key2": "value2",
735+
"key3": "value3",
736+
},
737+
},
738+
"nested key in searched map key": {
739+
url: "?mapkey[foo][nested]=value1",
740+
key: "mapkey",
741+
expectedResult: map[string]interface{}{
742+
"foo": map[string]interface{}{
743+
"nested": "value1",
744+
},
745+
},
746+
},
747+
"multiple nested keys in single key of searched map key": {
748+
url: "?mapkey[foo][nested1]=value1&mapkey[foo][nested2]=value2",
749+
key: "mapkey",
750+
expectedResult: map[string]interface{}{
751+
"foo": map[string]interface{}{
752+
"nested1": "value1",
753+
"nested2": "value2",
754+
},
755+
},
756+
},
757+
"multiple keys with nested keys of searched map key": {
758+
url: "?mapkey[key1][nested]=value1&mapkey[key2][nested]=value2",
759+
key: "mapkey",
760+
expectedResult: map[string]interface{}{
761+
"key1": map[string]interface{}{
762+
"nested": "value1",
763+
},
764+
"key2": map[string]interface{}{
765+
"nested": "value2",
766+
},
767+
},
768+
},
769+
"multiple levels of nesting in searched map key": {
770+
url: "?mapkey[key][nested][moreNested]=value1",
771+
key: "mapkey",
772+
expectedResult: map[string]interface{}{
773+
"key": map[string]interface{}{
774+
"nested": map[string]interface{}{
775+
"moreNested": "value1",
776+
},
777+
},
778+
},
779+
},
780+
"query keys similar to searched map key": {
781+
url: "?mapkey[key]=value&mapkeys[key1]=value1&mapkey1=foo",
782+
key: "mapkey",
783+
expectedResult: map[string]interface{}{
784+
"key": "value",
785+
},
786+
},
787+
"handle explicit arrays accessors ([]) at the value level": {
788+
url: "?mapkey[key][]=value1&mapkey[key][]=value2",
789+
key: "mapkey",
790+
expectedResult: map[string]interface{}{
791+
"key": []string{"value1", "value2"},
792+
},
793+
},
794+
"implicit arrays (duplicated key) at the value level will return only first value": {
795+
url: "?mapkey[key]=value1&mapkey[key]=value2",
796+
key: "mapkey",
797+
expectedResult: map[string]interface{}{
798+
"key": "value1",
799+
},
800+
},
801+
}
802+
for name, test := range tests {
803+
t.Run(name, func(t *testing.T) {
804+
u, err := url.Parse(test.url)
805+
require.NoError(t, err)
806+
807+
c := &Context{
808+
Request: &http.Request{
809+
URL: u,
810+
},
811+
}
812+
813+
dicts, err := c.ShouldGetQueryNestedMapForKey(test.key)
814+
require.Equal(t, test.expectedResult, dicts)
815+
require.NoError(t, err)
816+
})
817+
}
818+
}
819+
820+
func TestContextShouldGetQueryNestedForKeyParsingError(t *testing.T) {
821+
tests := map[string]struct {
822+
url string
823+
key string
824+
error string
825+
}{
826+
827+
"searched map key is value not a map": {
828+
url: "?mapkey=value",
829+
key: "mapkey",
830+
error: "invalid access to map",
831+
},
832+
"searched map key is array": {
833+
url: "?mapkey[]=value1&mapkey[]=value2",
834+
key: "mapkey",
835+
error: "invalid access to map",
836+
},
837+
"searched map key with invalid map access": {
838+
url: "?mapkey[key]nested=value",
839+
key: "mapkey",
840+
error: "invalid access to map key",
841+
},
842+
"searched map key with valid and invalid map access": {
843+
url: "?mapkey[key]invalidNested=value&mapkey[key][nested]=value1",
844+
key: "mapkey",
845+
error: "invalid access to map key",
846+
},
847+
"searched map key with valid before invalid map access": {
848+
url: "?mapkey[key][nested]=value1&mapkey[key]invalidNested=value",
849+
key: "mapkey",
850+
error: "invalid access to map key",
851+
},
852+
}
853+
for name, test := range tests {
854+
t.Run(name, func(t *testing.T) {
855+
u, err := url.Parse(test.url)
856+
require.NoError(t, err)
857+
858+
c := &Context{
859+
Request: &http.Request{
860+
URL: u,
861+
},
862+
}
863+
864+
dicts, err := c.ShouldGetQueryNestedMapForKey(test.key)
865+
require.Nil(t, dicts)
866+
require.ErrorContains(t, err, test.error)
867+
})
868+
}
869+
}
870+
577871
func TestContextPostFormMultipart(t *testing.T) {
578872
c, _ := CreateTestContext(httptest.NewRecorder())
579873
c.Request = createMultipartRequest()

0 commit comments

Comments
 (0)