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

Add support for nested struct array, adds feature in issue #8 #48

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
62 changes: 48 additions & 14 deletions query/encode.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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++ {
Expand All @@ -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
}
}
Expand Down Expand Up @@ -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
Expand All @@ -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
}
}
Expand Down Expand Up @@ -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 {
Expand All @@ -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:]
}
109 changes: 109 additions & 0 deletions query/encode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 }{},
Expand Down Expand Up @@ -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 := ""

Expand Down Expand Up @@ -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"}},
},
Expand Down