├── .travis.yml ├── README.md ├── _fixtures ├── 8863.json ├── 8952.json ├── maxitem.json ├── peterhellberg.json ├── topstories.json └── updates.json ├── client.go ├── client_test.go ├── items.go ├── items_test.go ├── live.go ├── live_test.go ├── users.go └── users_test.go /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.3.3 5 | 6 | install: 7 | - go get github.com/stretchr/testify/assert 8 | 9 | branches: 10 | only: 11 | - master 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hn 2 | 3 | Go library for the [Hacker News API](https://github.com/HackerNews/API) 4 | 5 | [![GoDoc](https://godoc.org/github.com/peterhellberg/hn?status.svg)](https://godoc.org/github.com/peterhellberg/hn) 6 | [![Build Status](https://travis-ci.org/peterhellberg/hn.svg?branch=master)](https://travis-ci.org/peterhellberg/hn) 7 | [![License MIT](https://img.shields.io/badge/license-MIT-lightgrey.svg?style=flat)](https://github.com/peterhellberg/hn#license-mit) 8 | 9 | ## Installation 10 | 11 | ```bash 12 | go get -u github.com/peterhellberg/hn 13 | ``` 14 | 15 | ## Services 16 | 17 | The client currently delegates to implementations of three interfaces: 18 | [ItemsService](https://godoc.org/github.com/peterhellberg/hn#ItemsService), 19 | [LiveService](https://godoc.org/github.com/peterhellberg/hn#LiveService) and 20 | [UsersService](https://godoc.org/github.com/peterhellberg/hn#UsersService). 21 | 22 | ## Example usage 23 | 24 | Showing the current top ten stories 25 | 26 | ```go 27 | package main 28 | 29 | import ( 30 | "fmt" 31 | 32 | "github.com/peterhellberg/hn" 33 | ) 34 | 35 | func main() { 36 | hn := hn.DefaultClient 37 | 38 | ids, err := hn.TopStories() 39 | if err != nil { 40 | panic(err) 41 | } 42 | 43 | for i, id := range ids[:10] { 44 | item, err := hn.Item(id) 45 | if err != nil { 46 | panic(err) 47 | } 48 | 49 | fmt.Println(i, "–", item.Title, "\n ", item.URL, "\n") 50 | } 51 | } 52 | ``` 53 | 54 | Showing the current top ten stories using goroutines, a channel and a wait group 55 | 56 | ```go 57 | package main 58 | 59 | import ( 60 | "fmt" 61 | "net/http" 62 | "sync" 63 | "time" 64 | 65 | "github.com/peterhellberg/hn" 66 | ) 67 | 68 | type indexItem struct { 69 | Index int 70 | Item *hn.Item 71 | } 72 | 73 | var ( 74 | items = map[int]*hn.Item{} 75 | messages = make(chan indexItem) 76 | ) 77 | 78 | func main() { 79 | hn := hn.NewClient(&http.Client{ 80 | Timeout: time.Duration(5 * time.Second), 81 | }) 82 | 83 | ids, err := hn.TopStories() 84 | if err != nil { 85 | panic(err) 86 | } 87 | 88 | go func() { 89 | for i := range messages { 90 | items[i.Index] = i.Item 91 | } 92 | }() 93 | 94 | var wg sync.WaitGroup 95 | 96 | for i, id := range ids[:10] { 97 | wg.Add(1) 98 | go func(i, id int) { 99 | defer wg.Done() 100 | 101 | item, err := hn.Item(id) 102 | if err != nil { 103 | panic(err) 104 | } 105 | 106 | messages <- indexItem{i, item} 107 | }(i, id) 108 | } 109 | 110 | wg.Wait() 111 | 112 | for i := 0; i < 10; i++ { 113 | fmt.Println(i, "–", items[i].Title, "\n ", items[i].URL, "\n") 114 | } 115 | } 116 | ``` 117 | 118 | Showing information about a given user (first argument) 119 | 120 | ```go 121 | package main 122 | 123 | import ( 124 | "fmt" 125 | "os" 126 | 127 | "github.com/peterhellberg/hn" 128 | ) 129 | 130 | func main() { 131 | if len(os.Args) < 2 { 132 | return 133 | } 134 | 135 | if u, err := hn.DefaultClient.User(os.Args[1]); err == nil { 136 | fmt.Println("ID: ", u.ID) 137 | fmt.Println("About:", u.About) 138 | fmt.Println("Karma:", u.Karma) 139 | } 140 | } 141 | ``` 142 | 143 | ## License (MIT) 144 | 145 | Copyright (c) 2014-2015 [Peter Hellberg](http://c7.se/) 146 | 147 | > Permission is hereby granted, free of charge, to any person obtaining 148 | > a copy of this software and associated documentation files (the 149 | > "Software"), to deal in the Software without restriction, including 150 | > without limitation the rights to use, copy, modify, merge, publish, 151 | > distribute, sublicense, and/or sell copies of the Software, and to 152 | > permit persons to whom the Software is furnished to do so, subject to 153 | > the following conditions: 154 | 155 | > The above copyright notice and this permission notice shall be 156 | > included in all copies or substantial portions of the Software. 157 | 158 | > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 159 | > EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 160 | > MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 161 | > NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 162 | > LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 163 | > OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 164 | > WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 165 | -------------------------------------------------------------------------------- /_fixtures/8863.json: -------------------------------------------------------------------------------- 1 | { 2 | "by" : "dhouston", 3 | "id" : 8863, 4 | "kids" : [ 8952, 9224, 8917, 8884, 8887, 8943, 8869, 8958, 9005, 9671, 8940, 9067, 8908, 9055, 8865, 8881, 8872, 8873, 8955, 10403, 8903, 8928, 9125, 8998, 8901, 8902, 8907, 8894, 8878, 8870, 8980, 8934, 8876 ], 5 | "score" : 111, 6 | "time" : 1175714200, 7 | "title" : "My YC app: Dropbox - Throw away your USB drive", 8 | "type" : "story", 9 | "url" : "http://www.getdropbox.com/u/2/screencast.html" 10 | } 11 | -------------------------------------------------------------------------------- /_fixtures/8952.json: -------------------------------------------------------------------------------- 1 | { 2 | "by" : "nickb", 3 | "id" : 8952, 4 | "kids" : [ 9153 ], 5 | "parent" : 8863, 6 | "text" : "The only problem is that you have to install something. See, it's not the same as USB drive. Most corporate laptops are locked and you can't install anything on them. That's gonna be the problem. Also, another point where your USB comparison fails is that USB works in places where you don't have internet access.

My suggestion is to drop the \"Throw away your USB drive\" tag line and use something else... it will just muddy your vision.

Kudos for launching it!!! Launching/shipping is extremely hard and you pulled it off! Super!", 7 | "time" : 1175727286, 8 | "type" : "comment" 9 | } 10 | -------------------------------------------------------------------------------- /_fixtures/maxitem.json: -------------------------------------------------------------------------------- 1 | 8424452 -------------------------------------------------------------------------------- /_fixtures/peterhellberg.json: -------------------------------------------------------------------------------- 1 | { 2 | "about" : "Ruby developer, Mac convert, Linux aficionado and Boulder climber.", 3 | "created" : 1300226645, 4 | "delay" : 0, 5 | "id" : "peterhellberg", 6 | "karma" : 2, 7 | "submitted" : [ 4333104, 4205185, 3802708 ] 8 | } 9 | -------------------------------------------------------------------------------- /_fixtures/topstories.json: -------------------------------------------------------------------------------- 1 | [ 8422599, 8422087, 8423825, 8422928, 8422581, 8422051, 8422408, 8423936, 8422695, 8422546, 8420274, 8424403, 8423083, 8424169, 8420902, 8424150, 8424182, 8424245, 8421656, 8421594, 8423486, 8424418, 8421900, 8423474, 8423007, 8424391, 8423966, 8421866, 8421518, 8424278, 8422407, 8421816, 8420309, 8424407, 8421607, 8421623, 8421937, 8423797, 8421263, 8421779, 8422864, 8424337, 8419658, 8423782, 8421862, 8422646, 8424161, 8416393, 8422094, 8419222, 8422156, 8421466, 8423091, 8421013, 8424203, 8417062, 8421220, 8421238, 8419702, 8416693, 8423946, 8422409, 8421147, 8421375, 8421562, 8421445, 8421932, 8418836, 8421790, 8422964, 8422965, 8419628, 8423895, 8421684, 8424165, 8415912, 8415647, 8420417, 8421807, 8421088, 8418865, 8417343, 8419210, 8416455, 8419807, 8419984, 8418677, 8424264, 8420918, 8420013, 8423386, 8418588, 8419801, 8419734, 8419803, 8424040, 8422960, 8423132, 8423354, 8411638 ] 2 | -------------------------------------------------------------------------------- /_fixtures/updates.json: -------------------------------------------------------------------------------- 1 | { 2 | "items" : [ 8424464, 8424430, 8424352, 8422930, 8424124, 8424299, 8423988, 8423650, 8424118, 8422929, 8424345, 8424212, 8422196, 8423316, 8424442, 8424463, 8422762, 8424054, 8423825, 8422928, 8422581, 8421518, 8422051, 8419658, 8424403, 8424333, 8424462, 8424165, 8424391 ], 3 | "profiles" : [ "primigenus", "dragonwriter", "saadshamim", "gtani", "ahomescu1", "benologist", "Mandatum", "tshtf", "imanaccount247", "Fuzzwah", "mwcampbell", "lesterbuck", "GauntletWizard", "zokier", "dmmalam", "mikiem", "oori", "canguler", "picanpican", "jloughry", "Pwntastic", "codezero", "asadlionpk", "martey", "ScottBurson", "projct", "dtlyst", "frankdenbow", "saneefansari", "kevingadd", "kazinator", "spectruman", "tikhonj" ] 4 | } 5 | -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | package hn 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | "net/url" 8 | ) 9 | 10 | // DefaultClient is the default Hacker News API client 11 | var DefaultClient = NewClient() 12 | 13 | const ( 14 | libraryVersion = "0.0.1" 15 | userAgent = "hn.go/" + libraryVersion 16 | ) 17 | 18 | // A Client communicates with the Hacker News API. 19 | type Client struct { 20 | Items ItemsService 21 | Users UsersService 22 | Live LiveService 23 | 24 | // BaseURL is the base url for Hacker News API. 25 | BaseURL *url.URL 26 | 27 | // User agent used for HTTP requests to Hacker News API. 28 | UserAgent string 29 | 30 | // HTTP client used to communicate with the Hacker News API. 31 | httpClient *http.Client 32 | } 33 | 34 | // NewClient returns a new Hacker News API client. 35 | // If no *http.Client were provided then http.DefaultClient is used. 36 | func NewClient(httpClients ...*http.Client) *Client { 37 | var httpClient *http.Client 38 | 39 | if len(httpClients) > 0 && httpClients[0] != nil { 40 | httpClient = httpClients[0] 41 | } else { 42 | cloned := *http.DefaultClient 43 | httpClient = &cloned 44 | } 45 | 46 | c := &Client{ 47 | BaseURL: &url.URL{ 48 | Scheme: "https", 49 | Host: "hacker-news.firebaseio.com", 50 | Path: "/v0/", 51 | }, 52 | UserAgent: userAgent, 53 | httpClient: httpClient, 54 | } 55 | 56 | c.Items = &itemsService{c} 57 | c.Users = &usersService{c} 58 | c.Live = &liveService{c} 59 | 60 | return c 61 | } 62 | 63 | // NewRequest creates an API request. 64 | func (c *Client) NewRequest(s string) (*http.Request, error) { 65 | rel, err := url.Parse(s) 66 | if err != nil { 67 | return nil, err 68 | } 69 | 70 | u := c.BaseURL.ResolveReference(rel) 71 | 72 | req, err := http.NewRequest("GET", u.String(), nil) 73 | if err != nil { 74 | return nil, err 75 | } 76 | 77 | req.Header.Add("User-Agent", c.UserAgent) 78 | return req, nil 79 | } 80 | 81 | // Do sends an API request and returns the API response. The API response is 82 | // decoded and stored in the value pointed to by v, or returned as an error if 83 | // an API error has occurred. 84 | func (c *Client) Do(req *http.Request, v interface{}) (*http.Response, error) { 85 | // Make sure to close the connection after replying to this request 86 | req.Close = true 87 | 88 | resp, err := c.httpClient.Do(req) 89 | if err != nil { 90 | return nil, err 91 | } 92 | 93 | defer resp.Body.Close() 94 | 95 | if v != nil { 96 | err = json.NewDecoder(resp.Body).Decode(v) 97 | } 98 | 99 | if err != nil { 100 | return nil, fmt.Errorf("error reading response from %s %s: %s", req.Method, req.URL.RequestURI(), err) 101 | } 102 | 103 | return resp, nil 104 | } 105 | -------------------------------------------------------------------------------- /client_test.go: -------------------------------------------------------------------------------- 1 | package hn 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "net/http" 7 | "net/http/httptest" 8 | "net/url" 9 | "testing" 10 | ) 11 | 12 | func TestNewClient(t *testing.T) { 13 | c := NewClient() 14 | 15 | if got, want := c.BaseURL.String(), "https://hacker-news.firebaseio.com/v0/"; got != want { 16 | t.Fatalf(`c.BaseURL.String() = %q, want %q`, got, want) 17 | } 18 | 19 | if got, want := c.UserAgent, "hn.go/0.0.1"; got != want { 20 | t.Fatalf(`c.UserAgent = %q, want %q`, got, want) 21 | } 22 | } 23 | 24 | func TestNewRequest(t *testing.T) { 25 | r, err := NewClient(nil).NewRequest(fmt.Sprintf("foo?bar=%v", 123)) 26 | 27 | if err != nil { 28 | t.Fatalf(`err != nil, got %v`, err) 29 | } 30 | 31 | if got, want := r.URL.String(), "https://hacker-news.firebaseio.com/v0/foo?bar=123"; got != want { 32 | t.Fatalf(`r.URL.String() = %q, want %q`, got, want) 33 | } 34 | } 35 | 36 | func TestNewRequest_invalidPath(t *testing.T) { 37 | if _, err := NewClient(nil).NewRequest("%"); err == nil { 38 | t.Fatalf("expected to get error for invalid path") 39 | } 40 | } 41 | 42 | func testServerAndClient(body []byte) (*httptest.Server, *Client) { 43 | ts := testServer(body) 44 | 45 | c := DefaultClient 46 | c.BaseURL, _ = url.Parse(ts.URL) 47 | 48 | return ts, c 49 | } 50 | 51 | func testServerAndClientByFixture(fn string) (*httptest.Server, *Client) { 52 | body, _ := ioutil.ReadFile("_fixtures/" + fn + ".json") 53 | 54 | return testServerAndClient(body) 55 | } 56 | 57 | func testServer(body []byte) *httptest.Server { 58 | return httptest.NewServer(http.HandlerFunc( 59 | func(w http.ResponseWriter, r *http.Request) { 60 | w.Header().Set("Content-Type", "application/json") 61 | w.Write(body) 62 | })) 63 | } 64 | -------------------------------------------------------------------------------- /items.go: -------------------------------------------------------------------------------- 1 | package hn 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | // ItemsService communicates with the news 9 | // related endpoints in the Hacker News API 10 | type ItemsService interface { 11 | Get(id int) (*Item, error) 12 | } 13 | 14 | // itemsService implements ItemsService. 15 | type itemsService struct { 16 | client *Client 17 | } 18 | 19 | // Item represents a item 20 | type Item struct { 21 | ID int `json:"id"` 22 | Parent int `json:"parent"` 23 | Kids []int `json:"kids"` 24 | Descendants int `json:"descendants"` 25 | Parts []int `json:"parts"` 26 | Score int `json:"score"` 27 | Timestamp int `json:"time"` 28 | By string `json:"by"` 29 | Type string `json:"type"` 30 | Title string `json:"title"` 31 | Text string `json:"text"` 32 | URL string `json:"url"` 33 | Dead bool `json:"dead"` 34 | Deleted bool `json:"deleted"` 35 | } 36 | 37 | // Time return the time of the timestamp 38 | func (i *Item) Time() time.Time { 39 | return time.Unix(int64(i.Timestamp), 0) 40 | } 41 | 42 | // Item is a convenience method proxying Items.Get 43 | func (c *Client) Item(id int) (*Item, error) { 44 | return c.Items.Get(id) 45 | } 46 | 47 | // Get retrieves an item with the given id 48 | func (s *itemsService) Get(id int) (*Item, error) { 49 | req, err := s.client.NewRequest(s.getPath(id)) 50 | if err != nil { 51 | return nil, err 52 | } 53 | 54 | var item Item 55 | _, err = s.client.Do(req, &item) 56 | if err != nil { 57 | return nil, err 58 | } 59 | 60 | if item.Type == "story" && item.URL == "" { 61 | item.URL = fmt.Sprintf("https://news.ycombinator.com/item?id=%v", id) 62 | } 63 | 64 | return &item, nil 65 | } 66 | 67 | func (s *itemsService) getPath(id int) string { 68 | return fmt.Sprintf("item/%v.json", id) 69 | } 70 | -------------------------------------------------------------------------------- /items_test.go: -------------------------------------------------------------------------------- 1 | package hn 2 | 3 | import "testing" 4 | 5 | func TestItem_8863(t *testing.T) { 6 | ts, c := testServerAndClientByFixture("8863") 7 | defer ts.Close() 8 | 9 | item, err := c.Item(8863) 10 | 11 | if err != nil { 12 | t.Fatalf(`err != nil, got %v`, err) 13 | } 14 | 15 | if got, want := item.By, "dhouston"; got != want { 16 | t.Fatalf(`item.By = %q, want %q`, got, want) 17 | } 18 | 19 | if got, want := item.Score, 111; got != want { 20 | t.Fatalf(`item.Score = %d, want %d`, got, want) 21 | } 22 | 23 | if got, want := item.URL, "http://www.getdropbox.com/u/2/screencast.html"; got != want { 24 | t.Fatalf(`item.URL = %q, want %q`, got, want) 25 | } 26 | } 27 | 28 | func TestItem_8952(t *testing.T) { 29 | ts, c := testServerAndClientByFixture("8952") 30 | defer ts.Close() 31 | 32 | item, err := c.Item(8952) 33 | 34 | if err != nil { 35 | t.Fatalf(`err != nil, got %v`, err) 36 | } 37 | 38 | if got, want := item.By, "nickb"; got != want { 39 | t.Fatalf(`item.By = %q, want %q`, got, want) 40 | } 41 | 42 | if got, want := item.Timestamp, 1175727286; got != want { 43 | t.Fatalf(`item.Timestamp = %d, want %d`, got, want) 44 | } 45 | } 46 | 47 | func TestItemTime(t *testing.T) { 48 | item := Item{Timestamp: 1175727286} 49 | 50 | if got, want := item.Time().UTC().Format("2006-01-02"), "2007-04-04"; got != want { 51 | t.Fatalf(`item.Time().UTC().Format("2006-01-02") = %q, want %q`, got, want) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /live.go: -------------------------------------------------------------------------------- 1 | package hn 2 | 3 | // LiveService communicates with the news 4 | // related endpoints in the Hacker News API 5 | type LiveService interface { 6 | TopStories() ([]int, error) 7 | MaxItem() (int, error) 8 | Updates() (*Updates, error) 9 | } 10 | 11 | // liveService implements LiveService. 12 | type liveService struct { 13 | client *Client 14 | } 15 | 16 | // Updates contains the latest updated items and profiles 17 | type Updates struct { 18 | Items []int `json:"items"` 19 | Profiles []string `json:"profiles"` 20 | } 21 | 22 | // TopStories is a convenience method proxying Live.TopStories 23 | func (c *Client) TopStories() ([]int, error) { 24 | return c.Live.TopStories() 25 | } 26 | 27 | // TopStories retrieves the current top stories 28 | func (s *liveService) TopStories() ([]int, error) { 29 | req, err := s.client.NewRequest(s.topStoriesPath()) 30 | if err != nil { 31 | return nil, err 32 | } 33 | 34 | var value []int 35 | _, err = s.client.Do(req, &value) 36 | if err != nil { 37 | return nil, err 38 | } 39 | 40 | return value, nil 41 | } 42 | 43 | func (s *liveService) topStoriesPath() string { 44 | return "topstories.json" 45 | } 46 | 47 | // MaxItem is a convenience method proxying Live.MaxItem 48 | func (c *Client) MaxItem() (int, error) { 49 | return c.Live.MaxItem() 50 | } 51 | 52 | // MaxItem retrieves the current largest item id 53 | func (s *liveService) MaxItem() (int, error) { 54 | req, err := s.client.NewRequest(s.maxItemPath()) 55 | if err != nil { 56 | return 0, err 57 | } 58 | 59 | var value int 60 | _, err = s.client.Do(req, &value) 61 | if err != nil { 62 | return 0, err 63 | } 64 | 65 | return value, nil 66 | } 67 | 68 | func (s *liveService) maxItemPath() string { 69 | return "maxitem.json" 70 | } 71 | 72 | // Updates is a convenience method proxying Live.Updates 73 | func (c *Client) Updates() (*Updates, error) { 74 | return c.Live.Updates() 75 | } 76 | 77 | // Updates retrieves the current largest item id 78 | func (s *liveService) Updates() (*Updates, error) { 79 | req, err := s.client.NewRequest(s.updatesPath()) 80 | if err != nil { 81 | return nil, err 82 | } 83 | 84 | var value Updates 85 | _, err = s.client.Do(req, &value) 86 | if err != nil { 87 | return nil, err 88 | } 89 | 90 | return &value, nil 91 | } 92 | 93 | func (s *liveService) updatesPath() string { 94 | return "updates.json" 95 | } 96 | -------------------------------------------------------------------------------- /live_test.go: -------------------------------------------------------------------------------- 1 | package hn 2 | 3 | import "testing" 4 | 5 | func TestTopStories(t *testing.T) { 6 | ts, c := testServerAndClientByFixture("topstories") 7 | defer ts.Close() 8 | 9 | top, err := c.TopStories() 10 | 11 | if err != nil { 12 | t.Fatalf(`err != nil, got %v`, err) 13 | } 14 | 15 | if got, want := len(top), 100; got != want { 16 | t.Fatalf(`len(top) = %d, want %d`, got, want) 17 | } 18 | } 19 | 20 | func TestMaxItem(t *testing.T) { 21 | ts, c := testServerAndClientByFixture("maxitem") 22 | defer ts.Close() 23 | 24 | item, err := c.MaxItem() 25 | 26 | if err != nil { 27 | t.Fatalf(`err != nil, got %v`, err) 28 | } 29 | 30 | if got, want := item, 8424452; got != want { 31 | t.Fatalf(`item = %d, want %d`, got, want) 32 | } 33 | } 34 | 35 | func TestUpdates(t *testing.T) { 36 | ts, c := testServerAndClientByFixture("updates") 37 | defer ts.Close() 38 | 39 | updates, err := c.Updates() 40 | 41 | if err != nil { 42 | t.Fatalf(`err != nil, got %v`, err) 43 | } 44 | 45 | if got, want := updates.Profiles[5], "benologist"; got != want { 46 | t.Fatalf(`updates.Profiles[5] = %q, want %q`, got, want) 47 | } 48 | 49 | if got, want := updates.Items[7], 8423650; got != want { 50 | t.Fatalf(`updates.Items[7] = %d, want %d`, got, want) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /users.go: -------------------------------------------------------------------------------- 1 | package hn 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | var errMissingID = fmt.Errorf("missing id") 9 | 10 | // UsersService communicates with the news 11 | // related endpoints in the Hacker News API 12 | type UsersService interface { 13 | Get(id string) (*User, error) 14 | } 15 | 16 | // usersService implements LiveService. 17 | type usersService struct { 18 | client *Client 19 | } 20 | 21 | // User represents a Hacker News user 22 | type User struct { 23 | About string `json:"about"` 24 | Created int `json:"created"` 25 | Delay int `json:"delay"` 26 | ID string `json:"id"` 27 | Karma int `json:"karma"` 28 | Submitted []int `json:"submitted"` 29 | } 30 | 31 | // CreatedTime return the time of the created 32 | func (u *User) CreatedTime() time.Time { 33 | return time.Unix(int64(u.Created), 0) 34 | } 35 | 36 | // User is a convenience method proxying Users.Get 37 | func (c *Client) User(id string) (*User, error) { 38 | return c.Users.Get(id) 39 | } 40 | 41 | // Get retrieves a user with the given id 42 | func (s *usersService) Get(id string) (*User, error) { 43 | if id == "" { 44 | return nil, errMissingID 45 | } 46 | 47 | req, err := s.client.NewRequest(s.getPath(id)) 48 | if err != nil { 49 | return nil, err 50 | } 51 | 52 | var user User 53 | _, err = s.client.Do(req, &user) 54 | if err != nil { 55 | return nil, err 56 | } 57 | 58 | return &user, nil 59 | } 60 | 61 | func (s *usersService) getPath(id string) string { 62 | return fmt.Sprintf("user/%v.json", id) 63 | } 64 | -------------------------------------------------------------------------------- /users_test.go: -------------------------------------------------------------------------------- 1 | package hn 2 | 3 | import "testing" 4 | 5 | func TestUser(t *testing.T) { 6 | ts, c := testServerAndClientByFixture("peterhellberg") 7 | defer ts.Close() 8 | 9 | user, err := c.User("peterhellberg") 10 | 11 | if err != nil { 12 | t.Fatalf(`err != nil, got %v`, err) 13 | } 14 | 15 | if got, want := user.Created, 1300226645; got != want { 16 | t.Fatalf(`user.Created = %d, want %d`, got, want) 17 | } 18 | 19 | if got, want := user.CreatedTime().UTC().Format("2006-01-02"), "2011-03-15"; got != want { 20 | t.Fatalf(`user.CreatedTime().UTC().Format("2006-01-02") = %q, want %q`, got, want) 21 | } 22 | } 23 | 24 | func TestMissingUser(t *testing.T) { 25 | ts, c := testServerAndClient([]byte(`{}`)) 26 | defer ts.Close() 27 | 28 | if _, err := c.User(""); err != errMissingID { 29 | t.Fatalf(`err != errMissingID, got %v`, err) 30 | } 31 | } 32 | --------------------------------------------------------------------------------