vex_llm/tools/
json_path.rs

1//! JSON Path tool for querying JSON data
2//!
3//! Extracts values from JSON using path expressions.
4//!
5//! # Security
6//!
7//! - Input size limited (prevents memory exhaustion)
8//! - Pure computation, no I/O
9//! - Path syntax validated before execution
10
11use async_trait::async_trait;
12use serde_json::Value;
13
14use crate::tool::{Capability, Tool, ToolDefinition};
15use crate::tool_error::ToolError;
16
17/// JSON Path tool for extracting values from JSON.
18///
19/// Supports simple dot-notation paths like "user.name" and array access "[0]".
20///
21/// # Example
22///
23/// ```ignore
24/// use vex_llm::JsonPathTool;
25/// use vex_llm::Tool;
26///
27/// let jp = JsonPathTool::new();
28/// let result = jp.execute(json!({
29///     "data": {"user": {"name": "Alice"}},
30///     "path": "user.name"
31/// })).await?;
32/// println!("{}", result["value"]); // "Alice"
33/// ```
34pub struct JsonPathTool {
35    definition: ToolDefinition,
36}
37
38impl JsonPathTool {
39    /// Create a new JSON path tool
40    pub fn new() -> Self {
41        Self {
42            definition: ToolDefinition::new(
43                "json_path",
44                "Extract values from JSON using dot-notation paths. Supports: 'key', 'key.nested', 'array[0]', 'data.items[2].name'",
45                r#"{
46                    "type": "object",
47                    "properties": {
48                        "data": {
49                            "description": "JSON data to query"
50                        },
51                        "path": {
52                            "type": "string",
53                            "description": "Path expression (e.g., 'user.name', 'items[0]')"
54                        }
55                    },
56                    "required": ["data", "path"]
57                }"#,
58            ),
59        }
60    }
61
62    /// Navigate to a nested value using a path string
63    fn navigate<'a>(data: &'a Value, path: &str) -> Option<&'a Value> {
64        let mut current = data;
65
66        for segment in Self::parse_path(path) {
67            match segment {
68                PathSegment::Key(key) => {
69                    current = current.get(key)?;
70                }
71                PathSegment::Index(idx) => {
72                    current = current.get(idx)?;
73                }
74            }
75        }
76
77        Some(current)
78    }
79
80    /// Parse a path string into segments
81    fn parse_path(path: &str) -> Vec<PathSegment> {
82        let mut segments = Vec::new();
83        let mut current_key = String::new();
84        let mut chars = path.chars().peekable();
85
86        while let Some(c) = chars.next() {
87            match c {
88                '.' => {
89                    if !current_key.is_empty() {
90                        segments.push(PathSegment::Key(std::mem::take(&mut current_key)));
91                    }
92                }
93                '[' => {
94                    if !current_key.is_empty() {
95                        segments.push(PathSegment::Key(std::mem::take(&mut current_key)));
96                    }
97                    // Parse array index
98                    let mut idx_str = String::new();
99                    while let Some(&next_c) = chars.peek() {
100                        if next_c == ']' {
101                            chars.next(); // consume ']'
102                            break;
103                        }
104                        idx_str.push(chars.next().unwrap());
105                    }
106                    if let Ok(idx) = idx_str.parse::<usize>() {
107                        segments.push(PathSegment::Index(idx));
108                    }
109                }
110                ']' => {} // Already handled
111                _ => {
112                    current_key.push(c);
113                }
114            }
115        }
116
117        if !current_key.is_empty() {
118            segments.push(PathSegment::Key(current_key));
119        }
120
121        segments
122    }
123}
124
125#[derive(Debug)]
126enum PathSegment {
127    Key(String),
128    Index(usize),
129}
130
131impl Default for JsonPathTool {
132    fn default() -> Self {
133        Self::new()
134    }
135}
136
137#[async_trait]
138impl Tool for JsonPathTool {
139    fn definition(&self) -> &ToolDefinition {
140        &self.definition
141    }
142
143    fn capabilities(&self) -> Vec<Capability> {
144        vec![Capability::PureComputation]
145    }
146
147    fn validate(&self, args: &Value) -> Result<(), ToolError> {
148        // Check data exists
149        if args.get("data").is_none() {
150            return Err(ToolError::invalid_args(
151                "json_path",
152                "Missing required field 'data'",
153            ));
154        }
155
156        // Check path exists and is valid
157        let path = args
158            .get("path")
159            .and_then(|p| p.as_str())
160            .ok_or_else(|| ToolError::invalid_args("json_path", "Missing required field 'path'"))?;
161
162        if path.is_empty() {
163            return Err(ToolError::invalid_args("json_path", "Path cannot be empty"));
164        }
165
166        if path.len() > 200 {
167            return Err(ToolError::invalid_args(
168                "json_path",
169                "Path too long (max 200 characters)",
170            ));
171        }
172
173        Ok(())
174    }
175
176    async fn execute(&self, args: Value) -> Result<Value, ToolError> {
177        let data = args
178            .get("data")
179            .ok_or_else(|| ToolError::invalid_args("json_path", "Missing 'data' field"))?;
180
181        let path = args["path"]
182            .as_str()
183            .ok_or_else(|| ToolError::invalid_args("json_path", "Missing 'path' field"))?;
184
185        let value = Self::navigate(data, path);
186
187        Ok(serde_json::json!({
188            "path": path,
189            "found": value.is_some(),
190            "value": value.cloned()
191        }))
192    }
193}
194
195#[cfg(test)]
196mod tests {
197    use super::*;
198
199    #[tokio::test]
200    async fn test_simple_key() {
201        let tool = JsonPathTool::new();
202        let result = tool
203            .execute(serde_json::json!({
204                "data": {"name": "Alice"},
205                "path": "name"
206            }))
207            .await
208            .unwrap();
209
210        assert_eq!(result["found"], true);
211        assert_eq!(result["value"], "Alice");
212    }
213
214    #[tokio::test]
215    async fn test_nested_key() {
216        let tool = JsonPathTool::new();
217        let result = tool
218            .execute(serde_json::json!({
219                "data": {"user": {"name": "Bob"}},
220                "path": "user.name"
221            }))
222            .await
223            .unwrap();
224
225        assert_eq!(result["found"], true);
226        assert_eq!(result["value"], "Bob");
227    }
228
229    #[tokio::test]
230    async fn test_array_index() {
231        let tool = JsonPathTool::new();
232        let result = tool
233            .execute(serde_json::json!({
234                "data": {"items": ["a", "b", "c"]},
235                "path": "items[1]"
236            }))
237            .await
238            .unwrap();
239
240        assert_eq!(result["found"], true);
241        assert_eq!(result["value"], "b");
242    }
243
244    #[tokio::test]
245    async fn test_complex_path() {
246        let tool = JsonPathTool::new();
247        let result = tool
248            .execute(serde_json::json!({
249                "data": {
250                    "users": [
251                        {"name": "Alice", "age": 30},
252                        {"name": "Bob", "age": 25}
253                    ]
254                },
255                "path": "users[1].name"
256            }))
257            .await
258            .unwrap();
259
260        assert_eq!(result["found"], true);
261        assert_eq!(result["value"], "Bob");
262    }
263
264    #[tokio::test]
265    async fn test_not_found() {
266        let tool = JsonPathTool::new();
267        let result = tool
268            .execute(serde_json::json!({
269                "data": {"name": "Alice"},
270                "path": "age"
271            }))
272            .await
273            .unwrap();
274
275        assert_eq!(result["found"], false);
276        assert!(result["value"].is_null());
277    }
278
279    #[tokio::test]
280    async fn test_empty_path() {
281        let tool = JsonPathTool::new();
282        let result = tool.validate(&serde_json::json!({
283            "data": {"name": "Alice"},
284            "path": ""
285        }));
286
287        assert!(matches!(result, Err(ToolError::InvalidArguments { .. })));
288    }
289}