diff --git a/Cargo.lock b/Cargo.lock index cce744f..bd0663d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -338,6 +338,7 @@ dependencies = [ "num", "pretty_assertions", "regex", + "roxmltree", "serde", "stoppable_thread", "structopt", @@ -1440,6 +1441,15 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "roxmltree" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17dfc6c39f846bfc7d2ec442ad12055d79608d501380789b965d22f9354451f2" +dependencies = [ + "xmlparser", +] + [[package]] name = "rustc-demangle" version = "0.1.16" @@ -1789,3 +1799,9 @@ dependencies = [ "winapi 0.2.8", "winapi-build", ] + +[[package]] +name = "xmlparser" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "114ba2b24d2167ef6d67d7d04c8cc86522b87f490025f39f0303b7db5bf5e3d8" diff --git a/Cargo.toml b/Cargo.toml index bd38bdd..a8ccc49 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,6 +28,7 @@ calloop = "0.6" crossbeam-channel = "0.4" num = "0.3" stoppable_thread = "0.2" +roxmltree = "0.13" #thiserror = "1.0" diff --git a/src/config/element.rs b/src/config/element.rs index 8a45280..65f1fb8 100644 --- a/src/config/element.rs +++ b/src/config/element.rs @@ -14,6 +14,35 @@ pub struct WidgetDefinition { } impl WidgetDefinition { + pub fn from_xml(xml: roxmltree::Node) -> Result { + if !xml.is_element() { + bail!("Tried to parse element of type {:?} as Widget definition", xml.node_type()); + } else if xml.tag_name().name().to_lowercase() != "def" { + bail!( + "Illegal element: only may be used in definition block, but found '{}'", + xml.tag_name().name() + ); + } else if xml.children().count() != 1 { + bail!( + "Widget definition '{}' needs to contain exactly one element", + xml.tag_name().name() + ); + } + + Ok(WidgetDefinition { + name: xml.try_attribute("name")?.to_owned(), + + size: if let Some(node) = xml.children().find(|child| child.tag_name().name() == "size") { + Some((node.try_attribute("x")?.parse()?, node.try_attribute("y")?.parse()?)) + } else { + None + }, + + // we can unwrap here, because we previously verified that there is exactly one child + structure: WidgetUse::from_xml(xml.first_child().unwrap())?, + }) + } + pub fn parse_hocon(name: String, hocon: &Hocon) -> Result { let definition = hocon.as_hash()?; let structure = definition @@ -42,6 +71,40 @@ pub struct WidgetUse { pub attrs: HashMap, } +impl WidgetUse { + pub fn from_xml(xml: roxmltree::Node) -> Result { + match xml.node_type() { + roxmltree::NodeType::Text => Ok(WidgetUse::simple_text(AttrValue::parse_string( + xml.text() + .context("couldn't get text from node")? + .trim_matches('\n') + .trim() + .to_owned(), + ))), + roxmltree::NodeType::Element => { + let widget_name = xml.tag_name(); + let attrs = xml + .attributes() + .iter() + .map(|attr| (attr.name().to_owned(), AttrValue::parse_string(attr.value().to_owned()))) + .collect::>(); + let children = xml + .children() + .filter(|child| !child.is_comment()) + .filter(|child| !(child.is_text() && child.text().unwrap().trim().trim_matches('\n').is_empty())) + .map(|child| WidgetUse::from_xml(child)) + .collect::>()?; + Ok(WidgetUse { + name: widget_name.name().to_string(), + attrs, + children, + }) + } + _ => Err(anyhow!("Tried to parse node of type {:?} as widget use", xml.node_type())), + } + } +} + impl WidgetUse { pub fn new(name: String, children: Vec) -> Self { WidgetUse { @@ -174,4 +237,29 @@ mod test { expected ); } + + #[test] + fn test_parse_widget_use_xml() { + let input = r#" + + + foo + + "#; + let document = roxmltree::Document::parse(input).unwrap(); + let xml = document.root_element().clone(); + + let expected = WidgetUse { + name: "widget_name".to_owned(), + attrs: hashmap! { + "attr1".to_owned() => AttrValue::Concrete(PrimitiveValue::String("hi".to_owned())), + "attr2".to_owned() => AttrValue::Concrete(PrimitiveValue::Number(12f64)), + }, + children: vec![ + WidgetUse::new("child_widget".to_owned(), Vec::new()), + WidgetUse::simple_text(AttrValue::Concrete(PrimitiveValue::String("foo".to_owned()))), + ], + }; + assert_eq!(expected, WidgetUse::from_xml(xml).unwrap()); + } } diff --git a/src/config/mod.rs b/src/config/mod.rs index 6f6d819..adff727 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -5,9 +5,11 @@ use hocon::*; use hocon_ext::HoconExt; use std::collections::HashMap; use std::convert::TryFrom; +use xml_ext::*; pub mod element; pub mod hocon_ext; +pub mod xml_ext; #[allow(unused)] macro_rules! try_type { @@ -31,7 +33,49 @@ pub struct EwwConfig { impl EwwConfig { pub fn read_from_file>(path: P) -> Result { let content = std::fs::read_to_string(path)?; - EwwConfig::from_hocon(&parse_hocon(&content)?) + let document = roxmltree::Document::parse(&content)?; + EwwConfig::from_xml(document.root_element()) + } + + pub fn from_xml(xml: roxmltree::Node) -> Result { + let definitions = xml + .find_child_with_tag("definitions")? + .children() + .map(|child| { + let def = WidgetDefinition::from_xml(child)?; + Ok((def.name.clone(), def)) + }) + .collect::>>() + .context("error parsing widget definitions")?; + + let windows = xml + .find_child_with_tag("windows")? + .children() + .map(|child| Ok((child.try_attribute("name")?.to_owned(), EwwWindowDefinition::from_xml(child)?))) + .collect::>>() + .context("error parsing window definitions")?; + + let default_vars = xml + .find_child_with_tag("variables") + .map(|variables_node| { + variables_node + .children() + .map(|child| { + Some(( + child.tag_name().name().to_owned(), + PrimitiveValue::parse_string(child.text()?.trim_matches('\n').trim()), + )) + }) + .collect::>>() + }) + .unwrap_or_default() + .context("error parsing default variable value")?; + + Ok(EwwConfig { + widgets: definitions, + windows, + default_vars, + }) } pub fn from_hocon(hocon: &Hocon) -> Result { @@ -87,7 +131,24 @@ pub struct EwwWindowDefinition { } impl EwwWindowDefinition { - pub fn from_hocon(hocon: &Hocon) -> Result { + pub fn from_xml(xml: roxmltree::Node) -> Result { + if xml.tag_name().name() != "window" { + bail!( + "Only tags are valid window definitions, but found {}", + xml.tag_name().name() + ); + } + + let size_node = xml.find_child_with_tag("size")?; + let size = (size_node.try_attribute("x")?.parse()?, size_node.try_attribute("y")?.parse()?); + let pos_node = xml.find_child_with_tag("pos")?; + let position = (pos_node.try_attribute("x")?.parse()?, pos_node.try_attribute("y")?.parse()?); + + let widget = WidgetUse::from_xml(xml.find_child_with_tag("widget")?)?; + Ok(EwwWindowDefinition { position, size, widget }) + } + + pub fn from_hocon(hocon: &Hocon) -> Result { let data = hocon.as_hash().context("window config has to be a map structure")?; let position: Option<_> = try { ( diff --git a/src/config/xml_ext.rs b/src/config/xml_ext.rs new file mode 100644 index 0000000..649455b --- /dev/null +++ b/src/config/xml_ext.rs @@ -0,0 +1,19 @@ +use anyhow::*; +use extend::ext; + +#[ext(pub)] +impl<'a, 'b> roxmltree::Node<'a, 'b> { + fn find_child_with_tag(&self, tag_name: &str) -> Result + where + Self: Sized, + { + self.children() + .find(|child| child.tag_name().name() == tag_name) + .with_context(|| anyhow!("node {} contained no child of type {}", self.tag_name().name(), tag_name,)) + } + + fn try_attribute(&self, key: &str) -> Result<&str> { + self.attribute(key) + .with_context(|| anyhow!("attribute '{}' missing from '{}'", key, self.tag_name().name())) + } +} diff --git a/src/value.rs b/src/value.rs index ad52638..d56821a 100644 --- a/src/value.rs +++ b/src/value.rs @@ -12,20 +12,12 @@ pub enum PrimitiveValue { Boolean(bool), } -#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] -pub struct PollingCommandValue { - command: String, - interval: std::time::Duration, -} - impl std::str::FromStr for PrimitiveValue { type Err = anyhow::Error; + /// 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(s.parse() - .map(PrimitiveValue::Number) - .or_else(|_| s.parse().map(PrimitiveValue::Boolean)) - .unwrap_or_else(|_| PrimitiveValue::String(remove_surrounding(s, '\'').to_string()))) + Ok(PrimitiveValue::parse_string(s)) } } @@ -61,6 +53,13 @@ impl From<&str> for PrimitiveValue { } impl PrimitiveValue { + /// parses the value, trying to turn it into a number and a boolean first, before deciding that it is a string. + pub fn parse_string(s: &str) -> Self { + s.parse() + .map(PrimitiveValue::Number) + .or_else(|_| s.parse().map(PrimitiveValue::Boolean)) + .unwrap_or_else(|_| PrimitiveValue::String(remove_surrounding(s, '\'').to_string())) + } pub fn as_string(&self) -> Result { match self { PrimitiveValue::String(x) => Ok(x.clone()), @@ -137,11 +136,13 @@ impl AttrValue { try_match!(AttrValue::VarRef(x) = self).map_err(|e| anyhow!("{:?} is not a VarRef", e)) } - pub fn from_string(s: String) -> Self { + /// parses the value, trying to turn it into VarRef, + /// a number and a boolean first, before deciding that it is a string. + pub fn parse_string(s: String) -> Self { if s.starts_with("$$") { AttrValue::VarRef(s.trim_start_matches("$$").to_string()) } else { - AttrValue::Concrete(PrimitiveValue::String(s.clone())) + AttrValue::Concrete(PrimitiveValue::parse_string(&s)) } } } @@ -155,7 +156,7 @@ impl std::convert::TryFrom<&Hocon> for AttrValue { type Error = anyhow::Error; fn try_from(value: &Hocon) -> Result { Ok(match value { - Hocon::String(s) => AttrValue::from_string(s.clone()), + Hocon::String(s) => AttrValue::parse_string(s.clone()), Hocon::Integer(n) => AttrValue::Concrete(PrimitiveValue::Number(*n as f64)), Hocon::Real(n) => AttrValue::Concrete(PrimitiveValue::Number(*n as f64)), Hocon::Boolean(b) => AttrValue::Concrete(PrimitiveValue::Boolean(*b)),