use crate::imports::*;
use include_dir::{include_dir, Dir};
use std::collections::HashMap;
use std::path::PathBuf;
use ureq;
#[cfg(feature = "resources")]
pub const RESOURCES_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/resources");
pub trait SerdeAPI: Serialize + for<'a> Deserialize<'a> {
const ACCEPTED_BYTE_FORMATS: &'static [&'static str] = &["yaml", "json", "toml", "bin"];
const ACCEPTED_STR_FORMATS: &'static [&'static str] = &["yaml", "json", "toml"];
const RESOURCE_PREFIX: &'static str = "";
const CACHE_FOLDER: &'static str = "";
fn init(&mut self) -> anyhow::Result<()> {
Ok(())
}
#[cfg(feature = "resources")]
fn list_resources() -> Vec<String> {
if Self::RESOURCE_PREFIX.is_empty() {
Vec::<String>::new()
} else if let Some(resources_path) = RESOURCES_DIR.get_dir(Self::RESOURCE_PREFIX) {
let mut file_names: Vec<String> = resources_path
.files()
.filter_map(|entry| entry.path().file_name()?.to_str().map(String::from))
.collect();
file_names.retain(|f| {
Self::ACCEPTED_STR_FORMATS.contains(&f.split(".").last().unwrap_or_default())
});
file_names.sort();
file_names
} else {
Vec::<String>::new()
}
}
#[cfg(feature = "resources")]
fn from_resource<P: AsRef<Path>>(filepath: P, skip_init: bool) -> anyhow::Result<Self> {
let filepath = Path::new(Self::RESOURCE_PREFIX).join(filepath);
let extension = filepath
.extension()
.and_then(OsStr::to_str)
.with_context(|| format!("File extension could not be parsed: {filepath:?}"))?;
let file = RESOURCES_DIR
.get_file(&filepath)
.with_context(|| format!("File not found in resources: {filepath:?}"))?;
Self::from_reader(file.contents(), extension, skip_init)
}
fn to_file<P: AsRef<Path>>(&self, filepath: P) -> anyhow::Result<()> {
let filepath = filepath.as_ref();
let extension = filepath
.extension()
.and_then(OsStr::to_str)
.with_context(|| format!("File extension could not be parsed: {filepath:?}"))?;
self.to_writer(File::create(filepath)?, extension)
}
fn to_writer<W: std::io::Write>(&self, mut wtr: W, format: &str) -> anyhow::Result<()> {
match format.trim_start_matches('.').to_lowercase().as_str() {
"yaml" | "yml" => serde_yaml::to_writer(wtr, self)?,
"json" => serde_json::to_writer(wtr, self)?,
"toml" => wtr.write_all(self.to_toml()?.as_bytes())?,
#[cfg(feature = "bincode")]
"bin" => bincode::serialize_into(wtr, self)?,
_ => bail!(
"Unsupported format {format:?}, must be one of {:?}",
Self::ACCEPTED_BYTE_FORMATS
),
}
Ok(())
}
fn from_file<P: AsRef<Path>>(filepath: P, skip_init: bool) -> anyhow::Result<Self> {
let filepath = filepath.as_ref();
let extension = filepath
.extension()
.and_then(OsStr::to_str)
.with_context(|| format!("File extension could not be parsed: {filepath:?}"))?;
let file = File::open(filepath).with_context(|| {
if !filepath.exists() {
format!("File not found: {filepath:?}")
} else {
format!("Could not open file: {filepath:?}")
}
})?;
Self::from_reader(file, extension, skip_init)
}
fn to_str(&self, format: &str) -> anyhow::Result<String> {
match format.trim_start_matches('.').to_lowercase().as_str() {
"yaml" | "yml" => self.to_yaml(),
"json" => self.to_json(),
"toml" => self.to_toml(),
_ => bail!(
"Unsupported format {format:?}, must be one of {:?}",
Self::ACCEPTED_STR_FORMATS
),
}
}
fn from_str<S: AsRef<str>>(contents: S, format: &str, skip_init: bool) -> anyhow::Result<Self> {
Ok(
match format.trim_start_matches('.').to_lowercase().as_str() {
"yaml" | "yml" => Self::from_yaml(contents, skip_init)?,
"json" => Self::from_json(contents, skip_init)?,
"toml" => Self::from_toml(contents, skip_init)?,
_ => bail!(
"Unsupported format {format:?}, must be one of {:?}",
Self::ACCEPTED_STR_FORMATS
),
},
)
}
fn from_reader<R: std::io::Read>(
mut rdr: R,
format: &str,
skip_init: bool,
) -> anyhow::Result<Self> {
let mut deserialized: Self = match format.trim_start_matches('.').to_lowercase().as_str() {
"yaml" | "yml" => serde_yaml::from_reader(rdr)?,
"json" => serde_json::from_reader(rdr)?,
"toml" => {
let mut buf = String::new();
rdr.read_to_string(&mut buf)?;
Self::from_toml(buf, skip_init)?
}
#[cfg(feature = "bincode")]
"bin" => bincode::deserialize_from(rdr)?,
_ => bail!(
"Unsupported format {format:?}, must be one of {:?}",
Self::ACCEPTED_BYTE_FORMATS
),
};
if !skip_init {
deserialized.init()?;
}
Ok(deserialized)
}
fn to_json(&self) -> anyhow::Result<String> {
Ok(serde_json::to_string(&self)?)
}
fn from_json<S: AsRef<str>>(json_str: S, skip_init: bool) -> anyhow::Result<Self> {
let mut json_de: Self = serde_json::from_str(json_str.as_ref())?;
if !skip_init {
json_de.init()?;
}
Ok(json_de)
}
fn to_yaml(&self) -> anyhow::Result<String> {
Ok(serde_yaml::to_string(&self)?)
}
fn from_yaml<S: AsRef<str>>(yaml_str: S, skip_init: bool) -> anyhow::Result<Self> {
let mut yaml_de: Self = serde_yaml::from_str(yaml_str.as_ref())?;
if !skip_init {
yaml_de.init()?;
}
Ok(yaml_de)
}
fn to_toml(&self) -> anyhow::Result<String> {
Ok(toml::to_string(&self)?)
}
fn from_toml<S: AsRef<str>>(toml_str: S, skip_init: bool) -> anyhow::Result<Self> {
let mut toml_de: Self = toml::from_str(toml_str.as_ref())?;
if !skip_init {
toml_de.init()?;
}
Ok(toml_de)
}
#[cfg(feature = "bincode")]
fn to_bincode(&self) -> anyhow::Result<Vec<u8>> {
Ok(bincode::serialize(&self)?)
}
#[cfg(feature = "bincode")]
fn from_bincode(encoded: &[u8], skip_init: bool) -> anyhow::Result<Self> {
let mut bincode_de: Self = bincode::deserialize(encoded)?;
if !skip_init {
bincode_de.init()?;
}
Ok(bincode_de)
}
fn from_url<S: AsRef<str>>(url: S, skip_init: bool) -> anyhow::Result<Self> {
let url = url::Url::parse(url.as_ref())?;
let format = url
.path_segments()
.and_then(|segments| segments.last())
.and_then(|filename| Path::new(filename).extension())
.and_then(OsStr::to_str)
.with_context(|| "Could not parse file format from URL: {url:?}")?;
let response = ureq::get(url.as_ref()).call()?.into_reader();
Self::from_reader(response, format, skip_init)
}
#[cfg(feature = "default")]
fn to_cache<P: AsRef<Path>>(&self, file_path: P) -> anyhow::Result<()> {
let file_name = file_path
.as_ref()
.file_name()
.with_context(|| "Could not determine file name")?
.to_str()
.context("Could not determine file name.")?;
let file_path_internal = file_path
.as_ref()
.to_str()
.context("Could not determine file name.")?;
let subpath = if file_name == file_path_internal {
PathBuf::from(Self::CACHE_FOLDER)
} else {
Path::new(Self::CACHE_FOLDER).join(
file_path_internal
.strip_suffix(file_name)
.context("Could not determine path to subdirectory.")?,
)
};
let data_subdirectory = create_project_subdir(subpath)
.with_context(|| "Could not find or build Fastsim data subdirectory.")?;
let file_path = data_subdirectory.join(file_name);
self.to_file(file_path)
}
#[cfg(feature = "default")]
fn from_cache<P: AsRef<Path>>(file_path: P, skip_init: bool) -> anyhow::Result<Self> {
let full_file_path = Path::new(Self::CACHE_FOLDER).join(file_path);
let path_including_directory = path_to_cache()?.join(full_file_path);
Self::from_file(path_including_directory, skip_init)
}
}
pub trait ApproxEq<Rhs = Self> {
fn approx_eq(&self, other: &Rhs, tol: f64) -> bool;
}
macro_rules! impl_approx_eq_for_strict_eq_types {
($($strict_eq_type: ty),*) => {
$(
impl ApproxEq for $strict_eq_type {
fn approx_eq(&self, other: &$strict_eq_type, _tol: f64) -> bool {
return self == other;
}
}
)*
}
}
impl_approx_eq_for_strict_eq_types!(
u8, u16, u32, u64, u128, usize, i8, i16, i32, i64, i128, isize, bool, &str, String
);
macro_rules! impl_approx_eq_for_floats {
($($float_type: ty),*) => {
$(
impl ApproxEq for $float_type {
fn approx_eq(&self, other: &$float_type, tol: f64) -> bool {
return (((other - self) / (self + other)).abs() as f64) < tol || ((other - self).abs() as f64) < tol;
}
}
)*
}
}
impl_approx_eq_for_floats!(f32, f64);
impl<T> ApproxEq for Vec<T>
where
T: ApproxEq,
{
fn approx_eq(&self, other: &Vec<T>, tol: f64) -> bool {
return self
.iter()
.zip(other.iter())
.all(|(x, y)| x.approx_eq(y, tol));
}
}
impl<T> ApproxEq for Array1<T>
where
T: ApproxEq + std::clone::Clone,
{
fn approx_eq(&self, other: &Array1<T>, tol: f64) -> bool {
self.to_vec().approx_eq(&other.to_vec(), tol)
}
}
impl<T> ApproxEq for Option<T>
where
T: ApproxEq,
{
fn approx_eq(&self, other: &Option<T>, tol: f64) -> bool {
if self.is_none() && other.is_none() {
true
} else if self.is_some() && other.is_some() {
self.as_ref()
.unwrap()
.approx_eq(other.as_ref().unwrap(), tol)
} else {
false
}
}
}
impl<K, V, S> ApproxEq for HashMap<K, V, S>
where
K: Eq + std::hash::Hash,
V: ApproxEq,
S: std::hash::BuildHasher,
{
fn approx_eq(&self, other: &HashMap<K, V, S>, tol: f64) -> bool {
if self.len() != other.len() {
return false;
}
return self
.iter()
.all(|(key, value)| other.get(key).map_or(false, |v| value.approx_eq(v, tol)));
}
}
pub trait IterMaxMin<A: PartialOrd> {
fn max(&self) -> anyhow::Result<&A>;
fn min(&self) -> anyhow::Result<&A>;
}
#[allow(clippy::manual_try_fold)] impl IterMaxMin<f64> for Array1<f64> {
fn max(&self) -> anyhow::Result<&f64> {
let first = self.first().ok_or(anyhow!("empty input"))?;
self.fold(Ok(first), |acc, elem| {
let acc = acc?;
match elem.partial_cmp(acc).ok_or(anyhow!("undefined order"))? {
cmp::Ordering::Greater => Ok(elem),
_ => Ok(acc),
}
})
}
fn min(&self) -> anyhow::Result<&f64> {
let first = self.first().ok_or(anyhow!("empty input"))?;
self.fold(Ok(first), |acc, elem| {
let acc = acc?;
match elem.partial_cmp(acc).ok_or(anyhow!("undefined order"))? {
cmp::Ordering::Less => Ok(elem),
_ => Ok(acc),
}
})
}
}
#[cfg(test)]
mod tests {
use crate::imports::SerdeAPI;
#[test]
#[cfg(feature = "resources")]
fn test_list_resources() {
let cyc_resource_list = crate::cycle::RustCycle::list_resources();
assert!(cyc_resource_list.len() == 3);
assert!(cyc_resource_list[0] == "HHDDTCruiseSmooth.csv");
let veh_resource_list = crate::vehicle::RustVehicle::list_resources();
println!("{:?}", veh_resource_list);
assert!(veh_resource_list.len() == 1);
assert!(veh_resource_list[0] == "2017_Toyota_Highlander_3.5_L.yaml")
}
}