use crate::file_ops::{FileError, write_text_file};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::path::Path;
use utils::display_meta::{FileDiff, ToolDisplayMeta, ToolResultMeta, basename};
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct WriteFileArgs {
#[serde(alias = "file_path")]
pub file_path: String,
pub content: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct WriteFileResponse {
pub message: String,
pub bytes_written: usize,
pub file_path: String,
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
#[schemars(skip)]
pub meta: Option<ToolResultMeta>,
}
pub async fn write_file_contents(args: WriteFileArgs) -> Result<WriteFileResponse, FileError> {
let file_path = Path::new(&args.file_path);
let result = write_text_file(file_path, &args.content).await?;
let file_path = result.path.to_string_lossy().into_owned();
let display_meta = ToolDisplayMeta::new("Write file", basename(&file_path));
let file_diff =
FileDiff { path: file_path.clone(), old_text: result.original_content, new_text: Some(args.content) };
Ok(WriteFileResponse {
message: format!("Successfully wrote {} bytes to {}", result.bytes_written, file_path),
bytes_written: result.bytes_written,
file_path,
meta: Some(ToolResultMeta::with_file_diff(display_meta, file_diff)),
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::testing::TestWorkspace;
use std::fs;
#[tokio::test]
async fn write_file_produces_file_diff_with_no_old_text() {
let workspace = TestWorkspace::new();
let file_path = workspace.path("new_file.rs");
let content = "fn main() {\n println!(\"Hello\");\n}\n";
let result = write_file_contents(WriteFileArgs {
file_path: file_path.to_string_lossy().to_string(),
content: content.to_string(),
})
.await
.unwrap();
assert!(file_path.exists());
assert_eq!(fs::read_to_string(&file_path).unwrap(), content);
let meta = result.meta.unwrap();
let diff = meta.file_diff.unwrap();
assert!(diff.old_text.is_none());
assert_eq!(diff.new_text.as_deref(), Some(content));
assert_eq!(diff.path, file_path.to_string_lossy().to_string());
}
#[tokio::test]
async fn write_file_file_diff_has_correct_path() {
let workspace = TestWorkspace::new();
let file_path = workspace.path("test.rs");
let result = write_file_contents(WriteFileArgs {
file_path: file_path.to_string_lossy().to_string(),
content: "let x = 1;".to_string(),
})
.await
.unwrap();
let diff = result.meta.unwrap().file_diff.unwrap();
assert_eq!(diff.path, file_path.to_string_lossy().to_string());
}
#[tokio::test]
async fn write_file_handles_empty_content() {
let workspace = TestWorkspace::new();
let file_path = workspace.path("empty.txt");
let result = write_file_contents(WriteFileArgs {
file_path: file_path.to_string_lossy().to_string(),
content: String::new(),
})
.await
.unwrap();
let diff = result.meta.unwrap().file_diff.unwrap();
assert_eq!(diff.new_text.as_deref(), Some(""));
assert!(diff.old_text.is_none());
}
#[tokio::test]
async fn write_file_overwrites_existing_file() {
let workspace = TestWorkspace::new().file("existing.txt", "old content");
let file_path = workspace.path("existing.txt");
let new_content = "new content\nsecond line\n";
let result = write_file_contents(WriteFileArgs {
file_path: file_path.to_string_lossy().to_string(),
content: new_content.to_string(),
})
.await
.unwrap();
assert_eq!(fs::read_to_string(&file_path).unwrap(), new_content);
let diff = result.meta.unwrap().file_diff.unwrap();
assert_eq!(diff.old_text.as_deref(), Some("old content"));
assert_eq!(diff.new_text.as_deref(), Some(new_content));
}
#[test]
fn write_file_args_accepts_snake_case_file_path() {
let args: WriteFileArgs = serde_json::from_value(serde_json::json!({
"file_path": "/tmp/out.txt",
"content": "hello"
}))
.unwrap();
assert_eq!(args.file_path, "/tmp/out.txt");
assert_eq!(args.content, "hello");
}
}