-
Notifications
You must be signed in to change notification settings - Fork 60
/
client.go
190 lines (158 loc) · 5.32 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
181
182
183
184
185
186
187
188
189
190
//
// Copyright (c) 2022 MessageBird B.V.
// All rights reserved.
//
// Author: Maurice Nonnekes <[email protected]>
// Package messagebird is an official library for interacting with MessageBird.com API.
// The MessageBird API connects your website or application to operators around the world. With our API you can integrate SMS, Chat & Voice.
// More documentation you can find on the MessageBird developers portal: https://developers.messagebird.com/
package messagebird
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"log"
"net/http"
"net/url"
"runtime"
"strings"
"time"
)
const (
// ClientVersion is used in User-Agent request header to provide server with API level.
ClientVersion = "9.1.0"
// Endpoint points you to MessageBird REST API.
Endpoint = "https://rest.messagebird.com"
// httpClientTimeout is used to limit http.Client waiting time.
httpClientTimeout = 15 * time.Second
)
var (
// ErrUnexpectedResponse is used when there was an internal server error and nothing can be done at this point.
ErrUnexpectedResponse = errors.New("the MessageBird API is currently unavailable")
)
// A Feature can be enabled
type Feature int
type Client interface {
Request(v interface{}, method, path string, data interface{}) error
}
// DefaultClient is used to access API with a given key.
// Uses standard lib HTTP client internally, so should be reused instead of created as needed and it is safe for concurrent use.
type DefaultClient struct {
AccessKey string // The API access key.
HTTPClient *http.Client // The HTTP client to send requests on.
DebugLog *log.Logger // Optional logger for debugging purposes.
}
type contentType string
const (
contentTypeEmpty contentType = ""
contentTypeJSON contentType = "application/json"
contentTypeFormURLEncoded contentType = "application/x-www-form-urlencoded"
)
// errorReader reads the provided byte slice into an appropriate error.
type errorReader func([]byte) error
var customErrorReader errorReader
// SetErrorReader takes an errorReader that must parse raw JSON errors
func SetErrorReader(r errorReader) {
customErrorReader = r
}
// New creates a new MessageBird client object.
func New(accessKey string) *DefaultClient {
return &DefaultClient{
AccessKey: accessKey,
HTTPClient: &http.Client{
Timeout: httpClientTimeout,
},
}
}
// Request is for internal use only and unstable.
func (c *DefaultClient) Request(v interface{}, method, path string, data interface{}) error {
if !strings.HasPrefix(path, "https://") && !strings.HasPrefix(path, "http://") {
path = fmt.Sprintf("%s/%s", Endpoint, path)
}
uri, err := url.Parse(path)
if err != nil {
return err
}
body, contentType, err := prepareRequestBody(data)
if err != nil {
return err
}
request, err := http.NewRequest(method, uri.String(), bytes.NewBuffer(body))
if err != nil {
return err
}
request.Header.Set("Accept", "application/json")
request.Header.Set("Authorization", "AccessKey "+c.AccessKey)
request.Header.Set("User-Agent", "MessageBird/ApiClient/"+ClientVersion+" Go/"+runtime.Version())
if contentType != contentTypeEmpty {
request.Header.Set("Content-Type", string(contentType))
}
if c.DebugLog != nil {
if data != nil {
c.DebugLog.Printf("HTTP REQUEST: %s %s %s", method, uri.String(), body)
} else {
c.DebugLog.Printf("HTTP REQUEST: %s %s", method, uri.String())
}
}
response, err := c.HTTPClient.Do(request)
if err != nil {
return err
}
defer response.Body.Close()
responseBody, err := ioutil.ReadAll(response.Body)
if err != nil {
return err
}
if c.DebugLog != nil {
c.DebugLog.Printf("HTTP RESPONSE: %s", string(responseBody))
}
switch response.StatusCode {
case http.StatusOK, http.StatusCreated, http.StatusAccepted:
// Status codes 200 and 201 are indicative of being able to convert the
// response body to the struct that was specified.
if err := json.Unmarshal(responseBody, &v); err != nil {
return fmt.Errorf("could not decode response JSON, %s: %v", string(responseBody), err)
}
return nil
case http.StatusNoContent:
// Status code 204 is returned for successful DELETE requests. Don't try to
// unmarshal the body: that would return errors.
return nil
case http.StatusInternalServerError:
// Status code 500 is a server error and means nothing can be done at this
// point.
return ErrUnexpectedResponse
default:
// Anything else than a 200/201/204/500 should be a JSON error.
if customErrorReader != nil {
return customErrorReader(responseBody)
}
return defaultErrorReader(responseBody)
}
}
func defaultErrorReader(b []byte) error {
var errorResponse ErrorResponse
if err := json.Unmarshal(b, &errorResponse); err != nil {
return fmt.Errorf("failed to unmarshal response json %s, error: %v", string(b), err)
}
return errorResponse
}
// prepareRequestBody takes untyped data and attempts constructing a meaningful
// request body from it. It also returns the appropriate Content-Type.
func prepareRequestBody(data interface{}) ([]byte, contentType, error) {
switch data := data.(type) {
case nil:
// Nil bodies are accepted by `net/http`, so this is not an error.
return nil, contentTypeEmpty, nil
case string:
return []byte(data), contentTypeFormURLEncoded, nil
default:
b, err := json.Marshal(data)
if err != nil {
return nil, "", err
}
return b, contentTypeJSON, nil
}
}