Module: Vehicles

Extended by:
OtherOption
Defined in:
lib/vehicles.rb,
lib/vehicles/make.rb,
lib/vehicles/color.rb,
lib/vehicles/model.rb,
lib/vehicles/colors.rb,
lib/vehicles/dataset.rb,
lib/vehicles/railtie.rb,
lib/vehicles/version.rb,
lib/vehicles/refresher.rb,
lib/vehicles/mcp_server.rb,
lib/vehicles/other_option.rb,
lib/vehicles/configuration.rb,
lib/vehicles/providers/local_provider.rb,
lib/vehicles/providers/hosted_provider.rb,
lib/generators/vehicles/install_generator.rb,
sig/vehicles.rbs

Overview

Car makes & models for your Rails app — dropdowns, search, validation. Bundled data, zero config, no network calls. Standalone first; an SDK for the hosted VehiclesDB API second.

Vehicles.makes                   # => ["Alfa Romeo", "Audi", "BMW", ...]
Vehicles.models("VW")            # => ["Golf", "Polo", "Tiguan", ...]
Vehicles.find("vw golf")         # => #<Vehicles::Model "Volkswagen Golf">
Vehicles.make_options            # => [["Alfa Romeo", "alfa-romeo"], ...]  (for select)

Defined Under Namespace

Modules: Colors, Generators, OtherOption, Providers, Refresher Classes: Color, Configuration, Dataset, Error, Make, McpServer, Model, Railtie

Constant Summary collapse

DATA_PATH =

The bundled snapshot, packaged in the gem. Overridable via Vehicles.data_path=.

Returns:

  • (String)
File.expand_path("../data/vehicles.json", __dir__)
VERSION =

Returns:

  • (String)
"0.4.0"

Constants included from OtherOption

OtherOption::OTHER_SLUG

Class Attribute Summary collapse

Class Method Summary collapse

Methods included from OtherOption

other?, other_label

Class Attribute Details

.data_pathString

Path to the bundled snapshot (or an explicit override via data_path=).

Returns:

  • (String)


52
53
54
# File 'lib/vehicles.rb', line 52

def data_path
  @data_path || DATA_PATH
end

.loggerObject



272
273
274
# File 'lib/vehicles.rb', line 272

def logger
  @logger ||= (defined?(Rails) && Rails.respond_to?(:logger) ? Rails.logger : nil)
end

Class Method Details

.active_data_pathString

The dataset file actually in effect: an explicit override wins; otherwise a refreshed cache (if present and use_cache); otherwise the bundled snapshot. This is how a refresh reaches the running app — no gem upgrade needed.

Returns:

  • (String)


61
62
63
64
65
66
# File 'lib/vehicles.rb', line 61

def active_data_path
  return @data_path if @data_path
  return Refresher.cached_path if configuration.use_cache && Refresher.cached?

  DATA_PATH
end

.catalog(kind: nil, region: nil) ⇒ Hash[String, Array[String]]

A { make => [model names] } map for the given filters — everything you need to build a dependent make → model picker entirely on the client: embed it once (Vehicles.catalog(kind: :car).to_json) and switch the model list in a few lines of JS. No endpoint, no extra request, instant. The whole car catalog is small (tens of KB), so this is the simplest path for most apps. Vehicles.catalog(kind: :car) # => { "Audi" => ["A3", "A4", ...], ... }

Parameters:

  • kind: (Symbol, nil) (defaults to: nil)
  • region: (Symbol, nil) (defaults to: nil)

Returns:

  • (Hash[String, Array[String]])


217
218
219
220
221
# File 'lib/vehicles.rb', line 217

def catalog(kind: nil, region: nil)
  makes(kind: kind, region: region).to_h do |name|
    [name, models(name, kind: kind, region: region)]
  end
end

.catalog_slice(kind: nil, region: nil, rarity: nil, max_decile: nil) ⇒ Object

A curated slice of models by kind/continent/rarity — the "give me sensible data to show" entry point (ranked, unranked models excluded when a rarity or max_decile filter is set). Vehicles.catalog_slice(kind: :car, region: :eu, rarity: :common)



160
161
162
163
# File 'lib/vehicles.rb', line 160

def catalog_slice(kind: nil, region: nil, rarity: nil, max_decile: nil)
  dataset.all_models_filtered(kind: kind, region: region || configuration.region,
                              rarity: rarity, max_decile: max_decile)
end

.color(query) ⇒ Color?

Resolve a color by slug or name (forgiving: case, diacritics, synonyms like "gray"→grey, "navy"→blue). Returns a Vehicles::Color, or nil.

Parameters:

  • query (Object)

Returns:



185
186
187
188
189
190
# File 'lib/vehicles.rb', line 185

def color(query)
  q = normalize(query)
  return nil if q.empty?

  Colors::BY_SLUG[q] || Colors::BY_NAME[q] || Colors::BY_SLUG[Colors::SYNONYMS[q]]
end

.color_optionsArray[[String, String]]

[[name, slug], ...] for a Rails select. Names are English — localize the labels in your app; the slug is the stable value you store.

Returns:

  • (Array[[String, String]])


179
180
181
# File 'lib/vehicles.rb', line 179

def color_options
  Colors::ALL.map { |c| [c.name, c.slug] }
end

.colorsArray[Color]

The canonical color palette, frequency-ordered. => [Vehicles::Color, ...]

Returns:



173
174
175
# File 'lib/vehicles.rb', line 173

def colors
  Colors::ALL
end

.configurationConfiguration

--- configuration -------------------------------------------------------

Returns:



31
32
33
# File 'lib/vehicles.rb', line 31

def configuration
  @configuration ||= Configuration.new
end

.configure {|configuration| ... } ⇒ void

This method returns an undefined value.

Yields:

Yield Parameters:

Yield Returns:

  • (void)


35
36
37
# File 'lib/vehicles.rb', line 35

def configure
  yield(configuration)
end

.data_versionString?

Version of the dataset currently in effect (refreshed cache or bundled), e.g. "2026.06.0".

Returns:

  • (String, nil)


87
88
89
# File 'lib/vehicles.rb', line 87

def data_version
  dataset.version
end

.datasetDataset

Returns:



68
69
70
# File 'lib/vehicles.rb', line 68

def dataset
  Dataset.load(active_data_path)
end

.find(query) ⇒ Model?

Resolve a free-text "make + model" string into one Model (or nil).

Parameters:

  • query (Object)

Returns:



123
124
125
# File 'lib/vehicles.rb', line 123

def find(query)
  dataset.find_model(query)
end

.fold_diacritics(str) ⇒ Object

Shared diacritic folding for normalize/slugify. NFKD splits "š" into "s" + a combining caron; we strip the combining marks (\pMn) BEFORE the separator gsub, or "Škoda" would become "s koda". Defends against non-UTF-8/invalid encodings (e.g. a binary string) so callers never hit Encoding errors.



265
266
267
268
269
270
# File 'lib/vehicles.rb', line 265

def fold_diacritics(str)
  s = str.to_s
  s = s.dup.force_encoding(Encoding::UTF_8) unless s.encoding == Encoding::UTF_8
  s = s.scrub("") unless s.valid_encoding?
  s.unicode_normalize(:nfkd).gsub(/\p{Mn}+/, "")
end

.kindsObject

Every kind in the dataset. => [:bus, :car, :moped, :motorcycle, :truck, :van]



144
145
146
# File 'lib/vehicles.rb', line 144

def kinds
  dataset.kinds
end

.load_validators!Object

Require the ActiveModel validators (idempotent; no-op without ActiveModel).



277
278
279
280
281
282
283
284
# File 'lib/vehicles.rb', line 277

def load_validators!
  return if @validators_loaded
  return unless defined?(ActiveModel::EachValidator)

  require_relative "vehicles/validators/vehicle_make_validator"
  require_relative "vehicles/validators/vehicle_model_validator"
  @validators_loaded = true
end

.make(query) ⇒ Make?

The rich Make object (or nil). Forgiving: name, slug, or alias.

Parameters:

  • query (Object)

Returns:



118
119
120
# File 'lib/vehicles.rb', line 118

def make(query)
  dataset.find_make(query)
end

.make_options(kind: nil, region: nil, include_other: false) ⇒ Array[[String, String]]

[[label, value], ...] of makes for a Rails select. Vehicles.make_options(include_other: true) # ... + [other_label, "other"]

Parameters:

  • kind: (Symbol, nil) (defaults to: nil)
  • region: (Symbol, nil) (defaults to: nil)

Returns:

  • (Array[[String, String]])


194
195
196
197
# File 'lib/vehicles.rb', line 194

def make_options(kind: nil, region: nil, include_other: false)
  opts = dataset.makes(kind: kind, region: region || configuration.region).map { |m| [m.name, m.slug] }
  append_other_option(opts, include_other)
end

.makes(kind: nil, region: nil, include_other: false) ⇒ Array[String]

Make display names. => ["Abarth", "Alfa Romeo", ...] Vehicles.makes(kind: :motorcycle, region: :as) # continent filter Vehicles.makes(include_other: true) # ... + the "Other" escape hatch

Parameters:

  • kind: (Symbol, nil) (defaults to: nil)
  • region: (Symbol, nil) (defaults to: nil)

Returns:

  • (Array[String])


101
102
103
104
# File 'lib/vehicles.rb', line 101

def makes(kind: nil, region: nil, include_other: false)
  names = dataset.makes(kind: kind, region: region || configuration.region).map(&:name)
  append_other_name(names, include_other)
end

.model(make_name, model_name) ⇒ Model?

Resolve a stored make + model PAIR into a Model (or nil). The structured counterpart to find (which parses one free-text string) — reach for this when your records keep make and model in separate columns and you want the model's metadata (kind, body_type, …) back. Vehicles.model("Audi", "A3") # => #<Vehicles::Model "Audi A3"> Vehicles.model("vw", "golf") # forgiving, like every other lookup

Parameters:

  • make_name (Object)
  • model_name (Object)

Returns:



133
134
135
136
# File 'lib/vehicles.rb', line 133

def model(make_name, model_name)
  found = make(make_name)
  found&.model(model_name)
end

.model_options(make, kind: nil, body_type: nil, include_other: false) ⇒ Array[[String, String]]

[[label, value], ...] of a make's models for a Rails select. Unknown => []. Vehicles.model_options("audi", include_other: true) # ... + [other_label, "other"]

Parameters:

  • make (Object)
  • kind: (Symbol, nil) (defaults to: nil)
  • body_type: (Symbol, nil) (defaults to: nil)

Returns:

  • (Array[[String, String]])


201
202
203
204
205
# File 'lib/vehicles.rb', line 201

def model_options(make, kind: nil, body_type: nil, include_other: false)
  found = make(make)
  opts = found ? found.model_options(kind: kind, body_type: body_type) : []
  append_other_option(opts, include_other)
end

.models(make, kind: nil, body_type: nil, region: nil, rarity: nil, max_decile: nil, include_other: false) ⇒ Array[String]

Model display names for a make. => ["Golf", "Polo", ...]. Unknown make => []. Vehicles.models("Toyota", region: :eu, rarity: :common) Vehicles.models("Toyota", include_other: true) # ... + the "Other" escape hatch With include_other:, an unknown make (e.g. the "Other" make itself) yields just [other_label], so a make→model picker never dead-ends.

Parameters:

  • make (Object)
  • kind: (Symbol, nil) (defaults to: nil)
  • body_type: (Symbol, nil) (defaults to: nil)
  • region: (Symbol, nil) (defaults to: nil)

Returns:

  • (Array[String])


111
112
113
114
115
# File 'lib/vehicles.rb', line 111

def models(make, kind: nil, body_type: nil, region: nil, rarity: nil, max_decile: nil, include_other: false)
  names = make(make)&.models(kind: kind, body_type: body_type, region: region || configuration.region,
                             rarity: rarity, max_decile: max_decile)&.map(&:name) || []
  append_other_name(names, include_other)
end

.normalize(str) ⇒ String

Match-normalize a string: fold diacritics, downcase, collapse anything non-alphanumeric to single spaces. "Mercedes-Benz" => "mercedes benz", "Škoda" => "skoda". Used everywhere lookups need to be forgiving — so it must NEVER raise (the API contract is "garbage in => empty/nil out, not an error").

Parameters:

  • str (Object)

Returns:

  • (String)


252
253
254
# File 'lib/vehicles.rb', line 252

def normalize(str)
  fold_diacritics(str).downcase.gsub(/[^a-z0-9]+/, " ").strip
end

.providersObject



242
243
244
# File 'lib/vehicles.rb', line 242

def providers
  @providers ||= [Providers::HostedProvider, Providers::LocalProvider]
end

.refresh!Boolean

Pull the latest published dataset into the local cache, so data fixes / new makes land WITHOUT a gem upgrade. Returns true/false; never raises. Schedule it (e.g. a daily job — rails g vehicles:install sets one up).

Returns:

  • (Boolean)


75
76
77
# File 'lib/vehicles.rb', line 75

def refresh!
  Refresher.refresh!
end

.regionSymbol

Region the bundled data covers, as a Symbol, e.g. :eu.

Returns:

  • (Symbol)


92
93
94
# File 'lib/vehicles.rb', line 92

def region
  dataset.region.to_s.downcase.to_sym
end

.regionsObject

Continent codes present in the dataset. => [:af, :as, :eu, :na, :oc, :sa]



166
167
168
# File 'lib/vehicles.rb', line 166

def regions
  @regions ||= dataset.all_models.flat_map(&:regions).uniq.sort
end

.reload!void

This method returns an undefined value.

Drop the in-memory dataset so the next access reloads from disk (after a refresh, a cache clear, or a data_path= change).



81
82
83
# File 'lib/vehicles.rb', line 81

def reload!
  Dataset.reset!
end

.reset_configuration!void

This method returns an undefined value.

Reset config + caches (config, data path, dataset, providers). Primarily for the test suite — genuinely returns the gem to a pristine state.



41
42
43
44
45
46
47
# File 'lib/vehicles.rb', line 41

def reset_configuration!
  @configuration = Configuration.new
  @providers = nil
  @data_path = nil
  Dataset.reset!
  Providers::HostedProvider.reset!
end

.resolve(attribute, model, **opts) ⇒ Object

Ask each available provider for an attribute, hosted first, until one gives a non-nil answer. Returns nil if none can. Never raises. Backs Model#years / #segment / #image.



228
229
230
231
232
233
234
235
236
237
238
239
240
# File 'lib/vehicles.rb', line 228

def resolve(attribute, model, **opts)
  providers.each do |provider|
    next unless provider.available?

    value = provider.public_send(attribute, model, **opts)
    return value unless value.nil?
  rescue StandardError => e
    # A misbehaving provider must never break a model read — log and move on.
    logger&.error("[vehicles] provider #{provider} failed on #{attribute}: #{e.message}")
    next
  end
  nil
end

.search(query) ⇒ Array[Model]

Every model matching a query, ranked. => [Vehicles::Model, ...]

Parameters:

  • query (Object)

Returns:



139
140
141
# File 'lib/vehicles.rb', line 139

def search(query)
  dataset.search(query)
end

.slugify(str) ⇒ String

Slugify a display name: "Alfa Romeo" => "alfa-romeo", "Škoda" => "skoda".

Parameters:

  • str (Object)

Returns:

  • (String)


257
258
259
# File 'lib/vehicles.rb', line 257

def slugify(str)
  fold_diacritics(str).downcase.gsub(/[^a-z0-9]+/, "-").gsub(/(\A-|-\z)/, "")
end

.top_models(kind: nil, country: nil, region: nil, limit: 20) ⇒ Object

The most popular models by official registration counts, most popular first. Filter by country (ISO alpha-2) or continent (:eu/:as/…). Vehicles.top_models(kind: :car, country: :nl, limit: 10).map(&:name) Vehicles.top_models(kind: :motorcycle, region: :as, limit: 10)



152
153
154
# File 'lib/vehicles.rb', line 152

def top_models(kind: nil, country: nil, region: nil, limit: 20)
  dataset.top_models(kind: kind, country: country, region: region, limit: limit)
end