astrid_tools/
write_file.rs1use crate::{BuiltinTool, ToolContext, ToolError, ToolResult};
4use serde_json::Value;
5
6pub struct WriteFileTool;
8
9#[async_trait::async_trait]
10impl BuiltinTool for WriteFileTool {
11 fn name(&self) -> &'static str {
12 "write_file"
13 }
14
15 fn description(&self) -> &'static str {
16 "Writes content to a file. Creates parent directories if they don't exist. \
17 Overwrites the file if it already exists."
18 }
19
20 fn input_schema(&self) -> Value {
21 serde_json::json!({
22 "type": "object",
23 "properties": {
24 "file_path": {
25 "type": "string",
26 "description": "Absolute path to the file to write"
27 },
28 "content": {
29 "type": "string",
30 "description": "The content to write to the file"
31 }
32 },
33 "required": ["file_path", "content"]
34 })
35 }
36
37 async fn execute(&self, args: Value, _ctx: &ToolContext) -> ToolResult {
38 let file_path = args
39 .get("file_path")
40 .and_then(Value::as_str)
41 .ok_or_else(|| ToolError::InvalidArguments("file_path is required".into()))?;
42
43 let content = args
44 .get("content")
45 .and_then(Value::as_str)
46 .ok_or_else(|| ToolError::InvalidArguments("content is required".into()))?;
47
48 let path = std::path::Path::new(file_path);
49
50 if let Some(parent) = path.parent() {
52 tokio::fs::create_dir_all(parent).await?;
53 }
54
55 tokio::fs::write(path, content).await?;
56
57 let bytes = content.len();
58 Ok(format!("Wrote {bytes} bytes to {file_path}"))
59 }
60}
61
62#[cfg(test)]
63mod tests {
64 use super::*;
65 use tempfile::TempDir;
66
67 fn ctx() -> ToolContext {
68 ToolContext::new(std::env::temp_dir())
69 }
70
71 #[tokio::test]
72 async fn test_write_file_basic() {
73 let dir = TempDir::new().unwrap();
74 let path = dir.path().join("test.txt");
75
76 let result = WriteFileTool
77 .execute(
78 serde_json::json!({
79 "file_path": path.to_str().unwrap(),
80 "content": "hello world"
81 }),
82 &ctx(),
83 )
84 .await
85 .unwrap();
86
87 assert!(result.contains("11 bytes"));
88 assert_eq!(std::fs::read_to_string(&path).unwrap(), "hello world");
89 }
90
91 #[tokio::test]
92 async fn test_write_file_creates_dirs() {
93 let dir = TempDir::new().unwrap();
94 let path = dir.path().join("a").join("b").join("c").join("test.txt");
95
96 WriteFileTool
97 .execute(
98 serde_json::json!({
99 "file_path": path.to_str().unwrap(),
100 "content": "nested"
101 }),
102 &ctx(),
103 )
104 .await
105 .unwrap();
106
107 assert_eq!(std::fs::read_to_string(&path).unwrap(), "nested");
108 }
109
110 #[tokio::test]
111 async fn test_write_file_overwrites() {
112 let dir = TempDir::new().unwrap();
113 let path = dir.path().join("test.txt");
114 std::fs::write(&path, "old content").unwrap();
115
116 WriteFileTool
117 .execute(
118 serde_json::json!({
119 "file_path": path.to_str().unwrap(),
120 "content": "new content"
121 }),
122 &ctx(),
123 )
124 .await
125 .unwrap();
126
127 assert_eq!(std::fs::read_to_string(&path).unwrap(), "new content");
128 }
129
130 #[tokio::test]
131 async fn test_write_file_missing_args() {
132 let result = WriteFileTool
133 .execute(serde_json::json!({"file_path": "/tmp/test.txt"}), &ctx())
134 .await;
135 assert!(result.is_err());
136
137 let result = WriteFileTool
138 .execute(serde_json::json!({"content": "hello"}), &ctx())
139 .await;
140 assert!(result.is_err());
141 }
142}