vex_llm/tools/
uuid_tool.rs

1//! UUID generation tool
2//!
3//! Uses the `uuid` crate for generating UUIDs.
4//!
5//! # Security
6//!
7//! - Uses cryptographically secure random number generator
8//! - Only generates UUIDs, no I/O operations
9//! - Pure computation: safe for any sandbox
10
11use async_trait::async_trait;
12use serde_json::Value;
13use uuid::Uuid;
14
15use crate::tool::{Capability, Tool, ToolDefinition};
16use crate::tool_error::ToolError;
17
18/// UUID generation tool.
19///
20/// Generates version 4 (random) UUIDs using a cryptographically
21/// secure random number generator.
22///
23/// # Example
24///
25/// ```ignore
26/// use vex_llm::UuidTool;
27/// use vex_llm::Tool;
28///
29/// let uuid_tool = UuidTool::new();
30/// let result = uuid_tool.execute(json!({})).await?;
31/// println!("{}", result["uuid"]);
32/// ```
33pub struct UuidTool {
34    definition: ToolDefinition,
35}
36
37impl UuidTool {
38    /// Create a new UUID tool
39    pub fn new() -> Self {
40        Self {
41            definition: ToolDefinition::new(
42                "uuid",
43                "Generate a universally unique identifier (UUID v4).",
44                r#"{
45                    "type": "object",
46                    "properties": {
47                        "count": {
48                            "type": "integer",
49                            "minimum": 1,
50                            "maximum": 100,
51                            "default": 1,
52                            "description": "Number of UUIDs to generate (1-100)"
53                        },
54                        "format": {
55                            "type": "string",
56                            "enum": ["hyphenated", "simple", "urn"],
57                            "default": "hyphenated",
58                            "description": "Output format: 'hyphenated' (550e8400-e29b-41d4-a716-446655440000), 'simple' (550e8400e29b41d4a716446655440000), 'urn' (urn:uuid:550e8400-e29b-41d4-a716-446655440000)"
59                        }
60                    }
61                }"#,
62            ),
63        }
64    }
65}
66
67impl Default for UuidTool {
68    fn default() -> Self {
69        Self::new()
70    }
71}
72
73#[async_trait]
74impl Tool for UuidTool {
75    fn definition(&self) -> &ToolDefinition {
76        &self.definition
77    }
78
79    fn capabilities(&self) -> Vec<Capability> {
80        // Uses cryptographic RNG, tag it accordingly
81        vec![Capability::PureComputation, Capability::Cryptography]
82    }
83
84    fn validate(&self, args: &Value) -> Result<(), ToolError> {
85        // Validate count if provided
86        if let Some(count) = args.get("count") {
87            if let Some(n) = count.as_i64() {
88                if !(1..=100).contains(&n) {
89                    return Err(ToolError::invalid_args(
90                        "uuid",
91                        format!("Count must be between 1 and 100, got {}", n),
92                    ));
93                }
94            } else if !count.is_null() {
95                return Err(ToolError::invalid_args("uuid", "Count must be an integer"));
96            }
97        }
98
99        // Validate format if provided
100        if let Some(fmt) = args.get("format").and_then(|v| v.as_str()) {
101            if fmt != "hyphenated" && fmt != "simple" && fmt != "urn" {
102                return Err(ToolError::invalid_args(
103                    "uuid",
104                    format!(
105                        "Invalid format '{}'. Must be 'hyphenated', 'simple', or 'urn'",
106                        fmt
107                    ),
108                ));
109            }
110        }
111
112        Ok(())
113    }
114
115    async fn execute(&self, args: Value) -> Result<Value, ToolError> {
116        let count = args.get("count").and_then(|v| v.as_i64()).unwrap_or(1) as usize;
117
118        let format = args
119            .get("format")
120            .and_then(|v| v.as_str())
121            .unwrap_or("hyphenated");
122
123        let uuids: Vec<String> = (0..count)
124            .map(|_| {
125                let uuid = Uuid::new_v4();
126                match format {
127                    "simple" => uuid.simple().to_string(),
128                    "urn" => uuid.urn().to_string(),
129                    _ => uuid.hyphenated().to_string(),
130                }
131            })
132            .collect();
133
134        if count == 1 {
135            Ok(serde_json::json!({
136                "uuid": uuids[0],
137                "format": format,
138                "version": 4
139            }))
140        } else {
141            Ok(serde_json::json!({
142                "uuids": uuids,
143                "count": count,
144                "format": format,
145                "version": 4
146            }))
147        }
148    }
149}
150
151#[cfg(test)]
152mod tests {
153    use super::*;
154
155    #[tokio::test]
156    async fn test_generate_single_uuid() {
157        let tool = UuidTool::new();
158        let result = tool.execute(serde_json::json!({})).await.unwrap();
159
160        assert!(result["uuid"].is_string());
161        let uuid = result["uuid"].as_str().unwrap();
162
163        // Validate UUID format (hyphenated has 36 chars)
164        assert_eq!(uuid.len(), 36);
165        assert!(uuid.contains('-'));
166    }
167
168    #[tokio::test]
169    async fn test_generate_multiple_uuids() {
170        let tool = UuidTool::new();
171        let result = tool.execute(serde_json::json!({"count": 5})).await.unwrap();
172
173        let uuids = result["uuids"].as_array().unwrap();
174        assert_eq!(uuids.len(), 5);
175
176        // All UUIDs should be unique
177        let mut unique: std::collections::HashSet<&str> = std::collections::HashSet::new();
178        for uuid in uuids {
179            unique.insert(uuid.as_str().unwrap());
180        }
181        assert_eq!(unique.len(), 5);
182    }
183
184    #[tokio::test]
185    async fn test_simple_format() {
186        let tool = UuidTool::new();
187        let result = tool
188            .execute(serde_json::json!({"format": "simple"}))
189            .await
190            .unwrap();
191
192        let uuid = result["uuid"].as_str().unwrap();
193        assert_eq!(uuid.len(), 32); // No hyphens
194        assert!(!uuid.contains('-'));
195    }
196
197    #[tokio::test]
198    async fn test_urn_format() {
199        let tool = UuidTool::new();
200        let result = tool
201            .execute(serde_json::json!({"format": "urn"}))
202            .await
203            .unwrap();
204
205        let uuid = result["uuid"].as_str().unwrap();
206        assert!(uuid.starts_with("urn:uuid:"));
207    }
208
209    #[tokio::test]
210    async fn test_invalid_count() {
211        let tool = UuidTool::new();
212        let result = tool.validate(&serde_json::json!({"count": 500}));
213
214        assert!(matches!(result, Err(ToolError::InvalidArguments { .. })));
215    }
216
217    #[tokio::test]
218    async fn test_invalid_format() {
219        let tool = UuidTool::new();
220        let result = tool.validate(&serde_json::json!({"format": "invalid"}));
221
222        assert!(matches!(result, Err(ToolError::InvalidArguments { .. })));
223    }
224
225    #[tokio::test]
226    async fn test_uuid_version() {
227        let tool = UuidTool::new();
228        let result = tool.execute(serde_json::json!({})).await.unwrap();
229
230        assert_eq!(result["version"], 4);
231    }
232}