forked from getconversio/go-shopify
-
Notifications
You must be signed in to change notification settings - Fork 0
/
goshopify.go
executable file
·396 lines (344 loc) · 11.2 KB
/
goshopify.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
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
// Package goshopify provides methods for making requests to Shopify's admin API.
package goshopify
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"reflect"
"sort"
"strconv"
"strings"
"time"
"github.com/google/go-querystring/query"
)
const (
UserAgent = "goshopify/1.0.0"
)
// App represents basic app settings such as Api key, secret, scope, and redirect url.
// See oauth.go for OAuth related helper functions.
type App struct {
ApiKey string
ApiSecret string
RedirectUrl string
Scope string
Password string
}
// Client manages communication with the Shopify API.
type Client struct {
// HTTP client used to communicate with the DO API.
Client *http.Client
// App settings
app App
// Base URL for API requests.
// This is set on a per-store basis which means that each store must have
// its own client.
baseURL *url.URL
// A permanent access token
token string
// Services used for communicating with the API
Product ProductService
CustomCollection CustomCollectionService
SmartCollection SmartCollectionService
Customer CustomerService
Order OrderService
Shop ShopService
Webhook WebhookService
Variant VariantService
Image ImageService
Transaction TransactionService
Theme ThemeService
Asset AssetService
ScriptTag ScriptTagService
RecurringApplicationCharge RecurringApplicationChargeService
Metafield MetafieldService
Blog BlogService
ApplicationCharge ApplicationChargeService
}
// A general response error that follows a similar layout to Shopify's response
// errors, i.e. either a single message or a list of messages.
type ResponseError struct {
Status int
Message string
Errors []string
}
func (e ResponseError) Error() string {
if e.Message != "" {
return e.Message
}
sort.Strings(e.Errors)
s := strings.Join(e.Errors, ", ")
if s != "" {
return s
}
return "Unknown Error"
}
// ResponseDecodingError occurs when the response body from Shopify could
// not be parsed.
type ResponseDecodingError struct {
Body []byte
Message string
Status int
}
func (e ResponseDecodingError) Error() string {
return e.Message
}
// An error specific to a rate-limiting response. Embeds the ResponseError to
// allow consumers to handle it the same was a normal ResponseError.
type RateLimitError struct {
ResponseError
RetryAfter int
}
// Creates an API request. A relative URL can be provided in urlStr, which will
// be resolved to the BaseURL of the Client. Relative URLS should always be
// specified without a preceding slash. If specified, the value pointed to by
// body is JSON encoded and included as the request body.
func (c *Client) NewRequest(method, urlStr string, body, options interface{}) (*http.Request, error) {
rel, err := url.Parse(urlStr)
if err != nil {
return nil, err
}
// Make the full url based on the relative path
u := c.baseURL.ResolveReference(rel)
// Add custom options
if options != nil {
optionsQuery, err := query.Values(options)
if err != nil {
return nil, err
}
for k, values := range u.Query() {
for _, v := range values {
optionsQuery.Add(k, v)
}
}
u.RawQuery = optionsQuery.Encode()
}
// A bit of JSON ceremony
var js []byte = nil
if body != nil {
js, err = json.Marshal(body)
if err != nil {
return nil, err
}
}
req, err := http.NewRequest(method, u.String(), bytes.NewBuffer(js))
if err != nil {
return nil, err
}
req.Header.Add("Content-Type", "application/json")
req.Header.Add("Accept", "application/json")
req.Header.Add("User-Agent", UserAgent)
if c.token != "" {
req.Header.Add("X-Shopify-Access-Token", c.token)
} else if c.app.Password != "" {
req.SetBasicAuth(c.app.ApiKey, c.app.Password)
}
return req, nil
}
// Returns a new Shopify API client with an already authenticated shopname and
// token. The shopName parameter is the shop's myshopify domain,
// e.g. "theshop.myshopify.com", or simply "theshop"
func NewClient(app App, shopName, token string) *Client {
httpClient := http.DefaultClient
baseURL, _ := url.Parse(ShopBaseUrl(shopName))
c := &Client{Client: httpClient, app: app, baseURL: baseURL, token: token}
c.Product = &ProductServiceOp{client: c}
c.CustomCollection = &CustomCollectionServiceOp{client: c}
c.SmartCollection = &SmartCollectionServiceOp{client: c}
c.Customer = &CustomerServiceOp{client: c}
c.Order = &OrderServiceOp{client: c}
c.Shop = &ShopServiceOp{client: c}
c.Webhook = &WebhookServiceOp{client: c}
c.Variant = &VariantServiceOp{client: c}
c.Image = &ImageServiceOp{client: c}
c.Transaction = &TransactionServiceOp{client: c}
c.Theme = &ThemeServiceOp{client: c}
c.Asset = &AssetServiceOp{client: c}
c.ScriptTag = &ScriptTagServiceOp{client: c}
c.RecurringApplicationCharge = &RecurringApplicationChargeServiceOp{client: c}
c.Metafield = &MetafieldServiceOp{client: c}
c.Blog = &BlogServiceOp{client: c}
c.ApplicationCharge = &ApplicationChargeServiceOp{client: c}
return c
}
// Do sends an API request and populates the given interface with the parsed
// response. It does not make much sense to call Do without a prepared
// interface instance.
func (c *Client) Do(req *http.Request, v interface{}) error {
resp, err := c.Client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
err = CheckResponseError(resp)
if err != nil {
return err
}
if v != nil {
decoder := json.NewDecoder(resp.Body)
err := decoder.Decode(&v)
if err != nil {
return err
}
}
return nil
}
func wrapSpecificError(r *http.Response, err ResponseError) error {
if err.Status == 429 {
f, _ := strconv.ParseFloat(r.Header.Get("retry-after"), 64)
return RateLimitError{
ResponseError: err,
RetryAfter: int(f),
}
}
if err.Status == 406 {
err.Message = "Not acceptable"
}
return err
}
func CheckResponseError(r *http.Response) error {
if r.StatusCode >= 200 && r.StatusCode < 300 {
return nil
}
// Create an anonoymous struct to parse the JSON data into.
shopifyError := struct {
Error string `json:"error"`
Errors interface{} `json:"errors"`
}{}
bodyBytes, err := ioutil.ReadAll(r.Body)
if err != nil {
return err
}
// empty body, this probably means shopify returned an error with no body
// we'll handle that error in wrapSpecificError()
if len(bodyBytes) > 0 {
err := json.Unmarshal(bodyBytes, &shopifyError)
if err != nil {
return ResponseDecodingError{
Body: bodyBytes,
Message: err.Error(),
Status: r.StatusCode,
}
}
}
// Create the response error from the Shopify error.
responseError := ResponseError{
Status: r.StatusCode,
Message: shopifyError.Error,
}
// If the errors field is not filled out, we can return here.
if shopifyError.Errors == nil {
return wrapSpecificError(r, responseError)
}
// Shopify errors usually have the form:
// {
// "errors": {
// "title": [
// "something is wrong"
// ]
// }
// }
// This structure is flattened to a single array:
// [ "title: something is wrong" ]
//
// Unfortunately, "errors" can also be a single string so we have to deal
// with that. Lots of reflection :-(
switch reflect.TypeOf(shopifyError.Errors).Kind() {
case reflect.String:
// Single string, use as message
responseError.Message = shopifyError.Errors.(string)
case reflect.Slice:
// An array, parse each entry as a string and join them on the message
// json always serializes JSON arrays into []interface{}
for _, elem := range shopifyError.Errors.([]interface{}) {
responseError.Errors = append(responseError.Errors, fmt.Sprint(elem))
}
responseError.Message = strings.Join(responseError.Errors, ", ")
case reflect.Map:
// A map, parse each error for each key in the map.
// json always serializes into map[string]interface{} for objects
for k, v := range shopifyError.Errors.(map[string]interface{}) {
// Check to make sure the interface is a slice
// json always serializes JSON arrays into []interface{}
if reflect.TypeOf(v).Kind() == reflect.Slice {
for _, elem := range v.([]interface{}) {
// If the primary message of the response error is not set, use
// any message.
if responseError.Message == "" {
responseError.Message = fmt.Sprintf("%v: %v", k, elem)
}
topicAndElem := fmt.Sprintf("%v: %v", k, elem)
responseError.Errors = append(responseError.Errors, topicAndElem)
}
}
}
}
return wrapSpecificError(r, responseError)
}
// General list options that can be used for most collections of entities.
type ListOptions struct {
Page int `url:"page,omitempty"`
Limit int `url:"limit,omitempty"`
SinceID int `url:"since_id,omitempty"`
CreatedAtMin time.Time `url:"created_at_min,omitempty"`
CreatedAtMax time.Time `url:"created_at_max,omitempty"`
UpdatedAtMin time.Time `url:"updated_at_min,omitempty"`
UpdatedAtMax time.Time `url:"updated_at_max,omitempty"`
Order string `url:"order,omitempty"`
Fields string `url:"fields,omitempty"`
}
// General count options that can be used for most collection counts.
type CountOptions struct {
CreatedAtMin time.Time `url:"created_at_min,omitempty"`
CreatedAtMax time.Time `url:"created_at_max,omitempty"`
UpdatedAtMin time.Time `url:"updated_at_min,omitempty"`
UpdatedAtMax time.Time `url:"updated_at_max,omitempty"`
}
func (c *Client) Count(path string, options interface{}) (int, error) {
resource := struct {
Count int `json:"count"`
}{}
err := c.Get(path, &resource, options)
return resource.Count, err
}
// CreateAndDo performs a web request to Shopify with the given method (GET,
// POST, PUT, DELETE) and relative path (e.g. "/admin/orders.json").
// The data, options and resource arguments are optional and only relevant in
// certain situations.
// If the data argument is non-nil, it will be used as the body of the request
// for POST and PUT requests.
// The options argument is used for specifying request options such as search
// parameters like created_at_min
// Any data returned from Shopify will be marshalled into resource argument.
func (c *Client) CreateAndDo(method, path string, data, options, resource interface{}) error {
req, err := c.NewRequest(method, path, data, options)
if err != nil {
return err
}
err = c.Do(req, resource)
if err != nil {
return err
}
return nil
}
// Get performs a GET request for the given path and saves the result in the
// given resource.
func (c *Client) Get(path string, resource, options interface{}) error {
return c.CreateAndDo("GET", path, nil, options, resource)
}
// Post performs a POST request for the given path and saves the result in the
// given resource.
func (c *Client) Post(path string, data, resource interface{}) error {
return c.CreateAndDo("POST", path, data, nil, resource)
}
// Put performs a PUT request for the given path and saves the result in the
// given resource.
func (c *Client) Put(path string, data, resource interface{}) error {
return c.CreateAndDo("PUT", path, data, nil, resource)
}
// Delete performs a DELETE request for the given path
func (c *Client) Delete(path string) error {
return c.CreateAndDo("DELETE", path, nil, nil, nil)
}