Skip to main content

vin_decode/
catalog.rs

1use std::path::Path;
2
3use crate::data::{EngineRow, EuModelRow, ModelRow};
4use crate::maps::{FstMap, FstSet, data_dir};
5use crate::types::{BodyClass, FuelType};
6
7/// Read-only catalog of every make and model present in the lookup data.
8///
9/// Useful for populating dropdowns, validators, or schema-typed enum columns
10/// in webapps. Combines the vPIC pattern-derived index (`make_models`) with the
11/// EU/global rip catalog (`eu_brand_models`, `eu_engines`) for non-US coverage.
12pub struct Catalog {
13    makes: FstSet,
14    make_models: FstMap<ModelRow>,
15    eu_brand_models: Option<FstMap<EuModelRow>>,
16    eu_engines: Option<FstMap<EngineRow>>,
17}
18
19impl Catalog {
20    /// Open the catalog using the default data directory (see [`crate::Decoder::new`]).
21    pub fn new() -> crate::Result<Self> {
22        let dir = data_dir();
23        #[cfg(feature = "embedded")]
24        crate::embedded::ensure_installed(&dir).ok();
25        Self::open(&dir)
26    }
27
28    /// Open the catalog against an explicit data directory. EU rip tables are
29    /// optional — they're only loaded if the corresponding `.fst` files exist.
30    pub fn open(dir: &Path) -> crate::Result<Self> {
31        let eu_brand_models = if dir.join("eu_brand_models.fst").exists() {
32            Some(FstMap::open(dir)?)
33        } else {
34            None
35        };
36        let eu_engines = if dir.join("eu_engines.fst").exists() {
37            Some(FstMap::open(dir)?)
38        } else {
39            None
40        };
41        Ok(Catalog {
42            makes: FstSet::open(&dir.join("makes.fst"))?,
43            make_models: FstMap::open(dir)?,
44            eu_brand_models,
45            eu_engines,
46        })
47    }
48
49    /// Sorted list of every make name (uppercase, deduped).
50    pub fn all_makes(&self) -> Vec<String> {
51        self.makes.keys()
52    }
53
54    /// Case-insensitive membership check for a make name.
55    pub fn has_make(&self, make: &str) -> bool {
56        self.makes.contains(&make.to_ascii_uppercase())
57    }
58
59    /// Total number of distinct makes.
60    pub fn make_count(&self) -> u64 {
61        self.makes.len()
62    }
63
64    /// Sorted list of model names known for the given make (case-insensitive lookup).
65    ///
66    /// Merges results from the vPIC pattern-derived index and the EU/global rip
67    /// catalog, deduped and sorted.
68    pub fn models_for_make(&self, make: &str) -> Vec<String> {
69        let key = crate::decoder::normalize_make(make);
70        let mut models: Vec<String> = self
71            .make_models
72            .get(&key)
73            .map(|rows| rows.into_iter().map(|r| r.name).collect())
74            .unwrap_or_default();
75        if let Some(eu) = &self.eu_brand_models {
76            if let Some(rows) = eu.get(&key) {
77                for r in rows {
78                    models.push(r.name);
79                }
80            }
81        }
82        models.sort();
83        models.dedup();
84        models
85    }
86
87    /// EU/global rip listing of models for the given brand, with year ranges.
88    /// Returns an empty vec when the brand is unknown or the EU catalog is not
89    /// embedded.
90    pub fn eu_models_for(&self, brand: &str) -> Vec<EuModelRow> {
91        let key = crate::decoder::normalize_make(brand);
92        self.eu_brand_models
93            .as_ref()
94            .and_then(|m| m.get(&key))
95            .unwrap_or_default()
96    }
97
98    /// Engine variants known for the given brand. Filter by `model` in user
99    /// code, or use [`Catalog::engines_for`].
100    pub fn engines_for_brand(&self, brand: &str) -> Vec<EngineRow> {
101        let key = crate::decoder::normalize_make(brand);
102        self.eu_engines
103            .as_ref()
104            .and_then(|m| m.get(&key))
105            .unwrap_or_default()
106    }
107
108    /// Engine variants known for `(brand, model)`. Both args are matched
109    /// case-insensitively against the canonical uppercase keys.
110    pub fn engines_for(&self, brand: &str, model: &str) -> Vec<EngineRow> {
111        let model_key = model.to_ascii_uppercase();
112        self.engines_for_brand(brand)
113            .into_iter()
114            .filter(|r| r.model == model_key)
115            .collect()
116    }
117
118    /// Static list of every [`BodyClass`] variant — useful for typed dropdowns.
119    pub fn body_classes() -> &'static [BodyClass] {
120        &[
121            BodyClass::Sedan,
122            BodyClass::Coupe,
123            BodyClass::Hatchback,
124            BodyClass::Wagon,
125            BodyClass::Convertible,
126            BodyClass::Suv,
127            BodyClass::Crossover,
128            BodyClass::Pickup,
129            BodyClass::Van,
130            BodyClass::Minivan,
131            BodyClass::Bus,
132            BodyClass::Truck,
133            BodyClass::Motorcycle,
134            BodyClass::Trailer,
135            BodyClass::Incomplete,
136            BodyClass::Other,
137        ]
138    }
139
140    /// Static list of every [`FuelType`] variant — useful for typed dropdowns.
141    pub fn fuel_types() -> &'static [FuelType] {
142        &[
143            FuelType::Gasoline,
144            FuelType::Diesel,
145            FuelType::Electric,
146            FuelType::Hybrid,
147            FuelType::PluginHybrid,
148            FuelType::Ethanol,
149            FuelType::FlexFuel,
150            FuelType::Cng,
151            FuelType::Lng,
152            FuelType::Lpg,
153            FuelType::Hydrogen,
154            FuelType::FuelCell,
155            FuelType::Methanol,
156            FuelType::NaturalGas,
157            FuelType::Other,
158        ]
159    }
160}