From 4e62bb73d58e2fde477efaf3ea5d8c892071deda Mon Sep 17 00:00:00 2001 From: Beyang Liu Date: Wed, 31 Jul 2013 14:48:34 -0700 Subject: [PATCH] initial search service and repository search --- github/github.go | 4 ++ github/github_test.go | 6 ++ github/search.go | 134 +++++++++++++++++++++++++++++++++++++++ github/search_test.go | 141 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 285 insertions(+) create mode 100644 github/search.go create mode 100644 github/search_test.go diff --git a/github/github.go b/github/github.go index 56a71ba..c9d268f 100644 --- a/github/github.go +++ b/github/github.go @@ -65,6 +65,8 @@ const ( headerRateLimit = "X-RateLimit-Limit" headerRateRemaining = "X-RateLimit-Remaining" headerRateReset = "X-RateLimit-Reset" + + mimePreview = "application/vnd.github.preview" ) // A Client manages communication with the GitHub API. @@ -96,6 +98,7 @@ type Client struct { Users *UsersService Gists *GistsService Activity *ActivityService + Search *SearchService } // ListOptions specifies the optional parameters to various List methods that @@ -124,6 +127,7 @@ func NewClient(httpClient *http.Client) *Client { c.Users = &UsersService{client: c} c.Gists = &GistsService{client: c} c.Activity = &ActivityService{client: c} + c.Search = &SearchService{client: c} return c } diff --git a/github/github_test.go b/github/github_test.go index 401875d..12a7c34 100644 --- a/github/github_test.go +++ b/github/github_test.go @@ -63,6 +63,12 @@ func testFormValues(t *testing.T, r *http.Request, values values) { } } +func testHeader(t *testing.T, r *http.Request, header string, want string) { + if value := r.Header.Get(header); want != value { + t.Errorf("Header %s = %s, want: %s", header, value, want) + } +} + func testURLParseError(t *testing.T, err error) { if err == nil { t.Errorf("Expected error to be returned") diff --git a/github/search.go b/github/search.go new file mode 100644 index 0000000..8592783 --- /dev/null +++ b/github/search.go @@ -0,0 +1,134 @@ +// 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 ( + "fmt" + "net/url" + "strconv" +) + +// SearchService provides access to the search related functions +// in the GitHub API. +// +// GitHub API docs: http://developer.github.com/v3/search/ +type SearchService struct { + client *Client +} + +// SearchOptions specifies optional parameters to the SearchService methods. +type SearchOptions struct { + // How to sort the search results. Possible values are: + // - for repositories: stars, fork, updated + // - for code: indexed + // - for issues: comments, created, updated + // - for users: followers, repositories, joined + // + // Default is to sort by best match. + Sort string + + // Sort order if sort parameter is provided. Possible values are: asc, + // desc. Default is desc. + Order string + + // Page of results to retrieve. + Page int + + // Number of results to show per page. This can be up to 100 + // according to GitHub. + PerPage int +} + +// RepositoriesSearchResult represents the result of a repositories search. +type RepositoriesSearchResult struct { + Total int `json:"total_count,omitempty"` + Repositories []Repository `json:"items,omitempty"` +} + +// Repositories searches repositories via various criteria. +// +// GitHub API docs: http://developer.github.com/v3/search/#search-repositories +func (s *SearchService) Repositories(query string, opt *SearchOptions) (*RepositoriesSearchResult, *Response, error) { + result := new(RepositoriesSearchResult) + resp, err := s.search("repositories", query, opt, result) + return result, resp, err +} + +// IssuesSearchResult represents the result of an issues search. +type IssuesSearchResult struct { + Total int `json:"total_count,omitempty"` + Issues []Issue `json:"items,omitempty"` +} + +// Issues searches issues via various criteria. +// +// GitHub API docs: http://developer.github.com/v3/search/#search-issues +func (s *SearchService) Issues(query string, opt *SearchOptions) (*IssuesSearchResult, *Response, error) { + result := new(IssuesSearchResult) + resp, err := s.search("issues", query, opt, result) + return result, resp, err +} + +// UsersSearchResult represents the result of an issues search. +type UsersSearchResult struct { + Total int `json:"total_count,omitempty"` + Users []User `json:"items,omitempty"` +} + +// Users searches users via various criteria. +// +// GitHub API docs: http://developer.github.com/v3/search/#search-users +func (s *SearchService) Users(query string, opt *SearchOptions) (*UsersSearchResult, *Response, error) { + result := new(UsersSearchResult) + resp, err := s.search("users", query, opt, result) + return result, resp, err +} + +// CodeSearchResult represents the result of an code search. +type CodeSearchResult struct { + Total int `json:"total_count,omitempty"` + CodeResults []CodeResult `json:"items,omitempty"` +} + +// CodeResult represents a single search result. +type CodeResult struct { + Name string `json:"name,omitempty"` + Path string `json:"path,omitempty"` + SHA string `json:"sha,omitempty"` + HTMLURL string `json:"html_url,omitempty"` + Repository *Repository `json:"repository,omitempty"` +} + +// Code searches code via various criteria. +// +// GitHub API docs: http://developer.github.com/v3/search/#search-code +func (s *SearchService) Code(query string, opt *SearchOptions) (*CodeSearchResult, *Response, error) { + result := new(CodeSearchResult) + resp, err := s.search("code", query, opt, result) + return result, resp, err +} + +// Helper function that executes search queries against different +// GitHub search types (repositories, code, issues, users) +func (s *SearchService) search(searchType string, query string, opt *SearchOptions, result interface{}) (*Response, error) { + params := url.Values{"q": []string{query}} + if opt != nil { + params.Add("sort", opt.Sort) + params.Add("order", opt.Order) + params.Add("page", strconv.Itoa(opt.Page)) + params.Add("per_page", strconv.Itoa(opt.PerPage)) + } + u := fmt.Sprintf("search/%s?%s", searchType, params.Encode()) + + req, err := s.client.NewRequest("GET", u, nil) + if err != nil { + return nil, err + } + req.Header.Add("Accept", mimePreview) + + resp, err := s.client.Do(req, result) + return resp, err +} diff --git a/github/search_test.go b/github/search_test.go new file mode 100644 index 0000000..42dce67 --- /dev/null +++ b/github/search_test.go @@ -0,0 +1,141 @@ +package github + +import ( + "fmt" + "net/http" + "reflect" + + "testing" +) + +func TestSearchService_Repositories(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/search/repositories", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + testHeader(t, r, "Accept", mimePreview) + testFormValues(t, r, values{ + "q": "blah", + "sort": "forks", + "order": "desc", + "page": "2", + "per_page": "2", + }) + + fmt.Fprint(w, `{"total_count": 4, "items": [{"id":1},{"id":2}]}`) + }) + + opts := &SearchOptions{Sort: "forks", Order: "desc", Page: 2, PerPage: 2} + result, _, err := client.Search.Repositories("blah", opts) + if err != nil { + t.Errorf("Search.Repositories returned error: %v", err) + } + + want := &RepositoriesSearchResult{ + Total: 4, + Repositories: []Repository{{ID: 1}, {ID: 2}}, + } + if !reflect.DeepEqual(result, want) { + t.Errorf("Search.Repositories returned %+v, want %+v", result, want) + } +} + +func TestSearchService_Issues(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/search/issues", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + testHeader(t, r, "Accept", mimePreview) + testFormValues(t, r, values{ + "q": "blah", + "sort": "forks", + "order": "desc", + "page": "2", + "per_page": "2", + }) + + fmt.Fprint(w, `{"total_count": 4, "items": [{"number":1},{"number":2}]}`) + }) + + opts := &SearchOptions{Sort: "forks", Order: "desc", Page: 2, PerPage: 2} + result, _, err := client.Search.Issues("blah", opts) + if err != nil { + t.Errorf("Search.Issues returned error: %v", err) + } + + want := &IssuesSearchResult{ + Total: 4, + Issues: []Issue{{Number: 1}, {Number: 2}}, + } + if !reflect.DeepEqual(result, want) { + t.Errorf("Search.Issues returned %+v, want %+v", result, want) + } +} + +func TestSearchService_Users(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/search/users", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + testHeader(t, r, "Accept", mimePreview) + testFormValues(t, r, values{ + "q": "blah", + "sort": "forks", + "order": "desc", + "page": "2", + "per_page": "2", + }) + + fmt.Fprint(w, `{"total_count": 4, "items": [{"id":1},{"id":2}]}`) + }) + + opts := &SearchOptions{Sort: "forks", Order: "desc", Page: 2, PerPage: 2} + result, _, err := client.Search.Users("blah", opts) + if err != nil { + t.Errorf("Search.Issues returned error: %v", err) + } + + want := &UsersSearchResult{ + Total: 4, + Users: []User{{ID: 1}, {ID: 2}}, + } + if !reflect.DeepEqual(result, want) { + t.Errorf("Search.Users returned %+v, want %+v", result, want) + } +} + +func TestSearchService_Code(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/search/code", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + testHeader(t, r, "Accept", mimePreview) + testFormValues(t, r, values{ + "q": "blah", + "sort": "forks", + "order": "desc", + "page": "2", + "per_page": "2", + }) + + fmt.Fprint(w, `{"total_count": 4, "items": [{"name":"1"},{"name":"2"}]}`) + }) + + opts := &SearchOptions{Sort: "forks", Order: "desc", Page: 2, PerPage: 2} + result, _, err := client.Search.Code("blah", opts) + if err != nil { + t.Errorf("Search.Code returned error: %v", err) + } + + want := &CodeSearchResult{ + Total: 4, + CodeResults: []CodeResult{{Name: "1"}, {Name: "2"}}, + } + if !reflect.DeepEqual(result, want) { + t.Errorf("Search.Code returned %+v, want %+v", result, want) + } +}