diff --git a/query/encode.go b/query/encode.go index ffb2097..ec5c0ee 100644 --- a/query/encode.go +++ b/query/encode.go @@ -165,6 +165,10 @@ func reflectValue(values url.Values, val reflect.Value, scope string) error { } name, opts := parseTag(tag) + if opts.Contains("omitempty") && isEmptyValue(sv) { + continue + } + if name == "" { if sf.Anonymous { v := reflect.Indirect(sv) @@ -182,10 +186,6 @@ func reflectValue(values url.Values, val reflect.Value, scope string) error { name = scope + "[" + name + "]" } - if opts.Contains("omitempty") && isEmptyValue(sv) { - continue - } - // unwrap interface values so the concrete type's Encoder is used if sv.Kind() == reflect.Interface && !sv.IsNil() { sv = sv.Elem() @@ -334,6 +334,10 @@ func isEmptyValue(v reflect.Value) bool { return v.IsNil() } + if !v.CanInterface() { + return false + } + type zeroable interface { IsZero() bool } diff --git a/query/encode_test.go b/query/encode_test.go index 241e4f1..e65afa9 100644 --- a/query/encode_test.go +++ b/query/encode_test.go @@ -424,6 +424,56 @@ func TestValues_EmbeddedStructs(t *testing.T) { } } +// OptionalEmbedded supports omitting all of its promoted fields. +type OptionalEmbedded struct { + Value string `url:"value"` +} + +func (v OptionalEmbedded) IsZero() bool { return v.Value == "" } + +func TestValues_EmbeddedOmitEmpty(t *testing.T) { + type Options struct { + OptionalEmbedded `url:",omitempty"` + } + type privateScalar string + type privateEmbedded struct { + Value string `url:"value"` + } + tests := []struct { + name string + input interface{} + want url.Values + }{ + {"empty", Options{}, url.Values{}}, + {"nonempty", Options{OptionalEmbedded{Value: "present"}}, url.Values{"value": {"present"}}}, + {"without option", struct{ OptionalEmbedded }{}, url.Values{"value": {""}}}, + {"named empty", struct { + OptionalEmbedded `url:"nested,omitempty"` + }{}, url.Values{}}, + {"named nonempty", struct { + OptionalEmbedded `url:"nested,omitempty"` + }{OptionalEmbedded{Value: "present"}}, url.Values{"nested[value]": {"present"}}}, + {"nil pointer", struct { + *OptionalEmbedded `url:",omitempty"` + }{}, url.Values{}}, + {"non-nil pointer", struct { + *OptionalEmbedded `url:",omitempty"` + }{&OptionalEmbedded{}}, url.Values{"value": {""}}}, + {"empty unexported scalar", struct { + privateScalar `url:",omitempty"` + }{}, url.Values{}}, + {"nil unexported pointer", struct { + *privateScalar `url:",omitempty"` + }{}, url.Values{}}, + {"unexported embed", struct { + privateEmbedded `url:",omitempty"` + }{privateEmbedded{Value: "present"}}, url.Values{"value": {"present"}}}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { testValue(t, tt.input, tt.want) }) + } +} + func TestValues_InvalidInput(t *testing.T) { _, err := Values("") if err == nil {