Skip to content

Commit

Permalink
Support time.Duration string format
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
husio authored and arp242 committed May 28, 2022
1 parent 2004196 commit fa027bd
Show file tree
Hide file tree
Showing 4 changed files with 64 additions and 1 deletion.
12 changes: 12 additions & 0 deletions decode.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"os"
"reflect"
"strings"
"time"
)

// Unmarshaler is the interface implemented by objects that can unmarshal a
Expand Down Expand Up @@ -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:
Expand Down
28 changes: 28 additions & 0 deletions decode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 4 additions & 1 deletion encode.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
20 changes: 20 additions & 0 deletions encode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit fa027bd

Please sign in to comment.