From fa027bd191e659b931552d7efce4a4fe3f1e1bd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Husiaty=C5=84ski?= Date: Fri, 18 Feb 2022 15:17:31 +0100 Subject: [PATCH] Support time.Duration string format Using numeric representation of time.Duration is confusing, because value is stored as UNIX nanoseconds. Humans tend to be more comfortable with the string representation of the time.Duration. In addition to time.Duration being an int64 number and encoding/decoding as such, allow to use its string representation. For example, the below structure: Configuration := struct { ExpireIn time.Duration }{ ExpireIn 15 * time.Minute, } Is currently serialized as: ExpireIn = 900000000000 This commit is prioritizing its string representation and the new encoding will produce: ExpireIn = "15m0s" This change is backward compatible. Both old numeric value and the new string reprentation are accepted by the decoder. --- decode.go | 12 ++++++++++++ decode_test.go | 28 ++++++++++++++++++++++++++++ encode.go | 5 ++++- encode_test.go | 20 ++++++++++++++++++++ 4 files changed, 64 insertions(+), 1 deletion(-) diff --git a/decode.go b/decode.go index 46aaa9c9..89c39255 100644 --- a/decode.go +++ b/decode.go @@ -10,6 +10,7 @@ import ( "os" "reflect" "strings" + "time" ) // Unmarshaler is the interface implemented by objects that can unmarshal a @@ -417,6 +418,17 @@ func (md *MetaData) unifyInt(data interface{}, rv reflect.Value) error { return md.badtype("integer", data) } + if _, ok := rv.Interface().(time.Duration); ok { + if s, ok := data.(string); ok { + dur, err := time.ParseDuration(s) + if err != nil { + return md.e("value %q is not a valid duration: %w", s, err) + } + rv.SetInt(int64(dur)) + return nil + } + } + rvk := rv.Kind() switch { case rvk >= reflect.Int && rvk <= reflect.Int64: diff --git a/decode_test.go b/decode_test.go index bfde330a..a1034e8e 100644 --- a/decode_test.go +++ b/decode_test.go @@ -348,6 +348,34 @@ func TestDecodeIntOverflow(t *testing.T) { } } +func TestDecodeStringDuration(t *testing.T) { + type table struct { + Value time.Duration + } + var tab table + if _, err := Decode(`value = "5m4s"`, &tab); err != nil { + t.Fatalf("Cannot decode duration string: %s", err) + } + + if tab.Value != 5*time.Minute+4*time.Second { + t.Fatalf("Unexpected value: %q", tab.Value) + } +} + +func TestDecodeIntegerDuration(t *testing.T) { + type table struct { + Value time.Duration + } + var tab table + if _, err := Decode(`value = 12345678`, &tab); err != nil { + t.Fatalf("Cannot decode duration integer: %s", err) + } + + if tab.Value != 12345678 { + t.Fatalf("Unexpected value: %q", tab.Value) + } +} + func TestDecodeFloatOverflow(t *testing.T) { tests := []struct { value string diff --git a/encode.go b/encode.go index e7d4eeb4..f6af8ca4 100644 --- a/encode.go +++ b/encode.go @@ -145,7 +145,10 @@ func (enc *Encoder) encode(key Key, rv reflect.Value) { case time.Time, encoding.TextMarshaler, Marshaler: enc.writeKeyValue(key, rv, false) return - // TODO: #76 would make this superfluous after implemented. + // TODO: #76 would make this superfluous after implemented. + case time.Duration: + enc.writeKeyValue(key, reflect.ValueOf(t.String()), false) + return case Primitive: enc.encode(key, reflect.ValueOf(t.undecoded)) return diff --git a/encode_test.go b/encode_test.go index 120fa076..31710087 100644 --- a/encode_test.go +++ b/encode_test.go @@ -268,6 +268,26 @@ func TestEncodeNaN(t *testing.T) { encodeExpected(t, "", s2, "nan = nan\ninf = -inf\n", nil) } +func TestEncodeDuration(t *testing.T) { + cases := []time.Duration{ + 0, + time.Second, + time.Minute, + time.Hour, + 248*time.Hour + 45*time.Minute + 24*time.Second, + 12345678 * time.Nanosecond, + 12345678 * time.Second, + } + + for _, d := range cases { + encodeExpected(t, d.String(), struct { + Dur time.Duration + }{ + Dur: d, + }, fmt.Sprintf("Dur = %q", d), nil) + } +} + func TestEncodePrimitive(t *testing.T) { type MyStruct struct { Data Primitive