Skip to content

Instantly share code, notes, and snippets.

@onwsk8r
Created July 16, 2019 17:25
Show Gist options
  • Save onwsk8r/a70a105014e437068b927c11be75c1e4 to your computer and use it in GitHub Desktop.
Save onwsk8r/a70a105014e437068b927c11be75c1e4 to your computer and use it in GitHub Desktop.
Example API Client implementation
/*package apiclient provides an API client*/
package apiclient
import (
"bytes"
"context"
"fmt"
"io"
"net/http"
"net/url"
"time"
)
// These constants correlate to domains the API uses
const (
APIDomainBase string = "https://api.example.com"
)
// These constants contain endpoints for the API
const (
APIVersion1 string = "v1"
)
// Client provides low-level access to the API.
type Client struct {
baseURL *url.URL
ticker <-chan time.Time
}
// NewClient creates a new client
func NewClient(domain, apiPath, token string) (*Client, error) {
u, err := url.Parse(fmt.Sprintf("%s/%s", domain, apiPath))
if err != nil {
return nil, err
}
q := u.Query()
q.Set("token", token)
u.RawQuery = q.Encode()
return &Client{
baseURL: u,
ticker: time.Tick(10 * time.Millisecond),
}, nil
}
// The methods below are based off of https://blog.golang.org/context
// These are slightly more opinionated because of the use-case while still
// being extensible. This code has been modified to remove domain-specific
// things, so it may not be valid.
// request makes the request with the context, passing the response to handler.
func (c *Client) request(ctx context.Context, req *http.Request, handler func(*http.Response) error) error {
ch := make(chan error, 1)
req = req.WithContext(ctx)
go func() {
// Wait for the ticker to tick so we don't go over API rate limits
// Listen for ctx.Done() as well, just in case.
select {
case <-ctx.Done():
return
case <-c.ticker:
}
if resp, err := http.DefaultClient.Do(req); err != nil {
ch <- err
} else {
ch <- handler(resp)
}
}()
select {
case <-ctx.Done():
<-ch // Wait for f to return.
return ctx.Err()
case err := <-ch:
return err
}
}
// Get streams the response from path to handler.
// Path is expected to be a relative path with fragments or QSP if necessary. The
// context is used with the request, and the handler function will receive an io.Reader
// with the contents of the request body.
func (c *Client) Get(ctx context.Context, path *url.URL, handler func(io.Reader) error) error {
var b *bytes.Buffer
req, _ := http.NewRequest("GET", c.baseURL.ResolveReference(path).String(), b)
return c.request(ctx, req, func(resp *http.Response) error {
if resp.StatusCode != 200 {
return fmt.Errorf("Non-200 Status Code %d Received: %s", resp.StatusCode, resp.Status)
}
return handler(b)
})
}
package apiclient
import (
"context"
"encoding/json"
"fmt"
"io"
"net/url"
"github.com/myapp/apiface"
)
// SomeEndpoint fulfills apiface.SomeEndpoint
type SomeEndpoint struct {
*Client
}
// NewSomeEndpoint returns a SomeEndpoint with the given Client.
func NewSomeEndpoint(c *Client) *SomeEndpoint {
return &SomeEndpoint{
Client: c,
}
}
// GetSomeInformation fulfills apiface.SomeEndpoint
func (s *SomeEndpoint) GetSomeInformation(ctx context.Context, some string,
params apiface.SomeInformationParams) ([]apiface.SomeInformation, error) {
// Figure out the correct URL
u, err := url.Parse(fmt.Sprintf("/the/%s/endpoint", some))
if err != nil {
return nil, err
}
// Parameterize adds the params to the query string
if err = params.Parameterize(u); err != nil {
return nil, err
}
// Make the request and parse the result
var data []apiface.SomeInformation
err = s.Get(ctx, u, func(r io.Reader) error {
if params.Range == apiface.TimeframeDynamic {
res := struct {
Range string `json:"range"`
Data []apiface.SomeInformation `json:"data"`
}{Data: data}
return json.NewDecoder(r).Decode(&res)
}
return json.NewDecoder(r).Decode(&data)
})
return data, err
}
func main() {
// Get the client, which will likely only ever have one instance
client, err := apiclient.NewClient(apiclient.APIDomainBase, apiclient.APIVersion1, "mySecr3tToekn")
if err != nil {
log.Fatal(err)
}
// Get the endpoint, which is also thread-safe
theEndpoint := apiclient.NewSomeEndpoint(client)
// Assemble the things we need to make a request
ctx := context.Background()
thing := "baz"
theParams := apiface.SomeInformationParams{
Thing: "foo",
Bar: true,
}
// Make the request
res, err := theEndpoint.GetSomeInformation(ctx, thing, theParams)
if err != nil {
log.Fatal(err)
}
// do stuff with res...
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment