ktmpl 0.9.1

Parameterized templates for Kubernetes manifests.
Documentation
use lazy_static::lazy_static;
use regex::{Captures, Regex};
use yaml_rust::yaml::{Array, Hash};
use yaml_rust::Yaml;

use crate::parameter::ParamMap;

pub fn process_yaml(yaml: &mut Yaml, parameters: &ParamMap) -> Option<Yaml> {
    match yaml {
        &mut Yaml::Array(ref mut array) => process_array(array, parameters),
        &mut Yaml::Hash(ref mut hash) => process_hash(hash, parameters),
        &mut Yaml::String(ref mut string) => process_string(string, parameters),
        _ => None,
    }
}

fn process_array(array: &mut Array, parameters: &ParamMap) -> Option<Yaml> {
    for value in array {
        if let Some(new_value) = process_yaml(value, parameters) {
            *value = new_value;
        }
    }

    None
}

fn process_hash(hash: &mut Hash, parameters: &ParamMap) -> Option<Yaml> {
    let mut keys_to_replace = Vec::new();

    for (key, value) in hash.iter_mut() {
        let mut key_string = match key {
            Yaml::String(key_string) => key_string.clone(),
            _ => panic!("all hash keys must be strings"),
        };

        if let Some(new_key) = process_string(&mut key_string, parameters) {
            if &new_key != key {
                keys_to_replace.push((key.clone(), new_key));
            }
        }

        if let Some(new_value) = process_yaml(value, parameters) {
            *value = new_value;
        }
    }

    for (key, new_key) in keys_to_replace {
        let value = hash.remove(&key).expect("removing old value while replacing keys");
        hash.insert(new_key, value);
    }

    None
}

fn process_string(string: &mut String, parameters: &ParamMap) -> Option<Yaml> {
    lazy_static! {
        static ref LITERAL_INTERPOLATION: Regex =
            Regex::new(r"\$\({2}([^\)]*)\){2}").expect("Failed to compile regex.");
    }

    lazy_static! {
        static ref STRING_INTERPOLATION: Regex =
            Regex::new(r"\$\(([^\)]*)\)").expect("Failed to compile regex.");
    }

    let interpolate = |captures: &Captures| -> String {
        let key = captures
            .get(1)
            .expect("Failed to extract regex capture group.");

        match parameters.get(key.as_str()) {
            Some(parameter) => parameter.value.clone().unwrap_or("~".to_owned()),
            None => captures
                .get(0)
                .expect("Failed to extract regex match.")
                .as_str()
                .to_owned(),
        }
    };

    let replacement = LITERAL_INTERPOLATION.replace_all(string, &interpolate);

    let contains_literal_replacement = &replacement != string;

    let final_replacement = STRING_INTERPOLATION.replace_all(&replacement, &interpolate);

    let contains_string_replacement = &final_replacement != &replacement;

    if !contains_literal_replacement && !contains_string_replacement {
        None
    } else if contains_literal_replacement && !contains_string_replacement {
        Some(Yaml::from_str(&final_replacement))
    } else {
        Some(Yaml::String(final_replacement.to_string()))
    }
}