fastsim_core/
traits.rs

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    /// Specialized code to execute upon initialization
17    fn init(&mut self) -> anyhow::Result<()> {
18        Ok(())
19    }
20
21    /// List available (compiled) resources (stored in the rust binary)
22    /// # RESULT
23    /// vector of string of resource names that can be loaded
24    #[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    /// Read (deserialize) an object from a resource file packaged with the `fastsim-core` crate
44    ///
45    /// # Arguments:
46    ///
47    /// * `filepath` - Filepath, relative to the top of the `resources` folder (excluding any relevant prefix), from which to read the object
48    #[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    /// Write (serialize) an object to a file.
62    /// Supported file extensions are listed in [`ACCEPTED_BYTE_FORMATS`](`SerdeAPI::ACCEPTED_BYTE_FORMATS`).
63    /// Creates a new file if it does not already exist, otherwise truncates the existing file.
64    ///
65    /// # Arguments
66    ///
67    /// * `filepath` - The filepath at which to write the object
68    ///
69    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    /// Read (deserialize) an object from a file.
94    /// Supported file extensions are listed in [`ACCEPTED_BYTE_FORMATS`](`SerdeAPI::ACCEPTED_BYTE_FORMATS`).
95    ///
96    /// # Arguments:
97    ///
98    /// * `filepath`: The filepath from which to read the object
99    ///
100    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    /// Write (serialize) an object into a string
117    ///
118    /// # Arguments:
119    ///
120    /// * `format` - The target format, any of those listed in [`ACCEPTED_STR_FORMATS`](`SerdeAPI::ACCEPTED_STR_FORMATS`)
121    ///
122    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    /// Read (deserialize) an object from a string
135    ///
136    /// # Arguments:
137    ///
138    /// * `contents` - The string containing the object data
139    /// * `format` - The source format, any of those listed in [`ACCEPTED_STR_FORMATS`](`SerdeAPI::ACCEPTED_STR_FORMATS`)
140    ///
141    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    /// Deserialize an object from anything that implements [`std::io::Read`]
156    ///
157    /// # Arguments:
158    ///
159    /// * `rdr` - The reader from which to read object data
160    /// * `format` - The source format, any of those listed in [`ACCEPTED_BYTE_FORMATS`](`SerdeAPI::ACCEPTED_BYTE_FORMATS`)
161    ///
162    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    /// Write (serialize) an object to a JSON string
189    fn to_json(&self) -> anyhow::Result<String> {
190        Ok(serde_json::to_string(&self)?)
191    }
192
193    /// Read (deserialize) an object to a JSON string
194    ///
195    /// # Arguments
196    ///
197    /// * `json_str` - JSON-formatted string to deserialize from
198    ///
199    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    /// Write (serialize) an object to a YAML string
208    fn to_yaml(&self) -> anyhow::Result<String> {
209        Ok(serde_yaml::to_string(&self)?)
210    }
211
212    /// Read (deserialize) an object from a YAML string
213    ///
214    /// # Arguments
215    ///
216    /// * `yaml_str` - YAML-formatted string to deserialize from
217    ///
218    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    /// Write (serialize) an object to bincode-encoded bytes
239    #[cfg(feature = "bincode")]
240    fn to_bincode(&self) -> anyhow::Result<Vec<u8>> {
241        Ok(bincode::serialize(&self)?)
242    }
243
244    /// Read (deserialize) an object from bincode-encoded bytes
245    ///
246    /// # Arguments
247    ///
248    /// * `encoded` - Encoded bytes to deserialize from
249    ///
250    #[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    /// Instantiates an object from a url.  Accepts yaml and json file types  
260    /// # Arguments  
261    /// - url: URL (either as a string or url type) to object  
262    ///
263    /// Note: The URL needs to be a URL pointing directly to a file, for example
264    /// a raw github URL.
265    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    /// Takes an instantiated Rust object and saves it in the FASTSim data directory in
278    /// a rust_objects folder.  
279    /// WARNING: If there is a file already in the data subdirectory with the
280    /// same name, it will be replaced by the new file.  
281    /// # Arguments  
282    /// - self (rust object)  
283    /// - file_path: path to file within subdirectory. If only the file name is
284    /// listed, file will sit directly within the subdirectory of
285    /// the FASTSim data directory. If a path is given, the file will live
286    /// within the path specified, within the subdirectory CACHE_FOLDER of the
287    /// FASTSim data directory.
288    #[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    /// Instantiates a Rust object from the subdirectory within the FASTSim data
316    /// directory corresponding to the Rust Object ("vehices" for a RustVehice,
317    /// "cycles" for a RustCycle, and the root folder of the data directory for
318    /// all other objects).  
319    /// # Arguments  
320    /// - file_path: subpath to object, including file name, within subdirectory.
321    ///   If the file sits directly in the subdirectory, this will just be the
322    ///   file name.  
323    /// Note: This function will work for all objects cached using the
324    /// to_cache() method. If a file has been saved manually to a different
325    /// subdirectory than the correct one for the object type (for instance a
326    /// RustVehicle saved within a subdirectory other than "vehicles" using the
327    /// utils::url_to_cache() function), then from_cache() will not be able to
328    /// find and instantiate the object. Instead, use the from_file method, and
329    /// use the utils::path_to_cache() to find the FASTSim data directory
330    /// location if needed.
331    #[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
427/// This trait was heavily inspired by `ndarray-stats` crate
428pub 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)] // `try_fold` is apparently not implemented
434impl 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        // NOTE: at the time of writing this test, there is no
468        // vehicles subdirectory. The agreed-upon behavior in
469        // that case is that list_resources should return an
470        // empty vector of string.
471        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}