eww/src/util.rs
2021-06-23 11:00:40 +02:00

199 lines
6.1 KiB
Rust

use anyhow::*;
use extend::ext;
use itertools::Itertools;
use std::path::Path;
#[macro_export]
macro_rules! impl_try_from {
($typ:ty {
$(
for $for:ty => |$arg:ident| $code:expr
);*;
}) => {
$(impl TryFrom<$typ> for $for {
type Error = anyhow::Error;
fn try_from($arg: $typ) -> Result<Self> {
$code
}
})*
};
}
#[macro_export]
macro_rules! try_logging_errors {
($context:expr => $code:block) => {{
let result: Result<_> = try { $code };
if let Err(err) = result {
log::error!("[{}:{}] Error while {}: {:?}", ::std::file!(), ::std::line!(), $context, err);
}
}};
}
#[macro_export]
macro_rules! print_result_err {
($context:expr, $result:expr $(,)?) => {{
if let Err(err) = $result {
log::error!("[{}:{}] Error {}: {:?}", ::std::file!(), ::std::line!(), $context, err);
}
}};
}
#[macro_export]
macro_rules! loop_select {
($($body:tt)*) => {
loop {
::tokio::select! {
$($body)*
};
}
}
}
/// Parse a string with a concrete set of options into some data-structure,
/// and return a nicely formatted error message on invalid values. I.e.:
/// ```rs
/// let input = "up";
/// enum_parse { "direction", input,
/// "up" => Direction::Up,
/// "down" => Direction::Down,
/// }
/// ```
#[macro_export]
macro_rules! enum_parse {
($name:literal, $input:expr, $($($s:literal)|* => $val:expr),* $(,)?) => {
let input = $input;
match input {
$( $( $s )|* => Ok($val) ),*,
_ => Err(anyhow!(concat!("Couldn't parse ", $name, ": '{}'. Possible values are ", $($($s),*),*), input))
}
};
}
/// Compute the difference of two lists, returning a tuple of
/// (
/// elements that where in a but not in b,
/// elements that where in b but not in a
/// ).
pub fn list_difference<'a, 'b, T: PartialEq>(a: &'a [T], b: &'b [T]) -> (Vec<&'a T>, Vec<&'b T>) {
let mut missing = Vec::new();
for elem in a {
if !b.contains(elem) {
missing.push(elem);
}
}
let mut new = Vec::new();
for elem in b {
if !a.contains(elem) {
new.push(elem);
}
}
(missing, new)
}
/// Joins two paths while keeping it somewhat pretty.
/// If the second path is absolute, this will just return the second path.
/// If it is relative, it will return the second path joined onto the first path, removing any `./` if present.
/// TODO this is not yet perfect, as it will still leave ../ and multiple ./ etc,... check for a Path::simplify or something.
pub fn join_path_pretty<P: AsRef<std::path::Path>, P2: AsRef<std::path::Path>>(a: P, b: P2) -> std::path::PathBuf {
let a = a.as_ref();
let b = b.as_ref();
if b.is_absolute() {
b.to_path_buf()
} else {
a.parent().unwrap().join(b.strip_prefix("./").unwrap_or(&b))
}
}
/// extends a hashmap, returning a list of keys that already where present in the hashmap.
pub fn extend_safe<K: std::cmp::Eq + std::hash::Hash + Clone, V, T: IntoIterator<Item = (K, V)>>(
a: &mut std::collections::HashMap<K, V>,
b: T,
) -> Vec<K> {
b.into_iter().filter_map(|(k, v)| a.insert(k.clone(), v).map(|_| k.clone())).collect()
}
/// read an scss file, replace all environment variable references within it and
/// then parse it into css.
pub fn parse_scss_from_file(path: &Path) -> Result<String> {
let config_dir = path.parent().context("Given SCSS file has no parent directory?!")?;
let scss_file_content =
std::fs::read_to_string(path).with_context(|| format!("Given SCSS File Doesnt Exist! {}", path.display()))?;
let file_content = replace_env_var_references(scss_file_content);
let grass_config = grass::Options::default().load_path(config_dir);
grass::from_string(file_content, &grass_config).map_err(|err| anyhow!("Encountered SCSS parsing error: {:?}", err))
}
#[ext(pub, name = StringExt)]
impl<T: AsRef<str>> T {
/// check if the string is empty after removing all linebreaks and trimming
/// whitespace
fn is_blank(self) -> bool {
self.as_ref().replace('\n', "").trim().is_empty()
}
/// trim all lines in a string
fn trim_lines(self) -> String {
self.as_ref().lines().map(|line| line.trim()).join("\n")
}
}
pub fn parse_duration(s: &str) -> Result<std::time::Duration> {
use std::time::Duration;
if s.ends_with("ms") {
Ok(Duration::from_millis(s.trim_end_matches("ms").parse()?))
} else if s.ends_with('s') {
Ok(Duration::from_secs(s.trim_end_matches('s').parse()?))
} else if s.ends_with('m') {
Ok(Duration::from_secs(s.trim_end_matches('m').parse::<u64>()? * 60))
} else if s.ends_with('h') {
Ok(Duration::from_secs(s.trim_end_matches('h').parse::<u64>()? * 60 * 60))
} else {
Err(anyhow!("unrecognized time format: {}", s))
}
}
pub trait IterAverage {
fn avg(self) -> f32;
}
impl<I: Iterator<Item = f32>> IterAverage for I {
fn avg(self) -> f32 {
let mut total = 0f32;
let mut cnt = 0f32;
for value in self {
total += value;
cnt += 1f32;
}
total / cnt
}
}
/// Replace all env-var references of the format `"something ${foo}"` in a string
/// by the actual env-variables. If the env-var isn't found, will replace the
/// reference with an empty string.
pub fn replace_env_var_references(input: String) -> String {
lazy_static::lazy_static! {
static ref ENV_VAR_PATTERN: regex::Regex = regex::Regex::new(r"\$\{([^\s]*)\}").unwrap();
}
ENV_VAR_PATTERN
.replace_all(&input, |var_name: &regex::Captures| std::env::var(var_name.get(1).unwrap().as_str()).unwrap_or_default())
.into_owned()
}
#[cfg(test)]
mod test {
use super::replace_env_var_references;
use std;
#[test]
fn test_replace_env_var_references() {
let scss = "$test: ${USER};";
assert_eq!(
replace_env_var_references(String::from(scss)),
format!("$test: {};", std::env::var("USER").unwrap_or_default())
)
}
}