-
Notifications
You must be signed in to change notification settings - Fork 39
/
Copy pathclient.go
180 lines (151 loc) · 5.23 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
// Copyright (c) 2018, Randy Westlund. All rights reserved.
// This code is under the BSD-2-Clause license.
package quickbooks
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"time"
)
// Client is your handle to the QuickBooks API.
type Client struct {
// Get this from oauth2.NewClient().
Client *http.Client
// Set to ProductionEndpoint or SandboxEndpoint.
endpoint *url.URL
// The set of quickbooks APIs
discoveryAPI *DiscoveryAPI
// The client Id
clientId string
// The client Secret
clientSecret string
// The minor version of the QB API
minorVersion string
// The account Id you're connecting to.
realmId string
// Flag set if the limit of 500req/s has been hit (source: https://developer.intuit.com/app/developer/qbo/docs/learn/rest-api-features#limits-and-throttles)
throttled bool
}
// NewClient initializes a new QuickBooks client for interacting with their Online API
func NewClient(clientId string, clientSecret string, realmId string, isProduction bool, minorVersion string, token *BearerToken) (c *Client, err error) {
if minorVersion == "" {
minorVersion = "65"
}
client := Client{
clientId: clientId,
clientSecret: clientSecret,
minorVersion: minorVersion,
realmId: realmId,
throttled: false,
}
if isProduction {
client.endpoint, err = url.Parse(ProductionEndpoint.String() + "/v3/company/" + realmId + "/")
if err != nil {
return nil, fmt.Errorf("failed to parse API endpoint: %v", err)
}
client.discoveryAPI, err = CallDiscoveryAPI(DiscoveryProductionEndpoint)
if err != nil {
return nil, fmt.Errorf("failed to obtain discovery endpoint: %v", err)
}
} else {
client.endpoint, err = url.Parse(SandboxEndpoint.String() + "/v3/company/" + realmId + "/")
if err != nil {
return nil, fmt.Errorf("failed to parse API endpoint: %v", err)
}
client.discoveryAPI, err = CallDiscoveryAPI(DiscoverySandboxEndpoint)
if err != nil {
return nil, fmt.Errorf("failed to obtain discovery endpoint: %v", err)
}
}
if token != nil {
client.Client = getHttpClient(token)
}
return &client, nil
}
// FindAuthorizationUrl compiles the authorization url from the discovery api's auth endpoint.
//
// Example: qbClient.FindAuthorizationUrl("com.intuit.quickbooks.accounting", "security_token", "https://developer.intuit.com/v2/OAuth2Playground/RedirectUrl")
//
// You can find live examples from https://developer.intuit.com/app/developer/playground
func (c *Client) FindAuthorizationUrl(scope string, state string, redirectUri string) (string, error) {
var authorizationUrl *url.URL
authorizationUrl, err := url.Parse(c.discoveryAPI.AuthorizationEndpoint)
if err != nil {
return "", fmt.Errorf("failed to parse auth endpoint: %v", err)
}
urlValues := url.Values{}
urlValues.Add("client_id", c.clientId)
urlValues.Add("response_type", "code")
urlValues.Add("scope", scope)
urlValues.Add("redirect_uri", redirectUri)
urlValues.Add("state", state)
authorizationUrl.RawQuery = urlValues.Encode()
return authorizationUrl.String(), nil
}
func (c *Client) req(method string, endpoint string, payloadData interface{}, responseObject interface{}, queryParameters map[string]string) error {
// TODO: possibly just wait until c.throttled is false, and continue the request?
if c.throttled {
return errors.New("waiting for rate limit")
}
endpointUrl := *c.endpoint
endpointUrl.Path += endpoint
urlValues := url.Values{}
if len(queryParameters) > 0 {
for param, value := range queryParameters {
urlValues.Add(param, value)
}
}
urlValues.Set("minorversion", c.minorVersion)
urlValues.Encode()
endpointUrl.RawQuery = urlValues.Encode()
var err error
var marshalledJson []byte
if payloadData != nil {
marshalledJson, err = json.Marshal(payloadData)
if err != nil {
return fmt.Errorf("failed to marshal payload: %v", err)
}
}
req, err := http.NewRequest(method, endpointUrl.String(), bytes.NewBuffer(marshalledJson))
if err != nil {
return fmt.Errorf("failed to create request: %v", err)
}
req.Header.Add("Accept", "application/json")
req.Header.Add("Content-Type", "application/json")
resp, err := c.Client.Do(req)
if err != nil {
return fmt.Errorf("failed to make request: %v", err)
}
defer resp.Body.Close()
switch resp.StatusCode {
case http.StatusOK:
break
case http.StatusTooManyRequests:
c.throttled = true
go func(c *Client) {
time.Sleep(1 * time.Minute)
c.throttled = false
}(c)
default:
return parseFailure(resp)
}
if responseObject != nil {
if err = json.NewDecoder(resp.Body).Decode(&responseObject); err != nil {
return fmt.Errorf("failed to unmarshal response into object: %v", err)
}
}
return nil
}
func (c *Client) get(endpoint string, responseObject interface{}, queryParameters map[string]string) error {
return c.req("GET", endpoint, nil, responseObject, queryParameters)
}
func (c *Client) post(endpoint string, payloadData interface{}, responseObject interface{}, queryParameters map[string]string) error {
return c.req("POST", endpoint, payloadData, responseObject, queryParameters)
}
// query makes the specified QBO `query` and unmarshals the result into `responseObject`
func (c *Client) query(query string, responseObject interface{}) error {
return c.get("query", responseObject, map[string]string{"query": query})
}