mofa 0.1.2

the config lib that merges options from anywhere
Documentation
use itertools::Itertools;
use regex::{Captures, Regex};
use std::collections::HashMap;
use std::error::Error;
use toml::Value;

pub trait Processor {
    /// transform value.
    /// return true if the processor do modify the value
    fn process(&self, value: &mut Value) -> Result<bool, Box<dyn std::error::Error>>;
}

pub struct EnvironmentVariableProcessor;

impl Processor for EnvironmentVariableProcessor {
    fn process(&self, value: &mut Value) -> Result<bool, Box<dyn Error>> {
        fn resolve_environment_placeholder(value: &mut Value) -> bool {
            let mut is_modify = false;

            let environment_pattern = Regex::new("\\$\\{(?<env>[A-Z]+(_[A-Z]+)*)\\}").unwrap();

            match value {
                Value::String(ref mut inner) => {
                    let ret = environment_pattern.replace_all(inner, |caps: &Captures| {
                        let env_variable: &str = &caps["env"];
                        std::env::var(env_variable).unwrap_or("".to_owned())
                    });

                    if ret.as_ref().ne(inner.as_str()) {
                        *inner = ret.to_string();
                        is_modify = true;
                    }
                }
                Value::Array(inner) => {
                    for element in inner {
                        is_modify = is_modify || resolve_environment_placeholder(element);
                    }
                }
                Value::Table(table) => {
                    for (_, value) in table.iter_mut() {
                        is_modify = is_modify || resolve_environment_placeholder(value);
                    }
                }
                _ => {}
            }
            is_modify
        }

        Ok(resolve_environment_placeholder(value))
    }
}

pub struct PathVariableProcessor;

impl Processor for PathVariableProcessor {
    fn process(&self, value: &mut Value) -> Result<bool, Box<dyn Error>> {
        let mut collectors = HashMap::new();
        collect_path_placeholder(value, value, &mut collectors);
        resolve_path_placeholder(value, &collectors);

        Ok(!collectors.is_empty())
    }
}

fn get_value_by_path_inner<'b, 'a: 'b>(
    value: &'a Value,
    paths: &'b [&'b str],
) -> Option<&'a Value> {
    match paths.len() {
        0 => unreachable!(),
        1 => value.as_table().and_then(|table| table.get(paths[0])),
        _ => {
            let option = value.as_table().and_then(|table| table.get(paths[0]));
            option.and_then(|tier| get_value_by_path_inner(tier, &paths[1..]))
        }
    }
}

fn get_value_by_path<'b, 'a: 'b>(value: &'a Value, path: &'b str) -> Option<&'a Value> {
    let paths = path.split('.').collect_vec();
    get_value_by_path_inner(value, &paths[..])
}

fn resolve_path_placeholder(value: &mut Value, collectors: &HashMap<String, String>) {
    let path_pattern =
        Regex::new("\\$\\{(?<path>[a-z]+(_[a-z]+)*(\\.[a-z]+(_[a-z]+)*)*)\\}").unwrap();

    match value {
        Value::String(ref mut inner) => {
            let ret = path_pattern.replace_all(inner, |caps: &Captures| {
                let path: &str = &caps["path"];
                collectors.get(path).cloned().unwrap_or("".to_string())
            });
            *inner = ret.to_string();
        }
        Value::Array(inner) => {
            for element in inner {
                resolve_path_placeholder(element, collectors);
            }
        }
        Value::Table(table) => {
            for (_, value) in table.iter_mut() {
                resolve_path_placeholder(value, collectors);
            }
        }
        _ => {}
    }
}

fn collect_path_placeholder(root: &Value, value: &Value, collectors: &mut HashMap<String, String>) {
    let path_pattern =
        Regex::new("\\$\\{(?<path>[a-z]+(_[a-z]+)*(\\.[a-z]+(_[a-z]+)*)*)\\}").unwrap();

    match value {
        Value::String(inner) => {
            for caps in path_pattern.captures_iter(inner) {
                let path: &str = &caps["path"];
                collectors.insert(
                    path.to_string(),
                    get_value_by_path(root, path)
                        .and_then(|it| it.as_str())
                        .map(|it| it.to_string())
                        .unwrap_or("".to_string()),
                );
            }
        }
        Value::Array(inner) => {
            for element in inner {
                collect_path_placeholder(root, element, collectors);
            }
        }
        Value::Table(table) => {
            for (_, value) in table.iter() {
                collect_path_placeholder(root, value, collectors);
            }
        }
        _ => {}
    }
}