From 05ac49fd68940a2b0af10085c791a746cb4aa437 Mon Sep 17 00:00:00 2001 From: Matthieu 'JP' DERASSE Date: Wed, 9 Aug 2023 12:05:37 +0000 Subject: [PATCH] feat(first): First commit with basic implementation. Missing test --- .drone.yml | 116 +++++++++++++ .gitignore | 5 + .golangci.yml | 210 ++++++++++++++++++++++ Dockerfile | 15 ++ LICENSE | 0 README.md | 3 + client.go | 284 ++++++++++++++++++++++++++++++ client_options.go | 11 ++ constant.go | 33 ++++ error.go | 25 +++ go.mod | 3 + stock.go | 38 ++++ stock_struct.go | 434 ++++++++++++++++++++++++++++++++++++++++++++++ 13 files changed, 1177 insertions(+) create mode 100644 .drone.yml create mode 100644 .gitignore create mode 100644 .golangci.yml create mode 100644 Dockerfile create mode 100644 LICENSE create mode 100644 README.md create mode 100644 client.go create mode 100644 client_options.go create mode 100644 constant.go create mode 100644 error.go create mode 100644 go.mod create mode 100644 stock.go create mode 100644 stock_struct.go 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"` +}