| 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. |
| // |
| // Generally, you acquire a ProviderClient by calling the NewClient method in |
| // the appropriate provider's child package, providing whatever authentication |
| // credentials are required. |
| type ProviderClient struct { |
| // IdentityBase is the base URL used for a particular provider's identity |
| // service - it will be used when issuing authenticatation requests. It |
| // should point to the root resource of the identity service, not a specific |
| // identity version. |
| IdentityBase string |
| |
| // IdentityEndpoint is the identity endpoint. This may be a specific version |
| // of the identity service. If this is the case, this endpoint is used rather |
| // than querying versions first. |
| IdentityEndpoint string |
| |
| // TokenID is the ID of the most recently issued valid token. |
| TokenID string |
| |
| // 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.Set("Content-Type", *contentType) |
| } |
| req.Header.Set("Accept", applicationJSON) |
| |
| for k, v := range client.AuthenticatedHeaders() { |
| req.Header.Add(k, v) |
| } |
| |
| if options.MoreHeaders != nil { |
| for k, v := range options.MoreHeaders { |
| fmt.Printf("Applying header [%s: %v]\n", k, v) |
| if v != "" { |
| req.Header.Set(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 |
| } |