aeon 0.6.0

A configuration file format with macros for brevity.
Documentation
use crate::value::AeonValue;
use std::collections::HashMap;
use std::iter::FromIterator;

#[derive(Clone, Debug)]
pub struct AeonMacro {
    pub name: String,
    pub args: Vec<String>,
}

impl AeonMacro {
    pub fn new(name: String, args: Vec<String>) -> AeonMacro {
        AeonMacro { name, args }
    }

    pub fn new_cloned(name: &str, args: Vec<&str>) -> AeonMacro {
        AeonMacro {
            name: name.to_string(),
            args: args.into_iter().map(|a| a.to_string()).collect(),
        }
    }

    pub fn is_empty(&self) -> bool {
        self.args.is_empty()
    }

    pub fn len(&self) -> usize {
        self.args.len()
    }

    pub fn can_apply(&self, map: &HashMap<String, AeonValue>) -> bool {
        for arg in &self.args {
            if !map.contains_key(arg) {
                return false;
            }
        }
        true
    }

    pub fn apply(&self, index: usize, value: AeonValue, map: &mut HashMap<String, AeonValue>) {
        map.insert(self.args[index].clone(), value);
    }
}

#[derive(Debug)]
pub struct AeonProperty {
    pub name: String,
    pub value: AeonValue,
}

impl AeonProperty {
    pub fn new(name: String, value: AeonValue) -> AeonProperty {
        AeonProperty { name, value }
    }
}

#[derive(Debug, Default)]
pub struct AeonDocument {
    pub macros: HashMap<String, AeonMacro>,
    pub properties: HashMap<String, AeonProperty>,
    pub is_empty: bool,
}

impl AeonDocument {
    pub fn new() -> AeonDocument {
        AeonDocument {
            macros: HashMap::new(),
            properties: HashMap::new(),
            is_empty: true,
        }
    }

    pub fn try_from_object(object: AeonValue) -> Option<AeonDocument> {
        match object {
            AeonValue::Object(obj) => Some(Self::from_iter(obj)),
            _ => None,
        }
    }

    pub fn add_property(&mut self, name: &str, value: AeonValue) {
        if self.properties.contains_key(name) {
            return;
        }
        self.properties
            .insert(name.to_string(), AeonProperty::new(name.to_string(), value));
        self.is_empty = false;
    }

    pub fn add_or_replace_property(&mut self, name: String, value: AeonValue) {
        self.properties
            .insert(name.clone(), AeonProperty::new(name, value));
        self.is_empty = false;
    }

    pub fn add_macro(&mut self, mac: AeonMacro) {
        if self.macros.contains_key(&mac.name) {
            return;
        }
        self.macros.insert(mac.name.clone(), mac);
        self.is_empty = false;
    }

    pub fn add_or_replace_macro(&mut self, mac: AeonMacro) {
        self.macros.insert(mac.name.clone(), mac);
        self.is_empty = false;
    }

    pub fn set_macros(&mut self, macros: HashMap<String, AeonMacro>) {
        self.macros = macros;
    }

    pub fn apply_macro(&mut self, name: String, mut params: Vec<AeonValue>) -> AeonValue {
        if let Some(mac) = self.macros.get(name.as_str()) {
            let len = params.len();
            if mac.len() != len {
                panic!(
                    "Wrong number of args to macro {}: was {}, expected {}",
                    name,
                    len,
                    mac.len()
                );
            }

            let mut map = HashMap::<String, AeonValue>::new();

            for (idx, parameter) in params.drain(..).enumerate() {
                mac.apply(idx, parameter, &mut map);
            }
            AeonValue::Object(map)
        } else {
            panic!("Macro does not exist: {}", name);
        }
    }

    pub fn try_get_macro(&self, map: &HashMap<String, AeonValue>) -> Option<&AeonMacro> {
        if let Some((_, m)) = self
            .macros
            .iter()
            .filter(|(_, v)| v.len() == map.len())
            .find(|(_, v)| v.can_apply(map))
        {
            return Some(m);
        }
        None
    }

    pub fn copy_macros_to(&self, other: &mut AeonDocument) {
        other.macros.extend(self.macros.clone());
    }
}

impl FromIterator<(String, AeonValue)> for AeonDocument {
    fn from_iter<T: IntoIterator<Item = (String, AeonValue)>>(iter: T) -> Self {
        let mut doc = Self::new();
        iter.into_iter().for_each(|(k, v)| {
            doc.add_or_replace_property(k, v);
        });
        doc
    }
}

#[cfg(test)]
mod tests {
    use crate::convert::*;
    use crate::document::{AeonDocument, AeonMacro, AeonValue};
    use crate::map;

    #[test]
    pub fn get_property_integer() {
        let mut aeon = AeonDocument::new();
        aeon.add_property("num", AeonValue::Integer(79));
        assert_eq!(79, aeon.remove("num").int().unwrap());
    }

    #[test]
    pub fn get_property_string() {
        let mut aeon = AeonDocument::new();
        aeon.add_property("char", AeonValue::String("åöäºÃœ".into()));
        assert_eq!("åöäºÃœ".to_string(), aeon.remove("char").str().unwrap());
    }

    #[test]
    pub fn copy_macros() {
        let mut aeon = AeonDocument::new();
        aeon.add_macro(AeonMacro::new("bread".into(), vec!["'s good".into()]));
        let mut second_aeon = AeonDocument::new();
        second_aeon.add_macro(AeonMacro::new("tubes".into(), vec!["of copper".into()]));
        aeon.copy_macros_to(&mut second_aeon);
        assert_eq!(1, aeon.macros.len());
        assert_eq!(2, second_aeon.macros.len());
    }

    #[test]
    pub fn get_nested_property() {
        let mut aeon = AeonDocument::new();
        aeon.add_property(
            "char",
            AeonValue::Object(map![
               "name".into() => AeonValue::String("erki".into()),
               "world".into() => AeonValue::Integer(1),
               "double".into() => AeonValue::Double(139.3567),
               "or_nothing".into() => AeonValue::Object(map![
                     "name".into() => AeonValue::String("unused".into()),
                     "world".into() => AeonValue::Integer(-53),
                     "double".into() => AeonValue::Double(-11.38),
                     "or_nothing".into() => AeonValue::Nil,
               ]),
            ]),
        );
        assert_eq!(
            -53,
            aeon.remove("char")
                .remove("or_nothing")
                .get("world")
                .int()
                .unwrap()
        );
    }

    #[test]
    pub fn get_nested_property_using_path_syntax() {
        let mut aeon = AeonDocument::new();
        aeon.add_property(
            "char",
            AeonValue::Object(map![
               "name".into() => AeonValue::String("erki".into()),
               "world".into() => AeonValue::Integer(1),
               "double".into() => AeonValue::Double(139.3567),
               "or_nothing".into() => AeonValue::Object(map![
                     "name".into() => AeonValue::String("unused".into()),
                     "world".into() => AeonValue::Integer(-53),
                     "double".into() => AeonValue::Double(-11.38),
                     "or_nothing".into() => AeonValue::Nil,
               ]),
            ]),
        );
        assert_eq!(-53, aeon.get_path("char/or_nothing/world").int().unwrap());
    }

    #[test]
    pub fn remove_nested_property_using_path_syntax() {
        let mut aeon = AeonDocument::new();
        aeon.add_property(
            "char",
            AeonValue::Object(map![
               "name".into() => AeonValue::String("erki".into()),
               "world".into() => AeonValue::Integer(1),
               "double".into() => AeonValue::Double(139.3567),
               "or_nothing".into() => AeonValue::Object(map![
                     "name".into() => AeonValue::String("unused".into()),
                     "world".into() => AeonValue::Integer(-53),
                     "double".into() => AeonValue::Double(-11.38),
                     "or_nothing".into() => AeonValue::Nil,
               ]),
            ]),
        );
        assert_eq!(
            -53,
            aeon.remove_path("char/or_nothing/world").int().unwrap()
        );
    }
}