Skip to content

Commit

Permalink
Use Timestamp type for all time fields.
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
dmitshur authored and andygrunwald committed May 6, 2018
1 parent 5416038 commit 70bbb05
Show file tree
Hide file tree
Showing 6 changed files with 114 additions and 13 deletions.
2 changes: 1 addition & 1 deletion accounts.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ type AccountInput struct {
// AccountDetailInfo entity contains detailed information about an account.
type AccountDetailInfo struct {
AccountInfo
RegisteredOn string `json:"registered_on"`
RegisteredOn Timestamp `json:"registered_on"`
}

// AccountNameInput entity contains information for setting a name for an account.
Expand Down
22 changes: 11 additions & 11 deletions changes.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@ type WebLinkInfo struct {

// GitPersonInfo entity contains information about the author/committer of a commit.
type GitPersonInfo struct {
Name string `json:"name"`
Email string `json:"email"`
Date string `json:"date"`
TZ int `json:"tz"`
Name string `json:"name"`
Email string `json:"email"`
Date Timestamp `json:"date"`
TZ int `json:"tz"`
}

// NotifyInfo entity contains detailed information about who should be
Expand Down Expand Up @@ -67,7 +67,7 @@ type ChangeEditMessageInput struct {
type ChangeMessageInfo struct {
ID string `json:"id"`
Author AccountInfo `json:"author,omitempty"`
Date string `json:"date"`
Date Timestamp `json:"date"`
Message string `json:"message"`
Tag string `json:"tag,omitempty"`
RevisionNumber int `json:"_revision_number,omitempty"`
Expand Down Expand Up @@ -247,7 +247,7 @@ type CommentInput struct {
Line int `json:"line,omitempty"`
Range *CommentRange `json:"range,omitempty"`
InReplyTo string `json:"in_reply_to,omitempty"`
Updated string `json:"updated,omitempty"`
Updated *Timestamp `json:"updated,omitempty"`
Message string `json:"message,omitempty"`
}

Expand All @@ -274,9 +274,9 @@ type ChangeInfo struct {
ChangeID string `json:"change_id"`
Subject string `json:"subject"`
Status string `json:"status"`
Created string `json:"created"`
Updated string `json:"updated"`
Submitted string `json:"submitted,omitempty"`
Created Timestamp `json:"created"`
Updated Timestamp `json:"updated"`
Submitted *Timestamp `json:"submitted,omitempty"`
Starred bool `json:"starred,omitempty"`
Reviewed bool `json:"reviewed,omitempty"`
Mergeable bool `json:"mergeable,omitempty"`
Expand Down Expand Up @@ -318,7 +318,7 @@ type LabelInfo struct {
type RevisionInfo struct {
Draft bool `json:"draft,omitempty"`
Number int `json:"_number"`
Created string `json:"created"`
Created Timestamp `json:"created"`
Uploader AccountInfo `json:"uploader"`
Ref string `json:"ref"`
Fetch map[string]FetchInfo `json:"fetch"`
Expand All @@ -339,7 +339,7 @@ type CommentInfo struct {
Range CommentRange `json:"range,omitempty"`
InReplyTo string `json:"in_reply_to,omitempty"`
Message string `json:"message,omitempty"`
Updated string `json:"updated"`
Updated Timestamp `json:"updated"`
Author AccountInfo `json:"author,omitempty"`
}

Expand Down
3 changes: 2 additions & 1 deletion groups.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ type GroupAuditEventInfo struct {
// TODO Member AccountInfo OR GroupInfo `json:"member"`
Type string `json:"type"`
User AccountInfo `json:"user"`
Date string `json:"date"`
Date Timestamp `json:"date"`
}

// GroupInfo entity contains information about a group.
Expand All @@ -30,6 +30,7 @@ type GroupInfo struct {
GroupID int `json:"group_id,omitempty"`
Owner string `json:"owner,omitempty"`
OwnerID string `json:"owner_id,omitempty"`
CreatedOn *Timestamp `json:"created_on,omitempty"`
Members []AccountInfo `json:"members,omitempty"`
Includes []GroupInfo `json:"includes,omitempty"`
}
Expand Down
1 change: 1 addition & 0 deletions projects_tag.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ type TagInfo struct {
Object string `json:"object"`
Message string `json:"message"`
Tagger GitPersonInfo `json:"tagger"`
Created *Timestamp `json:"created,omitempty"`
}

// ListTags list the tags of a project.
Expand Down
47 changes: 47 additions & 0 deletions types.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,55 @@ import (
"encoding/json"
"errors"
"strconv"
"time"
)

// Timestamp represents an instant in time with nanosecond precision, in UTC time zone.
// It encodes to and from JSON in Gerrit's timestamp format.
// All exported methods of time.Time can be called on Timestamp.
//
// Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/rest-api.html#timestamp
type Timestamp struct {
// Time is an instant in time. Its time zone must be UTC.
time.Time
}

// MarshalJSON implements the json.Marshaler interface.
// The time is a quoted string in Gerrit's timestamp format.
// An error is returned if t.Time time zone is not UTC.
func (t Timestamp) MarshalJSON() ([]byte, error) {
if t.Location() != time.UTC {
return nil, errors.New("Timestamp.MarshalJSON: time zone must be UTC")
}
if y := t.Year(); y < 0 || 9999 < y {
// RFC 3339 is clear that years are 4 digits exactly.
// See golang.org/issue/4556#issuecomment-66073163 for more discussion.
return nil, errors.New("Timestamp.MarshalJSON: year outside of range [0,9999]")
}
b := make([]byte, 0, len(timeLayout)+2)
b = append(b, '"')
b = t.AppendFormat(b, timeLayout)
b = append(b, '"')
return b, nil
}

// UnmarshalJSON implements the json.Unmarshaler interface.
// The time is expected to be a quoted string in Gerrit's timestamp format.
func (t *Timestamp) UnmarshalJSON(b []byte) error {
// Ignore null, like in the main JSON package.
if string(b) == "null" {
return nil
}
var err error
t.Time, err = time.Parse(`"`+timeLayout+`"`, string(b))
return err
}

// Gerrit's timestamp layout is like time.RFC3339Nano, but with a space instead
// of the "T", without a timezone (it's always in UTC), and always includes nanoseconds.
// See https://gerrit-review.googlesource.com/Documentation/rest-api.html#timestamp.
const timeLayout = "2006-01-02 15:04:05.000000000"

// Number is a string representing a number. This type is only used in cases
// where the API being queried may return an inconsistent result.
type Number string
Expand Down
52 changes: 52 additions & 0 deletions types_test.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,64 @@
package gerrit_test

import (
"bytes"
"encoding/json"
"reflect"
"testing"
"time"

"github.com/andygrunwald/go-gerrit"
)

func TestTimestamp(t *testing.T) {
const jsonData = `{
"subject": "net/http: write status code in Redirect when Content-Type header set",
"created": "2018-05-04 17:24:39.000000000",
"updated": "0001-01-01 00:00:00.000000000",
"submitted": "2018-05-04 18:01:10.000000000",
"_number": 111517
}
`
type ChangeInfo struct {
Subject string `json:"subject"`
Created gerrit.Timestamp `json:"created"`
Updated gerrit.Timestamp `json:"updated"`
Submitted *gerrit.Timestamp `json:"submitted,omitempty"`
Omitted *gerrit.Timestamp `json:"omitted,omitempty"`
Number int `json:"_number"`
}
ci := ChangeInfo{
Subject: "net/http: write status code in Redirect when Content-Type header set",
Created: gerrit.Timestamp{Time: time.Date(2018, 5, 4, 17, 24, 39, 0, time.UTC)},
Updated: gerrit.Timestamp{},
Submitted: &gerrit.Timestamp{Time: time.Date(2018, 5, 4, 18, 1, 10, 0, time.UTC)},
Omitted: nil,
Number: 111517,
}

// Try decoding JSON data into a ChangeInfo struct.
var v ChangeInfo
err := json.Unmarshal([]byte(jsonData), &v)
if err != nil {
t.Fatal(err)
}
if got, want := v, ci; !reflect.DeepEqual(got, want) {
t.Errorf("decoding JSON data into a ChangeInfo struct:\ngot:\n%v\nwant:\n%v", got, want)
}

// Try encoding a ChangeInfo struct into JSON data.
var buf bytes.Buffer
e := json.NewEncoder(&buf)
e.SetIndent("", "\t")
err = e.Encode(ci)
if err != nil {
t.Fatal(err)
}
if got, want := buf.String(), jsonData; got != want {
t.Errorf("encoding a ChangeInfo struct into JSON data:\ngot:\n%v\nwant:\n%v", got, want)
}
}

func TestTypesNumber_String(t *testing.T) {
number := gerrit.Number("7")
if number.String() != "7" {
Expand Down

0 comments on commit 70bbb05

Please sign in to comment.