vin_decode/data.rs
1//! Internal rkyv archive types for the lookup tables.
2//!
3//! These types are exposed publicly only when the `build` feature is enabled,
4//! since the build pipeline needs to construct them. End users of the decoder
5//! never see these — they go through [`crate::Decoder`] / [`crate::Catalog`].
6
7use rkyv::{
8 Archive, Deserialize, Serialize,
9 api::high::{HighDeserializer, HighSerializer},
10 rancor::Error as RkyvError,
11 ser::allocator::ArenaHandle,
12 util::AlignedVec,
13};
14
15use crate::types::{BodyType, DriveType, FuelType, Transmission};
16
17/// Marker trait for types that can be rkyv-deserialized via the high-level helpers.
18pub trait RkyvDe<T>: Deserialize<T, HighDeserializer<RkyvError>> {}
19
20/// Marker trait for types that can be rkyv-serialized via the high-level helpers.
21pub trait RkyvSer:
22 for<'a> Serialize<HighSerializer<AlignedVec, ArenaHandle<'a>, RkyvError>>
23{
24}
25
26/// Trait that maps a row type to its on-disk file name (sans extension).
27pub trait Saveable {
28 /// Base name for the `.fst`/`.bin` file pair on disk.
29 fn base_name() -> &'static str;
30}
31
32/// Single make-name row, keyed by WMI in the `wmi_make` table.
33///
34/// Country and region are populated when the WMI was sourced from KBA / vPIC /
35/// merged datasets that carried plant origin metadata. Empty string means the
36/// upstream source did not record this field.
37#[derive(Archive, Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
38pub struct MakeRow {
39 /// Make name as it appears in the source (e.g. `"Honda"`).
40 pub name: String,
41 /// Plant country (uppercased canonical form, e.g. `"GERMANY"`).
42 pub country: String,
43 /// Region bucket (`"EUROPE"`, `"NORTH_AMERICA"`, `"ASIA"`, `"OCEANIA"`,
44 /// `"SOUTH_AMERICA"`, `"AFRICA"`).
45 pub region: String,
46}
47impl RkyvSer for MakeRow {}
48impl RkyvDe<MakeRow> for ArchivedMakeRow {}
49impl Saveable for MakeRow {
50 fn base_name() -> &'static str {
51 "wmi_make"
52 }
53}
54
55/// Brand → model index row with year range, sourced from the EU/global rip.
56#[derive(Archive, Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
57pub struct EuModelRow {
58 /// Model name (uppercase canonical, e.g. `"FABIA"`, `"OCTAVIA RS"`).
59 pub name: String,
60 /// First production year known for this model. `0` if unknown.
61 pub first_year: u16,
62 /// Last production year known for this model. `0` if unknown.
63 pub last_year: u16,
64}
65impl RkyvSer for EuModelRow {}
66impl RkyvDe<EuModelRow> for ArchivedEuModelRow {}
67impl Saveable for EuModelRow {
68 fn base_name() -> &'static str {
69 "eu_brand_models"
70 }
71}
72
73/// Engine variant row, keyed by `BRAND` in the engine catalog. Filter by
74/// `model` in user code (or use `Catalog::engines_for(brand, model)`).
75#[derive(Archive, Serialize, Deserialize, Debug, PartialEq, Clone)]
76pub struct EngineRow {
77 /// Model name this engine belongs to (e.g. `"FABIA"`, `"OCTAVIA RS"`).
78 pub model: String,
79 /// Production year of this engine variant.
80 pub year: u16,
81 /// Engine variant display name (e.g. `"2.0 TDI 150hp"`).
82 pub name: String,
83 /// Number of cylinders / configuration string (e.g. `"4"`, `"V6"`, `"I4"`).
84 pub cylinders: String,
85 /// Displacement in cubic centimetres.
86 pub displacement_cm3: u32,
87 /// Continuous power output in kilowatts.
88 pub power_kw: u32,
89 /// Continuous power output in metric horsepower.
90 pub power_hp: u32,
91 /// Peak torque in newton-metres.
92 pub torque_nm: u32,
93 /// Fuel name as published (e.g. `"Gasoline"`, `"Diesel"`, `"Electric"`).
94 pub fuel: String,
95 /// Drive type (e.g. `"Front Wheel Drive"`, `"All Wheel Drive"`).
96 pub drive: String,
97 /// Gearbox description (e.g. `"6-Speed Manual"`).
98 pub gearbox: String,
99}
100impl RkyvSer for EngineRow {}
101impl RkyvDe<EngineRow> for ArchivedEngineRow {}
102impl Saveable for EngineRow {
103 fn base_name() -> &'static str {
104 "eu_engines"
105 }
106}
107
108impl EngineRow {
109 /// Primary fuel as a typed [`FuelType`], parsed from raw [`EngineRow::fuel`].
110 pub fn fuel_type(&self) -> FuelType {
111 FuelType::parse(&self.fuel)
112 }
113
114 /// Drive layout as a typed [`DriveType`], parsed from raw [`EngineRow::drive`].
115 pub fn drive_type(&self) -> DriveType {
116 DriveType::parse(&self.drive)
117 }
118
119 /// Transmission as a typed [`Transmission`], parsed from raw [`EngineRow::gearbox`].
120 pub fn transmission(&self) -> Transmission {
121 Transmission::parse(&self.gearbox)
122 }
123
124 /// Gear count parsed from the gearbox string (`"6-Speed Manual"` → `Some(6)`).
125 /// `None` when the box carries no leading number (e.g. `"CVT"`).
126 pub fn gearbox_speeds(&self) -> Option<u8> {
127 let bytes = self.gearbox.as_bytes();
128 let mut i = 0;
129 while i < bytes.len() && !bytes[i].is_ascii_digit() {
130 i += 1;
131 }
132 let start = i;
133 while i < bytes.len() && bytes[i].is_ascii_digit() {
134 i += 1;
135 }
136 if start == i {
137 None
138 } else {
139 self.gearbox[start..i].parse().ok()
140 }
141 }
142}
143
144#[cfg(test)]
145mod engine_tests {
146 use super::*;
147
148 fn row(fuel: &str, drive: &str, gearbox: &str) -> EngineRow {
149 EngineRow {
150 model: "FABIA".into(),
151 year: 2020,
152 name: "1.0 TSI".into(),
153 cylinders: "3".into(),
154 displacement_cm3: 999,
155 power_kw: 70,
156 power_hp: 95,
157 torque_nm: 175,
158 fuel: fuel.into(),
159 drive: drive.into(),
160 gearbox: gearbox.into(),
161 }
162 }
163
164 #[test]
165 fn engine_typed_accessors() {
166 let r = row("Gasoline", "Front Wheel Drive", "6-Speed Manual");
167 assert_eq!(r.fuel_type(), FuelType::Gasoline);
168 assert_eq!(r.drive_type(), DriveType::Fwd);
169 assert_eq!(r.transmission(), Transmission::Manual);
170 assert_eq!(r.gearbox_speeds(), Some(6));
171
172 let ev = row("Electric", "All Wheel Drive", "1-Speed");
173 assert_eq!(ev.fuel_type(), FuelType::Electric);
174 assert_eq!(ev.drive_type(), DriveType::Awd);
175 assert_eq!(ev.transmission(), Transmission::SingleSpeed);
176 assert_eq!(ev.gearbox_speeds(), Some(1));
177
178 let cvt = row("Hybrid", "Front Wheel Drive", "CVT");
179 assert_eq!(cvt.transmission(), Transmission::Cvt);
180 assert_eq!(cvt.gearbox_speeds(), None);
181 }
182}
183
184/// Per-model bodywork row, keyed by `BRAND` (uppercase) in the `eu_body` table —
185/// the same keying as `eu_brand_models`. Each row carries one model and the set
186/// of EU type-approval [`BodyType`] variants observed for it. Look up with
187/// [`crate::Catalog::body_for`].
188#[derive(Archive, Serialize, Deserialize, Debug, PartialEq, Clone)]
189pub struct BodyRow {
190 /// Model name (uppercase canonical, e.g. `"OCTAVIA"`).
191 pub model: String,
192 /// Sorted, deduped EU type-approval bodywork types seen for this model.
193 pub bodies: Vec<BodyType>,
194}
195impl RkyvSer for BodyRow {}
196impl RkyvDe<BodyRow> for ArchivedBodyRow {}
197impl Saveable for BodyRow {
198 fn base_name() -> &'static str {
199 "eu_body"
200 }
201}
202
203/// Schema identifier row, keyed by WMI in the `wmi_schema` table.
204#[derive(Archive, Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
205pub struct SchemaRow {
206 /// Schema identifier string (vPIC schema_id column).
207 pub id: String,
208}
209impl RkyvSer for SchemaRow {}
210impl RkyvDe<SchemaRow> for ArchivedSchemaRow {}
211impl Saveable for SchemaRow {
212 fn base_name() -> &'static str {
213 "wmi_schema"
214 }
215}
216
217/// Single lookup row, keyed by schema id in the `schema_lookup` table.
218#[derive(Archive, Serialize, Deserialize, Debug, PartialEq, Clone)]
219pub struct LookupRow {
220 /// VIN pattern with optional `|VIS` metadata suffix.
221 pub pattern: String,
222 /// vPIC element name (e.g. `"Model"`, `"BodyClass"`, `"FuelTypePrimary"`).
223 pub element: String,
224 /// Resolved attribute value to apply when this pattern matches.
225 pub value: String,
226 /// Element weight — higher wins when multiple patterns match the same element.
227 pub weight: u32,
228}
229impl RkyvSer for LookupRow {}
230impl RkyvDe<LookupRow> for ArchivedLookupRow {}
231impl Saveable for LookupRow {
232 fn base_name() -> &'static str {
233 "schema_lookup"
234 }
235}
236
237/// Single model-name row, keyed by uppercase make in the `make_models` reverse index.
238#[derive(Archive, Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
239pub struct ModelRow {
240 /// Model name as it appears in vPIC patterns.
241 pub name: String,
242}
243impl RkyvSer for ModelRow {}
244impl RkyvDe<ModelRow> for ArchivedModelRow {}
245impl Saveable for ModelRow {
246 fn base_name() -> &'static str {
247 "make_models"
248 }
249}
250
251/// Per-VIN rule keyed by 3-char WMI in the `wmi_rules` table. Each rule
252/// supplies a VDS-prefix `remainder` (matched against `vin[3..]`) plus optional
253/// `make` / `model` overrides. Empty strings mean "leave the existing value".
254///
255/// The build pipeline pre-sorts a WMI's rules by `remainder.len()` DESC so the
256/// decoder can do longest-prefix-match by walking the list and picking the
257/// first hit. A row with empty `remainder` acts as a fallback that fires for
258/// any VIN sharing the WMI (handy for fixing mislabeled WMI → make mappings
259/// from upstream sources without losing valid model-specific overrides).
260#[derive(Archive, Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
261pub struct VinRuleRow {
262 /// VDS prefix appended after the WMI (e.g. `"ZZZ8X"`); empty matches all.
263 pub remainder: String,
264 /// Make override (uppercase canonical, matches `eu_brand_models` keys);
265 /// empty string means keep the make resolved from `wmi_make`.
266 pub make: String,
267 /// Model override; empty string means leave model unset (let pattern
268 /// decode handle it).
269 pub model: String,
270}
271impl RkyvSer for VinRuleRow {}
272impl RkyvDe<VinRuleRow> for ArchivedVinRuleRow {}
273impl Saveable for VinRuleRow {
274 fn base_name() -> &'static str {
275 "wmi_rules"
276 }
277}