-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathrest.go
309 lines (291 loc) · 12.9 KB
/
rest.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
// Copyright 2015 Alex Browne. All rights reserved.
// Use of this source code is governed by the MIT
// license, which can be found in the LICENSE file.
// package rest is a small package for sending requests to a RESTful API and
// unmarshaling the response. It compiles to javascript via gopherjs and is
// intended to run in the browser.
//
// Rest sends requests using CRUD semantics. It supports requests with a
// Content-Type of either application/x-www-form-urlencoded or application/json
// and parses json responses from the server.
//
// Version 0.2.0
package rest
import (
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"reflect"
"strings"
)
// ContentType represents a Content-Type header.
type ContentType string
const (
ContentJSON ContentType = "application/json"
ContentURLEncoded ContentType = "application/x-www-form-urlencoded"
)
// A client is capable of sending RESTful requests to some server and
// unmarshalling the response into an arbitrary struct type.
type Client struct {
// ContentType is used to determine the Content-Type header and encoding
// the the client will use when sending requests. By default, the value
// is ContentURLEncoded, which corresponds to the Content-Type header
// "application/x-www-form-urlencoded". To send requests encoded as JSON,
// you can set this to ContentJSON, which corresponds to the Content-Type
// header "application/json".
ContentType ContentType
}
// NewClient returns a new client with all the default settings.
func NewClient() *Client {
return &Client{
ContentType: ContentURLEncoded,
}
}
// Model must be satisfied by all models. Satisfying this interface allows you to
// use the helper methods which send http requests to a REST API. They are used
// for e.g., creating a new model or getting an existing model from the server.
// Because of the way reflection is used to encode the data, a Model must have an
// underlying type of a struct, and all fields you wish to be included in requests
// and responses must be exported.
type Model interface {
// ModelId returns a unique identifier for the model. It is used for determining
// which URL to send a request to.
ModelId() string
// RootURL returns the url for the REST resource corresponding to this model.
// If you want to send requests to the same server, it should look something
// like "/todos". If you want to send requests to a different server, you can
// include the entire domain in the url, e.g. "http://example.com/todos". Note
// that the trailing slash should not be included.
RootURL() string
}
// Create sends an http request to create the given model. It uses reflection to
// convert the fields of model to url-encoded data. Then it sends a POST request to
// model.RootURL() with the encoded data in the body and the appropriate Content-Type
// header. It expects a JSON response containing the created object from the server
// if the request was successful, in which case it will mutate model by setting the
// fields to the values in the JSON response. Since model may be mutated, it should
// be a pointer.
func (c *Client) Create(model Model) error {
fullURL := model.RootURL()
encodedModelData, err := c.encodeFields(model)
if err != nil {
return err
}
return c.sendRequestAndUnmarshal("POST", fullURL, encodedModelData, model)
}
// Read sends an http request to read (or fetch) the model with the given id
// from the server. It sends a GET request to model.RootURL() + "/" + model.ModelId().
// Read expects a JSON response containing the data for the requested model if the
// request was successful, in which case it will mutate model by setting the fields
// to the values in the JSON response. Since model may be mutated, it should be
// a pointer.
func (c *Client) Read(id string, model Model) error {
fullURL := model.RootURL() + "/" + id
return c.sendRequestAndUnmarshal("GET", fullURL, "", model)
}
// ReadAll sends an http request to get all the models of a particular
// type from the server (e.g. get all the todos). It sends a GET request to
// model.RootURL(). ReadAll expects a JSON response containing an array of objects,
// where each object contains data for one model. models must be a pointer to a slice
// of some type which implements Model. ReadAll will mutate models by growing or shrinking
// the slice as needed, and by setting the fields of each element to the values in the JSON
// response.
func (c *Client) ReadAll(models interface{}) error {
rootURL, err := getURLFromModels(models)
if err != nil {
return err
}
return c.sendRequestAndUnmarshal("GET", rootURL, "", models)
}
// Update sends an http request to update an existing model, i.e. to change some or all
// of the fields. It uses reflection to convert the fields of model to the proper encoding.
// Then it sends a PATCH request to model.RootURL() with the encoded data in the body and
// the appropriate Content-Type header. Update expects a JSON response containing the data
// for the updated model if the request was successful, in which case it will mutate model
// by setting the fields to the values in the JSON response. Since model may be mutated,
// it should be a pointer.
func (c *Client) Update(model Model) error {
fullURL := model.RootURL() + "/" + model.ModelId()
encodedModelData, err := c.encodeFields(model)
if err != nil {
return err
}
return c.sendRequestAndUnmarshal("PATCH", fullURL, encodedModelData, model)
}
// Delete sends an http request to delete an existing model. It sends a DELETE request
// to model.RootURL() + "/" + model.ModelId(). DELETE will not do anything with the
// response from the server and will not mutate model.
func (c *Client) Delete(model Model) error {
fullURL := model.RootURL() + "/" + model.ModelId()
req, err := http.NewRequest("DELETE", fullURL, nil)
if err != nil {
return fmt.Errorf("Something went wrong building DELETE request to %s: %s", fullURL, err.Error())
}
if _, err := http.DefaultClient.Do(req); err != nil {
return fmt.Errorf("Something went wrong with DELETE request to %s: %s", fullURL, err.Error())
}
return nil
}
// getURLFromModels returns the url that should be used for the type that corresponds
// to models. It does this by instantiating a new model of the correct type and then
// calling RootURL on it. models should be a pointer to a slice of models.
func getURLFromModels(models interface{}) (string, error) {
// Check the type of models
typ := reflect.TypeOf(models)
switch {
// Make sure its a pointer
case typ.Kind() != reflect.Ptr:
return "", fmt.Errorf("models must be a pointer to a slice of models. %T is not a pointer.", models)
// Make sure its a pointer to a slice
case typ.Elem().Kind() != reflect.Slice:
return "", fmt.Errorf("models must be a pointer to a slice of models. %T is not a pointer to a slice", models)
// Make sure the type of the elements of the slice implement Model
case !typ.Elem().Elem().Implements(reflect.TypeOf([]Model{}).Elem()):
return "", fmt.Errorf("models must be a pointer to a slice of models. The elem type %s does not implement model", typ.Elem().Elem().String())
}
// modelType is the type of the elements of models
modelType := typ.Elem().Elem()
// Ultimately, we need to be able to instantiate a new object of a type that
// implements Model so that we can call RootURL on it. The trouble is that
// reflect.New only works for things that are not pointers, and the type of
// the elements of models could be pointers. To solve for this, we are going
// to get the Elem of modelType if it is a pointer and keep track of the number
// of times we get the Elem. So if modelType is *Todo, we'll call Elem once to
// get the type Todo.
numDeref := 0
for modelType.Kind() == reflect.Ptr {
modelType = modelType.Elem()
numDeref += 1
}
// Now that we have the underlying type that is not a pointer, we can instantiate
// a new object with reflect.New.
newModelVal := reflect.New(modelType).Elem()
// Now we need to iteratively get the address of the object we created exactly
// numDeref times to get to a type that implements Model. Note that Addr is the
// inverse of Elem.
for i := 0; i < numDeref; i++ {
newModelVal = newModelVal.Addr()
}
// Now we can use a type assertion to convert the object we instantiated to a Model
newModel := newModelVal.Interface().(Model)
// Finally, once we have a Model we can get what we wanted by calling RootURL
return newModel.RootURL(), nil
}
// sendRequestAndUnmarshal constructs a request with the given method, url, and
// data. If data is an empty string, it will construct a request without any
// data in the body. If data is a non-empty string, it will send it as the body
// of the request and set the Content-Type header depending on what contentType has
// been set to. Then sendRequestAndUnmarshal sends the request using http.DefaultClient
// and marshals the response into v using the json package.
// TODO: do something if the response status code is non-200.
func (c *Client) sendRequestAndUnmarshal(method string, url string, data string, v interface{}) error {
// Build the request
var reqBody io.Reader = nil
if data != "" {
reqBody = strings.NewReader(data)
}
req, err := http.NewRequest(method, url, reqBody)
if err != nil {
return fmt.Errorf("Something went wrong building %s request to %s: %s", method, url, err.Error())
}
// Set the Content-Type header only if data was provided
if data != "" {
req.Header.Set("Content-Type", string(c.ContentType))
}
// Specify that we want json as the response type. This is especially useful
// for applications which share things between client and server
req.Header.Set("Accept", "application/json")
// Send the request using the default client
res, err := http.DefaultClient.Do(req)
if err != nil {
return fmt.Errorf("Something went wrong with %s request to %s: %s", req.Method, req.URL.String(), err.Error())
}
// Check if the status code is 2xx, indicating success
if res.StatusCode/100 != 2 {
return newHTTPError(res)
}
// Unmarshal the response into v
body, err := ioutil.ReadAll(res.Body)
if err != nil {
return fmt.Errorf("Couldn't read response to %s: %s", res.Request.URL.String(), err.Error())
}
return json.Unmarshal(body, v)
}
// encodeFields encodes the fields using either json encoding or url encoding, depending
// on the value of contentType.
func (c *Client) encodeFields(model Model) (string, error) {
switch c.ContentType {
case ContentURLEncoded:
return urlEncodeFields(model)
case ContentJSON:
data, err := json.Marshal(model)
return string(data), err
default:
return "", fmt.Errorf("rest: don't know how to handle ContentType: %s", c.ContentType)
}
}
// urlEncodeFields returns the fields of model represented as a url-encoded string.
// Suitable for POST requests with a content type of application/x-www-form-urlencoded.
// It returns an error if model is a nil pointer or if it is not a struct or a pointer
// to a struct. Any fields that are nil will not be added to the url-encoded string.
func urlEncodeFields(model Model) (string, error) {
modelVal := reflect.ValueOf(model)
// dereference the pointer until we reach the underlying struct value.
for modelVal.Kind() == reflect.Ptr {
if modelVal.IsNil() {
return "", errors.New("Error encoding model as url-encoded data: model was a nil pointer.")
}
modelVal = modelVal.Elem()
}
// Make sure the type of model after dereferencing is a struct.
if modelVal.Kind() != reflect.Struct {
return "", fmt.Errorf("Error encoding model as url-encoded data: model must be a struct or a pointer to a struct.")
}
values := url.Values{}
for i := 0; i < modelVal.Type().NumField(); i++ {
field := modelVal.Type().Field(i)
fieldValue := modelVal.FieldByName(field.Name)
valueStr, err := encodeString(fieldValue)
if err != nil {
if err == nilFieldError {
// If there was a nil field, continue without adding the field
// to the encoded data.
continue
}
// We should return any other kind of error
return "", err
}
values.Add(field.Name, valueStr)
}
return values.Encode(), nil
}
var nilFieldError = errors.New("field was nil")
// encodeString converts the given value to a string. It returns an error if
// value has a type which is unsupported. It returns a special error
// (nilFieldError) if a field has a value of nil. The supported types are int
// and its variants (int64, int32, etc.), uint and its variants (uint64, uint32,
// etc.), float32, float64, bool, string, and []byte.
func encodeString(value reflect.Value) (string, error) {
for value.Kind() == reflect.Ptr {
if value.IsNil() {
// Skip nil fields
return "", nilFieldError
}
value = value.Elem()
}
switch v := value.Interface().(type) {
case int, int64, int32, int16, int8, uint, uint64, uint32, uint16, uint8,
float64, float32, bool:
return fmt.Sprint(v), nil
case string:
return v, nil
case []byte:
return string(v), nil
default:
return "", fmt.Errorf("Error encoding model as url-encoded data: Don't know how to convert %v of type %T to a string.", v, v)
}
}