Skip to content

Commit

Permalink
omitempty tags implemented.
Browse files Browse the repository at this point in the history
struct fields can be tagged with
 msgp:",omitempty"

Such fields will not be serialized
if they contain their zero values.

There is no cost to this feature if
tags are not used. For structs with
many optional fields, the space and
time savings can be substantial.

Fixes #103
  • Loading branch information
glycerine committed Oct 16, 2016
1 parent ad0ff2e commit fc5241d
Show file tree
Hide file tree
Showing 11 changed files with 401 additions and 57 deletions.
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,3 +102,17 @@ If the output compiles, then there's a pretty good chance things are fine. (Plus
If you like benchmarks, see [here.](https://github.com/alecthomas/go_serialization_benchmarks)

As one might expect, the generated methods that deal with `[]byte` are faster, but the `io.Reader/Writer` methods are generally more memory-efficient for large (> 2KB) objects.

### `msgp:",omitempty"` tags on struct fields

In the following example,
```
type Hedgehog struct {
Furriness string `msgp:",omitempty"`
}
```
If Furriness is the empty string, the field will not be serialized, thus saving the space of the field name on the wire.

Generally, most zero values that have the omitempty tag applied will be skipped. Recursive struct elements are an exception; they are always included and are never impacted by omitempty tagging recursively. Naturally the member's own fields may have tags that will be in-force locally once the serializer reaches them. Note that member structs are different from member pointers-to-structs. Nil pointers that are tagged `omitempty` will have their field skipped.

Under tuple encoding, all fields are serialized and the omitempty tag is ignored.
57 changes: 57 additions & 0 deletions _generated/def.go
Original file line number Diff line number Diff line change
Expand Up @@ -191,3 +191,60 @@ type FileHandle struct {

type CustomInt int
type CustomBytes []byte

// Test omitempty tag
type TestOmitEmpty struct {

// scalars
Name string `msg:",omitempty"`
BirthDay time.Time `msg:",omitempty"`
Phone string `msg:",omitempty"`
Siblings int `msg:",omitempty"`
Spouse bool `msg:",omitempty"`
Money float64 `msg:",omitempty"`

// slices
SliceName []string `msg:",omitempty"`
SliceBirthDay []time.Time `msg:",omitempty"`
SlicePhone []string `msg:",omitempty"`
SliceSiblings []int `msg:",omitempty"`
SliceSpouse []bool `msg:",omitempty"`
SliceMoney []float64 `msg:",omitempty"`

// arrays
ArrayName [3]string `msg:",omitempty"`
ArrayBirthDay [3]time.Time `msg:",omitempty"`
ArrayPhone [3]string `msg:",omitempty"`
ArraySiblings [3]int `msg:",omitempty"`
ArraySpouse [3]bool `msg:",omitempty"`
ArrayMoney [3]float64 `msg:",omitempty"`

// maps
MapStringString map[string]string `msg:",omitempty"`
MapStringIface map[string]interface{} `msg:",omitempty"`

// pointers
PtrName *string `msg:",omitempty"`
PtrBirthDay *time.Time `msg:",omitempty"`
PtrPhone *string `msg:",omitempty"`
PtrSiblings *int `msg:",omitempty"`
PtrSpouse *bool `msg:",omitempty"`
PtrMoney *float64 `msg:",omitempty"`

Inside1 OmitEmptyInside1 `msg:",omitempty"`
}

type OmitEmptyInside1 struct {
CountOfMonteCrisco int
Name string `msg:"name,omitempty"`
Inside2 OmitEmptyInside2 `msg:",omitempty"`
}

type OmitEmptyInside2 struct {
Name string `msg:",omitempty"`
}

type OmitSimple struct {
CountDrocula int
Inside1 OmitEmptyInside1 `msg:",omitempty"`
}
29 changes: 29 additions & 0 deletions cfg/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package cfg

import (
"flag"
)

type MsgpConfig struct {
Out string
GoFile string
Encode bool
Marshal bool
Tests bool
Unexported bool
}

// call DefineFlags before myflags.Parse()
func (c *MsgpConfig) DefineFlags(fs *flag.FlagSet) {
fs.StringVar(&c.Out, "o", "", "output file")
fs.StringVar(&c.GoFile, "file", "", "input file")
fs.BoolVar(&c.Encode, "io", true, "create Encode and Decode methods")
fs.BoolVar(&c.Marshal, "marshal", true, "create Marshal and Unmarshal methods")
fs.BoolVar(&c.Tests, "tests", true, "create tests and benchmarks")
fs.BoolVar(&c.Unexported, "unexported", false, "also process unexported types")
}

// call c.ValidateConfig() after myflags.Parse()
func (c *MsgpConfig) ValidateConfig() error {
return nil
}
6 changes: 4 additions & 2 deletions gen/elem.go
Original file line number Diff line number Diff line change
Expand Up @@ -360,8 +360,9 @@ func (s *Ptr) Needsinit() bool {

type Struct struct {
common
Fields []StructField // field list
AsTuple bool // write as an array instead of a map
Fields []StructField // field list
AsTuple bool // write as an array instead of a map
hasOmitEmptyTags bool
}

func (s *Struct) TypeName() string {
Expand Down Expand Up @@ -404,6 +405,7 @@ type StructField struct {
FieldTag string // the string inside the `msg:""` tag
FieldName string // the name of the struct field
FieldElem Elem // the field type
OmitEmpty bool // if the tag `msg:",omitempty"` was found
}

// BaseElem is an element that
Expand Down
35 changes: 30 additions & 5 deletions gen/encode.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@ func encode(w io.Writer) *encodeGen {

type encodeGen struct {
passes
p printer
fuse []byte
p printer
fuse []byte
uniqEmpties int
}

func (e *encodeGen) Method() Method { return Encode }
Expand Down Expand Up @@ -102,17 +103,41 @@ func (e *encodeGen) appendraw(bts []byte) {

func (e *encodeGen) structmap(s *Struct) {
nfields := len(s.Fields)
data := msgp.AppendMapHeader(nil, uint32(nfields))
e.p.printf("\n// map header, size %d", nfields)
e.Fuse(data)
var data []byte
emptyName := fmt.Sprintf("empty%d", e.uniqEmpties)
e.uniqEmpties++
if s.hasOmitEmptyTags {
e.p.printf("\n\n// honor the omitempty tags\n")
e.p.printf("var %s [%d]bool\n", emptyName, nfields)
e.p.printf("fieldsInUse := %s.fieldsNotEmpty(%s[:])\n",
s.vname, emptyName)
e.p.printf("\n// map header\n")
e.p.printf(" err = en.WriteMapHeader(fieldsInUse)\n")
e.p.printf(" if err != nil {\n")
e.p.printf(" return err\n}\n")
} else {
data = msgp.AppendMapHeader(nil, uint32(nfields))
e.p.printf("\n// map header, size %d", nfields)
e.Fuse(data)
}

for i := range s.Fields {
if !e.p.ok() {
return
}

if s.hasOmitEmptyTags && s.Fields[i].OmitEmpty {
e.p.printf("\n if !%s[%d] {", emptyName, i)
}

data = msgp.AppendString(nil, s.Fields[i].FieldTag)
e.p.printf("\n// write %q", s.Fields[i].FieldTag)
e.Fuse(data)
next(e, s.Fields[i].FieldElem)

if s.hasOmitEmptyTags && s.Fields[i].OmitEmpty {
e.p.printf("\n }\n")
}
}
}

Expand Down
82 changes: 82 additions & 0 deletions gen/fieldsempty.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package gen

import (
"fmt"
"io"
)

func fieldsempty(w io.Writer) *fieldsEmpty {
return &fieldsEmpty{
p: printer{w: w},
}
}

type fieldsEmpty struct {
passes
p printer
recvr string
}

func (e *fieldsEmpty) Method() Method { return FieldsEmpty }

func (e *fieldsEmpty) Execute(p Elem) error {
if !e.p.ok() {
return e.p.err
}
p = e.applyall(p)
if p == nil {
return nil
}
if !IsPrintable(p) {
return nil
}

e.recvr = fmt.Sprintf("%s %s", p.Varname(), imutMethodReceiver(p))

next(e, p)
return e.p.err
}

func (e *fieldsEmpty) gStruct(s *Struct) {
e.p.printf("// fieldsNotEmpty supports omitempty tags\n")
e.p.printf("func (%s) fieldsNotEmpty(isempty []bool) uint32 {", e.recvr)

nfields := len(s.Fields)
numOE := 0
for i := range s.Fields {
if s.Fields[i].OmitEmpty {
numOE++
}
}
if numOE == 0 {
// no fields tagged with omitempty, just return the full field count.
e.p.printf("\nreturn %d }\n", nfields)
return
}

// remember this to avoid recomputing it in other passes.
s.hasOmitEmptyTags = true

om := emptyOmitter(&e.p, s.vname)

e.p.printf("if len(isempty) == 0 { return %d }\n", nfields)
e.p.printf("var fieldsInUse uint32 = %d\n", nfields)
for i := range s.Fields {
if s.Fields[i].OmitEmpty {
e.p.printf("isempty[%d] = ", i)
next(om, s.Fields[i].FieldElem)
e.p.printf("if isempty[%d] { fieldsInUse-- }\n", i)
}
}
e.p.printf("\n return fieldsInUse \n}\n")
}

func (s *fieldsEmpty) gPtr(p *Ptr) {}

func (s *fieldsEmpty) gSlice(sl *Slice) {}

func (s *fieldsEmpty) gArray(a *Array) {}

func (s *fieldsEmpty) gMap(m *Map) {}

func (s *fieldsEmpty) gBase(b *BaseElem) {}
28 changes: 23 additions & 5 deletions gen/marshal.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@ func marshal(w io.Writer) *marshalGen {

type marshalGen struct {
passes
p printer
fuse []byte
p printer
fuse []byte
uniqEmpties int
}

func (m *marshalGen) Method() Method { return Marshal }
Expand Down Expand Up @@ -97,19 +98,36 @@ func (m *marshalGen) tuple(s *Struct) {

func (m *marshalGen) mapstruct(s *Struct) {
data := make([]byte, 0, 64)
data = msgp.AppendMapHeader(data, uint32(len(s.Fields)))
m.p.printf("\n// map header, size %d", len(s.Fields))
m.Fuse(data)
nfields := len(s.Fields)
emptyName := fmt.Sprintf("empty%d", m.uniqEmpties)
m.uniqEmpties++
if s.hasOmitEmptyTags {
m.p.printf("\n\n// honor the omitempty tags\n")
m.p.printf("var %s [%d]bool\n", emptyName, nfields)
m.p.printf("fieldsInUse := %s.fieldsNotEmpty(%s[:])\n", s.vname, emptyName)
m.p.printf(" o = msgp.AppendMapHeader(o, fieldsInUse)\n")
} else {
data = msgp.AppendMapHeader(data, uint32(len(s.Fields)))
m.p.printf("\n// map header, size %d", len(s.Fields))
m.Fuse(data)
}
for i := range s.Fields {
if !m.p.ok() {
return
}
if s.hasOmitEmptyTags && s.Fields[i].OmitEmpty {
m.p.printf("\n if !%s[%d] {", emptyName, i)
}
data = msgp.AppendString(nil, s.Fields[i].FieldTag)

m.p.printf("\n// string %q", s.Fields[i].FieldTag)
m.Fuse(data)

next(m, s.Fields[i].FieldElem)

if s.hasOmitEmptyTags && s.Fields[i].OmitEmpty {
m.p.printf("\n }\n")
}
}
}

Expand Down
77 changes: 77 additions & 0 deletions gen/omitempty.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package gen

import (
"fmt"
)

func emptyOmitter(p *printer, varname string) *omitEmpty {
return &omitEmpty{
p: p,
varname: varname,
}
}

type omitEmpty struct {
p *printer
varname string
}

func (s *omitEmpty) gStruct(st *Struct) {
s.p.printf("false // struct values are never empty\n")
}

func (s *omitEmpty) gPtr(p *Ptr) {
s.p.printf("(%s == nil) // pointer, omitempty\n", p.vname)
}

func (s *omitEmpty) gSlice(sl *Slice) {
s.p.printf("%s", IsLenZero(sl.vname))
}

func (s *omitEmpty) gArray(a *Array) {
s.p.printf("%s", IsLenZero(a.vname))
}

func (s *omitEmpty) gMap(m *Map) {
s.p.printf("%s", IsLenZero(m.vname))
}

func (s *omitEmpty) gBase(b *BaseElem) {
switch b.Value {
case Bytes:
s.p.printf("%s", IsLenZero(b.Varname()))
case String:
s.p.printf("%s", IsLenZero(b.Varname()))
case Float32, Float64, Complex64, Complex128, Uint, Uint8, Uint16, Uint32, Uint64, Byte, Int, Int8, Int16, Int32, Int64:
s.p.printf("%s", IsEmptyNumber(b.Varname()))
case Bool:
s.p.printf("%s", IsEmptyBool(b.Varname()))
case Time: // time.Time
s.p.printf("%s", IsEmptyTime(b.Varname()))
case Intf: // interface{}
// assume, for now, never empty. rarely makes sense to serialize these.
fallthrough
default:
s.p.print("false\n")
}
}

func IsEmptyNumber(f string) string {
return fmt.Sprintf("(%s == 0) // number, omitempty\n",
f)
}

func IsLenZero(f string) string {
return fmt.Sprintf("(len(%s) == 0) // string, omitempty\n",
f)
}

func IsEmptyBool(f string) string {
return fmt.Sprintf("(!%s) // bool, omitempty\n",
f)
}

func IsEmptyTime(f string) string {
return fmt.Sprintf("(%s.IsZero()) // time.Time, omitempty\n",
f)
}
Loading

0 comments on commit fc5241d

Please sign in to comment.