statespace-tool-runtime 0.1.3

Core tool execution runtime for Statespace
Documentation
//! Command validation and placeholder expansion.

use crate::error::Error;
use crate::frontmatter::Frontmatter;
use crate::spec::{ToolSpec, is_valid_tool_call};
use std::collections::HashMap;

/// # Errors
///
/// Returns an error when the command is empty or not present in frontmatter.
pub fn validate_command(frontmatter: &Frontmatter, command: &[String]) -> Result<(), Error> {
    if command.is_empty() {
        return Err(Error::InvalidCommand("command cannot be empty".to_string()));
    }

    if !frontmatter.has_tool(command) {
        return Err(Error::CommandNotFound {
            command: command.join(" "),
        });
    }

    Ok(())
}

/// # Errors
///
/// Returns an error when the command is empty or does not match any spec.
pub fn validate_command_with_specs(specs: &[ToolSpec], command: &[String]) -> Result<(), Error> {
    if command.is_empty() {
        return Err(Error::InvalidCommand("command cannot be empty".to_string()));
    }

    if !is_valid_tool_call(command, specs) {
        return Err(Error::CommandNotFound {
            command: command.join(" "),
        });
    }

    Ok(())
}

#[must_use]
pub fn expand_placeholders<S: std::hash::BuildHasher>(
    command: &[String],
    args: &HashMap<String, String, S>,
) -> Vec<String> {
    command
        .iter()
        .map(|part| {
            let mut result = part.clone();

            for (key, value) in args {
                let placeholder = format!("{{{key}}}");
                result = result.replace(&placeholder, value);
            }

            result
        })
        .collect()
}

#[must_use]
pub fn expand_env_vars<S: std::hash::BuildHasher>(
    command: &[String],
    env: &HashMap<String, String, S>,
) -> Vec<String> {
    command
        .iter()
        .map(|part| {
            let mut result = part.clone();

            for (key, value) in env {
                let var = format!("${key}");
                result = result.replace(&var, value);
            }

            result
        })
        .collect()
}

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

    fn legacy_frontmatter(tools: Vec<Vec<String>>) -> Frontmatter {
        Frontmatter {
            specs: vec![],
            tools,
        }
    }

    #[test]
    fn test_validate_command_empty() {
        let fm = legacy_frontmatter(vec![]);
        let result = validate_command(&fm, &[]);
        assert!(matches!(result, Err(Error::InvalidCommand(_))));
    }

    #[test]
    fn test_validate_command_not_found() {
        let fm = legacy_frontmatter(vec![vec!["ls".to_string()]]);

        let result = validate_command(&fm, &["cat".to_string(), "file.md".to_string()]);
        assert!(matches!(result, Err(Error::CommandNotFound { .. })));
    }

    #[test]
    fn test_validate_command_success() {
        let fm = legacy_frontmatter(vec![
            vec!["ls".to_string(), "{path}".to_string()],
            vec!["cat".to_string(), "{path}".to_string()],
        ]);

        let result = validate_command(&fm, &["ls".to_string(), "docs/".to_string()]);
        assert!(result.is_ok());

        let result = validate_command(&fm, &["cat".to_string(), "index.md".to_string()]);
        assert!(result.is_ok());
    }

    #[test]
    fn test_expand_placeholders() {
        let command = vec![
            "curl".to_string(),
            "-X".to_string(),
            "GET".to_string(),
            "https://api.com/{endpoint}".to_string(),
        ];

        let mut args = HashMap::new();
        args.insert("endpoint".to_string(), "orders".to_string());

        let expanded = expand_placeholders(&command, &args);
        assert_eq!(
            expanded,
            vec!["curl", "-X", "GET", "https://api.com/orders"]
        );
    }

    #[test]
    fn test_expand_env_vars() {
        let command = vec![
            "curl".to_string(),
            "-H".to_string(),
            "Authorization: Bearer $API_KEY".to_string(),
        ];

        let mut env = HashMap::new();
        env.insert("API_KEY".to_string(), "secret123".to_string());

        let expanded = expand_env_vars(&command, &env);
        assert_eq!(
            expanded,
            vec!["curl", "-H", "Authorization: Bearer secret123"]
        );
    }
}