vex_llm/tools/
json_path.rs1use async_trait::async_trait;
12use serde_json::Value;
13
14use crate::tool::{Capability, Tool, ToolDefinition};
15use crate::tool_error::ToolError;
16
17pub struct JsonPathTool {
35 definition: ToolDefinition,
36}
37
38impl JsonPathTool {
39 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 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 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 let mut idx_str = String::new();
99 while let Some(&next_c) = chars.peek() {
100 if next_c == ']' {
101 chars.next(); 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 ']' => {} _ => {
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 if args.get("data").is_none() {
150 return Err(ToolError::invalid_args(
151 "json_path",
152 "Missing required field 'data'",
153 ));
154 }
155
156 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}