fob_graph/runtime/
native.rs1#![allow(clippy::disallowed_methods)]
31
32use async_trait::async_trait;
33use std::path::{Path, PathBuf};
34use tokio::task;
35
36use crate::runtime::{FileMetadata, Runtime, RuntimeError, RuntimeResult};
37
38#[derive(Debug, Clone, Copy)]
59pub struct NativeRuntime;
60
61impl NativeRuntime {
62 pub fn new() -> Self {
64 Self
65 }
66}
67
68impl Default for NativeRuntime {
69 fn default() -> Self {
70 Self::new()
71 }
72}
73
74#[async_trait]
75impl Runtime for NativeRuntime {
76 async fn read_file(&self, path: &Path) -> RuntimeResult<Vec<u8>> {
84 let path = path.to_path_buf();
85
86 task::spawn_blocking(move || {
87 std::fs::read(&path).map_err(|e| {
88 if e.kind() == std::io::ErrorKind::NotFound {
89 RuntimeError::FileNotFound(path.clone())
90 } else {
91 RuntimeError::Io(format!("Failed to read {}: {}", path.display(), e))
92 }
93 })
94 })
95 .await
96 .map_err(|e| RuntimeError::Other(format!("Task join error: {}", e)))?
97 }
98
99 async fn write_file(&self, path: &Path, content: &[u8]) -> RuntimeResult<()> {
101 let path = path.to_path_buf();
102 let content = content.to_vec();
103
104 task::spawn_blocking(move || {
105 std::fs::write(&path, content)
106 .map_err(|e| RuntimeError::Io(format!("Failed to write {}: {}", path.display(), e)))
107 })
108 .await
109 .map_err(|e| RuntimeError::Other(format!("Task join error: {}", e)))?
110 }
111
112 async fn metadata(&self, path: &Path) -> RuntimeResult<FileMetadata> {
114 let path = path.to_path_buf();
115
116 task::spawn_blocking(move || {
117 let metadata = std::fs::metadata(&path).map_err(|e| {
118 if e.kind() == std::io::ErrorKind::NotFound {
119 RuntimeError::FileNotFound(path.clone())
120 } else {
121 RuntimeError::Io(format!(
122 "Failed to get metadata for {}: {}",
123 path.display(),
124 e
125 ))
126 }
127 })?;
128
129 let modified = metadata
131 .modified()
132 .ok()
133 .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
134 .map(|d| d.as_millis() as u64);
135
136 Ok(FileMetadata {
137 size: metadata.len(),
138 is_dir: metadata.is_dir(),
139 is_file: metadata.is_file(),
140 modified,
141 })
142 })
143 .await
144 .map_err(|e| RuntimeError::Other(format!("Task join error: {}", e)))?
145 }
146
147 fn exists(&self, path: &Path) -> bool {
155 path.exists()
156 }
157
158 fn resolve(&self, specifier: &str, from: &Path) -> RuntimeResult<PathBuf> {
166 if Path::new(specifier).is_absolute() {
168 return Ok(PathBuf::from(specifier));
169 }
170
171 if specifier.starts_with("./") || specifier.starts_with("../") {
173 let from_dir = from.parent().unwrap_or(Path::new(""));
174 let resolved = from_dir.join(specifier);
175
176 return resolved
178 .canonicalize()
179 .map_err(|e| RuntimeError::ResolutionFailed {
180 specifier: specifier.to_string(),
181 from: from.to_path_buf(),
182 reason: format!("Canonicalization failed: {}", e),
183 });
184 }
185
186 Ok(PathBuf::from(specifier))
189 }
190
191 async fn create_dir(&self, path: &Path, recursive: bool) -> RuntimeResult<()> {
193 let path = path.to_path_buf();
194
195 task::spawn_blocking(move || {
196 let result = if recursive {
197 std::fs::create_dir_all(&path)
198 } else {
199 std::fs::create_dir(&path)
200 };
201
202 result.map_err(|e| {
203 RuntimeError::Io(format!(
204 "Failed to create directory {}: {}",
205 path.display(),
206 e
207 ))
208 })
209 })
210 .await
211 .map_err(|e| RuntimeError::Other(format!("Task join error: {}", e)))?
212 }
213
214 async fn remove_file(&self, path: &Path) -> RuntimeResult<()> {
216 let path = path.to_path_buf();
217
218 task::spawn_blocking(move || {
219 std::fs::remove_file(&path).map_err(|e| {
220 if e.kind() == std::io::ErrorKind::NotFound {
221 RuntimeError::FileNotFound(path.clone())
222 } else {
223 RuntimeError::Io(format!("Failed to remove {}: {}", path.display(), e))
224 }
225 })
226 })
227 .await
228 .map_err(|e| RuntimeError::Other(format!("Task join error: {}", e)))?
229 }
230
231 async fn read_dir(&self, path: &Path) -> RuntimeResult<Vec<String>> {
233 let path = path.to_path_buf();
234
235 task::spawn_blocking(move || {
236 let entries = std::fs::read_dir(&path).map_err(|e| {
237 if e.kind() == std::io::ErrorKind::NotFound {
238 RuntimeError::FileNotFound(path.clone())
239 } else {
240 RuntimeError::Io(format!(
241 "Failed to read directory {}: {}",
242 path.display(),
243 e
244 ))
245 }
246 })?;
247
248 let mut result = Vec::new();
249 for entry in entries {
250 let entry = entry.map_err(|e| {
251 RuntimeError::Io(format!("Failed to read directory entry: {}", e))
252 })?;
253
254 if let Some(name) = entry.file_name().to_str() {
255 result.push(name.to_string());
256 }
257 }
258
259 Ok(result)
260 })
261 .await
262 .map_err(|e| RuntimeError::Other(format!("Task join error: {}", e)))?
263 }
264
265 fn get_cwd(&self) -> RuntimeResult<PathBuf> {
273 std::env::current_dir().map_err(|e| {
274 RuntimeError::Io(format!("Failed to get current working directory: {}", e))
275 })
276 }
277}
278
279#[cfg(test)]
280mod tests {
281 use super::*;
282 use std::fs;
283 use tempfile::TempDir;
284
285 #[tokio::test]
286 async fn test_read_write_file() {
287 let temp_dir = TempDir::new().unwrap();
288 let file_path = temp_dir.path().join("test.txt");
289
290 let runtime = NativeRuntime::new();
291
292 let content = b"Hello, World!";
294 runtime.write_file(&file_path, content).await.unwrap();
295
296 let read_content = runtime.read_file(&file_path).await.unwrap();
298 assert_eq!(read_content, content);
299 }
300
301 #[tokio::test]
302 async fn test_metadata() {
303 let temp_dir = TempDir::new().unwrap();
304 let file_path = temp_dir.path().join("test.txt");
305
306 fs::write(&file_path, b"test content").unwrap();
307
308 let runtime = NativeRuntime::new();
309 let metadata = runtime.metadata(&file_path).await.unwrap();
310
311 assert!(metadata.is_file);
312 assert!(!metadata.is_dir);
313 assert_eq!(metadata.size, 12); }
315
316 #[tokio::test]
317 async fn test_exists() {
318 let temp_dir = TempDir::new().unwrap();
319 let file_path = temp_dir.path().join("test.txt");
320
321 let runtime = NativeRuntime::new();
322
323 assert!(!runtime.exists(&file_path));
325
326 fs::write(&file_path, b"test").unwrap();
328
329 assert!(runtime.exists(&file_path));
331 }
332
333 #[tokio::test]
334 async fn test_read_dir() {
335 let temp_dir = TempDir::new().unwrap();
336 fs::write(temp_dir.path().join("file1.txt"), b"test1").unwrap();
337 fs::write(temp_dir.path().join("file2.txt"), b"test2").unwrap();
338
339 let runtime = NativeRuntime::new();
340 let mut entries = runtime.read_dir(temp_dir.path()).await.unwrap();
341 entries.sort();
342
343 assert_eq!(entries, vec!["file1.txt", "file2.txt"]);
344 }
345
346 #[tokio::test]
347 async fn test_create_dir() {
348 let temp_dir = TempDir::new().unwrap();
349 let dir_path = temp_dir.path().join("subdir");
350
351 let runtime = NativeRuntime::new();
352 runtime.create_dir(&dir_path, false).await.unwrap();
353
354 assert!(dir_path.exists());
355 assert!(dir_path.is_dir());
356 }
357
358 #[tokio::test]
359 async fn test_create_dir_recursive() {
360 let temp_dir = TempDir::new().unwrap();
361 let nested_path = temp_dir.path().join("a").join("b").join("c");
362
363 let runtime = NativeRuntime::new();
364 runtime.create_dir(&nested_path, true).await.unwrap();
365
366 assert!(nested_path.exists());
367 assert!(nested_path.is_dir());
368 }
369}