From 0d142f9697e5a4d416af969ee338470dc9c05d5e Mon Sep 17 00:00:00 2001 From: Vu Dinh Date: Tue, 19 Dec 2023 10:21:41 -0500 Subject: [PATCH 1/2] Add encoding option to specify how omitempty fields are encoded The current behavior in the library is to omits if field value would encode as empty JSON value. This behavior is different from the bahavior in golang encoding/json which omits if field value is an empty Go value (defined as false, 0, a nil pointer, a nil interface value, and any empty array, slice, map, or string). The OmitEmptyMode = 1 is added to encoding option so that cbor would decode omitempty fields similarly to encoding/json in go. Signed-off-by: Vu Dinh --- encode.go | 32 ++++++++++++++++++++++++++++++-- encode_test.go | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 2 deletions(-) diff --git a/encode.go b/encode.go index f329f5da..3d193acf 100644 --- a/encode.go +++ b/encode.go @@ -283,6 +283,24 @@ func (m NilContainersMode) valid() bool { 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 ( + // OmitEmptyV1 specifies that the field should be omitted from the encoding + // if the field has an empty 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 with the current "v1" encoding/json library in go. + OmitEmptyV1 OmitEmptyMode = 1 + + maxOmitEmptyMode = 2 +) + +func (om OmitEmptyMode) valid() bool { + return om >= 0 && om < maxOmitEmptyMode +} + // EncOptions specifies encoding options. type EncOptions struct { // Sort specifies sorting order. @@ -316,6 +334,9 @@ type EncOptions struct { // 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, @@ -491,6 +512,9 @@ func (opts EncOptions) encMode() (*encMode, error) { 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, @@ -525,6 +549,7 @@ type encMode struct { indefLength IndefLengthMode nilContainers NilContainersMode tagsMd TagsMode + omitEmpty OmitEmptyMode } var defaultEncMode = &encMode{} @@ -541,6 +566,7 @@ func (em *encMode) EncOptions() EncOptions { TimeTag: em.timeTag, IndefLength: em.indefLength, TagsMd: em.tagsMd, + OmitEmpty: em.omitEmpty, } } @@ -1098,7 +1124,6 @@ func encodeStruct(e *encoderBuffer, em *encMode, v reflect.Value) (err error) { continue } } - if f.omitEmpty { empty, err := f.ief(fv) if err != nil { @@ -1106,7 +1131,10 @@ func encodeStruct(e *encoderBuffer, em *encMode, v reflect.Value) (err error) { return err } if empty { - continue + // If the emptyMode is set to 1 and the field is a struct, preserve the field name + if !(em.omitEmpty == OmitEmptyV1 && f.typ.Kind() != reflect.Struct) { + continue + } } } diff --git a/encode_test.go b/encode_test.go index e5d22e08..d1eb9843 100644 --- a/encode_test.go +++ b/encode_test.go @@ -1105,6 +1105,53 @@ 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": null, "m": {}, "p": nil, "intf": nil, "str": {}, "stro": {}} + want := []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, _ := EncOptions{OmitEmpty: OmitEmptyV1}.EncMode() + dm, _ := DecOptions{}.DecMode() + testRoundTrip(t, []roundTripTest{{"default values", v, want}}, em, dm) +} + func TestOmitEmptyForNestedStruct(t *testing.T) { type T1 struct { Bo bool `cbor:"bo,omitempty"` From f95914be2b75b82f3fa861f721a78a467600d3fc Mon Sep 17 00:00:00 2001 From: Vu Dinh Date: Wed, 3 Jan 2024 10:36:14 -0500 Subject: [PATCH 2/2] Address feedbacks Signed-off-by: Vu Dinh --- encode.go | 60 ++++++++++++++++++++++++++++---------------------- encode_test.go | 30 ++++++++++++++++++++----- 2 files changed, 58 insertions(+), 32 deletions(-) diff --git a/encode.go b/encode.go index 3d193acf..82f10bc5 100644 --- a/encode.go +++ b/encode.go @@ -288,13 +288,19 @@ func (m NilContainersMode) valid() bool { type OmitEmptyMode int const ( - // OmitEmptyV1 specifies that the field should be omitted from the encoding - // if the field has an empty 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 with the current "v1" encoding/json library in go. - OmitEmptyV1 OmitEmptyMode = 1 - - maxOmitEmptyMode = 2 + // 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 { @@ -526,6 +532,7 @@ func (opts EncOptions) encMode() (*encMode, error) { indefLength: opts.IndefLength, nilContainers: opts.NilContainers, tagsMd: opts.TagsMd, + omitEmpty: opts.OmitEmpty, } return &em, nil } @@ -626,7 +633,7 @@ func putEncoderBuffer(e *encoderBuffer) { } 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} @@ -1125,16 +1132,13 @@ func encodeStruct(e *encoderBuffer, em *encMode, v reflect.Value) (err error) { } } if f.omitEmpty { - empty, err := f.ief(fv) + empty, err := f.ief(em, fv) if err != nil { putEncoderBuffer(kve) return err } if empty { - // If the emptyMode is set to 1 and the field is a struct, preserve the field name - if !(em.omitEmpty == OmitEmptyV1 && f.typ.Kind() != reflect.Struct) { - continue - } + continue } } @@ -1429,52 +1433,56 @@ func getEncodeIndirectValueFunc(t reflect.Type) encodeFunc { } } -func alwaysNotEmpty(_ reflect.Value) (empty bool, err error) { +func alwaysNotEmpty(em *encMode, _ reflect.Value) (empty bool, err error) { return false, nil } -func isEmptyBool(v reflect.Value) (bool, error) { +func isEmptyBool(em *encMode, v reflect.Value) (bool, error) { return !v.Bool(), nil } -func isEmptyInt(v reflect.Value) (bool, error) { +func isEmptyInt(em *encMode, v reflect.Value) (bool, error) { return v.Int() == 0, nil } -func isEmptyUint(v reflect.Value) (bool, error) { +func isEmptyUint(em *encMode, v reflect.Value) (bool, error) { return v.Uint() == 0, nil } -func isEmptyFloat(v reflect.Value) (bool, error) { +func isEmptyFloat(em *encMode, v reflect.Value) (bool, error) { return v.Float() == 0.0, nil } -func isEmptyString(v reflect.Value) (bool, error) { +func isEmptyString(em *encMode, v reflect.Value) (bool, error) { return v.Len() == 0, nil } -func isEmptySlice(v reflect.Value) (bool, error) { +func isEmptySlice(em *encMode, v reflect.Value) (bool, error) { return v.Len() == 0, nil } -func isEmptyMap(v reflect.Value) (bool, error) { +func isEmptyMap(em *encMode, v reflect.Value) (bool, error) { return v.Len() == 0, nil } -func isEmptyPtr(v reflect.Value) (bool, error) { +func isEmptyPtr(em *encMode, v reflect.Value) (bool, error) { return v.IsNil(), nil } -func isEmptyIntf(v reflect.Value) (bool, error) { +func isEmptyIntf(em *encMode, v reflect.Value) (bool, error) { return v.IsNil(), nil } -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 } @@ -1501,7 +1509,7 @@ func isEmptyStruct(v reflect.Value) (bool, error) { } } - empty, err := f.ief(fv) + empty, err := f.ief(em, fv) if err != nil { return false, err } @@ -1512,7 +1520,7 @@ func isEmptyStruct(v reflect.Value) (bool, error) { 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()) diff --git a/encode_test.go b/encode_test.go index d1eb9843..50d14eba 100644 --- a/encode_test.go +++ b/encode_test.go @@ -1131,8 +1131,25 @@ func TestOmitEmptyMode(t *testing.T) { } v := T{} - // {"b": false, "ui": 0, "i":0, "f": 0, "s": "", "slc": null, "m": {}, "p": nil, "intf": nil, "str": {}, "stro": {}} - want := []byte{ + + // {"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, @@ -1143,13 +1160,14 @@ func TestOmitEmptyMode(t *testing.T) { 0x61, 0x6d, 0xf6, 0x61, 0x70, 0xf6, 0x64, 0x69, 0x6e, 0x74, 0x66, 0xf6, - 0x63, 0x73, 0x74, 0x72, - 0xa0, + 0x63, 0x73, 0x74, 0x72, 0xa0, } - em, _ := EncOptions{OmitEmpty: OmitEmptyV1}.EncMode() + em_govalue, _ := EncOptions{OmitEmpty: OmitEmptyGoValue}.EncMode() + em_cborvalue, _ := EncOptions{OmitEmpty: OmitEmptyCBORValue}.EncMode() dm, _ := DecOptions{}.DecMode() - testRoundTrip(t, []roundTripTest{{"default values", v, want}}, em, dm) + 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) {