Skip to content

Commit 42a61b7

Browse files
committed
jsontypes: add a package with helper types
Currently only one type: ParsingDuration, which handles both integer-nanoseconds and time.ParseDuration-compatible strings. This makes it compatible with any existing JSON config files, while expanding support for human readable strings.
1 parent 0ae1152 commit 42a61b7

File tree

2 files changed

+130
-0
lines changed

2 files changed

+130
-0
lines changed
+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// Package jsontypes contains helper types used by the JSON and Cue decoders to
2+
// facilitate more natural decoding.
3+
package jsontypes
4+
5+
import (
6+
"bytes"
7+
"encoding/json"
8+
"fmt"
9+
"time"
10+
)
11+
12+
// ParsingDuration implements [encoding/json.Unmarshaler], supporting both
13+
// quoted strings that are parseable with [time.ParseDuration], and integer nanoseconds if it's a number
14+
type ParsingDuration int64
15+
16+
// UnmarshalJSON implements [encoding/json.Unmarshaler] for ParsingDuration.
17+
func (p *ParsingDuration) UnmarshalJSON(b []byte) error {
18+
d := json.NewDecoder(bytes.NewReader(b))
19+
d.UseNumber()
20+
n, tokErr := d.Token()
21+
if tokErr != nil {
22+
return fmt.Errorf("failed to parse token: %w", tokErr)
23+
}
24+
switch v := n.(type) {
25+
case string:
26+
dur, durParseErr := time.ParseDuration(v)
27+
if durParseErr != nil {
28+
return fmt.Errorf("failed to parse %q as duration: %w", v, durParseErr)
29+
}
30+
*p = ParsingDuration(dur)
31+
return nil
32+
case json.Number:
33+
i, intParseErr := v.Int64()
34+
if intParseErr != nil {
35+
return fmt.Errorf("failed to parse number as integer nanoseconds: %w", intParseErr)
36+
}
37+
*p = ParsingDuration(i)
38+
return nil
39+
default:
40+
return fmt.Errorf("unexpected JSON token-type %T; expected string or number", n)
41+
}
42+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package jsontypes
2+
3+
import (
4+
"encoding/json"
5+
"reflect"
6+
"testing"
7+
"time"
8+
)
9+
10+
func ptrVal[V any](v V) *V {
11+
return &v
12+
}
13+
14+
func TestParsingDuration(t *testing.T) {
15+
t.Parallel()
16+
type decStruct struct {
17+
Dur ParsingDuration
18+
DurPtr *ParsingDuration
19+
}
20+
for _, tbl := range []struct {
21+
name string
22+
inJSON string
23+
expStruct any
24+
expErr bool
25+
}{
26+
{
27+
name: "integer_value_no_ptr",
28+
inJSON: `{"dur": 1234}`,
29+
expStruct: decStruct{Dur: 1234},
30+
expErr: false,
31+
},
32+
{
33+
name: "string_value_no_ptr",
34+
inJSON: `{"dur": "3s"}`,
35+
expStruct: decStruct{Dur: ParsingDuration(3 * time.Second)},
36+
expErr: false,
37+
},
38+
{
39+
name: "string_value_ptr",
40+
inJSON: `{"dur": "3s", "durptr": "9m"}`,
41+
expStruct: decStruct{Dur: ParsingDuration(3 * time.Second), DurPtr: ptrVal(ParsingDuration(9 * time.Minute))},
42+
expErr: false,
43+
},
44+
{
45+
name: "integer_value_ptr",
46+
inJSON: `{"dur": "3s", "durptr": 2048}`,
47+
expStruct: decStruct{Dur: ParsingDuration(3 * time.Second), DurPtr: ptrVal(ParsingDuration(2048))},
48+
expErr: false,
49+
},
50+
{
51+
name: "error_array_val",
52+
inJSON: `{"dur": [], "durptr": 2048}`,
53+
expErr: true,
54+
},
55+
{
56+
name: "error_object_val",
57+
inJSON: `{"dur": {}, "durptr": 2048}`,
58+
expErr: true,
59+
},
60+
{
61+
name: "error_unparsable_str_val",
62+
inJSON: `{"dur": "sssssssssss", "durptr": 2048}`,
63+
expErr: true,
64+
},
65+
{
66+
name: "error_float_fractional_val",
67+
inJSON: `{"dur": 0.333333, "durptr": 2048}`,
68+
expErr: true,
69+
},
70+
} {
71+
t.Run(tbl.name, func(t *testing.T) {
72+
v := decStruct{}
73+
decErr := json.Unmarshal([]byte(tbl.inJSON), &v)
74+
if decErr != nil {
75+
if !tbl.expErr {
76+
t.Errorf("unexpected error unmarshaling: %s", decErr)
77+
} else {
78+
t.Logf("expected error: %s", decErr)
79+
}
80+
return
81+
}
82+
if !reflect.DeepEqual(v, tbl.expStruct) {
83+
t.Errorf("unexpected value\n got: %+v\nwant:%+v", tbl.expStruct, v)
84+
}
85+
})
86+
87+
}
88+
}

0 commit comments

Comments
 (0)