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

Add encoding option to specify how omitempty fields are encoded #453

Merged
merged 2 commits into from
Jan 7, 2024
Merged
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
68 changes: 52 additions & 16 deletions encode.go
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,30 @@
return m >= 0 && m < maxNilContainersMode
}

// OmitEmptyMode specifies how to encode the fields with omitempty tag
// The default behavior omits if field value would encode as empty JSON value
type OmitEmptyMode int

const (
// OmitEmptyCBORValue specifies that fields tagged with "omitempty" should be
// omitted from encoding if the field would be encoded as an empty CBOR value,
// such as CBOR false, 0, 0.0, nil, empty byte, empty string, empty array,
// empty struct or empty map.
OmitEmptyCBORValue OmitEmptyMode = iota

// OmitEmptyGoValue specifies that fields tagged with "omitempty" should be
// omitted from encoding if the field has an empty Go value, defined as false, 0, a nil pointer,
// a nil interface value, and any empty array, slice, map, or string.
// This behavior is the same as the current (aka v1) encoding/json package included in Go.
OmitEmptyGoValue

maxOmitEmptyMode
)

func (om OmitEmptyMode) valid() bool {
return om >= 0 && om < maxOmitEmptyMode
}

// EncOptions specifies encoding options.
type EncOptions struct {
// Sort specifies sorting order.
Expand Down Expand Up @@ -316,6 +340,9 @@

// TagsMd specifies whether to allow CBOR tags (major type 6).
TagsMd TagsMode

// OmitEmptyMode specifies how to encode the fields with omitempty tag
OmitEmpty OmitEmptyMode
}

// CanonicalEncOptions returns EncOptions for "Canonical CBOR" encoding,
Expand Down Expand Up @@ -491,6 +518,9 @@
if opts.TagsMd == TagsForbidden && opts.TimeTag == EncTagRequired {
return nil, errors.New("cbor: cannot set TagsMd to TagsForbidden when TimeTag is EncTagRequired")
}
if !opts.OmitEmpty.valid() {
return nil, errors.New("cbor: invalid OmitEmpty " + strconv.Itoa(int(opts.OmitEmpty)))
}
em := encMode{
sort: opts.Sort,
shortestFloat: opts.ShortestFloat,
dinhxuanvu marked this conversation as resolved.
Show resolved Hide resolved
Expand All @@ -502,6 +532,7 @@
indefLength: opts.IndefLength,
nilContainers: opts.NilContainers,
tagsMd: opts.TagsMd,
omitEmpty: opts.OmitEmpty,
}
return &em, nil
}
Expand All @@ -525,6 +556,7 @@
indefLength IndefLengthMode
nilContainers NilContainersMode
tagsMd TagsMode
omitEmpty OmitEmptyMode
}

var defaultEncMode = &encMode{}
Expand All @@ -541,6 +573,7 @@
TimeTag: em.timeTag,
IndefLength: em.indefLength,
TagsMd: em.tagsMd,
OmitEmpty: em.omitEmpty,
}
}

Expand Down Expand Up @@ -600,7 +633,7 @@
}

type encodeFunc func(e *encoderBuffer, em *encMode, v reflect.Value) error
type isEmptyFunc func(v reflect.Value) (empty bool, err error)
type isEmptyFunc func(em *encMode, v reflect.Value) (empty bool, err error)

var (
cborFalse = []byte{0xf4}
Expand Down Expand Up @@ -1098,9 +1131,8 @@
continue
}
}

if f.omitEmpty {
empty, err := f.ief(fv)
empty, err := f.ief(em, fv)
if err != nil {
putEncoderBuffer(kve)
return err
Expand Down Expand Up @@ -1401,52 +1433,56 @@
}
}

func alwaysNotEmpty(_ reflect.Value) (empty bool, err error) {
func alwaysNotEmpty(em *encMode, _ reflect.Value) (empty bool, err error) {
return false, nil
}

Check failure on line 1438 in encode.go

View workflow job for this annotation

GitHub Actions / Lint

unused-parameter: parameter 'em' seems to be unused, consider removing or renaming it as _ (revive)

func isEmptyBool(v reflect.Value) (bool, error) {
func isEmptyBool(em *encMode, v reflect.Value) (bool, error) {
return !v.Bool(), nil
}

Check failure on line 1442 in encode.go

View workflow job for this annotation

GitHub Actions / Lint

unused-parameter: parameter 'em' seems to be unused, consider removing or renaming it as _ (revive)

func isEmptyInt(v reflect.Value) (bool, error) {
func isEmptyInt(em *encMode, v reflect.Value) (bool, error) {
return v.Int() == 0, nil
}

Check failure on line 1446 in encode.go

View workflow job for this annotation

GitHub Actions / Lint

unused-parameter: parameter 'em' seems to be unused, consider removing or renaming it as _ (revive)

func isEmptyUint(v reflect.Value) (bool, error) {
func isEmptyUint(em *encMode, v reflect.Value) (bool, error) {
return v.Uint() == 0, nil
}

Check failure on line 1450 in encode.go

View workflow job for this annotation

GitHub Actions / Lint

unused-parameter: parameter 'em' seems to be unused, consider removing or renaming it as _ (revive)

func isEmptyFloat(v reflect.Value) (bool, error) {
func isEmptyFloat(em *encMode, v reflect.Value) (bool, error) {
return v.Float() == 0.0, nil
}

Check failure on line 1454 in encode.go

View workflow job for this annotation

GitHub Actions / Lint

unused-parameter: parameter 'em' seems to be unused, consider removing or renaming it as _ (revive)

func isEmptyString(v reflect.Value) (bool, error) {
func isEmptyString(em *encMode, v reflect.Value) (bool, error) {
return v.Len() == 0, nil
}

Check failure on line 1458 in encode.go

View workflow job for this annotation

GitHub Actions / Lint

unused-parameter: parameter 'em' seems to be unused, consider removing or renaming it as _ (revive)

func isEmptySlice(v reflect.Value) (bool, error) {
func isEmptySlice(em *encMode, v reflect.Value) (bool, error) {
return v.Len() == 0, nil
}

Check failure on line 1462 in encode.go

View workflow job for this annotation

GitHub Actions / Lint

unused-parameter: parameter 'em' seems to be unused, consider removing or renaming it as _ (revive)

func isEmptyMap(v reflect.Value) (bool, error) {
func isEmptyMap(em *encMode, v reflect.Value) (bool, error) {
return v.Len() == 0, nil
}

Check failure on line 1466 in encode.go

View workflow job for this annotation

GitHub Actions / Lint

unused-parameter: parameter 'em' seems to be unused, consider removing or renaming it as _ (revive)

func isEmptyPtr(v reflect.Value) (bool, error) {
func isEmptyPtr(em *encMode, v reflect.Value) (bool, error) {
return v.IsNil(), nil
}

Check failure on line 1470 in encode.go

View workflow job for this annotation

GitHub Actions / Lint

unused-parameter: parameter 'em' seems to be unused, consider removing or renaming it as _ (revive)

func isEmptyIntf(v reflect.Value) (bool, error) {
func isEmptyIntf(em *encMode, v reflect.Value) (bool, error) {
return v.IsNil(), nil
}

Check failure on line 1474 in encode.go

View workflow job for this annotation

GitHub Actions / Lint

unused-parameter: parameter 'em' seems to be unused, consider removing or renaming it as _ (revive)

func isEmptyStruct(v reflect.Value) (bool, error) {
func isEmptyStruct(em *encMode, v reflect.Value) (bool, error) {
structType, err := getEncodingStructType(v.Type())
if err != nil {
return false, err
}

if em.omitEmpty == OmitEmptyGoValue {
return false, nil
}

if structType.toArray {
return len(structType.fields) == 0, nil
}
Expand All @@ -1473,7 +1509,7 @@
}
}

empty, err := f.ief(fv)
empty, err := f.ief(em, fv)
if err != nil {
return false, err
}
Expand All @@ -1484,7 +1520,7 @@
return true, nil
}

func isEmptyBinaryMarshaler(v reflect.Value) (bool, error) {
func isEmptyBinaryMarshaler(em *encMode, v reflect.Value) (bool, error) {
m, ok := v.Interface().(encoding.BinaryMarshaler)
if !ok {
pv := reflect.New(v.Type())
Expand Down
65 changes: 65 additions & 0 deletions encode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1105,6 +1105,71 @@ func TestOmitEmptyForStruct2(t *testing.T) {
testRoundTrip(t, []roundTripTest{{"non-default values", v, want}}, em, dm)
}

func TestOmitEmptyMode(t *testing.T) {
type T1 struct{}
type T struct {
B bool `cbor:"b"`
Bo bool `cbor:"bo,omitempty"`
UI uint `cbor:"ui"`
UIo uint `cbor:"uio,omitempty"`
I int `cbor:"i"`
Io int `cbor:"io,omitempty"`
F float64 `cbor:"f"`
Fo float64 `cbor:"fo,omitempty"`
S string `cbor:"s"`
So string `cbor:"so,omitempty"`
Slc []string `cbor:"slc"`
Slco []string `cbor:"slco,omitempty"`
M map[int]string `cbor:"m"`
Mo map[int]string `cbor:"mo,omitempty"`
P *int `cbor:"p"`
Po *int `cbor:"po,omitempty"`
Intf interface{} `cbor:"intf"`
Intfo interface{} `cbor:"intfo,omitempty"`
Str T1 `cbor:"str"`
Stro T1 `cbor:"stro,omitempty"`
}

v := T{}

// {"b": false, "ui": 0, "i":0, "f": 0, "s": "", "slc": nil, "m": nil, "p": nil, "intf": nil, "str": {}, "stro": {}}
wantGoValue := []byte{
0xab,
0x61, 0x62, 0xf4,
0x62, 0x75, 0x69, 0x00,
0x61, 0x69, 0x00,
0x61, 0x66, 0xfb, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x61, 0x73, 0x60,
0x63, 0x73, 0x6c, 0x63, 0xf6,
0x61, 0x6d, 0xf6,
0x61, 0x70, 0xf6,
0x64, 0x69, 0x6e, 0x74, 0x66, 0xf6,
0x63, 0x73, 0x74, 0x72, 0xa0,
0x64, 0x73, 0x74, 0x72, 0x6F, 0xa0,
}

// {"b": false, "ui": 0, "i":0, "f": 0, "s": "", "slc": nil, "m": nil, "p": nil, "intf": nil, "str": nil, "stro": nil}
wantCborValue := []byte{
0xaa,
0x61, 0x62, 0xf4,
0x62, 0x75, 0x69, 0x00,
0x61, 0x69, 0x00,
0x61, 0x66, 0xfb, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x61, 0x73, 0x60,
0x63, 0x73, 0x6c, 0x63, 0xf6,
0x61, 0x6d, 0xf6,
0x61, 0x70, 0xf6,
0x64, 0x69, 0x6e, 0x74, 0x66, 0xf6,
0x63, 0x73, 0x74, 0x72, 0xa0,
}

em_govalue, _ := EncOptions{OmitEmpty: OmitEmptyGoValue}.EncMode()
em_cborvalue, _ := EncOptions{OmitEmpty: OmitEmptyCBORValue}.EncMode()
dm, _ := DecOptions{}.DecMode()
testRoundTrip(t, []roundTripTest{{"OmitEmptyGoValue default values", v, wantGoValue}}, em_govalue, dm)
testRoundTrip(t, []roundTripTest{{"OmitEmptyCBORValue values", v, wantCborValue}}, em_cborvalue, dm)
}

func TestOmitEmptyForNestedStruct(t *testing.T) {
type T1 struct {
Bo bool `cbor:"bo,omitempty"`
Expand Down
Loading