From 3c7aee142d9810542189cb35f09f3b51dd7e62f3 Mon Sep 17 00:00:00 2001 From: Vartan Benohanian Date: Thu, 23 Apr 2020 22:57:47 -0400 Subject: [PATCH] Get subreddit by name Signed-off-by: Vartan Benohanian --- README.md | 14 ++++ geddit.go | 200 +++++++++++++++++++++++++++++++++++++++++++++++++ geddit_opts.go | 14 ++++ subreddit.go | 64 ++++++++++++++++ 4 files changed, 292 insertions(+) create mode 100644 geddit.go create mode 100644 geddit_opts.go create mode 100644 subreddit.go diff --git a/README.md b/README.md index 0871a94..5a3dc96 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,17 @@ # Geddit Geddit is a Go client library for accessing the Reddit API. + +## Install + +To get a specific version from the list of [versions](https://github.com/vartanbeno/geddit/releases): + +```sh +go get github.com/vartanbeno/geddit@vX.Y.Z +``` + +Or for the latest version: + +```sh +go get github.com/vartanbeno/geddit +``` diff --git a/geddit.go b/geddit.go new file mode 100644 index 0000000..e42efdb --- /dev/null +++ b/geddit.go @@ -0,0 +1,200 @@ +package geddit + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "net/http" + "net/url" +) + +const ( + libraryVersion = "0.0.1" + defaultBaseURL = "https://reddit.com" + defaultBaseURLOauth = "https://oauth.reddit.com" + userAgent = "geddit/" + libraryVersion + mediaType = "application/json" + + headerContentType = "Content-Type" + headerAccept = "Accept" + headerUserAgent = "User-Agent" +) + +// RequestCompletionCallback defines the type of the request callback function +type RequestCompletionCallback func(*http.Request, *http.Response) + +// Client manages communication with the Reddit API +type Client struct { + // HTTP client used to communicate with the Reddit API + client *http.Client + + // Base URL for HTTP requests + BaseURL *url.URL + + UserAgent string + + Subreddit SubredditService + + onRequestCompleted RequestCompletionCallback +} + +// OnRequestCompleted sets the client's request completion callback +func (c *Client) OnRequestCompleted(rc RequestCompletionCallback) { + c.onRequestCompleted = rc +} + +func newClient(httpClient *http.Client) *Client { + if httpClient == nil { + httpClient = &http.Client{} + } + + baseURL, _ := url.Parse(defaultBaseURL) + + c := &Client{client: httpClient, BaseURL: baseURL, UserAgent: userAgent} + c.Subreddit = &SubredditServiceOp{client: c} + + return c +} + +// New returns a client that can make requests to the Reddit API +func New(httpClient *http.Client, opts ...Opt) (c *Client, err error) { + c = newClient(httpClient) + for _, opt := range opts { + if err = opt(c); err != nil { + return + } + } + return +} + +// NewRequest creates an API request +// The path is the relative URL which will be resolves to the BaseURL of the Client +// It should always be specified without a preceding slash +func (c *Client) NewRequest(method, path string, body interface{}) (*http.Request, error) { + u, err := c.BaseURL.Parse(path) + if err != nil { + return nil, err + } + + buf := new(bytes.Buffer) + if body != nil { + err = json.NewEncoder(buf).Encode(body) + if err != nil { + return nil, err + } + } + + reqBody := bytes.NewReader(buf.Bytes()) + req, err := http.NewRequest(method, u.String(), reqBody) + if err != nil { + return nil, err + } + + req.Header.Add(headerContentType, mediaType) + req.Header.Add(headerAccept, mediaType) + req.Header.Add(headerUserAgent, c.UserAgent) + + return req, nil +} + +// Response is a PlayNetwork response. This wraps the standard http.Response returned from PlayNetwork. +type Response struct { + *http.Response +} + +// newResponse creates a new Response for the provided http.Response +func newResponse(r *http.Response) *Response { + response := Response{Response: r} + return &response +} + +// Do sends an API request and returns the API response. The API response is JSON decoded and stored in the value +// pointed to by v, or returned as an error if an API error has occurred. If v implements the io.Writer interface, +// the raw response will be written to v, without attempting to decode it. +func (c *Client) Do(ctx context.Context, req *http.Request, v interface{}) (*Response, error) { + resp, err := DoRequestWithClient(ctx, c.client, req) + if err != nil { + return nil, err + } + + if c.onRequestCompleted != nil { + c.onRequestCompleted(req, resp) + } + + response := newResponse(resp) + defer func() { + if rerr := response.Body.Close(); err == nil { + err = rerr + } + }() + + err = CheckResponse(resp) + if err != nil { + return response, err + } + + if v != nil { + if w, ok := v.(io.Writer); ok { + _, err = io.Copy(w, response.Body) + if err != nil { + return nil, err + } + } else { + err = json.NewDecoder(response.Body).Decode(v) + if err != nil { + return nil, err + } + } + } + + return response, err +} + +// DoRequest submits an HTTP request. +func DoRequest(ctx context.Context, req *http.Request) (*http.Response, error) { + return DoRequestWithClient(ctx, http.DefaultClient, req) +} + +// DoRequestWithClient submits an HTTP request using the specified client. +func DoRequestWithClient(ctx context.Context, client *http.Client, req *http.Request) (*http.Response, error) { + req = req.WithContext(ctx) + return client.Do(req) +} + +// An ErrorResponse reports the error caused by an API request +type ErrorResponse struct { + // HTTP response that caused this error + Response *http.Response + + // Error message + Message string `json:"message"` +} + +func (r *ErrorResponse) Error() string { + return fmt.Sprintf( + "%v %v: %d %v", + r.Response.Request.Method, r.Response.Request.URL, r.Response.StatusCode, r.Message, + ) +} + +// CheckResponse checks the API response for errors, and returns them if present. +// A response is considered an error if it has a status code outside the 200 range. +func CheckResponse(r *http.Response) error { + if c := r.StatusCode; c >= 200 && c <= 299 { + return nil + } + + errorResponse := &ErrorResponse{Response: r} + data, err := ioutil.ReadAll(r.Body) + if err == nil && len(data) > 0 { + err := json.Unmarshal(data, errorResponse) + if err != nil { + errorResponse.Message = string(data) + } + } + + return errorResponse +} diff --git a/geddit_opts.go b/geddit_opts.go new file mode 100644 index 0000000..71f6689 --- /dev/null +++ b/geddit_opts.go @@ -0,0 +1,14 @@ +package geddit + +import "fmt" + +// Opt is a configuration option to initialize a client +type Opt func(*Client) error + +// WithUserAgent sets the user agent for the client +func WithUserAgent(ua string) Opt { + return func(c *Client) error { + c.UserAgent = fmt.Sprintf("%s %s", ua, c.UserAgent) + return nil + } +} diff --git a/subreddit.go b/subreddit.go new file mode 100644 index 0000000..de90a3d --- /dev/null +++ b/subreddit.go @@ -0,0 +1,64 @@ +package geddit + +import ( + "context" + "errors" + "fmt" + "net/http" +) + +// SubredditService handles communication with the subreddit +// related methods of the Reddit API +type SubredditService interface { + GetByName(ctx context.Context, name string) (*Subreddit, *Response, error) +} + +// SubredditServiceOp implements the SubredditService interface +type SubredditServiceOp struct { + client *Client +} + +var _ SubredditService = &SubredditServiceOp{} + +type subredditRoot struct { + Kind *string `json:"kind,omitempty"` + Data *Subreddit `json:"data,omitempty"` +} + +// Subreddit holds information about a subreddit +type Subreddit struct { + ID *string `json:"id,omitempty"` + FullID *string `json:"name,omitempty"` + Created *float64 `json:"created_utc,omitempty"` + + URL *string `json:"url,omitempty"` + DisplayName *string `json:"display_name,omitempty"` + DisplayNamePrefixed *string `json:"display_name_prefixed,omitempty"` + Title *string `json:"title,omitempty"` + PublicDescription *string `json:"public_description,omitempty"` + + Subscribers *int `json:"subscribers,omitempty"` + ActiveUserCount *int `json:"active_user_count,omitempty"` +} + +// GetByName gets a subreddit by name +func (s *SubredditServiceOp) GetByName(ctx context.Context, name string) (*Subreddit, *Response, error) { + if name == "" { + return nil, nil, errors.New("empty subreddit name provided") + } + + path := fmt.Sprintf("r/%s/about.json", name) + + req, err := s.client.NewRequest(http.MethodGet, path, nil) + if err != nil { + return nil, nil, err + } + + root := new(subredditRoot) + resp, err := s.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + + return root.Data, resp, nil +}