Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,6 @@ Future Work

The following items would be nice to have in the future—though they are not being worked on yet:

- An option to treat all values as if they had been tagged with `omitempty`.
- An option to automatically treat all field names in `camelCase` or `underscore_case`.
- Built-in support for the types in [`math/big`](http://golang.org/pkg/math/big/).
- Built-in support for the types in [`image/color`](http://golang.org/pkg/image/color/).
Expand Down
51 changes: 29 additions & 22 deletions encode.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import (

// NewEncoder returns a new form Encoder.
func NewEncoder(w io.Writer) *Encoder {
return &Encoder{w, defaultDelimiter, defaultEscape, false}
return &Encoder{w, defaultDelimiter, defaultEscape, false, false}
}

// Encoder provides a way to encode to a Writer.
Expand All @@ -27,6 +27,7 @@ type Encoder struct {
d rune
e rune
z bool
o bool
}

// DelimitWith sets r as the delimiter used for composite keys by Encoder e and returns the latter; it is '.' by default.
Expand All @@ -47,10 +48,16 @@ func (e *Encoder) KeepZeros(z bool) *Encoder {
return e
}

// OmitEmpty sets whether Encoder e should omit empty (zero) struct fields during encoding, and returns the former; this is equivalent to having ",omitempty" on every field. By default, empty fields are included.
func (e *Encoder) OmitEmpty(o bool) *Encoder {
e.o = o
return e
}

// Encode encodes dst as form and writes it out using the Encoder's Writer.
func (e Encoder) Encode(dst interface{}) error {
v := reflect.ValueOf(dst)
n, err := encodeToNode(v, e.z)
n, err := encodeToNode(v, e.z, e.o)
if err != nil {
return err
}
Expand All @@ -72,7 +79,7 @@ func EncodeToString(dst interface{}, needEmptyValue ...bool) (string, error) {
if len(needEmptyValue) != 0 {
z = needEmptyValue[0]
}
n, err := encodeToNode(v, z)
n, err := encodeToNode(v, z, false)
if err != nil {
return "", err
}
Expand All @@ -87,24 +94,24 @@ func EncodeToValues(dst interface{}, needEmptyValue ...bool) (url.Values, error)
if len(needEmptyValue) != 0 {
z = needEmptyValue[0]
}
n, err := encodeToNode(v, z)
n, err := encodeToNode(v, z, false)
if err != nil {
return nil, err
}
vs := n.values(defaultDelimiter, defaultEscape)
return vs, nil
}

func encodeToNode(v reflect.Value, z bool) (n node, err error) {
func encodeToNode(v reflect.Value, z bool, o bool) (n node, err error) {
defer func() {
if e := recover(); e != nil {
err = fmt.Errorf("%v", e)
}
}()
return getNode(encodeValue(v, z)), nil
return getNode(encodeValue(v, z, o)), nil
}

func encodeValue(v reflect.Value, z bool) interface{} {
func encodeValue(v reflect.Value, z bool, o bool) interface{} {
t := v.Type()
k := v.Kind()

Expand All @@ -116,28 +123,28 @@ func encodeValue(v reflect.Value, z bool) interface{} {

switch k {
case reflect.Ptr, reflect.Interface:
return encodeValue(v.Elem(), z)
return encodeValue(v.Elem(), z, o)
case reflect.Struct:
if t.ConvertibleTo(timeType) {
return encodeTime(v)
} else if t.ConvertibleTo(urlType) {
return encodeURL(v)
}
return encodeStruct(v, z)
return encodeStruct(v, z, o)
case reflect.Slice:
return encodeSlice(v, z)
return encodeSlice(v, z, o)
case reflect.Array:
return encodeArray(v, z)
return encodeArray(v, z, o)
case reflect.Map:
return encodeMap(v, z)
return encodeMap(v, z, o)
case reflect.Invalid, reflect.Uintptr, reflect.UnsafePointer, reflect.Chan, reflect.Func:
panic(t.String() + " has unsupported kind " + t.Kind().String())
default:
return encodeBasic(v)
}
}

func encodeStruct(v reflect.Value, z bool) interface{} {
func encodeStruct(v reflect.Value, z bool, o bool) interface{} {
t := v.Type()
n := node{}
for i := 0; i < t.NumField(); i++ {
Expand All @@ -146,40 +153,40 @@ func encodeStruct(v reflect.Value, z bool) interface{} {

if k == "-" {
continue
} else if fv := v.Field(i); oe && isEmptyValue(fv) {
} else if fv := v.Field(i); (o || oe) && isEmptyValue(fv) {
delete(n, k)
} else {
n[k] = encodeValue(fv, z)
n[k] = encodeValue(fv, z, o)
}
}
return n
}

func encodeMap(v reflect.Value, z bool) interface{} {
func encodeMap(v reflect.Value, z bool, o bool) interface{} {
n := node{}
for _, i := range v.MapKeys() {
k := getString(encodeValue(i, z))
n[k] = encodeValue(v.MapIndex(i), z)
k := getString(encodeValue(i, z, o))
n[k] = encodeValue(v.MapIndex(i), z, o)
}
return n
}

func encodeArray(v reflect.Value, z bool) interface{} {
func encodeArray(v reflect.Value, z bool, o bool) interface{} {
n := node{}
for i := 0; i < v.Len(); i++ {
n[strconv.Itoa(i)] = encodeValue(v.Index(i), z)
n[strconv.Itoa(i)] = encodeValue(v.Index(i), z, o)
}
return n
}

func encodeSlice(v reflect.Value, z bool) interface{} {
func encodeSlice(v reflect.Value, z bool, o bool) interface{} {
t := v.Type()
if t.Elem().Kind() == reflect.Uint8 {
return string(v.Bytes()) // Encode byte slices as a single string by default.
}
n := node{}
for i := 0; i < v.Len(); i++ {
n[strconv.Itoa(i)] = encodeValue(v.Index(i), z)
n[strconv.Itoa(i)] = encodeValue(v.Index(i), z, o)
}
return n
}
Expand Down
37 changes: 37 additions & 0 deletions encode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,3 +99,40 @@ func TestEncode_KeepZero(t *testing.T) {
}
}
}

func TestEncode_OmitEmpty(t *testing.T) {
num := uint(0)
nonZeroNum := uint(42)
for _, c := range []struct {
b interface{}
s string
o bool
}{
// Thing3 and Thing4 have no omitempty tags, so OmitEmpty affects them.
{Thing3{"test", &nonZeroNum}, "name=test&num=42", false},
{Thing3{"test", &nonZeroNum}, "name=test&num=42", true},
{Thing3{"", &nonZeroNum}, "name=&num=42", false},
{Thing3{"", &nonZeroNum}, "num=42", true},
{Thing3{"test", nil}, "name=test&num=", false},
{Thing3{"test", nil}, "name=test", true},
{Thing4{"test", 0}, "name=test&num=", false},
{Thing4{"test", 0}, "name=test", true},
{Thing4{"test", 42}, "name=test&num=42", false},
{Thing4{"test", 42}, "name=test&num=42", true},
// Thing1 and Thing2 already have omitempty tags.
{Thing1{"test", &num}, "name=test&num=", false},
{Thing1{"test", &num}, "name=test&num=", true},
{Thing2{"test", 0}, "name=test", false},
{Thing2{"test", 0}, "name=test", true},
} {

var w bytes.Buffer
e := NewEncoder(&w)

if err := e.OmitEmpty(c.o).Encode(c.b); err != nil {
t.Errorf("OmitEmpty(%#v).Encode(%#v): %s", c.o, c.b, err)
} else if s := w.String(); c.s != s {
t.Errorf("OmitEmpty(%#v).Encode(%#v)\n want (%#v)\n have (%#v)", c.o, c.b, c.s, s)
}
}
}