commit 05ac49fd68940a2b0af10085c791a746cb4aa437 Author: Matthieu 'JP' DERASSE Date: Wed Aug 9 12:05:37 2023 +0000 feat(first): First commit with basic implementation. Missing test diff --git a/.drone.yml b/.drone.yml new file mode 100644 index 0000000..2aad105 --- /dev/null +++ b/.drone.yml @@ -0,0 +1,116 @@ +--- +kind: pipeline +type: docker +name: test-pipeline + +platform: + os: linux + arch: amd64 + +steps: +- name: environment + image: golang:1.19 + commands: + - go version + - go env + volumes: + - name: gopath + path: /go + +- name: tools + image: golang:1.19 + commands: + - curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.50.1 + - go install github.com/tebeka/go2xunit@latest + - go install github.com/t-yuki/gocover-cobertura@latest + volumes: + - name: gopath + path: /go + depends_on: + - environment + +- name: tidy + image: golang:1.19 + commands: + - go mod tidy + - git diff --exit-code -- go.mod go.sum + volumes: + - name: gopath + path: /go + depends_on: + - tools + +- name: lint + image: golang:1.19 + commands: + - echo 'Running linting' + - golangci-lint run + volumes: + - name: gopath + path: /go + depends_on: + - tools + +- name: test + image: golang:1.19 + commands: + - go test -cover -v ./... + volumes: + - name: gopath + path: /go + depends_on: + - tools + +- name: send telegram notification + image: appleboy/drone-telegram + settings: + token: + from_secret: telegram_token + to: + from_secret: telegram_chat_id + message: > + {{#success build.status}} + ✅ Build *#{{build.number}}* of *{{repo.name}}* succeeded. + {{else}} + ❌ Build *#{{build.number}}* of *{{repo.name}}* failed. + {{/success}} + + 📝 Commit on *{{commit.branch}}*: + + ``` {{commit.message}} ``` + + 🌐 {{ build.link }} + format: markdown + when: + status: + - failure + - success + depends_on: + - test + - lint + - tidy + +- name: docker + image: plugins/docker + settings: + dockerfile: Dockerfile + repo: registry.dev.m-and-m.ovh/m-and-m/tesla + username: + from_secret: registry_username + password: + from_secret: registry_password + registry: registry.dev.m-and-m.ovh + debug: true + tags: + - latest + when: + status: + - success + depends_on: + - test + - lint + - tidy + +volumes: +- name: gopath + temp: {} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a037142 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +/.vscode/ + +.DS_Store +.idea +*.i* \ No newline at end of file diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..ad29bf7 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,210 @@ +--- +# options for analysis running +run: + # timeout for analysis, e.g. 30s, 5m, default is 1m + timeout: 2m + +issues: + exclude: + - ST1000 + - ST1003 + - var-naming + - package-comments + # Excluding configuration per-path, per-linter, per-text and per-source + exclude-rules: + # Exclude some linters from running on tests files. + - path: _test\.go + linters: + - gocyclo + - errcheck + - dupl + - gosec + # Exclude known linters from partially hard-vendored code, + # which is impossible to exclude via `nolint` comments. + - path: internal/hmac/ + text: "weak cryptographic primitive" + linters: + - gosec + # Exclude `lll` issues for long lines with `go:generate`. + - linters: + - lll + source: "^//go:generate " + + exclude-use-default: false + + # Maximum count of issues with the same text. + # Set to 0 to disable. + # Default: 3 + max-same-issues: 3 + # Maximum issues count per one linter. + # Set to 0 to disable. + # Default: 50 + max-issues-per-linter: 50 + + fix: false + +output: + sort-results: true + +# Uncomment and add a path if needed to exclude +# skip-dirs: +# - some/path +# skip-files: +# - ".*\\.my\\.go$" +# - lib/bad.go + +# Find the whole list here https://golangci-lint.run/usage/linters/ +linters: + disable-all: true + enable: + - asciicheck # simple linter to check that your code does not contain non-ASCII identifiers + - bidichk + - bodyclose # checks whether HTTP response body is closed successfully + - containedctx + - decorder + - depguard + - dupl # tool for code clone detection + - durationcheck # check for two durations multiplied together + - errcheck # checking for unchecked errors in go programs + - errorlint # errorlint is a linter for that can be used to find code that will cause problems with the error wrapping scheme introduced in Go 1.13. + - execinquery + - exhaustive + - exportloopref # checks for pointers to enclosing loop variables + - goconst # finds repeated strings that could be replaced by a constant + - godot + - godox # tool for detection of FIXME, TODO and other comment keywords + - gofmt + - goimports # Goimports does everything that gofmt does. Additionally it checks unused imports + - gomoddirectives # manage the use of 'replace', 'retract', and 'excludes' directives in go.mod. + - gomodguard # check for blocked dependencies + - gosec # inspects source code for security problems + - gosimple # linter for Go source code that specializes in simplifying a code + - govet # Vet examines Go source code and reports suspicious constructs, such as Printf calls whose arguments do not align with the format string + - importas # enforces consistent import aliases + - ineffassign # detects when assignments to existing variables are not used + - makezero + - misspell # finds commonly misspelled English words in comments + - nakedret # finds naked returns in functions greater than a specified function length + - nilerr # finds the code that returns nil even if it checks that the error is not nil. + - nilnil + - noctx # noctx finds sending http request without context.Context + - nolintlint # reports ill-formed or insufficient nolint directives + - nonamedreturns + - prealloc # finds slice declarations that could potentially be preallocated + # Disable has we have a lot of enum. TODO: find a better way + # - revive + # Disable because of generic - sqlclosecheck + - staticcheck # Staticcheck is a go vet on steroids, applying a ton of static analysis checks + - stylecheck # a replacement for golint + - typecheck # Like the front-end of a Go compiler, parses and type-checks Go code + - unconvert # Remove unnecessary type conversions + - unparam # reports unused function parameters + - unused # checks Go code for unused constants, variables, functions and types + # Disable because of generic - wastedassign # wastedassign finds wasted assignment statements. + +# all available settings of specific linters +linters-settings: + errcheck: + # report about not checking of errors in type assertions: `a := b.(MyStruct)`; + # default is false: such cases aren't reported by default. + check-type-assertions: true + + errorlint: + # Check whether fmt.Errorf uses the %w verb for formatting errors. See the readme for caveats + errorf: true + # Check for plain type assertions and type switches + asserts: true + # Check for plain error comparisons + comparison: true + + goconst: + # minimal length of string constant, 3 by default + min-len: 3 + # minimal occurrences count to trigger, 3 by default + min-occurrences: 5 + + dupl: + # tokens count to trigger issue, 150 by default + threshold: 150 + + gomoddirectives: + # Allow local `replace` directives. Default is false. + replace-local: false + + goimports: + local-prefixes: github.com/elastic + + gomodguard: + blocked: + # List of blocked modules. + modules: + # Blocked module. + - github.com/pkg/errors: + # Recommended modules that should be used instead. (Optional) + recommendations: + - errors + - fmt + reason: "This package is deprecated, use `fmt.Errorf` with `%w` instead" + depguard: + list-type:: denylist + # Check the list against standard lib. + include-go-root: true + packages-with-error-message: + - io/ioutil: "The package is deprecated, use `io` or `so` instead." + + gosimple: + # Select the Go version to target. The default is '1.13'. + go: "1.20.7" + + misspell: + # Correct spellings using locale preferences for US or UK. + # Default is to use a neutral variety of English. + # Setting locale to US will correct the British spelling of 'colour' to 'color'. + # locale: US + # ignore-words: + # - IdP + + nakedret: + # make an issue if func has more lines of code than this setting and it has naked returns; default is 30 + max-func-lines: 0 + + prealloc: + # Report preallocation suggestions only on simple loops that have no returns/breaks/continues/gotos in them. + # True by default. + simple: true + range-loops: true # Report preallocation suggestions on range loops, true by default + for-loops: false # Report preallocation suggestions on for loops, false by default + + nolintlint: + # Enable to ensure that nolint directives are all used. Default is true. + allow-unused: false + # Disable to ensure that nolint directives don't have a leading space. Default is true. + allow-leading-space: false + # Exclude following linters from requiring an explanation. Default is []. + allow-no-explanation: [] + # Enable to require an explanation of nonzero length after each nolint directive. Default is false. + require-explanation: true + # Enable to require nolint directives to mention the specific linter being suppressed. Default is false. + require-specific: true + + staticcheck: + # Select the Go version to target. The default is '1.13'. + go: "1.20.7" + # https://staticcheck.io/docs/options#checks + checks: ["all"] + + stylecheck: + # Select the Go version to target. The default is '1.13'. + go: "1.20.7" + checks: ["all"] + + unparam: + # Inspect exported functions, default is false. Set to true if no external program/library imports your code. + # XXX: if you enable this setting, unparam will report a lot of false-positives in text editors: + # if it's called for subdir of a project it can't find external interfaces. All text editor integrations + # with golangci-lint call it on a directory with the changed file. + check-exported: false + + unused: + # Select the Go version to target. The default is '1.13'. + go: "1.18.5" \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..f9bf19c --- /dev/null +++ b/Dockerfile @@ -0,0 +1,15 @@ +FROM golang:alpine + +# Set destination for COPY +WORKDIR /app + +# Download Go modules + +COPY . /app +RUN go mod download + +# Build +RUN CGO_ENABLED=0 GOOS=linux go build -o /app/tesla + +# Run +CMD ["/app/tesla"] \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e69de29 diff --git a/README.md b/README.md new file mode 100644 index 0000000..492b476 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# Tesla Stock Sdk + +[![Build Status](https://drone.dev.m-and-m.ovh/api/badges/mderasse/teslastock-sdk/status.svg?ref=refs/heads/main)](https://drone.dev.m-and-m.ovh/mderasse/teslastock-sdk) diff --git a/client.go b/client.go new file mode 100644 index 0000000..26ba87a --- /dev/null +++ b/client.go @@ -0,0 +1,284 @@ +package teslastocksdk + +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(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) +} diff --git a/client_options.go b/client_options.go new file mode 100644 index 0000000..2d527a1 --- /dev/null +++ b/client_options.go @@ -0,0 +1,11 @@ +package teslastocksdk + +// ClientOptions allows for options to be passed into the Client for customization. +type ClientOptions func(*Client) + +// WithUserAgent will force the usage of a specific user agent. +func WithUserAgent(userAgent string) ClientOptions { + return func(c *Client) { + c.userAgent = userAgent + } +} diff --git a/constant.go b/constant.go new file mode 100644 index 0000000..bfd006a --- /dev/null +++ b/constant.go @@ -0,0 +1,33 @@ +package teslastocksdk + +const ( + // Version is the current version of the sdk. + Version = "1.0.0" + stockApiEndpoint = "" + defaultUserAgent = "TeslaApp/" + Version +) + +// DebugFlag represents a set of debug bit flags that can be bitwise-ORed +// together to configure the different behaviors. This allows us to expand +// functionality in the future without introducing breaking changes. +type DebugFlag uint64 + +const ( + // DEBUG_FLAG_DISABLED disables all debug behaviors. + DEBUG_FLAG_DISABLED DebugFlag = 0 + + // DEBUG_FLAG_CAPTURE_LAST_REQUEST captures the last HTTP request made to the API + // (if there was one) and makes it available via the LastAPIRequest() + // method. + // + // This may increase memory usage / GC, as we'll be making a copy of the + // full HTTP request body on each request and capturing it for inspection. + DEBUG_FLAG_CAPTURE_LAST_REQUEST DebugFlag = 1 << 0 + + // DEBUG_FLAG_CAPTURE_LAST_RESPONSE captures the last HTTP response from the API (if + // there was one) and makes it available via the LastAPIResponse() method. + // + // This may increase memory usage / GC, as we'll be making a copy of the + // full HTTP response body on each request and capturing it for inspection. + DEBUG_FLAG_CAPTURE_LAST_RESPONSE DebugFlag = 1 << 1 +) diff --git a/error.go b/error.go new file mode 100644 index 0000000..6080dd9 --- /dev/null +++ b/error.go @@ -0,0 +1,25 @@ +package teslastocksdk + +import "fmt" + +// APIError represent an API error. +type APIError struct { + StatusCode int `json:"-"` + Message string `json:"error"` +} + +// Error satisfy the error interface. +func (a APIError) Error() string { + if a.Message != "" { + return fmt.Sprintf( + "HTTP response failed with status code %d, error message: %s", + a.StatusCode, + a.Message, + ) + } + + return fmt.Sprintf( + "HTTP response failed with status code %d and no error message", + a.StatusCode, + ) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..15a8834 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module git.dev.m-and-m.ovh/mderasse/teslastock-sdk + +go 1.19 diff --git a/stock.go b/stock.go new file mode 100644 index 0000000..823d0a5 --- /dev/null +++ b/stock.go @@ -0,0 +1,38 @@ +package teslastocksdk + +import ( + "context" + "encoding/json" + "fmt" + "net/url" +) + +// GetAvailabilities return the car availabilities matching with the provided characteristics. +func (c *Client) GetAvailabilities(ctx context.Context, params AvailabilityParams) (*AvailabilitiesResponse, error) { + b, err := json.Marshal(params) + if err != nil { + return nil, fmt.Errorf("fail to marshal availability params. Error: %w", err) + } + + queryParams := url.Values{ + "query": {string(b)}, + } + + resp, err := c.get( + ctx, + fmt.Sprintf("/inventory/api/v1/inventory-results?%s", queryParams.Encode()), + nil, + ) + if err != nil { + return nil, err + } + + defer func() { _ = resp.Body.Close() }() // explicitly discard error + + availabilities := AvailabilitiesResponse{} + if err := json.NewDecoder(resp.Body).Decode(&availabilities); err != nil { + return nil, fmt.Errorf("fail to unmarshal response. Error: %w", err) + } + + return &availabilities, nil +} diff --git a/stock_struct.go b/stock_struct.go new file mode 100644 index 0000000..126c28b --- /dev/null +++ b/stock_struct.go @@ -0,0 +1,434 @@ +package teslastocksdk + +import "time" + +// modelEnum. +type modelEnum string + +const ( + MODEL_3 modelEnum = "m3" + MODEL_Y modelEnum = "my" + MODEL_S modelEnum = "ms" + MODEL_X modelEnum = "mx" +) + +// conditionEnum. +type conditionEnum string + +const ( + CONDITION_NEW conditionEnum = "new" + CONDITION_USED conditionEnum = "used" +) + +// arrangeByEnum allow to order availabilities by a specific element. +type arrangeByEnum string + +const ( + ARRANGE_BY_PRICE arrangeByEnum = "Price" + ARRANGE_BY_ODOMETER arrangeByEnum = "Odometer" + ARRANGE_BY_YEAR arrangeByEnum = "Year" + ARRANGE_BY_DISTANCE arrangeByEnum = "Distance" +) + +// orderByEnum. +type orderByEnum string + +const ( + ORDER_BY_ASC orderByEnum = "asc" + ORDER_BY_DESC orderByEnum = "desc" +) + +/** +/ OPTIONS ENUM +**/ + +// autopilotEnum. +type autopilotEnum string + +const ( + AUTOPILOT_AP autopilotEnum = "AUTOPILOT" + AUTOPILOT_ENHANCED autopilotEnum = "ENHANCED_AUTOPILOT" + AUTOPILOT_FSD autopilotEnum = "AUTOPILOT_FULL_SELF_DRIVING" + AUTOPILOT_ORIGINAL autopilotEnum = "AUTOPILOT_ORIGINAL" +) + +// cabinConfigEnum. +type cabinConfigEnum string + +const ( + CABIN_CONFIG_FIVE cabinConfigEnum = "FIVE" + CABIN_CONFIG_SIX cabinConfigEnum = "SIX" + CABIN_CONFIG_SEVEN cabinConfigEnum = "SEVEN" +) + +// interiorEnum. +type interiorEnum string + +const ( + INTERIOR_CREAM interiorEnum = "CREAM" + INTERIOR_WHITE interiorEnum = "WHITE" + INTERIOR_BLACK interiorEnum = "BLACK" +) + +// paintEnum. +type paintEnum string + +const ( + PAINT_RED paintEnum = "RED" + PAINT_WHITE paintEnum = "WHITE" + PAINT_BLACK paintEnum = "BLACK" + PAINT_GREY paintEnum = "GREY" + PAINT_BLUE paintEnum = "BLUE" + PAINT_SILVER paintEnum = "SILVER" +) + +// steeringWheelEnum. +type steeringWheelEnum string + +const ( + STEERING_WHEEL_YOKE steeringWheelEnum = "STEERING_YOKE" + STEERING_WHEEL_ROUND steeringWheelEnum = "STEERING_ROUND" +) + +// trimEnum. +type trimEnum string + +const ( + TRIM_MS_AWD trimEnum = "MSAWD" + TRIM_MS_PLAID trimEnum = "MSPLAID" + TRIM_M3_RWD trimEnum = "M3RWD" + TRIM_LR_AWD trimEnum = "LRAWD" + TRIM_PERF_AWD trimEnum = "PAWD" + TRIM_MX_PLAID trimEnum = "MXPLAID" + TRIM_MX_PERF trimEnum = "MXPERF" + TRIM_MX_AWD trimEnum = "MXAWD" + TRIM_MX_90D trimEnum = "90D" + TRIM_MY_RWD trimEnum = "MYRWD" +) + +// wheelsEnum. +type wheelsEnum string + +const ( + WHEELS_18 wheelsEnum = "EIGHTEEN" + WHEELS_19 wheelsEnum = "NINETEEN" + WHEELS_20 wheelsEnum = "TWENTY" + WHEELS_21 wheelsEnum = "TWENTY_ONE" + WHEELS_22 wheelsEnum = "TWENTY_TWO" +) + +// AvailabilityParams is the params accepted by the API. +type AvailabilityParams struct { + Query AvailabilityQueryParams `json:"query"` + Offset int `json:"offset"` + Count int `json:"count"` + OutsideOffset int `json:"outsideOffset"` + OutsideSearch bool `json:"outsideSearch"` +} + +// AvailabilityQueryParams are the params to filter results. +type AvailabilityQueryParams struct { + Arrangeby arrangeByEnum `json:"arrangeby"` + Condition conditionEnum `json:"condition"` + Language string `json:"language"` + Lat float64 `json:"lat"` + Lng float64 `json:"lng"` + Market string `json:"market"` + Model modelEnum `json:"model"` + Options OptionsParams `json:"options"` + Order orderByEnum `json:"order"` + Range int `json:"range"` + Region string `json:"region"` + SuperRegion string `json:"super_region"` + Zip string `json:"zip"` +} + +// OptionsParams contain the car option. +type OptionsParams struct { + AdditionalOptions []string `json:"ADL_OPTS"` + Autopilot []autopilotEnum `json:"AUTOPILOT"` + CabinConfig []cabinConfigEnum `json:"CABIN_CONFIG"` + Interior []interiorEnum `json:"INTERIOR"` + Paint []paintEnum `json:"PAINT"` + SteeringWheel []steeringWheelEnum `json:"STEERING_WHEEL"` + Trim []trimEnum `json:"TRIM"` + Wheels []wheelsEnum `json:"WHEELS"` + Year []string `json:"Year"` +} + +/** +/ Response. +**/ + +// Availability represent a available car with its characteristics. +type Availability struct { + InTransit bool `json:"InTransit,omitempty"` + AdlOpts any `json:"ADL_OPTS,omitempty"` + Autopilot []string `json:"AUTOPILOT,omitempty"` + AcquisitionSubType any `json:"AcquisitionSubType,omitempty"` + AcquisitionType any `json:"AcquisitionType,omitempty"` + ActualGAInDate string `json:"ActualGAInDate,omitempty"` + Battery any `json:"BATTERY,omitempty"` + CabinConfig []string `json:"CABIN_CONFIG,omitempty"` + CPORefurbishmentStatus any `json:"CPORefurbishmentStatus,omitempty"` + City string `json:"City,omitempty"` + CompositorViews struct { + FrontView string `json:"frontView,omitempty"` + SideView string `json:"sideView,omitempty"` + InteriorView string `json:"interiorView,omitempty"` + } `json:"CompositorViews,omitempty"` + CountryCode string `json:"CountryCode,omitempty"` + CountryCodes []string `json:"CountryCodes,omitempty"` + CountryHasVehicleAtLocation bool `json:"CountryHasVehicleAtLocation,omitempty"` + CountryOfOrigin string `json:"CountryOfOrigin,omitempty"` + CurrencyCode string `json:"CurrencyCode,omitempty"` + CurrencyCodes string `json:"CurrencyCodes,omitempty"` + Decor any `json:"DECOR,omitempty"` + Drive []string `json:"DRIVE,omitempty"` + DamageDisclosureStatus any `json:"DamageDisclosureStatus,omitempty"` + DestinationHandlingFee int `json:"DestinationHandlingFee,omitempty"` + Discount int `json:"Discount,omitempty"` + DisplayWarranty bool `json:"DisplayWarranty,omitempty"` + EtaToCurrent string `json:"EtaToCurrent,omitempty"` + FactoryCode string `json:"FactoryCode,omitempty"` + FactoryDepartureDate string `json:"FactoryDepartureDate,omitempty"` + FixedAssets bool `json:"FixedAssets,omitempty"` + FlexibleOptionsData []struct { + Code string `json:"code,omitempty"` + Description string `json:"description,omitempty"` + Group string `json:"group,omitempty"` + LongName string `json:"long_name,omitempty"` + Name string `json:"name,omitempty"` + Price int `json:"price,omitempty"` + } `json:"FlexibleOptionsData,omitempty"` + ForecastedFactoryGatedDate any `json:"ForecastedFactoryGatedDate,omitempty"` + Headliner any `json:"HEADLINER,omitempty"` + HasDamagePhotos bool `json:"HasDamagePhotos,omitempty"` + HasOptionCodeData bool `json:"HasOptionCodeData,omitempty"` + Hash string `json:"Hash,omitempty"` + Interior []string `json:"INTERIOR,omitempty"` + IncentivesDetails struct { + Current struct { + Fuel struct { + Data []struct { + Algorithm bool `json:"algorithm,omitempty"` + Amount any `json:"amount,omitempty"` + Description string `json:"description,omitempty"` + IncentiveType string `json:"incentiveType,omitempty"` + Market string `json:"market,omitempty"` + Period string `json:"period,omitempty"` + Variables struct { + Distance any `json:"distance,omitempty"` + FuelEfficiencyImperial any `json:"fuel_efficiency_imperial,omitempty"` + FuelEfficiencyMetric float64 `json:"fuel_efficiency_metric,omitempty"` + FuelPrice float64 `json:"fuel_price,omitempty"` + KwhConsumption float64 `json:"kwh_consumption,omitempty"` + KwhPrice float64 `json:"kwh_price,omitempty"` + Months int `json:"months,omitempty"` + TollSavings int `json:"toll_savings,omitempty"` + } `json:"variables,omitempty"` + Variant string `json:"variant,omitempty"` + } `json:"data,omitempty"` + Total int `json:"total,omitempty"` + } `json:"fuel,omitempty"` + } `json:"current,omitempty"` + Total struct { + Fuel any `json:"fuel,omitempty"` + IncludedInPurchasePrice int `json:"includedInPurchasePrice,omitempty"` + Monthly int `json:"monthly,omitempty"` + Once int `json:"once,omitempty"` + } `json:"total,omitempty"` + } `json:"IncentivesDetails,omitempty"` + InspectionDocumentGUID any `json:"InspectionDocumentGuid,omitempty"` + InventoryPrice int `json:"InventoryPrice,omitempty"` + IsAtLocation bool `json:"IsAtLocation,omitempty"` + IsChargingConnectorIncluded bool `json:"IsChargingConnectorIncluded,omitempty"` + IsDemo bool `json:"IsDemo,omitempty"` + IsFactoryGated bool `json:"IsFactoryGated,omitempty"` + IsInTransit bool `json:"IsInTransit,omitempty"` + IsLegacy bool `json:"IsLegacy,omitempty"` + IsPreProdWithDisclaimer bool `json:"IsPreProdWithDisclaimer,omitempty"` + IsTegra bool `json:"IsTegra,omitempty"` + Language string `json:"Language,omitempty"` + Languages []string `json:"Languages,omitempty"` + LexiconDefaultOptions []struct { + Code string `json:"code,omitempty"` + Description string `json:"description,omitempty"` + Group string `json:"group,omitempty"` + LongName string `json:"long_name,omitempty"` + Name string `json:"name,omitempty"` + } `json:"LexiconDefaultOptions,omitempty"` + ListingType string `json:"ListingType,omitempty"` + ListingTypes string `json:"ListingTypes,omitempty"` + MarketingInUseDate any `json:"MarketingInUseDate,omitempty"` + Model string `json:"Model,omitempty"` + Odometer int `json:"Odometer,omitempty"` + OdometerType string `json:"OdometerType,omitempty"` + OnConfiguratorPricePercentage int `json:"OnConfiguratorPricePercentage,omitempty"` + OptionCodeData []struct { + AccelerationUnitLong string `json:"acceleration_unit_long,omitempty"` + AccelerationUnitShort string `json:"acceleration_unit_short,omitempty"` + AccelerationValue string `json:"acceleration_value,omitempty"` + Code string `json:"code,omitempty"` + Group string `json:"group,omitempty"` + Price int `json:"price,omitempty"` + UnitLong string `json:"unit_long,omitempty"` + UnitShort string `json:"unit_short,omitempty"` + Value string `json:"value,omitempty"` + TopSpeedLabel string `json:"top_speed_label,omitempty"` + RangeLabelSource string `json:"range_label_source,omitempty"` + RangeSource string `json:"range_source,omitempty"` + RangeSourceInventoryNew string `json:"range_source_inventory_new,omitempty"` + Description string `json:"description,omitempty"` + LongName string `json:"long_name,omitempty"` + Name string `json:"name,omitempty"` + } `json:"OptionCodeData,omitempty"` + OptionCodeList string `json:"OptionCodeList,omitempty"` + OptionCodeListDisplayOnly any `json:"OptionCodeListDisplayOnly,omitempty"` + OptionCodePricing []struct { + Code string `json:"code,omitempty"` + Group string `json:"group,omitempty"` + Price int `json:"price,omitempty"` + } `json:"OptionCodePricing,omitempty"` + OrderFee struct { + Type string `json:"type,omitempty"` + Value int `json:"value,omitempty"` + } `json:"OrderFee,omitempty"` + OriginalDeliveryDate any `json:"OriginalDeliveryDate,omitempty"` + OriginalInCustomerGarageDate any `json:"OriginalInCustomerGarageDate,omitempty"` + Paint []string `json:"PAINT,omitempty"` + PlannedGADailyDate string `json:"PlannedGADailyDate,omitempty"` + Price int64 `json:"Price,omitempty"` + PurchasePrice int `json:"PurchasePrice,omitempty"` + Roof any `json:"ROOF,omitempty"` + RegistrationCount int `json:"RegistrationCount,omitempty"` + SteeringWheel any `json:"STEERING_WHEEL,omitempty"` + SalesMetro string `json:"SalesMetro,omitempty"` + StateProvince string `json:"StateProvince,omitempty"` + StateProvinceLongName string `json:"StateProvinceLongName,omitempty"` + Trim []string `json:"TRIM,omitempty"` + TaxScheme any `json:"TaxScheme,omitempty"` + ThirdPartyHistoryURL any `json:"ThirdPartyHistoryUrl,omitempty"` + TitleStatus string `json:"TitleStatus,omitempty"` + TitleSubtype []string `json:"TitleSubtype,omitempty"` + TotalPrice int `json:"TotalPrice,omitempty"` + TradeInType any `json:"TradeInType,omitempty"` + TransportFees struct { + ExemptVRL []any `json:"exemptVRL,omitempty"` + Fees []any `json:"fees,omitempty"` + MetroFees []any `json:"metro_fees,omitempty"` + UnfundedLocationFees []any `json:"unfunded_location_fees,omitempty"` + } `json:"TransportFees,omitempty"` + TrimCode string `json:"TrimCode,omitempty"` + TrimName string `json:"TrimName,omitempty"` + Trt int `json:"Trt,omitempty"` + TrtName string `json:"TrtName,omitempty"` + Vin string `json:"VIN,omitempty"` + VehicleHistory any `json:"VehicleHistory,omitempty"` + VehicleRegion string `json:"VehicleRegion,omitempty"` + VrlName string `json:"VrlName,omitempty"` + Wheels []string `json:"WHEELS,omitempty"` + WarrantyBatteryExpDate time.Time `json:"WarrantyBatteryExpDate,omitempty"` + WarrantyBatteryIsExpired bool `json:"WarrantyBatteryIsExpired,omitempty"` + WarrantyBatteryMile int `json:"WarrantyBatteryMile,omitempty"` + WarrantyBatteryYear int `json:"WarrantyBatteryYear,omitempty"` + WarrantyData struct { + UsedVehicleLimitedWarrantyMile int `json:"UsedVehicleLimitedWarrantyMile,omitempty"` + UsedVehicleLimitedWarrantyYear int `json:"UsedVehicleLimitedWarrantyYear,omitempty"` + WarrantyBatteryExpDate time.Time `json:"WarrantyBatteryExpDate,omitempty"` + WarrantyBatteryIsExpired bool `json:"WarrantyBatteryIsExpired,omitempty"` + WarrantyBatteryMile int `json:"WarrantyBatteryMile,omitempty"` + WarrantyBatteryYear int `json:"WarrantyBatteryYear,omitempty"` + WarrantyDriveUnitExpDate time.Time `json:"WarrantyDriveUnitExpDate,omitempty"` + WarrantyDriveUnitMile int `json:"WarrantyDriveUnitMile,omitempty"` + WarrantyDriveUnitYear int `json:"WarrantyDriveUnitYear,omitempty"` + WarrantyMile int `json:"WarrantyMile,omitempty"` + WarrantyVehicleExpDate time.Time `json:"WarrantyVehicleExpDate,omitempty"` + WarrantyVehicleIsExpired bool `json:"WarrantyVehicleIsExpired,omitempty"` + WarrantyYear int `json:"WarrantyYear,omitempty"` + } `json:"WarrantyData,omitempty"` + WarrantyDriveUnitExpDate time.Time `json:"WarrantyDriveUnitExpDate,omitempty"` + WarrantyDriveUnitMile int `json:"WarrantyDriveUnitMile,omitempty"` + WarrantyDriveUnitYear int `json:"WarrantyDriveUnitYear,omitempty"` + WarrantyMile int `json:"WarrantyMile,omitempty"` + WarrantyVehicleExpDate time.Time `json:"WarrantyVehicleExpDate,omitempty"` + WarrantyVehicleIsExpired bool `json:"WarrantyVehicleIsExpired,omitempty"` + WarrantyYear int `json:"WarrantyYear,omitempty"` + Year int `json:"Year,omitempty"` + AlternateCurrency []any `json:"AlternateCurrency,omitempty"` + UsedVehicleLimitedWarrantyMile int `json:"UsedVehicleLimitedWarrantyMile,omitempty"` + UsedVehicleLimitedWarrantyYear int `json:"UsedVehicleLimitedWarrantyYear,omitempty"` + OdometerTypeShort string `json:"OdometerTypeShort,omitempty"` + DeliveryDateDisplay bool `json:"DeliveryDateDisplay,omitempty"` + TransportationFee int `json:"TransportationFee,omitempty"` + VrlList []struct { + Vrl int `json:"vrl,omitempty"` + Lat int `json:"lat,omitempty"` + Lon int `json:"lon,omitempty"` + VrlLocks []any `json:"vrlLocks,omitempty"` + } `json:"vrlList,omitempty"` + OptionCodeSpecs struct { + CSpecs struct { + Code string `json:"code,omitempty"` + Name string `json:"name,omitempty"` + Options []struct { + Code string `json:"code,omitempty"` + Name string `json:"name,omitempty"` + LongName string `json:"long_name,omitempty"` + Description string `json:"description,omitempty"` + } `json:"options,omitempty"` + } `json:"C_SPECS,omitempty"` + CDesign struct { + Code string `json:"code,omitempty"` + Name string `json:"name,omitempty"` + Options []any `json:"options,omitempty"` + } `json:"C_DESIGN,omitempty"` + CCallouts struct { + Code string `json:"code,omitempty"` + Name string `json:"name,omitempty"` + Options []struct { + Code string `json:"code,omitempty"` + Name string `json:"name,omitempty"` + LongName string `json:"long_name,omitempty"` + Description string `json:"description,omitempty"` + Group string `json:"group,omitempty"` + List []string `json:"list,omitempty"` + Period string `json:"period,omitempty"` + } `json:"options,omitempty"` + } `json:"C_CALLOUTS,omitempty"` + COpts struct { + Code string `json:"code,omitempty"` + Name string `json:"name,omitempty"` + Options []struct { + Code string `json:"code,omitempty"` + Name string `json:"name,omitempty"` + LongName string `json:"long_name,omitempty"` + Description string `json:"description,omitempty"` + } `json:"options,omitempty"` + } `json:"C_OPTS,omitempty"` + } `json:"OptionCodeSpecs,omitempty"` + CompositorViewsCustom struct { + IsProductWithCustomViews bool `json:"isProductWithCustomViews,omitempty"` + ExternalZoom struct { + Order int `json:"order,omitempty"` + Search int `json:"search,omitempty"` + } `json:"externalZoom,omitempty"` + ExternalCrop struct { + Order string `json:"order,omitempty"` + Search string `json:"search,omitempty"` + } `json:"externalCrop,omitempty"` + } `json:"CompositorViewsCustom,omitempty"` + IsRangeStandard bool `json:"IsRangeStandard,omitempty"` + MetroName string `json:"MetroName,omitempty"` + GeoPoints [][]any `json:"geoPoints,omitempty"` + HasMarketingOptions bool `json:"HasMarketingOptions,omitempty"` + InTransitMetroName string `json:"InTransitMetroName,omitempty"` + InTransitSalesMetro string `json:"InTransitSalesMetro,omitempty"` + FirstRegistrationDate any `json:"FirstRegistrationDate,omitempty"` +} + +// AvailabilitiesResponse contain the a list of car availability. +type AvailabilitiesResponse struct { + Results []Availability `json:"results,omitempty"` + TotalMatchesFound string `json:"total_matches_found,omitempty"` +}