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 }