vehicles 0.6.1 → 0.8.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.
- checksums.yaml +4 -4
- data/.rubocop_todo.yml +164 -0
- data/CHANGELOG.md +274 -0
- data/README.md +55 -10
- data/data/PROVENANCE-vehicles.md +12 -0
- data/data/plates/PROVENANCE.md +5 -4
- data/data/plates/_decode/at-districts.yml +287 -0
- data/data/plates/_decode/ch-cantons.yml +110 -0
- data/data/plates/_decode/cn-issuing-offices.yml +463 -0
- data/data/plates/_decode/cn-provinces.yml +614 -0
- data/data/plates/_decode/cz-kraj.yml +159 -0
- data/data/plates/_decode/dk-serial-blocks.yml +327 -0
- data/data/plates/_decode/fr-departements.yml +191 -0
- data/data/plates/_decode/gb-age-identifiers.yml +170 -0
- data/data/plates/_decode/gb-memory-tags.yml +202 -0
- data/data/plates/_decode/gr-prefectures.yml +1408 -0
- data/data/plates/_decode/hr-cities.yml +315 -0
- data/data/plates/_decode/ie-counties.yml +227 -0
- data/data/plates/_decode/in-state-codes.yml +169 -0
- data/data/plates/_decode/is-districts-1938.yml +116 -0
- data/data/plates/_decode/it-provinces.yml +348 -0
- data/data/plates/_decode/jp-office-marks.yml +960 -0
- data/data/plates/_decode/jp-use-marks.yml +205 -0
- data/data/plates/_decode/kr-authority-marks.yml +146 -0
- data/data/plates/_decode/kr-use-syllables.yml +194 -0
- data/data/plates/_decode/lu-series-order.yml +638 -0
- data/data/plates/_decode/mx-entity-series.yml +380 -0
- data/data/plates/_decode/my-index-marks.yml +271 -0
- data/data/plates/_decode/no-counties-1913.yml +148 -0
- data/data/plates/_decode/no-districts.yml +901 -0
- data/data/plates/_decode/pl-powiats.yml +567 -0
- data/data/plates/_decode/pt-trailer-services.yml +96 -0
- data/data/plates/_decode/ro-counties.yml +213 -0
- data/data/plates/_decode/se-diplomatic.yml +220 -0
- data/data/plates/_decode/si-regions.yml +123 -0
- data/data/plates/_decode/sk-districts.yml +314 -0
- data/data/plates/_decode/th-registration-elements.yml +208 -0
- data/data/plates/_decode/ua-regions.yml +393 -0
- data/data/plates/_decode/za-licence-marks.yml +215 -0
- data/data/plates/_meta/classes.yml +1 -0
- data/data/plates/_meta/separators.yml +102 -0
- data/data/plates/ad.yml +961 -0
- data/data/plates/ar.yml +762 -0
- data/data/plates/at.yml +1424 -0
- data/data/plates/au-act.yml +472 -0
- data/data/plates/au-nsw.yml +282 -0
- data/data/plates/au-nt.yml +431 -0
- data/data/plates/au-qld.yml +272 -0
- data/data/plates/au-sa.yml +248 -0
- data/data/plates/au-tas.yml +302 -0
- data/data/plates/au-vic.yml +215 -0
- data/data/plates/au-wa.yml +309 -0
- data/data/plates/be.yml +1840 -0
- data/data/plates/br.yml +1152 -0
- data/data/plates/ca-ab.yml +430 -0
- data/data/plates/ca-bc.yml +697 -0
- data/data/plates/ca-mb.yml +499 -0
- data/data/plates/ca-nb.yml +537 -0
- data/data/plates/ca-nl.yml +743 -0
- data/data/plates/ca-ns.yml +808 -0
- data/data/plates/ca-nt.yml +610 -0
- data/data/plates/ca-nu.yml +280 -0
- data/data/plates/ca-on.yml +881 -0
- data/data/plates/ca-pe.yml +592 -0
- data/data/plates/ca-qc.yml +1036 -0
- data/data/plates/ca-sk.yml +256 -0
- data/data/plates/ca-yt.yml +656 -0
- data/data/plates/ch.yml +1273 -0
- data/data/plates/cn.yml +1431 -0
- data/data/plates/cy.yml +688 -0
- data/data/plates/cz.yml +1179 -0
- data/data/plates/de.yml +553 -118
- data/data/plates/dk.yml +1647 -0
- data/data/plates/ee.yml +1275 -0
- data/data/plates/es.yml +228 -32
- data/data/plates/fi.yml +1271 -0
- data/data/plates/fr.yml +1265 -0
- data/data/plates/gb.yml +1188 -0
- data/data/plates/gr.yml +721 -0
- data/data/plates/hr.yml +860 -0
- data/data/plates/hu.yml +959 -0
- data/data/plates/ie.yml +775 -0
- data/data/plates/in.yml +1172 -0
- data/data/plates/is.yml +667 -0
- data/data/plates/it.yml +1317 -0
- data/data/plates/jp.yml +3650 -0
- data/data/plates/kr.yml +1375 -0
- data/data/plates/li.yml +1010 -0
- data/data/plates/lt.yml +879 -0
- data/data/plates/lu.yml +842 -0
- data/data/plates/lv.yml +924 -0
- data/data/plates/mc.yml +1024 -0
- data/data/plates/mt.yml +854 -0
- data/data/plates/mx.yml +1372 -0
- data/data/plates/my.yml +685 -0
- data/data/plates/nl.yml +44 -0
- data/data/plates/no.yml +1018 -0
- data/data/plates/nz.yml +1699 -0
- data/data/plates/pl.yml +1296 -0
- data/data/plates/pt.yml +847 -0
- data/data/plates/ro.yml +785 -0
- data/data/plates/se.yml +1091 -0
- data/data/plates/si.yml +1105 -0
- data/data/plates/sk.yml +1439 -0
- data/data/plates/sm.yml +1119 -0
- data/data/plates/th.yml +1150 -0
- data/data/plates/ua.yml +1672 -0
- data/data/plates/us-ak.yml +763 -0
- data/data/plates/us-al.yml +457 -0
- data/data/plates/us-ar.yml +427 -0
- data/data/plates/us-as.yml +454 -0
- data/data/plates/us-az.yml +430 -0
- data/data/plates/us-ca.yml +892 -0
- data/data/plates/us-co.yml +452 -0
- data/data/plates/us-ct.yml +427 -0
- data/data/plates/us-dc.yml +614 -0
- data/data/plates/us-de.yml +551 -0
- data/data/plates/us-fl.yml +319 -96
- data/data/plates/us-ga.yml +644 -0
- data/data/plates/us-gu.yml +658 -0
- data/data/plates/us-hi.yml +585 -0
- data/data/plates/us-ia.yml +438 -0
- data/data/plates/us-id.yml +531 -0
- data/data/plates/us-il.yml +516 -0
- data/data/plates/us-in.yml +324 -0
- data/data/plates/us-ks.yml +351 -0
- data/data/plates/us-ky.yml +770 -0
- data/data/plates/us-la.yml +568 -0
- data/data/plates/us-ma.yml +379 -0
- data/data/plates/us-md.yml +536 -0
- data/data/plates/us-me.yml +445 -0
- data/data/plates/us-mi.yml +408 -0
- data/data/plates/us-mn.yml +411 -0
- data/data/plates/us-mo.yml +356 -0
- data/data/plates/us-mp.yml +402 -0
- data/data/plates/us-ms.yml +432 -0
- data/data/plates/us-mt.yml +619 -0
- data/data/plates/us-nc.yml +602 -0
- data/data/plates/us-nd.yml +419 -0
- data/data/plates/us-ne.yml +483 -0
- data/data/plates/us-nh.yml +394 -0
- data/data/plates/us-nj.yml +547 -0
- data/data/plates/us-nm.yml +574 -0
- data/data/plates/us-nv.yml +655 -0
- data/data/plates/us-ny.yml +592 -0
- data/data/plates/us-oh.yml +404 -0
- data/data/plates/us-ok.yml +513 -0
- data/data/plates/us-or.yml +392 -0
- data/data/plates/us-pa.yml +506 -0
- data/data/plates/us-pr.yml +507 -0
- data/data/plates/us-ri.yml +445 -0
- data/data/plates/us-sc.yml +605 -0
- data/data/plates/us-sd.yml +428 -0
- data/data/plates/us-tn.yml +471 -0
- data/data/plates/us-tx.yml +535 -0
- data/data/plates/us-ut.yml +384 -0
- data/data/plates/us-va.yml +567 -0
- data/data/plates/us-vi.yml +489 -0
- data/data/plates/us-vt.yml +458 -0
- data/data/plates/us-wa.yml +484 -0
- data/data/plates/us-wi.yml +509 -0
- data/data/plates/us-wv.yml +446 -0
- data/data/plates/us-wy.yml +523 -0
- data/data/plates/va.yml +604 -0
- data/data/plates/za.yml +1141 -0
- data/data/vehicles.json +160653 -105090
- data/lib/vehicles/configuration.rb +17 -3
- data/lib/vehicles/dataset.rb +73 -18
- data/lib/vehicles/fuzzy.rb +115 -0
- data/lib/vehicles/http.rb +48 -0
- data/lib/vehicles/make.rb +36 -6
- data/lib/vehicles/mcp_server.rb +5 -5
- data/lib/vehicles/model.rb +21 -2
- data/lib/vehicles/plates/series.rb +172 -18
- data/lib/vehicles/plates/template_fill.rb +188 -0
- data/lib/vehicles/plates/templates.rb +162 -0
- data/lib/vehicles/plates.rb +12 -0
- data/lib/vehicles/providers/hosted_provider.rb +28 -55
- data/lib/vehicles/validators/vehicle_make_validator.rb +3 -1
- data/lib/vehicles/validators/vehicle_model_validator.rb +1 -1
- data/lib/vehicles/version.rb +1 -1
- data/lib/vehicles.rb +32 -16
- data/rakelib/data_refresh.rake +109 -0
- data/rakelib/plates_refresh.rake +29 -0
- data/sig/vehicles.rbs +18 -0
- metadata +164 -2
|
@@ -4,34 +4,38 @@ require "json"
|
|
|
4
4
|
|
|
5
5
|
module Vehicles
|
|
6
6
|
module Providers
|
|
7
|
-
# Talks to the hosted VehiclesDB API
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
7
|
+
# Talks to the hosted VehiclesDB API for vehicle imagery. It is STRICTLY
|
|
8
|
+
# OPTIONAL: `available?` is true only when an api_key is configured, so a
|
|
9
|
+
# key-less install never reaches the network. Every call is error-isolated
|
|
10
|
+
# — a slow, missing, or failing API yields nil and the resolver falls back
|
|
11
|
+
# to the local data. Enrichment must never break the host app, so this
|
|
12
|
+
# never raises out.
|
|
13
13
|
#
|
|
14
14
|
# The images endpoint is LIVE (since 2026-07): mint a key at
|
|
15
15
|
# https://vehiclesdb.com/settings/api-keys, set `config.api_key`, and
|
|
16
16
|
# `model.image` / `model.images` answer with rendered vehicle imagery.
|
|
17
|
-
#
|
|
18
|
-
#
|
|
17
|
+
#
|
|
18
|
+
# `years` and `segment` are the wired-up seam for enrichment the API does
|
|
19
|
+
# not serve yet: they answer nil WITHOUT a request (a call to an endpoint
|
|
20
|
+
# that doesn't exist would only cost latency), so the chain falls through
|
|
21
|
+
# to the local data. They light up here when the API ships them.
|
|
19
22
|
module HostedProvider
|
|
23
|
+
# Every VehiclesDB API endpoint lives under this path on
|
|
24
|
+
# `config.api_base_url` (https://vehiclesdb.com/api/v1/...).
|
|
25
|
+
API_PREFIX = "/api/v1"
|
|
26
|
+
|
|
20
27
|
module_function
|
|
21
28
|
|
|
22
29
|
def available?
|
|
23
30
|
!Vehicles.configuration.api_key.to_s.empty?
|
|
24
31
|
end
|
|
25
32
|
|
|
26
|
-
def years(
|
|
27
|
-
|
|
28
|
-
return nil unless data && data["year_start"]
|
|
29
|
-
|
|
30
|
-
(data["year_start"]..data["year_end"]) # year_end may be nil => endless range
|
|
33
|
+
def years(_model)
|
|
34
|
+
nil
|
|
31
35
|
end
|
|
32
36
|
|
|
33
|
-
def segment(
|
|
34
|
-
|
|
37
|
+
def segment(_model)
|
|
38
|
+
nil
|
|
35
39
|
end
|
|
36
40
|
|
|
37
41
|
# One variant URL — the common "just give me an <img src>" case.
|
|
@@ -46,7 +50,7 @@ module Vehicles
|
|
|
46
50
|
# the API actually served, `requested_color` what you asked for (a color
|
|
47
51
|
# that isn't rendered yet falls back rather than 404ing).
|
|
48
52
|
#
|
|
49
|
-
# GET /v1/vehicles/:kind/:make_slug/:model_slug/images?color=<slug>
|
|
53
|
+
# GET /api/v1/vehicles/:kind/:make_slug/:model_slug/images?color=<slug>
|
|
50
54
|
#
|
|
51
55
|
# `year`/`trim` filters are RESERVED server-side today (the API 422s on
|
|
52
56
|
# them by contract, so callers can't silently build on unimplemented
|
|
@@ -55,50 +59,19 @@ module Vehicles
|
|
|
55
59
|
def images(model, color: nil)
|
|
56
60
|
params = {}
|
|
57
61
|
params[:color] = color.to_s unless color.to_s.empty?
|
|
58
|
-
get("/
|
|
62
|
+
get("#{API_PREFIX}/vehicles/#{model.kind}/#{model.make_slug}/#{model.model_slug}/images", params)
|
|
59
63
|
end
|
|
60
64
|
|
|
61
65
|
# --- internals -----------------------------------------------------------
|
|
62
66
|
|
|
63
|
-
#
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
return @cache[model.slug] if @cache.key?(model.slug)
|
|
67
|
-
|
|
68
|
-
@cache[model.slug] = get("/v1/models/#{model.slug}", {})
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
def reset!
|
|
72
|
-
@cache = {}
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
# Issue a GET and parse JSON. Returns nil on ANY failure (network, timeout,
|
|
76
|
-
# non-200, bad JSON) — callers treat nil as "fall back to local data".
|
|
67
|
+
# Issue a keyed GET and parse JSON. Returns nil on ANY failure (network,
|
|
68
|
+
# timeout, non-200, bad JSON) — callers treat nil as "fall back to local
|
|
69
|
+
# data". The wire rules (timeout, no retry) are Vehicles::HTTP's.
|
|
77
70
|
def get(path, params)
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
config = Vehicles.configuration
|
|
84
|
-
uri = URI.join(config.api_base_url, path)
|
|
85
|
-
uri.query = URI.encode_www_form(params) unless params.empty?
|
|
86
|
-
|
|
87
|
-
http = Net::HTTP.new(uri.host, uri.port)
|
|
88
|
-
http.use_ssl = uri.scheme == "https"
|
|
89
|
-
http.open_timeout = config.api_timeout
|
|
90
|
-
http.read_timeout = config.api_timeout
|
|
91
|
-
|
|
92
|
-
request = Net::HTTP::Get.new(uri)
|
|
93
|
-
request["Authorization"] = "Bearer #{config.api_key}"
|
|
94
|
-
request["Accept"] = "application/json"
|
|
95
|
-
|
|
96
|
-
response = http.request(request)
|
|
97
|
-
return nil unless response.is_a?(Net::HTTPSuccess)
|
|
98
|
-
|
|
99
|
-
JSON.parse(response.body)
|
|
100
|
-
rescue StandardError => e
|
|
101
|
-
warn "[vehicles] hosted lookup failed (#{e.class}); using local data" if $DEBUG
|
|
71
|
+
body = HTTP.get(HTTP.url(path, params),
|
|
72
|
+
"Authorization" => "Bearer #{Vehicles.configuration.api_key}", "Accept" => "application/json")
|
|
73
|
+
body && JSON.parse(body)
|
|
74
|
+
rescue StandardError # bad JSON, a malformed api_base_url
|
|
102
75
|
nil
|
|
103
76
|
end
|
|
104
77
|
end
|
|
@@ -15,7 +15,9 @@ class VehicleMakeValidator < ActiveModel::EachValidator
|
|
|
15
15
|
def validate_each(record, attribute, value)
|
|
16
16
|
return if value.blank?
|
|
17
17
|
return if options[:allow_other] && Vehicles.other?(value)
|
|
18
|
-
|
|
18
|
+
# Exact-only: a typo must fail validation, not be stored as if it were the
|
|
19
|
+
# make it resembles (Vehicles.make is typo-tolerant since 0.7.8).
|
|
20
|
+
return if Vehicles.make(value, fuzzy: false)
|
|
19
21
|
|
|
20
22
|
record.errors.add(attribute, options[:message] || "is not a recognized vehicle make")
|
|
21
23
|
rescue StandardError => e
|
|
@@ -20,7 +20,7 @@ class VehicleModelValidator < ActiveModel::EachValidator
|
|
|
20
20
|
|
|
21
21
|
make_attribute = options[:make] || :make
|
|
22
22
|
make_value = record.respond_to?(make_attribute) ? record.public_send(make_attribute) : nil
|
|
23
|
-
make = Vehicles.make(make_value)
|
|
23
|
+
make = Vehicles.make(make_value, fuzzy: false) # exact, same rule as the make validator
|
|
24
24
|
return if make.nil? # unknown make -> defer to the make validator
|
|
25
25
|
return if make.model(value)
|
|
26
26
|
|
data/lib/vehicles/version.rb
CHANGED
data/lib/vehicles.rb
CHANGED
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "vehicles/version"
|
|
4
4
|
require_relative "vehicles/configuration"
|
|
5
|
+
require_relative "vehicles/http"
|
|
5
6
|
require_relative "vehicles/make"
|
|
6
7
|
require_relative "vehicles/model"
|
|
7
8
|
require_relative "vehicles/color"
|
|
9
|
+
require_relative "vehicles/fuzzy"
|
|
8
10
|
require_relative "vehicles/dataset"
|
|
9
11
|
require_relative "vehicles/refresher"
|
|
10
12
|
require_relative "vehicles/providers/local_provider"
|
|
@@ -13,6 +15,8 @@ require_relative "vehicles/other_option"
|
|
|
13
15
|
require_relative "vehicles/plates"
|
|
14
16
|
require_relative "vehicles/plates/jurisdiction"
|
|
15
17
|
require_relative "vehicles/plates/series"
|
|
18
|
+
require_relative "vehicles/plates/template_fill"
|
|
19
|
+
require_relative "vehicles/plates/templates"
|
|
16
20
|
|
|
17
21
|
# Car makes & models for your Rails app — dropdowns, search, validation. Bundled
|
|
18
22
|
# data, zero config, no network calls. Standalone first; an SDK for the hosted
|
|
@@ -46,7 +50,6 @@ module Vehicles
|
|
|
46
50
|
@providers = nil
|
|
47
51
|
@data_path = nil
|
|
48
52
|
Dataset.reset!
|
|
49
|
-
Providers::HostedProvider.reset!
|
|
50
53
|
end
|
|
51
54
|
|
|
52
55
|
# --- data access ---------------------------------------------------------
|
|
@@ -111,20 +114,28 @@ module Vehicles
|
|
|
111
114
|
# Vehicles.models("Toyota", include_other: true) # ... + the "Other" escape hatch
|
|
112
115
|
# With `include_other:`, an unknown make (e.g. the "Other" make itself) yields
|
|
113
116
|
# just `[other_label]`, so a make→model picker never dead-ends.
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
+
# `fuzzy:` is forwarded to `make` (typo-tolerant by default since 0.7.8).
|
|
118
|
+
def models(make, kind: nil, body_type: nil, region: nil, rarity: nil, max_decile: nil, include_other: false,
|
|
119
|
+
fuzzy: true)
|
|
120
|
+
names = make(make, fuzzy: fuzzy)&.models(kind: kind, body_type: body_type,
|
|
121
|
+
region: region || configuration.region,
|
|
122
|
+
rarity: rarity, max_decile: max_decile)&.map(&:name) || []
|
|
117
123
|
append_other_name(names, include_other)
|
|
118
124
|
end
|
|
119
125
|
|
|
120
|
-
# The rich Make object (or nil). Forgiving: name, slug,
|
|
121
|
-
|
|
122
|
-
|
|
126
|
+
# The rich Make object (or nil). Forgiving: name, slug, alias — and, since
|
|
127
|
+
# 0.7.8, typos: "marcedes" → Mercedes-Benz, "volkswagon" → Volkswagen
|
|
128
|
+
# (bounded edit distance, only when ONE make is closest; ties → nil).
|
|
129
|
+
# Pass `fuzzy: false` for exact-only resolution (the validators do).
|
|
130
|
+
def make(query, fuzzy: true)
|
|
131
|
+
dataset.find_make(query, fuzzy: fuzzy)
|
|
123
132
|
end
|
|
124
133
|
|
|
125
134
|
# Resolve a free-text "make + model" string into one Model (or nil).
|
|
126
|
-
|
|
127
|
-
|
|
135
|
+
# Exact makes are tried first; a misspelled make is retried with the same
|
|
136
|
+
# typo tolerance as `make` ("volkswagon golf"). `fuzzy: false` disables it.
|
|
137
|
+
def find(query, fuzzy: true)
|
|
138
|
+
dataset.find_model(query, fuzzy: fuzzy)
|
|
128
139
|
end
|
|
129
140
|
|
|
130
141
|
# Resolve a stored make + model PAIR into a Model (or nil). The structured
|
|
@@ -133,9 +144,13 @@ module Vehicles
|
|
|
133
144
|
# model's metadata (kind, body_type, …) back.
|
|
134
145
|
# Vehicles.model("Audi", "A3") # => #<Vehicles::Model "Audi A3">
|
|
135
146
|
# Vehicles.model("vw", "golf") # forgiving, like every other lookup
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
147
|
+
# Vehicles.model("Kia", "XCeed") # spacing-blind: the dataset's "X Ceed"
|
|
148
|
+
# Vehicles.model("BMW", "Serie 1") # => BMW 1 Series
|
|
149
|
+
# Vehicles.model("Toyota", "Land Cruiser") # => the car (preferred)
|
|
150
|
+
# Vehicles.model("Toyota", "Land Cruiser", kind: :bus)
|
|
151
|
+
def model(make_name, model_name, fuzzy: true, kind: nil)
|
|
152
|
+
found = make(make_name, fuzzy: fuzzy)
|
|
153
|
+
found&.model(model_name, kind: kind)
|
|
139
154
|
end
|
|
140
155
|
|
|
141
156
|
# License plates (the bundled PRD-PLATES dataset). No argument: every
|
|
@@ -170,8 +185,9 @@ module Vehicles
|
|
|
170
185
|
# first. Filter by country (ISO alpha-2) or continent (:eu/:as/…).
|
|
171
186
|
# Vehicles.top_models(kind: :car, country: :nl, limit: 10).map(&:name)
|
|
172
187
|
# Vehicles.top_models(kind: :motorcycle, region: :as, limit: 10)
|
|
173
|
-
|
|
174
|
-
|
|
188
|
+
# Vehicles.top_models(kind: :car, by: :mass_decile) # by registration mass (data 2026.10.1+)
|
|
189
|
+
def top_models(kind: nil, country: nil, region: nil, limit: 20, by: :global_decile)
|
|
190
|
+
dataset.top_models(kind: kind, country: country, region: region, limit: limit, by: by)
|
|
175
191
|
end
|
|
176
192
|
|
|
177
193
|
# A curated slice of models by kind/continent/rarity — the "give me sensible
|
|
@@ -219,8 +235,8 @@ module Vehicles
|
|
|
219
235
|
|
|
220
236
|
# [[label, value], ...] of a make's models for a Rails `select`. Unknown => [].
|
|
221
237
|
# Vehicles.model_options("audi", include_other: true) # ... + [other_label, "other"]
|
|
222
|
-
def model_options(make, kind: nil, body_type: nil, include_other: false)
|
|
223
|
-
found = make(make)
|
|
238
|
+
def model_options(make, kind: nil, body_type: nil, include_other: false, fuzzy: true)
|
|
239
|
+
found = make(make, fuzzy: fuzzy)
|
|
224
240
|
opts = found ? found.model_options(kind: kind, body_type: body_type) : []
|
|
225
241
|
append_other_option(opts, include_other)
|
|
226
242
|
end
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "English"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
# The bundled dataset refresh (data/vehicles.json), as a task so the
|
|
7
|
+
# provenance step and the country-rank merge can't be forgotten.
|
|
8
|
+
#
|
|
9
|
+
# rake "data:refresh[v2026.10.0]" # a published release TAG, never a branch
|
|
10
|
+
#
|
|
11
|
+
# Source: the public data repo at a release tag — dist/vehicles.json (the gem
|
|
12
|
+
# projection) unchanged, EXCEPT that each model gains `country_ranks`
|
|
13
|
+
# ({ "gb" => 2, … }) when the projection does not carry it yet. Those ranks
|
|
14
|
+
# are copied from the SAME tag's catalog/<kind>/models.json
|
|
15
|
+
# (popularity.by_country[cc].rank — open CC-BY data), keyed by the full
|
|
16
|
+
# kind/make/model id, so the merged file is what the pipeline's own projection
|
|
17
|
+
# emits once it ships the key (vehiclesdb-pipeline emit.rb write_dist_json!,
|
|
18
|
+
# 2026-10-10). When the projection DOES carry the key, the task asserts every
|
|
19
|
+
# rank equals the catalog's and aborts on any mismatch. Output formatting is
|
|
20
|
+
# the pipeline's (JSON.pretty_generate + "\n"); stripping `country_ranks`
|
|
21
|
+
# from the output gives back the release file byte for byte.
|
|
22
|
+
module DataRefresh
|
|
23
|
+
module_function
|
|
24
|
+
|
|
25
|
+
def git_show(repo, rev, path)
|
|
26
|
+
out = IO.popen(["git", "-C", repo, "show", "#{rev}:#{path}"], &:read)
|
|
27
|
+
abort "missing #{path} at #{rev}" unless $CHILD_STATUS.success?
|
|
28
|
+
out
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# "kind/make/model" => { "gb" => 2, … } for every ranked model at the tag.
|
|
32
|
+
def catalog_ranks(repo, rev, kinds)
|
|
33
|
+
kinds.each_with_object({}) do |kind, ranks|
|
|
34
|
+
JSON.parse(git_show(repo, rev, "catalog/#{kind}/models.json")).each do |m|
|
|
35
|
+
by_country = m.dig("popularity", "by_country") || {}
|
|
36
|
+
r = by_country.filter_map { |cc, p| [cc, p["rank"]] if p["rank"] }.sort.to_h
|
|
37
|
+
ranks["#{kind}/#{m["id"]}"] = r unless r.empty?
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Insert `country_ranks` where the pipeline emits it (right after
|
|
43
|
+
# global_decile), or assert it when the projection already carries it.
|
|
44
|
+
# Returns [merged, native] counts.
|
|
45
|
+
def merge!(dist, ranks)
|
|
46
|
+
merged = native = 0
|
|
47
|
+
dist["makes"].each do |mk|
|
|
48
|
+
mk["models"].each do |m|
|
|
49
|
+
id = "#{m["kind"]}/#{mk["slug"]}/#{m["slug"]}"
|
|
50
|
+
want = ranks[id]
|
|
51
|
+
if m.key?("country_ranks")
|
|
52
|
+
native += 1
|
|
53
|
+
abort "country_ranks mismatch for #{id}" unless m["country_ranks"] == want
|
|
54
|
+
elsif want
|
|
55
|
+
m.replace(with_ranks(m, want))
|
|
56
|
+
merged += 1
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
[merged, native]
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def with_ranks(model, ranks)
|
|
64
|
+
out = model.each_with_object({}) do |(k, v), h|
|
|
65
|
+
h[k] = v
|
|
66
|
+
h["country_ranks"] = ranks if k == "global_decile"
|
|
67
|
+
end
|
|
68
|
+
out["country_ranks"] ||= ranks
|
|
69
|
+
out
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def provenance(tag, commit, version, merged, native)
|
|
73
|
+
<<~MD
|
|
74
|
+
# Bundled vehicles dataset
|
|
75
|
+
|
|
76
|
+
`data/vehicles.json` is the public data repo's `dist/vehicles.json`
|
|
77
|
+
(the gem projection) at a release tag:
|
|
78
|
+
|
|
79
|
+
- **Source**: https://github.com/vehiclesdb/vehiclesdb `dist/vehicles.json`
|
|
80
|
+
- **Tag**: `#{tag}` (commit `#{commit}`), dataset version `#{version}`
|
|
81
|
+
- **country_ranks**: #{native} model(s) carried it natively; #{merged} merged from
|
|
82
|
+
the same tag's `catalog/<kind>/models.json` `popularity.by_country[cc].rank`
|
|
83
|
+
- **License**: CC-BY 4.0 (see the data repo's LICENSE + ATTRIBUTION.md)
|
|
84
|
+
|
|
85
|
+
Refresh: `rake "data:refresh[<tag>]"` (this file is generated by it).
|
|
86
|
+
MD
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
namespace :data do
|
|
91
|
+
desc "Refresh data/vehicles.json from a release tag of the local dataset repo"
|
|
92
|
+
task :refresh, [:tag] do |_t, args|
|
|
93
|
+
tag = args[:tag] or abort 'usage: rake "data:refresh[vYYYY.MM.P]" (a release tag)'
|
|
94
|
+
root = File.expand_path("..", __dir__)
|
|
95
|
+
repo = File.expand_path(ENV.fetch("VDB_DATA_REPO", "~/GitHub/vehiclesdb"))
|
|
96
|
+
sh "git", "-C", repo, "fetch", "origin", "--tags", "-q"
|
|
97
|
+
# refs/tags/ only: a branch name or a bare SHA must not pass for a release.
|
|
98
|
+
commit = IO.popen(["git", "-C", repo, "rev-parse", "--verify", "-q", "refs/tags/#{tag}^{commit}"], &:read).strip
|
|
99
|
+
abort "not a release tag: #{tag}" unless $CHILD_STATUS.success? && !commit.empty?
|
|
100
|
+
|
|
101
|
+
# Read at the VERIFIED commit, not the tag name (a same-named branch must never win).
|
|
102
|
+
dist = JSON.parse(DataRefresh.git_show(repo, commit, "dist/vehicles.json"))
|
|
103
|
+
merged, native = DataRefresh.merge!(dist, DataRefresh.catalog_ranks(repo, commit, dist["kinds"]))
|
|
104
|
+
File.write("#{root}/data/vehicles.json", "#{JSON.pretty_generate(dist)}\n")
|
|
105
|
+
File.write("#{root}/data/PROVENANCE-vehicles.md",
|
|
106
|
+
DataRefresh.provenance(tag, commit, dist["version"], merged, native))
|
|
107
|
+
puts "data refreshed to #{tag} (#{dist["version"]}): #{merged} merged, #{native} native country_ranks"
|
|
108
|
+
end
|
|
109
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# The plates-data refresh, as a task so the PROVENANCE step can't be
|
|
2
|
+
# forgotten (it is gem-local; a bare rm+archive deletes it silently —
|
|
3
|
+
# that shipped once in 0.7.4 and broke consumers reading the pin).
|
|
4
|
+
namespace :plates do
|
|
5
|
+
desc "Refresh data/plates from the local dataset repo's origin/main"
|
|
6
|
+
task :refresh do
|
|
7
|
+
root = File.expand_path("..", __dir__)
|
|
8
|
+
ds = File.expand_path("~/GitHub/vehiclesdb")
|
|
9
|
+
sh "git -C #{ds} fetch origin -q"
|
|
10
|
+
rm_rf "#{root}/data/plates"
|
|
11
|
+
mkdir_p "#{root}/data/plates"
|
|
12
|
+
sh "git -C #{ds} archive origin/main plates | tar -x --exclude 'plates/_art' -C #{root}/data/"
|
|
13
|
+
rm_rf "#{root}/data/plates/_art"
|
|
14
|
+
commit = `git -C #{ds} rev-parse origin/main`.strip
|
|
15
|
+
File.write("#{root}/data/plates/PROVENANCE.md", <<~MD)
|
|
16
|
+
# Bundled plates dataset (PRD-PLATES, full corpus)
|
|
17
|
+
|
|
18
|
+
Copy of `plates/` from the public data repo (EXCLUDING `_art/`),
|
|
19
|
+
refreshed on gem releases:
|
|
20
|
+
|
|
21
|
+
- **Source**: https://github.com/vehiclesdb/vehiclesdb `plates/`
|
|
22
|
+
- **Commit**: `#{commit}`
|
|
23
|
+
- **License**: CC-BY 4.0 (see the data repo's LICENSE + ATTRIBUTION.md)
|
|
24
|
+
|
|
25
|
+
Refresh: `rake plates:refresh` (this file is generated by it).
|
|
26
|
+
MD
|
|
27
|
+
puts "plates refreshed at #{commit}"
|
|
28
|
+
end
|
|
29
|
+
end
|
data/sig/vehicles.rbs
CHANGED
|
@@ -49,6 +49,24 @@ module Vehicles
|
|
|
49
49
|
attr_accessor cache_path: String
|
|
50
50
|
attr_accessor use_cache: bool
|
|
51
51
|
attr_accessor refresh_timeout: Integer
|
|
52
|
+
attr_accessor plate_template_cache: untyped
|
|
53
|
+
attr_accessor plate_template_ttl: Integer
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
module Plates
|
|
57
|
+
def self.template: (code: String | Symbol, series: String, ?shape: String?, ?region: String?) -> String?
|
|
58
|
+
def self.render: (String registration, jurisdiction: String | Symbol, ?region: String?, ?label: String?) -> String?
|
|
59
|
+
|
|
60
|
+
module TemplateFill
|
|
61
|
+
class Error < StandardError
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
SCHEMA_VERSION: Integer
|
|
65
|
+
def self.fill: (String svg, String registration, ?label: String?) -> String
|
|
66
|
+
def self.shape_of: (String text) -> String?
|
|
67
|
+
def self.fit_size: (Numeric size, Numeric room, Numeric natural_width) -> Float
|
|
68
|
+
def self.recentered_baseline: (Numeric origin, Numeric size, Numeric per_size) -> Float
|
|
69
|
+
end
|
|
52
70
|
end
|
|
53
71
|
|
|
54
72
|
class Make
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: vehicles
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.8.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- rameerez
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-
|
|
10
|
+
date: 2026-11-09 00:00:00.000000000 Z
|
|
11
11
|
dependencies: []
|
|
12
12
|
description: vehicles ships a curated, bundled dataset of car makes and models (with
|
|
13
13
|
kind and body type) and a delightful, Rails-friendly API for vehicle dropdowns,
|
|
@@ -21,16 +21,172 @@ executables:
|
|
|
21
21
|
extensions: []
|
|
22
22
|
extra_rdoc_files: []
|
|
23
23
|
files:
|
|
24
|
+
- ".rubocop_todo.yml"
|
|
24
25
|
- CHANGELOG.md
|
|
25
26
|
- LICENSE.txt
|
|
26
27
|
- README.md
|
|
28
|
+
- data/PROVENANCE-vehicles.md
|
|
27
29
|
- data/plates/PROVENANCE.md
|
|
30
|
+
- data/plates/_decode/at-districts.yml
|
|
31
|
+
- data/plates/_decode/ch-cantons.yml
|
|
32
|
+
- data/plates/_decode/cn-issuing-offices.yml
|
|
33
|
+
- data/plates/_decode/cn-provinces.yml
|
|
34
|
+
- data/plates/_decode/cz-kraj.yml
|
|
35
|
+
- data/plates/_decode/dk-serial-blocks.yml
|
|
28
36
|
- data/plates/_decode/es-provinces.yml
|
|
37
|
+
- data/plates/_decode/fr-departements.yml
|
|
38
|
+
- data/plates/_decode/gb-age-identifiers.yml
|
|
39
|
+
- data/plates/_decode/gb-memory-tags.yml
|
|
40
|
+
- data/plates/_decode/gr-prefectures.yml
|
|
41
|
+
- data/plates/_decode/hr-cities.yml
|
|
42
|
+
- data/plates/_decode/ie-counties.yml
|
|
43
|
+
- data/plates/_decode/in-state-codes.yml
|
|
44
|
+
- data/plates/_decode/is-districts-1938.yml
|
|
45
|
+
- data/plates/_decode/it-provinces.yml
|
|
46
|
+
- data/plates/_decode/jp-office-marks.yml
|
|
47
|
+
- data/plates/_decode/jp-use-marks.yml
|
|
48
|
+
- data/plates/_decode/kr-authority-marks.yml
|
|
49
|
+
- data/plates/_decode/kr-use-syllables.yml
|
|
50
|
+
- data/plates/_decode/lu-series-order.yml
|
|
51
|
+
- data/plates/_decode/mx-entity-series.yml
|
|
52
|
+
- data/plates/_decode/my-index-marks.yml
|
|
53
|
+
- data/plates/_decode/no-counties-1913.yml
|
|
54
|
+
- data/plates/_decode/no-districts.yml
|
|
55
|
+
- data/plates/_decode/pl-powiats.yml
|
|
56
|
+
- data/plates/_decode/pt-trailer-services.yml
|
|
57
|
+
- data/plates/_decode/ro-counties.yml
|
|
58
|
+
- data/plates/_decode/se-diplomatic.yml
|
|
59
|
+
- data/plates/_decode/si-regions.yml
|
|
60
|
+
- data/plates/_decode/sk-districts.yml
|
|
61
|
+
- data/plates/_decode/th-registration-elements.yml
|
|
62
|
+
- data/plates/_decode/ua-regions.yml
|
|
63
|
+
- data/plates/_decode/za-licence-marks.yml
|
|
29
64
|
- data/plates/_meta/classes.yml
|
|
65
|
+
- data/plates/_meta/separators.yml
|
|
66
|
+
- data/plates/ad.yml
|
|
67
|
+
- data/plates/ar.yml
|
|
68
|
+
- data/plates/at.yml
|
|
69
|
+
- data/plates/au-act.yml
|
|
70
|
+
- data/plates/au-nsw.yml
|
|
71
|
+
- data/plates/au-nt.yml
|
|
72
|
+
- data/plates/au-qld.yml
|
|
73
|
+
- data/plates/au-sa.yml
|
|
74
|
+
- data/plates/au-tas.yml
|
|
75
|
+
- data/plates/au-vic.yml
|
|
76
|
+
- data/plates/au-wa.yml
|
|
77
|
+
- data/plates/be.yml
|
|
78
|
+
- data/plates/br.yml
|
|
79
|
+
- data/plates/ca-ab.yml
|
|
80
|
+
- data/plates/ca-bc.yml
|
|
81
|
+
- data/plates/ca-mb.yml
|
|
82
|
+
- data/plates/ca-nb.yml
|
|
83
|
+
- data/plates/ca-nl.yml
|
|
84
|
+
- data/plates/ca-ns.yml
|
|
85
|
+
- data/plates/ca-nt.yml
|
|
86
|
+
- data/plates/ca-nu.yml
|
|
87
|
+
- data/plates/ca-on.yml
|
|
88
|
+
- data/plates/ca-pe.yml
|
|
89
|
+
- data/plates/ca-qc.yml
|
|
90
|
+
- data/plates/ca-sk.yml
|
|
91
|
+
- data/plates/ca-yt.yml
|
|
92
|
+
- data/plates/ch.yml
|
|
93
|
+
- data/plates/cn.yml
|
|
94
|
+
- data/plates/cy.yml
|
|
95
|
+
- data/plates/cz.yml
|
|
30
96
|
- data/plates/de.yml
|
|
97
|
+
- data/plates/dk.yml
|
|
98
|
+
- data/plates/ee.yml
|
|
31
99
|
- data/plates/es.yml
|
|
100
|
+
- data/plates/fi.yml
|
|
101
|
+
- data/plates/fr.yml
|
|
102
|
+
- data/plates/gb.yml
|
|
103
|
+
- data/plates/gr.yml
|
|
104
|
+
- data/plates/hr.yml
|
|
105
|
+
- data/plates/hu.yml
|
|
106
|
+
- data/plates/ie.yml
|
|
107
|
+
- data/plates/in.yml
|
|
108
|
+
- data/plates/is.yml
|
|
109
|
+
- data/plates/it.yml
|
|
110
|
+
- data/plates/jp.yml
|
|
111
|
+
- data/plates/kr.yml
|
|
112
|
+
- data/plates/li.yml
|
|
113
|
+
- data/plates/lt.yml
|
|
114
|
+
- data/plates/lu.yml
|
|
115
|
+
- data/plates/lv.yml
|
|
116
|
+
- data/plates/mc.yml
|
|
117
|
+
- data/plates/mt.yml
|
|
118
|
+
- data/plates/mx.yml
|
|
119
|
+
- data/plates/my.yml
|
|
32
120
|
- data/plates/nl.yml
|
|
121
|
+
- data/plates/no.yml
|
|
122
|
+
- data/plates/nz.yml
|
|
123
|
+
- data/plates/pl.yml
|
|
124
|
+
- data/plates/pt.yml
|
|
125
|
+
- data/plates/ro.yml
|
|
126
|
+
- data/plates/se.yml
|
|
127
|
+
- data/plates/si.yml
|
|
128
|
+
- data/plates/sk.yml
|
|
129
|
+
- data/plates/sm.yml
|
|
130
|
+
- data/plates/th.yml
|
|
131
|
+
- data/plates/ua.yml
|
|
132
|
+
- data/plates/us-ak.yml
|
|
133
|
+
- data/plates/us-al.yml
|
|
134
|
+
- data/plates/us-ar.yml
|
|
135
|
+
- data/plates/us-as.yml
|
|
136
|
+
- data/plates/us-az.yml
|
|
137
|
+
- data/plates/us-ca.yml
|
|
138
|
+
- data/plates/us-co.yml
|
|
139
|
+
- data/plates/us-ct.yml
|
|
140
|
+
- data/plates/us-dc.yml
|
|
141
|
+
- data/plates/us-de.yml
|
|
33
142
|
- data/plates/us-fl.yml
|
|
143
|
+
- data/plates/us-ga.yml
|
|
144
|
+
- data/plates/us-gu.yml
|
|
145
|
+
- data/plates/us-hi.yml
|
|
146
|
+
- data/plates/us-ia.yml
|
|
147
|
+
- data/plates/us-id.yml
|
|
148
|
+
- data/plates/us-il.yml
|
|
149
|
+
- data/plates/us-in.yml
|
|
150
|
+
- data/plates/us-ks.yml
|
|
151
|
+
- data/plates/us-ky.yml
|
|
152
|
+
- data/plates/us-la.yml
|
|
153
|
+
- data/plates/us-ma.yml
|
|
154
|
+
- data/plates/us-md.yml
|
|
155
|
+
- data/plates/us-me.yml
|
|
156
|
+
- data/plates/us-mi.yml
|
|
157
|
+
- data/plates/us-mn.yml
|
|
158
|
+
- data/plates/us-mo.yml
|
|
159
|
+
- data/plates/us-mp.yml
|
|
160
|
+
- data/plates/us-ms.yml
|
|
161
|
+
- data/plates/us-mt.yml
|
|
162
|
+
- data/plates/us-nc.yml
|
|
163
|
+
- data/plates/us-nd.yml
|
|
164
|
+
- data/plates/us-ne.yml
|
|
165
|
+
- data/plates/us-nh.yml
|
|
166
|
+
- data/plates/us-nj.yml
|
|
167
|
+
- data/plates/us-nm.yml
|
|
168
|
+
- data/plates/us-nv.yml
|
|
169
|
+
- data/plates/us-ny.yml
|
|
170
|
+
- data/plates/us-oh.yml
|
|
171
|
+
- data/plates/us-ok.yml
|
|
172
|
+
- data/plates/us-or.yml
|
|
173
|
+
- data/plates/us-pa.yml
|
|
174
|
+
- data/plates/us-pr.yml
|
|
175
|
+
- data/plates/us-ri.yml
|
|
176
|
+
- data/plates/us-sc.yml
|
|
177
|
+
- data/plates/us-sd.yml
|
|
178
|
+
- data/plates/us-tn.yml
|
|
179
|
+
- data/plates/us-tx.yml
|
|
180
|
+
- data/plates/us-ut.yml
|
|
181
|
+
- data/plates/us-va.yml
|
|
182
|
+
- data/plates/us-vi.yml
|
|
183
|
+
- data/plates/us-vt.yml
|
|
184
|
+
- data/plates/us-wa.yml
|
|
185
|
+
- data/plates/us-wi.yml
|
|
186
|
+
- data/plates/us-wv.yml
|
|
187
|
+
- data/plates/us-wy.yml
|
|
188
|
+
- data/plates/va.yml
|
|
189
|
+
- data/plates/za.yml
|
|
34
190
|
- data/vehicles.json
|
|
35
191
|
- exe/vehicles-mcp
|
|
36
192
|
- lib/generators/vehicles/install_generator.rb
|
|
@@ -41,6 +197,8 @@ files:
|
|
|
41
197
|
- lib/vehicles/colors.rb
|
|
42
198
|
- lib/vehicles/configuration.rb
|
|
43
199
|
- lib/vehicles/dataset.rb
|
|
200
|
+
- lib/vehicles/fuzzy.rb
|
|
201
|
+
- lib/vehicles/http.rb
|
|
44
202
|
- lib/vehicles/make.rb
|
|
45
203
|
- lib/vehicles/mcp_server.rb
|
|
46
204
|
- lib/vehicles/model.rb
|
|
@@ -48,6 +206,8 @@ files:
|
|
|
48
206
|
- lib/vehicles/plates.rb
|
|
49
207
|
- lib/vehicles/plates/jurisdiction.rb
|
|
50
208
|
- lib/vehicles/plates/series.rb
|
|
209
|
+
- lib/vehicles/plates/template_fill.rb
|
|
210
|
+
- lib/vehicles/plates/templates.rb
|
|
51
211
|
- lib/vehicles/providers/hosted_provider.rb
|
|
52
212
|
- lib/vehicles/providers/local_provider.rb
|
|
53
213
|
- lib/vehicles/railtie.rb
|
|
@@ -55,6 +215,8 @@ files:
|
|
|
55
215
|
- lib/vehicles/validators/vehicle_make_validator.rb
|
|
56
216
|
- lib/vehicles/validators/vehicle_model_validator.rb
|
|
57
217
|
- lib/vehicles/version.rb
|
|
218
|
+
- rakelib/data_refresh.rake
|
|
219
|
+
- rakelib/plates_refresh.rake
|
|
58
220
|
- sig/vehicles.rbs
|
|
59
221
|
homepage: https://github.com/vehiclesdb/vehicles
|
|
60
222
|
licenses:
|