304 lines
8.7 KiB
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)
|
|
}
|