ynote.New   A
last analyzed

Complexity

Conditions 2

Size

Total Lines 21
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 16
dl 0
loc 21
rs 9.6
c 0
b 0
f 0
nop 1
1
package ynote
2
3
import (
4
	"bytes"
5
	"context"
6
	"encoding/json"
7
	"fmt"
8
	"io"
9
	"net/http"
10
	"net/url"
11
	"strconv"
12
	"strings"
13
	"sync"
14
	"time"
15
)
16
17
type service struct {
18
	client *Client
19
}
20
21
// Client is the Y-Note API client.
22
// Do not instantiate this client with Client{}. Use the New method instead.
23
type Client struct {
24
	httpClient *http.Client
25
	common     service
26
27
	customerKey    string
28
	customerSecret string
29
	clientID       string
30
	clientSecret   string
31
	tokenURL       string
32
	apiURL         string
33
34
	accessToken         string
35
	tokenExpirationTime int64
36
	mutex               sync.Mutex
37
38
	Refund *RefundService
39
}
40
41
// New creates and returns a new ynote.Client from a slice of ynote.Option.
42
func New(options ...Option) *Client {
43
	config := defaultClientConfig()
44
45
	for _, option := range options {
46
		option.apply(config)
47
	}
48
49
	client := &Client{
50
		httpClient:     config.httpClient,
51
		tokenURL:       config.tokenURL,
52
		apiURL:         config.apiURL,
53
		clientID:       config.clientID,
54
		clientSecret:   config.clientSecret,
55
		customerKey:    config.customerKey,
56
		customerSecret: config.customerSecret,
57
		mutex:          sync.Mutex{},
58
	}
59
60
	client.common.client = client
61
	client.Refund = (*RefundService)(&client.common)
62
	return client
63
}
64
65
// AccessToken fetches the access token used to authenticate api requests.
66
func (client *Client) AccessToken(ctx context.Context) (*AccessTokenResponse, *Response, error) {
67
	data := url.Values{}
68
	data.Set("grant_type", "client_credentials")
69
70
	request, err := http.NewRequestWithContext(ctx, http.MethodPost, client.tokenURL+"/oauth2/token", strings.NewReader(data.Encode()))
71
	if err != nil {
72
		return nil, nil, err
73
	}
74
75
	request.SetBasicAuth(client.clientID, client.clientSecret)
76
	request.Header.Add("Content-Type", "application/x-www-form-urlencoded")
77
	request.Header.Add("Content-Length", strconv.Itoa(len(data.Encode())))
78
79
	resp, err := client.do(request)
80
	if err != nil {
81
		return nil, resp, err
82
	}
83
84
	var token AccessTokenResponse
85
	if err = json.Unmarshal(*resp.Body, &token); err != nil {
86
		return nil, resp, err
87
	}
88
89
	return &token, resp, nil
90
}
91
92
// refreshToken refreshes the authentication AccessTokenResponse
93
func (client *Client) refreshToken(ctx context.Context) error {
94
	client.mutex.Lock()
95
	defer client.mutex.Unlock()
96
97
	if client.tokenExpirationTime > time.Now().UTC().Unix() {
98
		return nil
99
	}
100
101
	client.accessToken = ""
102
103
	token, _, err := client.AccessToken(ctx)
104
	if err != nil {
105
		return err
106
	}
107
108
	client.accessToken = token.AccessToken
109
	client.tokenExpirationTime = time.Now().UTC().Unix() + token.ExpiresIn - 100 // Give extra 100 second buffer
110
111
	return nil
112
}
113
114
// newRequest creates an API request. A relative URL can be provided in uri,
115
// in which case it is resolved relative to the BaseURL of the Client.
116
// URI's should always be specified without a preceding slash.
117
func (client *Client) newRequest(ctx context.Context, method, uri string, body any) (*http.Request, error) {
118
	var buf io.ReadWriter
119
	if body != nil {
120
		buf = &bytes.Buffer{}
121
		enc := json.NewEncoder(buf)
122
		enc.SetEscapeHTML(false)
123
		err := enc.Encode(body)
124
		if err != nil {
125
			return nil, err
126
		}
127
	}
128
129
	req, err := http.NewRequestWithContext(ctx, method, client.apiURL+uri, buf)
130
	if err != nil {
131
		return nil, err
132
	}
133
134
	req.Header.Set("Authorization", "Bearer "+client.accessToken)
135
	req.Header.Set("Content-Type", "application/json")
136
	req.Header.Set("Accept", "application/json")
137
138
	return req, nil
139
}
140
141
// do carries out an HTTP request and returns a Response
142
func (client *Client) do(req *http.Request) (*Response, error) {
143
	if req == nil {
144
		return nil, fmt.Errorf("%T cannot be nil", req)
145
	}
146
147
	httpResponse, err := client.httpClient.Do(req)
148
	if err != nil {
149
		return nil, err
150
	}
151
152
	defer func() { _ = httpResponse.Body.Close() }()
153
154
	resp, err := client.newResponse(httpResponse)
155
	if err != nil {
156
		return resp, err
157
	}
158
159
	_, err = io.Copy(io.Discard, httpResponse.Body)
160
	if err != nil {
161
		return resp, err
162
	}
163
164
	return resp, nil
165
}
166
167
// newResponse converts an *http.Response to *Response
168
func (client *Client) newResponse(httpResponse *http.Response) (*Response, error) {
169
	if httpResponse == nil {
170
		return nil, fmt.Errorf("%T cannot be nil", httpResponse)
171
	}
172
173
	resp := new(Response)
174
	resp.HTTPResponse = httpResponse
175
176
	buf, err := io.ReadAll(resp.HTTPResponse.Body)
177
	if err != nil {
178
		return nil, err
179
	}
180
	resp.Body = &buf
181
182
	return resp, resp.Error()
183
}
184