hen 0.8.1

Run API collections from the command line.
use std::{
    collections::HashMap,
    sync::{OnceLock, RwLock},
};

use pest::Parser;
use pest_derive::Parser;

#[derive(Parser)]
#[grammar = "src/parser/context.pest"]
struct VarPlaceholderParser;

pub fn inject_from_prompt(input: &str) -> String {
    let mut output = String::new();

    let mut pairs = VarPlaceholderParser::parse(Rule::text, input).unwrap();

    let pair = pairs.next().unwrap();

    for inner_pair in pair.into_inner() {
        match inner_pair.as_rule() {
            Rule::word => {
                output.push_str(inner_pair.as_str());
            }
            Rule::input => {
                let (key, default) = parse_input_pair(inner_pair.as_str());

                if let Some(value) = prompt_inputs().read().unwrap().get(&key).cloned() {
                    output.push_str(value.as_str());
                    continue;
                }

                let prompt = match &default {
                    Some(def) => format!("Provide a value for \"{}\" (default: {})", key, def),
                    None => format!("Provide a value for \"{}\"", key),
                };

                let mut dialog = dialoguer::Input::new().with_prompt(prompt);
                if let Some(def) = &default {
                    dialog = dialog.default(def.to_string());
                }

                let input: String = dialog.interact().unwrap();

                // store the resolved value so repeated prompts reuse it
                prompt_inputs()
                    .write()
                    .unwrap()
                    .insert(key.clone(), input.clone());

                output.push_str(input.as_str());
            }
            Rule::var => {
                // retain the variable placeholder
                // unresolved variables may be encountered in the context of a preamble.
                output.push_str(format!("{{{{{}}}}}", inner_pair.as_str()).as_str());
            }
            _ => {
                unreachable!("unexpected rule: {:?}", inner_pair.as_rule());
            }
        }
    }
    output
}

pub fn inject_from_variable(input: &str, context: &HashMap<String, String>) -> String {
    let mut output = String::new();

    let mut pairs = VarPlaceholderParser::parse(Rule::text, input).unwrap();

    let pair = pairs.next().unwrap();

    for inner_pair in pair.into_inner() {
        match inner_pair.as_rule() {
            Rule::word => {
                output.push_str(inner_pair.as_str());
            }
            Rule::var => {
                let key = inner_pair.as_str().to_string();
                let value = context
                    .get(&key)
                    .expect(&format!("No value found for variable: {}", key));
                output.push_str(value);
            }
            Rule::input => {
                // retain the input placeholder
                output.push_str(format!("[[{}]]", inner_pair.as_str()).as_str());
            }
            _ => {
                unreachable!("unexpected rule: {:?}", inner_pair.as_rule());
            }
        }
    }
    output
}

pub fn set_prompt_inputs(inputs: HashMap<String, String>) {
    let mut map = prompt_inputs().write().unwrap();
    map.clear();
    map.extend(inputs);
}

fn prompt_inputs() -> &'static RwLock<HashMap<String, String>> {
    static PROMPT_INPUTS: OnceLock<RwLock<HashMap<String, String>>> = OnceLock::new();
    PROMPT_INPUTS.get_or_init(|| RwLock::new(HashMap::new()))
}

fn parse_input_pair(raw: &str) -> (String, Option<String>) {
    let mut parts = raw.splitn(2, '=');
    let key = parts.next().unwrap().trim().to_string();
    let default = parts.next().map(|value| value.trim().to_string());
    (key, default)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn should_replace_variables() {
        let input = "this is a test with a {{variable}}";
        let mut context = HashMap::new();
        context.insert("variable".to_string(), "value".to_string());

        let output = inject_from_variable(input, &context);

        assert_eq!(output, "this is a test with a value");
    }

    #[test]
    fn should_use_provided_prompt_inputs() {
        let mut inputs = HashMap::new();
        inputs.insert("foo".to_string(), "bar".to_string());
        set_prompt_inputs(inputs);

        let output = inject_from_prompt("value [[ foo ]]");

        assert_eq!(output, "value bar");

        set_prompt_inputs(HashMap::new());
    }
}