Create the Request method.
diff --git a/provider_client.go b/provider_client.go
index 7754c20..618a0ef 100644
--- a/provider_client.go
+++ b/provider_client.go
@@ -1,5 +1,14 @@
package gophercloud
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "net/http"
+)
+
// ProviderClient stores details that are required to interact with any
// services within a specific provider's API.
//
@@ -24,10 +33,154 @@
// EndpointLocator describes how this provider discovers the endpoints for
// its constituent services.
EndpointLocator EndpointLocator
+
+ // HTTPClient allows users to interject arbitrary http, https, or other transit behaviors.
+ HTTPClient http.Client
}
// AuthenticatedHeaders returns a map of HTTP headers that are common for all
// authenticated service requests.
func (client *ProviderClient) AuthenticatedHeaders() map[string]string {
+ if client.TokenID == "" {
+ return map[string]string{}
+ }
return map[string]string{"X-Auth-Token": client.TokenID}
}
+
+// RequestOpts customizes the behavior of the provider.Request() method.
+type RequestOpts struct {
+ // JSONBody, if provided, will be encoded as JSON and used as the body of the HTTP request. The
+ // content type of the request will default to "application/json" unless overridden by MoreHeaders.
+ // It's an error to specify both a JSONBody and a RawBody.
+ JSONBody interface{}
+ // RawBody contains an io.Reader that will be consumed by the request directly. No content-type
+ // will be set unless one is provided explicitly by MoreHeaders.
+ RawBody io.Reader
+
+ // JSONResponse, if provided, will be populated with the contents of the response body parsed as
+ // JSON.
+ JSONResponse *interface{}
+ // OkCodes contains a list of numeric HTTP status codes that should be interpreted as success. If
+ // the response has a different code, an error will be returned.
+ OkCodes []int
+
+ // MoreHeaders specifies additional HTTP headers to be provide on the request. If a header is
+ // provided with a blank value (""), that header will be *omitted* instead: use this to suppress
+ // the default Accept header or an inferred Content-Type, for example.
+ MoreHeaders map[string]string
+}
+
+// UnexpectedResponseCodeError is returned by the Request method when a response code other than
+// those listed in OkCodes is encountered.
+type UnexpectedResponseCodeError struct {
+ URL string
+ Method string
+ Expected []int
+ Actual int
+ Body []byte
+}
+
+func (err *UnexpectedResponseCodeError) Error() string {
+ return fmt.Sprintf(
+ "Expected HTTP response code %v when accessing [%s %s], but got %d instead\n%s",
+ err.Expected, err.Method, err.URL, err.Actual, err.Body,
+ )
+}
+
+var applicationJSON = "application/json"
+
+// Request performs an HTTP request using the ProviderClient's current HTTPClient. An authentication
+// header will automatically be provided.
+func (client *ProviderClient) Request(method, url string, options RequestOpts) (*http.Response, error) {
+ var body io.Reader
+ var contentType *string
+
+ // Derive the content body by either encoding an arbitrary object as JSON, or by taking a provided
+ // io.Reader as-is. Default the content-type to application/json.
+
+ if options.JSONBody != nil {
+ if options.RawBody != nil {
+ panic("Please provide only one of JSONBody or RawBody to gophercloud.Request().")
+ }
+
+ rendered, err := json.Marshal(options.JSONBody)
+ if err != nil {
+ return nil, err
+ }
+
+ body = bytes.NewReader(rendered)
+ contentType = &applicationJSON
+ }
+
+ if options.RawBody != nil {
+ body = options.RawBody
+ }
+
+ // Construct the http.Request.
+
+ req, err := http.NewRequest(method, url, body)
+ if err != nil {
+ return nil, err
+ }
+
+ // Populate the request headers. Apply options.MoreHeaders last, to give the caller the chance to
+ // modify or omit any header.
+
+ if contentType != nil {
+ req.Header.Add("Content-Type", *contentType)
+ }
+ req.Header.Add("Accept", applicationJSON)
+
+ for k, v := range client.AuthenticatedHeaders() {
+ req.Header.Add(k, v)
+ }
+
+ if options.MoreHeaders != nil {
+ for k, v := range options.MoreHeaders {
+ if v != "" {
+ req.Header.Add(k, v)
+ } else {
+ req.Header.Del(k)
+ }
+ }
+ }
+
+ // Issue the request.
+
+ resp, err := client.HTTPClient.Do(req)
+ if err != nil {
+ return nil, err
+ }
+
+ // Validate the response code, if requested to do so.
+
+ if options.OkCodes != nil {
+ var ok bool
+ for _, code := range options.OkCodes {
+ if resp.StatusCode == code {
+ ok = true
+ break
+ }
+ }
+ if !ok {
+ body, _ := ioutil.ReadAll(resp.Body)
+ resp.Body.Close()
+ return resp, &UnexpectedResponseCodeError{
+ URL: url,
+ Method: method,
+ Expected: options.OkCodes,
+ Actual: resp.StatusCode,
+ Body: body,
+ }
+ }
+ }
+
+ // Parse the response body as JSON, if requested to do so.
+
+ if options.JSONResponse != nil {
+ defer resp.Body.Close()
+ json.NewDecoder(resp.Body).Decode(options.JSONResponse)
+ }
+
+ return resp, nil
+}