-
Notifications
You must be signed in to change notification settings - Fork 60
/
luno.go
182 lines (157 loc) · 4.42 KB
/
luno.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
// Package luno is a wrapper for the Luno API.
package luno
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"log"
"net/http"
"runtime"
"strings"
"time"
"golang.org/x/time/rate"
)
type Limiter interface {
Wait(context.Context) error
}
// Client is a Luno API client.
type Client struct {
httpClient *http.Client
rateLimiter Limiter
baseURL string
apiKeyID string
apiKeySecret string
debug bool
}
const (
defaultBaseURL = "https://api.luno.com"
defaultTimeout = 10 * time.Second
// Rate limiting parameters:
// Refer to https://www.luno.com/en/developers/api#tag/Rate-Limiting
// The default configuration allows for a burst of 1 request every 200ms
// which aggregates to 300 requests per minute.
//
// defaultRate specifies the rate at which requests are allowed.
defaultRate = time.Minute / 300
// defaultBurst specifies the maximum number of requests allowed in a burst.
defaultBurst = 1
)
// NewClient creates a new Luno API client with the default base URL.
func NewClient() *Client {
return &Client{
httpClient: &http.Client{Timeout: defaultTimeout},
baseURL: defaultBaseURL,
rateLimiter: rate.NewLimiter(rate.Every(defaultRate), defaultBurst),
}
}
// SetAuth provides the client with an API key and secret.
func (cl *Client) SetAuth(apiKeyID, apiKeySecret string) error {
if apiKeyID == "" || apiKeySecret == "" {
return errors.New("luno: no credentials provided")
}
cl.apiKeyID = apiKeyID
cl.apiKeySecret = apiKeySecret
return nil
}
// SetHTTPClient sets the HTTP client that will be used for API calls.
func (cl *Client) SetHTTPClient(httpClient *http.Client) {
cl.httpClient = httpClient
}
// SetRateLimiter sets the rate limiter that will be used to throttle calls
// made through the client.
func (cl *Client) SetRateLimiter(rateLimiter Limiter) {
cl.rateLimiter = rateLimiter
}
// SetTimeout sets the timeout for requests made by this client. Note: if you
// set a timeout and then call .SetHTTPClient(), the timeout in the new HTTP
// client will be used.
func (cl *Client) SetTimeout(timeout time.Duration) {
cl.httpClient.Timeout = timeout
}
// SetBaseURL overrides the default base URL. For internal use.
func (cl *Client) SetBaseURL(baseURL string) {
cl.baseURL = strings.TrimRight(baseURL, "/")
}
// SetDebug enables or disables debug mode. In debug mode, HTTP requests and
// responses will be logged.
func (cl *Client) SetDebug(debug bool) {
cl.debug = debug
}
func (cl *Client) do(ctx context.Context, method, path string,
req, res interface{}, auth bool,
) error {
err := cl.rateLimiter.Wait(ctx)
if err != nil {
return err
}
url := cl.baseURL + "/" + strings.TrimLeft(path, "/")
if cl.debug {
log.Printf("luno: Call: %s %s", method, path)
log.Printf("luno: Request: %#v", req)
}
var contentType string
var body io.Reader
if req != nil {
values := makeURLValues(req)
if strings.Contains(path, "{id}") {
url = strings.ReplaceAll(url, "{id}", values.Get("id"))
values.Del("id")
}
if method == http.MethodGet {
url = url + "?" + values.Encode()
} else {
body = strings.NewReader(values.Encode())
contentType = "application/x-www-form-urlencoded"
}
}
httpReq, err := http.NewRequest(method, url, body)
if err != nil {
return err
}
httpReq = httpReq.WithContext(ctx)
httpReq.Header.Set("User-Agent", makeUserAgent())
if contentType != "" {
httpReq.Header.Set("Content-Type", contentType)
}
if auth {
httpReq.SetBasicAuth(cl.apiKeyID, cl.apiKeySecret)
}
if method != http.MethodGet {
httpReq.Header.Set("content-type", "application/x-www-form-urlencoded")
}
httpRes, err := cl.httpClient.Do(httpReq)
if err != nil {
return err
}
defer httpRes.Body.Close()
body = httpRes.Body
if cl.debug {
b, err := io.ReadAll(body)
if err != nil {
log.Printf("luno: Error reading response body: %v", err)
} else {
log.Printf("Response: %s", string(b))
}
body = bytes.NewReader(b)
}
if httpRes.StatusCode == http.StatusTooManyRequests {
return errors.New("luno: too many requests")
}
if httpRes.StatusCode != http.StatusOK {
var e Error
err := json.NewDecoder(body).Decode(&e)
if err != nil {
return fmt.Errorf("luno: error decoding response (%d %s)",
httpRes.StatusCode, http.StatusText(httpRes.StatusCode))
}
return e
}
return json.NewDecoder(body).Decode(res)
}
func makeUserAgent() string {
return fmt.Sprintf("LunoGoSDK/%s %s %s %s",
Version, runtime.Version(), runtime.GOOS, runtime.GOARCH)
}