corgi-rs 0.3.0

Vehicle VIN decoding library based on NHTSA database for Rust
//! vPIC element metadata: the catalogue of attributes a VIN can resolve to.
//!
//! The table itself is generated by `build.rs` from `assets/element.tsv`, so it
//! always matches the shipped patterns.

include!(concat!(env!("OUT_DIR"), "/elements.rs"));

/// Description of one decodable attribute.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Element {
    /// vPIC `Element.Id`.
    pub id: u16,
    /// Short machine-readable code, e.g. `BodyClass` or `DisplacementL`. This is
    /// the key used in [`crate::VehicleInfo::attributes`].
    pub code: &'static str,
    /// Human-readable label, e.g. `Displacement (L)`.
    pub name: &'static str,
    /// `string`, `int`, `decimal` or `lookup`.
    pub data_type: &'static str,
    /// Grouping NHTSA files the element under, e.g. `Engine`.
    pub group: &'static str,
}

/// Look up an element by its vPIC id.
///
/// # Examples
///
/// ```
/// use corgi_rs::element;
/// assert_eq!(element::by_id(13).unwrap().code, "DisplacementL");
/// assert!(element::by_id(u16::MAX).is_none());
/// ```
pub fn by_id(id: u16) -> Option<&'static Element> {
    ELEMENTS
        .binary_search_by_key(&id, |e| e.id)
        .ok()
        .map(|idx| &ELEMENTS[idx])
}

/// The element code for `id`, or `None` if the id is unknown.
pub fn code_of(id: u16) -> Option<&'static str> {
    by_id(id).map(|e| e.code)
}

/// Look up an element by its code.
///
/// # Examples
///
/// ```
/// use corgi_rs::element;
/// assert_eq!(element::by_code("DriveType").unwrap().id, 15);
/// ```
pub fn by_code(code: &str) -> Option<&'static Element> {
    ELEMENTS.iter().find(|e| e.code == code)
}

/// Every element the decoder knows about, ordered by id.
pub fn all() -> &'static [Element] {
    ELEMENTS
}

/// Name of a vPIC vehicle type id, e.g. `2` is `Passenger Car`.
///
/// # Examples
///
/// ```
/// use corgi_rs::element;
/// assert_eq!(element::vehicle_type_name(2), Some("Passenger Car"));
/// assert_eq!(element::vehicle_type_name(3), Some("Truck"));
/// ```
pub fn vehicle_type_name(id: u16) -> Option<&'static str> {
    VEHICLE_TYPES
        .iter()
        .find(|(candidate, _)| *candidate == id)
        .map(|(_, name)| *name)
}

/// Vehicle types the decoder considers passenger vehicles: cars, MPVs and the
/// truck class that covers every pickup.
pub const PASSENGER_VEHICLE_TYPES: [u16; 3] = [2, 3, 7];

/// Whether this vehicle type takes NHTSA's car/MPV/light-truck rules for the
/// model year and check digit.
pub fn is_car_mpv_lt(vehicle_type_id: u16, truck_type_id: u16) -> bool {
    matches!(vehicle_type_id, 2 | 7) || (vehicle_type_id == 3 && truck_type_id == 1)
}

//
// Element ids the decoder treats specially. These mirror the constants
// hard-coded in NHTSA's `spVinDecode`.
//

pub const BATTERY_TYPE: u16 = 2;
pub const BODY_CLASS: u16 = 5;
pub const ENGINE_CYLINDERS: u16 = 9;
pub const DISPLACEMENT_CC: u16 = 11;
pub const DISPLACEMENT_CI: u16 = 12;
pub const DISPLACEMENT_L: u16 = 13;
pub const DOORS: u16 = 14;
pub const DRIVE_TYPE: u16 = 15;
pub const ENGINE_MODEL: u16 = 18;
pub const FUEL_TYPE_PRIMARY: u16 = 24;
pub const GVWR: u16 = 25;
pub const MAKE: u16 = 26;
pub const MANUFACTURER: u16 = 27;
pub const MODEL: u16 = 28;
pub const MODEL_YEAR: u16 = 29;
pub const PLANT_CITY: u16 = 31;
pub const SEATS: u16 = 33;
pub const SERIES: u16 = 34;
pub const TRANSMISSION_STYLE: u16 = 37;
pub const TRIM: u16 = 38;
pub const VEHICLE_TYPE: u16 = 39;
pub const FUEL_TYPE_SECONDARY: u16 = 66;
pub const ENGINE_HP: u16 = 71;
pub const PLANT_COUNTRY: u16 = 75;
pub const PLANT_STATE: u16 = 77;
pub const SERIES_2: u16 = 110;
pub const TRIM_2: u16 = 109;
pub const ELECTRIFICATION_LEVEL: u16 = 126;
pub const GVWR_TO: u16 = 190;

/// Elements NHTSA derives from the WMI or the request rather than from a VIN
/// pattern, and therefore excludes from pattern matching.
pub const WMI_DERIVED: [u16; 4] = [MAKE, MANUFACTURER, MODEL_YEAR, VEHICLE_TYPE];

/// Free-text elements that may legitimately hold several values at once, so the
/// decoder keeps every match instead of ranking them down to one.
///
/// These are `OtherBatteryInfo`, `Note`, `OtherRestraintSystemInfo`,
/// `OtherEngineInfo`, `OtherBusInfo`, `OtherMotorcycleInfo`, `OtherTrailerInfo`,
/// `ActiveSafetySysNote` and `NCSANote`.
pub const MULTI_VALUED: [u16; 9] = [1, 114, 121, 129, 150, 154, 155, 169, 186];

/// Whether `id` may hold several values.
pub fn is_multi_valued(id: u16) -> bool {
    MULTI_VALUED.contains(&id)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn the_generated_table_is_sorted_so_binary_search_works() {
        assert!(ELEMENTS.windows(2).all(|w| w[0].id < w[1].id));
    }

    #[test]
    fn constants_agree_with_the_generated_table() {
        for (id, code) in [
            (BODY_CLASS, "BodyClass"),
            (DRIVE_TYPE, "DriveType"),
            (MAKE, "Make"),
            (MODEL, "Model"),
            (TRANSMISSION_STYLE, "TransmissionStyle"),
            (DISPLACEMENT_L, "DisplacementL"),
            (ENGINE_HP, "EngineHP"),
            (GVWR_TO, "GVWR_to"),
            (ELECTRIFICATION_LEVEL, "ElectrificationLevel"),
        ] {
            assert_eq!(code_of(id), Some(code), "element {id}");
        }
    }

    #[test]
    fn lookup_by_code_round_trips() {
        let element = by_code("DisplacementL").expect("known element");
        assert_eq!(by_id(element.id), Some(element));
    }
}