sage_runtime/tools/
filesystem.rs1use crate::error::{SageError, SageResult};
6use crate::mock::{try_get_mock, MockResponse};
7use std::path::PathBuf;
8
9#[derive(Debug, Clone)]
13pub struct FsClient {
14 root: PathBuf,
15}
16
17impl FsClient {
18 pub fn new() -> Self {
20 Self {
21 root: PathBuf::from("."),
22 }
23 }
24
25 pub fn from_env() -> Self {
30 let root = std::env::var("SAGE_FS_ROOT")
31 .map(PathBuf::from)
32 .unwrap_or_else(|_| PathBuf::from("."));
33
34 Self { root }
35 }
36
37 pub fn with_root(root: PathBuf) -> Self {
39 Self { root }
40 }
41
42 fn resolve_path(&self, path: &str) -> PathBuf {
44 self.root.join(path)
45 }
46
47 pub async fn read(&self, path: String) -> SageResult<String> {
55 if let Some(mock_response) = try_get_mock("Fs", "read") {
57 return Self::apply_mock_string(mock_response);
58 }
59
60 let full_path = self.resolve_path(&path);
61 let content = tokio::fs::read_to_string(&full_path).await?;
62 Ok(content)
63 }
64
65 pub async fn write(&self, path: String, content: String) -> SageResult<()> {
74 if let Some(mock_response) = try_get_mock("Fs", "write") {
76 return Self::apply_mock_unit(mock_response);
77 }
78
79 let full_path = self.resolve_path(&path);
80 if let Some(parent) = full_path.parent() {
82 tokio::fs::create_dir_all(parent).await?;
83 }
84 tokio::fs::write(&full_path, content).await?;
85 Ok(())
86 }
87
88 pub async fn exists(&self, path: String) -> SageResult<bool> {
96 if let Some(mock_response) = try_get_mock("Fs", "exists") {
98 return Self::apply_mock_bool(mock_response);
99 }
100
101 let full_path = self.resolve_path(&path);
102 Ok(full_path.exists())
103 }
104
105 pub async fn list(&self, path: String) -> SageResult<Vec<String>> {
113 if let Some(mock_response) = try_get_mock("Fs", "list") {
115 return Self::apply_mock_vec_string(mock_response);
116 }
117
118 let full_path = self.resolve_path(&path);
119 let mut entries = tokio::fs::read_dir(&full_path).await?;
120 let mut names = Vec::new();
121 while let Some(entry) = entries.next_entry().await? {
122 if let Some(name) = entry.file_name().to_str() {
123 names.push(name.to_string());
124 }
125 }
126 Ok(names)
127 }
128
129 pub async fn delete(&self, path: String) -> SageResult<()> {
137 if let Some(mock_response) = try_get_mock("Fs", "delete") {
139 return Self::apply_mock_unit(mock_response);
140 }
141
142 let full_path = self.resolve_path(&path);
143 tokio::fs::remove_file(&full_path).await?;
144 Ok(())
145 }
146
147 fn apply_mock_string(mock_response: MockResponse) -> SageResult<String> {
149 match mock_response {
150 MockResponse::Value(v) => serde_json::from_value(v)
151 .map_err(|e| SageError::Tool(format!("mock deserialize: {e}"))),
152 MockResponse::Fail(msg) => Err(SageError::Tool(msg)),
153 }
154 }
155
156 fn apply_mock_unit(mock_response: MockResponse) -> SageResult<()> {
158 match mock_response {
159 MockResponse::Value(_) => Ok(()),
160 MockResponse::Fail(msg) => Err(SageError::Tool(msg)),
161 }
162 }
163
164 fn apply_mock_bool(mock_response: MockResponse) -> SageResult<bool> {
166 match mock_response {
167 MockResponse::Value(v) => serde_json::from_value(v)
168 .map_err(|e| SageError::Tool(format!("mock deserialize: {e}"))),
169 MockResponse::Fail(msg) => Err(SageError::Tool(msg)),
170 }
171 }
172
173 fn apply_mock_vec_string(mock_response: MockResponse) -> SageResult<Vec<String>> {
175 match mock_response {
176 MockResponse::Value(v) => serde_json::from_value(v)
177 .map_err(|e| SageError::Tool(format!("mock deserialize: {e}"))),
178 MockResponse::Fail(msg) => Err(SageError::Tool(msg)),
179 }
180 }
181}
182
183impl Default for FsClient {
184 fn default() -> Self {
185 Self::new()
186 }
187}
188
189#[cfg(test)]
190mod tests {
191 use super::*;
192
193 #[test]
194 fn filesystem_client_creates() {
195 let client = FsClient::new();
196 assert_eq!(client.root, PathBuf::from("."));
197 }
198
199 #[test]
200 fn filesystem_client_with_root() {
201 let client = FsClient::with_root(PathBuf::from("/tmp"));
202 assert_eq!(client.root, PathBuf::from("/tmp"));
203 }
204
205 #[tokio::test]
206 async fn filesystem_read_write() {
207 let temp_dir = tempfile::tempdir().unwrap();
208 let client = FsClient::with_root(temp_dir.path().to_path_buf());
209
210 client
212 .write("test.txt".to_string(), "Hello, World!".to_string())
213 .await
214 .unwrap();
215
216 let content = client.read("test.txt".to_string()).await.unwrap();
218 assert_eq!(content, "Hello, World!");
219
220 assert!(client.exists("test.txt".to_string()).await.unwrap());
222
223 client.delete("test.txt".to_string()).await.unwrap();
225
226 assert!(!client.exists("test.txt".to_string()).await.unwrap());
228 }
229
230 #[tokio::test]
231 async fn filesystem_list() {
232 let temp_dir = tempfile::tempdir().unwrap();
233 let client = FsClient::with_root(temp_dir.path().to_path_buf());
234
235 client
237 .write("a.txt".to_string(), "a".to_string())
238 .await
239 .unwrap();
240 client
241 .write("b.txt".to_string(), "b".to_string())
242 .await
243 .unwrap();
244
245 let mut files = client.list(".".to_string()).await.unwrap();
247 files.sort();
248 assert_eq!(files, vec!["a.txt", "b.txt"]);
249 }
250
251 #[tokio::test]
252 async fn filesystem_write_creates_parents() {
253 let temp_dir = tempfile::tempdir().unwrap();
254 let client = FsClient::with_root(temp_dir.path().to_path_buf());
255
256 client
258 .write("nested/dir/file.txt".to_string(), "content".to_string())
259 .await
260 .unwrap();
261
262 assert!(client
264 .exists("nested/dir/file.txt".to_string())
265 .await
266 .unwrap());
267 }
268}