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.

From a zero-alloc standpoint,
UnmarshalMsg and DecodeMsg continue,
as before, to re-use existing fields in an
object rather than allocating new
ones. So, if you decode into the same
object repeatedly, things like slices
maps, and fields that point to other
structures won't be re-allocated.

Instead, maps and fields will be re-sized
appropriately. In other words, mutable
fields are simply mutated in-place.

Fixes #103
  • Loading branch information
glycerine committed Oct 23, 2016
1 parent ad0ff2e commit 68f025a
Show file tree
Hide file tree
Showing 31 changed files with 1,732 additions and 231 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.
88 changes: 88 additions & 0 deletions _generated/def.go
Original file line number Diff line number Diff line change
Expand Up @@ -191,3 +191,91 @@ 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"`
Greetings string `msg:",omitempty"`
Bullwinkle *Rocky `msg:",omitempty"`
}

type TopNester struct {
TopId int
Greetings string `msg:",omitempty"`
Bullwinkle *Rocky `msg:",omitempty"`

MyIntArray [3]int
MyByteArray [3]byte
MyMap map[string]string
MyArrayMap [3]map[string]string
}

type Rocky struct {
Bugs *Bunny `msg:",omitempty"`
Road string `msg:",omitempty"`
Moose *Moose `msg:",omitempty"`
}

type Bunny struct {
Carrots []int `msg:",omitempty"`
Sayings map[string]string `msg:",omitempty"`
BunnyId int `msg:",omitempty"`
}

type Moose struct {
Trees []int `msg:",omitempty"`
Sayings map[string]string `msg:",omitempty"`
Id int
}

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"`
}
196 changes: 196 additions & 0 deletions _generated/omitempty_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
package _generated

import (
"bytes"
"fmt"
"github.com/shurcooL/go-goon"
"github.com/tinylib/msgp/msgp"
"testing"
)

var VerboseOmitTest = false

func TestMissingNilledOutWhenUnmarshallingOrDecodingNilIntoNestedStructs(t *testing.T) {

underTest := "UnmarshalMsg"
for passNo := 0; passNo < 2; passNo++ {
if passNo == 1 {
underTest = "UnmarshalMsg"
}

// passNo 0 => UnmarshalMsg
// passNo 1 => DecodeMsg
//
// Given a tree of structs with element points
// that is three levels deep, when omitempty fields
// are omitted from the wire (msgpack data), we
// should re-use the existing structs, maps, and slices,
// and shrink them without re-allocation.

s := TopNester{
TopId: 43,
Greetings: "greetings",
Bullwinkle: &Rocky{
Road: "Long and winding",
Bugs: &Bunny{
Carrots: []int{41, 8},
Sayings: map[string]string{"whatsup": "doc"},
BunnyId: 88,
},
Moose: &Moose{
Trees: []int{0, 1, 2, 3},
Sayings: map[string]string{"one": "singular sensation"},
Id: 2,
},
},
MyIntArray: [3]int{4, 5, 6},
MyByteArray: [3]byte{1, 2, 3},
MyMap: map[string]string{"key": "to my heart"},
MyArrayMap: [3]map[string]string{{"key1": "to my heart1"}, {"key2": "to my heart2"}, {}},
}

// so pointers should not change upon decoding from nil
pGreet := &s.Greetings
pBull := &s.Bullwinkle
pBugs := &s.Bullwinkle.Bugs
pCarrots := &s.Bullwinkle.Bugs.Carrots
pSay1 := &s.Bullwinkle.Bugs.Sayings
pMoose := &s.Bullwinkle.Moose
pTree := &s.Bullwinkle.Moose.Trees
pSay2 := &s.Bullwinkle.Moose.Sayings

if VerboseOmitTest {
fmt.Printf("\n ======== BEGIN goon.Dump of TopNester *BEFORE* %s:\n", underTest)
goon.Dump(s)
fmt.Printf("\n ======== END goon.Dump of TopNester *BEFORE* %s\n", underTest)
}
var o []byte
var err error
nilMsg := []byte{0xc0}
if passNo == 0 {
if VerboseOmitTest {
fmt.Printf("\n testing UnmarshalMsg ******************\n")
}
o, err = s.UnmarshalMsg(nilMsg)
if err != nil {
panic(err)
}
} else {
if VerboseOmitTest {
fmt.Printf("\n testing DecodeMsg ******************\n")
}
nilMsg = []byte{0xc0}
dc := msgp.NewReader(bytes.NewBuffer(nilMsg))
err := s.DecodeMsg(dc) // msgp: attempted to decode type "nil" with method for "map"
if err != nil {
panic(err)
}
}

if VerboseOmitTest {
fmt.Printf("\n ======== BEGIN goon.Dump of TopNester AFTER %s:\n", underTest)
goon.Dump(s)
fmt.Printf("\n ======== END goon.Dump of TopNester AFTER %s\n", underTest)
}

if len(o) != 0 {
t.Fatal("nilMsg should have been consumed")
}

for i := range s.MyIntArray {
if s.MyIntArray[i] != 0 {
panic("shoud have been set to 0")
}
}
for i := range s.MyByteArray {
if s.MyIntArray[i] != 0 {
panic("shoud have been set to 0")
}
}

if pGreet != &s.Greetings {
t.Fatal("pGreet differed from original")
}
if pBull != &s.Bullwinkle {
t.Fatal("pBull differed from original")
}
if pBugs != &s.Bullwinkle.Bugs {
t.Fatal("pBugs differed from original")
}
if pCarrots != &s.Bullwinkle.Bugs.Carrots {
t.Fatal("pCarrots differed from original")
}
if pSay1 != &s.Bullwinkle.Bugs.Sayings {
t.Fatal("pSay1 differed from original")
}
if pMoose != &s.Bullwinkle.Moose {
t.Fatal("pMoose differed from original")
}
if pTree != &s.Bullwinkle.Moose.Trees {
t.Fatal("pTree differed from original")
}
if pSay2 != &s.Bullwinkle.Moose.Sayings {
t.Fatal("pSay2 differed from original")
}

// and yet, the maps and slices should be size 0,
// the strings empty, the integers zeroed out.

// TopNester
if s.TopId != 0 {
t.Fatal("s.TopId should be 0")
}
if len(s.Greetings) != 0 {
t.Fatal("s.Grettings should be len 0")
}
if s.Bullwinkle == nil {
t.Fatal("s.Bullwinkle should not be nil")
}

// TopNester.Bullwinkle
if s.Bullwinkle.Bugs == nil {
t.Fatal("s.Bullwinkle.Bugs should not be nil")
}
if len(s.Bullwinkle.Road) != 0 {
t.Fatal("s.Bullwinkle.Road should be len 0")
}
if s.Bullwinkle.Moose == nil {
t.Fatal("s.Bullwinkle.Moose should not be nil")
}

// TopNester.Bullwinkle.Bugs
if len(s.Bullwinkle.Bugs.Carrots) != 0 {
panic("this is wrong")
}
if len(s.Bullwinkle.Bugs.Sayings) != 0 {
panic("this is wrong")
}
if s.Bullwinkle.Bugs.BunnyId != 0 {
panic("this is wrong")
}

// TopNester.Bullwinkle.Moose
if len(s.Bullwinkle.Moose.Trees) != 0 {
panic("this is wrong")
}
if len(s.Bullwinkle.Moose.Sayings) != 0 {
panic("this is wrong")
}
if s.Bullwinkle.Moose.Id != 0 {
panic("this is wrong")
}

// MyMap should have been emptied out
if len(s.MyMap) != 0 {
panic("MyMap should have been len 0 after decode from nil")
}

// each map in MyArrayMap should have been emptied
for k := range s.MyArrayMap {
if len(s.MyArrayMap[k]) != 0 {
panic(fmt.Sprintf("MyArrayMap[%v] should have been len 0 after decode from nil", k))
}
}

} // end passNo loop
}
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
}
Loading

0 comments on commit 68f025a

Please sign in to comment.