diff --git a/examples/example.go b/examples/example.go index 0b3125a..c9975e5 100644 --- a/examples/example.go +++ b/examples/example.go @@ -21,7 +21,7 @@ func main() { if err != nil { fmt.Printf("error: %v\n\n", err) } else { - fmt.Printf("%#v\n\n", repos) + fmt.Printf("%v\n\n", github.Stringify(repos)) } rate, _, err := client.RateLimit() diff --git a/github/activity_events.go b/github/activity_events.go index 54c62c7..d9b2c84 100644 --- a/github/activity_events.go +++ b/github/activity_events.go @@ -25,6 +25,10 @@ type Event struct { ID *string `json:"id,omitempty"` } +func (e *Event) String() string { + return Stringify(e) +} + // Payload returns the parsed event payload. For recognized event types // (PushEvent), a value of the corresponding struct type will be returned. func (e *Event) Payload() (payload interface{}) { @@ -49,6 +53,10 @@ type PushEvent struct { Commits []PushEventCommit `json:"commits,omitempty"` } +func (p *PushEvent) String() string { + return Stringify(p) +} + // PushEventCommit represents a git commit in a GitHub PushEvent. type PushEventCommit struct { SHA *string `json:"sha,omitempty"` @@ -58,6 +66,10 @@ type PushEventCommit struct { Distinct *bool `json:"distinct"` } +func (p *PushEventCommit) String() string { + return Stringify(p) +} + // List public events. // // GitHub API docs: http://developer.github.com/v3/activity/events/#list-public-events diff --git a/github/gists.go b/github/gists.go index 8eb0d47..7b5112a 100644 --- a/github/gists.go +++ b/github/gists.go @@ -33,6 +33,10 @@ type Gist struct { CreatedAt *time.Time `json:"created_at,omitempty"` } +func (g *Gist) String() string { + return Stringify(g) +} + // GistFilename represents filename on a gist. type GistFilename string @@ -44,6 +48,10 @@ type GistFile struct { Content *string `json:"content,omitempty"` } +func (g *GistFile) String() string { + return Stringify(g) +} + // GistListOptions specifies the optional parameters to the // GistsService.List, GistsService.ListAll, and GistsService.ListStarred methods. type GistListOptions struct { diff --git a/github/gists_comments.go b/github/gists_comments.go index ba1c834..9d792e5 100644 --- a/github/gists_comments.go +++ b/github/gists_comments.go @@ -19,6 +19,10 @@ type GistComment struct { CreatedAt *time.Time `json:"created_at,omitempty"` } +func (g *GistComment) String() string { + return Stringify(g) +} + // ListComments lists all comments for a gist. // // GitHub API docs: http://developer.github.com/v3/gists/comments/#list-comments-on-a-gist diff --git a/github/git_commits.go b/github/git_commits.go index 981dc0f..3c9b7a4 100644 --- a/github/git_commits.go +++ b/github/git_commits.go @@ -20,6 +20,10 @@ type Commit struct { Parents []Commit `json:"parents,omitempty"` } +func (c *Commit) String() string { + return Stringify(c) +} + // CommitAuthor represents the author or committer of a commit. The commit // author may not correspond to a GitHub User. type CommitAuthor struct { @@ -28,6 +32,10 @@ type CommitAuthor struct { Email *string `json:"email,omitempty"` } +func (c *CommitAuthor) String() string { + return Stringify(c) +} + // GetCommit fetchs the Commit object for a given SHA. // // GitHub API docs: http://developer.github.com/v3/git/commits/#get-a-commit diff --git a/github/git_trees.go b/github/git_trees.go index 52d9e21..2b21cdb 100644 --- a/github/git_trees.go +++ b/github/git_trees.go @@ -13,6 +13,10 @@ type Tree struct { Entries []TreeEntry `json:"tree,omitempty"` } +func (t *Tree) String() string { + return Stringify(t) +} + // TreeEntry represents the contents of a tree structure. TreeEntry can // represent either a blob, a commit (in the case of a submodule), or another // tree. @@ -24,6 +28,10 @@ type TreeEntry struct { Size *int `json:"size,omitempty"` } +func (t *TreeEntry) String() string { + return Stringify(t) +} + // GetTree fetches the Tree object for a given sha hash from a repository. // // GitHub API docs: http://developer.github.com/v3/git/trees/#get-a-tree diff --git a/github/issues.go b/github/issues.go index 0a9df89..f4d6f6d 100644 --- a/github/issues.go +++ b/github/issues.go @@ -38,6 +38,10 @@ type Issue struct { // TODO(willnorris): milestone } +func (i *Issue) String() string { + return Stringify(i) +} + // IssueListOptions specifies the optional parameters to the IssuesService.List // and IssuesService.ListByOrg methods. type IssueListOptions struct { diff --git a/github/issues_comments.go b/github/issues_comments.go index dda85a9..acb5f0e 100644 --- a/github/issues_comments.go +++ b/github/issues_comments.go @@ -20,6 +20,10 @@ type IssueComment struct { UpdatedAt *time.Time `json:"updated_at,omitempty"` } +func (i *IssueComment) String() string { + return Stringify(i) +} + // IssueListCommentsOptions specifies the optional parameters to the // IssuesService.ListComments method. type IssueListCommentsOptions struct { diff --git a/github/issues_labels.go b/github/issues_labels.go index 20e59fb..fae8287 100644 --- a/github/issues_labels.go +++ b/github/issues_labels.go @@ -14,8 +14,8 @@ type Label struct { Color *string `json:"color,omitempty"` } -func (label Label) String() string { - return fmt.Sprint(label.Name) +func (l Label) String() string { + return fmt.Sprint(*l.Name) } // ListLabels lists all labels for a repository. diff --git a/github/orgs.go b/github/orgs.go index b359681..0bc90d4 100644 --- a/github/orgs.go +++ b/github/orgs.go @@ -30,6 +30,10 @@ type Organization struct { CreatedAt *time.Time `json:"created_at,omitempty"` } +func (o *Organization) String() string { + return Stringify(o) +} + // List the organizations for a user. Passing the empty string will list // organizations for the authenticated user. // diff --git a/github/orgs_teams.go b/github/orgs_teams.go index 9303b23..8c36a65 100644 --- a/github/orgs_teams.go +++ b/github/orgs_teams.go @@ -19,6 +19,10 @@ type Team struct { ReposCount *int `json:"repos_count,omitempty"` } +func (t *Team) String() string { + return Stringify(t) +} + // ListTeams lists all of the teams for an organization. // // GitHub API docs: http://developer.github.com/v3/orgs/teams/#list-teams diff --git a/github/pulls.go b/github/pulls.go index 6fdc461..cfec684 100644 --- a/github/pulls.go +++ b/github/pulls.go @@ -42,6 +42,10 @@ type PullRequest struct { // TODO(willnorris): add head and base once we have a Commit struct defined somewhere } +func (p *PullRequest) String() string { + return Stringify(p) +} + // PullRequestListOptions specifies the optional parameters to the // PullRequestsService.List method. type PullRequestListOptions struct { diff --git a/github/pulls_comments.go b/github/pulls_comments.go index 47a9cb4..a062a46 100644 --- a/github/pulls_comments.go +++ b/github/pulls_comments.go @@ -23,6 +23,10 @@ type PullRequestComment struct { UpdatedAt *time.Time `json:"updated_at,omitempty"` } +func (p *PullRequestComment) String() string { + return Stringify(p) +} + // PullRequestListCommentsOptions specifies the optional parameters to the // PullRequestsService.ListComments method. type PullRequestListCommentsOptions struct { diff --git a/github/repos.go b/github/repos.go index b5c93e4..78ac045 100644 --- a/github/repos.go +++ b/github/repos.go @@ -34,6 +34,10 @@ type Repository struct { HasWiki *bool `json:"has_wiki"` } +func (r *Repository) String() string { + return Stringify(r) +} + // RepositoryListOptions specifies the optional parameters to the // RepositoriesService.List method. type RepositoryListOptions struct { diff --git a/github/repos_comments.go b/github/repos_comments.go index ec425f6..c167cd4 100644 --- a/github/repos_comments.go +++ b/github/repos_comments.go @@ -26,6 +26,10 @@ type RepositoryComment struct { Position *int `json:"position,omitempty"` } +func (r *RepositoryComment) String() string { + return Stringify(r) +} + // ListComments lists all the comments for the repository. // // GitHub API docs: http://developer.github.com/v3/repos/comments/#list-commit-comments-for-a-repository diff --git a/github/repos_hooks.go b/github/repos_hooks.go index 7257be0..0acb011 100644 --- a/github/repos_hooks.go +++ b/github/repos_hooks.go @@ -33,6 +33,10 @@ type WebHookPayload struct { Repo *Repository `json:"repository,omitempty"` } +func (w *WebHookPayload) String() string { + return Stringify(w) +} + // WebHookCommit represents the commit variant we receive from GitHub in a // WebHookPayload. type WebHookCommit struct { @@ -47,6 +51,10 @@ type WebHookCommit struct { Timestamp *time.Time `json:"timestamp,omitempty"` } +func (w *WebHookCommit) String() string { + return Stringify(w) +} + // WebHookAuthor represents the author or committer of a commit, as specified // in a WebHookCommit. The commit author may not correspond to a GitHub User. type WebHookAuthor struct { @@ -55,6 +63,10 @@ type WebHookAuthor struct { Username *string `json:"username,omitempty"` } +func (w *WebHookAuthor) String() string { + return Stringify(w) +} + // Hook represents a GitHub (web and service) hook for a repository. type Hook struct { CreatedAt *time.Time `json:"created_at,omitempty"` @@ -66,6 +78,10 @@ type Hook struct { ID *int `json:"id,omitempty"` } +func (h *Hook) String() string { + return Stringify(h) +} + // CreateHook creates a Hook for the specified repository. // Name and Config are required fields. // diff --git a/github/repos_statuses.go b/github/repos_statuses.go index 87beb44..305b6bd 100644 --- a/github/repos_statuses.go +++ b/github/repos_statuses.go @@ -30,6 +30,10 @@ type RepoStatus struct { UpdatedAt *time.Time `json:"updated_at,omitempty"` } +func (r *RepoStatus) String() string { + return Stringify(r) +} + // ListStatuses lists the statuses of a repository at the specified // reference. ref can be a SHA, a branch name, or a tag name. // diff --git a/github/search.go b/github/search.go index 6248187..2370960 100644 --- a/github/search.go +++ b/github/search.go @@ -102,6 +102,10 @@ type CodeResult struct { Repository *Repository `json:"repository,omitempty"` } +func (c *CodeResult) String() string { + return Stringify(c) +} + // Code searches code via various criteria. // // GitHub API docs: http://developer.github.com/v3/search/#search-code diff --git a/github/strings.go b/github/strings.go new file mode 100644 index 0000000..c877251 --- /dev/null +++ b/github/strings.go @@ -0,0 +1,93 @@ +// Copyright 2013 The go-github AUTHORS. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package github + +import ( + "bytes" + "fmt" + "io" + + "reflect" +) + +var timestampType = reflect.TypeOf(Timestamp{}) + +// Stringify attempts to create a reasonable string representation of types in +// the GitHub library. It does things like resolve pointers to their values +// and omits struct fields with nil values. +func Stringify(message interface{}) string { + var buf bytes.Buffer + v := reflect.ValueOf(message) + stringifyValue(&buf, v) + return buf.String() +} + +// stringifyValue was heavily inspired by the goprotobuf library. + +func stringifyValue(w io.Writer, val reflect.Value) { + if val.Kind() == reflect.Ptr && val.IsNil() { + w.Write([]byte("")) + return + } + + v := reflect.Indirect(val) + + switch v.Kind() { + case reflect.String: + fmt.Fprintf(w, `"%s"`, v) + case reflect.Slice: + w.Write([]byte{'['}) + for i := 0; i < v.Len(); i++ { + if i > 0 { + w.Write([]byte{' '}) + } + + stringifyValue(w, v.Index(i)) + } + + w.Write([]byte{']'}) + return + case reflect.Struct: + if v.Type().Name() != "" { + w.Write([]byte(v.Type().String())) + } + + // special handling of Timestamp values + if v.Type() == timestampType { + fmt.Fprint(w, v.Interface()) + return + } + + w.Write([]byte{'{'}) + + var sep bool + for i := 0; i < v.NumField(); i++ { + fv := v.Field(i) + if fv.Kind() == reflect.Ptr && fv.IsNil() { + continue + } + if fv.Kind() == reflect.Slice && fv.IsNil() { + continue + } + + if sep { + w.Write([]byte(", ")) + } else { + sep = true + } + + w.Write([]byte(v.Type().Field(i).Name)) + w.Write([]byte{':'}) + stringifyValue(w, fv) + } + + w.Write([]byte{'}'}) + default: + if v.CanInterface() { + fmt.Fprint(w, v.Interface()) + } + } +} diff --git a/github/strings_test.go b/github/strings_test.go new file mode 100644 index 0000000..d6f5764 --- /dev/null +++ b/github/strings_test.go @@ -0,0 +1,75 @@ +// Copyright 2013 The go-github AUTHORS. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package github + +import ( + "testing" + "time" +) + +func TestStringify(t *testing.T) { + var nilPointer *string + + var tests = []struct { + in interface{} + out string + }{ + // basic types + {"foo", `"foo"`}, + {123, `123`}, + {1.5, `1.5`}, + {false, `false`}, + { + []string{"a", "b"}, + `["a" "b"]`, + }, + { + struct { + A []string + }{nil}, + // nil slice is skipped + `{}`, + }, + { + struct { + A string + }{"foo"}, + // structs not of a named type get no prefix + `{A:"foo"}`, + }, + + // pointers + {nilPointer, ``}, + {String("foo"), `"foo"`}, + {Int(123), `123`}, + {Bool(false), `false`}, + { + []*string{String("a"), String("b")}, + `["a" "b"]`, + }, + + // actual GitHub structs + { + Timestamp{time.Date(2006, 01, 02, 15, 04, 05, 0, time.UTC)}, + `github.Timestamp{2006-01-02 15:04:05 +0000 UTC}`, + }, + { + User{ID: Int(123), Name: String("n")}, + `github.User{ID:123, Name:"n"}`, + }, + { + Repository{Owner: &User{ID: Int(123)}}, + `github.Repository{Owner:github.User{ID:123}}`, + }, + } + + for i, tt := range tests { + s := Stringify(tt.in) + if s != tt.out { + t.Errorf("%d. Stringify(%q) => %q, want %q", i, tt.in, s, tt.out) + } + } +} diff --git a/github/timestamp.go b/github/timestamp.go index 7aa31e9..47b7e04 100644 --- a/github/timestamp.go +++ b/github/timestamp.go @@ -18,6 +18,10 @@ type Timestamp struct { time.Time } +func (t *Timestamp) String() string { + return t.Time.String() +} + // UnmarshalJSON implements the json.Unmarshaler interface. // Time is expected in RFC3339 or Unix format. func (t *Timestamp) UnmarshalJSON(data []byte) (err error) { diff --git a/github/users.go b/github/users.go index 5dacc69..c165ea8 100644 --- a/github/users.go +++ b/github/users.go @@ -39,6 +39,10 @@ type User struct { CreatedAt *time.Time `json:"created_at,omitempty"` } +func (u *User) String() string { + return Stringify(u) +} + // Get fetches a user. Passing the empty string will fetch the authenticated // user. //