Browse Source

omitempty use IsZero

As per issue #244, time.Time does not work well with
omitempty, because it has no public fields.

We make the omitempty logic consult the IsZero
method, if available, to determine whether to omit
a value.

Omitting objects with private fields seems a bit
dubious, but since that's existing behaviour,
we'll keep it.

We also make *time.Time work the same as time.Time
for the purposes of timestamp tags.

Fixes issue #244.
Roger Peppe 8 năm trước cách đây
mục cha
commit
60a2abf4e0
4 tập tin đã thay đổi với 60 bổ sung14 xóa
  1. 1 0
      decode.go
  2. 11 12
      encode.go
  3. 28 0
      encode_test.go
  4. 20 2
      yaml.go

+ 1 - 0
decode.go

@@ -233,6 +233,7 @@ var (
 	defaultMapType = reflect.TypeOf(map[interface{}]interface{}{})
 	ifaceType      = defaultMapType.Elem()
 	timeType       = reflect.TypeOf(time.Time{})
+	ptrTimeType    = reflect.TypeOf(&time.Time{})
 )
 
 func newDecoder(strict bool) *decoder {

+ 11 - 12
encode.go

@@ -89,6 +89,11 @@ func (e *encoder) marshal(tag string, in reflect.Value) {
 	}
 	iface := in.Interface()
 	switch m := iface.(type) {
+	case time.Time, *time.Time:
+		// Although time.Time implements TextMarshaler,
+		// we don't want to treat it as a string for YAML
+		// purposes because YAML has special support for
+		// timestamps.
 	case Marshaler:
 		v, err := m.MarshalYAML()
 		if err != nil {
@@ -99,30 +104,24 @@ func (e *encoder) marshal(tag string, in reflect.Value) {
 			return
 		}
 		in = reflect.ValueOf(v)
-	case time.Time:
-		// Although time.Time implements TextMarshaler,
-		// we don't want to treat it as a string for YAML
-		// purposes because YAML has special support for
-		// timestamps.
 	case encoding.TextMarshaler:
 		text, err := m.MarshalText()
 		if err != nil {
 			fail(err)
 		}
 		in = reflect.ValueOf(string(text))
+	case nil:
+		e.nilv()
+		return
 	}
 	switch in.Kind() {
 	case reflect.Interface:
-		if in.IsNil() {
-			e.nilv()
-		} else {
-			e.marshal(tag, in.Elem())
-		}
+		e.marshal(tag, in.Elem())
 	case reflect.Map:
 		e.mapv(tag, in)
 	case reflect.Ptr:
-		if in.IsNil() {
-			e.nilv()
+		if in.Type() == ptrTimeType {
+			e.timev(tag, in.Elem())
 		} else {
 			e.marshal(tag, in.Elem())
 		}

+ 28 - 0
encode_test.go

@@ -202,6 +202,25 @@ var marshalTests = []struct {
 		}{1, 0},
 		"a: 1\n",
 	},
+	{
+		&struct {
+			T1 time.Time  "t1,omitempty"
+			T2 time.Time  "t2,omitempty"
+			T3 *time.Time "t3,omitempty"
+			T4 *time.Time "t4,omitempty"
+		}{
+			T2: time.Date(2018, 1, 9, 10, 40, 47, 0, time.UTC),
+			T4: newTime(time.Date(2098, 1, 9, 10, 40, 47, 0, time.UTC)),
+		},
+		"t2: !!timestamp 2018-01-09T10:40:47Z\nt4: !!timestamp 2098-01-09T10:40:47Z\n",
+	},
+	// Nil interface that implements Marshaler.
+	{
+		map[string]yaml.Marshaler{
+			"a": nil,
+		},
+		"a: null\n",
+	},
 
 	// Flow flag
 	{
@@ -307,10 +326,15 @@ var marshalTests = []struct {
 		map[string]net.IP{"a": net.IPv4(1, 2, 3, 4)},
 		"a: 1.2.3.4\n",
 	},
+	// time.Time gets a timestamp tag.
 	{
 		map[string]time.Time{"a": time.Date(2015, 2, 24, 18, 19, 39, 0, time.UTC)},
 		"a: !!timestamp 2015-02-24T18:19:39Z\n",
 	},
+	{
+		map[string]*time.Time{"a": newTime(time.Date(2015, 2, 24, 18, 19, 39, 0, time.UTC))},
+		"a: !!timestamp 2015-02-24T18:19:39Z\n",
+	},
 	// Ensure timestamp-like strings are quoted.
 	{
 		map[string]string{"a": "2015-02-24T18:19:39Z"},
@@ -547,3 +571,7 @@ func (s *S) TestSortedOutput(c *C) {
 		last = index
 	}
 }
+
+func newTime(t time.Time) *time.Time {
+	return &t
+}

+ 20 - 2
yaml.go

@@ -172,7 +172,10 @@ func unmarshal(in []byte, out interface{}, strict bool) (err error) {
 //
 //     omitempty    Only include the field if it's not set to the zero
 //                  value for the type or to empty slices or maps.
-//                  Does not apply to zero valued structs.
+//                  Zero valued structs will be omitted if all their public
+//                  fields are zero, unless they implement an IsZero
+//                  method (see the IsZeroer interface type), in which
+//                  case the field will be included if that method returns true.
 //
 //     flow         Marshal using a flow style (useful for structs,
 //                  sequences and maps).
@@ -414,8 +417,23 @@ func getStructInfo(st reflect.Type) (*structInfo, error) {
 	return sinfo, nil
 }
 
+// IsZeroer is used to check whether an object is zero to
+// determine whether it should be omitted when marshaling
+// with the omitempty flag. One notable implementation
+// is time.Time.
+type IsZeroer interface {
+	IsZero() bool
+}
+
 func isZero(v reflect.Value) bool {
-	switch v.Kind() {
+	kind := v.Kind()
+	if z, ok := v.Interface().(IsZeroer); ok {
+		if (kind == reflect.Ptr || kind == reflect.Interface) && v.IsNil() {
+			return true
+		}
+		return z.IsZero()
+	}
+	switch kind {
 	case reflect.String:
 		return len(v.String()) == 0
 	case reflect.Interface, reflect.Ptr: