-
Notifications
You must be signed in to change notification settings - Fork 14
/
Copy pathclient.go
238 lines (194 loc) · 5.53 KB
/
client.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
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
package natureremo
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"strings"
"time"
)
const version = "0.0.1"
const (
baseURL = "https://api.nature.global/"
apiVersion = "1"
)
var defaultUserAgent string
func init() {
defaultUserAgent = "tenntenn-natureremo/" + version + " (+https://github.com/tenntenn/natureremo)"
}
// Client is an API client for Nature Remo Cloud API.
type Client struct {
UserService UserService
DeviceService DeviceService
ApplianceService ApplianceService
SignalService SignalService
HTTPClient *http.Client
AccessToken string
BaseURL string
UserAgent string
LastRateLimit *RateLimit
}
// NewClient creates new client with access token of Nature Remo API.
// You can get access token from https://home.nature.global/.
func NewClient(accessToken string) *Client {
var cli Client
cli.AccessToken = accessToken
cli.UserService = &userService{cli: &cli}
cli.DeviceService = &deviceService{cli: &cli}
cli.ApplianceService = &applianceService{cli: &cli}
cli.SignalService = &signalService{cli: &cli}
cli.BaseURL = baseURL + apiVersion
cli.UserAgent = defaultUserAgent
return &cli
}
func (cli *Client) getUA() string {
if cli.UserAgent != "" {
return cli.UserAgent
}
return defaultUserAgent
}
func (cli *Client) httpClient() *http.Client {
if cli.HTTPClient != nil {
return cli.HTTPClient
}
return http.DefaultClient
}
func (cli *Client) do(ctx context.Context, req *http.Request) (*http.Response, error) {
req = req.WithContext(ctx)
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", cli.AccessToken))
req.Header.Set("User-Agent", cli.getUA())
return cli.httpClient().Do(req)
}
func (cli *Client) get(ctx context.Context, path string, params url.Values, v interface{}) error {
reqURL := cli.BaseURL + "/" + path
if params != nil {
reqURL += "?" + params.Encode()
}
req, err := http.NewRequest(http.MethodGet, reqURL, nil)
if err != nil {
return fmt.Errorf("cannot create HTTP request: %w", err)
}
resp, err := cli.do(ctx, req)
if err != nil {
return err
}
defer resp.Body.Close()
if !(resp.StatusCode >= http.StatusOK && resp.StatusCode < http.StatusMultipleChoices) {
return cli.error(resp.StatusCode, resp.Body)
}
rl, err := RateLimitFromHeader(resp.Header)
if err != nil {
return err
}
cli.LastRateLimit = rl
if err := json.NewDecoder(resp.Body).Decode(v); err != nil {
return fmt.Errorf("cannot parse HTTP body: %w", err)
}
return nil
}
func (cli *Client) postForm(ctx context.Context, path string, data url.Values, v interface{}) error {
reqURL := cli.BaseURL + "/" + path
body := strings.NewReader(data.Encode())
req, err := http.NewRequest(http.MethodPost, reqURL, body)
if err != nil {
return fmt.Errorf("cannot create HTTP request: %w", err)
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
resp, err := cli.do(ctx, req)
if err != nil {
return err
}
defer resp.Body.Close()
if !(resp.StatusCode >= http.StatusOK && resp.StatusCode < http.StatusMultipleChoices) {
return cli.error(resp.StatusCode, resp.Body)
}
if v == nil {
return nil
}
var respBody io.Reader = resp.Body
// For Debug
//var buf bytes.Buffer
//if _, err := buf.ReadFrom(resp.Body); err != nil {
// return fmt.Errorf("cannot parse HTTP body: %w")
//}
//fmt.Println(buf.String())
//respBody = &buf
if err := json.NewDecoder(respBody).Decode(v); err != nil {
return fmt.Errorf("cannot parse HTTP body: %w", err)
}
return nil
}
func (cli *Client) post(ctx context.Context, path string, v interface{}) error {
reqURL := cli.BaseURL + "/" + path
req, err := http.NewRequest(http.MethodPost, reqURL, nil)
if err != nil {
return fmt.Errorf("cannot create HTTP request: %w", err)
}
resp, err := cli.do(ctx, req)
if err != nil {
return err
}
defer resp.Body.Close()
if !(resp.StatusCode >= http.StatusOK && resp.StatusCode < http.StatusMultipleChoices) {
return cli.error(resp.StatusCode, resp.Body)
}
if v == nil {
return nil
}
if err := json.NewDecoder(resp.Body).Decode(v); err != nil {
return fmt.Errorf("cannot parse HTTP body: %w", err)
}
return nil
}
func (cli *Client) error(statusCode int, body io.Reader) error {
var aerr APIError
if err := json.NewDecoder(body).Decode(&aerr); err != nil {
return &APIError{HTTPStatus: statusCode}
}
aerr.HTTPStatus = statusCode
return &aerr
}
// RateLimit has values of X-Rate-Limit-* in the response header.
type RateLimit struct {
// Limit is a limit of request.
Limit int64
// Remaining is remaining request count.
Remaining int64
// Reset is time which a limit of request would be reseted.
Reset time.Time
}
func RateLimitFromHeader(h http.Header) (*RateLimit, error) {
ls := h.Get("X-Rate-Limit-Limit")
if ls == "" {
return nil, errors.New("cannot get X-Rate-Limit-Limit from header")
}
l, err := strconv.ParseInt(ls, 10, 64)
if err != nil {
return nil, fmt.Errorf("X-Rate-Limit-Limit is invalid value: %w", err)
}
rs := h.Get("X-Rate-Limit-Remaining")
if rs == "" {
return nil, errors.New("cannot get X-Rate-Limit-Remaining from header")
}
r, err := strconv.ParseInt(rs, 10, 64)
if err != nil {
return nil, fmt.Errorf("X-Rate-Limit-Remaining is invalid value: %w", err)
}
ts := h.Get("X-Rate-Limit-Reset")
if ts == "" {
return nil, errors.New("cannot get X-Rate-Limit-Reset from header")
}
t, err := strconv.ParseInt(ts, 10, 64)
if err != nil {
return nil, fmt.Errorf("X-Rate-Limit-Reset is invalid value: %w", err)
}
return &RateLimit{
Limit: l,
Remaining: r,
Reset: time.Unix(t, 0),
}, nil
}