Module: Vehicles::Providers::HostedProvider

Defined in:
lib/vehicles/providers/hosted_provider.rb

Overview

Talks to the hosted VehiclesDB API to enrich models with years, images, and segments. It is STRICTLY OPTIONAL: available? is true only when an api_key is configured, so a key-less install never reaches the network. Every call is error-isolated — a slow, missing, or failing API yields nil and the resolver falls back to the local data. Tracking/enrichment must never break the host app, so this never raises out.

The images endpoint is LIVE (since 2026-07): mint a key at https://vehiclesdb.com/settings/api-keys, set config.api_key, and model.image / model.images answer with rendered vehicle imagery. The enrichment endpoints (years/segment) are still the wired-up seam — they safely return nil until the service ships them.

Class Method Summary collapse

Class Method Details

.available?Boolean

Returns:

  • (Boolean)


22
23
24
# File 'lib/vehicles/providers/hosted_provider.rb', line 22

def available?
  !Vehicles.configuration.api_key.to_s.empty?
end

.fetch(model) ⇒ Object

Fetch + memoize the full model payload for this process. Keyed by slug.



64
65
66
67
68
69
# File 'lib/vehicles/providers/hosted_provider.rb', line 64

def fetch(model)
  @cache ||= {}
  return @cache[model.slug] if @cache.key?(model.slug)

  @cache[model.slug] = get("/v1/models/#{model.slug}", {})
end

.get(path, params) ⇒ Object

Issue a GET and parse JSON. Returns nil on ANY failure (network, timeout, non-200, bad JSON) — callers treat nil as "fall back to local data".



77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
# File 'lib/vehicles/providers/hosted_provider.rb', line 77

def get(path, params)
  # Lazy-required: the gem is standalone-first, and this whole module is
  # inert without an api_key, so we don't pay net/http at load time.
  require "net/http"
  require "uri"

  config = Vehicles.configuration
  uri = URI.join(config.api_base_url, path)
  uri.query = URI.encode_www_form(params) unless params.empty?

  http = Net::HTTP.new(uri.host, uri.port)
  http.use_ssl = uri.scheme == "https"
  http.open_timeout = config.api_timeout
  http.read_timeout = config.api_timeout

  request = Net::HTTP::Get.new(uri)
  request["Authorization"] = "Bearer #{config.api_key}"
  request["Accept"] = "application/json"

  response = http.request(request)
  return nil unless response.is_a?(Net::HTTPSuccess)

  JSON.parse(response.body)
rescue StandardError => e
  warn "[vehicles] hosted lookup failed (#{e.class}); using local data" if $DEBUG
  nil
end

.image(model, year: nil, color: nil, size: :md) ⇒ Object

One variant URL — the common "just give me an " case. size picks from the API's rendered variants (:sm 320×180, :md 640×360, :lg 1280×720; webp).



40
41
42
# File 'lib/vehicles/providers/hosted_provider.rb', line 40

def image(model, year: nil, color: nil, size: :md)
  images(model, color: color)&.dig("variants", size.to_s, "url")
end

.images(model, color: nil) ⇒ Object

The full images payload for a model — palette, every variant with dimensions, provenance, and the honest color fallback: color is what the API actually served, requested_color what you asked for (a color that isn't rendered yet falls back rather than 404ing).

GET /v1/vehicles/:kind/:make_slug/:model_slug/images?color=

year/trim filters are RESERVED server-side today (the API 422s on them by contract, so callers can't silently build on unimplemented semantics) — which is why image accepts year: but never sends it: serving the current rendering beats an error until the filter ships.



55
56
57
58
59
# File 'lib/vehicles/providers/hosted_provider.rb', line 55

def images(model, color: nil)
  params = {}
  params[:color] = color.to_s unless color.to_s.empty?
  get("/v1/vehicles/#{model.kind}/#{model.make_slug}/#{model.model_slug}/images", params)
end

.reset!Object



71
72
73
# File 'lib/vehicles/providers/hosted_provider.rb', line 71

def reset!
  @cache = {}
end

.segment(model) ⇒ Object



33
34
35
# File 'lib/vehicles/providers/hosted_provider.rb', line 33

def segment(model)
  fetch(model)&.dig("segment")&.to_sym
end

.years(model) ⇒ Object



26
27
28
29
30
31
# File 'lib/vehicles/providers/hosted_provider.rb', line 26

def years(model)
  data = fetch(model)
  return nil unless data && data["year_start"]

  (data["year_start"]..data["year_end"]) # year_end may be nil => endless range
end