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.make_options            # => [["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.expand_path("../data/vehicles.json", __dir__)
VERSION =
"0.1.1"

Class Attribute Summary collapse

Class Method Summary collapse

Class Attribute Details

.data_pathObject

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

.loggerObject



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_pathObject

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_optionsObject

[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 color_options
  Colors::ALL.map { |c| [c.name, c.slug] }
end

.colorsObject

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



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

def colors
  Colors::ALL
end

.configurationObject

— configuration ——————————————————-



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

def configuration
  @configuration ||= Configuration.new
end

.configure {|configuration| ... } ⇒ Object

Yields:



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

def configure
  yield(configuration)
end

.data_versionObject

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

.datasetObject



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 make_options(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 model_options(make, kind: nil, body_type: nil)
  found = make(make)
  found ? found.model_options(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

.providersObject



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

.regionObject

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.message}")
    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