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 } // NewClient create a new tesla-stock client with default HTTP Client. func NewClient(options ...ClientOptions) *Client { client := Client{ debugFlag: new(uint64), lastRequest: &atomic.Value{}, lastResponse: &atomic.Value{}, HTTPClient: http.DefaultClient, 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 (, 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 (, // 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 } 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) } 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) } 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 } func (c *Client) Do(r *http.Request, authRequired bool) (*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, stockApiEndpoint+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) { 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) } // Stock 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) }