Add omitempty to Go marshal (#2784)

If you provide an omitempty tag and the Go value is the zero/empty value
then we do not include that field in the resulting Noms struct.

Towards #2376
This commit is contained in:
Erik Arvidsson
2016-10-28 17:36:02 -07:00
committed by GitHub
parent 9dafe3da6c
commit 817af58a5b
3 changed files with 222 additions and 17 deletions

View File

@@ -225,8 +225,9 @@ func structDecoder(t reflect.Type) decoderFunc {
continue
}
name, _ := parseTags(tags, f)
fields = append(fields, decField{
name: getFieldName(tags, f),
name: name,
decoder: typeDecoder(f.Type),
index: i,
})

View File

@@ -31,6 +31,10 @@ import (
//
// Struct values are encoded as Noms structs (types.Struct). Each exported Go struct field becomes a member of the Noms struct unless
// - the field's tag is "-"
// - the field is empty and its tag specifies the "omitempty" option.
//
// The empty values are false, 0, any nil pointer or interface value, and any array, slice, map, or string of length zero.
//
// The Noms struct default field name is the Go struct field name where the first character is lower cased,
// but can be specified in the Go struct field's tag value. The "noms" key in
// the Go struct field's tag value is the field name. Examples:
@@ -44,7 +48,13 @@ import (
// // Field appears in a Noms struct as key "myName".
// Field int `noms:"myName"`
//
// Unlike encoding/json Marshal, this does not support "omitempty".
// // Field appears in a Noms struct as key "myName" and the field is
// // omitted from the object if its value is empty, as defined above.
// Field int `noms:"myName,omitempty"
//
// // Field appears in a Noms struct as key "field" and the field is
// // omitted from the object if its value is empty, as defined above.
// Field int `noms:",omitempty"
//
// The name of the Noms struct is the name of the Go struct where the first character is changed to upper case.
//
@@ -182,7 +192,11 @@ func structEncoder(t reflect.Type, parentStructTypes []reflect.Type) encoderFunc
e = func(v reflect.Value) types.Value {
data := make(types.StructData, len(fields))
for _, f := range fields {
data[f.name] = f.encoder(v.Field(f.index))
fv := v.Field(f.index)
if !fv.IsValid() || f.omitEmpty && isEmptyValue(fv) {
continue
}
data[f.name] = f.encoder(fv)
}
return types.NewStruct(name, data)
}
@@ -192,11 +206,33 @@ func structEncoder(t reflect.Type, parentStructTypes []reflect.Type) encoderFunc
return e
}
func isEmptyValue(v reflect.Value) bool {
switch v.Kind() {
case reflect.Array, reflect.Map, reflect.Slice, reflect.String:
return v.Len() == 0
case reflect.Bool:
return !v.Bool()
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return v.Int() == 0
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return v.Uint() == 0
case reflect.Float32, reflect.Float64:
return v.Float() == 0
case reflect.Struct:
z := reflect.Zero(v.Type())
return z.Interface() == v.Interface()
case reflect.Interface:
return v.IsNil()
}
return false
}
type field struct {
name string
encoder encoderFunc
index int
nomsType *types.Type
name string
encoder encoderFunc
index int
nomsType *types.Type
omitEmpty bool
}
type fieldSlice []field
@@ -227,14 +263,26 @@ func (c *encoderCacheT) set(t reflect.Type, e encoderFunc) {
c.m[t] = e
}
func getFieldName(fieldName string, f reflect.StructField) string {
if fieldName == "" {
fieldName = strings.ToLower(f.Name[:1]) + f.Name[1:]
func parseTags(tags string, f reflect.StructField) (name string, omitEmpty bool) {
idx := strings.Index(tags, ",")
if tags == "" || idx == 0 {
name = strings.ToLower(f.Name[:1]) + f.Name[1:]
} else if idx == -1 {
name = tags
} else {
name = tags[:idx]
}
if !types.IsValidStructFieldName(fieldName) {
panic(&InvalidTagError{"Invalid struct field name: " + fieldName})
if !types.IsValidStructFieldName(name) {
panic(&InvalidTagError{"Invalid struct field name: " + name})
}
return fieldName
if idx != -1 {
// This is pretty simplistic but it is good enough for now.
omitEmpty = tags[idx+1:] == "omitempty"
}
return
}
func validateField(f reflect.StructField, t reflect.Type) {
@@ -260,12 +308,18 @@ func typeFields(t reflect.Type, parentStructTypes []reflect.Type) (fields fieldS
continue
}
name, omitEmpty := parseTags(tags, f)
if omitEmpty {
canComputeStructType = false
}
fields = append(fields, field{
name: getFieldName(tags, f),
encoder: typeEncoder(f.Type, parentStructTypes),
index: i,
nomsType: nt,
name: name,
encoder: typeEncoder(f.Type, parentStructTypes),
index: i,
nomsType: nt,
omitEmpty: omitEmpty,
})
}
sort.Sort(fields)
if canComputeStructType {

View File

@@ -216,6 +216,156 @@ func TestEncodeInvalidNamedFields(t *testing.T) {
assertEncodeErrorMessage(t, S{42}, "Invalid struct field name: 1a")
}
func TestEncodeOmitEmpty(t *testing.T) {
assert := assert.New(t)
type S struct {
String string `noms:",omitempty"`
Bool bool `noms:",omitempty"`
Int int `noms:",omitempty"`
Int8 int8 `noms:",omitempty"`
Int16 int16 `noms:",omitempty"`
Int32 int32 `noms:",omitempty"`
Int64 int64 `noms:",omitempty"`
Uint uint `noms:",omitempty"`
Uint8 uint8 `noms:",omitempty"`
Uint16 uint16 `noms:",omitempty"`
Uint32 uint32 `noms:",omitempty"`
Uint64 uint64 `noms:",omitempty"`
Float32 float32 `noms:",omitempty"`
Float64 float64 `noms:",omitempty"`
}
s := S{
String: "s",
Bool: true,
Int: 1,
Int8: 1,
Int16: 1,
Int32: 1,
Int64: 1,
Uint: 1,
Uint8: 1,
Uint16: 1,
Uint32: 1,
Uint64: 1,
Float32: 1,
Float64: 1,
}
v, err := Marshal(s)
assert.NoError(err)
assert.True(types.NewStruct("S", types.StructData{
"string": types.String("s"),
"bool": types.Bool(true),
"int": types.Number(1),
"int8": types.Number(1),
"int16": types.Number(1),
"int32": types.Number(1),
"int64": types.Number(1),
"uint": types.Number(1),
"uint8": types.Number(1),
"uint16": types.Number(1),
"uint32": types.Number(1),
"uint64": types.Number(1),
"float32": types.Number(1),
"float64": types.Number(1),
}).Equals(v))
s2 := S{
String: "",
Bool: false,
Int: 0,
Int8: 0,
Int16: 0,
Int32: 0,
Int64: 0,
Uint: 0,
Uint8: 0,
Uint16: 0,
Uint32: 0,
Uint64: 0,
Float32: 0,
Float64: 0,
}
v2, err := Marshal(s2)
assert.NoError(err)
assert.True(types.NewStruct("S", types.StructData{}).Equals(v2))
type S2 struct {
Slice []int `noms:",omitempty"`
Map map[int]int `noms:",omitempty"`
}
s3 := S2{
Slice: []int{0},
Map: map[int]int{0: 0},
}
v3, err := Marshal(s3)
assert.NoError(err)
assert.True(types.NewStruct("S2", types.StructData{
"slice": types.NewList(types.Number(0)),
"map": types.NewMap(types.Number(0), types.Number(0)),
}).Equals(v3))
s4 := S2{
Slice: []int{},
Map: map[int]int{},
}
v4, err := Marshal(s4)
assert.NoError(err)
assert.True(types.NewStruct("S2", types.StructData{}).Equals(v4))
s5 := S2{
Slice: nil,
Map: nil,
}
v5, err := Marshal(s5)
assert.NoError(err)
assert.True(types.NewStruct("S2", types.StructData{}).Equals(v5))
type S3 struct {
List types.List `noms:",omitempty"`
Value types.Value `noms:",omitempty"`
}
s6 := S3{
List: types.NewList(),
Value: types.Number(0),
}
v6, err := Marshal(s6)
assert.NoError(err)
assert.True(types.NewStruct("S3", types.StructData{
"list": types.NewList(),
"value": types.Number(0),
}).Equals(v6))
s7 := S3{
List: types.List{},
Value: nil,
}
v7, err := Marshal(s7)
assert.NoError(err)
assert.True(types.NewStruct("S3", types.StructData{}).Equals(v7))
// Both name and omitempty
type S4 struct {
X int `noms:"y,omitempty"`
}
s8 := S4{
X: 1,
}
v8, err := Marshal(s8)
assert.NoError(err)
assert.True(types.NewStruct("S4", types.StructData{
"y": types.Number(1),
}).Equals(v8))
s9 := S4{
X: 0,
}
v9, err := Marshal(s9)
assert.NoError(err)
assert.True(types.NewStruct("S4", types.StructData{}).Equals(v9))
}
func ExampleMarshal() {
type Person struct {
Given string