use eww_shared_util::{Span, Spanned}; use itertools::Itertools; use serde::{Deserialize, Serialize}; use std::{convert::TryFrom, fmt, iter::FromIterator, str::FromStr}; pub type Result = std::result::Result; #[derive(Debug, thiserror::Error)] #[error("Failed to turn `{value}` into a value of type {target_type}")] pub struct ConversionError { pub value: DynVal, pub target_type: &'static str, pub source: Option>, } impl ConversionError { pub fn new(value: DynVal, target_type: &'static str, source: impl std::error::Error + 'static + Sync + Send) -> Self { ConversionError { value, target_type, source: Some(Box::new(source)) } } } impl Spanned for ConversionError { fn span(&self) -> Span { self.value.1 } } #[derive(Clone, Deserialize, Serialize, Eq)] pub struct DynVal(pub String, pub Span); impl From for DynVal { fn from(s: String) -> Self { DynVal(s, Span::DUMMY) } } impl fmt::Display for DynVal { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", self.0) } } impl fmt::Debug for DynVal { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "\"{}\"", self.0) } } /// Manually implement equality, to allow for values in different formats (i.e. "1" and "1.0") to still be considered as equal. impl std::cmp::PartialEq for DynVal { fn eq(&self, other: &Self) -> bool { if let (Ok(a), Ok(b)) = (self.as_f64(), other.as_f64()) { a == b } else { self.0 == other.0 } } } impl FromIterator for DynVal { fn from_iter>(iter: T) -> Self { DynVal(iter.into_iter().join(""), Span::DUMMY) } } impl std::str::FromStr for DynVal { type Err = ConversionError; /// parses the value, trying to turn it into a number and a boolean first, /// before deciding that it is a string. fn from_str(s: &str) -> Result { Ok(DynVal::from_string(s.to_string())) } } pub trait FromDynVal: Sized { type Err; fn from_dynval(x: &DynVal) -> std::result::Result; } impl> FromDynVal for T { type Err = E; fn from_dynval(x: &DynVal) -> std::result::Result { x.0.parse() } } macro_rules! impl_dynval_from { ($($t:ty),*) => { $(impl From<$t> for DynVal { fn from(x: $t) -> Self { DynVal(x.to_string(), Span::DUMMY) } })* }; } impl_dynval_from!(bool, i32, u32, f32, u8, f64, &str); impl TryFrom for DynVal { type Error = serde_json::Error; fn try_from(value: serde_json::Value) -> std::result::Result { Ok(DynVal(serde_json::to_string(&value)?, Span::DUMMY)) } } impl From for DynVal { fn from(d: std::time::Duration) -> Self { DynVal(format!("{}ms", d.as_millis()), Span::DUMMY) } } impl From<&serde_json::Value> for DynVal { fn from(v: &serde_json::Value) -> Self { DynVal( v.as_str() .map(|x| x.to_string()) .or_else(|| serde_json::to_string(v).ok()) .unwrap_or_else(|| "".to_string()), Span::DUMMY, ) } } impl Spanned for DynVal { fn span(&self) -> Span { self.1 } } impl DynVal { pub fn at(mut self, span: Span) -> Self { self.1 = span; self } pub fn from_string(s: String) -> Self { DynVal(s, Span::DUMMY) } pub fn read_as>(&self) -> std::result::Result { T::from_dynval(self) } pub fn into_inner(self) -> String { self.0 } /// This will never fail pub fn as_string(&self) -> Result { Ok(self.0.to_owned()) } pub fn as_f64(&self) -> Result { self.0.parse().map_err(|e| ConversionError::new(self.clone(), "f64", e)) } pub fn as_i32(&self) -> Result { self.0.parse().map_err(|e| ConversionError::new(self.clone(), "i32", e)) } pub fn as_bool(&self) -> Result { self.0.parse().map_err(|e| ConversionError::new(self.clone(), "bool", e)) } pub fn as_duration(&self) -> Result { use std::time::Duration; let s = &self.0; if s.ends_with("ms") { Ok(Duration::from_millis( s.trim_end_matches("ms").parse().map_err(|e| ConversionError::new(self.clone(), "integer", e))?, )) } else if s.ends_with('s') { let secs = s.trim_end_matches('s').parse::().map_err(|e| ConversionError::new(self.clone(), "number", e))?; Ok(Duration::from_millis(f64::floor(secs * 1000f64) as u64)) } else if s.ends_with('m') || s.ends_with("min") { let minutes = s .trim_end_matches("min") .trim_end_matches('m') .parse::() .map_err(|e| ConversionError::new(self.clone(), "number", e))?; Ok(Duration::from_secs(f64::floor(minutes * 60f64) as u64)) } else if s.ends_with('h') { let hours = s.trim_end_matches('h').parse::().map_err(|e| ConversionError::new(self.clone(), "number", e))?; Ok(Duration::from_secs(f64::floor(hours * 60f64 * 60f64) as u64)) } else { Err(ConversionError { value: self.clone(), target_type: "duration", source: None }) } } // TODO this should return Result> and use json parsing pub fn as_vec(&self) -> Result> { if self.0.is_empty() { Ok(Vec::new()) } else { match self.0.strip_prefix('[').and_then(|x| x.strip_suffix(']')) { Some(content) => { let mut items: Vec = content.split(',').map(|x: &str| x.to_string()).collect(); let mut removed = 0; for times_ran in 0..items.len() { // escapes `,` if there's a `\` before em if items[times_ran - removed].ends_with('\\') { items[times_ran - removed].pop(); let it = items.remove((times_ran + 1) - removed); items[times_ran - removed] += ","; items[times_ran - removed] += ⁢ removed += 1; } } Ok(items) } None => Err(ConversionError { value: self.clone(), target_type: "vec", source: None }), } } } pub fn as_json_value(&self) -> Result { serde_json::from_str::(&self.0) .map_err(|e| ConversionError::new(self.clone(), "json-value", Box::new(e))) } pub fn as_json_array(&self) -> Result> { serde_json::from_str::(&self.0) .map_err(|e| ConversionError::new(self.clone(), "json-value", Box::new(e)))? .as_array() .cloned() .ok_or_else(|| ConversionError { value: self.clone(), target_type: "json-array", source: None }) } pub fn as_json_object(&self) -> Result> { serde_json::from_str::(&self.0) .map_err(|e| ConversionError::new(self.clone(), "json-value", Box::new(e)))? .as_object() .cloned() .ok_or_else(|| ConversionError { value: self.clone(), target_type: "json-object", source: None }) } } #[cfg(test)] mod test { use super::*; #[test] fn test_parse_vec() { insta::assert_debug_snapshot!(DynVal::from("[]").as_vec()); insta::assert_debug_snapshot!(DynVal::from("[hi]").as_vec()); insta::assert_debug_snapshot!(DynVal::from("[hi,ho,hu]").as_vec()); insta::assert_debug_snapshot!(DynVal::from("[hi\\,ho]").as_vec()); insta::assert_debug_snapshot!(DynVal::from("[hi\\,ho,hu]").as_vec()); insta::assert_debug_snapshot!(DynVal::from("").as_vec()); insta::assert_debug_snapshot!(DynVal::from("[a,b").as_vec()); insta::assert_debug_snapshot!(DynVal::from("a]").as_vec()); } #[test] fn test_parse_duration() { insta::assert_debug_snapshot!(DynVal::from("100ms").as_duration()); insta::assert_debug_snapshot!(DynVal::from("1s").as_duration()); insta::assert_debug_snapshot!(DynVal::from("0.1s").as_duration()); insta::assert_debug_snapshot!(DynVal::from("5m").as_duration()); insta::assert_debug_snapshot!(DynVal::from("5min").as_duration()); insta::assert_debug_snapshot!(DynVal::from("0.5m").as_duration()); insta::assert_debug_snapshot!(DynVal::from("1h").as_duration()); insta::assert_debug_snapshot!(DynVal::from("0.5h").as_duration()); } }