Skip to content

Commit

Permalink
Add JSON-marshalable duration (#381)
Browse files Browse the repository at this point in the history
  • Loading branch information
maximpertsov authored Nov 1, 2024
1 parent 1a169f7 commit 42f7f8e
Show file tree
Hide file tree
Showing 3 changed files with 106 additions and 0 deletions.
41 changes: 41 additions & 0 deletions duration.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package utils

import (
"encoding/json"
"errors"
"time"
)

// Duration is a custom duration type that supports marshalling/unmarshalling.
// This type and supporting functionality can be removed once go supports for
// [time.Duration], which is planned for go2: https://github.com/golang/go/issues/10275
type Duration time.Duration

// MarshalJSON marshals a [Duration] into JSON.
func (d Duration) MarshalJSON() ([]byte, error) {
return json.Marshal(time.Duration(d).String())
}

// UnmarshalJSON unmarshals JSON data into a [Duration].
func (d *Duration) UnmarshalJSON(b []byte) error {
var v interface{}
if err := json.Unmarshal(b, &v); err != nil {
return err
}
switch value := v.(type) {
case string:
tmp, err := time.ParseDuration(value)
if err != nil {
return err
}
*d = Duration(tmp)
return nil
default:
return errors.New("invalid duration")
}
}

// Unwrap converts a custom [Duration] into a native [time.Duration].
func (d Duration) Unwrap() time.Duration {
return time.Duration(d)
}
63 changes: 63 additions & 0 deletions duration_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package utils_test

import (
"encoding/json"
"testing"
"time"

"go.viam.com/test"

"go.viam.com/utils"
)

func FuzzDurationJSONRoundtrip(f *testing.F) {
f.Add("not a duration")
f.Add("2h")
f.Add("1m")
f.Add("5s")
f.Add("12ms")
f.Add("8us")
f.Add("600ns")
f.Add("1h2m10s")
f.Add("`0`")
f.Add("5")
f.Fuzz(func(t *testing.T, s string) {
// marshal input to JSON. this should always succeed.
data, err := json.Marshal(s)
test.That(t, err, test.ShouldBeNil)

// unmarshal marshaled input directly to custom [utils.Duration].
var ud utils.Duration
errUD := json.Unmarshal(data, &ud)

// parse input to built-in [time.Duration]
td, errTD := time.ParseDuration(s)

// the previous two marshall/parse operations should either both
// succeed or both fail.
if errUD != nil || errTD != nil {
test.That(t, errUD, test.ShouldNotBeNil)
test.That(t, errTD, test.ShouldNotBeNil)
return
}

// if unmarshaling/parsing is successful, both durations should be equal.
test.That(t, ud.Unwrap(), test.ShouldEqual, td)

// marshal custom [util.Duration] value back to JSON. this should always succeed.
// note that the resulting JSON value might not match initially marshaled input string.
// the marshaling function for [util.Duration] is using [time.Duration.String], which
// might "pad" the resulting string with zero values duration types.
// for example, the following marshaling roundtrip is possible:
// `"2h"` -> 2 * time.Hour -> `"2h0m0s""`.
jsonUD, err := json.Marshal(ud)
test.That(t, err, test.ShouldBeNil)

// stringify and marshal built-in [time.Duration] value to JSON.
// this should always succeed and match the result of marshaling the custom
// [util.Duration] value.
jsonTD, err := json.Marshal(td.String())
test.That(t, err, test.ShouldBeNil)
test.That(t, jsonUD, test.ShouldResemble, jsonTD)
})
}
2 changes: 2 additions & 0 deletions testdata/fuzz/FuzzDurationJSONRoundtrip/771e938e4458e983
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
go test fuzz v1
string("0")

0 comments on commit 42f7f8e

Please sign in to comment.