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. # => [["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=. File.("../data/vehicles.json", __dir__)
- VERSION =
"0.4.0"
Constants included from OtherOption
Class Attribute Summary collapse
-
.data_path ⇒ String
Path to the bundled snapshot (or an explicit override via
data_path=). - .logger ⇒ Object
Class Method Summary collapse
-
.active_data_path ⇒ String
The dataset file actually in effect: an explicit override wins; otherwise a refreshed cache (if present and
use_cache); otherwise the bundled snapshot. -
.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. -
.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).
-
.color(query) ⇒ Color?
Resolve a color by slug or name (forgiving: case, diacritics, synonyms like "gray"→grey, "navy"→blue).
-
.color_options ⇒ Array[[String, String]]
[[name, slug], ...] for a Rails
select. -
.colors ⇒ Array[Color]
The canonical color palette, frequency-ordered.
-
.configuration ⇒ Configuration
--- configuration -------------------------------------------------------.
- .configure {|configuration| ... } ⇒ void
-
.data_version ⇒ String?
Version of the dataset currently in effect (refreshed cache or bundled), e.g.
- .dataset ⇒ Dataset
-
.find(query) ⇒ Model?
Resolve a free-text "make + model" string into one Model (or nil).
-
.fold_diacritics(str) ⇒ Object
Shared diacritic folding for normalize/slugify.
-
.kinds ⇒ Object
Every kind in the dataset.
-
.load_validators! ⇒ Object
Require the ActiveModel validators (idempotent; no-op without ActiveModel).
-
.make(query) ⇒ Make?
The rich Make object (or nil).
-
.make_options(kind: nil, region: nil, include_other: false) ⇒ Array[[String, String]]
[[label, value], ...] of makes for a Rails
select. -
.makes(kind: nil, region: nil, include_other: false) ⇒ Array[String]
Make display names.
-
.model(make_name, model_name) ⇒ Model?
Resolve a stored make + model PAIR into a Model (or nil).
-
.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. -
.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.
-
.normalize(str) ⇒ String
Match-normalize a string: fold diacritics, downcase, collapse anything non-alphanumeric to single spaces.
- .providers ⇒ Object
-
.refresh! ⇒ Boolean
Pull the latest published dataset into the local cache, so data fixes / new makes land WITHOUT a gem upgrade.
-
.region ⇒ Symbol
Region the bundled data covers, as a Symbol, e.g.
-
.regions ⇒ Object
Continent codes present in the dataset.
-
.reload! ⇒ void
Drop the in-memory dataset so the next access reloads from disk (after a refresh, a cache clear, or a
data_path=change). -
.reset_configuration! ⇒ void
Reset config + caches (config, data path, dataset, providers).
-
.resolve(attribute, model, **opts) ⇒ Object
Ask each available provider for an attribute, hosted first, until one gives a non-nil answer.
-
.search(query) ⇒ Array[Model]
Every model matching a query, ranked.
-
.slugify(str) ⇒ String
Slugify a display name: "Alfa Romeo" => "alfa-romeo", "Škoda" => "skoda".
-
.top_models(kind: nil, country: nil, region: nil, limit: 20) ⇒ Object
The most popular models by official registration counts, most popular first.
Methods included from OtherOption
Class Attribute Details
.data_path ⇒ String
Path to the bundled snapshot (or an explicit override via data_path=).
52 53 54 |
# File 'lib/vehicles.rb', line 52 def data_path @data_path || DATA_PATH end |
.logger ⇒ Object
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_path ⇒ String
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.
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", ...], ... }
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.
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_options ⇒ Array[[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.
179 180 181 |
# File 'lib/vehicles.rb', line 179 def Colors::ALL.map { |c| [c.name, c.slug] } end |
.colors ⇒ Array[Color]
The canonical color palette, frequency-ordered. => [Vehicles::Color, ...]
173 174 175 |
# File 'lib/vehicles.rb', line 173 def colors Colors::ALL end |
.configuration ⇒ Configuration
--- configuration -------------------------------------------------------
31 32 33 |
# File 'lib/vehicles.rb', line 31 def configuration @configuration ||= Configuration.new end |
.configure {|configuration| ... } ⇒ void
This method returns an undefined value.
35 36 37 |
# File 'lib/vehicles.rb', line 35 def configure yield(configuration) end |
.data_version ⇒ String?
Version of the dataset currently in effect (refreshed cache or bundled), e.g. "2026.06.0".
87 88 89 |
# File 'lib/vehicles.rb', line 87 def data_version dataset.version end |
.dataset ⇒ Dataset
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).
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 |
.kinds ⇒ Object
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.
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"]
194 195 196 197 |
# File 'lib/vehicles.rb', line 194 def (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
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
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"]
201 202 203 204 205 |
# File 'lib/vehicles.rb', line 201 def (make, kind: nil, body_type: nil, include_other: false) found = make(make) opts = found ? found.(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.
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").
252 253 254 |
# File 'lib/vehicles.rb', line 252 def normalize(str) fold_diacritics(str).downcase.gsub(/[^a-z0-9]+/, " ").strip end |
.providers ⇒ Object
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).
75 76 77 |
# File 'lib/vehicles.rb', line 75 def refresh! Refresher.refresh! end |
.region ⇒ Symbol
Region the bundled data covers, as a Symbol, e.g. :eu.
92 93 94 |
# File 'lib/vehicles.rb', line 92 def region dataset.region.to_s.downcase.to_sym end |
.regions ⇒ Object
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.}") next end nil end |
.search(query) ⇒ Array[Model]
Every model matching a query, ranked. => [Vehicles::Model, ...]
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".
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 |