cornfig 0.0.0-alpha

A simple and pain-free configuration language.
Documentation
use std::collections::HashMap;
use std::fmt::Formatter;
use std::process::exit;

use pest::error::{Error};
use pest::iterators::Pair;
use pest::Parser;

use crate::{Config, Value, Variables};
use crate::error::{ERR_VARIABLE, print_err};

#[derive(pest_derive::Parser)]
#[grammar = "grammar.pest"]
pub struct ConfigParser;

impl std::fmt::Display for Rule {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        write!(f, "{:?}", self)
    }
}

fn attempt_get_variable<'a>(key: &'a str, variables: &Variables<'a>) -> Value<'a> {
    let value = variables.get(key);

    if let Some(value) = value {
        value.clone()
    } else {
        print_err(format!("Variable `{}` was used but not declared", key), None);
        exit(ERR_VARIABLE);
    }
}

fn parse_value<'a>(pair: Pair<'a, Rule>, variables: &Variables<'a>) -> Value<'a> {
    match pair.as_rule() {
        Rule::object => Value::Object(parse_object(pair, variables)),
        Rule::array => Value::Array(parse_array(pair, variables)),
        Rule::string => Value::String(parse_string(pair, variables)),
        Rule::integer => Value::Integer(pair.as_str().parse().unwrap()),
        Rule::float => Value::Float(pair.as_str().parse().unwrap()),
        Rule::boolean => Value::Boolean(pair.as_str().parse().unwrap()),
        Rule::null => Value::Null,
        Rule::variable => {
            let key = pair.as_str();
            attempt_get_variable(key, variables)
        }
        _ => unreachable!(),
    }
}

fn add_at_path<'a>(
    mut obj: HashMap<&'a str, Value<'a>>,
    path: &[&'a str],
    value: Value<'a>,
) -> HashMap<&'a str, Value<'a>> {
    let (part, path_rest) = path.split_first().unwrap();

    if path_rest.is_empty() {
        obj.insert(part, value);
        return obj;
    }

    if !obj.contains_key(part) {
        obj.insert(part, Value::Object(HashMap::new()));
    }

    let child_obj = obj.get(part).unwrap().clone(); // TODO: Not clone

    match child_obj {
        Value::Object(map) => {
            obj.insert(part, Value::Object(add_at_path(map, path_rest, value)));
        }
        _ => panic!(
            "{} attempts to use object dot notation for a non-object type",
            path.join(".")
        ),
    };

    obj
}

fn parse_string<'a>(pair: Pair<'a, Rule>, variables: &Variables<'a>) -> String {
    pair.into_inner()
        .map(|char| {
            let value = char.as_str();
            return if value.len() == 1 {
                value.to_string()
            } else {
                let value = parse_value(char.into_inner().next().unwrap(), variables);
                match value {
                    Value::String(val) => val,
                    Value::Integer(val) => val.to_string(),
                    Value::Float(val) => val.to_string(),
                    Value::Boolean(val) => val.to_string(),
                    _ => unimplemented!(),
                }
            };
        })
        .collect::<String>()
}

fn parse_array<'a>(block: Pair<'a, Rule>, variables: &Variables<'a>) -> Vec<Value<'a>> {
    assert_eq!(block.as_rule(), Rule::array);
    block
        .into_inner()
        .map(|pair| parse_value(pair, variables))
        .collect::<Vec<_>>()
}

pub fn parse_object<'a>(
    block: Pair<'a, Rule>,
    variables: &Variables<'a>,
) -> HashMap<&'a str, Value<'a>> {
    assert_eq!(block.as_rule(), Rule::object);

    let mut obj = HashMap::new();

    for rule in block.into_inner() {
        match rule.as_rule() {
            Rule::pair => {
                let mut path_rules = rule.into_inner();
                let path = path_rules.next().unwrap().as_str();
                let value = parse_value(path_rules.next().unwrap(), variables);

                obj = add_at_path(obj, path.split('.').collect::<Vec<_>>().as_slice(), value);
            }
            _ => unreachable!(),
        }
    }

    obj
}

pub fn parse_assign_block(block: Pair<Rule>) -> Variables {
    assert_eq!(block.as_rule(), Rule::assign_block);

    let mut variables = HashMap::new();

    for pair in block.into_inner() {
        let mut assign_rules = pair.into_inner();
        let name = assign_rules.next().unwrap().as_str();
        let value = parse_value(assign_rules.next().unwrap(), &variables);

        variables.insert(name, value);
    }

    variables
}

pub fn parse(file: &str) -> Result<Config, Error<Rule>> {
    let rules = ConfigParser::parse(Rule::config, file);

    match rules {
        Ok(mut rules) => {
            let assign_block = rules.next().expect("Error parsing assign block");
            let value_block = rules.next().expect("Error parsing value block");

            let variables = parse_assign_block(assign_block);
            let value_block = parse_object(value_block, &variables);

            Ok(Config {
                variables,
                value: value_block,
            })
        }
        Err(error) => {
            Err(error)
        }
    }
}