vehicles 0.4.0 → 0.6.1
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/CHANGELOG.md +55 -1
- data/README.md +36 -8
- data/data/plates/PROVENANCE.md +9 -0
- data/data/plates/_decode/es-provinces.yml +116 -0
- data/data/plates/_meta/classes.yml +25 -0
- data/data/plates/de.yml +672 -0
- data/data/plates/es.yml +692 -0
- data/data/plates/nl.yml +972 -0
- data/data/plates/us-fl.yml +518 -0
- data/data/vehicles.json +28109 -37241
- data/lib/vehicles/configuration.rb +4 -2
- data/lib/vehicles/model.rb +23 -6
- data/lib/vehicles/plates/jurisdiction.rb +32 -0
- data/lib/vehicles/plates/series.rb +137 -0
- data/lib/vehicles/plates.rb +110 -0
- data/lib/vehicles/providers/hosted_provider.rb +26 -8
- data/lib/vehicles/providers/local_provider.rb +4 -3
- data/lib/vehicles/version.rb +1 -1
- data/lib/vehicles.rb +21 -0
- metadata +12 -2
|
@@ -22,7 +22,9 @@ module Vehicles
|
|
|
22
22
|
# works on the bundled data — the gem is standalone first, SDK second.
|
|
23
23
|
attr_accessor :api_key
|
|
24
24
|
|
|
25
|
-
#
|
|
25
|
+
# ORIGIN of the hosted VehiclesDB API — scheme + host only, no path
|
|
26
|
+
# (endpoint paths carry their own /v1 prefix, and URI.join would drop a
|
|
27
|
+
# path suffix here anyway). Overridable for self-hosting/testing.
|
|
26
28
|
attr_accessor :api_base_url
|
|
27
29
|
|
|
28
30
|
# Network timeout (seconds) for hosted API calls. Kept short so a slow/missing
|
|
@@ -63,7 +65,7 @@ module Vehicles
|
|
|
63
65
|
def initialize
|
|
64
66
|
@region = nil # no continent filter by default (global dataset)
|
|
65
67
|
@api_key = nil
|
|
66
|
-
@api_base_url = "https://
|
|
68
|
+
@api_base_url = "https://vehiclesdb.com"
|
|
67
69
|
@api_timeout = 2
|
|
68
70
|
@aliases = {}
|
|
69
71
|
@other_label = "Other"
|
data/lib/vehicles/model.rb
CHANGED
|
@@ -25,10 +25,10 @@ module Vehicles
|
|
|
25
25
|
REGIONS = %i[eu na as sa oc af].freeze
|
|
26
26
|
|
|
27
27
|
attr_reader :make, :make_slug, :name, :kind, :body_type, :global_decile,
|
|
28
|
-
:availability, :regions, :aliases
|
|
28
|
+
:availability, :regions, :aliases, :former_ids
|
|
29
29
|
|
|
30
30
|
# @param attrs [Hash] one model entry from the dataset (name/slug/kind +
|
|
31
|
-
# optional body_type/global_decile/availability/regions/aliases)
|
|
31
|
+
# optional body_type/global_decile/availability/regions/aliases/former_ids)
|
|
32
32
|
# @param make [String] the parent make's display name
|
|
33
33
|
# @param make_slug [String] the parent make's slug (for the composite slug)
|
|
34
34
|
def initialize(attrs, make:, make_slug:)
|
|
@@ -51,6 +51,10 @@ module Vehicles
|
|
|
51
51
|
@regions = (attrs["regions"] || []).map(&:to_sym).freeze
|
|
52
52
|
# Documented alternate names (nicknames, native scripts, market names).
|
|
53
53
|
@aliases = (attrs["aliases"] || []).freeze
|
|
54
|
+
# Full canonical ids ("car/alfa-romeo/159sw") this record absorbed via
|
|
55
|
+
# the dataset's append-only migration contract (SCHEMA.md: renames
|
|
56
|
+
# alias, nothing is silently deleted). Empty for never-renamed records.
|
|
57
|
+
@former_ids = (attrs["former_ids"] || []).freeze
|
|
54
58
|
freeze
|
|
55
59
|
end
|
|
56
60
|
|
|
@@ -126,7 +130,8 @@ module Vehicles
|
|
|
126
130
|
def to_h
|
|
127
131
|
{ make: make, model: name, slug: slug, kind: kind, body_type: body_type,
|
|
128
132
|
global_decile: global_decile, rarity: rarity,
|
|
129
|
-
availability: availability, regions: regions, aliases: aliases
|
|
133
|
+
availability: availability, regions: regions, aliases: aliases,
|
|
134
|
+
former_ids: former_ids }
|
|
130
135
|
end
|
|
131
136
|
|
|
132
137
|
# Value-object equality — two models with the same slug are equal.
|
|
@@ -159,9 +164,21 @@ module Vehicles
|
|
|
159
164
|
Vehicles.resolve(:segment, self)
|
|
160
165
|
end
|
|
161
166
|
|
|
162
|
-
#
|
|
163
|
-
|
|
164
|
-
|
|
167
|
+
# One rendered image URL, ready for an <img src>. `size` picks the
|
|
168
|
+
# variant (:sm/:md/:lg), `color` a palette slug (Vehicles.colors); an
|
|
169
|
+
# un-rendered color falls back honestly server-side rather than erroring.
|
|
170
|
+
# `year` is accepted for forward-compatibility but not yet served.
|
|
171
|
+
# nil without hosted data — always render a placeholder path.
|
|
172
|
+
def image(year: nil, color: nil, size: :md)
|
|
173
|
+
Vehicles.resolve(:image, self, year: year, color: color, size: size)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# The full hosted images payload — every variant with dimensions, the
|
|
177
|
+
# rendered palette for this model, served-vs-requested color, provenance.
|
|
178
|
+
# Reach for this when one URL isn't enough (srcset, color pickers,
|
|
179
|
+
# caching whole responses). nil without hosted data.
|
|
180
|
+
def images(color: nil)
|
|
181
|
+
Vehicles.resolve(:images, self, color: color)
|
|
165
182
|
end
|
|
166
183
|
end
|
|
167
184
|
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Vehicles
|
|
4
|
+
module Plates
|
|
5
|
+
# One issuing jurisdiction — a country, or a state where plates are
|
|
6
|
+
# state-issued ("us-fl"; a bare "us" will never exist). Wraps the raw
|
|
7
|
+
# dataset YAML: authority, binding (vehicle vs owner), the
|
|
8
|
+
# jurisdiction-wide charset law, and every series.
|
|
9
|
+
class Jurisdiction
|
|
10
|
+
attr_reader :code, :authority_name, :authority_url, :binding, :charset, :series
|
|
11
|
+
|
|
12
|
+
def self.load(path)
|
|
13
|
+
raw = YAML.safe_load_file(path, aliases: true)
|
|
14
|
+
new(code: File.basename(path, ".yml"), raw: raw)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def initialize(code:, raw:)
|
|
18
|
+
@code = code
|
|
19
|
+
@authority_name = raw.dig("authority", "name")
|
|
20
|
+
@authority_url = raw.dig("authority", "url")
|
|
21
|
+
@binding = raw["binding"]
|
|
22
|
+
@charset = (raw["charset_defaults"] || {}).freeze
|
|
23
|
+
@series = (raw["series"] || []).map { |entry| Series.new(entry) }
|
|
24
|
+
.sort_by { |s| [ s.klass == "standard" ? 0 : 1, s.klass, s.period["start"] || 0 ] }.freeze
|
|
25
|
+
freeze
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def to_s = code
|
|
29
|
+
def inspect = %(#<Vehicles::Plates::Jurisdiction #{code} (#{series.size} series)>)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Vehicles
|
|
4
|
+
module Plates
|
|
5
|
+
# One registration series: a pattern identity with a period, a class
|
|
6
|
+
# (standard/taxi/diplomatic/…), categories, sourced design facts, and the
|
|
7
|
+
# two regexes of the dataset's regex policy — `regex` (recall: the
|
|
8
|
+
# pattern's own alphabet) and `regex_strict` (validation: the alphabet
|
|
9
|
+
# the authority actually issues). Validation prefers strict.
|
|
10
|
+
class Series
|
|
11
|
+
attr_reader :id, :klass, :categories, :period, :pattern, :regex,
|
|
12
|
+
:regex_strict, :design, :variants, :sources, :notes,
|
|
13
|
+
:validation_regexp, :lenient_regexp
|
|
14
|
+
|
|
15
|
+
def initialize(entry)
|
|
16
|
+
@id = entry["id"]
|
|
17
|
+
@klass = entry["class"] || "standard"
|
|
18
|
+
@categories = Array(entry["categories"]).freeze
|
|
19
|
+
@period = (entry["period"] || {}).freeze
|
|
20
|
+
@pattern = entry.dig("format", "pattern")
|
|
21
|
+
@regex = entry.dig("format", "regex")
|
|
22
|
+
@regex_strict = entry.dig("format", "regex_strict")
|
|
23
|
+
@design = (entry["design"] || {}).freeze
|
|
24
|
+
@variants = Array(entry["variants"]).freeze
|
|
25
|
+
@sources = Array(entry["sources"]).freeze
|
|
26
|
+
@notes = entry["notes"]
|
|
27
|
+
# Compiled eagerly — the object is frozen, so no lazy memoization.
|
|
28
|
+
@validation_regexp = compile(@regex_strict || @regex)
|
|
29
|
+
@lenient_regexp = compile(self.class.strip_separators(@regex_strict || @regex))
|
|
30
|
+
freeze
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# A strict-validated series has an authority-alphabet regex twin; a
|
|
34
|
+
# recall-only series (export/dark-blue catch-alls that accept any
|
|
35
|
+
# current shape) matches loosely by design. Consumers validating user
|
|
36
|
+
# input should weight strict hits above recall-only ones — Match does.
|
|
37
|
+
def strict?
|
|
38
|
+
!regex_strict.nil?
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# "2000 →" (open, runs until exhausted) or "1971–2000".
|
|
42
|
+
def period_label
|
|
43
|
+
finish = period["end"]
|
|
44
|
+
finish ? "#{period["start"]}–#{finish}" : "#{period["start"]} →"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Re-print a separator-less serial the way this series formats it:
|
|
48
|
+
# walk the pattern; 9/L and literal serial characters consume one input
|
|
49
|
+
# character, separator characters re-emerge from the pattern itself.
|
|
50
|
+
def format_serial(serial)
|
|
51
|
+
return nil if serial.nil? || pattern.nil?
|
|
52
|
+
|
|
53
|
+
idx = 0
|
|
54
|
+
out = pattern.each_char.with_object(+"") do |ch, str|
|
|
55
|
+
if [ "-", " ", "·", "." ].include?(ch)
|
|
56
|
+
str << ch
|
|
57
|
+
else
|
|
58
|
+
str << (serial[idx] || "")
|
|
59
|
+
idx += 1
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
out.freeze
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Tokens that read as "a separator" when they make up a whole
|
|
66
|
+
# character class — `[- ]` in the ES consular series is dash-or-space,
|
|
67
|
+
# i.e. a separator spelled as a class.
|
|
68
|
+
SEPARATOR_TOKENS = [ " ", "-", "·", ".", '\s', '\-', '\.', '\ ' ].freeze
|
|
69
|
+
|
|
70
|
+
# Remove separator LITERALS from a regex source, respecting character
|
|
71
|
+
# classes (the "-" inside [A-Z] is a range, not a separator; a class
|
|
72
|
+
# composed ONLY of separators, like `[- ]`, is a separator and goes).
|
|
73
|
+
# A quantifier left dangling by a removed separator ("-?", "[- ]?") is
|
|
74
|
+
# swallowed with it. Linear scan — regexes in this dataset are simple.
|
|
75
|
+
def self.strip_separators(src)
|
|
76
|
+
return nil if src.nil?
|
|
77
|
+
|
|
78
|
+
out = +""
|
|
79
|
+
chars = src.chars
|
|
80
|
+
i = 0
|
|
81
|
+
while i < chars.size
|
|
82
|
+
ch = chars[i]
|
|
83
|
+
|
|
84
|
+
if ch == "\\" && i + 1 < chars.size
|
|
85
|
+
nxt = chars[i + 1]
|
|
86
|
+
if [ "s", "-", ".", " " ].include?(nxt)
|
|
87
|
+
i += 2
|
|
88
|
+
i += 1 if [ "?", "*" ].include?(chars[i]) # optional separator: drop its quantifier too
|
|
89
|
+
else
|
|
90
|
+
out << ch << nxt
|
|
91
|
+
i += 2
|
|
92
|
+
end
|
|
93
|
+
next
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
if ch == "["
|
|
97
|
+
# Copy or drop the WHOLE class span in one decision.
|
|
98
|
+
closing = i + 1
|
|
99
|
+
closing += 1 while closing < chars.size && chars[closing] != "]"
|
|
100
|
+
content = chars[(i + 1)...closing].join
|
|
101
|
+
tokens = content.scan(/\\.|./m)
|
|
102
|
+
if tokens.any? && tokens.all? { |t| SEPARATOR_TOKENS.include?(t) }
|
|
103
|
+
i = closing + 1
|
|
104
|
+
i += 1 if [ "?", "*" ].include?(chars[i])
|
|
105
|
+
else
|
|
106
|
+
out << chars[i..closing].join
|
|
107
|
+
i = closing + 1
|
|
108
|
+
end
|
|
109
|
+
next
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
if [ "-", " ", "·" ].include?(ch)
|
|
113
|
+
i += 1
|
|
114
|
+
i += 1 if [ "?", "*" ].include?(chars[i])
|
|
115
|
+
else
|
|
116
|
+
out << ch
|
|
117
|
+
i += 1
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
out
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def inspect = %(#<Vehicles::Plates::Series #{id} "#{pattern}">)
|
|
124
|
+
|
|
125
|
+
private
|
|
126
|
+
|
|
127
|
+
# Bad regex in data must degrade like every other lookup: nil, not raise.
|
|
128
|
+
def compile(source)
|
|
129
|
+
return nil if source.nil?
|
|
130
|
+
|
|
131
|
+
Regexp.new(source)
|
|
132
|
+
rescue RegexpError
|
|
133
|
+
nil
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
|
|
5
|
+
module Vehicles
|
|
6
|
+
# License-plate & registration-mark knowledge (the VehiclesDB PRD-PLATES
|
|
7
|
+
# dataset, bundled offline like everything else in this gem). Ask it two
|
|
8
|
+
# kinds of question:
|
|
9
|
+
#
|
|
10
|
+
# Vehicles.plates # => every Jurisdiction
|
|
11
|
+
# Vehicles.plates(:nl) # => one Jurisdiction (series, charset, authority)
|
|
12
|
+
# Vehicles.plate("12-GB-BD", jurisdiction: :nl) # => Plates::Match
|
|
13
|
+
#
|
|
14
|
+
# Matching is TWO-TIER, because humans type plates every way at once:
|
|
15
|
+
#
|
|
16
|
+
# exact — the input, upcased, matches a series' STRICT regex as-typed
|
|
17
|
+
# (separators included): formatted exactly as issued.
|
|
18
|
+
# lenient — the input with separators stripped ("1234XYZ", "1234 xyz",
|
|
19
|
+
# "1234-XYZ" all collapse to "1234XYZ") matches the same strict
|
|
20
|
+
# alphabet with the separators removed. Still the authority's
|
|
21
|
+
# real issuing alphabet — only the punctuation is forgiven.
|
|
22
|
+
#
|
|
23
|
+
# A lenient-only match carries `suggestion`: the serial re-formatted the
|
|
24
|
+
# way it appears on the physical plate ("did you mean 1234 XYZ").
|
|
25
|
+
# Same posture as every lookup in this gem: garbage in => empty result,
|
|
26
|
+
# never an exception.
|
|
27
|
+
module Plates
|
|
28
|
+
DATA_DIR = File.expand_path("../../data/plates", __dir__)
|
|
29
|
+
|
|
30
|
+
# What people type between plate groups: space(s), hyphen/en/em dash,
|
|
31
|
+
# middot, period. Stripped for the lenient tier (and from the strict
|
|
32
|
+
# regexes to derive their lenient twins).
|
|
33
|
+
SEPARATORS = /[\s\-–—·.]+/
|
|
34
|
+
|
|
35
|
+
module_function
|
|
36
|
+
|
|
37
|
+
def jurisdictions
|
|
38
|
+
@jurisdictions ||= Dir.glob(File.join(DATA_DIR, "*.yml")).sort.map do |path|
|
|
39
|
+
Jurisdiction.load(path)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def jurisdiction(code)
|
|
44
|
+
jurisdictions.find { |j| j.code == code.to_s.downcase.tr("_", "-") }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def classes
|
|
48
|
+
@classes ||= YAML.safe_load_file(File.join(DATA_DIR, "_meta", "classes.yml")) || {}
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# The matcher behind Vehicles.plate. Unknown jurisdiction => empty Match.
|
|
52
|
+
def match(input, jurisdiction:)
|
|
53
|
+
juris = jurisdiction(jurisdiction)
|
|
54
|
+
typed = input.to_s.upcase.strip
|
|
55
|
+
canon = typed.gsub(SEPARATORS, "")
|
|
56
|
+
return Match.new(input: input.to_s, jurisdiction: juris, canon: canon, hits: []) if juris.nil? || canon.empty?
|
|
57
|
+
|
|
58
|
+
hits = juris.series.filter_map do |series|
|
|
59
|
+
next nil unless series.validation_regexp
|
|
60
|
+
|
|
61
|
+
exact = series.validation_regexp.match?(typed)
|
|
62
|
+
lenient = exact || series.lenient_regexp&.match?(canon)
|
|
63
|
+
Match::Hit.new(series: series, exact: exact) if lenient
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
Match.new(input: input.to_s, jurisdiction: juris, canon: canon, hits: hits)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# The answer to "is this a plate, and which series issues it?".
|
|
70
|
+
class Match
|
|
71
|
+
Hit = Struct.new(:series, :exact, keyword_init: true) do
|
|
72
|
+
def exact? = exact
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
attr_reader :input, :jurisdiction, :canon, :hits
|
|
76
|
+
|
|
77
|
+
def initialize(input:, jurisdiction:, canon:, hits:)
|
|
78
|
+
@input = input
|
|
79
|
+
@jurisdiction = jurisdiction
|
|
80
|
+
@canon = canon
|
|
81
|
+
# Exact hits outrank lenient ones, strict-validated series outrank
|
|
82
|
+
# recall-only catch-alls; series order (standard first) breaks ties.
|
|
83
|
+
@hits = hits.sort_by { |h| [ h.exact? ? 0 : 1, h.series.strict? ? 0 : 1 ] }.freeze
|
|
84
|
+
freeze
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def valid? = hits.any?
|
|
88
|
+
def exact? = hits.any?(&:exact?)
|
|
89
|
+
|
|
90
|
+
# True when at least one hit is a strict-validated series — the strong
|
|
91
|
+
# signal ("the authority issues this alphabet"), vs a catch-all shape
|
|
92
|
+
# union like an export plate. Prefer this for user-input validation.
|
|
93
|
+
def strict? = hits.any? { |h| h.series.strict? }
|
|
94
|
+
|
|
95
|
+
def series = hits.map(&:series)
|
|
96
|
+
def best = hits.first&.series
|
|
97
|
+
|
|
98
|
+
# The serial formatted the way the best-matching series prints it on
|
|
99
|
+
# the physical plate — "1234XYZ" typed, "1234 XYZ" issued.
|
|
100
|
+
def formatted
|
|
101
|
+
best&.format_serial(canon)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Only when the user's punctuation was off: the "did you mean" string.
|
|
105
|
+
def suggestion
|
|
106
|
+
formatted if valid? && !exact?
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
@@ -11,10 +11,11 @@ module Vehicles
|
|
|
11
11
|
# resolver falls back to the local data. Tracking/enrichment must never break
|
|
12
12
|
# the host app, so this never raises out.
|
|
13
13
|
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
14
|
+
# The images endpoint is LIVE (since 2026-07): mint a key at
|
|
15
|
+
# https://vehiclesdb.com/settings/api-keys, set `config.api_key`, and
|
|
16
|
+
# `model.image` / `model.images` answer with rendered vehicle imagery.
|
|
17
|
+
# The enrichment endpoints (years/segment) are still the wired-up seam —
|
|
18
|
+
# they safely return nil until the service ships them.
|
|
18
19
|
module HostedProvider
|
|
19
20
|
module_function
|
|
20
21
|
|
|
@@ -33,11 +34,28 @@ module Vehicles
|
|
|
33
34
|
fetch(model)&.dig("segment")&.to_sym
|
|
34
35
|
end
|
|
35
36
|
|
|
36
|
-
|
|
37
|
+
# One variant URL — the common "just give me an <img src>" case.
|
|
38
|
+
# `size` picks from the API's rendered variants (:sm 320×180, :md
|
|
39
|
+
# 640×360, :lg 1280×720; webp).
|
|
40
|
+
def image(model, year: nil, color: nil, size: :md)
|
|
41
|
+
images(model, color: color)&.dig("variants", size.to_s, "url")
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# The full images payload for a model — palette, every variant with
|
|
45
|
+
# dimensions, provenance, and the honest color fallback: `color` is what
|
|
46
|
+
# the API actually served, `requested_color` what you asked for (a color
|
|
47
|
+
# that isn't rendered yet falls back rather than 404ing).
|
|
48
|
+
#
|
|
49
|
+
# GET /v1/vehicles/:kind/:make_slug/:model_slug/images?color=<slug>
|
|
50
|
+
#
|
|
51
|
+
# `year`/`trim` filters are RESERVED server-side today (the API 422s on
|
|
52
|
+
# them by contract, so callers can't silently build on unimplemented
|
|
53
|
+
# semantics) — which is why `image` accepts `year:` but never sends it:
|
|
54
|
+
# serving the current rendering beats an error until the filter ships.
|
|
55
|
+
def images(model, color: nil)
|
|
37
56
|
params = {}
|
|
38
|
-
params[:
|
|
39
|
-
params
|
|
40
|
-
get("/v1/models/#{model.slug}/image", params)&.dig("url")
|
|
57
|
+
params[:color] = color.to_s unless color.to_s.empty?
|
|
58
|
+
get("/v1/vehicles/#{model.kind}/#{model.make_slug}/#{model.model_slug}/images", params)
|
|
41
59
|
end
|
|
42
60
|
|
|
43
61
|
# --- internals -----------------------------------------------------------
|
|
@@ -17,9 +17,10 @@ module Vehicles
|
|
|
17
17
|
# The bundled snapshot is make/model/kind/body_type only — richer fields
|
|
18
18
|
# come from the hosted API. Returning nil here lets the resolver move on
|
|
19
19
|
# (and ultimately yield nil) instead of raising.
|
|
20
|
-
def years(_model)
|
|
21
|
-
def segment(_model)
|
|
22
|
-
def image(_model, year
|
|
20
|
+
def years(_model) = nil
|
|
21
|
+
def segment(_model) = nil
|
|
22
|
+
def image(_model, year: nil, color: nil, size: :md) = nil
|
|
23
|
+
def images(_model, color: nil) = nil
|
|
23
24
|
end
|
|
24
25
|
end
|
|
25
26
|
end
|
data/lib/vehicles/version.rb
CHANGED
data/lib/vehicles.rb
CHANGED
|
@@ -10,6 +10,9 @@ require_relative "vehicles/refresher"
|
|
|
10
10
|
require_relative "vehicles/providers/local_provider"
|
|
11
11
|
require_relative "vehicles/providers/hosted_provider"
|
|
12
12
|
require_relative "vehicles/other_option"
|
|
13
|
+
require_relative "vehicles/plates"
|
|
14
|
+
require_relative "vehicles/plates/jurisdiction"
|
|
15
|
+
require_relative "vehicles/plates/series"
|
|
13
16
|
|
|
14
17
|
# Car makes & models for your Rails app — dropdowns, search, validation. Bundled
|
|
15
18
|
# data, zero config, no network calls. Standalone first; an SDK for the hosted
|
|
@@ -135,6 +138,24 @@ module Vehicles
|
|
|
135
138
|
found&.model(model_name)
|
|
136
139
|
end
|
|
137
140
|
|
|
141
|
+
# License plates (the bundled PRD-PLATES dataset). No argument: every
|
|
142
|
+
# jurisdiction. With a code: that jurisdiction (or nil — forgiving).
|
|
143
|
+
# Vehicles.plates # => [#<Jurisdiction nl …>, …]
|
|
144
|
+
# Vehicles.plates(:nl) # => the Netherlands entry (33 series)
|
|
145
|
+
def plates(code = nil)
|
|
146
|
+
code.nil? ? Plates.jurisdictions : Plates.jurisdiction(code)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Validate a typed registration against a jurisdiction's real series.
|
|
150
|
+
# Two-tier: exact as-issued, else separator-forgiving with a formatting
|
|
151
|
+
# suggestion. Never raises; unknown jurisdictions yield an empty match.
|
|
152
|
+
# Vehicles.plate("12-GB-BD", jurisdiction: :nl).valid? # => true (exact)
|
|
153
|
+
# Vehicles.plate("1234XYZ", jurisdiction: :es).suggestion # => "1234 XYZ"
|
|
154
|
+
# Vehicles.plate("12-AB-CD", jurisdiction: :nl).valid? # => false (vowel purge)
|
|
155
|
+
def plate(input, jurisdiction:)
|
|
156
|
+
Plates.match(input, jurisdiction: jurisdiction)
|
|
157
|
+
end
|
|
158
|
+
|
|
138
159
|
# Every model matching a query, ranked. => [Vehicles::Model, ...]
|
|
139
160
|
def search(query)
|
|
140
161
|
dataset.search(query)
|
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.6.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- rameerez
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-
|
|
10
|
+
date: 2026-08-01 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,
|
|
@@ -24,6 +24,13 @@ files:
|
|
|
24
24
|
- CHANGELOG.md
|
|
25
25
|
- LICENSE.txt
|
|
26
26
|
- README.md
|
|
27
|
+
- data/plates/PROVENANCE.md
|
|
28
|
+
- data/plates/_decode/es-provinces.yml
|
|
29
|
+
- data/plates/_meta/classes.yml
|
|
30
|
+
- data/plates/de.yml
|
|
31
|
+
- data/plates/es.yml
|
|
32
|
+
- data/plates/nl.yml
|
|
33
|
+
- data/plates/us-fl.yml
|
|
27
34
|
- data/vehicles.json
|
|
28
35
|
- exe/vehicles-mcp
|
|
29
36
|
- lib/generators/vehicles/install_generator.rb
|
|
@@ -38,6 +45,9 @@ files:
|
|
|
38
45
|
- lib/vehicles/mcp_server.rb
|
|
39
46
|
- lib/vehicles/model.rb
|
|
40
47
|
- lib/vehicles/other_option.rb
|
|
48
|
+
- lib/vehicles/plates.rb
|
|
49
|
+
- lib/vehicles/plates/jurisdiction.rb
|
|
50
|
+
- lib/vehicles/plates/series.rb
|
|
41
51
|
- lib/vehicles/providers/hosted_provider.rb
|
|
42
52
|
- lib/vehicles/providers/local_provider.rb
|
|
43
53
|
- lib/vehicles/railtie.rb
|