Created
October 23, 2018 14:16
-
-
Save aliuygur/804b2293c56ccce6f7d22ca6a62b10f0 to your computer and use it in GitHub Desktop.
etag caching example in go
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
/* | |
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, ¤cies); err != nil { | |
return nil, err | |
} | |
return ¤cies, 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