Module: Vehicles
- 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/configuration.rb,
lib/vehicles/providers/local_provider.rb,
lib/vehicles/providers/hosted_provider.rb,
lib/generators/vehicles/install_generator.rb
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, Providers, Refresher Classes: Color, Configuration, Dataset, Error, Make, 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.1.1"
Class Attribute Summary collapse
-
.data_path ⇒ Object
Path to the bundled snapshot (or an explicit override via ‘data_path=`).
- .logger ⇒ Object
Class Method Summary collapse
-
.active_data_path ⇒ Object
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) ⇒ Object
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.
-
.color(query) ⇒ Object
Resolve a color by slug or name (forgiving: case, diacritics, synonyms like “gray”→grey, “navy”→blue).
-
.color_options ⇒ Object
- [name, slug], …
-
for a Rails ‘select`.
-
.colors ⇒ Object
The canonical color palette, frequency-ordered.
-
.configuration ⇒ Object
— configuration ——————————————————-.
- .configure {|configuration| ... } ⇒ Object
-
.data_version ⇒ Object
Version of the dataset currently in effect (refreshed cache or bundled), e.g.
- .dataset ⇒ Object
-
.find(query) ⇒ Object
Resolve a free-text “make + model” string into one Model (or nil).
-
.fold_diacritics(str) ⇒ Object
Shared diacritic folding for normalize/slugify.
-
.load_validators! ⇒ Object
Require the ActiveModel validators (idempotent; no-op without ActiveModel).
-
.make(query) ⇒ Object
The rich Make object (or nil).
-
.make_options(kind: nil, region: nil) ⇒ Object
- [label, value], …
-
of makes for a Rails ‘select`.
-
.makes(kind: nil, region: nil) ⇒ Object
Make display names.
-
.model(make_name, model_name) ⇒ Object
Resolve a stored make + model PAIR into a Model (or nil).
-
.model_options(make, kind: nil, body_type: nil) ⇒ Object
- [label, value], …
-
of a make’s models for a Rails ‘select`.
-
.models(make, kind: nil, body_type: nil, region: nil) ⇒ Object
Model display names for a make.
-
.normalize(str) ⇒ Object
Match-normalize a string: fold diacritics, downcase, collapse anything non-alphanumeric to single spaces.
- .providers ⇒ Object
-
.refresh! ⇒ Object
Pull the latest published dataset into the local cache, so data fixes / new makes land WITHOUT a gem upgrade.
-
.region ⇒ Object
Region the bundled data covers, as a Symbol, e.g.
-
.reload! ⇒ Object
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! ⇒ Object
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) ⇒ Object
Every model matching a query, ranked.
-
.slugify(str) ⇒ Object
Slugify a display name: “Alfa Romeo” => “alfa-romeo”, “Škoda” => “skoda”.
Class Attribute Details
.data_path ⇒ Object
Path to the bundled snapshot (or an explicit override via ‘data_path=`).
51 52 53 |
# File 'lib/vehicles.rb', line 51 def data_path @data_path || DATA_PATH end |
.logger ⇒ Object
231 232 233 |
# File 'lib/vehicles.rb', line 231 def logger @logger ||= (defined?(Rails) && Rails.respond_to?(:logger) ? Rails.logger : nil) end |
Class Method Details
.active_data_path ⇒ Object
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.
60 61 62 63 64 65 |
# File 'lib/vehicles.rb', line 60 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) ⇒ Object
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", ...], ... }
176 177 178 179 180 |
# File 'lib/vehicles.rb', line 176 def catalog(kind: nil, region: nil) makes(kind: kind, region: region).to_h do |name| [name, models(name, kind: kind, region: region)] end end |
.color(query) ⇒ Object
Resolve a color by slug or name (forgiving: case, diacritics, synonyms like “gray”→grey, “navy”→blue). Returns a Vehicles::Color, or nil.
152 153 154 155 156 157 |
# File 'lib/vehicles.rb', line 152 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 ⇒ Object
- [name, slug], …
-
for a Rails ‘select`. Names are English — localize the
labels in your app; the slug is the stable value you store.
146 147 148 |
# File 'lib/vehicles.rb', line 146 def Colors::ALL.map { |c| [c.name, c.slug] } end |
.colors ⇒ Object
The canonical color palette, frequency-ordered. => [Vehicles::Color, …]
140 141 142 |
# File 'lib/vehicles.rb', line 140 def colors Colors::ALL end |
.configuration ⇒ Object
— configuration ——————————————————-
30 31 32 |
# File 'lib/vehicles.rb', line 30 def configuration @configuration ||= Configuration.new end |
.configure {|configuration| ... } ⇒ Object
34 35 36 |
# File 'lib/vehicles.rb', line 34 def configure yield(configuration) end |
.data_version ⇒ Object
Version of the dataset currently in effect (refreshed cache or bundled), e.g. “2026.06.0”.
86 87 88 |
# File 'lib/vehicles.rb', line 86 def data_version dataset.version end |
.dataset ⇒ Object
67 68 69 |
# File 'lib/vehicles.rb', line 67 def dataset Dataset.load(active_data_path) end |
.find(query) ⇒ Object
Resolve a free-text “make + model” string into one Model (or nil).
117 118 119 |
# File 'lib/vehicles.rb', line 117 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.
224 225 226 227 228 229 |
# File 'lib/vehicles.rb', line 224 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 |
.load_validators! ⇒ Object
Require the ActiveModel validators (idempotent; no-op without ActiveModel).
236 237 238 239 240 241 242 243 |
# File 'lib/vehicles.rb', line 236 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) ⇒ Object
The rich Make object (or nil). Forgiving: name, slug, or alias.
112 113 114 |
# File 'lib/vehicles.rb', line 112 def make(query) dataset.find_make(query) end |
.make_options(kind: nil, region: nil) ⇒ Object
- [label, value], …
-
of makes for a Rails ‘select`.
160 161 162 |
# File 'lib/vehicles.rb', line 160 def (kind: nil, region: nil) dataset.makes(kind: kind, region: region || configuration.region).map { |m| [m.name, m.slug] } end |
.makes(kind: nil, region: nil) ⇒ Object
Make display names. => [“Abarth”, “Alfa Romeo”, …]
98 99 100 |
# File 'lib/vehicles.rb', line 98 def makes(kind: nil, region: nil) dataset.makes(kind: kind, region: region || configuration.region).map(&:name) end |
.model(make_name, model_name) ⇒ Object
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
127 128 129 130 |
# File 'lib/vehicles.rb', line 127 def model(make_name, model_name) found = make(make_name) found&.model(model_name) end |
.model_options(make, kind: nil, body_type: nil) ⇒ Object
- [label, value], …
-
of a make’s models for a Rails ‘select`. Unknown => [].
165 166 167 168 |
# File 'lib/vehicles.rb', line 165 def (make, kind: nil, body_type: nil) found = make(make) found ? found.(kind: kind, body_type: body_type) : [] end |
.models(make, kind: nil, body_type: nil, region: nil) ⇒ Object
Model display names for a make. => [“Golf”, “Polo”, …]. Unknown make => [].
103 104 105 106 107 108 109 |
# File 'lib/vehicles.rb', line 103 def models(make, kind: nil, body_type: nil, region: nil) region ||= configuration.region return [] if region && !dataset.region?(region) found = make(make) found ? found.models(kind: kind, body_type: body_type).map(&:name) : [] end |
.normalize(str) ⇒ Object
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”).
211 212 213 |
# File 'lib/vehicles.rb', line 211 def normalize(str) fold_diacritics(str).downcase.gsub(/[^a-z0-9]+/, " ").strip end |
.providers ⇒ Object
201 202 203 |
# File 'lib/vehicles.rb', line 201 def providers @providers ||= [Providers::HostedProvider, Providers::LocalProvider] end |
.refresh! ⇒ Object
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).
74 75 76 |
# File 'lib/vehicles.rb', line 74 def refresh! Refresher.refresh! end |
.region ⇒ Object
Region the bundled data covers, as a Symbol, e.g. :eu.
91 92 93 |
# File 'lib/vehicles.rb', line 91 def region dataset.region.to_s.downcase.to_sym end |
.reload! ⇒ Object
Drop the in-memory dataset so the next access reloads from disk (after a refresh, a cache clear, or a ‘data_path=` change).
80 81 82 |
# File 'lib/vehicles.rb', line 80 def reload! Dataset.reset! end |
.reset_configuration! ⇒ Object
Reset config + caches (config, data path, dataset, providers). Primarily for the test suite — genuinely returns the gem to a pristine state.
40 41 42 43 44 45 46 |
# File 'lib/vehicles.rb', line 40 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.
187 188 189 190 191 192 193 194 195 196 197 198 199 |
# File 'lib/vehicles.rb', line 187 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) ⇒ Object
Every model matching a query, ranked. => [Vehicles::Model, …]
133 134 135 |
# File 'lib/vehicles.rb', line 133 def search(query) dataset.search(query) end |
.slugify(str) ⇒ Object
Slugify a display name: “Alfa Romeo” => “alfa-romeo”, “Škoda” => “skoda”.
216 217 218 |
# File 'lib/vehicles.rb', line 216 def slugify(str) fold_diacritics(str).downcase.gsub(/[^a-z0-9]+/, "-").gsub(/(\A-|-\z)/, "") end |