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
 `msg:",omitempty"`

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

For structs with many optional
fields, the space and time savings
can be substantial.

Further details:

* to avoid allocating memory, we
  reuse the structs provided to
  deserialize into, without clearing
  totally first. Available fields
  overwrite any prior values. If
  a field is missing on the wire,
  we treat it as if the wire held
  a nil value. This means that
  maps, slices, and pointers
  continue as before.

Fixes #103
  • Loading branch information
glycerine committed Oct 20, 2016
1 parent ad0ff2e commit 2ee78ce
Show file tree
Hide file tree
Showing 19 changed files with 998 additions and 88 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@ _generated/generated_test.go
_generated/*_gen.go
_generated/*_gen_test.go
msgp/defgen_test.go
msgp/nestedgen_test.go
msgp/cover.out
*~
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
# generated integration test files
GGEN = ./_generated/generated.go ./_generated/generated_test.go
# generated unit test files
MGEN = ./msgp/defgen_test.go
MGEN = ./msgp/defgen_test.go ./msgp/nestedgen_test.go

SHELL := /bin/bash

Expand Down
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 {
NameSuey 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
}
72 changes: 64 additions & 8 deletions gen/decode.go
Original file line number Diff line number Diff line change
@@ -1,21 +1,27 @@
package gen

import (
"fmt"
"github.com/tinylib/msgp/cfg"
"io"
"strconv"
)

func decode(w io.Writer) *decodeGen {
func decode(w io.Writer, cfg *cfg.MsgpConfig) *decodeGen {
return &decodeGen{
p: printer{w: w},
hasfield: false,
cfg: cfg,
}
}

type decodeGen struct {
passes
p printer
hasfield bool
depth int
cfg *cfg.MsgpConfig
lifo []bool
}

func (d *decodeGen) Method() Method { return Decode }
Expand Down Expand Up @@ -43,15 +49,23 @@ func (d *decodeGen) Execute(p Elem) error {
}

d.p.comment("DecodeMsg implements msgp.Decodable")

d.p.comment("We treat empty fields as if we read a Nil from the wire.")
d.p.printf("\nfunc (%s %s) DecodeMsg(dc *msgp.Reader) (err error) {", p.Varname(), methodReceiver(p))

// next will increment k, but we want the first, top level DecodeMsg
// to refer to this same k ...
next(d, p)
d.p.nakedReturn()
unsetReceiver(p)
return d.p.err
}

func (d *decodeGen) gStruct(s *Struct) {
d.depth++
defer func() {
d.depth--
}()

if !d.p.ok() {
return
}
Expand Down Expand Up @@ -86,18 +100,53 @@ func (d *decodeGen) structAsTuple(s *Struct) {
}
}

/* func (d *decodeGen) structAsMap(s *Struct):
//
// Missing (empty) field handling logic:
//
// The approach to missing field handling is to
// keep the logic the same whether the field is
// missing or nil on the wire. To do so we use
// the Reader.PushAlwaysNil() method to tell
// the Reader to pretend to supply
// only nils until further notice. The further
// notice comes from the terminating dc.PopAlwaysNil()
// calls emptying the LIFO. The stack is
// needed because multiple struct decodes may
// be nested due to inlining.
*/
func (d *decodeGen) structAsMap(s *Struct) {
n := len(s.Fields)
if n == 0 {
return
}
d.needsField()
sz := randIdent()
d.p.declare(sz, u32)
d.assignAndCheck(sz, mapHeader)

d.p.printf("\nfor %s > 0 {\n%s--", sz, sz)
d.assignAndCheck("field", mapKey)
d.p.print("\nswitch msgp.UnsafeString(field) {")
k := genSerial()
tmpl, nStr := genDecodeMsgTemplate(k)

fieldOrder := fmt.Sprintf("\n var decodeMsgFieldOrder%s = []string{", nStr)
for i := range s.Fields {
fieldOrder += fmt.Sprintf("%q,", s.Fields[i].FieldTag)
}
fieldOrder += "}\n"
d.p.printf("%s", fieldOrder)

d.p.printf("const maxFields%s = %d\n", nStr, n)

found := "found" + nStr
d.p.printf(tmpl)
// after printing tmpl, we are at this point:
// switch curField_ {
// -- templateDecodeMsg ends here --

for i := range s.Fields {
d.p.printf("\ncase \"%s\":", s.Fields[i].FieldTag)
d.p.printf("\n%s[%d]=true;", found, i)
//d.p.printf("\n fmt.Printf(\"I found field '%s' at depth=%d. dc.AlwaysNil = %%v\", dc.AlwaysNil);\n", s.Fields[i].FieldTag, d.depth)
d.depth++
next(d, s.Fields[i].FieldElem)
d.depth--
if !d.p.ok() {
return
}
Expand All @@ -106,6 +155,8 @@ func (d *decodeGen) structAsMap(s *Struct) {
d.p.print(errcheck)
d.p.closeblock() // close switch
d.p.closeblock() // close for loop

d.p.printf("\n if nextMiss%s != -1 {dc.PopAlwaysNil(); }\n", nStr)
}

func (d *decodeGen) gBase(b *BaseElem) {
Expand Down Expand Up @@ -153,6 +204,11 @@ func (d *decodeGen) gBase(b *BaseElem) {
}

func (d *decodeGen) gMap(m *Map) {
d.depth++
defer func() {
d.depth--
}()

if !d.p.ok() {
return
}
Expand Down
Loading

0 comments on commit 2ee78ce

Please sign in to comment.