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/plates.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/plates/series.rb,
lib/vehicles/plates/jurisdiction.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, Plates, 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.6.1"

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)


55
56
57
# File 'lib/vehicles.rb', line 55

def data_path
  @data_path || DATA_PATH
end

.loggerObject



293
294
295
# File 'lib/vehicles.rb', line 293

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)


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

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]])


238
239
240
241
242
# File 'lib/vehicles.rb', line 238

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)



181
182
183
184
# File 'lib/vehicles.rb', line 181

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:



206
207
208
209
210
211
# File 'lib/vehicles.rb', line 206

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]])


200
201
202
# File 'lib/vehicles.rb', line 200

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

.colorsArray[Color]

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

Returns:



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

def colors
  Colors::ALL
end

.configurationConfiguration

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

Returns:



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

def configuration
  @configuration ||= Configuration.new
end

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

This method returns an undefined value.

Yields:

Yield Parameters:

Yield Returns:

  • (void)


38
39
40
# File 'lib/vehicles.rb', line 38

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)


90
91
92
# File 'lib/vehicles.rb', line 90

def data_version
  dataset.version
end

.datasetDataset

Returns:



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

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:



126
127
128
# File 'lib/vehicles.rb', line 126

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.



286
287
288
289
290
291
# File 'lib/vehicles.rb', line 286

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]



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

def kinds
  dataset.kinds
end

.load_validators!Object

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



298
299
300
301
302
303
304
305
# File 'lib/vehicles.rb', line 298

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:



121
122
123
# File 'lib/vehicles.rb', line 121

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]])


215
216
217
218
# File 'lib/vehicles.rb', line 215

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])


104
105
106
107
# File 'lib/vehicles.rb', line 104

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:



136
137
138
139
# File 'lib/vehicles.rb', line 136

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]])


222
223
224
225
226
# File 'lib/vehicles.rb', line 222

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])


114
115
116
117
118
# File 'lib/vehicles.rb', line 114

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)


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

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

.plate(input, jurisdiction:) ⇒ Object

Validate a typed registration against a jurisdiction's real series. Two-tier: exact as-issued, else separator-forgiving with a formatting suggestion. Never raises; unknown jurisdictions yield an empty match. Vehicles.plate("12-GB-BD", jurisdiction: :nl).valid? # => true (exact) Vehicles.plate("1234XYZ", jurisdiction: :es).suggestion # => "1234 XYZ" Vehicles.plate("12-AB-CD", jurisdiction: :nl).valid? # => false (vowel purge)



155
156
157
# File 'lib/vehicles.rb', line 155

def plate(input, jurisdiction:)
  Plates.match(input, jurisdiction: jurisdiction)
end

.plates(code = nil) ⇒ Object

License plates (the bundled PRD-PLATES dataset). No argument: every jurisdiction. With a code: that jurisdiction (or nil — forgiving). Vehicles.plates # => [#<Jurisdiction nl …>, …] Vehicles.plates(:nl) # => the Netherlands entry (33 series)



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

def plates(code = nil)
  code.nil? ? Plates.jurisdictions : Plates.jurisdiction(code)
end

.providersObject



263
264
265
# File 'lib/vehicles.rb', line 263

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)


78
79
80
# File 'lib/vehicles.rb', line 78

def refresh!
  Refresher.refresh!
end

.regionSymbol

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

Returns:

  • (Symbol)


95
96
97
# File 'lib/vehicles.rb', line 95

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

.regionsObject

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



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

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).



84
85
86
# File 'lib/vehicles.rb', line 84

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.



44
45
46
47
48
49
50
# File 'lib/vehicles.rb', line 44

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.



249
250
251
252
253
254
255
256
257
258
259
260
261
# File 'lib/vehicles.rb', line 249

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:



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

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)


278
279
280
# File 'lib/vehicles.rb', line 278

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)



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

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