summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorJoe Tsai <joetsai@digital-static.net>2025-04-17 15:51:41 -0700
committerGitHub <noreply@github.com>2025-04-17 15:51:41 -0700
commitaff8f1b3587b4e2b166f506fba34d8da8ea0e99a (patch)
tree495b11f8ec156efa6046986f20a1f1506e4a7462
parent898cf068985f04db51093d04cdd9fbb215f376c7 (diff)
downloadtailscale-aff8f1b3587b4e2b166f506fba34d8da8ea0e99a.tar.xz
tailscale-aff8f1b3587b4e2b166f506fba34d8da8ea0e99a.zip
tstime: add GoDuration which JSON serializes with time.Duration.String (#15726)
The encoding/json/v2 effort may end up changing the default represention of time.Duration in JSON. See https://go.dev/issue/71631 The GoDuration type allows us to explicitly use the time.Duration.String representation regardless of whether we serialize with v1 or v2 of encoding/json. Updates tailscale/corp#27502 Signed-off-by: Joe Tsai <joetsai@digital-static.net>
-rw-r--r--tstime/tstime.go38
-rw-r--r--tstime/tstime_test.go17
2 files changed, 55 insertions, 0 deletions
diff --git a/tstime/tstime.go b/tstime/tstime.go
index 1c006355f..6e5b7f9f4 100644
--- a/tstime/tstime.go
+++ b/tstime/tstime.go
@@ -6,6 +6,7 @@ package tstime
import (
"context"
+ "encoding"
"strconv"
"strings"
"time"
@@ -183,3 +184,40 @@ func (StdClock) AfterFunc(d time.Duration, f func()) TimerController {
func (StdClock) Since(t time.Time) time.Duration {
return time.Since(t)
}
+
+// GoDuration is a [time.Duration] but JSON serializes with [time.Duration.String].
+//
+// Note that this format is specific to Go and non-standard,
+// but excels in being most humanly readable compared to alternatives.
+// The wider industry still lacks consensus for the representation
+// of a time duration in humanly-readable text.
+// See https://go.dev/issue/71631 for more discussion.
+//
+// Regardless of how the industry evolves into the future,
+// this type explicitly uses the Go format.
+type GoDuration struct{ time.Duration }
+
+var (
+ _ encoding.TextAppender = (*GoDuration)(nil)
+ _ encoding.TextMarshaler = (*GoDuration)(nil)
+ _ encoding.TextUnmarshaler = (*GoDuration)(nil)
+)
+
+func (d GoDuration) AppendText(b []byte) ([]byte, error) {
+ // The String method is inlineable (see https://go.dev/cl/520602),
+ // so this may not allocate since the string does not escape.
+ return append(b, d.String()...), nil
+}
+
+func (d GoDuration) MarshalText() ([]byte, error) {
+ return []byte(d.String()), nil
+}
+
+func (d *GoDuration) UnmarshalText(b []byte) error {
+ d2, err := time.ParseDuration(string(b))
+ if err != nil {
+ return err
+ }
+ d.Duration = d2
+ return nil
+}
diff --git a/tstime/tstime_test.go b/tstime/tstime_test.go
index 3ffeaf0ff..556ad4e8b 100644
--- a/tstime/tstime_test.go
+++ b/tstime/tstime_test.go
@@ -4,8 +4,11 @@
package tstime
import (
+ "encoding/json"
"testing"
"time"
+
+ "tailscale.com/util/must"
)
func TestParseDuration(t *testing.T) {
@@ -34,3 +37,17 @@ func TestParseDuration(t *testing.T) {
}
}
}
+
+func TestGoDuration(t *testing.T) {
+ wantDur := GoDuration{time.Hour + time.Minute + time.Second + time.Millisecond + time.Microsecond + time.Nanosecond}
+ gotJSON := string(must.Get(json.Marshal(wantDur)))
+ wantJSON := `"1h1m1.001001001s"`
+ if gotJSON != wantJSON {
+ t.Errorf("json.Marshal(%v) = %s, want %s", wantDur, gotJSON, wantJSON)
+ }
+ var gotDur GoDuration
+ must.Do(json.Unmarshal([]byte(wantJSON), &gotDur))
+ if gotDur != wantDur {
+ t.Errorf("json.Unmarshal(%s) = %v, want %v", wantJSON, gotDur, wantDur)
+ }
+}