mirror of
https://github.com/dolthub/dolt.git
synced 2026-01-23 18:58:50 -06:00
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:
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user