Skip to main content

astrid_tools/
write_file.rs

1//! Write file tool — writes content to a file, creating parent directories as needed.
2
3use crate::{BuiltinTool, ToolContext, ToolError, ToolResult};
4use serde_json::Value;
5
6/// Built-in tool for writing files.
7pub 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        // Create parent directories
51        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}