confiner 0.2.1

A config language for things that look like trees.
Documentation
use super::*;

use std::path::PathBuf;
use std::collections::HashMap;

#[derive(Debug, PartialEq, Clone)]
pub enum BlockItem
{
    Property(String, Expr),
    Child(Option<String>, Child)
}

#[derive(Debug, PartialEq, Clone)]
pub enum Child
{
    Defined(Block),
    Ref(String)
}

pub type Block = Arc<BlockInfo>;

#[derive(Debug, PartialEq, Clone)]
pub struct BlockInfo
{
    pub kind:               String,
    pub name:               Option<String>,
    pub properties:         HashMap<String, Expr>,
    pub children:           Vec<(Option<String>, Child)>
}

pub enum DocumentElem
{
    Root(Block),
    Ref(Block),
    Include(PathBuf)
}

pub struct Document
{
    pub elems: Vec<DocumentElem>
}

#[derive(Debug, PartialEq, Clone)]
pub struct Enum
{
    pub name:  String,
    pub value: Option<Box<Expr>>
}

#[derive(Debug, PartialEq, Clone)]
pub enum Expr
{
    Null,
    Bool(bool),
    Int(i64),
    Float(f64),
    Str(String),
    List(Vec<Expr>),
    Map(HashMap<String, Expr>),
    Enum(Enum),
    Path(PathBuf)
}

peg::parser!
{
    grammar confiner_grammar(root_path: &std::path::Path) for str
    {
        rule alpha() -> char = ch:quiet!{['a'..='z' | 'A'..='Z' | '_']} { ch }
        rule digit() -> char = ch:quiet!{['0'..='9']} { ch }
        rule hex() = quiet!{['a'..='f' | 'A'..='F' | '0'..='9']}
        rule sep() = quiet!{_ "," _}
        rule _() = quiet!{[' ' | '\t' | '\n']*}

        rule identifier() -> String
            = id:$(alpha() ( alpha() / digit() )*) { id.to_owned() }
            / expected!("Identifier")

        rule bool_expr() -> bool = "true" { true } / "false" { false }

        rule int_expr() -> i64
            = "0x" n:$(hex()+)
                {? i64::from_str_radix(n, 16).or(Err("Hex Integer")) }
            / n:$("-"? digit()+)
                {? i64::from_str_radix(n, 10).or(Err("Decimal Integer")) }

        rule float_expr() -> f64
            = n:$(
                    (['-'|'+'])?
                    digit()+
                    (
                        "." digit()+
                        / ("." digit()+)? "e" (['+'|'-'])? digit()+
                    )
                )
                {? n.parse().or(Err("Float")) }

        rule literal_str_char(hashes: usize) -> char
            = !("\"" "#"*<{hashes}>) ch:[_] { ch }

        rule literal_str_expr(hashes: usize) -> String
            = "\"" chars:literal_str_char(hashes)* "\"" "#"*<{hashes}>
                { chars.into_iter().collect() }
            / "#" s:literal_str_expr(hashes + 1) { s }

        rule str_char() -> char
            = ch:[^ '\n' | '"' | '\\'] { ch }
            / "\\n" { '\n' }
            / "\\r" { '\r' }
            / "\\" ch:[_] { ch }

        rule str_expr() -> String
            = "\"" chars:str_char()* "\"" { chars.into_iter().collect() }
            / "#" s:literal_str_expr(1) { s }

        rule list_expr() -> Vec<Expr>
            = "[" _ vec:(expr() ** sep()) _ "]" { vec }

        rule key_value() -> (String, Expr)
            = key:identifier() _ "=" _ value:expr() { (key, value) }

        rule map_expr() -> HashMap<String, Expr>
            = "{" _ vec:(key_value() ** sep()) _ "}"
                { vec.into_iter().collect() }

        rule enum_expr() -> Enum
            = "!" name:identifier() _ value:expr()
                { Enum { name, value: Some(Box::new(value)) } }
            / "!" name:identifier()
                { Enum { name, value: None } }

        rule path_char() -> char
            = ch:alpha() { ch }
            / ch:digit() { ch }
            / ch:['-' | '/' | '.'] { ch }
            / "\\n" { '\n' }
            / "\\r" { '\r' }
            / "\\" ch:[_] { ch }

        rule path_expr() -> PathBuf
            = "./" chars:path_char()+
                { root_path.join(&chars.into_iter().collect::<String>()) }
            / "." { root_path.to_owned() }

        pub rule expr() -> Expr
            = "null"           { Expr::Null }
            / val:bool_expr()  { Expr::Bool(val) }
            / val:float_expr() { Expr::Float(val) }
            / val:int_expr()   { Expr::Int(val) }
            / val:str_expr()   { Expr::Str(val) }
            / val:list_expr()  { Expr::List(val) }
            / val:map_expr()   { Expr::Map(val) }
            / val:enum_expr()  { Expr::Enum(val) }
            / val:path_expr()  { Expr::Path(val) }
            / expected!("Expression")

        rule role() -> String = "@" role:identifier() { role }

        rule block_item() -> BlockItem
            = kv:key_value()
                { BlockItem::Property(kv.0, kv.1) }
            / role:role()? _ child:block()
                { BlockItem::Child(role, Child::Defined(child)) }
            / role:role()? _ "&" name:identifier()
                { BlockItem::Child(role, Child::Ref(name)) }

        pub rule block() -> Block
            = name:identifier()? _ ":" _ kind:identifier() _
                "{" _ items:(block_item() ** sep()) _ "}"
                {
                    let mut properties = HashMap::<String, Expr>::new();
                    let mut children   = Vec::<(Option<String>, Child)>::new();

                    for item in items
                    {
                        match item
                        {
                            BlockItem::Property(k, v) =>
                                { properties.insert(k, v); },
                            BlockItem::Child(role, child) =>
                                { children.push((role, child)); }
                        }
                    }

                    Arc::new(BlockInfo { kind, name, properties, children })
                }

        pub rule document_elem() -> DocumentElem
            = "&" _ block:block() { DocumentElem::Ref(block) }
            / block:block() { DocumentElem::Root(block) }
            / "@include" _ path:path_expr() { DocumentElem::Include(path) }

        pub rule document() -> Document
            = _ elems:(document_elem() ** _) _ { Document { elems } }
    }
}

pub use confiner_grammar::document;
pub use confiner_grammar::block;
pub use confiner_grammar::expr;

#[test]
fn test_block_info_empty_body()
{
    assert_eq!(
        *block(": kind {}", "/".as_ref()).unwrap(),
        BlockInfo {
            kind: "kind".to_owned(),
            name: None,
            properties: Default::default(),
            children: Default::default()
        }
    );
    assert_eq!(
        *block("name: kind {}", "/".as_ref()).unwrap(),
        BlockInfo {
            kind: "kind".to_owned(),
            name: Some("name".to_owned()),
            children: Default::default(),
            properties: Default::default()
        }
    );
}

#[test]
fn test_child_types()
{
    assert_eq!(
        block(": kind { &ref, : child {} }", "/".as_ref())
            .unwrap().children,
        vec![
            (None, Child::Ref("ref".to_owned())),
            (None, Child::Defined(
                grammar::block(": child {}", "/".as_ref()).unwrap()))
        ]
    )
}

#[test]
fn test_child_roles()
{
    assert_eq!(
        block(": kind { @rol &ref, @rol : child {} }", "/".as_ref())
            .unwrap().children,
        vec![
            (Some("rol".to_owned()), Child::Ref("ref".to_owned())),
            (Some("rol".to_owned()), Child::Defined(
                grammar::block(": child {}", "/".as_ref()).unwrap()))
        ]
    )
}
#[test]
fn test_expr_true()
{
    assert_eq!(
        expr("true", "/".as_ref()).unwrap(),
        Expr::Bool(true)
    );
}

#[test]
fn test_expr_false()
{
    assert_eq!(
        expr("false", "/".as_ref()).unwrap(),
        Expr::Bool(false)
    );
}

#[test]
fn test_expr_int()
{
    assert_eq!(expr("100", "/".as_ref()).unwrap(), Expr::Int(100));
    assert_eq!(expr("-100", "/".as_ref()).unwrap(), Expr::Int(-100));
    assert_eq!(expr("0x100", "/".as_ref()).unwrap(), Expr::Int(0x100));
}

#[test]
fn test_expr_float()
{
    assert_eq!(expr("0.1", "/".as_ref()).unwrap(), Expr::Float(0.1));
    assert_eq!(expr("-1e6", "/".as_ref()).unwrap(), Expr::Float(-1000000.0));
}

#[test]
fn test_expr_str()
{
    assert_eq!(
        expr(r#""\"Hello\nWorld\"""#, "/".as_ref()).unwrap(),
        Expr::Str("\"Hello\nWorld\"".to_owned())
    );
    assert_eq!(
        expr(r#""""#, "/".as_ref()).unwrap(),
        Expr::Str("".to_owned())
    );
}

#[test]
fn test_expr_list()
{
    assert_eq!(
        expr("[1, 2, 3]", "/".as_ref()).unwrap(),
        Expr::List(vec![Expr::Int(1), Expr::Int(2), Expr::Int(3)])
    );
}

#[test]
fn test_expr_map()
{
    assert_eq!(
        expr(r#"{ key = "Hello", yek = "olleH" }"#, "/".as_ref()).unwrap(),
        Expr::Map(vec![
            ("key".to_owned(), Expr::Str("Hello".to_owned())),
            ("yek".to_owned(), Expr::Str("olleH".to_owned()))
        ].into_iter().collect())
    );
}

#[test]
fn test_expr_enum_with_data()
{
    assert_eq!(
        expr("!name", "/".as_ref()).unwrap(),
        Expr::Enum(Enum{ name: "name".to_owned(), value: None })
    );
}

#[test]
fn test_expr_enum_no_data()
{
    assert_eq!(
        expr("!name 2", "/".as_ref()).unwrap(),
        Expr::Enum(Enum {
            name: "name".to_owned(),
            value: Some(Box::new(Expr::Int(2)))
        })
    );
}

#[test]
fn test_expr_path()
{
    assert_eq!(
        expr("./test.txt", "/".as_ref()).unwrap(),
        Expr::Path(From::from("/test.txt"))
    );
    assert_eq!(
        expr("./test.txt".as_ref(), "/directory".as_ref()).unwrap(),
        Expr::Path(From::from("/directory/test.txt"))
    );
    assert_eq!(
        expr(".".as_ref(), "/directory".as_ref()).unwrap(),
        Expr::Path(From::from("/directory"))
    );
}

#[test]
fn test_hash_str()
{
    assert_eq!(
        expr("#\"\"\"#", "/".as_ref()).unwrap(),
        Expr::Str("\"".to_owned())
    );
    assert_eq!(
        expr("##\"#\"\"##", "/".as_ref()).unwrap(),
        Expr::Str("#\"".to_owned())
    );
    assert_eq!(
        expr("#\"\\\"#", "/".as_ref()).unwrap(),
        Expr::Str("\\".to_owned())
    );
}