Skip to main content

cortexai_tools/
encoding.rs

1//! Encoding and data transformation tools
2
3use async_trait::async_trait;
4use base64::{engine::general_purpose::STANDARD as BASE64, Engine};
5use cortexai_core::{errors::ToolError, ExecutionContext, Tool, ToolSchema};
6use serde_json::{json, Value};
7use sha2::{Digest, Sha256, Sha512};
8
9/// JSON manipulation tool
10pub struct JsonTool;
11
12impl Default for JsonTool {
13    fn default() -> Self {
14        Self::new()
15    }
16}
17
18impl JsonTool {
19    pub fn new() -> Self {
20        Self
21    }
22}
23
24#[async_trait]
25impl Tool for JsonTool {
26    fn schema(&self) -> ToolSchema {
27        ToolSchema::new("json", "Parse, format, and manipulate JSON data")
28            .with_parameters(json!({
29                "type": "object",
30                "properties": {
31                    "operation": {
32                        "type": "string",
33                        "enum": ["parse", "stringify", "format", "minify", "get", "set", "merge", "validate"],
34                        "description": "Operation to perform"
35                    },
36                    "data": {
37                        "description": "JSON data (string or object)"
38                    },
39                    "path": {
40                        "type": "string",
41                        "description": "JSON path for get/set operations (e.g., 'user.name', 'items[0].id')"
42                    },
43                    "value": {
44                        "description": "Value to set (for 'set' operation)"
45                    },
46                    "merge_with": {
47                        "description": "Object to merge with (for 'merge' operation)"
48                    }
49                },
50                "required": ["operation", "data"]
51            }))
52    }
53
54    async fn execute(
55        &self,
56        _context: &ExecutionContext,
57        arguments: serde_json::Value,
58    ) -> Result<serde_json::Value, ToolError> {
59        let operation = arguments["operation"]
60            .as_str()
61            .ok_or_else(|| ToolError::InvalidArguments("Missing 'operation'".to_string()))?;
62
63        match operation {
64            "parse" => {
65                let data_str = arguments["data"].as_str().ok_or_else(|| {
66                    ToolError::InvalidArguments("'data' must be a string for parse".to_string())
67                })?;
68
69                let parsed: Value = serde_json::from_str(data_str)
70                    .map_err(|e| ToolError::ExecutionFailed(format!("JSON parse error: {}", e)))?;
71
72                Ok(json!({
73                    "success": true,
74                    "result": parsed
75                }))
76            }
77            "stringify" | "format" => {
78                let data = &arguments["data"];
79                let formatted = serde_json::to_string_pretty(data)
80                    .map_err(|e| ToolError::ExecutionFailed(e.to_string()))?;
81
82                Ok(json!({
83                    "success": true,
84                    "result": formatted
85                }))
86            }
87            "minify" => {
88                let data = &arguments["data"];
89                let minified = serde_json::to_string(data)
90                    .map_err(|e| ToolError::ExecutionFailed(e.to_string()))?;
91
92                Ok(json!({
93                    "success": true,
94                    "result": minified
95                }))
96            }
97            "get" => {
98                let path = arguments["path"]
99                    .as_str()
100                    .ok_or_else(|| ToolError::InvalidArguments("Missing 'path'".to_string()))?;
101
102                let value = get_json_path(&arguments["data"], path)?;
103
104                Ok(json!({
105                    "success": true,
106                    "path": path,
107                    "result": value
108                }))
109            }
110            "set" => {
111                let path = arguments["path"]
112                    .as_str()
113                    .ok_or_else(|| ToolError::InvalidArguments("Missing 'path'".to_string()))?;
114                let value = &arguments["value"];
115
116                let mut data = arguments["data"].clone();
117                set_json_path(&mut data, path, value.clone())?;
118
119                Ok(json!({
120                    "success": true,
121                    "path": path,
122                    "result": data
123                }))
124            }
125            "merge" => {
126                let merge_with = &arguments["merge_with"];
127                let mut result = arguments["data"].clone();
128
129                merge_json(&mut result, merge_with);
130
131                Ok(json!({
132                    "success": true,
133                    "result": result
134                }))
135            }
136            "validate" => {
137                let data_str = arguments["data"].as_str();
138                let (valid, error) = match data_str {
139                    Some(s) => match serde_json::from_str::<Value>(s) {
140                        Ok(_) => (true, None),
141                        Err(e) => (false, Some(e.to_string())),
142                    },
143                    None => (true, None), // Already parsed as JSON
144                };
145
146                Ok(json!({
147                    "valid": valid,
148                    "error": error
149                }))
150            }
151            _ => Err(ToolError::InvalidArguments(format!(
152                "Unknown operation: {}",
153                operation
154            ))),
155        }
156    }
157}
158
159fn get_json_path(data: &Value, path: &str) -> Result<Value, ToolError> {
160    let mut current = data;
161
162    for part in path.split('.') {
163        // Handle array indexing like "items[0]"
164        if let Some(bracket_pos) = part.find('[') {
165            let key = &part[..bracket_pos];
166            let index_str = &part[bracket_pos + 1..part.len() - 1];
167            let index: usize = index_str.parse().map_err(|_| {
168                ToolError::InvalidArguments(format!("Invalid array index: {}", index_str))
169            })?;
170
171            if !key.is_empty() {
172                current = current
173                    .get(key)
174                    .ok_or_else(|| ToolError::ExecutionFailed(format!("Key not found: {}", key)))?;
175            }
176
177            current = current.get(index).ok_or_else(|| {
178                ToolError::ExecutionFailed(format!("Index {} out of bounds", index))
179            })?;
180        } else if !part.is_empty() {
181            current = current
182                .get(part)
183                .ok_or_else(|| ToolError::ExecutionFailed(format!("Key not found: {}", part)))?;
184        }
185    }
186
187    Ok(current.clone())
188}
189
190fn set_json_path(data: &mut Value, path: &str, value: Value) -> Result<(), ToolError> {
191    let parts: Vec<&str> = path.split('.').collect();
192    let mut current = data;
193
194    for (i, part) in parts.iter().enumerate() {
195        let is_last = i == parts.len() - 1;
196
197        if let Some(bracket_pos) = part.find('[') {
198            let key = &part[..bracket_pos];
199            let index_str = &part[bracket_pos + 1..part.len() - 1];
200            let index: usize = index_str.parse().map_err(|_| {
201                ToolError::InvalidArguments(format!("Invalid array index: {}", index_str))
202            })?;
203
204            if !key.is_empty() {
205                current = current
206                    .get_mut(key)
207                    .ok_or_else(|| ToolError::ExecutionFailed(format!("Key not found: {}", key)))?;
208            }
209
210            if is_last {
211                if let Some(arr) = current.as_array_mut() {
212                    if index < arr.len() {
213                        arr[index] = value;
214                        return Ok(());
215                    }
216                }
217                return Err(ToolError::ExecutionFailed(format!(
218                    "Index {} out of bounds",
219                    index
220                )));
221            }
222
223            current = current.get_mut(index).ok_or_else(|| {
224                ToolError::ExecutionFailed(format!("Index {} out of bounds", index))
225            })?;
226        } else if is_last {
227            if let Some(obj) = current.as_object_mut() {
228                obj.insert(part.to_string(), value);
229                return Ok(());
230            }
231            return Err(ToolError::ExecutionFailed(
232                "Cannot set property on non-object".to_string(),
233            ));
234        } else {
235            current = current
236                .get_mut(*part)
237                .ok_or_else(|| ToolError::ExecutionFailed(format!("Key not found: {}", part)))?;
238        }
239    }
240
241    Ok(())
242}
243
244fn merge_json(target: &mut Value, source: &Value) {
245    match (target, source) {
246        (Value::Object(target_map), Value::Object(source_map)) => {
247            for (key, value) in source_map {
248                if let Some(target_value) = target_map.get_mut(key) {
249                    merge_json(target_value, value);
250                } else {
251                    target_map.insert(key.clone(), value.clone());
252                }
253            }
254        }
255        (target, source) => {
256            *target = source.clone();
257        }
258    }
259}
260
261/// Base64 encoding/decoding tool
262pub struct Base64Tool;
263
264impl Default for Base64Tool {
265    fn default() -> Self {
266        Self::new()
267    }
268}
269
270impl Base64Tool {
271    pub fn new() -> Self {
272        Self
273    }
274}
275
276#[async_trait]
277impl Tool for Base64Tool {
278    fn schema(&self) -> ToolSchema {
279        ToolSchema::new("base64", "Encode or decode Base64 strings").with_parameters(json!({
280            "type": "object",
281            "properties": {
282                "operation": {
283                    "type": "string",
284                    "enum": ["encode", "decode"],
285                    "description": "Whether to encode or decode"
286                },
287                "data": {
288                    "type": "string",
289                    "description": "Data to encode or decode"
290                }
291            },
292            "required": ["operation", "data"]
293        }))
294    }
295
296    async fn execute(
297        &self,
298        _context: &ExecutionContext,
299        arguments: serde_json::Value,
300    ) -> Result<serde_json::Value, ToolError> {
301        let operation = arguments["operation"]
302            .as_str()
303            .ok_or_else(|| ToolError::InvalidArguments("Missing 'operation'".to_string()))?;
304        let data = arguments["data"]
305            .as_str()
306            .ok_or_else(|| ToolError::InvalidArguments("Missing 'data'".to_string()))?;
307
308        match operation {
309            "encode" => {
310                let encoded = BASE64.encode(data.as_bytes());
311                Ok(json!({
312                    "operation": "encode",
313                    "input": data,
314                    "result": encoded
315                }))
316            }
317            "decode" => {
318                let decoded_bytes = BASE64.decode(data).map_err(|e| {
319                    ToolError::ExecutionFailed(format!("Base64 decode error: {}", e))
320                })?;
321                let decoded = String::from_utf8(decoded_bytes).map_err(|e| {
322                    ToolError::ExecutionFailed(format!("UTF-8 decode error: {}", e))
323                })?;
324                Ok(json!({
325                    "operation": "decode",
326                    "input": data,
327                    "result": decoded
328                }))
329            }
330            _ => Err(ToolError::InvalidArguments(format!(
331                "Unknown operation: {}",
332                operation
333            ))),
334        }
335    }
336}
337
338/// Hash generation tool
339pub struct HashTool;
340
341impl Default for HashTool {
342    fn default() -> Self {
343        Self::new()
344    }
345}
346
347impl HashTool {
348    pub fn new() -> Self {
349        Self
350    }
351}
352
353#[async_trait]
354impl Tool for HashTool {
355    fn schema(&self) -> ToolSchema {
356        ToolSchema::new("hash", "Generate cryptographic hashes").with_parameters(json!({
357            "type": "object",
358            "properties": {
359                "data": {
360                    "type": "string",
361                    "description": "Data to hash"
362                },
363                "algorithm": {
364                    "type": "string",
365                    "enum": ["sha256", "sha512"],
366                    "description": "Hash algorithm (default: sha256)"
367                },
368                "output": {
369                    "type": "string",
370                    "enum": ["hex", "base64"],
371                    "description": "Output format (default: hex)"
372                }
373            },
374            "required": ["data"]
375        }))
376    }
377
378    async fn execute(
379        &self,
380        _context: &ExecutionContext,
381        arguments: serde_json::Value,
382    ) -> Result<serde_json::Value, ToolError> {
383        let data = arguments["data"]
384            .as_str()
385            .ok_or_else(|| ToolError::InvalidArguments("Missing 'data'".to_string()))?;
386        let algorithm = arguments["algorithm"].as_str().unwrap_or("sha256");
387        let output_format = arguments["output"].as_str().unwrap_or("hex");
388
389        let hash_bytes: Vec<u8> = match algorithm {
390            "sha256" => {
391                let mut hasher = Sha256::new();
392                hasher.update(data.as_bytes());
393                hasher.finalize().to_vec()
394            }
395            "sha512" => {
396                let mut hasher = Sha512::new();
397                hasher.update(data.as_bytes());
398                hasher.finalize().to_vec()
399            }
400            _ => {
401                return Err(ToolError::InvalidArguments(format!(
402                    "Unknown algorithm: {}",
403                    algorithm
404                )))
405            }
406        };
407
408        let result = match output_format {
409            "hex" => hex::encode(&hash_bytes),
410            "base64" => BASE64.encode(&hash_bytes),
411            _ => {
412                return Err(ToolError::InvalidArguments(format!(
413                    "Unknown output format: {}",
414                    output_format
415                )))
416            }
417        };
418
419        Ok(json!({
420            "data": data,
421            "algorithm": algorithm,
422            "output_format": output_format,
423            "hash": result,
424            "length": hash_bytes.len() * 8
425        }))
426    }
427}
428
429/// URL encoding/decoding tool
430pub struct UrlEncodeTool;
431
432impl Default for UrlEncodeTool {
433    fn default() -> Self {
434        Self::new()
435    }
436}
437
438impl UrlEncodeTool {
439    pub fn new() -> Self {
440        Self
441    }
442}
443
444#[async_trait]
445impl Tool for UrlEncodeTool {
446    fn schema(&self) -> ToolSchema {
447        ToolSchema::new("url_encode", "Encode or decode URL strings").with_parameters(json!({
448            "type": "object",
449            "properties": {
450                "operation": {
451                    "type": "string",
452                    "enum": ["encode", "decode"],
453                    "description": "Whether to encode or decode"
454                },
455                "data": {
456                    "type": "string",
457                    "description": "String to encode or decode"
458                }
459            },
460            "required": ["operation", "data"]
461        }))
462    }
463
464    async fn execute(
465        &self,
466        _context: &ExecutionContext,
467        arguments: serde_json::Value,
468    ) -> Result<serde_json::Value, ToolError> {
469        let operation = arguments["operation"]
470            .as_str()
471            .ok_or_else(|| ToolError::InvalidArguments("Missing 'operation'".to_string()))?;
472        let data = arguments["data"]
473            .as_str()
474            .ok_or_else(|| ToolError::InvalidArguments("Missing 'data'".to_string()))?;
475
476        match operation {
477            "encode" => {
478                let encoded = urlencoding::encode(data);
479                Ok(json!({
480                    "operation": "encode",
481                    "input": data,
482                    "result": encoded.to_string()
483                }))
484            }
485            "decode" => {
486                let decoded = urlencoding::decode(data)
487                    .map_err(|e| ToolError::ExecutionFailed(format!("URL decode error: {}", e)))?;
488                Ok(json!({
489                    "operation": "decode",
490                    "input": data,
491                    "result": decoded.to_string()
492                }))
493            }
494            _ => Err(ToolError::InvalidArguments(format!(
495                "Unknown operation: {}",
496                operation
497            ))),
498        }
499    }
500}
501
502#[cfg(test)]
503mod tests {
504    use super::*;
505    use cortexai_core::types::AgentId;
506
507    fn test_ctx() -> ExecutionContext {
508        ExecutionContext::new(AgentId::new("test-agent"))
509    }
510
511    #[tokio::test]
512    async fn test_json_parse() {
513        let tool = JsonTool::new();
514        let ctx = test_ctx();
515
516        let result = tool
517            .execute(
518                &ctx,
519                json!({
520                    "operation": "parse",
521                    "data": r#"{"name": "test", "value": 42}"#
522                }),
523            )
524            .await
525            .unwrap();
526
527        assert!(result["success"].as_bool().unwrap());
528        assert_eq!(result["result"]["name"], "test");
529        assert_eq!(result["result"]["value"], 42);
530    }
531
532    #[tokio::test]
533    async fn test_json_get_path() {
534        let tool = JsonTool::new();
535        let ctx = test_ctx();
536
537        let result = tool
538            .execute(
539                &ctx,
540                json!({
541                    "operation": "get",
542                    "data": {"user": {"name": "John", "age": 30}},
543                    "path": "user.name"
544                }),
545            )
546            .await
547            .unwrap();
548
549        assert_eq!(result["result"], "John");
550    }
551
552    #[tokio::test]
553    async fn test_json_merge() {
554        let tool = JsonTool::new();
555        let ctx = test_ctx();
556
557        let result = tool
558            .execute(
559                &ctx,
560                json!({
561                    "operation": "merge",
562                    "data": {"a": 1, "b": 2},
563                    "merge_with": {"b": 3, "c": 4}
564                }),
565            )
566            .await
567            .unwrap();
568
569        assert_eq!(result["result"]["a"], 1);
570        assert_eq!(result["result"]["b"], 3);
571        assert_eq!(result["result"]["c"], 4);
572    }
573
574    #[tokio::test]
575    async fn test_base64() {
576        let tool = Base64Tool::new();
577        let ctx = test_ctx();
578
579        let encoded = tool
580            .execute(
581                &ctx,
582                json!({
583                    "operation": "encode",
584                    "data": "Hello, World!"
585                }),
586            )
587            .await
588            .unwrap();
589
590        assert_eq!(encoded["result"], "SGVsbG8sIFdvcmxkIQ==");
591
592        let decoded = tool
593            .execute(
594                &ctx,
595                json!({
596                    "operation": "decode",
597                    "data": "SGVsbG8sIFdvcmxkIQ=="
598                }),
599            )
600            .await
601            .unwrap();
602
603        assert_eq!(decoded["result"], "Hello, World!");
604    }
605
606    #[tokio::test]
607    async fn test_hash() {
608        let tool = HashTool::new();
609        let ctx = test_ctx();
610
611        let result = tool
612            .execute(
613                &ctx,
614                json!({
615                    "data": "hello",
616                    "algorithm": "sha256"
617                }),
618            )
619            .await
620            .unwrap();
621
622        assert_eq!(
623            result["hash"],
624            "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
625        );
626    }
627}