1use 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(), 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}