Skip to content

Commit 6cd5094

Browse files
committed
Pb types
1 parent 959aa0f commit 6cd5094

6 files changed

Lines changed: 71 additions & 145 deletions

File tree

common/types/duration/duration.go

Lines changed: 24 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,97 +1,67 @@
11
package duration
22

33
import (
4-
"encoding/json"
5-
"fmt"
64
"net/url"
75
"strings"
86
"time"
7+
8+
"google.golang.org/protobuf/encoding/protojson"
9+
"google.golang.org/protobuf/types/known/durationpb"
910
)
1011

11-
// Duration is a wrapper for time.Duration to provide custom marshaling
12+
// Duration is a wrapper for durationpb.Duration to provide custom marshaling
1213
// for JSON and URL query strings.
1314
//
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.
1717
//
1818
// Example:
1919
//
2020
// customDur := durationpb.New(30 * time.Second)
21-
// goDur := customDur.AsDuration() // Access the underlying time.Duration
21+
// goDur := customDur.AsDuration()
2222
type Duration struct {
23-
time.Duration
23+
internal *durationpb.Duration
2424
}
2525

2626
// New creates a custom Duration from a standard time.Duration.
2727
func New(d time.Duration) *Duration {
28-
return &Duration{Duration: d}
28+
return &Duration{internal: durationpb.New(d)}
2929
}
3030

3131
// AsDuration returns the underlying time.Duration value.
3232
func (x *Duration) AsDuration() time.Duration {
3333
if x == nil {
3434
return 0
3535
}
36-
return x.Duration
36+
return x.internal.AsDuration()
3737
}
3838

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.
4141
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)
7143
}
7244

7345
// EncodeValues implements the [query.Encoder] interface by encoding the
7446
// duration as a string, like "3.3s".
7547
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)
7755
return nil
7856
}
7957

8058
// 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.
8260
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 {
9363
return err
9464
}
95-
*d = *New(dur)
65+
*d = *New(pb.AsDuration())
9666
return nil
9767
}

common/types/duration/duration_test.go

Lines changed: 12 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,6 @@ import (
77
"time"
88
)
99

10-
func TestNew(t *testing.T) {
11-
d := time.Second * 5
12-
dur := New(d)
13-
if dur.Duration != d {
14-
t.Errorf("New() = %v, want %v", dur.Duration, d)
15-
}
16-
}
17-
1810
func TestAsDuration(t *testing.T) {
1911
d := time.Second * 5
2012
dur := New(d)
@@ -49,12 +41,12 @@ func TestDuration_MarshalJSON(t *testing.T) {
4941
{
5042
name: "negative duration with fractional seconds",
5143
duration: *New(-2*time.Minute + 100*time.Millisecond),
52-
expected: "-119.9s",
44+
expected: "-119.900s",
5345
},
5446
{
5547
name: "fractional seconds",
5648
duration: *New(1500 * time.Millisecond),
57-
expected: "1.5s",
49+
expected: "1.500s",
5850
},
5951
{
6052
name: "large duration",
@@ -142,22 +134,14 @@ func TestDuration_UnmarshalJSON(t *testing.T) {
142134
t.Errorf("Duration.UnmarshalJSON() error = %v, wantErr %v", err, tt.wantErr)
143135
return
144136
}
145-
if !tt.wantErr && d != tt.want {
146-
t.Errorf("Duration.UnmarshalJSON() = %v, want %v", d, tt.want)
137+
if !tt.wantErr {
138+
// We cannot compare Proto messages directly, so we compare the underlying time.Duration
139+
if d.AsDuration() != tt.want.AsDuration() {
140+
t.Errorf("Duration.UnmarshalJSON() = %v, want %v", d.AsDuration(), tt.want.AsDuration())
141+
}
147142
}
148-
})
149-
}
150-
}
151143

152-
func TestDuration_UnmarshalJSON_NilPointer(t *testing.T) {
153-
var d *Duration
154-
err := d.UnmarshalJSON([]byte(`"5s"`))
155-
if err == nil {
156-
t.Error("Duration.UnmarshalJSON() on nil pointer should return error")
157-
}
158-
expectedErr := "json.Unmarshal on nil pointer"
159-
if err.Error() != expectedErr {
160-
t.Errorf("Duration.UnmarshalJSON() error = %v, want %v", err.Error(), expectedErr)
144+
})
161145
}
162146
}
163147

@@ -190,7 +174,7 @@ func TestDuration_EncodeValues(t *testing.T) {
190174
name: "fractional seconds",
191175
duration: *New(1500 * time.Millisecond),
192176
key: "interval",
193-
expected: "1.5s",
177+
expected: "1.500s",
194178
},
195179
{
196180
name: "large duration",
@@ -261,9 +245,10 @@ func TestDuration_JSONRoundTrip(t *testing.T) {
261245
}
262246

263247
// Check that the round trip preserved the value
264-
if result != tt.duration {
265-
t.Errorf("JSON round trip failed: original = %v, result = %v", tt.duration, result)
248+
if result.AsDuration() != tt.duration.AsDuration() {
249+
t.Errorf("Duration.UnmarshalJSON() = %v, want %v", result.AsDuration(), tt.duration.AsDuration())
266250
}
251+
267252
})
268253
}
269254
}

common/types/time/time.go

Lines changed: 29 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,75 +1,67 @@
11
package time
22

33
import (
4-
"encoding/json"
5-
"errors"
6-
"fmt"
74
"net/url"
5+
"strings"
86
"time"
7+
8+
"google.golang.org/protobuf/encoding/protojson"
9+
"google.golang.org/protobuf/types/known/timestamppb"
910
)
1011

11-
// Time is a wrapper for time.Time to provide custom marshaling
12+
// Time is a wrapper for timestamppb.Timestamp to provide custom marshaling
1213
// for JSON and URL query strings.
1314
//
14-
// It embeds time.Time, so all standard methods (Format, Parse, etc.)
15-
// are directly accessible. The underlying time.Time value can be
16-
// accessed via the .AsTime() method.
15+
// It embeds timestamppb.Timestamp and exposes the .AsTime() method to
16+
// easily convert to time.Time.
1717
//
1818
// Example:
1919
//
2020
// customTime := time.New(stdtime.Now())
21-
// goTime := customTime.AsTime() // Access the underlying stdtime.Time
21+
// goTime := customTime.AsTime()
2222
type Time struct {
23-
time.Time
23+
internal *timestamppb.Timestamp
2424
}
2525

26-
// New creates a custom Time from a standard stdtime.Time.
26+
// New creates a custom Time from a standard time.Time.
2727
func New(t time.Time) *Time {
28-
return &Time{Time: t}
28+
return &Time{internal: timestamppb.New(t)}
2929
}
3030

31-
// AsTime returns the underlying stdtime.Time value.
31+
// AsTime returns the underlying time.Time value.
3232
func (x *Time) AsTime() time.Time {
3333
if x == nil {
3434
return time.Time{}
3535
}
36-
return x.Time
36+
return x.internal.AsTime()
3737
}
3838

39-
// MarshalJSON implements the [json.Marshaler] interface by formatting the
40-
// time as a string according to RFC3339Nano
39+
// MarshalJSON implements the [json.Marshaler] interface
40+
// by marshalling the time as a protobuf Timestamp.
4141
func (t Time) MarshalJSON() ([]byte, error) {
42-
return json.Marshal(t.Time.Format(time.RFC3339Nano))
42+
return protojson.Marshal(t.internal)
4343
}
4444

45-
// UnmarshalJSON implements the [json.Unmarshaler] interface by parsing the
46-
// time from a string according to RFC3339Nano
45+
// UnmarshalJSON implements the [json.Unmarshaler] interface
46+
// by unmarshalling the time from a protobuf Timestamp.
4747
func (t *Time) UnmarshalJSON(b []byte) error {
48-
if t == nil {
49-
return fmt.Errorf("json.Unmarshal on nil pointer")
50-
}
51-
52-
var s string
53-
if err := json.Unmarshal(b, &s); err != nil {
48+
var pb timestamppb.Timestamp
49+
if err := protojson.Unmarshal(b, &pb); err != nil {
5450
return err
5551
}
56-
57-
if s == "" {
58-
return errors.New("time is empty. It should be in RFC3339Nano format")
59-
}
60-
61-
timeValue, err := time.Parse(time.RFC3339Nano, s)
62-
if err != nil {
63-
return err
64-
}
65-
66-
*t = Time{Time: timeValue}
52+
*t = *New(pb.AsTime())
6753
return nil
6854
}
6955

7056
// EncodeValues implements the [query.Encoder] interface by encoding the
71-
// time as a string according to RFC3339Nano.
57+
// time as a protobuf Timestamp.
7258
func (t Time) EncodeValues(key string, v *url.Values) error {
73-
v.Set(key, t.Time.Format(time.RFC3339Nano))
59+
res, err := protojson.Marshal(t.internal)
60+
if err != nil {
61+
return err
62+
}
63+
// remove the quotes from the string.
64+
queryValue := strings.Trim(string(res), "\"")
65+
v.Set(key, queryValue)
7466
return nil
7567
}

common/types/time/time_test.go

Lines changed: 5 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,6 @@ import (
77
"time"
88
)
99

10-
func TestNew(t *testing.T) {
11-
now := time.Now()
12-
timeVal := New(now)
13-
if !timeVal.Time.Equal(now) {
14-
t.Errorf("New() = %v, want %v", timeVal.Time, now)
15-
}
16-
}
17-
1810
func TestAsTime(t *testing.T) {
1911
now := time.Now()
2012
timeVal := New(now)
@@ -58,7 +50,7 @@ func TestTime_MarshalJSON(t *testing.T) {
5850
{
5951
name: "time with timezone",
6052
time: *New(time.Date(2023, 12, 25, 10, 30, 0, 0, time.FixedZone("EST", -5*3600))),
61-
expected: `"2023-12-25T10:30:00-05:00"`,
53+
expected: `"2023-12-25T15:30:00Z"`,
6254
},
6355
}
6456

@@ -134,25 +126,13 @@ func TestTime_UnmarshalJSON(t *testing.T) {
134126
t.Errorf("Time.UnmarshalJSON() error = %v, wantErr %v", err, tt.wantErr)
135127
return
136128
}
137-
if !tt.wantErr && !timeVal.Time.Equal(tt.want.Time) {
129+
if !tt.wantErr && !timeVal.AsTime().Equal(tt.want.AsTime()) {
138130
t.Errorf("Time.UnmarshalJSON() = %v, want %v", timeVal, tt.want)
139131
}
140132
})
141133
}
142134
}
143135

144-
func TestTime_UnmarshalJSON_NilPointer(t *testing.T) {
145-
var timeVal *Time
146-
err := timeVal.UnmarshalJSON([]byte(`"2023-12-25T10:30:00Z"`))
147-
if err == nil {
148-
t.Error("Time.UnmarshalJSON() on nil pointer should return error")
149-
}
150-
expectedErr := "json.Unmarshal on nil pointer"
151-
if err.Error() != expectedErr {
152-
t.Errorf("Time.UnmarshalJSON() error = %v, want %v", err.Error(), expectedErr)
153-
}
154-
}
155-
156136
func TestTime_EncodeValues(t *testing.T) {
157137
tests := []struct {
158138
name string
@@ -182,7 +162,7 @@ func TestTime_EncodeValues(t *testing.T) {
182162
name: "time with timezone",
183163
time: *New(time.Date(2023, 12, 25, 10, 30, 0, 0, time.FixedZone("EST", -5*3600))),
184164
key: "local_time",
185-
expected: "2023-12-25T10:30:00-05:00",
165+
expected: "2023-12-25T15:30:00Z",
186166
},
187167
}
188168

@@ -247,7 +227,7 @@ func TestTime_JSONRoundTrip(t *testing.T) {
247227
}
248228

249229
// Check that the round trip preserved the value
250-
if !result.Time.Equal(tt.time.Time) {
230+
if !result.AsTime().Equal(tt.time.AsTime()) {
251231
t.Errorf("JSON round trip failed: original = %v, result = %v", tt.time, result)
252232
}
253233
})
@@ -290,7 +270,7 @@ func TestTime_EdgeCases(t *testing.T) {
290270
return
291271
}
292272

293-
if !result.Time.Equal(tt.time) {
273+
if !result.AsTime().Equal(tt.time) {
294274
t.Errorf("Time round trip failed: original = %v, result = %v", tt.time, result)
295275
}
296276
})

config/.cache/Microsoft/DeveloperTools/deviceid

Lines changed: 0 additions & 1 deletion
This file was deleted.

0 commit comments

Comments
 (0)