fastsim_schema/v1/
index.rs1use super::{VehicleSchemaV1, VehicleSchemaV1Error};
8use serde::{Deserialize, Serialize};
9use std::io::Write;
10use std::str::FromStr;
11
12#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
17pub struct IndexEntryV1 {
18 pub path: String,
19 pub id: String,
20 pub fastsim_version: u32,
21 pub powertrain: String,
22 pub make: String,
23 pub model: String,
24 pub year: String,
25 pub variant: String,
26 pub revision: u32,
27}
28
29impl IndexEntryV1 {}
30
31impl FromStr for IndexEntryV1 {
32 type Err = VehicleSchemaV1Error;
33
34 fn from_str(path: &str) -> Result<Self, Self::Err> {
46 let id_str = path.rsplit_once('.').map(|(id, _ext)| id).unwrap_or(path);
47 let schema = id_str.parse::<VehicleSchemaV1>()?;
48
49 Ok(Self {
50 path: path.to_string(),
51 id: schema.to_string(),
52 fastsim_version: schema.fastsim_version,
53 powertrain: schema.powertrain,
54 make: schema.make,
55 model: schema.model,
56 year: schema.year,
57 variant: schema.variant,
58 revision: schema.revision,
59 })
60 }
61}
62
63pub fn write_jsonl_v1<W: Write>(
68 writer: &mut W,
69 entries: &[IndexEntryV1],
70) -> Result<(), serde_json::Error> {
71 for entry in entries {
72 serde_json::to_writer(&mut *writer, entry)?;
73 writer.write_all(b"\n").map_err(serde_json::Error::io)?;
74 }
75 Ok(())
76}
77
78pub fn read_jsonl_v1(text: &str) -> Result<Vec<IndexEntryV1>, serde_json::Error> {
84 text.lines()
85 .filter(|line| !line.trim().is_empty())
86 .map(serde_json::from_str)
87 .collect()
88}
89
90#[cfg(test)]
91mod index_tests {
92 use super::*;
93
94 #[test]
95 fn new_parses_and_populates_all_fields() {
96 let entry = "v1/fastsim-3/conv/ford/fusion/2012/base/r1.yaml"
97 .parse::<IndexEntryV1>()
98 .unwrap();
99
100 assert_eq!(
101 entry.path,
102 "v1/fastsim-3/conv/ford/fusion/2012/base/r1.yaml"
103 );
104 assert_eq!(entry.id, "v1/fastsim-3/conv/ford/fusion/2012/base/r1");
105 assert_eq!(entry.fastsim_version, 3);
106 assert_eq!(entry.powertrain, "conv");
107 assert_eq!(entry.make, "ford");
108 assert_eq!(entry.model, "fusion");
109 assert_eq!(entry.year, "2012");
110 assert_eq!(entry.variant, "base");
111 assert_eq!(entry.revision, 1);
112 }
113
114 #[test]
115 fn new_rejects_invalid_schema_path() {
116 let result = "v1/not-enough-segments.yaml".parse::<IndexEntryV1>();
117 assert!(result.is_err());
118 }
119
120 #[test]
121 fn new_rejects_non_canonical_identifiers() {
122 let result = "v1/fastsim-3/conv/Ford/fusion/2012/base/r1.yaml".parse::<IndexEntryV1>();
125 assert!(matches!(
126 result,
127 Err(VehicleSchemaV1Error::InvalidIdentifier { .. })
128 ));
129 }
130
131 #[test]
132 fn new_round_trips_through_json() {
133 let entry = "v1/fastsim-3/bev/tesla/model-3/2020/base/r1.yaml"
134 .parse::<IndexEntryV1>()
135 .unwrap();
136 let json = serde_json::to_string(&entry).unwrap();
137 let round_tripped: IndexEntryV1 = serde_json::from_str(&json).unwrap();
138 assert_eq!(entry, round_tripped);
139 }
140}
141
142#[cfg(test)]
143mod jsonl_tests {
144 use super::*;
145
146 fn sample_entries() -> Vec<IndexEntryV1> {
147 vec![
148 "v1/fastsim-3/conv/ford/fusion/2012/base/r1.yaml"
149 .parse::<IndexEntryV1>()
150 .unwrap(),
151 "v1/fastsim-3/bev/tesla/model-3/2020/base/r1.yaml"
152 .parse::<IndexEntryV1>()
153 .unwrap(),
154 ]
155 }
156
157 #[test]
158 fn write_then_read_round_trips() {
159 let entries = sample_entries();
160 let mut buf = Vec::new();
161 write_jsonl_v1(&mut buf, &entries).unwrap();
162 let jsonl = String::from_utf8(buf).unwrap();
163 assert_eq!(jsonl.lines().count(), 2);
164
165 let parsed = read_jsonl_v1(&jsonl).unwrap();
166 assert_eq!(parsed, entries);
167 }
168
169 #[test]
170 fn write_jsonl_of_empty_slice_writes_nothing() {
171 let mut buf = Vec::new();
172 write_jsonl_v1(&mut buf, &[]).unwrap();
173 assert!(buf.is_empty());
174 }
175
176 #[test]
177 fn read_jsonl_of_empty_string_is_empty_vec() {
178 let parsed = read_jsonl_v1("").unwrap();
179 assert!(parsed.is_empty());
180 }
181
182 #[test]
183 fn read_jsonl_surfaces_malformed_line_error() {
184 let text = "{\"not\": \"a valid IndexEntryV1\"}\n";
185 assert!(read_jsonl_v1(text).is_err());
186 }
187}