nadi_core 0.8.1

Core library for Nadi systems, for use by plugins
Documentation
use crate::attrs::{AttrMap, Attribute, HasAttributes};
use crate::parser::{
    components::*,
    errors::{ParseError, ParseErrorType},
    tokenizer::{RawToken, Token},
};
use nom::{branch::alt, combinator::map, sequence::delimited, Finish};

pub fn attr_group<'a, 'b>(inp: &'a [Token<'b>]) -> MatchRes<'a, 'b, Vec<String>> {
    delimited(
        bracket_start,
        maybe_space(dot_variable),
        maybe_space(bracket_end),
    )(inp)
}

pub enum Line {
    Group(Vec<String>),
    KeyVal((Vec<String>, Attribute)),
}

pub fn attr_file_line<'a, 'b>(inp: &'a [Token<'b>]) -> MatchRes<'a, 'b, Line> {
    alt((map(attr_group, Line::Group), map(key_val_dot, Line::KeyVal)))(inp)
}

pub fn attr_file<'a, 'b>(inp: &'a [Token<'b>]) -> MatchRes<'a, 'b, Vec<Line>> {
    trailing_newlines(newline_separated(attr_file_line))(inp)
}

pub fn parse(tokens: Vec<RawToken>) -> Result<AttrMap, ParseError> {
    let tokens = Token::validate(tokens)?;
    let lines = match attr_file(&tokens).finish() {
        Ok((rest, lines)) => {
            if rest.is_empty() {
                lines
            } else {
                return match maybe_newline(attr_file_line)(rest).finish() {
                    Ok((rest, _)) => {
                        Err(ParseError::new(&tokens, rest, ParseErrorType::SyntaxError))
                    }
                    Err(err) => Err(ParseError::new(&tokens, err.internal.input, err.ty)),
                };
            }
        }
        Err(e) => return Err(ParseError::new(&tokens, e.internal.input, e.ty)),
    };

    let mut attrmap = AttrMap::new();
    let mut curr_var = &mut attrmap;

    for line in lines {
        match line {
            Line::Group(grp) => {
                curr_var = move_in(&grp, curr_var)?;
            }
            Line::KeyVal((keys, val)) => {
                let old = match keys.as_slice() {
                    [] => return Err(ParseError::custom("Empty attribute group".into())),
                    [name] => curr_var.set_attr(name, val.clone()),
                    [pre @ .., name] => {
                        let map = move_in(pre, curr_var)?;
                        map.set_attr(name, val.clone())
                    }
                };
                if let Some(oval) = old {
                    return Err(ParseError::custom(format!(
                        "Key {} already set to value {} (new: {})",
                        keys.join("."),
                        val,
                        oval
                    )));
                }
            }
        };
    }
    Ok(attrmap)
}

fn move_in<'a>(keys: &[String], table: &'a mut AttrMap) -> Result<&'a mut AttrMap, ParseError> {
    let mut map = table;
    for k in keys {
        map = match map
            .entry(k.to_string().into())
            .or_insert(Attribute::Table(AttrMap::new()))
        {
            Attribute::Table(ref mut mp) => mp,
            val => {
                return Err(ParseError::custom(format!(
                    "Key {k} in {} is not a table (value: {})",
                    keys.join("."),
                    val,
                )));
            }
        };
    }
    Ok(map)
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::attr_map;
    use crate::parser::tokenizer::get_tokens;
    use rstest::rstest;

    #[rstest]
    #[case("val = 12", attr_map!(val => 12))]
    #[case("val = true\nval2 = \"sth\"", attr_map!(val => true, val2 => "sth"))]
    #[case("val = true\n[grp]\nval2 = \"sth\"", attr_map!(val => true, grp => attr_map!(val2 => "sth")))]
    #[case("val.sth = 12", attr_map!(val => attr_map!(sth => 12)))]
    #[case("[zzz]\nval.sth = 12", attr_map!(zzz => attr_map!(val => attr_map!(sth => 12))))]
    fn attr_test(#[case] txt: &str, #[case] attrs: AttrMap) {
        let tokens = get_tokens(txt);
        let am = parse(tokens).unwrap();
        assert_eq!(am, attrs);
    }
}