1use std::collections::HashSet;
7
8use tracing::{debug, warn};
9
10use crate::types::errors::StrandsError;
11use crate::types::tools::{ToolSpec, ToolUse};
12
13pub const MAX_TOOL_NAME_LENGTH: usize = 64;
15
16pub const MIN_TOOL_NAME_LENGTH: usize = 1;
18
19pub fn validate_tool_spec(spec: &ToolSpec) -> Result<(), StrandsError> {
34
35 if spec.name.is_empty() {
36 return Err(StrandsError::InvalidToolName {
37 name: spec.name.clone(),
38 reason: "Tool name cannot be empty".to_string(),
39 });
40 }
41
42 if spec.name.len() > MAX_TOOL_NAME_LENGTH {
43 return Err(StrandsError::InvalidToolName {
44 name: spec.name.clone(),
45 reason: format!(
46 "Tool name exceeds maximum length of {} characters",
47 MAX_TOOL_NAME_LENGTH
48 ),
49 });
50 }
51
52
53 if !spec.name.chars().all(|c| c.is_alphanumeric() || c == '_' || c == '-') {
54 return Err(StrandsError::InvalidToolName {
55 name: spec.name.clone(),
56 reason: "Tool name can only contain alphanumeric characters, underscores, and hyphens"
57 .to_string(),
58 });
59 }
60
61
62 if spec.description.is_empty() {
63 warn!(
64 tool_name = %spec.name,
65 "Tool has empty description, which may reduce LLM effectiveness"
66 );
67 }
68
69 Ok(())
70}
71
72pub fn validate_tool_specs(specs: &[ToolSpec]) -> Result<(), StrandsError> {
84 let mut seen_names = HashSet::new();
85
86 for spec in specs {
87
88 validate_tool_spec(spec)?;
89
90
91 if !seen_names.insert(&spec.name) {
92 return Err(StrandsError::DuplicateToolName {
93 name: spec.name.clone(),
94 });
95 }
96 }
97
98 debug!(tool_count = specs.len(), "Validated tool specifications");
99 Ok(())
100}
101
102pub fn validate_and_prepare_tools(
119 specs: Vec<ToolSpec>,
120 strict: bool,
121) -> Result<Vec<ToolSpec>, StrandsError> {
122 let mut validated = Vec::with_capacity(specs.len());
123 let mut seen_names = HashSet::new();
124
125 for spec in specs {
126 match validate_tool_spec(&spec) {
127 Ok(()) => {
128 if seen_names.insert(spec.name.clone()) {
129 validated.push(spec);
130 } else if strict {
131 return Err(StrandsError::DuplicateToolName { name: spec.name });
132 } else {
133 warn!(
134 tool_name = %spec.name,
135 "Duplicate tool name found, skipping"
136 );
137 }
138 }
139 Err(e) => {
140 if strict {
141 return Err(e);
142 }
143 warn!(error = %e, "Invalid tool specification, skipping");
144 }
145 }
146 }
147
148 debug!(
149 total = validated.len(),
150 "Tools validated and prepared"
151 );
152
153 Ok(validated)
154}
155
156pub fn validate_tool_use(
170 tool_use: &ToolUse,
171 registered_tools: &HashSet<String>,
172) -> Result<(), StrandsError> {
173
174 if !registered_tools.contains(&tool_use.name) {
175 return Err(StrandsError::InvalidToolUseName {
176 name: tool_use.name.clone(),
177 available_tools: registered_tools.iter().cloned().collect(),
178 });
179 }
180
181
182 if tool_use.tool_use_id.is_empty() {
183 return Err(StrandsError::ToolValidationError {
184 message: format!("Tool use '{}' has empty tool_use_id", tool_use.name),
185 });
186 }
187
188 Ok(())
189}
190
191#[derive(Debug, Clone)]
193pub struct ToolUseValidationResult {
194 pub valid: Vec<ToolUse>,
196 pub invalid: Vec<(ToolUse, String)>,
198}
199
200impl ToolUseValidationResult {
201 pub fn all_valid(&self) -> bool {
203 self.invalid.is_empty()
204 }
205
206 pub fn valid_count(&self) -> usize {
208 self.valid.len()
209 }
210
211 pub fn invalid_count(&self) -> usize {
213 self.invalid.len()
214 }
215}
216
217pub fn validate_tool_uses(
228 tool_uses: &[ToolUse],
229 registered_tools: &HashSet<String>,
230) -> ToolUseValidationResult {
231 let mut valid = Vec::new();
232 let mut invalid = Vec::new();
233
234 for tool_use in tool_uses {
235 match validate_tool_use(tool_use, registered_tools) {
236 Ok(()) => valid.push(tool_use.clone()),
237 Err(e) => invalid.push((tool_use.clone(), e.to_string())),
238 }
239 }
240
241 ToolUseValidationResult { valid, invalid }
242}
243
244pub fn is_valid_tool_name(name: &str) -> bool {
254 !name.is_empty()
255 && name.len() <= MAX_TOOL_NAME_LENGTH
256 && name.chars().all(|c| c.is_alphanumeric() || c == '_' || c == '-')
257}
258
259pub fn sanitize_tool_name(name: &str) -> String {
271 let sanitized: String = name
272 .chars()
273 .map(|c| {
274 if c.is_alphanumeric() || c == '_' || c == '-' {
275 c
276 } else {
277 '_'
278 }
279 })
280 .collect();
281
282
283 if sanitized.len() > MAX_TOOL_NAME_LENGTH {
284 sanitized[..MAX_TOOL_NAME_LENGTH].to_string()
285 } else if sanitized.is_empty() {
286 "unnamed_tool".to_string()
287 } else {
288 sanitized
289 }
290}
291
292#[cfg(test)]
293mod tests {
294 use super::*;
295
296 #[test]
297 fn test_validate_tool_spec_valid() {
298 let spec = ToolSpec::new("valid_tool", "A valid tool");
299 assert!(validate_tool_spec(&spec).is_ok());
300 }
301
302 #[test]
303 fn test_validate_tool_spec_empty_name() {
304 let spec = ToolSpec::new("", "Description");
305 let result = validate_tool_spec(&spec);
306 assert!(result.is_err());
307 assert!(matches!(result, Err(StrandsError::InvalidToolName { .. })));
308 }
309
310 #[test]
311 fn test_validate_tool_spec_invalid_chars() {
312 let spec = ToolSpec::new("invalid tool!", "Description");
313 let result = validate_tool_spec(&spec);
314 assert!(result.is_err());
315 }
316
317 #[test]
318 fn test_validate_tool_specs_no_duplicates() {
319 let specs = vec![
320 ToolSpec::new("tool1", "Tool 1"),
321 ToolSpec::new("tool2", "Tool 2"),
322 ];
323 assert!(validate_tool_specs(&specs).is_ok());
324 }
325
326 #[test]
327 fn test_validate_tool_specs_with_duplicates() {
328 let specs = vec![
329 ToolSpec::new("tool1", "Tool 1"),
330 ToolSpec::new("tool1", "Tool 1 duplicate"),
331 ];
332 let result = validate_tool_specs(&specs);
333 assert!(result.is_err());
334 assert!(matches!(result, Err(StrandsError::DuplicateToolName { .. })));
335 }
336
337 #[test]
338 fn test_validate_and_prepare_tools_strict() {
339 let specs = vec![
340 ToolSpec::new("valid", "Valid tool"),
341 ToolSpec::new("", "Invalid tool"),
342 ];
343 let result = validate_and_prepare_tools(specs, true);
344 assert!(result.is_err());
345 }
346
347 #[test]
348 fn test_validate_and_prepare_tools_lenient() {
349 let specs = vec![
350 ToolSpec::new("valid", "Valid tool"),
351 ToolSpec::new("", "Invalid tool"),
352 ];
353 let result = validate_and_prepare_tools(specs, false).unwrap();
354 assert_eq!(result.len(), 1);
355 assert_eq!(result[0].name, "valid");
356 }
357
358 #[test]
359 fn test_validate_tool_use_valid() {
360 let tool_use = ToolUse::new("my_tool", "123", serde_json::json!({}));
361 let registered = HashSet::from(["my_tool".to_string()]);
362 assert!(validate_tool_use(&tool_use, ®istered).is_ok());
363 }
364
365 #[test]
366 fn test_validate_tool_use_not_found() {
367 let tool_use = ToolUse::new("unknown", "123", serde_json::json!({}));
368 let registered = HashSet::from(["my_tool".to_string()]);
369 let result = validate_tool_use(&tool_use, ®istered);
370 assert!(result.is_err());
371 assert!(matches!(result, Err(StrandsError::InvalidToolUseName { .. })));
372 }
373
374 #[test]
375 fn test_validate_tool_uses_mixed() {
376 let tool_uses = vec![
377 ToolUse::new("valid", "1", serde_json::json!({})),
378 ToolUse::new("invalid", "2", serde_json::json!({})),
379 ];
380 let registered = HashSet::from(["valid".to_string()]);
381 let result = validate_tool_uses(&tool_uses, ®istered);
382
383 assert_eq!(result.valid_count(), 1);
384 assert_eq!(result.invalid_count(), 1);
385 assert!(!result.all_valid());
386 }
387
388 #[test]
389 fn test_is_valid_tool_name() {
390 assert!(is_valid_tool_name("valid_tool"));
391 assert!(is_valid_tool_name("valid-tool"));
392 assert!(is_valid_tool_name("ValidTool123"));
393 assert!(!is_valid_tool_name(""));
394 assert!(!is_valid_tool_name("invalid tool"));
395 assert!(!is_valid_tool_name("invalid!tool"));
396 }
397
398 #[test]
399 fn test_sanitize_tool_name() {
400 assert_eq!(sanitize_tool_name("valid_tool"), "valid_tool");
401 assert_eq!(sanitize_tool_name("invalid tool"), "invalid_tool");
402 assert_eq!(sanitize_tool_name("test@#$"), "test___");
403 assert_eq!(sanitize_tool_name(""), "unnamed_tool");
404 }
405}
406