From 0ce136952a200f926f07c3b6aef7ecd63c53df9b Mon Sep 17 00:00:00 2001 From: Brad Peabody Date: Thu, 26 Oct 2023 16:54:51 -0700 Subject: [PATCH] omitzero implementation --- _generated/omitzero.go | 86 +++++++++++++++++++++++++++ _generated/omitzero_ext.go | 114 ++++++++++++++++++++++++++++++++++++ _generated/omitzero_test.go | 77 ++++++++++++++++++++++++ gen/elem.go | 6 +- gen/encode.go | 20 +++++-- gen/marshal.go | 18 ++++-- 6 files changed, 308 insertions(+), 13 deletions(-) create mode 100644 _generated/omitzero.go create mode 100644 _generated/omitzero_ext.go create mode 100644 _generated/omitzero_test.go diff --git a/_generated/omitzero.go b/_generated/omitzero.go new file mode 100644 index 00000000..1732ffb7 --- /dev/null +++ b/_generated/omitzero.go @@ -0,0 +1,86 @@ +package _generated + +import "time" + +//go:generate msgp + +// check some specific cases for omitzero + +type OmitZero0 struct { + AStruct OmitZeroA `msg:"astruct,omitempty"` // leave this one omitempty + BStruct OmitZeroA `msg:"bstruct,omitzero"` // and compare to this + AStructPtr *OmitZeroA `msg:"astructptr,omitempty"` // a pointer case omitempty + BStructPtr *OmitZeroA `msg:"bstructptr,omitzero"` // a pointer case omitzero + AExt OmitZeroExt `msg:"aext,omitzero"` // external type case + AExtPtr *OmitZeroExtPtr `msg:"aextptr,omitzero"` // external type pointer case + + // more + APtrNamedStr *NamedStringOZ `msg:"aptrnamedstr,omitzero"` + ANamedStruct NamedStructOZ `msg:"anamedstruct,omitzero"` + APtrNamedStruct *NamedStructOZ `msg:"aptrnamedstruct,omitzero"` + EmbeddableStruct `msg:",flatten,omitzero"` // embed flat + EmbeddableStructOZ `msg:"embeddablestruct2,omitzero"` // embed non-flat + ATime time.Time `msg:"atime,omitzero"` + + OmitZeroTuple OmitZeroTuple `msg:"ozt"` // the inside of a tuple should ignore both omitempty and omitzero +} + +type OmitZeroA struct { + A string `msg:"a,omitempty"` + B NamedStringOZ `msg:"b,omitzero"` + C NamedStringOZ `msg:"c,omitzero"` +} + +func (o *OmitZeroA) IsZero() bool { + if o == nil { + return true + } + return *o == (OmitZeroA{}) +} + +type NamedStructOZ struct { + A string `msg:"a,omitempty"` + B string `msg:"b,omitempty"` +} + +func (ns *NamedStructOZ) IsZero() bool { + if ns == nil { + return true + } + return *ns == (NamedStructOZ{}) +} + +type NamedStringOZ string + +func (ns *NamedStringOZ) IsZero() bool { + if ns == nil { + return true + } + return *ns == "" +} + +type EmbeddableStructOZ struct { + SomeEmbed string `msg:"someembed2,omitempty"` +} + +func (es EmbeddableStructOZ) IsZero() bool { return es == (EmbeddableStructOZ{}) } + +type EmbeddableStructOZ2 struct { + SomeEmbed2 string `msg:"someembed2,omitempty"` +} + +func (es EmbeddableStructOZ2) IsZero() bool { return es == (EmbeddableStructOZ2{}) } + +//msgp:tuple OmitZeroTuple + +// OmitZeroTuple is flagged for tuple output, it should ignore all omitempty and omitzero functionality +// since it's fundamentally incompatible. +type OmitZeroTuple struct { + FieldA string `msg:"fielda,omitempty"` + FieldB NamedStringOZ `msg:"fieldb,omitzero"` + FieldC NamedStringOZ `msg:"fieldc,omitzero"` +} + +type OmitZero1 struct { + T1 OmitZeroTuple `msg:"t1"` +} diff --git a/_generated/omitzero_ext.go b/_generated/omitzero_ext.go new file mode 100644 index 00000000..f711fd96 --- /dev/null +++ b/_generated/omitzero_ext.go @@ -0,0 +1,114 @@ +package _generated + +import ( + "github.com/tinylib/msgp/msgp" +) + +// this has "external" types that will show up +// as generic IDENT during code generation + +type OmitZeroExt struct { + a int // custom type +} + +// IsZero will return true if a is not positive +func (o OmitZeroExt) IsZero() bool { return o.a <= 0 } + +// EncodeMsg implements msgp.Encodable +func (o OmitZeroExt) EncodeMsg(en *msgp.Writer) (err error) { + if o.a > 0 { + return en.WriteInt(o.a) + } + return en.WriteNil() +} + +// DecodeMsg implements msgp.Decodable +func (o *OmitZeroExt) DecodeMsg(dc *msgp.Reader) (err error) { + if dc.IsNil() { + err = dc.ReadNil() + if err != nil { + return + } + o.a = 0 + return + } + o.a, err = dc.ReadInt() + return err +} + +// MarshalMsg implements msgp.Marshaler +func (o OmitZeroExt) MarshalMsg(b []byte) (ret []byte, err error) { + ret = msgp.Require(b, o.Msgsize()) + if o.a > 0 { + return msgp.AppendInt(ret, o.a), nil + } + return msgp.AppendNil(ret), nil +} + +// UnmarshalMsg implements msgp.Unmarshaler +func (o *OmitZeroExt) UnmarshalMsg(bts []byte) (ret []byte, err error) { + if msgp.IsNil(bts) { + bts, err = msgp.ReadNilBytes(bts) + return bts, err + } + o.a, bts, err = msgp.ReadIntBytes(bts) + return bts, err +} + +// Msgsize implements msgp.Msgsizer +func (o OmitZeroExt) Msgsize() (s int) { + return msgp.IntSize +} + +type OmitZeroExtPtr struct { + a int // custom type +} + +// IsZero will return true if a is nil or not positive +func (o *OmitZeroExtPtr) IsZero() bool { return o == nil || o.a <= 0 } + +// EncodeMsg implements msgp.Encodable +func (o *OmitZeroExtPtr) EncodeMsg(en *msgp.Writer) (err error) { + if o.a > 0 { + return en.WriteInt(o.a) + } + return en.WriteNil() +} + +// DecodeMsg implements msgp.Decodable +func (o *OmitZeroExtPtr) DecodeMsg(dc *msgp.Reader) (err error) { + if dc.IsNil() { + err = dc.ReadNil() + if err != nil { + return + } + o.a = 0 + return + } + o.a, err = dc.ReadInt() + return err +} + +// MarshalMsg implements msgp.Marshaler +func (o *OmitZeroExtPtr) MarshalMsg(b []byte) (ret []byte, err error) { + ret = msgp.Require(b, o.Msgsize()) + if o.a > 0 { + return msgp.AppendInt(ret, o.a), nil + } + return msgp.AppendNil(ret), nil +} + +// UnmarshalMsg implements msgp.Unmarshaler +func (o *OmitZeroExtPtr) UnmarshalMsg(bts []byte) (ret []byte, err error) { + if msgp.IsNil(bts) { + bts, err = msgp.ReadNilBytes(bts) + return bts, err + } + o.a, bts, err = msgp.ReadIntBytes(bts) + return bts, err +} + +// Msgsize implements msgp.Msgsizer +func (o *OmitZeroExtPtr) Msgsize() (s int) { + return msgp.IntSize +} diff --git a/_generated/omitzero_test.go b/_generated/omitzero_test.go new file mode 100644 index 00000000..749f6ba5 --- /dev/null +++ b/_generated/omitzero_test.go @@ -0,0 +1,77 @@ +package _generated + +import ( + "bytes" + "testing" +) + +func TestOmitZero(t *testing.T) { + + t.Run("OmitZeroExt_not_empty", func(t *testing.T) { + + z := OmitZero0{AExt: OmitZeroExt{a: 1}} + b, err := z.MarshalMsg(nil) + if err != nil { + t.Fatal(err) + } + if !bytes.Contains(b, []byte("aext")) { + t.Errorf("expected to find aext in bytes %X", b) + } + z = OmitZero0{} + _, err = z.UnmarshalMsg(b) + if err != nil { + t.Fatal(err) + } + if z.AExt.a != 1 { + t.Errorf("z.AExt.a expected 1 but got %d", z.AExt.a) + } + + }) + + t.Run("OmitZeroExt_negative", func(t *testing.T) { + + z := OmitZero0{AExt: OmitZeroExt{a: -1}} // negative value should act as empty, via IsEmpty() call + b, err := z.MarshalMsg(nil) + if err != nil { + t.Fatal(err) + } + if bytes.Contains(b, []byte("aext")) { + t.Errorf("expected to not find aext in bytes %X", b) + } + z = OmitZero0{} + _, err = z.UnmarshalMsg(b) + if err != nil { + t.Fatal(err) + } + if z.AExt.a != 0 { + t.Errorf("z.AExt.a expected 0 but got %d", z.AExt.a) + } + + }) + + t.Run("OmitZeroTuple", func(t *testing.T) { + + // make sure tuple encoding isn't affected by omitempty or omitzero + + z := OmitZero0{OmitZeroTuple: OmitZeroTuple{FieldA: "", FieldB: "", FieldC: "fcval"}} + b, err := z.MarshalMsg(nil) + if err != nil { + t.Fatal(err) + } + // verify the exact binary encoding, that the values follow each other without field names + if !bytes.Contains(b, []byte{0xA0, 0xA0, 0xA5, 'f', 'c', 'v', 'a', 'l'}) { + t.Errorf("failed to find expected bytes in %X", b) + } + z = OmitZero0{} + _, err = z.UnmarshalMsg(b) + if err != nil { + t.Fatal(err) + } + if z.OmitZeroTuple.FieldA != "" || + z.OmitZeroTuple.FieldB != "" || + z.OmitZeroTuple.FieldC != "fcval" { + t.Errorf("z.OmitZeroTuple unexpected value: %#v", z.OmitZeroTuple) + } + + }) +} diff --git a/gen/elem.go b/gen/elem.go index ef2c3fb9..5c48d5c2 100644 --- a/gen/elem.go +++ b/gen/elem.go @@ -190,11 +190,13 @@ type Elem interface { // This is true for slices and maps. AllowNil() bool - // IfZeroExpr returns the expression to compare to zero/empty - // for this type. It is meant to be used in an if statement + // IfZeroExpr returns the expression to compare to an empty value + // for this type, per the rules of the `omitempty` feature. + // It is meant to be used in an if statement // and may include the simple statement form followed by // semicolon and then the expression. // Returns "" if zero/empty not supported for this Elem. + // Note that this is NOT used by the `omitzero` feature. IfZeroExpr() string hidden() diff --git a/gen/encode.go b/gen/encode.go index 5f23691c..77f04ccf 100644 --- a/gen/encode.go +++ b/gen/encode.go @@ -129,12 +129,13 @@ func (e *encodeGen) structmap(s *Struct) { } omitempty := s.AnyHasTagPart("omitempty") + omitzero := s.AnyHasTagPart("omitzero") var fieldNVar string - if omitempty { + if omitempty || omitzero { fieldNVar = oeIdentPrefix + "Len" - e.p.printf("\n// omitempty: check for empty values") + e.p.printf("\n// check for omitted fields") e.p.printf("\n%s := uint32(%d)", fieldNVar, nfields) e.p.printf("\n%s", bm.typeDecl()) e.p.printf("\n_ = %s", bm.varname) @@ -147,6 +148,11 @@ func (e *encodeGen) structmap(s *Struct) { e.p.printf("\n%s--", fieldNVar) e.p.printf("\n%s", bm.setStmt(i)) e.p.printf("\n}") + } else if sf.HasTagPart("omitzero") { + e.p.printf("\nif %s.IsZero() {", sf.FieldElem.Varname()) + e.p.printf("\n%s--", fieldNVar) + e.p.printf("\n%s", bm.setStmt(i)) + e.p.printf("\n}") } } @@ -164,7 +170,7 @@ func (e *encodeGen) structmap(s *Struct) { } else { - // non-omitempty version + // non-omit version data = msgp.AppendMapHeader(nil, uint32(nfields)) e.p.printf("\n// map header, size %d", nfields) e.Fuse(data) @@ -179,10 +185,12 @@ func (e *encodeGen) structmap(s *Struct) { return } - // if field is omitempty, wrap with if statement based on the emptymask - oeField := omitempty && s.Fields[i].HasTagPart("omitempty") && s.Fields[i].FieldElem.IfZeroExpr() != "" + // if field is omitempty or omitzero, wrap with if statement based on the emptymask + oeField := (omitempty || omitzero) && + ((s.Fields[i].HasTagPart("omitempty") && s.Fields[i].FieldElem.IfZeroExpr() != "") || + s.Fields[i].HasTagPart("omitzero")) if oeField { - e.p.printf("\nif %s == 0 { // if not empty", bm.readExpr(i)) + e.p.printf("\nif %s == 0 { // if not omitted", bm.readExpr(i)) } data = msgp.AppendString(nil, s.Fields[i].FieldTag) diff --git a/gen/marshal.go b/gen/marshal.go index 79bede4b..5782f787 100644 --- a/gen/marshal.go +++ b/gen/marshal.go @@ -124,12 +124,13 @@ func (m *marshalGen) mapstruct(s *Struct) { } omitempty := s.AnyHasTagPart("omitempty") + omitzero := s.AnyHasTagPart("omitzero") var fieldNVar string - if omitempty { + if omitempty || omitzero { fieldNVar = oeIdentPrefix + "Len" - m.p.printf("\n// omitempty: check for empty values") + m.p.printf("\n// check for omitted fields") m.p.printf("\n%s := uint32(%d)", fieldNVar, nfields) m.p.printf("\n%s", bm.typeDecl()) m.p.printf("\n_ = %s", bm.varname) @@ -142,6 +143,11 @@ func (m *marshalGen) mapstruct(s *Struct) { m.p.printf("\n%s--", fieldNVar) m.p.printf("\n%s", bm.setStmt(i)) m.p.printf("\n}") + } else if sf.HasTagPart("omitzero") { + m.p.printf("\nif %s.IsZero() {", sf.FieldElem.Varname()) + m.p.printf("\n%s--", fieldNVar) + m.p.printf("\n%s", bm.setStmt(i)) + m.p.printf("\n}") } } @@ -174,10 +180,12 @@ func (m *marshalGen) mapstruct(s *Struct) { return } - // if field is omitempty, wrap with if statement based on the emptymask - oeField := s.Fields[i].HasTagPart("omitempty") && s.Fields[i].FieldElem.IfZeroExpr() != "" + // if field is omitempty or omitzero, wrap with if statement based on the emptymask + oeField := (omitempty || omitzero) && + ((s.Fields[i].HasTagPart("omitempty") && s.Fields[i].FieldElem.IfZeroExpr() != "") || + s.Fields[i].HasTagPart("omitzero")) if oeField { - m.p.printf("\nif %s == 0 { // if not empty", bm.readExpr(i)) + m.p.printf("\nif %s == 0 { // if not omitted", bm.readExpr(i)) } data = msgp.AppendString(nil, s.Fields[i].FieldTag)