Skip to content
This repository has been archived by the owner on Mar 9, 2022. It is now read-only.

Commit

Permalink
refactor: Duration → OptionalDuration
Browse files Browse the repository at this point in the history
This makes it possible to use OptionalDuration with `json:",omitempty"`
so the null is not serialized to JSON, and get working WithDefault as well.
  • Loading branch information
lidel committed Oct 27, 2021
1 parent 753de75 commit 848e479
Show file tree
Hide file tree
Showing 3 changed files with 37 additions and 25 deletions.
2 changes: 1 addition & 1 deletion autonat.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,5 +77,5 @@ type AutoNATThrottleConfig struct {
// global/peer dialback limits.
//
// When unset, this defaults to 1 minute.
Interval Duration
Interval OptionalDuration `json:",omitempty"`
}
28 changes: 14 additions & 14 deletions types.go
Original file line number Diff line number Diff line change
Expand Up @@ -211,56 +211,56 @@ func (p Priority) String() string {
var _ json.Unmarshaler = (*Priority)(nil)
var _ json.Marshaler = (*Priority)(nil)

// Duration wraps time.Duration to provide json serialization and deserialization.
// OptionalDuration wraps time.Duration to provide json serialization and deserialization.
//
// NOTE: the zero value encodes to "default" string.
type Duration struct {
// NOTE: the zero value encodes to JSON nill
type OptionalDuration struct {
value *time.Duration
}

func (d *Duration) UnmarshalJSON(input []byte) error {
func (d *OptionalDuration) UnmarshalJSON(input []byte) error {
switch string(input) {
case "null", "undefined", "\"null\"", "", "default", "\"\"", "\"default\"":
*d = Duration{}
*d = OptionalDuration{}
return nil
default:
text := strings.Trim(string(input), "\"")
value, err := time.ParseDuration(text)
if err != nil {
return err
}
*d = Duration{value: &value}
*d = OptionalDuration{value: &value}
return nil
}
}

func (d *Duration) IsDefault() bool {
return d.value == nil
func (d *OptionalDuration) IsDefault() bool {
return d == nil || d.value == nil
}

func (d *Duration) WithDefault(defaultValue time.Duration) time.Duration {
if d.value == nil {
func (d *OptionalDuration) WithDefault(defaultValue time.Duration) time.Duration {
if d == nil || d.value == nil {
return defaultValue
}
return *d.value
}

func (d Duration) MarshalJSON() ([]byte, error) {
func (d OptionalDuration) MarshalJSON() ([]byte, error) {
if d.value == nil {
return json.Marshal(nil)
}
return json.Marshal(d.value.String())
}

func (d Duration) String() string {
func (d OptionalDuration) String() string {
if d.value == nil {
return "default"
}
return d.value.String()
}

var _ json.Unmarshaler = (*Duration)(nil)
var _ json.Marshaler = (*Duration)(nil)
var _ json.Unmarshaler = (*OptionalDuration)(nil)
var _ json.Marshaler = (*OptionalDuration)(nil)

// OptionalInteger represents an integer that has a default value
//
Expand Down
32 changes: 22 additions & 10 deletions types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,19 @@ import (
"time"
)

func TestDuration(t *testing.T) {
func TestOptionalDuration(t *testing.T) {
makeDurationPointer := func(d time.Duration) *time.Duration { return &d }

t.Run("marshalling and unmarshalling", func(t *testing.T) {
out, err := json.Marshal(Duration{value: makeDurationPointer(time.Second)})
out, err := json.Marshal(OptionalDuration{value: makeDurationPointer(time.Second)})
if err != nil {
t.Fatal(err)
}
expected := "\"1s\""
if string(out) != expected {
t.Fatalf("expected %s, got %s", expected, string(out))
}
var d Duration
var d OptionalDuration

if err := json.Unmarshal(out, &d); err != nil {
t.Fatal(err)
Expand All @@ -31,7 +31,7 @@ func TestDuration(t *testing.T) {

t.Run("default value", func(t *testing.T) {
for _, jsonStr := range []string{"null", "\"null\"", "\"\"", "\"default\""} {
var d Duration
var d OptionalDuration
if !d.IsDefault() {
t.Fatal("expected value to be the default initially")
}
Expand All @@ -47,21 +47,33 @@ func TestDuration(t *testing.T) {
}
})

t.Run("omitempty", func(t *testing.T) {
t.Run("omitempty with default value", func(t *testing.T) {
type Foo struct {
D *Duration `json:",omitempty"`
D *OptionalDuration `json:",omitempty"`
}
// marshall to JSON without empty field
out, err := json.Marshal(new(Foo))
if err != nil {
t.Fatal(err)
}
if string(out) != "{}" {
t.Fatalf("expected omitempty to omit the duration, got %s", out)
}
// unmarshall missing value and get the default
var foo2 Foo
if err := json.Unmarshal(out, &foo2); err != nil {
t.Fatalf("%s failed to unmarshall with %s", string(out), err)
}
if dur := foo2.D.WithDefault(time.Hour); dur != time.Hour {
t.Fatalf("expected default value to be used, got %s", dur)
}
if !foo2.D.IsDefault() {
t.Fatal("expected value to be the default")
}
})

t.Run("roundtrip including the default values", func(t *testing.T) {
for jsonStr, goValue := range map[string]Duration{
for jsonStr, goValue := range map[string]OptionalDuration{
// there are various footguns user can hit, normalize them to the canonical default
"null": {}, // JSON null → default value
"\"null\"": {}, // JSON string "null" sent/set by "ipfs config" cli → default value
Expand All @@ -70,7 +82,7 @@ func TestDuration(t *testing.T) {
"\"1s\"": {value: makeDurationPointer(time.Second)},
"\"42h1m3s\"": {value: makeDurationPointer(42*time.Hour + 1*time.Minute + 3*time.Second)},
} {
var d Duration
var d OptionalDuration
err := json.Unmarshal([]byte(jsonStr), &d)
if err != nil {
t.Fatal(err)
Expand Down Expand Up @@ -104,10 +116,10 @@ func TestDuration(t *testing.T) {
for _, invalid := range []string{
"\"s\"", "\"\"", "\"-1\"", "\"1H\"", "\"day\"",
} {
var d Duration
var d OptionalDuration
err := json.Unmarshal([]byte(invalid), &d)
if err == nil {
t.Errorf("expected to fail to decode %s as a Duration, got %s instead", invalid, d)
t.Errorf("expected to fail to decode %s as an OptionalDuration, got %s instead", invalid, d)
}
}
})
Expand Down

0 comments on commit 848e479

Please sign in to comment.