statespace_tool_runtime/
validation.rs1use crate::error::Error;
4use crate::frontmatter::Frontmatter;
5use crate::spec::{ToolSpec, is_valid_tool_call};
6use std::collections::HashMap;
7
8pub 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
25pub 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}