|
1 | 1 | package duration |
2 | 2 |
|
3 | 3 | import ( |
4 | | - "encoding/json" |
5 | | - "fmt" |
6 | 4 | "net/url" |
7 | 5 | "strings" |
8 | 6 | "time" |
| 7 | + |
| 8 | + "google.golang.org/protobuf/encoding/protojson" |
| 9 | + "google.golang.org/protobuf/types/known/durationpb" |
9 | 10 | ) |
10 | 11 |
|
11 | | -// Duration is a wrapper for time.Duration to provide custom marshaling |
| 12 | +// Duration is a wrapper for durationpb.Duration to provide custom marshaling |
12 | 13 | // for JSON and URL query strings. |
13 | 14 | // |
14 | | -// It embeds time.Duration, so all standard methods (Seconds, String, etc.) |
15 | | -// are directly accessible. The underlying time.Duration value can be |
16 | | -// accessed via the .AsDuration() method. |
| 15 | +// It embeds durationpb.Duration and exposes the .AsDuration() method to |
| 16 | +// easily convert to time.Duration. |
17 | 17 | // |
18 | 18 | // Example: |
19 | 19 | // |
20 | 20 | // customDur := durationpb.New(30 * time.Second) |
21 | | -// goDur := customDur.AsDuration() // Access the underlying time.Duration |
| 21 | +// goDur := customDur.AsDuration() |
22 | 22 | type Duration struct { |
23 | | - time.Duration |
| 23 | + internal *durationpb.Duration |
24 | 24 | } |
25 | 25 |
|
26 | 26 | // New creates a custom Duration from a standard time.Duration. |
27 | 27 | func New(d time.Duration) *Duration { |
28 | | - return &Duration{Duration: d} |
| 28 | + return &Duration{internal: durationpb.New(d)} |
29 | 29 | } |
30 | 30 |
|
31 | 31 | // AsDuration returns the underlying time.Duration value. |
32 | 32 | func (x *Duration) AsDuration() time.Duration { |
33 | 33 | if x == nil { |
34 | 34 | return 0 |
35 | 35 | } |
36 | | - return x.Duration |
| 36 | + return x.internal.AsDuration() |
37 | 37 | } |
38 | 38 |
|
39 | | -// MarshalJSON implements the [json.Marshaler] interface by formatting the |
40 | | -// duration as a string according to Google Well Known Type. |
| 39 | +// MarshalJSON implements the [json.Marshaler] interface |
| 40 | +// by marshalling the duration as a protobuf Duration. |
41 | 41 | func (d Duration) MarshalJSON() ([]byte, error) { |
42 | | - return json.Marshal(d.toWireFormat()) |
43 | | -} |
44 | | - |
45 | | -// toWireFormat returns a string representation of Duration |
46 | | -// which follows the wire format from Google Well Known Type. |
47 | | -// a String that ends in s to indicate seconds and is preceded by |
48 | | -// the number of seconds, with nanoseconds expressed as fractional seconds. |
49 | | -// |
50 | | -// https://protobuf.dev/reference/protobuf/google.protobuf/#duration |
51 | | -func (d Duration) toWireFormat() string { |
52 | | - // We do not use the standard time.Duration.String() and d.Duration.Seconds() |
53 | | - // method because they use float64 which loses precision. |
54 | | - |
55 | | - // Get the total nanoseconds as a precise integer. |
56 | | - sign := "" |
57 | | - if d.Duration < 0 { |
58 | | - sign = "-" |
59 | | - d.Duration = -d.Duration |
60 | | - } |
61 | | - |
62 | | - sec := d.Duration / time.Second |
63 | | - nsec := d.Duration % time.Second |
64 | | - |
65 | | - if nsec == 0 { |
66 | | - return fmt.Sprintf("%s%ds", sign, sec) |
67 | | - } |
68 | | - |
69 | | - frac := strings.TrimRight(fmt.Sprintf("%09d", nsec), "0") |
70 | | - return fmt.Sprintf("%s%d.%ss", sign, sec, frac) |
| 42 | + return protojson.Marshal(d.internal) |
71 | 43 | } |
72 | 44 |
|
73 | 45 | // EncodeValues implements the [query.Encoder] interface by encoding the |
74 | 46 | // duration as a string, like "3.3s". |
75 | 47 | func (d Duration) EncodeValues(key string, v *url.Values) error { |
76 | | - v.Set(key, d.toWireFormat()) |
| 48 | + res, err := protojson.Marshal(d.internal) |
| 49 | + if err != nil { |
| 50 | + return err |
| 51 | + } |
| 52 | + // remove the quotes from the string |
| 53 | + queryValue := strings.Trim(string(res), "\"") |
| 54 | + v.Set(key, queryValue) |
77 | 55 | return nil |
78 | 56 | } |
79 | 57 |
|
80 | 58 | // UnmarshalJSON implements the [json.Unmarshaler] interface. It can parse a |
81 | | -// duration from the Google well-known type format (e.g., "3.123s"). |
| 59 | +// duration from the protobuf Duration. |
82 | 60 | func (d *Duration) UnmarshalJSON(b []byte) error { |
83 | | - if d == nil { |
84 | | - return fmt.Errorf("json.Unmarshal on nil pointer") |
85 | | - } |
86 | | - // Remove the quotes from the string. |
87 | | - var s string |
88 | | - if err := json.Unmarshal(b, &s); err != nil { |
89 | | - return err |
90 | | - } |
91 | | - dur, err := time.ParseDuration(s) |
92 | | - if err != nil { |
| 61 | + var pb durationpb.Duration |
| 62 | + if err := protojson.Unmarshal(b, &pb); err != nil { |
93 | 63 | return err |
94 | 64 | } |
95 | | - *d = *New(dur) |
| 65 | + *d = *New(pb.AsDuration()) |
96 | 66 | return nil |
97 | 67 | } |
0 commit comments