From 0bc0feb4bb20c6d79b06cd8450f41b90bba8f987 Mon Sep 17 00:00:00 2001 From: Joel Speed Date: Fri, 3 Jul 2020 18:38:26 +0100 Subject: [PATCH] Add request builder to simplify request handling --- pkg/requests/builder.go | 173 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 173 insertions(+) create mode 100644 pkg/requests/builder.go diff --git a/pkg/requests/builder.go b/pkg/requests/builder.go new file mode 100644 index 00000000..2e1a358e --- /dev/null +++ b/pkg/requests/builder.go @@ -0,0 +1,173 @@ +package requests + +import ( + "context" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "net/http" + + "github.com/bitly/go-simplejson" +) + +// Builder allows users to construct a request and then either get the requests +// response via Do(), parse the response into a simplejson.Json via JSON(), +// or to parse the json response into an object via UnmarshalInto(). +type Builder interface { + WithContext(context.Context) Builder + WithBody(io.Reader) Builder + WithMethod(string) Builder + WithHeaders(http.Header) Builder + SetHeader(key, value string) Builder + Do() (*http.Response, error) + UnmarshalInto(interface{}) error + UnmarshalJSON() (*simplejson.Json, error) +} + +type builder struct { + context context.Context + method string + endpoint string + body io.Reader + header http.Header + response *http.Response +} + +// New provides a new Builder for the given endpoint. +func New(endpoint string) Builder { + return &builder{ + endpoint: endpoint, + method: "GET", + } +} + +// WithContext adds a context to the request. +// If no context is provided, context.Background() is used instead. +func (r *builder) WithContext(ctx context.Context) Builder { + r.context = ctx + return r +} + +// WithBody adds a body to the request. +func (r *builder) WithBody(body io.Reader) Builder { + r.body = body + return r +} + +// WithMethod sets the request method. Defaults to "GET". +func (r *builder) WithMethod(method string) Builder { + r.method = method + return r +} + +// WithHeaders replaces the request header map with the given header map. +func (r *builder) WithHeaders(header http.Header) Builder { + r.header = header + return r +} + +// SetHeader sets a single header to the given value. +// May be used to add multiple headers. +func (r *builder) SetHeader(key, value string) Builder { + if r.header == nil { + r.header = make(http.Header) + } + r.header.Set(key, value) + return r +} + +// Do performs the request and returns the response in its raw form. +// If the request has already been performed, returns the previous result. +// This will not allow you to repeat a request. +func (r *builder) Do() (*http.Response, error) { + if r.response != nil { + // Request has already been done + return r.response, nil + } + + // Must provide a non-nil context to NewRequestWithContext + if r.context == nil { + r.context = context.Background() + } + + req, err := http.NewRequestWithContext(r.context, r.method, r.endpoint, r.body) + if err != nil { + return nil, fmt.Errorf("error creating request: %v", err) + } + req.Header = r.header + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, fmt.Errorf("error performing request: %v", err) + } + + r.response = resp + return resp, nil +} + +// UnmarshalInto performs the request and attempts to unmarshal the response into the +// the given interface. The response body is assumed to be JSON. +// The response must have a 200 status otherwise an error will be returned. +func (r *builder) UnmarshalInto(into interface{}) error { + resp, err := r.Do() + if err != nil { + return err + } + + return UnmarshalInto(resp, into) +} + +// UnmarshalJSON performs the request and attempts to unmarshal the response into a +// simplejson.Json. The response body is assume to be JSON. +// The response must have a 200 status otherwise an error will be returned. +func (r *builder) UnmarshalJSON() (*simplejson.Json, error) { + resp, err := r.Do() + if err != nil { + return nil, err + } + + body, err := getResponseBody(resp) + if err != nil { + return nil, err + } + + data, err := simplejson.NewJson(body) + if err != nil { + return nil, fmt.Errorf("error reading json: %v", err) + } + return data, nil +} + +// UnmarshalInto attempts to unmarshal the response into the the given interface. +// The response body is assumed to be JSON. +// The response must have a 200 status otherwise an error will be returned. +func UnmarshalInto(resp *http.Response, into interface{}) error { + body, err := getResponseBody(resp) + if err != nil { + return err + } + + if err := json.Unmarshal(body, into); err != nil { + return fmt.Errorf("error unmarshalling body: %v", err) + } + + return nil +} + +// getResponseBody extracts the response body, but will only return the body +// if the response was successful. +func getResponseBody(resp *http.Response) ([]byte, error) { + defer resp.Body.Close() + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("error reading response body: %v", err) + } + + // Only unmarshal body if the response was successful + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("unexpected status \"%d\": %s", resp.StatusCode, body) + } + + return body, nil +}