Created
July 16, 2019 17:25
-
-
Save onwsk8r/a70a105014e437068b927c11be75c1e4 to your computer and use it in GitHub Desktop.
Example API Client implementation
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/*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) | |
}) | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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