-
Notifications
You must be signed in to change notification settings - Fork 1
/
client.go
133 lines (114 loc) · 3 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
package fio
import (
"bytes"
"context"
"encoding/json"
"encoding/xml"
"fmt"
"io"
"net/http"
"net/url"
"strings"
)
const (
defaultBaseURL = "https://fioapi.fio.cz/"
dateFormat = "2006-01-02"
)
// NewClient returns new fio http client.
func NewClient(token string, client *http.Client) *Client {
baseURL, _ := url.Parse(defaultBaseURL)
if client == nil {
client = http.DefaultClient
}
c := &Client{
BaseURL: baseURL,
Token: token,
client: client,
}
c.Transactions = &TransactionsService{client: c}
return c
}
// Client is fio http api client.
type Client struct {
client *http.Client
Token string
BaseURL *url.URL
Transactions *TransactionsService
}
func (c *Client) newGetRequest(ctx context.Context, urlStr string) (*http.Request, error) {
return c.newRequest(ctx, http.MethodGet, urlStr, nil)
}
func (c *Client) newRequest(ctx context.Context, method string, urlStr string, body interface{}) (*http.Request, error) {
var buf io.ReadWriter
if body != nil {
buf = new(bytes.Buffer)
if err := json.NewEncoder(buf).Encode(body); err != nil {
return nil, err
}
}
return http.NewRequestWithContext(ctx, method, urlStr, buf)
}
func (c *Client) do(req *http.Request) (*http.Response, error) {
resp, err := c.client.Do(req)
if err != nil {
return nil, err
}
if err := c.checkResponse(resp); err != nil {
return nil, err
}
return resp, nil
}
func (c *Client) buildURL(resource string, segments ...string) string {
var parts []string
parts = append(parts, resource, c.Token)
parts = append(parts, segments...)
urlStr := strings.Join(parts, "/")
ref, _ := url.Parse(urlStr)
u := c.BaseURL.ResolveReference(ref)
return u.String()
}
// ErrorResponse wraps http response errors.
type ErrorResponse struct {
Response *http.Response
Message string
Token string
}
func (r *ErrorResponse) Error() string {
return fmt.Sprintf("%v %v: %d %v",
r.Response.Request.Method, SanitizeURL(r.Token, r.Response.Request.URL),
r.Response.StatusCode, r.Message)
}
func (c *Client) checkResponse(r *http.Response) error {
if c := r.StatusCode; http.StatusOK <= c && c <= 299 {
return nil
}
resp := &ErrorResponse{
Response: r,
Token: c.Token,
}
defer r.Body.Close()
// this seems to be the current response code mapping for fio API
// 500 validation errors
// 500 invalid token
// 409 rate limit (one request per 30 seconds is allowed)
// 404 resource not found
// 400 invalid date format in url
// 200 ok
// try to handle validation error
if r.StatusCode == http.StatusInternalServerError && strings.Contains(r.Header.Get("Content-Type"), "text/xml") {
var errResp xmlErrorResponse
if err := xml.NewDecoder(r.Body).Decode(&errResp); err == nil {
resp.Message = errResp.Result.Message
}
}
return resp
}
// SanitizeURL redacts the token part of the URL.
func SanitizeURL(token string, u *url.URL) *url.URL {
if token == "" {
return u
}
redacted := strings.ReplaceAll(u.String(), token, "REDACTED")
redactedURL, _ := url.Parse(redacted)
return redactedURL
}