diff --git a/conversions.go b/conversions.go index ca1c2de..7b0d812 100644 --- a/conversions.go +++ b/conversions.go @@ -5,7 +5,6 @@ import ( "encoding/base64" "encoding/json" "errors" - "fmt" "net/url" ) @@ -13,6 +12,12 @@ import ( // separate the Base64 string from the security signature. const SignatureSeparator = "_" +// URLValuesSliceKeySuffix is the character that is used to +// specify a suffic for slices parsed by URLValues. +// Ex: Suffix "[]" would have the form a[]=b&a[]=c +// OR Suffix "" would have the form a=b&a=c +var URLValuesSliceKeySuffix = "[]" + // JSON converts the contained object to a JSON string // representation func (m Map) JSON() (string, error) { @@ -94,13 +99,67 @@ func (m Map) MustSignedBase64(key string) string { // function requires that the wrapped object be a map[string]interface{} func (m Map) URLValues() url.Values { vals := make(url.Values) - for k, v := range m { - //TODO: can this be done without sprintf? - vals.Set(k, fmt.Sprintf("%v", v)) - } + + m.parseURLValues(m, vals, "") + return vals } +func (m Map) parseURLValues(queryMap Map, vals url.Values, key string) { + for k, v := range queryMap { + val := &Value{data: v} + switch { + case val.IsObjxMap(): + if key == "" { + m.parseURLValues(v.(Map), vals, k) + } else { + m.parseURLValues(v.(Map), vals, key+"["+k+"]") + } + case val.IsObjxMapSlice(): + sliceKey := k + URLValuesSliceKeySuffix + if key != "" { + sliceKey = key + "[" + k + "]" + URLValuesSliceKeySuffix + } + + for _, sv := range val.MustObjxMapSlice() { + m.parseURLValues(sv, vals, sliceKey) + } + case val.IsMSI(): + if key == "" { + m.parseURLValues(New(v), vals, k) + } else { + m.parseURLValues(New(v), vals, key+"["+k+"]") + } + case val.IsMSISlice(): + sliceKey := k + URLValuesSliceKeySuffix + if key != "" { + sliceKey = key + "[" + k + "]" + URLValuesSliceKeySuffix + } + + for _, sv := range val.MustMSISlice() { + m.parseURLValues(New(sv), vals, sliceKey) + } + case val.IsStrSlice(), val.IsBoolSlice(), + val.IsFloat32Slice(), val.IsFloat64Slice(), + val.IsIntSlice(), val.IsInt8Slice(), val.IsInt16Slice(), val.IsInt32Slice(), val.IsInt64Slice(), + val.IsUintSlice(), val.IsUint8Slice(), val.IsUint16Slice(), val.IsUint32Slice(), val.IsUint64Slice(): + + sliceKey := k + URLValuesSliceKeySuffix + if key != "" { + sliceKey = key + "[" + k + "]" + URLValuesSliceKeySuffix + } + + vals[sliceKey] = val.StringSlice() + default: + if key == "" { + vals.Set(k, val.String()) + } else { + vals.Set(key+"["+k+"]", val.String()) + } + } + } +} + // URLQuery gets an encoded URL query representing the given // Obj. This function requires that the wrapped object be a // map[string]interface{} diff --git a/conversions_test.go b/conversions_test.go index 4584208..011694f 100644 --- a/conversions_test.go +++ b/conversions_test.go @@ -80,17 +80,58 @@ func TestConversionSignedBase64WithError(t *testing.T) { } func TestConversionURLValues(t *testing.T) { - m := objx.Map{"abc": 123, "name": "Mat"} + m := getURLQueryMap() u := m.URLValues() - assert.Equal(t, url.Values{"abc": []string{"123"}, "name": []string{"Mat"}}, u) + assert.Equal(t, url.Values{ + "abc": []string{"123"}, + "name": []string{"Mat"}, + "data[age]": []string{"30"}, + "data[height]": []string{"162"}, + "data[arr][]": []string{"1", "2"}, + "stats[]": []string{"1", "2"}, + "bools[]": []string{"true", "false"}, + "mapSlice[][age]": []string{"40"}, + "mapSlice[][height]": []string{"152"}, + }, u) } func TestConversionURLQuery(t *testing.T) { - m := objx.Map{"abc": 123, "name": "Mat"} + m := getURLQueryMap() u, err := m.URLQuery() assert.Nil(t, err) require.NotNil(t, u) - assert.Equal(t, "abc=123&name=Mat", u) + + ue, err := url.QueryUnescape(u) + assert.Nil(t, err) + require.NotNil(t, ue) + + assert.Equal(t, "abc=123&bools[]=true&bools[]=false&data[age]=30&data[arr][]=1&data[arr][]=2&data[height]=162&mapSlice[][age]=40&mapSlice[][height]=152&name=Mat&stats[]=1&stats[]=2", ue) +} + +func TestConversionURLQueryNoSliceKeySuffix(t *testing.T) { + m := getURLQueryMap() + objx.URLValuesSliceKeySuffix = "" + u, err := m.URLQuery() + + assert.Nil(t, err) + require.NotNil(t, u) + + ue, err := url.QueryUnescape(u) + assert.Nil(t, err) + require.NotNil(t, ue) + + assert.Equal(t, "abc=123&bools=true&bools=false&data[age]=30&data[arr]=1&data[arr]=2&data[height]=162&mapSlice[age]=40&mapSlice[height]=152&name=Mat&stats=1&stats=2", ue) +} + +func getURLQueryMap() objx.Map { + return objx.Map{ + "abc": 123, + "name": "Mat", + "data": objx.Map{"age": 30, "height": 162, "arr": []int{1, 2}}, + "mapSlice": []objx.Map{objx.Map{"age": 40}, objx.Map{"height": 152}}, + "stats": []string{"1", "2"}, + "bools": []bool{true, false}, + } } diff --git a/value.go b/value.go index e4b4a14..8598f60 100644 --- a/value.go +++ b/value.go @@ -51,3 +51,107 @@ func (v *Value) String() string { } return fmt.Sprintf("%#v", v.Data()) } + +// StringSlice returns the value always as a []string +func (v *Value) StringSlice(optionalDefault ...[]string) []string { + switch { + case v.IsStrSlice(): + return v.MustStrSlice() + case v.IsBoolSlice(): + slice := v.MustBoolSlice() + vals := make([]string, len(slice)) + for i, iv := range slice { + vals[i] = strconv.FormatBool(iv) + } + return vals + case v.IsFloat32Slice(): + slice := v.MustFloat32Slice() + vals := make([]string, len(slice)) + for i, iv := range slice { + vals[i] = strconv.FormatFloat(float64(iv), 'f', -1, 32) + } + return vals + case v.IsFloat64Slice(): + slice := v.MustFloat64Slice() + vals := make([]string, len(slice)) + for i, iv := range slice { + vals[i] = strconv.FormatFloat(iv, 'f', -1, 64) + } + return vals + case v.IsIntSlice(): + slice := v.MustIntSlice() + vals := make([]string, len(slice)) + for i, iv := range slice { + vals[i] = strconv.FormatInt(int64(iv), 10) + } + return vals + case v.IsInt8Slice(): + slice := v.MustInt8Slice() + vals := make([]string, len(slice)) + for i, iv := range slice { + vals[i] = strconv.FormatInt(int64(iv), 10) + } + return vals + case v.IsInt16Slice(): + slice := v.MustInt16Slice() + vals := make([]string, len(slice)) + for i, iv := range slice { + vals[i] = strconv.FormatInt(int64(iv), 10) + } + return vals + case v.IsInt32Slice(): + slice := v.MustInt32Slice() + vals := make([]string, len(slice)) + for i, iv := range slice { + vals[i] = strconv.FormatInt(int64(iv), 10) + } + return vals + case v.IsInt64Slice(): + slice := v.MustInt64Slice() + vals := make([]string, len(slice)) + for i, iv := range slice { + vals[i] = strconv.FormatInt(iv, 10) + } + return vals + case v.IsUintSlice(): + slice := v.MustUintSlice() + vals := make([]string, len(slice)) + for i, iv := range slice { + vals[i] = strconv.FormatUint(uint64(iv), 10) + } + return vals + case v.IsUint8Slice(): + slice := v.MustUint8Slice() + vals := make([]string, len(slice)) + for i, iv := range slice { + vals[i] = strconv.FormatUint(uint64(iv), 10) + } + return vals + case v.IsUint16Slice(): + slice := v.MustUint16Slice() + vals := make([]string, len(slice)) + for i, iv := range slice { + vals[i] = strconv.FormatUint(uint64(iv), 10) + } + return vals + case v.IsUint32Slice(): + slice := v.MustUint32Slice() + vals := make([]string, len(slice)) + for i, iv := range slice { + vals[i] = strconv.FormatUint(uint64(iv), 10) + } + return vals + case v.IsUint64Slice(): + slice := v.MustUint64Slice() + vals := make([]string, len(slice)) + for i, iv := range slice { + vals[i] = strconv.FormatUint(iv, 10) + } + return vals + } + if len(optionalDefault) == 1 { + return optionalDefault[0] + } + + return []string{} +} diff --git a/value_test.go b/value_test.go index 1b1e309..ab25e84 100644 --- a/value_test.go +++ b/value_test.go @@ -72,3 +72,70 @@ func TestStringTypeOther(t *testing.T) { assert.Equal(t, "[]string{\"foo\", \"bar\"}", m.Get("other").String()) } + +func TestStringSliceTypeString(t *testing.T) { + m := objx.Map{ + "string": []string{"foo", "bar"}, + } + + assert.Equal(t, []string{"foo", "bar"}, m.Get("string").StringSlice()) +} + +func TestStringSliceTypeBool(t *testing.T) { + m := objx.Map{ + "bool": []bool{true, false}, + } + + assert.Equal(t, []string{"true", "false"}, m.Get("bool").StringSlice()) +} + +func TestStringSliceTypeInt(t *testing.T) { + m := objx.Map{ + "int": []int{1, 2}, + "int8": []int8{8, 9}, + "int16": []int16{16, 17}, + "int32": []int32{32, 33}, + "int64": []int64{64, 65}, + } + + assert.Equal(t, []string{"1", "2"}, m.Get("int").StringSlice()) + assert.Equal(t, []string{"8", "9"}, m.Get("int8").StringSlice()) + assert.Equal(t, []string{"16", "17"}, m.Get("int16").StringSlice()) + assert.Equal(t, []string{"32", "33"}, m.Get("int32").StringSlice()) + assert.Equal(t, []string{"64", "65"}, m.Get("int64").StringSlice()) +} + +func TestStringSliceTypeUint(t *testing.T) { + m := objx.Map{ + "uint": []uint{1, 2}, + "uint8": []uint8{8, 9}, + "uint16": []uint16{16, 17}, + "uint32": []uint32{32, 33}, + "uint64": []uint64{64, 65}, + } + + assert.Equal(t, []string{"1", "2"}, m.Get("uint").StringSlice()) + assert.Equal(t, []string{"8", "9"}, m.Get("uint8").StringSlice()) + assert.Equal(t, []string{"16", "17"}, m.Get("uint16").StringSlice()) + assert.Equal(t, []string{"32", "33"}, m.Get("uint32").StringSlice()) + assert.Equal(t, []string{"64", "65"}, m.Get("uint64").StringSlice()) +} + +func TestStringSliceTypeFloat(t *testing.T) { + m := objx.Map{ + "float32": []float32{32.32, 33.33}, + "float64": []float64{64.64, 65.65}, + } + + assert.Equal(t, []string{"32.32", "33.33"}, m.Get("float32").StringSlice()) + assert.Equal(t, []string{"64.64", "65.65"}, m.Get("float64").StringSlice()) +} + +func TestStringSliceTypeOther(t *testing.T) { + m := objx.Map{ + "other": "foo", + } + + assert.Equal(t, []string{}, m.Get("other").StringSlice()) + assert.Equal(t, []string{"bar"}, m.Get("other").StringSlice([]string{"bar"})) +}