Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow bind with a map[string]string #2484

Merged
merged 4 commits into from Oct 30, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
93 changes: 93 additions & 0 deletions binding/binding_test.go
Expand Up @@ -13,6 +13,7 @@ import (
"mime/multipart"
"net/http"
"os"
"reflect"
"strconv"
"strings"
"testing"
Expand Down Expand Up @@ -200,6 +201,12 @@ func TestBindingJSONDisallowUnknownFields(t *testing.T) {
`{"foo": "bar"}`, `{"foo": "bar", "what": "this"}`)
}

func TestBindingJSONStringMap(t *testing.T) {
testBodyBindingStringMap(t, JSON,
"/", "/",
`{"foo": "bar", "hello": "world"}`, `{"num": 2}`)
}

func TestBindingForm(t *testing.T) {
testFormBinding(t, "POST",
"/", "/",
Expand Down Expand Up @@ -336,6 +343,37 @@ func TestBindingFormForType(t *testing.T) {
"", "", "StructPointer")
}

func TestBindingFormStringMap(t *testing.T) {
testBodyBindingStringMap(t, Form,
"/", "",
`foo=bar&hello=world`, "")
// Should pick the last value
testBodyBindingStringMap(t, Form,
"/", "",
`foo=something&foo=bar&hello=world`, "")
}

func TestBindingFormStringSliceMap(t *testing.T) {
obj := make(map[string][]string)
req := requestWithBody("POST", "/", "foo=something&foo=bar&hello=world")
req.Header.Add("Content-Type", MIMEPOSTForm)
err := Form.Bind(req, &obj)
assert.NoError(t, err)
assert.NotNil(t, obj)
assert.Len(t, obj, 2)
target := map[string][]string{
"foo": {"something", "bar"},
"hello": {"world"},
}
assert.True(t, reflect.DeepEqual(obj, target))

objInvalid := make(map[string][]int)
req = requestWithBody("POST", "/", "foo=something&foo=bar&hello=world")
req.Header.Add("Content-Type", MIMEPOSTForm)
err = Form.Bind(req, &objInvalid)
assert.Error(t, err)
}

func TestBindingQuery(t *testing.T) {
testQueryBinding(t, "POST",
"/?foo=bar&bar=foo", "/",
Expand Down Expand Up @@ -366,6 +404,28 @@ func TestBindingQueryBoolFail(t *testing.T) {
"bool_foo=unused", "")
}

func TestBindingQueryStringMap(t *testing.T) {
b := Query

obj := make(map[string]string)
req := requestWithBody("GET", "/?foo=bar&hello=world", "")
err := b.Bind(req, &obj)
assert.NoError(t, err)
assert.NotNil(t, obj)
assert.Len(t, obj, 2)
assert.Equal(t, "bar", obj["foo"])
assert.Equal(t, "world", obj["hello"])

obj = make(map[string]string)
req = requestWithBody("GET", "/?foo=bar&foo=2&hello=world", "") // should pick last
err = b.Bind(req, &obj)
assert.NoError(t, err)
assert.NotNil(t, obj)
assert.Len(t, obj, 2)
assert.Equal(t, "2", obj["foo"])
assert.Equal(t, "world", obj["hello"])
}

func TestBindingXML(t *testing.T) {
testBodyBinding(t,
XML, "xml",
Expand All @@ -387,6 +447,13 @@ func TestBindingYAML(t *testing.T) {
`foo: bar`, `bar: foo`)
}

func TestBindingYAMLStringMap(t *testing.T) {
// YAML is a superset of JSON, so the test below is JSON (to avoid newlines)
testBodyBindingStringMap(t, YAML,
"/", "/",
`{"foo": "bar", "hello": "world"}`, `{"nested": {"foo": "bar"}}`)
}

func TestBindingYAMLFail(t *testing.T) {
testBodyBindingFail(t,
YAML, "yaml",
Expand Down Expand Up @@ -1114,6 +1181,32 @@ func testBodyBinding(t *testing.T, b Binding, name, path, badPath, body, badBody
assert.Error(t, err)
}

func testBodyBindingStringMap(t *testing.T, b Binding, path, badPath, body, badBody string) {
obj := make(map[string]string)
req := requestWithBody("POST", path, body)
if b.Name() == "form" {
req.Header.Add("Content-Type", MIMEPOSTForm)
}
err := b.Bind(req, &obj)
assert.NoError(t, err)
assert.NotNil(t, obj)
assert.Len(t, obj, 2)
assert.Equal(t, "bar", obj["foo"])
assert.Equal(t, "world", obj["hello"])

if badPath != "" && badBody != "" {
obj = make(map[string]string)
req = requestWithBody("POST", badPath, badBody)
err = b.Bind(req, &obj)
assert.Error(t, err)
}

objInt := make(map[string]int)
req = requestWithBody("POST", path, body)
err = b.Bind(req, &objInt)
assert.Error(t, err)
}

func testBodyBindingUseNumber(t *testing.T, b Binding, name, path, badPath, body, badBody string) {
assert.Equal(t, name, b.Name())

Expand Down
37 changes: 37 additions & 0 deletions binding/form_mapping.go
Expand Up @@ -29,6 +29,21 @@ func mapForm(ptr interface{}, form map[string][]string) error {
var emptyField = reflect.StructField{}

func mapFormByTag(ptr interface{}, form map[string][]string, tag string) error {
// Check if ptr is a map
ptrVal := reflect.ValueOf(ptr)
var pointed interface{}
if ptrVal.Kind() == reflect.Ptr {
ptrVal = ptrVal.Elem()
pointed = ptrVal.Interface()
}
if ptrVal.Kind() == reflect.Map &&
ptrVal.Type().Key().Kind() == reflect.String {
if pointed != nil {
ptr = pointed
}
return setFormMap(ptr, form)
}

return mappingByPtr(ptr, formSource(form), tag)
}

Expand Down Expand Up @@ -349,3 +364,25 @@ func head(str, sep string) (head string, tail string) {
}
return str[:idx], str[idx+len(sep):]
}

func setFormMap(ptr interface{}, form map[string][]string) error {
el := reflect.TypeOf(ptr).Elem()
if el.Kind() == reflect.Slice {
ptrMap, ok := ptr.(map[string][]string)
if !ok {
return errors.New("cannot convert to map slices of strings")
}
for k, v := range form {
ptrMap[k] = v
}
} else {
ptrMap, ok := ptr.(map[string]string)
if !ok {
return errors.New("cannot convert to map of strings")
}
for k, v := range form {
ptrMap[k] = v[len(v)-1] // pick last
}
}
ItalyPaleAle marked this conversation as resolved.
Show resolved Hide resolved
return nil
}
9 changes: 9 additions & 0 deletions binding/json_test.go
Expand Up @@ -19,3 +19,12 @@ func TestJSONBindingBindBody(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "FOO", s.Foo)
}

func TestJSONBindingBindBodyMap(t *testing.T) {
s := make(map[string]string)
err := jsonBinding{}.BindBody([]byte(`{"foo": "FOO","hello":"world"}`), &s)
require.NoError(t, err)
assert.Len(t, s, 2)
assert.Equal(t, "FOO", s["foo"])
assert.Equal(t, "world", s["hello"])
}