Skip to content

Instantly share code, notes, and snippets.

@aliuygur
Created October 23, 2018 14:16
Show Gist options
  • Save aliuygur/804b2293c56ccce6f7d22ca6a62b10f0 to your computer and use it in GitHub Desktop.
Save aliuygur/804b2293c56ccce6f7d22ca6a62b10f0 to your computer and use it in GitHub Desktop.
etag caching example in go
/*
A client for openexchangerates.org's API
This package is a small client for openexchangerates.org's HTTP API. It
respects the HTTP etags returned by the service, and implements most of
the available methods at the moment.
*/
package openexchangerates
import (
"encoding/json"
"io/ioutil"
"net/http"
"net/url"
)
const (
Host = "openexchangerates.org"
)
var (
// You must register to use openexchangerates.org's API
AppId = ""
// If for some reason you don't want to use HTTPS, change Scheme to
// "http".
Scheme = "https"
etags = map[string]Etag{}
)
type Rates struct {
Timestamp int64 `json:"timestamp"`
Base string `json:"base"`
Rates map[string]float64 `json:"rates"`
}
type Etag struct {
Key string
Date string
Body []byte
}
type ApiError struct {
apiError bool `json:"error"`
Status int `json:"status"`
Message string `json:"message"`
Description string `json:"description"`
}
func (e ApiError) Error() string {
return e.Description
}
type ConfigError struct {
Description string
}
func (e ConfigError) Error() string {
return e.Description
}
// Currencies fetches the list of known currencies.
//
// Example:
// currencies, err := api.Currencies()
// for code, name := range currencies {
// // ...
// }
func Currencies() (*map[string]string, error) {
data, err := get("/currencies.json")
if err != nil {
return nil, err
}
var currencies map[string]string
if err := json.Unmarshal(data, &currencies); err != nil {
return nil, err
}
return &currencies, nil
}
// Fetch the latest exchange rates using USD as base.
//
// For example:
// rates, err := api.Latest()
func Latest() (*Rates, error) {
return LatestFrom("USD")
}
// Fetch the latest exchange rates for a given currency.
//
// For example:
// rates, err := api.LatestFrom("EUR")
func LatestFrom(currency string) (*Rates, error) {
data, err := get("/latest.json?base=" + currency)
if err != nil {
return nil, err
}
var rates Rates
if err := json.Unmarshal(data, &rates); err != nil {
return nil, err
}
return &rates, nil
}
// Clear HTTP response cache.
func Flush() {
for url, _ := range etags {
delete(etags, url)
}
}
// Fetch JSON data from the API, at the given path.
func get(path string) ([]byte, error) {
if AppId == "" {
return nil, ConfigError{"Missing App ID"}
}
// Build absolute url from path, and append AppID param
api_url, err := absoluteUrl(path)
if err != nil {
return nil, err
}
// Build a GET Request, including optional If-None-Match header.
req, err := buildRequest(api_url)
if err != nil {
return nil, err
}
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
// No change from latest known Etag?
if resp.StatusCode == http.StatusNotModified {
return etags[api_url].Body, nil
}
if resp.StatusCode != 200 {
return errorHandler(resp)
}
return updateEtagCache(api_url, resp)
}
// Update the internal Etags cache.
func updateEtagCache(api_url string, resp *http.Response) ([]byte, error) {
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
// If etags headers are missing, ignore.
key := resp.Header.Get("ETag")
date := resp.Header.Get("Date")
if key == "" || date == "" {
return body, err
}
etags[api_url] = Etag{key, date, body}
return body, err
}
// Build an HTTP request, including Etags data.
func buildRequest(api_url string) (*http.Request, error) {
req, err := http.NewRequest("GET", api_url, nil)
if err != nil {
return nil, err
}
if _, present := etags[api_url]; present {
req.Header.Add("If-None-Match", etags[api_url].Key)
req.Header.Add("If-Modified-Since", etags[api_url].Date)
}
return req, err
}
// Convert API request path to absolute URL, and optionally append the
// configured App ID.
func absoluteUrl(path string) (string, error) {
api_url, err := url.Parse(api() + path)
query := api_url.Query()
if err != nil {
return "", err
}
if AppId != "" {
query.Add("app_id", AppId)
}
api_url.RawQuery = query.Encode()
return api_url.String(), nil
}
func api() string {
return Scheme + "://" + Host + "/api"
}
// Handle JSON error messages from the API
func errorHandler(resp *http.Response) ([]byte, error) {
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var apiError ApiError
if err := json.Unmarshal(body, &apiError); err != nil {
return nil, err
}
return body, apiError
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment