Better support for array and struct serialization This patch allows better encoding of nested structs and arrays to follow [conventions followed by frameworks like Rails][1] when parsing query strings such as ```ids[]=1&ids[]=2&ids[]=3``` for arrays and ```user[name]=acme&user[addr][postcode]=1234&user[addr][city]=SFO``` for hashes. Changes: * Encode nested attributes: ```Parameter.Nested.Value = "str"``` would be encoded as ```parameter[nested][value]=str``` * New ```brackets``` tag option for arrays so that in stead of encoding an array as ```a=one&a=two``` it's encoded as ```a[]=one&a[]=two``` which are read [1]: http://guides.rubyonrails.org/action_controller_overview.html#hash-and-array-parameters
diff --git a/query/encode.go b/query/encode.go index 31d36e7..396d804 100644 --- a/query/encode.go +++ b/query/encode.go
@@ -22,7 +22,6 @@ import ( "bytes" - "errors" "fmt" "net/url" "reflect" @@ -88,7 +87,9 @@ // Slice and Array values default to encoding as multiple URL values of the // same name. Including the "comma" option signals that the field should be // encoded as a single comma-delimited value. Including the "space" option -// similarly encodes the value as a single space-delimited string. +// similarly encodes the value as a single space-delimited string. Including +// the "brackets" option signals that the multiple URL values should have "[]" +// appended to the value name. // // Anonymous struct fields are usually encoded as if their inner exported // fields were fields in the outer struct, subject to the standard Go @@ -97,32 +98,41 @@ // // Non-nil pointer values are encoded as the value pointed to. // +// Nested structs are encoded including parent fields in value names for +// scoping. e.g: +// +// "user[name]=acme&user[addr][postcode]=1234&user[addr][city]=SFO" +// // All other values are encoded using their default string representation. // // Multiple fields that encode to the same URL parameter name will be included // as multiple URL values of the same name. func Values(v interface{}) (url.Values, error) { + values := make(url.Values) val := reflect.ValueOf(v) for val.Kind() == reflect.Ptr { if val.IsNil() { - return nil, errors.New("query: Values() expects non-nil value") + return values, nil } val = val.Elem() } + if v == nil { + return values, nil + } + if val.Kind() != reflect.Struct { return nil, fmt.Errorf("query: Values() expects struct input. Got %v", val.Kind()) } - values := make(url.Values) - err := reflectValue(values, val) + err := reflectValue(values, val, "") return values, err } // reflectValue populates the values parameter from the struct fields in val. // Embedded structs are followed recursively (using the rules defined in the // Values function documentation) breadth-first. -func reflectValue(values url.Values, val reflect.Value) error { +func reflectValue(values url.Values, val reflect.Value, scope string) error { var embedded []reflect.Value typ := val.Type() @@ -148,6 +158,10 @@ name = sf.Name } + if scope != "" { + name = scope + "[" + name + "]" + } + if opts.Contains("omitempty") && isEmptyValue(sv) { continue } @@ -166,6 +180,8 @@ del = ',' } else if opts.Contains("space") { del = ' ' + } else if opts.Contains("brackets") { + name = name + "[]" } if del != 0 { @@ -188,11 +204,28 @@ continue } + if sv.Type() == timeType { + values.Add(name, valueString(sv, opts)) + continue + } + + for sv.Kind() == reflect.Ptr { + if sv.IsNil() { + break + } + sv = sv.Elem() + } + + if sv.Kind() == reflect.Struct { + reflectValue(values, sv, name) + continue + } + values.Add(name, valueString(sv, opts)) } for _, f := range embedded { - if err := reflectValue(values, f); err != nil { + if err := reflectValue(values, f, scope); err != nil { return err } }
diff --git a/query/encode_test.go b/query/encode_test.go index 8afbd0b..09efb0f 100644 --- a/query/encode_test.go +++ b/query/encode_test.go
@@ -12,6 +12,16 @@ "time" ) +type Nested struct { + A SubNested `url:"a"` + B *SubNested `url:"b"` + Ptr *SubNested `url:"ptr,omitempty"` +} + +type SubNested struct { + Value string `url:"value"` +} + func TestValues_types(t *testing.T) { str := "string" strPtr := &str @@ -61,6 +71,7 @@ F [2]string `url:",space"` G []*string `url:",space"` H []bool `url:",int,space"` + I []string `url:",brackets"` }{ A: []string{"a", "b"}, B: []string{"a", "b"}, @@ -70,16 +81,18 @@ F: [2]string{"a", "b"}, G: []*string{&str, &str}, H: []bool{true, false}, + I: []string{"a", "b"}, }, url.Values{ - "A": {"a", "b"}, - "B": {"a,b"}, - "C": {"a b"}, - "D": {"a", "b"}, - "E": {"a,b"}, - "F": {"a b"}, - "G": {"string string"}, - "H": {"1 0"}, + "A": {"a", "b"}, + "B": {"a,b"}, + "C": {"a b"}, + "D": {"a", "b"}, + "E": {"a,b"}, + "F": {"a b"}, + "G": {"string string"}, + "H": {"1 0"}, + "I[]": {"a", "b"}, }, }, { @@ -102,6 +115,41 @@ "D": {"0"}, }, }, + { + struct { + Nest Nested `url:"nest"` + }{ + Nested{ + A: SubNested{ + Value: "that", + }, + }, + }, + url.Values{ + "nest[a][value]": {"that"}, + "nest[b]": {""}, + }, + }, + { + struct { + Nest Nested `url:"nest"` + }{ + Nested{ + Ptr: &SubNested{ + Value: "that", + }, + }, + }, + url.Values{ + "nest[a][value]": {""}, + "nest[b]": {""}, + "nest[ptr][value]": {"that"}, + }, + }, + { + nil, + url.Values{}, + }, } for i, tt := range tests {