diff --git a/query/encode.go b/query/encode.go index 91198f8..08c5200 100644 --- a/query/encode.go +++ b/query/encode.go @@ -107,6 +107,10 @@ type Encoder interface { // // separated by exclamation points "!". // Field []bool `url:",int" del:"!"` // +// Including the "indexed" option for slices and arrays will encode the Slice and Array +// values using Ruby format, and would lead to recursive serialization of all the nested struct +// fields and slice/array within that struct as well (This was added in PR # 48) +// // Anonymous struct fields are usually encoded as if their inner exported // fields were fields in the outer struct, subject to the standard Go // visibility rules. An anonymous struct field with a name given in its URL @@ -149,7 +153,13 @@ func Values(v interface{}) (url.Values, error) { // Embedded structs are followed recursively (using the rules defined in the // Values function documentation) breadth-first. func reflectValue(values url.Values, val reflect.Value, scope string) error { - var embedded []reflect.Value + var embedded []*reflect.Value + + /** + * Provide scopes for embedded values, helpful for the indexed option as the scope argument of this function is used there to + * ensure correct property names for properties of the nested structs + */ + var scopes map[*reflect.Value]string typ := val.Type() for i := 0; i < typ.NumField(); i++ { @@ -170,7 +180,7 @@ func reflectValue(values url.Values, val reflect.Value, scope string) error { v := reflect.Indirect(sv) if v.IsValid() && v.Kind() == reflect.Struct { // save embedded struct for later processing - embedded = append(embedded, v) + embedded = append(embedded, &v) continue } } @@ -236,11 +246,28 @@ func reflectValue(values url.Values, val reflect.Value, scope string) error { values.Add(name, s.String()) } else { for i := 0; i < sv.Len(); i++ { - k := name + tagName := name + var indexValue reflect.Value = sv.Index(i) + if opts.Contains("numbered") { - k = fmt.Sprintf("%s%d", name, i) + tagName = fmt.Sprintf("%s%d", name, i) + } + + if opts.Contains("indexed") { + if scopes == nil { + scopes = make(map[*reflect.Value]string) + } + + tagName = fmt.Sprintf("%s[%d]", name, i) + + if indexValue.Kind() == reflect.Struct { + embedded = append(embedded, &indexValue) + scopes[&indexValue] = tagName + continue + } } - values.Add(k, valueString(sv.Index(i), opts, sf)) + + values.Add(tagName, valueString(indexValue, opts, sf)) } } continue @@ -261,8 +288,15 @@ func reflectValue(values url.Values, val reflect.Value, scope string) error { values.Add(name, valueString(sv, opts, sf)) } - for _, f := range embedded { - if err := reflectValue(values, f, scope); err != nil { + for _, val := range embedded { + var s string = scope + valueScope, ok := scopes[val] + + if ok { + s = valueScope + } + + if err := reflectValue(values, *val, s); err != nil { return err } } @@ -339,13 +373,6 @@ func isEmptyValue(v reflect.Value) bool { // the empty string. It does not include the leading comma. type tagOptions []string -// parseTag splits a struct field's url tag into its name and comma-separated -// options. -func parseTag(tag string) (string, tagOptions) { - s := strings.Split(tag, ",") - return s[0], s[1:] -} - // Contains checks whether the tagOptions contains the specified option. func (o tagOptions) Contains(option string) bool { for _, s := range o { @@ -355,3 +382,10 @@ func (o tagOptions) Contains(option string) bool { } return false } + +// parseTag splits a struct field's url tag into its name and comma-separated +// options. +func parseTag(tag string) (string, tagOptions) { + s := strings.Split(tag, ",") + return s[0], s[1:] +} diff --git a/query/encode_test.go b/query/encode_test.go index 8487858..b856352 100644 --- a/query/encode_test.go +++ b/query/encode_test.go @@ -178,6 +178,13 @@ func TestValues_Slices(t *testing.T) { url.Values{"V0": {"a"}, "V1": {"b"}}, }, + { + struct { + V []string `url:",indexed"` + }{[]string{"a", "b"}}, + url.Values{"V[0]": {"a"}, "V[1]": {"b"}}, + }, + // arrays of strings { struct{ V [2]string }{}, @@ -309,6 +316,107 @@ func TestValues_NestedTypes(t *testing.T) { } } +func TestValues_ArrayIndexNestedTypes(t *testing.T) { + type AnotherSubNested struct { + AnotherValue string `url:"d"` + } + + type SubNested struct { + Value string `url:"value"` + D []AnotherSubNested `url:"anotherSubNested,indexed"` + } + + type Nested struct { + C []SubNested `url:",indexed"` + } + + tests := []struct { + input interface{} + want url.Values + }{ + { + Nested{ + []SubNested{ + {"value0", []AnotherSubNested{}}, + {"value1", []AnotherSubNested{}}, + {"value2", []AnotherSubNested{}}, + {"value3", []AnotherSubNested{{"value0"}}}, + }, + }, + url.Values{ + "C[0][value]": {"value0"}, + "C[1][value]": {"value1"}, + "C[2][value]": {"value2"}, + "C[3][value]": {"value3"}, + "C[3][anotherSubNested][0][d]": {"value0"}, + }, + }, + { + Nested{ + []SubNested{ + {"value0", []AnotherSubNested{}}, + {"value1", []AnotherSubNested{}}, + {"value2", nil}, + {"value3", []AnotherSubNested{{"value0"}}}, + }, + }, + url.Values{ + "C[0][value]": {"value0"}, + "C[1][value]": {"value1"}, + "C[2][value]": {"value2"}, + "C[3][value]": {"value3"}, + "C[3][anotherSubNested][0][d]": {"value0"}, + }, + }, + } + + for _, tt := range tests { + testValue(t, tt.input, tt.want) + } +} + +/** + * Example taken from the author of Original Issue https://github.com/google/go-querystring/issues/8 + */ +func TestValues_ArrayIndexNestedTypes_GithubIssue_Number_8(t *testing.T) { + type Nested struct { + A string `url:"theA,omitempty"` + B string `url:"theB,omitempty"` + } + + type NestedArr []Nested + + type Main struct { + A NestedArr `url:"arr,indexed"` + B Nested `url:"nested"` + } + + tests := []struct { + input interface{} + want url.Values + }{ + { + Main{ + NestedArr{{"aa", "bb"}, {"aaa", "bbb"}}, + Nested{"xx", "zz"}, + }, + + url.Values{ + "arr[0][theA]": {"aa"}, + "arr[0][theB]": {"bb"}, + "arr[1][theA]": {"aaa"}, + "arr[1][theB]": {"bbb"}, + "nested[theA]": {"xx"}, + "nested[theB]": {"zz"}, + }, + }, + } + + for _, tt := range tests { + testValue(t, tt.input, tt.want) + } +} + func TestValues_OmitEmpty(t *testing.T) { str := "" @@ -384,6 +492,7 @@ func TestValues_EmbeddedStructs(t *testing.T) { url.Values{"V": {"a"}}, }, { + // This step would happen before anything else, so we need not worry about it Mixed{Inner: Inner{V: "a"}, V: "b"}, url.Values{"V": {"b", "a"}}, },