forked from sgt-kabukiman/srapi
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathhttp.go
More file actions
214 lines (169 loc) · 5.42 KB
/
http.go
File metadata and controls
214 lines (169 loc) · 5.42 KB
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
// Copyright (c) 2015, Sgt. Kabukiman | MIT licensed
package srapi
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
)
// ErrorBadJSON represents an invalid response from the API server, usually due to
// server-side downtimes or bugs.
const ErrorBadJSON = 900
// ErrorNetwork represents connection timeouts and other network issues.
const ErrorNetwork = 901
// ErrorBadURL represents the unlikely case of trying to fetch an invalid URL.
// Except for bugs in this package, this should never occur.
const ErrorBadURL = 902
// ErrorBadLogic represents a programmer mistake, like trying to get a leaderboard
// without specifying the game and category.
const ErrorBadLogic = 903
// ErrorNoSuchLink represents the case when the package wants to follow a link
// in the resource which is suddenly not present. As the code relies on links to
// move around, this is bad.
const ErrorNoSuchLink = 904
// BaseURL is the base URL for all API calls.
const BaseURL = "http://www.speedrun.com/api/v1"
// our http client, initialized by init
var httpClient *apiClient
// internal request counter, used for tests to determine if embeds worked
var requestCount int
// requests are only counted when this flag is set
var countRequests bool
// initialize the httpClient
func init() {
httpClient = &apiClient{
baseURL: BaseURL,
client: &http.Client{},
project: "",
}
requestCount = 0
countRequests = false
}
// SetProjectName can be used to append a custom string to the User-Agent header.
// If the given value is not empty, it will be appended including a separator,
// so give something like "myapp/1.0".
func SetProjectName(name string) {
httpClient.project = name
}
// request represents all options relevant for making an actual HTTP request.
// These fields are mapped to a http.Request when performing a request.
type request struct {
// HTTP method, like "GET"
method string
// the URL, relative to BaseURL
url string
// optional filter (will be applied to the query string)
filter filter
// optional sorting (will be applied to the query string)
sorting *Sorting
// optional cursor (will be applied to the query string)
cursor *Cursor
// embeds as a comma-separated string
embeds string
}
// apiClient is our helper to not pollute the package-wide variables
type apiClient struct {
// the effective base url
baseURL string
// project name
project string
// the underlying, concurrency-safe HTTP client
client *http.Client
}
// do performs a HTTP request by transforming the request and applying the
// filters. The data is parsed as JSON and unmarshaled into dst. An error is
// returned when the request failed or when invalid JSON was received.
func (ac *apiClient) do(request request, dst interface{}) *Error {
// prepare the actual net.http.Request
u, err := url.Parse(ac.baseURL + request.url)
if err != nil {
return failedRequest(request, nil, err, ErrorBadURL)
}
if request.filter != nil {
request.filter.applyToURL(u)
}
if request.cursor != nil {
request.cursor.applyToURL(u)
}
if request.sorting != nil {
request.sorting.applyToURL(u)
}
if request.embeds != "" {
values := u.Query()
values.Set("embed", request.embeds)
u.RawQuery = values.Encode()
}
userAgent := "go-srapi/" + Version
if ac.project != "" {
userAgent = userAgent + "; " + ac.project
}
req := http.Request{
Method: request.method,
URL: u,
Header: map[string][]string{
// "Accept-Encoding": {"gzip, deflate"},
"Accept": {"application/json, text/json"},
"User-Agent": {userAgent},
"Connection": {"keep-alive"},
},
}
if countRequests {
requestCount++
}
// hit the network
response, err := ac.client.Do(&req)
if err != nil {
return failedRequest(request, nil, err, ErrorNetwork)
}
// decode a successful response
if response.StatusCode == 200 || response.StatusCode == 201 {
defer response.Body.Close()
err = json.NewDecoder(response.Body).Decode(dst)
if err != nil {
return failedRequest(request, nil, err, ErrorBadJSON)
}
// everything went fine
return nil
}
// something went wrong
return failedRequest(request, response, nil, 0)
}
// Error is an error that occured in this package. It contains basic information
// about the failed request (if any, some errors are independent of requests)
// and about what failed.
type Error struct {
// the HTTP method of the request that failed, empty if no request involved
Method string
// the URL that failed, empty if no request involved
URL string
// the HTTP status code, set to one of the Error* constants of this package for
// internal errors
Status int
// a description of what failed
Message string
}
// Error returns a string including all details of the Error struct.
func (e *Error) Error() string {
return fmt.Sprintf("[%d] %s (%s %s)", e.Status, e.Message, e.Method, e.URL)
}
// failedRequest is a helper to assemble a Error struct when a request failed.
func failedRequest(request request, response *http.Response, previous error, errorCode int) *Error {
// build an incomplete error struct
result := &Error{
Method: request.method,
URL: request.url,
}
// decode the body into an Error
if previous != nil {
result.Status = errorCode
result.Message = previous.Error()
} else {
defer response.Body.Close()
err := json.NewDecoder(response.Body).Decode(result)
if err != nil {
result.Status = ErrorBadJSON
result.Message = "Could not decode response body as JSON. Site is probably having issues."
}
}
return result
}