teslainventory_sdk/client.go

304 lines
8.7 KiB
Go

package teslainventory_sdk
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"sync/atomic"
)
// HTTPClient is an interface which declares the functionality we need from an
// HTTP client. This is to allow consumers to provide their own HTTP client as
// needed, without restricting them to only using *http.Client.
type HTTPClient interface {
Do(*http.Request) (*http.Response, error)
}
// Client wraps http client.
type Client struct {
debugFlag *uint64
lastRequest *atomic.Value
lastResponse *atomic.Value
HTTPClient HTTPClient
userAgent string
endpoint string
headers map[string]string
}
// NewClient create a new tesla-inventory client with default HTTP Client.
func NewClient(options ...ClientOptions) *Client {
client := Client{
debugFlag: new(uint64),
lastRequest: &atomic.Value{},
lastResponse: &atomic.Value{},
HTTPClient: http.DefaultClient,
endpoint: inventoryApiEndpoint,
userAgent: defaultUserAgent,
}
for _, opt := range options {
opt(&client)
}
return &client
}
// SetDebugFlag sets the DebugFlag of the client, which are just bit flags that
// tell the client how to behave. They can be bitwise-ORed together to enable
// multiple behaviors.
func (c *Client) SetDebugFlag(flag DebugFlag) {
atomic.StoreUint64(c.debugFlag, uint64(flag))
}
func (c *Client) debugCaptureRequest() bool {
return atomic.LoadUint64(c.debugFlag)&uint64(DEBUG_FLAG_CAPTURE_LAST_REQUEST) > 0
}
func (c *Client) debugCaptureResponse() bool {
return atomic.LoadUint64(c.debugFlag)&uint64(DEBUG_FLAG_CAPTURE_LAST_RESPONSE) > 0
}
// LastAPIRequest returns the last request sent to the API, if enabled. This can
// be turned on by using the SetDebugFlag() method while providing the
// DebugCaptureLastRequest flag.
//
// The bool value returned from this method is false if the request is unset or
// is nil. If there was an error prepping the request to be sent to the server,
// there will be no *http.Request to capture so this will return (<nil>, false).
//
// This is meant to help with debugging unexpected API interactions, so most
// won't need to use it. Also, callers will need to ensure the *Client isn't
// used concurrently, otherwise they might receive another method's *http.Request
// and not the one they anticipated.
//
// The *http.Request made within the Do() method is not captured by the client,
// and thus won't be returned by this method.
func (c *Client) LastAPIRequest() (*http.Request, bool) {
v := c.lastRequest.Load()
if v == nil {
return nil, false
}
// comma ok idiom omitted, if this is something else explode
if r, ok := v.(*http.Request); ok && r != nil {
return r, true
}
return nil, false
}
// LastAPIResponse returns the last response received from the API, if enabled.
// This can be turned on by using the SetDebugFlag() method while providing the
// DebugCaptureLastResponse flag.
//
// The bool value returned from this method is false if the response is unset or
// is nil. If the HTTP exchange failed (e.g., there was a connection error)
// there will be no *http.Response to capture so this will return (<nil>,
// false).
//
// This is meant to help with debugging unexpected API interactions, so most
// won't need to use it. Also, callers will need to ensure the *Client isn't
// used concurrently, otherwise they might receive another method's *http.Response
// and not the one they anticipated.
//
// The *http.Response from the Do() method is not captured by the client, and thus
// won't be returned by this method.
func (c *Client) LastAPIResponse() (*http.Response, bool) {
v := c.lastResponse.Load()
if v == nil {
return nil, false
}
// comma ok idiom omitted, if this is something else explode
if r, ok := v.(*http.Response); ok && r != nil {
return r, true
}
return nil, false
}
//nolint:unused // keeping to respect classical http verb.
func (c *Client) delete(ctx context.Context, path string, payload interface{}, headers map[string]string) (*http.Response, error) {
if payload != nil {
data, err := json.Marshal(payload)
if err != nil {
return nil, err
}
return c.do(ctx, http.MethodDelete, path, bytes.NewBuffer(data), headers)
}
return c.do(ctx, http.MethodDelete, path, nil, headers)
}
//nolint:unused // keeping to respect classical http verb.
func (c *Client) put(ctx context.Context, path string, payload interface{}, headers map[string]string) (*http.Response, error) {
if payload != nil {
data, err := json.Marshal(payload)
if err != nil {
return nil, err
}
return c.do(ctx, http.MethodPut, path, bytes.NewBuffer(data), headers)
}
return c.do(ctx, http.MethodPut, path, nil, headers)
}
//nolint:unused // keeping to respect classical http verb.
func (c *Client) post(ctx context.Context, path string, payload interface{}, headers map[string]string) (*http.Response, error) {
data, err := json.Marshal(payload)
if err != nil {
return nil, err
}
return c.do(ctx, http.MethodPost, path, bytes.NewBuffer(data), headers)
}
func (c *Client) get(ctx context.Context, path string, headers map[string]string) (*http.Response, error) {
return c.do(ctx, http.MethodGet, path, nil, headers)
}
func dupeRequest(r *http.Request) (*http.Request, error) {
dreq := r.Clone(r.Context())
if r.Body != nil {
data, err := io.ReadAll(r.Body)
if err != nil {
return nil, fmt.Errorf("failed to copy request body: %w", err)
}
_ = r.Body.Close()
r.Body = io.NopCloser(bytes.NewReader(data))
dreq.Body = io.NopCloser(bytes.NewReader(data))
}
return dreq, nil
}
// Do allow anyone to extend sdk.
func (c *Client) Do(r *http.Request) (*http.Response, error) {
c.prepareRequest(r, nil)
return c.HTTPClient.Do(r)
}
func (c *Client) do(ctx context.Context, method, path string, body io.Reader, headers map[string]string) (*http.Response, error) {
var dreq *http.Request
var resp *http.Response
// so that the last request and response can be nil if there was an error
// before the request could be fully processed by the origin, we defer these
// calls here
if c.debugCaptureResponse() {
defer func() {
c.lastResponse.Store(resp)
}()
}
if c.debugCaptureRequest() {
defer func() {
c.lastRequest.Store(dreq)
}()
}
req, err := http.NewRequestWithContext(ctx, method, c.endpoint+path, body)
if err != nil {
return nil, fmt.Errorf("failed to build request: %w", err)
}
c.prepareRequest(req, headers)
// if in debug mode, copy request before making it
if c.debugCaptureRequest() {
if dreq, err = dupeRequest(req); err != nil {
return nil, fmt.Errorf("failed to duplicate request for debug capture: %w", err)
}
}
resp, err = c.HTTPClient.Do(req)
return c.checkResponse(resp, err)
}
func (c *Client) prepareRequest(req *http.Request, headers map[string]string) {
// set permanent headers provided at the creation of the client
for k, v := range c.headers {
req.Header.Set(k, v)
}
// adding / overiding headers those set for this request
for k, v := range headers {
req.Header.Set(k, v)
}
req.Header.Set("User-Agent", c.userAgent)
}
func (c *Client) checkResponse(resp *http.Response, err error) (*http.Response, error) {
if err != nil {
return resp, fmt.Errorf("error calling the API endpoint: %w", err)
}
// Inventory API always return a 200
if resp.StatusCode != http.StatusOK {
return resp, c.getErrorFromResponse(resp)
}
return resp, nil
}
func (c *Client) getErrorFromResponse(resp *http.Response) APIError {
// check whether the error response is declared as JSON
if !strings.HasPrefix(resp.Header.Get("Content-Type"), "application/json") {
defer func() {
if err := resp.Body.Close(); err != nil {
fmt.Printf("Error closing body: %s", err)
}
}()
aerr := APIError{
StatusCode: resp.StatusCode,
Message: fmt.Sprintf("HTTP response with status code %d does not contain Content-Type: application/json", resp.StatusCode),
}
return aerr
}
var document APIError
// because of above check this probably won't fail, but it's possible...
if err := c.decodeJSON(resp, &document); err != nil {
aerr := APIError{
StatusCode: resp.StatusCode,
Message: fmt.Sprintf("HTTP response with status code %d, JSON error object decode failed: %s", resp.StatusCode, err),
}
return aerr
}
document.StatusCode = resp.StatusCode
return document
}
func (c *Client) decodeJSON(resp *http.Response, payload interface{}) error {
// close the original response body, and not the copy we may make if
// debugCaptureResponse is true
orb := resp.Body
defer func() { _ = orb.Close() }() // explicitly discard error
body, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("failed to read response body: %w", err)
}
if c.debugCaptureResponse() { // reset body as we capture the response elsewhere
resp.Body = io.NopCloser(bytes.NewReader(body))
}
return json.Unmarshal(body, payload)
}