@cryptotaxi247 / kubo / commits / 2cf170d5f

go-ipfs-config: feat: add an OptionalDuration type (#148)

* feat: make it possible to define optional durations * test: empty/default optional durations does not crash if user restores default value and sets it to empty string "" * refactor: use null in JSON * refactor(duration): use JSON null as the default Rationale: https://github.com/ipfs/go-ipfs-config/pull/148#discussion_r736975879 * refactor: Duration → OptionalDuration This makes it possible to use OptionalDuration with `json:",omitempty"` so the null is not serialized to JSON, and get working WithDefault as well. Co-authored-by: Marcin Rataj <lidel@lidel.org>

Marten Seemann committed Oct 27, 2021 at 18:23 UTC 2cf170d5fae00da6625bb0831beb5c67a4f71ee1
3 files changed +160 -43
config/autonat.go
+1 -1
@@ -77,5 +77,5 @@ type AutoNATThrottleConfig struct {
77 // global/peer dialback limits.
78 //
79 // When unset, this defaults to 1 minute.
80 - Interval Duration `json:",omitempty"`
80 + Interval OptionalDuration `json:",omitempty"`
81 }
config/types.go
+43 -14
@@ -1,9 +1,9 @@
1 package config
2
3 import (
4 - "encoding"
4 "encoding/json"
5 "fmt"
6 + "strings"
7 "time"
8 )
9
@@ -211,27 +211,56 @@ func (p Priority) String() string {
211 var _ json.Unmarshaler = (*Priority)(nil)
212 var _ json.Marshaler = (*Priority)(nil)
213
214 -// Duration wraps time.Duration to provide json serialization and deserialization.
214 +// OptionalDuration wraps time.Duration to provide json serialization and deserialization.
215 //
216 -// NOTE: the zero value encodes to an empty string.
217 -type Duration time.Duration
216 +// NOTE: the zero value encodes to JSON nill
217 +type OptionalDuration struct {
218 + value *time.Duration
219 +}
220
219 -func (d *Duration) UnmarshalText(text []byte) error {
220 - dur, err := time.ParseDuration(string(text))
221 - *d = Duration(dur)
222 - return err
221 +func (d *OptionalDuration) UnmarshalJSON(input []byte) error {
222 + switch string(input) {
223 + case "null", "undefined", "\"null\"", "", "default", "\"\"", "\"default\"":
224 + *d = OptionalDuration{}
225 + return nil
226 + default:
227 + text := strings.Trim(string(input), "\"")
228 + value, err := time.ParseDuration(text)
229 + if err != nil {
230 + return err
231 + }
232 + *d = OptionalDuration{value: &value}
233 + return nil
234 + }
235 }
236
225 -func (d Duration) MarshalText() ([]byte, error) {
226 - return []byte(time.Duration(d).String()), nil
237 +func (d *OptionalDuration) IsDefault() bool {
238 + return d == nil || d.value == nil
239 }
240
229 -func (d Duration) String() string {
230 - return time.Duration(d).String()
241 +func (d *OptionalDuration) WithDefault(defaultValue time.Duration) time.Duration {
242 + if d == nil || d.value == nil {
243 + return defaultValue
244 + }
245 + return *d.value
246 +}
247 +
248 +func (d OptionalDuration) MarshalJSON() ([]byte, error) {
249 + if d.value == nil {
250 + return json.Marshal(nil)
251 + }
252 + return json.Marshal(d.value.String())
253 +}
254 +
255 +func (d OptionalDuration) String() string {
256 + if d.value == nil {
257 + return "default"
258 + }
259 + return d.value.String()
260 }
261
233 -var _ encoding.TextUnmarshaler = (*Duration)(nil)
234 -var _ encoding.TextMarshaler = (*Duration)(nil)
262 +var _ json.Unmarshaler = (*OptionalDuration)(nil)
263 +var _ json.Marshaler = (*OptionalDuration)(nil)
264
265 // OptionalInteger represents an integer that has a default value
266 //
config/types_test.go
+116 -28
@@ -1,40 +1,128 @@
1 package config
2
3 import (
4 + "bytes"
5 "encoding/json"
6 "testing"
7 "time"
8 )
9
9 -func TestDuration(t *testing.T) {
10 - out, err := json.Marshal(Duration(time.Second))
11 - if err != nil {
12 - t.Fatal(err)
10 +func TestOptionalDuration(t *testing.T) {
11 + makeDurationPointer := func(d time.Duration) *time.Duration { return &d }
12
14 - }
15 - expected := "\"1s\""
16 - if string(out) != expected {
17 - t.Fatalf("expected %s, got %s", expected, string(out))
18 - }
19 - var d Duration
20 - err = json.Unmarshal(out, &d)
21 - if err != nil {
22 - t.Fatal(err)
23 - }
24 - if time.Duration(d) != time.Second {
25 - t.Fatal("expected a second")
26 - }
27 - type Foo struct {
28 - D Duration `json:",omitempty"`
29 - }
30 - out, err = json.Marshal(new(Foo))
31 - if err != nil {
32 - t.Fatal(err)
33 - }
34 - expected = "{}"
35 - if string(out) != expected {
36 - t.Fatal("expected omitempty to omit the duration")
37 - }
13 + t.Run("marshalling and unmarshalling", func(t *testing.T) {
14 + out, err := json.Marshal(OptionalDuration{value: makeDurationPointer(time.Second)})
15 + if err != nil {
16 + t.Fatal(err)
17 + }
18 + expected := "\"1s\""
19 + if string(out) != expected {
20 + t.Fatalf("expected %s, got %s", expected, string(out))
21 + }
22 + var d OptionalDuration
23 +
24 + if err := json.Unmarshal(out, &d); err != nil {
25 + t.Fatal(err)
26 + }
27 + if *d.value != time.Second {
28 + t.Fatal("expected a second")
29 + }
30 + })
31 +
32 + t.Run("default value", func(t *testing.T) {
33 + for _, jsonStr := range []string{"null", "\"null\"", "\"\"", "\"default\""} {
34 + var d OptionalDuration
35 + if !d.IsDefault() {
36 + t.Fatal("expected value to be the default initially")
37 + }
38 + if err := json.Unmarshal([]byte(jsonStr), &d); err != nil {
39 + t.Fatalf("%s failed to unmarshall with %s", jsonStr, err)
40 + }
41 + if dur := d.WithDefault(time.Hour); dur != time.Hour {
42 + t.Fatalf("expected default value to be used, got %s", dur)
43 + }
44 + if !d.IsDefault() {
45 + t.Fatal("expected value to be the default")
46 + }
47 + }
48 + })
49 +
50 + t.Run("omitempty with default value", func(t *testing.T) {
51 + type Foo struct {
52 + D *OptionalDuration `json:",omitempty"`
53 + }
54 + // marshall to JSON without empty field
55 + out, err := json.Marshal(new(Foo))
56 + if err != nil {
57 + t.Fatal(err)
58 + }
59 + if string(out) != "{}" {
60 + t.Fatalf("expected omitempty to omit the duration, got %s", out)
61 + }
62 + // unmarshall missing value and get the default
63 + var foo2 Foo
64 + if err := json.Unmarshal(out, &foo2); err != nil {
65 + t.Fatalf("%s failed to unmarshall with %s", string(out), err)
66 + }
67 + if dur := foo2.D.WithDefault(time.Hour); dur != time.Hour {
68 + t.Fatalf("expected default value to be used, got %s", dur)
69 + }
70 + if !foo2.D.IsDefault() {
71 + t.Fatal("expected value to be the default")
72 + }
73 + })
74 +
75 + t.Run("roundtrip including the default values", func(t *testing.T) {
76 + for jsonStr, goValue := range map[string]OptionalDuration{
77 + // there are various footguns user can hit, normalize them to the canonical default
78 + "null": {}, // JSON null → default value
79 + "\"null\"": {}, // JSON string "null" sent/set by "ipfs config" cli → default value
80 + "\"default\"": {}, // explicit "default" as string
81 + "\"\"": {}, // user removed custom value, empty string should also parse as default
82 + "\"1s\"": {value: makeDurationPointer(time.Second)},
83 + "\"42h1m3s\"": {value: makeDurationPointer(42*time.Hour + 1*time.Minute + 3*time.Second)},
84 + } {
85 + var d OptionalDuration
86 + err := json.Unmarshal([]byte(jsonStr), &d)
87 + if err != nil {
88 + t.Fatal(err)
89 + }
90 +
91 + if goValue.value == nil && d.value == nil {
92 + } else if goValue.value == nil && d.value != nil {
93 + t.Errorf("expected nil for %s, got %s", jsonStr, d)
94 + } else if *d.value != *goValue.value {
95 + t.Fatalf("expected %s for %s, got %s", goValue, jsonStr, d)
96 + }
97 +
98 + // Test Reverse
99 + out, err := json.Marshal(goValue)
100 + if err != nil {
101 + t.Fatal(err)
102 + }
103 + if goValue.value == nil {
104 + if !bytes.Equal(out, []byte("null")) {
105 + t.Fatalf("expected JSON null for %s, got %s", jsonStr, string(out))
106 + }
107 + continue
108 + }
109 + if string(out) != jsonStr {
110 + t.Fatalf("expected %s, got %s", jsonStr, string(out))
111 + }
112 + }
113 + })
114 +
115 + t.Run("invalid duration values", func(t *testing.T) {
116 + for _, invalid := range []string{
117 + "\"s\"", "\"1ę\"", "\"-1\"", "\"1H\"", "\"day\"",
118 + } {
119 + var d OptionalDuration
120 + err := json.Unmarshal([]byte(invalid), &d)
121 + if err == nil {
122 + t.Errorf("expected to fail to decode %s as an OptionalDuration, got %s instead", invalid, d)
123 + }
124 + }
125 + })
126 }
127
128 func TestOneStrings(t *testing.T) {