-
Notifications
You must be signed in to change notification settings - Fork 12
/
client.go
169 lines (136 loc) · 4.18 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
package jsc
import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"strconv"
"strings"
"github.com/derekdowling/go-json-spec-handler"
)
/*
Document validates the HTTP response and attempts to parse a JSON API compatible
Document from the response body before closing it.
*/
func Document(response *http.Response, mode jsh.DocumentMode) (*jsh.Document, *jsh.Error) {
document, err := buildParser(response).Document(response.Body, mode)
if err != nil {
return nil, err
}
document.Status = response.StatusCode
return document, nil
}
/*
DumpBody is a convenience function that parses the body of the response into a
string BUT DOESN'T close the ReadCloser. Useful for debugging.
*/
func DumpBody(response *http.Response) (string, *jsh.Error) {
byteData, err := ioutil.ReadAll(io.LimitReader(response.Body, jsh.MaxContentLength))
if err != nil {
return "", jsh.ISE(fmt.Sprintf("Error attempting to read request body: %s", err.Error()))
}
return string(byteData), nil
}
func buildParser(response *http.Response) *jsh.Parser {
return &jsh.Parser{
Method: "",
Headers: response.Header,
}
}
/*
setPath builds a JSON url.Path for a given resource type.
*/
func setPath(url *url.URL, resource string) {
// ensure that path is "/" terminated before concatting resource
if url.Path != "" && !strings.HasSuffix(url.Path, "/") {
url.Path = url.Path + "/"
}
// don't pluralize resource automagically, JSON API spec is agnostic
url.Path = fmt.Sprintf("%s%s", url.Path, resource)
}
/*
setIDPath creates a JSON url.Path for a specific resource type including an
ID specifier.
*/
func setIDPath(url *url.URL, resource string, id string) {
setPath(url, resource)
// concat "/:id" if not empty
if id != "" {
url.Path = strings.Join([]string{url.Path, id}, "/")
}
}
/*
prepareBody first prepares/validates the object to ensure it is JSON
spec compatible, and then marshals it to JSON, sets the request body and
corresponding attributes.
*/
func prepareBody(request *http.Request, object *jsh.Object) error {
err := object.Validate(request, false)
if err != nil {
return fmt.Errorf("Error preparing object: %s", err.Error())
}
doc := jsh.Build(object)
jsonContent, jsonErr := json.MarshalIndent(doc, "", " ")
if jsonErr != nil {
return fmt.Errorf("Unable to prepare JSON content: %s", jsonErr.Error())
}
request.Body = jsh.CreateReadCloser(jsonContent)
request.ContentLength = int64(len(jsonContent))
return nil
}
/*
Do sends a the specified request to a JSON API compatible endpoint and
returns the resulting JSON Document if possible along with the response,
and any errors that were encountered while sending, or parsing the
JSON Document.
Useful in conjunction with any of the method Request builders or
for times when you want to send a request to a custom endpoint, but would still
like a JSONAPI response.
*/
func Do(request *http.Request, mode jsh.DocumentMode) (*jsh.Document, *http.Response, error) {
client := &http.Client{}
response, clientErr := client.Do(request)
if clientErr != nil {
return nil, nil, fmt.Errorf(
"Error sending %s request: %s", request.Method, clientErr.Error(),
)
}
doc, parseErr := ParseResponse(response, mode)
if parseErr != nil {
return nil, response, fmt.Errorf("Error parsing response: %s", parseErr.Error())
}
return doc, response, parseErr
}
/*
ParseResponse handles parsing an HTTP response into a JSON Document if
possible.
*/
func ParseResponse(response *http.Response, mode jsh.DocumentMode) (*jsh.Document, error) {
skipCodes := []int{
http.StatusNoContent,
http.StatusNotFound,
}
for _, code := range skipCodes {
if code == response.StatusCode {
return nil, nil
}
}
document, err := Document(response, mode)
if err != nil {
return nil, err
}
return document, nil
}
// NewRequest builds a basic request object with the necessary configurations to
// achieve JSON API compatibility
func NewRequest(method string, urlStr string, body io.Reader) (*http.Request, error) {
request, err := http.NewRequest(method, urlStr, body)
if err != nil {
return nil, err
}
request.Header.Set("Content-Type", jsh.ContentType)
request.Header.Set("Content-Length", strconv.Itoa(int(request.ContentLength)))
return request, err
}