Skip to main content

fastsim_schema/
lib.rs

1//! Vehicle database schema for the `fastsim-vehicles` repository.
2//!
3//! This crate provides serialization/deserialization of vehicle identification paths
4//! into structured data, along with index management and WebAssembly bindings.
5//! Currently supports schema version 1.
6
7use serde::{de::Error as DeError, Deserialize, Deserializer, Serialize};
8use std::str::FromStr;
9
10mod v1;
11
12pub use v1::{
13    read_jsonl_v1, search_v1, write_jsonl_v1, IndexEntryV1, QueryV1, VehicleSchemaV1,
14    VehicleSchemaV1Error,
15};
16
17pub const DEFAULT_DB_URL: &str =
18    "https://raw.githubusercontent.com/NatLabRockies/fastsim-vehicles/main";
19
20#[derive(Debug, thiserror::Error)]
21pub enum VehicleSchemaError {
22    #[error("invalid schema {raw:?}: unknown schema version prefix {version:?}. Expected a schema path starting with 'v1/'")]
23    UnknownVersion { raw: String, version: String },
24
25    #[error(transparent)]
26    V1(#[from] VehicleSchemaV1Error),
27}
28
29#[derive(Debug, Clone, PartialEq, Serialize)]
30#[serde(untagged)]
31pub enum VehicleSchema {
32    V1(VehicleSchemaV1),
33}
34
35impl FromStr for VehicleSchema {
36    type Err = VehicleSchemaError;
37    fn from_str(raw: &str) -> Result<Self, Self::Err> {
38        let version = raw.split('/').next().unwrap_or_default();
39        match version {
40            "v1" => Ok(VehicleSchema::V1(VehicleSchemaV1::from_str(raw)?)),
41            _ => Err(VehicleSchemaError::UnknownVersion {
42                raw: raw.to_string(),
43                version: version.to_string(),
44            }),
45        }
46    }
47}
48
49impl<'de> Deserialize<'de> for VehicleSchema {
50    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
51    where
52        D: Deserializer<'de>,
53    {
54        let raw = String::deserialize(deserializer)?;
55        raw.parse::<VehicleSchema>().map_err(|err| {
56            let msg = match &err {
57                VehicleSchemaError::UnknownVersion { .. } => err.to_string(), // already self-contained
58                VehicleSchemaError::V1(_) => {
59                    let mut msg = format!(
60                        "{err}. Expected format: v1/fastsim-{{N}}/{{powertrain}}/{{make}}/{{model}}/{{year}}/{{variant}}/r{{N}}"
61                    );
62                    if let Some((current, suggested)) = VehicleSchemaV1::suggest_normalized_path(&raw) {
63                        msg.push_str(&format!(
64                            "\nCurrent path: {current}\nSuggested normalized path: {suggested}"
65                        ));
66                    }
67                    msg
68                }
69            };
70            D::Error::custom(msg)
71        })
72    }
73}
74
75#[cfg(test)]
76mod tests {
77    use super::*;
78
79    #[test]
80    fn schema_deserializes_valid_v1_path() {
81        let raw = "\"v1/fastsim-3/conv/ford/fusion/2012/base/r1\"";
82        let schema: VehicleSchema = serde_json::from_str(raw).unwrap();
83        match schema {
84            VehicleSchema::V1(v1) => {
85                assert_eq!(v1.fastsim_version, 3);
86                assert_eq!(v1.powertrain, "conv");
87            }
88        }
89    }
90
91    #[test]
92    fn schema_deserialize_error_is_actionable() {
93        let raw = "\"v1/bad-schema\"";
94        let err = serde_json::from_str::<VehicleSchema>(raw)
95            .unwrap_err()
96            .to_string();
97        println!("Error: {err}");
98        assert!(err.contains("expected 8 path segments"));
99        assert!(err.contains("Expected format:"));
100    }
101
102    #[test]
103    fn schema_deserialize_error_suggests_normalized_path() {
104        let raw = "\"v1/fastsim-3/BEV/Ford/F-150 Lightning/2024/Base/r1\"";
105        let err = serde_json::from_str::<VehicleSchema>(raw)
106            .unwrap_err()
107            .to_string();
108        assert!(err.contains("Current path:"));
109        assert!(err.contains("Suggested normalized path:"));
110        assert!(err.contains("v1/fastsim-3/bev/ford/f-150-lightning/2024/base/r1"));
111    }
112
113    #[test]
114    fn schema_deserialize_error_for_unknown_version() {
115        let raw = "\"v9/fastsim-3/conv/ford/fusion/2012/base/r1\"";
116        let err = serde_json::from_str::<VehicleSchema>(raw)
117            .unwrap_err()
118            .to_string();
119        assert!(err.contains("unknown schema version prefix"));
120        assert!(err.contains("v1/"));
121    }
122
123    #[test]
124    fn schema_rejects_unknown_schema_version() {
125        let raw = "\"v2/fastsim-3/conv/ford/fusion/2012/base/r1\"";
126        let err = serde_json::from_str::<VehicleSchema>(raw)
127            .unwrap_err()
128            .to_string();
129        assert!(err.contains("unknown schema version prefix"));
130    }
131
132    #[test]
133    fn schema_deserialize_error_suggests_punctuation_cleanup() {
134        let raw = "\"v1/fastsim-3/conv/ford/f--usio..n/2022/thermal_DFCO/r1\"";
135        let err = serde_json::from_str::<VehicleSchema>(raw)
136            .unwrap_err()
137            .to_string();
138        assert!(err.contains("v1/fastsim-3/conv/ford/f-usio.n/2022/thermal-dfco/r1"));
139    }
140}