1use crate::imports::*;
2use include_dir::{include_dir, Dir};
3use std::collections::HashMap;
4use std::path::PathBuf;
5use ureq;
6
7#[cfg(feature = "resources")]
8pub const RESOURCES_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/resources");
9
10pub trait SerdeAPI: Serialize + for<'a> Deserialize<'a> {
11 const ACCEPTED_BYTE_FORMATS: &'static [&'static str] = &["yaml", "json", "toml", "bin"];
12 const ACCEPTED_STR_FORMATS: &'static [&'static str] = &["yaml", "json", "toml"];
13 const RESOURCE_PREFIX: &'static str = "";
14 const CACHE_FOLDER: &'static str = "";
15
16 fn init(&mut self) -> anyhow::Result<()> {
18 Ok(())
19 }
20
21 #[cfg(feature = "resources")]
25 fn list_resources() -> Vec<String> {
26 if Self::RESOURCE_PREFIX.is_empty() {
27 Vec::<String>::new()
28 } else if let Some(resources_path) = RESOURCES_DIR.get_dir(Self::RESOURCE_PREFIX) {
29 let mut file_names: Vec<String> = resources_path
30 .files()
31 .filter_map(|entry| entry.path().file_name()?.to_str().map(String::from))
32 .collect();
33 file_names.retain(|f| {
34 Self::ACCEPTED_STR_FORMATS.contains(&f.split(".").last().unwrap_or_default())
35 });
36 file_names.sort();
37 file_names
38 } else {
39 Vec::<String>::new()
40 }
41 }
42
43 #[cfg(feature = "resources")]
49 fn from_resource<P: AsRef<Path>>(filepath: P, skip_init: bool) -> anyhow::Result<Self> {
50 let filepath = Path::new(Self::RESOURCE_PREFIX).join(filepath);
51 let extension = filepath
52 .extension()
53 .and_then(OsStr::to_str)
54 .with_context(|| format!("File extension could not be parsed: {filepath:?}"))?;
55 let file = RESOURCES_DIR
56 .get_file(&filepath)
57 .with_context(|| format!("File not found in resources: {filepath:?}"))?;
58 Self::from_reader(file.contents(), extension, skip_init)
59 }
60
61 fn to_file<P: AsRef<Path>>(&self, filepath: P) -> anyhow::Result<()> {
70 let filepath = filepath.as_ref();
71 let extension = filepath
72 .extension()
73 .and_then(OsStr::to_str)
74 .with_context(|| format!("File extension could not be parsed: {filepath:?}"))?;
75 self.to_writer(File::create(filepath)?, extension)
76 }
77
78 fn to_writer<W: std::io::Write>(&self, mut wtr: W, format: &str) -> anyhow::Result<()> {
79 match format.trim_start_matches('.').to_lowercase().as_str() {
80 "yaml" | "yml" => serde_yaml::to_writer(wtr, self)?,
81 "json" => serde_json::to_writer(wtr, self)?,
82 "toml" => wtr.write_all(self.to_toml()?.as_bytes())?,
83 #[cfg(feature = "bincode")]
84 "bin" => bincode::serialize_into(wtr, self)?,
85 _ => bail!(
86 "Unsupported format {format:?}, must be one of {:?}",
87 Self::ACCEPTED_BYTE_FORMATS
88 ),
89 }
90 Ok(())
91 }
92
93 fn from_file<P: AsRef<Path>>(filepath: P, skip_init: bool) -> anyhow::Result<Self> {
101 let filepath = filepath.as_ref();
102 let extension = filepath
103 .extension()
104 .and_then(OsStr::to_str)
105 .with_context(|| format!("File extension could not be parsed: {filepath:?}"))?;
106 let file = File::open(filepath).with_context(|| {
107 if !filepath.exists() {
108 format!("File not found: {filepath:?}")
109 } else {
110 format!("Could not open file: {filepath:?}")
111 }
112 })?;
113 Self::from_reader(file, extension, skip_init)
114 }
115
116 fn to_str(&self, format: &str) -> anyhow::Result<String> {
123 match format.trim_start_matches('.').to_lowercase().as_str() {
124 "yaml" | "yml" => self.to_yaml(),
125 "json" => self.to_json(),
126 "toml" => self.to_toml(),
127 _ => bail!(
128 "Unsupported format {format:?}, must be one of {:?}",
129 Self::ACCEPTED_STR_FORMATS
130 ),
131 }
132 }
133
134 fn from_str<S: AsRef<str>>(contents: S, format: &str, skip_init: bool) -> anyhow::Result<Self> {
142 Ok(
143 match format.trim_start_matches('.').to_lowercase().as_str() {
144 "yaml" | "yml" => Self::from_yaml(contents, skip_init)?,
145 "json" => Self::from_json(contents, skip_init)?,
146 "toml" => Self::from_toml(contents, skip_init)?,
147 _ => bail!(
148 "Unsupported format {format:?}, must be one of {:?}",
149 Self::ACCEPTED_STR_FORMATS
150 ),
151 },
152 )
153 }
154
155 fn from_reader<R: std::io::Read>(
163 mut rdr: R,
164 format: &str,
165 skip_init: bool,
166 ) -> anyhow::Result<Self> {
167 let mut deserialized: Self = match format.trim_start_matches('.').to_lowercase().as_str() {
168 "yaml" | "yml" => serde_yaml::from_reader(rdr)?,
169 "json" => serde_json::from_reader(rdr)?,
170 "toml" => {
171 let mut buf = String::new();
172 rdr.read_to_string(&mut buf)?;
173 Self::from_toml(buf, skip_init)?
174 }
175 #[cfg(feature = "bincode")]
176 "bin" => bincode::deserialize_from(rdr)?,
177 _ => bail!(
178 "Unsupported format {format:?}, must be one of {:?}",
179 Self::ACCEPTED_BYTE_FORMATS
180 ),
181 };
182 if !skip_init {
183 deserialized.init()?;
184 }
185 Ok(deserialized)
186 }
187
188 fn to_json(&self) -> anyhow::Result<String> {
190 Ok(serde_json::to_string(&self)?)
191 }
192
193 fn from_json<S: AsRef<str>>(json_str: S, skip_init: bool) -> anyhow::Result<Self> {
200 let mut json_de: Self = serde_json::from_str(json_str.as_ref())?;
201 if !skip_init {
202 json_de.init()?;
203 }
204 Ok(json_de)
205 }
206
207 fn to_yaml(&self) -> anyhow::Result<String> {
209 Ok(serde_yaml::to_string(&self)?)
210 }
211
212 fn from_yaml<S: AsRef<str>>(yaml_str: S, skip_init: bool) -> anyhow::Result<Self> {
219 let mut yaml_de: Self = serde_yaml::from_str(yaml_str.as_ref())?;
220 if !skip_init {
221 yaml_de.init()?;
222 }
223 Ok(yaml_de)
224 }
225
226 fn to_toml(&self) -> anyhow::Result<String> {
227 Ok(toml::to_string(&self)?)
228 }
229
230 fn from_toml<S: AsRef<str>>(toml_str: S, skip_init: bool) -> anyhow::Result<Self> {
231 let mut toml_de: Self = toml::from_str(toml_str.as_ref())?;
232 if !skip_init {
233 toml_de.init()?;
234 }
235 Ok(toml_de)
236 }
237
238 #[cfg(feature = "bincode")]
240 fn to_bincode(&self) -> anyhow::Result<Vec<u8>> {
241 Ok(bincode::serialize(&self)?)
242 }
243
244 #[cfg(feature = "bincode")]
251 fn from_bincode(encoded: &[u8], skip_init: bool) -> anyhow::Result<Self> {
252 let mut bincode_de: Self = bincode::deserialize(encoded)?;
253 if !skip_init {
254 bincode_de.init()?;
255 }
256 Ok(bincode_de)
257 }
258
259 fn from_url<S: AsRef<str>>(url: S, skip_init: bool) -> anyhow::Result<Self> {
266 let url = url::Url::parse(url.as_ref())?;
267 let format = url
268 .path_segments()
269 .and_then(|segments| segments.last())
270 .and_then(|filename| Path::new(filename).extension())
271 .and_then(OsStr::to_str)
272 .with_context(|| "Could not parse file format from URL: {url:?}")?;
273 let response = ureq::get(url.as_ref()).call()?.into_reader();
274 Self::from_reader(response, format, skip_init)
275 }
276
277 #[cfg(feature = "default")]
289 fn to_cache<P: AsRef<Path>>(&self, file_path: P) -> anyhow::Result<()> {
290 let file_name = file_path
291 .as_ref()
292 .file_name()
293 .with_context(|| "Could not determine file name")?
294 .to_str()
295 .context("Could not determine file name.")?;
296 let file_path_internal = file_path
297 .as_ref()
298 .to_str()
299 .context("Could not determine file name.")?;
300 let subpath = if file_name == file_path_internal {
301 PathBuf::from(Self::CACHE_FOLDER)
302 } else {
303 Path::new(Self::CACHE_FOLDER).join(
304 file_path_internal
305 .strip_suffix(file_name)
306 .context("Could not determine path to subdirectory.")?,
307 )
308 };
309 let data_subdirectory = create_project_subdir(subpath)
310 .with_context(|| "Could not find or build Fastsim data subdirectory.")?;
311 let file_path = data_subdirectory.join(file_name);
312 self.to_file(file_path)
313 }
314
315 #[cfg(feature = "default")]
332 fn from_cache<P: AsRef<Path>>(file_path: P, skip_init: bool) -> anyhow::Result<Self> {
333 let full_file_path = Path::new(Self::CACHE_FOLDER).join(file_path);
334 let path_including_directory = path_to_cache()?.join(full_file_path);
335 Self::from_file(path_including_directory, skip_init)
336 }
337}
338
339pub trait ApproxEq<Rhs = Self> {
340 fn approx_eq(&self, other: &Rhs, tol: f64) -> bool;
341}
342
343macro_rules! impl_approx_eq_for_strict_eq_types {
344 ($($strict_eq_type: ty),*) => {
345 $(
346 impl ApproxEq for $strict_eq_type {
347 fn approx_eq(&self, other: &$strict_eq_type, _tol: f64) -> bool {
348 return self == other;
349 }
350 }
351 )*
352 }
353}
354
355impl_approx_eq_for_strict_eq_types!(
356 u8, u16, u32, u64, u128, usize, i8, i16, i32, i64, i128, isize, bool, &str, String
357);
358
359macro_rules! impl_approx_eq_for_floats {
360 ($($float_type: ty),*) => {
361 $(
362 impl ApproxEq for $float_type {
363 fn approx_eq(&self, other: &$float_type, tol: f64) -> bool {
364 return (((other - self) / (self + other)).abs() as f64) < tol || ((other - self).abs() as f64) < tol;
365 }
366 }
367 )*
368 }
369}
370
371impl_approx_eq_for_floats!(f32, f64);
372
373impl<T> ApproxEq for Vec<T>
374where
375 T: ApproxEq,
376{
377 fn approx_eq(&self, other: &Vec<T>, tol: f64) -> bool {
378 return self
379 .iter()
380 .zip(other.iter())
381 .all(|(x, y)| x.approx_eq(y, tol));
382 }
383}
384
385impl<T> ApproxEq for Array1<T>
386where
387 T: ApproxEq + std::clone::Clone,
388{
389 fn approx_eq(&self, other: &Array1<T>, tol: f64) -> bool {
390 self.to_vec().approx_eq(&other.to_vec(), tol)
391 }
392}
393
394impl<T> ApproxEq for Option<T>
395where
396 T: ApproxEq,
397{
398 fn approx_eq(&self, other: &Option<T>, tol: f64) -> bool {
399 if self.is_none() && other.is_none() {
400 true
401 } else if self.is_some() && other.is_some() {
402 self.as_ref()
403 .unwrap()
404 .approx_eq(other.as_ref().unwrap(), tol)
405 } else {
406 false
407 }
408 }
409}
410
411impl<K, V, S> ApproxEq for HashMap<K, V, S>
412where
413 K: Eq + std::hash::Hash,
414 V: ApproxEq,
415 S: std::hash::BuildHasher,
416{
417 fn approx_eq(&self, other: &HashMap<K, V, S>, tol: f64) -> bool {
418 if self.len() != other.len() {
419 return false;
420 }
421 return self
422 .iter()
423 .all(|(key, value)| other.get(key).map_or(false, |v| value.approx_eq(v, tol)));
424 }
425}
426
427pub trait IterMaxMin<A: PartialOrd> {
429 fn max(&self) -> anyhow::Result<&A>;
430 fn min(&self) -> anyhow::Result<&A>;
431}
432
433#[allow(clippy::manual_try_fold)] impl IterMaxMin<f64> for Array1<f64> {
435 fn max(&self) -> anyhow::Result<&f64> {
436 let first = self.first().ok_or(anyhow!("empty input"))?;
437 self.fold(Ok(first), |acc, elem| {
438 let acc = acc?;
439 match elem.partial_cmp(acc).ok_or(anyhow!("undefined order"))? {
440 cmp::Ordering::Greater => Ok(elem),
441 _ => Ok(acc),
442 }
443 })
444 }
445 fn min(&self) -> anyhow::Result<&f64> {
446 let first = self.first().ok_or(anyhow!("empty input"))?;
447 self.fold(Ok(first), |acc, elem| {
448 let acc = acc?;
449 match elem.partial_cmp(acc).ok_or(anyhow!("undefined order"))? {
450 cmp::Ordering::Less => Ok(elem),
451 _ => Ok(acc),
452 }
453 })
454 }
455}
456
457#[cfg(test)]
458mod tests {
459 use crate::imports::SerdeAPI;
460
461 #[test]
462 #[cfg(feature = "resources")]
463 fn test_list_resources() {
464 let cyc_resource_list = crate::cycle::RustCycle::list_resources();
465 assert!(cyc_resource_list.len() == 3);
466 assert!(cyc_resource_list[0] == "HHDDTCruiseSmooth.csv");
467 let veh_resource_list = crate::vehicle::RustVehicle::list_resources();
472 println!("{:?}", veh_resource_list);
473 assert!(veh_resource_list.len() == 1);
474 assert!(veh_resource_list[0] == "2017_Toyota_Highlander_3.5_L.yaml")
475 }
476}