Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

omitisempty support #326

Closed
Closed
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
60 changes: 60 additions & 0 deletions _generated/omitisempty.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package _generated

import "time"

//go:generate msgp

// check some specific cases for omitisempty

type OmitIsEmpty0 struct {
ABool bool `msg:"abool,omitisempty"` // unaliased primitives should act exactly as omitempty
ABoolPtr *bool `msg:"aboolptr,omitisempty"` // pointers to primitives will do unnecessary interface check for now but function correctly
AStruct OmitIsEmptyA `msg:"astruct,omitempty"` // leave this one omitempty
BStruct OmitIsEmptyA `msg:"bstruct,omitisempty"` // and compare to this
AStructPtr *OmitIsEmptyA `msg:"astructptr,omitempty"` // a pointer case omitempty
BStructPtr *OmitIsEmptyA `msg:"bstructptr,omitisempty"` // a pointer case omitisempty
AExt OmitIsEmptyExt `msg:"aext,omitisempty"` // external type case
AExtPtr *OmitIsEmptyExtPtr `msg:"aextptr,omitisempty"` // external type pointer case

// check a bunch of other types just to make sure none of the output breaks on compilation

AInt int `msg:"aint,omitisempty"`
AInt8 int8 `msg:"aint8,omitisempty"`
AInt16 int16 `msg:"aint16,omitisempty"`
AInt32 int32 `msg:"aint32,omitisempty"`
AInt64 int64 `msg:"aint64,omitisempty"`
AUint uint `msg:"auint,omitisempty"`
AUint8 uint8 `msg:"auint8,omitisempty"`
AUint16 uint16 `msg:"auint16,omitisempty"`
AUint32 uint32 `msg:"auint32,omitisempty"`
AUint64 uint64 `msg:"auint64,omitisempty"`
AFloat32 float32 `msg:"afloat32,omitisempty"`
AFloat64 float64 `msg:"afloat64,omitisempty"`
AComplex64 complex64 `msg:"acomplex64,omitisempty"`
AComplex128 complex128 `msg:"acomplex128,omitisempty"`
ANamedBool bool `msg:"anamedbool,omitisempty"`
ANamedInt int `msg:"anamedint,omitisempty"`
ANamedFloat64 float64 `msg:"anamedfloat64,omitisempty"`
AMapStrStr map[string]string `msg:"amapstrstr,omitisempty"`
APtrNamedStr *NamedString `msg:"aptrnamedstr,omitisempty"`
AString string `msg:"astring,omitisempty"`
ANamedString string `msg:"anamedstring,omitisempty"`
AByteSlice []byte `msg:"abyteslice,omitisempty"`
ASliceString []string `msg:"aslicestring,omitisempty"`
ASliceNamedString []NamedString `msg:"aslicenamedstring,omitisempty"`
ANamedStruct NamedStruct `msg:"anamedstruct,omitisempty"`
APtrNamedStruct *NamedStruct `msg:"aptrnamedstruct,omitisempty"`
AUnnamedStruct struct {
A string `msg:"a,omitisempty"`
} `msg:"aunnamedstruct,omitisempty"` // omitisempty not supported on unnamed struct
EmbeddableStruct `msg:",flatten,omitisempty"` // embed flat
EmbeddableStruct2 `msg:"embeddablestruct2,omitisempty"` // embed non-flat
AArrayInt [5]int `msg:"aarrayint,omitisempty"` // not supported
ATime time.Time `msg:"atime,omitisempty"`
}

type OmitIsEmptyA struct {
A int `msg:"a,omitempty"`
B int `msg:"b,omitisempty"`
C string `msg:"c,omitisempty"`
}
114 changes: 114 additions & 0 deletions _generated/omitisempty_ext.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package _generated

import (
"github.com/tinylib/msgp/msgp"
)

// this has "external" types that will show up
// has generic IDENT during code generation

type OmitIsEmptyExt struct {
a int // custom type
}

// IsEmpty will return true if a is positive
func (o OmitIsEmptyExt) IsEmpty() bool { return o.a <= 0 }

// EncodeMsg implements msgp.Encodable
func (o OmitIsEmptyExt) EncodeMsg(en *msgp.Writer) (err error) {
if o.a > 0 {
return en.WriteInt(o.a)
}
return en.WriteNil()
}

// DecodeMsg implements msgp.Decodable
func (o *OmitIsEmptyExt) 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 OmitIsEmptyExt) 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 *OmitIsEmptyExt) 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 OmitIsEmptyExt) Msgsize() (s int) {
return msgp.IntSize
}

type OmitIsEmptyExtPtr struct {
a int // custom type
}

// IsEmpty will return true if a is positive
func (o *OmitIsEmptyExtPtr) IsEmpty() bool { return o == nil || o.a <= 0 }

// EncodeMsg implements msgp.Encodable
func (o *OmitIsEmptyExtPtr) EncodeMsg(en *msgp.Writer) (err error) {
if o.a > 0 {
return en.WriteInt(o.a)
}
return en.WriteNil()
}

// DecodeMsg implements msgp.Decodable
func (o *OmitIsEmptyExtPtr) 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 *OmitIsEmptyExtPtr) 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 *OmitIsEmptyExtPtr) 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 *OmitIsEmptyExtPtr) Msgsize() (s int) {
return msgp.IntSize
}
51 changes: 51 additions & 0 deletions _generated/omitisempty_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package _generated

import (
"bytes"
"testing"
)

func TestOmitIsEmpty(t *testing.T) {

t.Run("OmitIsEmptyExt_not_empty", func(t *testing.T) {

z := OmitIsEmpty0{AExt: OmitIsEmptyExt{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 = OmitIsEmpty0{}
_, 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("OmitIsEmptyExt_negative", func(t *testing.T) {

z := OmitIsEmpty0{AExt: OmitIsEmptyExt{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 = OmitIsEmpty0{}
_, 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)
}

})
}
88 changes: 74 additions & 14 deletions gen/elem.go
Original file line number Diff line number Diff line change
Expand Up @@ -194,8 +194,12 @@ type Elem interface {
// for this type. It is meant to be used in an if statement
// and may include the simple statement form followed by
// semicolon and then the expression.
// The useIsEmptyIntf argument indicates if this type should
// use a check against a type that is expected to be declared
// as `type isEmpty interface { IsEmpty() bool }`, if supported
// for this type. This is used by the `omitisempty` option.
// Returns "" if zero/empty not supported for this Elem.
IfZeroExpr() string
IfZeroExpr(useIsEmptyIntf bool) string

hidden()
}
Expand Down Expand Up @@ -252,8 +256,13 @@ func (a *Array) Complexity() int { return 1 + a.Els.Complexity() }
// ZeroExpr returns the zero/empty expression or empty string if not supported. Unsupported for this case.
func (a *Array) ZeroExpr() string { return "" }

// IfZeroExpr unsupported
func (a *Array) IfZeroExpr() string { return "" }
// IfZeroExpr only supported via isEmpty if aliased
func (a *Array) IfZeroExpr(useIsEmptyIntf bool) string {
if useIsEmptyIntf && a.common.alias != "" {
return ifEmptyIntfExpr(a.Varname(), "")
}
return ""
}

// Map is a map[string]Elem
type Map struct {
Expand Down Expand Up @@ -297,7 +306,14 @@ func (m *Map) Complexity() int { return 2 + m.Value.Complexity() }
func (m *Map) ZeroExpr() string { return "nil" }

// IfZeroExpr returns the expression to compare to zero/empty.
func (m *Map) IfZeroExpr() string { return m.Varname() + " == nil" }
func (m *Map) IfZeroExpr(useIsEmptyIntf bool) (ret string) {
ret = m.Varname() + " == nil"
if useIsEmptyIntf && m.common.alias != "" {
// can have interface check if requested and alias
ret = ifEmptyIntfExpr(m.Varname(), ret)
}
return ret
}

// AllowNil is true for maps.
func (m *Map) AllowNil() bool { return true }
Expand Down Expand Up @@ -341,7 +357,14 @@ func (s *Slice) Complexity() int {
func (s *Slice) ZeroExpr() string { return "nil" }

// IfZeroExpr returns the expression to compare to zero/empty.
func (s *Slice) IfZeroExpr() string { return s.Varname() + " == nil" }
func (s *Slice) IfZeroExpr(useIsEmptyIntf bool) (ret string) {
ret = s.Varname() + " == nil"
if useIsEmptyIntf && s.common.alias != "" {
// can have interface check if requested and alias
ret = ifEmptyIntfExpr(s.Varname(), ret)
}
return ret
}

// AllowNil is true for slices.
func (s *Slice) AllowNil() bool { return true }
Expand Down Expand Up @@ -404,7 +427,20 @@ func (s *Ptr) Needsinit() bool {
func (s *Ptr) ZeroExpr() string { return "nil" }

// IfZeroExpr returns the expression to compare to zero/empty.
func (s *Ptr) IfZeroExpr() string { return s.Varname() + " == nil" }
func (s *Ptr) IfZeroExpr(useIsEmptyIntf bool) (ret string) {
ret = s.Varname() + " == nil"

if useIsEmptyIntf {
// TODO: this could be improved by inspecting the type being pointed to
// more closely to remove some impossible cases (e.g. `*string` will
// never implement isEmpty), but this keeps things simple
// and should function correctly regardless of target type. Plus,
// useIsEmptyIntf is opt-in anyway based on omitisempty struct tag.
ret = ifEmptyIntfExpr(s.Varname(), ret)
}
return ret

}

type Struct struct {
common
Expand Down Expand Up @@ -459,11 +495,16 @@ func (s *Struct) ZeroExpr() string {
}

// IfZeroExpr returns the expression to compare to zero/empty.
func (s *Struct) IfZeroExpr() string {
if s.alias == "" {
return "" // structs with no names not supported (for now)
func (s *Struct) IfZeroExpr(useIsEmptyIntf bool) (ret string) {
z := s.ZeroExpr()
if z != "" {
ret = s.Varname() + " == " + z
}
if useIsEmptyIntf && s.common.alias != "" {
// interface check if requested and has alias
ret = ifEmptyIntfExpr(s.Varname(), ret)
}
return s.Varname() + " == " + s.ZeroExpr()
return ret
}

// AnyHasTagPart returns true if HasTagPart(p) is true for any field.
Expand Down Expand Up @@ -679,12 +720,18 @@ func (s *BaseElem) ZeroExpr() string {
}

// IfZeroExpr returns the expression to compare to zero/empty.
func (s *BaseElem) IfZeroExpr() string {
func (s *BaseElem) IfZeroExpr(useIsEmptyIntf bool) (ret string) {
z := s.ZeroExpr()
if z == "" {
return ""
if z != "" {
ret = s.Varname() + " == " + z
}
return s.Varname() + " == " + z
if useIsEmptyIntf && s.common.alias != "" {
// aliased base types can have interface check if requested,
// this specifically includes the IDENT case of a type we
// are not code generating for
ret = ifEmptyIntfExpr(s.Varname(), ret)
}
return ret
}

func (k Primitive) String() string {
Expand Down Expand Up @@ -758,3 +805,16 @@ func writeStructFields(s []StructField, name string) {
func coerceArraySize(asz string) string {
return fmt.Sprintf("uint32(%s)", asz)
}

// ifEmptyIntfExpr is a shorthand for checking empty against IsEmpty() plus
// whatever custom expression
func ifEmptyIntfExpr(varExpr, eqCheck string) string {
ret := "checkIsEmptyIntf(" + varExpr + ")"
// Note: instead of calling checkIsEmptyIntf, it is possible to do this check inline
// on any type, but it's quite messy:
// ret := "ie, ok := (func(v interface{})interface{}{return v})(" + varExpr + ").(isEmpty); " + "(ok && ie.IsEmpty())"
if eqCheck != "" {
ret = "(" + eqCheck + ") || " + ret
}
return ret
}
Loading