Skip to content

Commit 70bbb05

Browse files
dmitshurandygrunwald
authored andcommitted
Use Timestamp type for all time fields.
This is a breaking API change. It improves usability of time fields by automatically parsing them into a time.Time-like type. Pointers are used for optional fields, so that the ,omitempty option correctly omits them when they have zero value (i.e., nil). This can't be done with values at this time (see golang/go#11939). Resolves #55.
1 parent 5416038 commit 70bbb05

File tree

6 files changed

+114
-13
lines changed

6 files changed

+114
-13
lines changed

accounts.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ type AccountInput struct {
9494
// AccountDetailInfo entity contains detailed information about an account.
9595
type AccountDetailInfo struct {
9696
AccountInfo
97-
RegisteredOn string `json:"registered_on"`
97+
RegisteredOn Timestamp `json:"registered_on"`
9898
}
9999

100100
// AccountNameInput entity contains information for setting a name for an account.

changes.go

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,10 @@ type WebLinkInfo struct {
2525

2626
// GitPersonInfo entity contains information about the author/committer of a commit.
2727
type GitPersonInfo struct {
28-
Name string `json:"name"`
29-
Email string `json:"email"`
30-
Date string `json:"date"`
31-
TZ int `json:"tz"`
28+
Name string `json:"name"`
29+
Email string `json:"email"`
30+
Date Timestamp `json:"date"`
31+
TZ int `json:"tz"`
3232
}
3333

3434
// NotifyInfo entity contains detailed information about who should be
@@ -67,7 +67,7 @@ type ChangeEditMessageInput struct {
6767
type ChangeMessageInfo struct {
6868
ID string `json:"id"`
6969
Author AccountInfo `json:"author,omitempty"`
70-
Date string `json:"date"`
70+
Date Timestamp `json:"date"`
7171
Message string `json:"message"`
7272
Tag string `json:"tag,omitempty"`
7373
RevisionNumber int `json:"_revision_number,omitempty"`
@@ -247,7 +247,7 @@ type CommentInput struct {
247247
Line int `json:"line,omitempty"`
248248
Range *CommentRange `json:"range,omitempty"`
249249
InReplyTo string `json:"in_reply_to,omitempty"`
250-
Updated string `json:"updated,omitempty"`
250+
Updated *Timestamp `json:"updated,omitempty"`
251251
Message string `json:"message,omitempty"`
252252
}
253253

@@ -274,9 +274,9 @@ type ChangeInfo struct {
274274
ChangeID string `json:"change_id"`
275275
Subject string `json:"subject"`
276276
Status string `json:"status"`
277-
Created string `json:"created"`
278-
Updated string `json:"updated"`
279-
Submitted string `json:"submitted,omitempty"`
277+
Created Timestamp `json:"created"`
278+
Updated Timestamp `json:"updated"`
279+
Submitted *Timestamp `json:"submitted,omitempty"`
280280
Starred bool `json:"starred,omitempty"`
281281
Reviewed bool `json:"reviewed,omitempty"`
282282
Mergeable bool `json:"mergeable,omitempty"`
@@ -318,7 +318,7 @@ type LabelInfo struct {
318318
type RevisionInfo struct {
319319
Draft bool `json:"draft,omitempty"`
320320
Number int `json:"_number"`
321-
Created string `json:"created"`
321+
Created Timestamp `json:"created"`
322322
Uploader AccountInfo `json:"uploader"`
323323
Ref string `json:"ref"`
324324
Fetch map[string]FetchInfo `json:"fetch"`
@@ -339,7 +339,7 @@ type CommentInfo struct {
339339
Range CommentRange `json:"range,omitempty"`
340340
InReplyTo string `json:"in_reply_to,omitempty"`
341341
Message string `json:"message,omitempty"`
342-
Updated string `json:"updated"`
342+
Updated Timestamp `json:"updated"`
343343
Author AccountInfo `json:"author,omitempty"`
344344
}
345345

groups.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ type GroupAuditEventInfo struct {
1616
// TODO Member AccountInfo OR GroupInfo `json:"member"`
1717
Type string `json:"type"`
1818
User AccountInfo `json:"user"`
19-
Date string `json:"date"`
19+
Date Timestamp `json:"date"`
2020
}
2121

2222
// GroupInfo entity contains information about a group.
@@ -30,6 +30,7 @@ type GroupInfo struct {
3030
GroupID int `json:"group_id,omitempty"`
3131
Owner string `json:"owner,omitempty"`
3232
OwnerID string `json:"owner_id,omitempty"`
33+
CreatedOn *Timestamp `json:"created_on,omitempty"`
3334
Members []AccountInfo `json:"members,omitempty"`
3435
Includes []GroupInfo `json:"includes,omitempty"`
3536
}

projects_tag.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ type TagInfo struct {
1212
Object string `json:"object"`
1313
Message string `json:"message"`
1414
Tagger GitPersonInfo `json:"tagger"`
15+
Created *Timestamp `json:"created,omitempty"`
1516
}
1617

1718
// ListTags list the tags of a project.

types.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,55 @@ import (
44
"encoding/json"
55
"errors"
66
"strconv"
7+
"time"
78
)
89

10+
// Timestamp represents an instant in time with nanosecond precision, in UTC time zone.
11+
// It encodes to and from JSON in Gerrit's timestamp format.
12+
// All exported methods of time.Time can be called on Timestamp.
13+
//
14+
// Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/rest-api.html#timestamp
15+
type Timestamp struct {
16+
// Time is an instant in time. Its time zone must be UTC.
17+
time.Time
18+
}
19+
20+
// MarshalJSON implements the json.Marshaler interface.
21+
// The time is a quoted string in Gerrit's timestamp format.
22+
// An error is returned if t.Time time zone is not UTC.
23+
func (t Timestamp) MarshalJSON() ([]byte, error) {
24+
if t.Location() != time.UTC {
25+
return nil, errors.New("Timestamp.MarshalJSON: time zone must be UTC")
26+
}
27+
if y := t.Year(); y < 0 || 9999 < y {
28+
// RFC 3339 is clear that years are 4 digits exactly.
29+
// See golang.org/issue/4556#issuecomment-66073163 for more discussion.
30+
return nil, errors.New("Timestamp.MarshalJSON: year outside of range [0,9999]")
31+
}
32+
b := make([]byte, 0, len(timeLayout)+2)
33+
b = append(b, '"')
34+
b = t.AppendFormat(b, timeLayout)
35+
b = append(b, '"')
36+
return b, nil
37+
}
38+
39+
// UnmarshalJSON implements the json.Unmarshaler interface.
40+
// The time is expected to be a quoted string in Gerrit's timestamp format.
41+
func (t *Timestamp) UnmarshalJSON(b []byte) error {
42+
// Ignore null, like in the main JSON package.
43+
if string(b) == "null" {
44+
return nil
45+
}
46+
var err error
47+
t.Time, err = time.Parse(`"`+timeLayout+`"`, string(b))
48+
return err
49+
}
50+
51+
// Gerrit's timestamp layout is like time.RFC3339Nano, but with a space instead
52+
// of the "T", without a timezone (it's always in UTC), and always includes nanoseconds.
53+
// See https://gerrit-review.googlesource.com/Documentation/rest-api.html#timestamp.
54+
const timeLayout = "2006-01-02 15:04:05.000000000"
55+
956
// Number is a string representing a number. This type is only used in cases
1057
// where the API being queried may return an inconsistent result.
1158
type Number string

types_test.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,64 @@
11
package gerrit_test
22

33
import (
4+
"bytes"
45
"encoding/json"
6+
"reflect"
57
"testing"
8+
"time"
69

710
"github.com/andygrunwald/go-gerrit"
811
)
912

13+
func TestTimestamp(t *testing.T) {
14+
const jsonData = `{
15+
"subject": "net/http: write status code in Redirect when Content-Type header set",
16+
"created": "2018-05-04 17:24:39.000000000",
17+
"updated": "0001-01-01 00:00:00.000000000",
18+
"submitted": "2018-05-04 18:01:10.000000000",
19+
"_number": 111517
20+
}
21+
`
22+
type ChangeInfo struct {
23+
Subject string `json:"subject"`
24+
Created gerrit.Timestamp `json:"created"`
25+
Updated gerrit.Timestamp `json:"updated"`
26+
Submitted *gerrit.Timestamp `json:"submitted,omitempty"`
27+
Omitted *gerrit.Timestamp `json:"omitted,omitempty"`
28+
Number int `json:"_number"`
29+
}
30+
ci := ChangeInfo{
31+
Subject: "net/http: write status code in Redirect when Content-Type header set",
32+
Created: gerrit.Timestamp{Time: time.Date(2018, 5, 4, 17, 24, 39, 0, time.UTC)},
33+
Updated: gerrit.Timestamp{},
34+
Submitted: &gerrit.Timestamp{Time: time.Date(2018, 5, 4, 18, 1, 10, 0, time.UTC)},
35+
Omitted: nil,
36+
Number: 111517,
37+
}
38+
39+
// Try decoding JSON data into a ChangeInfo struct.
40+
var v ChangeInfo
41+
err := json.Unmarshal([]byte(jsonData), &v)
42+
if err != nil {
43+
t.Fatal(err)
44+
}
45+
if got, want := v, ci; !reflect.DeepEqual(got, want) {
46+
t.Errorf("decoding JSON data into a ChangeInfo struct:\ngot:\n%v\nwant:\n%v", got, want)
47+
}
48+
49+
// Try encoding a ChangeInfo struct into JSON data.
50+
var buf bytes.Buffer
51+
e := json.NewEncoder(&buf)
52+
e.SetIndent("", "\t")
53+
err = e.Encode(ci)
54+
if err != nil {
55+
t.Fatal(err)
56+
}
57+
if got, want := buf.String(), jsonData; got != want {
58+
t.Errorf("encoding a ChangeInfo struct into JSON data:\ngot:\n%v\nwant:\n%v", got, want)
59+
}
60+
}
61+
1062
func TestTypesNumber_String(t *testing.T) {
1163
number := gerrit.Number("7")
1264
if number.String() != "7" {

0 commit comments

Comments
 (0)