Skip to main content

statespace_tool_runtime/
validation.rs

1//! Command validation and placeholder expansion.
2
3use crate::error::Error;
4use crate::frontmatter::Frontmatter;
5use crate::spec::{ToolSpec, is_valid_tool_call};
6use std::collections::HashMap;
7
8/// # Errors
9///
10/// Returns an error when the command is empty or not present in frontmatter.
11pub fn validate_command(frontmatter: &Frontmatter, command: &[String]) -> Result<(), Error> {
12    if command.is_empty() {
13        return Err(Error::InvalidCommand("command cannot be empty".to_string()));
14    }
15
16    if !frontmatter.has_tool(command) {
17        return Err(Error::CommandNotFound {
18            command: command.join(" "),
19        });
20    }
21
22    Ok(())
23}
24
25/// # Errors
26///
27/// Returns an error when the command is empty or does not match any spec.
28pub fn validate_command_with_specs(specs: &[ToolSpec], command: &[String]) -> Result<(), Error> {
29    if command.is_empty() {
30        return Err(Error::InvalidCommand("command cannot be empty".to_string()));
31    }
32
33    if !is_valid_tool_call(command, specs) {
34        return Err(Error::CommandNotFound {
35            command: command.join(" "),
36        });
37    }
38
39    Ok(())
40}
41
42#[must_use]
43pub fn expand_placeholders<S: std::hash::BuildHasher>(
44    command: &[String],
45    args: &HashMap<String, String, S>,
46) -> Vec<String> {
47    command
48        .iter()
49        .map(|part| {
50            let mut result = part.clone();
51
52            for (key, value) in args {
53                let placeholder = format!("{{{key}}}");
54                result = result.replace(&placeholder, value);
55            }
56
57            result
58        })
59        .collect()
60}
61
62#[must_use]
63pub fn expand_env_vars<S: std::hash::BuildHasher>(
64    command: &[String],
65    env: &HashMap<String, String, S>,
66) -> Vec<String> {
67    command
68        .iter()
69        .map(|part| {
70            let mut result = part.clone();
71
72            for (key, value) in env {
73                let var = format!("${key}");
74                result = result.replace(&var, value);
75            }
76
77            result
78        })
79        .collect()
80}
81
82#[cfg(test)]
83mod tests {
84    use super::*;
85
86    fn legacy_frontmatter(tools: Vec<Vec<String>>) -> Frontmatter {
87        Frontmatter {
88            specs: vec![],
89            tools,
90        }
91    }
92
93    #[test]
94    fn test_validate_command_empty() {
95        let fm = legacy_frontmatter(vec![]);
96        let result = validate_command(&fm, &[]);
97        assert!(matches!(result, Err(Error::InvalidCommand(_))));
98    }
99
100    #[test]
101    fn test_validate_command_not_found() {
102        let fm = legacy_frontmatter(vec![vec!["ls".to_string()]]);
103
104        let result = validate_command(&fm, &["cat".to_string(), "file.md".to_string()]);
105        assert!(matches!(result, Err(Error::CommandNotFound { .. })));
106    }
107
108    #[test]
109    fn test_validate_command_success() {
110        let fm = legacy_frontmatter(vec![
111            vec!["ls".to_string(), "{path}".to_string()],
112            vec!["cat".to_string(), "{path}".to_string()],
113        ]);
114
115        let result = validate_command(&fm, &["ls".to_string(), "docs/".to_string()]);
116        assert!(result.is_ok());
117
118        let result = validate_command(&fm, &["cat".to_string(), "index.md".to_string()]);
119        assert!(result.is_ok());
120    }
121
122    #[test]
123    fn test_expand_placeholders() {
124        let command = vec![
125            "curl".to_string(),
126            "-X".to_string(),
127            "GET".to_string(),
128            "https://api.com/{endpoint}".to_string(),
129        ];
130
131        let mut args = HashMap::new();
132        args.insert("endpoint".to_string(), "orders".to_string());
133
134        let expanded = expand_placeholders(&command, &args);
135        assert_eq!(
136            expanded,
137            vec!["curl", "-X", "GET", "https://api.com/orders"]
138        );
139    }
140
141    #[test]
142    fn test_expand_env_vars() {
143        let command = vec![
144            "curl".to_string(),
145            "-H".to_string(),
146            "Authorization: Bearer $API_KEY".to_string(),
147        ];
148
149        let mut env = HashMap::new();
150        env.insert("API_KEY".to_string(), "secret123".to_string());
151
152        let expanded = expand_env_vars(&command, &env);
153        assert_eq!(
154            expanded,
155            vec!["curl", "-H", "Authorization: Bearer secret123"]
156        );
157    }
158}