vehicles 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vehicles
4
+ module Providers
5
+ # The always-available provider, backed by the bundled dataset. It answers
6
+ # whatever the local snapshot knows and returns nil for everything it doesn't
7
+ # (years/segment/image today) — which is exactly what makes graceful
8
+ # degradation work: the hosted provider enriches, this one guarantees the
9
+ # gem never breaks when the hosted data is absent.
10
+ module LocalProvider
11
+ module_function
12
+
13
+ def available?
14
+ true
15
+ end
16
+
17
+ # The bundled snapshot is make/model/kind/body_type only — richer fields
18
+ # come from the hosted API. Returning nil here lets the resolver move on
19
+ # (and ultimately yield nil) instead of raising.
20
+ def years(_model) = nil
21
+ def segment(_model) = nil
22
+ def image(_model, year:, color:) = nil
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/railtie"
4
+
5
+ module Vehicles
6
+ # Wires the gem into Rails without forcing anything: it only registers the
7
+ # ActiveModel validators, and only once ActiveRecord/ActiveModel is loaded.
8
+ # There's no engine, no migration, no table — the data lives in the bundled
9
+ # JSON, so there's nothing to install.
10
+ class Railtie < ::Rails::Railtie
11
+ initializer "vehicles.validators" do
12
+ ActiveSupport.on_load(:active_record) { Vehicles.load_validators! }
13
+ # Also cover plain-ActiveModel hosts (e.g. a Tableless model) loaded early.
14
+ Vehicles.load_validators! if defined?(ActiveModel::EachValidator)
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Vehicles
6
+ # Pulls the latest published dataset from the VehiclesDB data repo (config.data_url)
7
+ # into a local file cache (config.cache_path). This is how data fixes and new
8
+ # makes reach an app WITHOUT a gem upgrade — schedule `Vehicles.refresh!` (e.g.
9
+ # a daily job) and loads will prefer the cached data over the bundled snapshot.
10
+ #
11
+ # Error-isolated by design: a failed/partial download never corrupts the cache
12
+ # and never raises — the gem just keeps serving whatever it already had
13
+ # (cached, or the bundled snapshot). Atomic write so a reader never sees a
14
+ # half-written file.
15
+ module Refresher
16
+ MAX_REDIRECTS = 3
17
+
18
+ module_function
19
+
20
+ # Download + cache the latest dataset. Returns true on success, false on any
21
+ # failure (network, non-200, unparseable/empty payload). Never raises.
22
+ def refresh!
23
+ config = Vehicles.configuration
24
+ body = fetch(config.data_url, config.refresh_timeout)
25
+ return false unless valid_payload?(body)
26
+
27
+ write_atomic(config.cache_path, body)
28
+ Vehicles.reload! # drop the in-memory dataset so the next access uses fresh data
29
+ true
30
+ rescue StandardError => e
31
+ Vehicles.logger&.warn("[vehicles] refresh failed: #{e.class}: #{e.message}")
32
+ false
33
+ end
34
+
35
+ # True when a refreshed dataset is cached on disk.
36
+ def cached?
37
+ path = Vehicles.configuration.cache_path
38
+ !path.to_s.empty? && File.exist?(path)
39
+ end
40
+
41
+ def cached_path
42
+ cached? ? Vehicles.configuration.cache_path : nil
43
+ end
44
+
45
+ # Remove the cached dataset (the gem falls back to the bundled snapshot).
46
+ def clear_cache!
47
+ path = Vehicles.configuration.cache_path
48
+ File.delete(path) if path && File.exist?(path)
49
+ Vehicles.reload!
50
+ end
51
+
52
+ # --- internals -----------------------------------------------------------
53
+
54
+ # A payload is only accepted if it parses and looks like our dataset, so a
55
+ # CDN error page / truncated download can never replace good data.
56
+ def valid_payload?(body)
57
+ return false if body.to_s.empty?
58
+
59
+ data = JSON.parse(body)
60
+ data.is_a?(Hash) && data["makes"].is_a?(Array) && !data["makes"].empty?
61
+ rescue JSON::ParserError
62
+ false
63
+ end
64
+
65
+ def fetch(url, timeout, redirects_left = MAX_REDIRECTS)
66
+ require "net/http"
67
+ require "uri"
68
+
69
+ uri = URI(url)
70
+ http = Net::HTTP.new(uri.host, uri.port)
71
+ http.use_ssl = uri.scheme == "https"
72
+ http.open_timeout = timeout
73
+ http.read_timeout = timeout
74
+
75
+ response = http.get(uri.request_uri, "Accept" => "application/json")
76
+ case response
77
+ when Net::HTTPSuccess
78
+ response.body
79
+ when Net::HTTPRedirection
80
+ return nil if redirects_left <= 0
81
+
82
+ fetch(response["location"], timeout, redirects_left - 1)
83
+ end
84
+ end
85
+
86
+ def write_atomic(path, body)
87
+ require "fileutils"
88
+ FileUtils.mkdir_p(File.dirname(path))
89
+ tmp = "#{path}.#{Process.pid}.tmp"
90
+ File.write(tmp, body)
91
+ File.rename(tmp, path) # atomic on the same filesystem
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Drop-in validator: `validates :make, vehicle_make: true`.
4
+ #
5
+ # Rails resolves the `vehicle_make:` key to this top-level constant automatically
6
+ # (camelize + "Validator"), so no registration is needed — requiring the file is
7
+ # enough. Forgiving (aliases/case/slug) like every other lookup, and defensive:
8
+ # blank values pass (pair with `presence: true` if you want them rejected), and
9
+ # any internal error degrades to a generic message instead of blowing up a form.
10
+ class VehicleMakeValidator < ActiveModel::EachValidator
11
+ def validate_each(record, attribute, value)
12
+ return if value.blank?
13
+ return if Vehicles.make(value)
14
+
15
+ record.errors.add(attribute, options[:message] || "is not a recognized vehicle make")
16
+ rescue StandardError => e
17
+ Vehicles.logger&.error("[vehicles] vehicle_make validation error: #{e.message}")
18
+ record.errors.add(attribute, options[:message] || "is not a recognized vehicle make")
19
+ end
20
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Drop-in validator: `validates :model, vehicle_model: { make: :make }`.
4
+ #
5
+ # Checks that the value is a real model OF the make held in another attribute.
6
+ # Pass `make:` pointing at the attribute that holds the make (defaults to :make).
7
+ #
8
+ # validates :model, vehicle_model: { make: :car_make }
9
+ #
10
+ # Defensive by design: blank model passes; an unknown/blank make can't disprove
11
+ # the model, so it passes (let the make's own validator flag that); errors never
12
+ # raise out of a form submission.
13
+ class VehicleModelValidator < ActiveModel::EachValidator
14
+ def validate_each(record, attribute, value)
15
+ return if value.blank?
16
+
17
+ make_attribute = options[:make] || :make
18
+ make_value = record.respond_to?(make_attribute) ? record.public_send(make_attribute) : nil
19
+ make = Vehicles.make(make_value)
20
+ return if make.nil? # unknown make -> defer to the make validator
21
+ return if make.model(value)
22
+
23
+ record.errors.add(attribute, options[:message] || "is not a recognized #{make.name} model")
24
+ rescue StandardError => e
25
+ Vehicles.logger&.error("[vehicles] vehicle_model validation error: #{e.message}")
26
+ record.errors.add(attribute, options[:message] || "is not a recognized vehicle model")
27
+ end
28
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vehicles
4
+ VERSION = "0.1.0"
5
+ end
data/lib/vehicles.rb ADDED
@@ -0,0 +1,254 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "vehicles/version"
4
+ require_relative "vehicles/configuration"
5
+ require_relative "vehicles/make"
6
+ require_relative "vehicles/model"
7
+ require_relative "vehicles/color"
8
+ require_relative "vehicles/dataset"
9
+ require_relative "vehicles/refresher"
10
+ require_relative "vehicles/providers/local_provider"
11
+ require_relative "vehicles/providers/hosted_provider"
12
+
13
+ # Car makes & models for your Rails app — dropdowns, search, validation. Bundled
14
+ # data, zero config, no network calls. Standalone first; an SDK for the hosted
15
+ # VehiclesDB API second.
16
+ #
17
+ # Vehicles.makes # => ["Alfa Romeo", "Audi", "BMW", ...]
18
+ # Vehicles.models("VW") # => ["Golf", "Polo", "Tiguan", ...]
19
+ # Vehicles.find("vw golf") # => #<Vehicles::Model "Volkswagen Golf">
20
+ # Vehicles.make_options # => [["Alfa Romeo", "alfa-romeo"], ...] (for select)
21
+ module Vehicles
22
+ class Error < StandardError; end
23
+
24
+ # The bundled snapshot, packaged in the gem. Overridable via `Vehicles.data_path=`.
25
+ DATA_PATH = File.expand_path("../data/vehicles.json", __dir__)
26
+
27
+ class << self
28
+ # --- configuration -------------------------------------------------------
29
+
30
+ def configuration
31
+ @configuration ||= Configuration.new
32
+ end
33
+
34
+ def configure
35
+ yield(configuration)
36
+ end
37
+
38
+ # Reset config + caches (config, data path, dataset, providers). Primarily
39
+ # for the test suite — genuinely returns the gem to a pristine state.
40
+ def reset_configuration!
41
+ @configuration = Configuration.new
42
+ @providers = nil
43
+ @data_path = nil
44
+ Dataset.reset!
45
+ Providers::HostedProvider.reset!
46
+ end
47
+
48
+ # --- data access ---------------------------------------------------------
49
+
50
+ # Path to the bundled snapshot (or an explicit override via `data_path=`).
51
+ def data_path
52
+ @data_path || DATA_PATH
53
+ end
54
+
55
+ attr_writer :data_path, :logger
56
+
57
+ # The dataset file actually in effect: an explicit override wins; otherwise a
58
+ # refreshed cache (if present and `use_cache`); otherwise the bundled snapshot.
59
+ # This is how a refresh reaches the running app — no gem upgrade needed.
60
+ def active_data_path
61
+ return @data_path if @data_path
62
+ return Refresher.cached_path if configuration.use_cache && Refresher.cached?
63
+
64
+ DATA_PATH
65
+ end
66
+
67
+ def dataset
68
+ Dataset.load(active_data_path)
69
+ end
70
+
71
+ # Pull the latest published dataset into the local cache, so data fixes / new
72
+ # makes land WITHOUT a gem upgrade. Returns true/false; never raises. Schedule
73
+ # it (e.g. a daily job — `rails g vehicles:install` sets one up).
74
+ def refresh!
75
+ Refresher.refresh!
76
+ end
77
+
78
+ # Drop the in-memory dataset so the next access reloads from disk (after a
79
+ # refresh, a cache clear, or a `data_path=` change).
80
+ def reload!
81
+ Dataset.reset!
82
+ end
83
+
84
+ # Version of the dataset currently in effect (refreshed cache or bundled),
85
+ # e.g. "2026.06.0".
86
+ def data_version
87
+ dataset.version
88
+ end
89
+
90
+ # Region the bundled data covers, as a Symbol, e.g. :eu.
91
+ def region
92
+ dataset.region.to_s.downcase.to_sym
93
+ end
94
+
95
+ # --- core query API ------------------------------------------------------
96
+
97
+ # Make display names. => ["Abarth", "Alfa Romeo", ...]
98
+ def makes(kind: nil, region: nil)
99
+ dataset.makes(kind: kind, region: region || configuration.region).map(&:name)
100
+ end
101
+
102
+ # Model display names for a make. => ["Golf", "Polo", ...]. Unknown make => [].
103
+ def models(make, kind: nil, body_type: nil, region: nil)
104
+ region ||= configuration.region
105
+ return [] if region && !dataset.region?(region)
106
+
107
+ found = make(make)
108
+ found ? found.models(kind: kind, body_type: body_type).map(&:name) : []
109
+ end
110
+
111
+ # The rich Make object (or nil). Forgiving: name, slug, or alias.
112
+ def make(query)
113
+ dataset.find_make(query)
114
+ end
115
+
116
+ # Resolve a free-text "make + model" string into one Model (or nil).
117
+ def find(query)
118
+ dataset.find_model(query)
119
+ end
120
+
121
+ # Resolve a stored make + model PAIR into a Model (or nil). The structured
122
+ # counterpart to `find` (which parses one free-text string) — reach for this
123
+ # when your records keep make and model in separate columns and you want the
124
+ # model's metadata (kind, body_type, …) back.
125
+ # Vehicles.model("Audi", "A3") # => #<Vehicles::Model "Audi A3">
126
+ # Vehicles.model("vw", "golf") # forgiving, like every other lookup
127
+ def model(make_name, model_name)
128
+ found = make(make_name)
129
+ found&.model(model_name)
130
+ end
131
+
132
+ # Every model matching a query, ranked. => [Vehicles::Model, ...]
133
+ def search(query)
134
+ dataset.search(query)
135
+ end
136
+
137
+ # --- colors (canonical reference palette) --------------------------------
138
+
139
+ # The canonical color palette, frequency-ordered. => [Vehicles::Color, ...]
140
+ def colors
141
+ Colors::ALL
142
+ end
143
+
144
+ # [[name, slug], ...] for a Rails `select`. Names are English — localize the
145
+ # labels in your app; the slug is the stable value you store.
146
+ def color_options
147
+ Colors::ALL.map { |c| [c.name, c.slug] }
148
+ end
149
+
150
+ # Resolve a color by slug or name (forgiving: case, diacritics, synonyms like
151
+ # "gray"→grey, "navy"→blue). Returns a Vehicles::Color, or nil.
152
+ def color(query)
153
+ q = normalize(query)
154
+ return nil if q.empty?
155
+
156
+ Colors::BY_SLUG[q] || Colors::BY_NAME[q] || Colors::BY_SLUG[Colors::SYNONYMS[q]]
157
+ end
158
+
159
+ # [[label, value], ...] of makes for a Rails `select`.
160
+ def make_options(kind: nil, region: nil)
161
+ dataset.makes(kind: kind, region: region || configuration.region).map { |m| [m.name, m.slug] }
162
+ end
163
+
164
+ # [[label, value], ...] of a make's models for a Rails `select`. Unknown => [].
165
+ def model_options(make, kind: nil, body_type: nil)
166
+ found = make(make)
167
+ found ? found.model_options(kind: kind, body_type: body_type) : []
168
+ end
169
+
170
+ # A { make => [model names] } map for the given filters — everything you need
171
+ # to build a dependent make → model picker entirely on the client: embed it
172
+ # once (`Vehicles.catalog(kind: :car).to_json`) and switch the model list in a
173
+ # few lines of JS. No endpoint, no extra request, instant. The whole car
174
+ # catalog is small (tens of KB), so this is the simplest path for most apps.
175
+ # Vehicles.catalog(kind: :car) # => { "Audi" => ["A3", "A4", ...], ... }
176
+ def catalog(kind: nil, region: nil)
177
+ makes(kind: kind, region: region).to_h do |name|
178
+ [name, models(name, kind: kind, region: region)]
179
+ end
180
+ end
181
+
182
+ # --- provider resolution (hosted enrichment, optional) -------------------
183
+
184
+ # Ask each available provider for an attribute, hosted first, until one gives
185
+ # a non-nil answer. Returns nil if none can. Never raises. Backs Model#years
186
+ # / #segment / #image.
187
+ def resolve(attribute, model, **opts)
188
+ providers.each do |provider|
189
+ next unless provider.available?
190
+
191
+ value = provider.public_send(attribute, model, **opts)
192
+ return value unless value.nil?
193
+ rescue StandardError => e
194
+ # A misbehaving provider must never break a model read — log and move on.
195
+ logger&.error("[vehicles] provider #{provider} failed on #{attribute}: #{e.message}")
196
+ next
197
+ end
198
+ nil
199
+ end
200
+
201
+ def providers
202
+ @providers ||= [Providers::HostedProvider, Providers::LocalProvider]
203
+ end
204
+
205
+ # --- helpers -------------------------------------------------------------
206
+
207
+ # Match-normalize a string: fold diacritics, downcase, collapse anything
208
+ # non-alphanumeric to single spaces. "Mercedes-Benz" => "mercedes benz",
209
+ # "Škoda" => "skoda". Used everywhere lookups need to be forgiving — so it must
210
+ # NEVER raise (the API contract is "garbage in => empty/nil out, not an error").
211
+ def normalize(str)
212
+ fold_diacritics(str).downcase.gsub(/[^a-z0-9]+/, " ").strip
213
+ end
214
+
215
+ # Slugify a display name: "Alfa Romeo" => "alfa-romeo", "Škoda" => "skoda".
216
+ def slugify(str)
217
+ fold_diacritics(str).downcase.gsub(/[^a-z0-9]+/, "-").gsub(/(\A-|-\z)/, "")
218
+ end
219
+
220
+ # Shared diacritic folding for normalize/slugify. NFKD splits "š" into "s" + a
221
+ # combining caron; we strip the combining marks (\p{Mn}) BEFORE the separator
222
+ # gsub, or "Škoda" would become "s koda". Defends against non-UTF-8/invalid
223
+ # encodings (e.g. a binary string) so callers never hit Encoding errors.
224
+ def fold_diacritics(str)
225
+ s = str.to_s
226
+ s = s.dup.force_encoding(Encoding::UTF_8) unless s.encoding == Encoding::UTF_8
227
+ s = s.scrub("") unless s.valid_encoding?
228
+ s.unicode_normalize(:nfkd).gsub(/\p{Mn}+/, "")
229
+ end
230
+
231
+ def logger
232
+ @logger ||= (defined?(Rails) && Rails.respond_to?(:logger) ? Rails.logger : nil)
233
+ end
234
+
235
+ # Require the ActiveModel validators (idempotent; no-op without ActiveModel).
236
+ def load_validators!
237
+ return if @validators_loaded
238
+ return unless defined?(ActiveModel::EachValidator)
239
+
240
+ require_relative "vehicles/validators/vehicle_make_validator"
241
+ require_relative "vehicles/validators/vehicle_model_validator"
242
+ @validators_loaded = true
243
+ end
244
+ end
245
+ end
246
+
247
+ # Loaded after the module so its lookup tables can use Vehicles.normalize.
248
+ require_relative "vehicles/colors"
249
+
250
+ # Rails integration is opt-in and detected at load time — the gem works fine in
251
+ # plain Ruby without it.
252
+ require_relative "vehicles/railtie" if defined?(Rails::Railtie)
253
+ # If ActiveModel is already present (e.g. required directly), wire validators now.
254
+ Vehicles.load_validators! if defined?(ActiveModel::EachValidator)
data/sig/vehicles.rbs ADDED
@@ -0,0 +1,94 @@
1
+ # Type signatures for the public API. (Best-effort; the README and code are the
2
+ # source of truth.)
3
+
4
+ module Vehicles
5
+ VERSION: String
6
+ DATA_PATH: String
7
+
8
+ def self.configuration: () -> Configuration
9
+ def self.configure: () { (Configuration) -> void } -> void
10
+ def self.reset_configuration!: () -> void
11
+
12
+ def self.data_path: () -> String
13
+ def self.data_version: () -> String?
14
+ def self.region: () -> Symbol
15
+
16
+ def self.makes: (?kind: Symbol?, ?region: Symbol?) -> Array[String]
17
+ def self.models: (untyped make, ?kind: Symbol?, ?body_type: Symbol?, ?region: Symbol?) -> Array[String]
18
+ def self.make: (untyped query) -> Make?
19
+ def self.find: (untyped query) -> Model?
20
+ def self.model: (untyped make_name, untyped model_name) -> Model?
21
+ def self.search: (untyped query) -> Array[Model]
22
+ def self.make_options: (?kind: Symbol?, ?region: Symbol?) -> Array[[String, String]]
23
+ def self.model_options: (untyped make, ?kind: Symbol?, ?body_type: Symbol?) -> Array[[String, String]]
24
+ def self.catalog: (?kind: Symbol?, ?region: Symbol?) -> Hash[String, Array[String]]
25
+
26
+ def self.colors: () -> Array[Color]
27
+ def self.color: (untyped query) -> Color?
28
+ def self.color_options: () -> Array[[String, String]]
29
+
30
+ def self.dataset: () -> Dataset
31
+ def self.data_path: () -> String
32
+ def self.data_path=: (String) -> void
33
+ def self.active_data_path: () -> String
34
+ def self.refresh!: () -> bool
35
+ def self.reload!: () -> void
36
+ def self.normalize: (untyped str) -> String
37
+ def self.slugify: (untyped str) -> String
38
+
39
+ class Error < StandardError
40
+ end
41
+
42
+ class Configuration
43
+ attr_accessor region: Symbol
44
+ attr_accessor api_key: String?
45
+ attr_accessor api_base_url: String
46
+ attr_accessor api_timeout: Integer
47
+ attr_reader aliases: Hash[String, String]
48
+ attr_accessor data_url: String
49
+ attr_accessor cache_path: String
50
+ attr_accessor use_cache: bool
51
+ attr_accessor refresh_timeout: Integer
52
+ end
53
+
54
+ class Make
55
+ attr_reader name: String
56
+ attr_reader slug: String
57
+ attr_reader aliases: Array[String]
58
+ attr_reader kinds: Array[Symbol]
59
+ def models: (?kind: Symbol?, ?body_type: Symbol?) -> Array[Model]
60
+ def model: (untyped query) -> Model?
61
+ def model_names: (?kind: Symbol?, ?body_type: Symbol?) -> Array[String]
62
+ def model_options: (?kind: Symbol?, ?body_type: Symbol?) -> Array[[String, String]]
63
+ def to_h: () -> Hash[Symbol, untyped]
64
+ end
65
+
66
+ class Color
67
+ attr_reader slug: String
68
+ attr_reader name: String
69
+ attr_reader hex: String
70
+ def to_h: () -> Hash[Symbol, String]
71
+ end
72
+
73
+ class Model
74
+ KINDS: Array[Symbol]
75
+ BODY_TYPES: Array[Symbol]
76
+ attr_reader make: String
77
+ attr_reader make_slug: String
78
+ attr_reader name: String
79
+ attr_reader kind: Symbol
80
+ attr_reader body_type: Symbol
81
+ def full_name: () -> String
82
+ def to_s: () -> String
83
+ def slug: () -> String
84
+ def model_slug: () -> String
85
+ def to_h: () -> Hash[Symbol, untyped]
86
+ # predicate sugar for each kind/body type: car?, suv?, hatchback?, coupe?, ...
87
+ def car?: () -> bool
88
+ def suv?: () -> bool
89
+ def hatchback?: () -> bool
90
+ def years: () -> Range[Integer]?
91
+ def segment: () -> Symbol?
92
+ def image: (?year: Integer?, ?color: untyped) -> String?
93
+ end
94
+ end
metadata ADDED
@@ -0,0 +1,72 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: vehicles
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - rameerez
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 2026-06-23 00:00:00.000000000 Z
11
+ dependencies: []
12
+ description: vehicles ships a curated, bundled dataset of car makes and models (with
13
+ kind and body type) and a delightful, Rails-friendly API for vehicle dropdowns,
14
+ search, and validation. No API keys, no network calls, no database table — the data
15
+ lives inside the gem and just works. It also acts as the SDK for the hosted VehiclesDB
16
+ API. Code is MIT; the bundled data is CC-BY 4.0 (derived from RDW Open Data).
17
+ email:
18
+ - rubygems@rameerez.com
19
+ executables: []
20
+ extensions: []
21
+ extra_rdoc_files: []
22
+ files:
23
+ - CHANGELOG.md
24
+ - LICENSE.txt
25
+ - README.md
26
+ - data/vehicles.json
27
+ - lib/generators/vehicles/install_generator.rb
28
+ - lib/generators/vehicles/templates/initializer.rb
29
+ - lib/generators/vehicles/templates/vehicles_refresh_job.rb
30
+ - lib/vehicles.rb
31
+ - lib/vehicles/color.rb
32
+ - lib/vehicles/colors.rb
33
+ - lib/vehicles/configuration.rb
34
+ - lib/vehicles/dataset.rb
35
+ - lib/vehicles/make.rb
36
+ - lib/vehicles/model.rb
37
+ - lib/vehicles/providers/hosted_provider.rb
38
+ - lib/vehicles/providers/local_provider.rb
39
+ - lib/vehicles/railtie.rb
40
+ - lib/vehicles/refresher.rb
41
+ - lib/vehicles/validators/vehicle_make_validator.rb
42
+ - lib/vehicles/validators/vehicle_model_validator.rb
43
+ - lib/vehicles/version.rb
44
+ - sig/vehicles.rbs
45
+ homepage: https://github.com/rameerez/vehicles
46
+ licenses:
47
+ - MIT
48
+ metadata:
49
+ allowed_push_host: https://rubygems.org
50
+ homepage_uri: https://github.com/rameerez/vehicles
51
+ source_code_uri: https://github.com/rameerez/vehicles/tree/main
52
+ changelog_uri: https://github.com/rameerez/vehicles/blob/main/CHANGELOG.md
53
+ rubygems_mfa_required: 'true'
54
+ rdoc_options: []
55
+ require_paths:
56
+ - lib
57
+ required_ruby_version: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: 3.1.0
62
+ required_rubygems_version: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - ">="
65
+ - !ruby/object:Gem::Version
66
+ version: '0'
67
+ requirements: []
68
+ rubygems_version: 3.6.2
69
+ specification_version: 4
70
+ summary: Car makes & models for your Rails app — dropdowns, search, validation. Zero
71
+ config, no API keys.
72
+ test_files: []