Skip to main content

iris_core/
io.rs

1//! Async file I/O utilities built on Tokio.
2//!
3//! Provides convenience functions for reading/writing files in an async
4//! context, path resolution helpers, and a file change detector.
5
6use std::path::{Path, PathBuf};
7use tokio::fs;
8use tokio::io::{AsyncReadExt, AsyncWriteExt};
9
10/// Read an entire file as a string (async).
11///
12/// Returns `Ok(content)` or an error message.
13pub async fn read_to_string<P: AsRef<Path>>(path: P) -> Result<String, String> {
14    let path = path.as_ref();
15    let mut file = fs::File::open(path)
16        .await
17        .map_err(|e| format!("Failed to open {}: {}", path.display(), e))?;
18    let mut contents = String::new();
19    file.read_to_string(&mut contents)
20        .await
21        .map_err(|e| format!("Failed to read {}: {}", path.display(), e))?;
22    Ok(contents)
23}
24
25/// Read an entire file as bytes (async).
26pub async fn read_bytes<P: AsRef<Path>>(path: P) -> Result<Vec<u8>, String> {
27    let path = path.as_ref();
28    fs::read(path)
29        .await
30        .map_err(|e| format!("Failed to read {}: {}", path.display(), e))
31}
32
33/// Write a string to a file (async), creating parent directories if needed.
34pub async fn write_string<P: AsRef<Path>>(path: P, content: &str) -> Result<(), String> {
35    let path = path.as_ref();
36    if let Some(parent) = path.parent() {
37        fs::create_dir_all(parent)
38            .await
39            .map_err(|e| format!("Failed to create dir {}: {}", parent.display(), e))?;
40    }
41    let mut file = fs::File::create(path)
42        .await
43        .map_err(|e| format!("Failed to create {}: {}", path.display(), e))?;
44    file.write_all(content.as_bytes())
45        .await
46        .map_err(|e| format!("Failed to write {}: {}", path.display(), e))
47}
48
49/// Write bytes to a file (async), creating parent directories if needed.
50pub async fn write_bytes<P: AsRef<Path>>(path: P, data: &[u8]) -> Result<(), String> {
51    let path = path.as_ref();
52    if let Some(parent) = path.parent() {
53        fs::create_dir_all(parent)
54            .await
55            .map_err(|e| format!("Failed to create dir {}: {}", parent.display(), e))?;
56    }
57    fs::write(path, data)
58        .await
59        .map_err(|e| format!("Failed to write {}: {}", path.display(), e))
60}
61
62/// Check if a path exists.
63pub async fn exists<P: AsRef<Path>>(path: P) -> bool {
64    fs::try_exists(path).await.unwrap_or(false)
65}
66
67/// Resolve a relative path against a base directory, canonicalizing the result.
68pub fn resolve(base: &Path, relative: &Path) -> PathBuf {
69    if relative.is_absolute() {
70        relative.to_path_buf()
71    } else {
72        base.join(relative)
73    }
74}
75
76/// Walk a directory recursively, yielding all file paths matching `extensions`.
77///
78/// Returns `Vec<PathBuf>` of matching files.
79pub fn walk_files(dir: &Path, extensions: &[&str]) -> Vec<PathBuf> {
80    let mut results = Vec::new();
81    if !dir.is_dir() {
82        return results;
83    }
84    walk_dir_recursive(dir, extensions, &mut results);
85    results
86}
87
88fn walk_dir_recursive(dir: &Path, extensions: &[&str], results: &mut Vec<PathBuf>) {
89    if let Ok(entries) = std::fs::read_dir(dir) {
90        for entry in entries.flatten() {
91            let path = entry.path();
92            if path.is_dir() {
93                walk_dir_recursive(&path, extensions, results);
94            } else if let Some(ext) = path.extension() {
95                if extensions.iter().any(|e| ext == *e) {
96                    results.push(path);
97                }
98            }
99        }
100    }
101}
102
103#[cfg(test)]
104mod tests {
105    use super::*;
106
107    #[tokio::test]
108    async fn test_write_and_read_string() {
109        let dir = std::env::temp_dir().join("iris-io-test");
110        let path = dir.join("hello.txt");
111        write_string(&path, "Hello Iris!").await.unwrap();
112        let content = read_to_string(&path).await.unwrap();
113        assert_eq!(content, "Hello Iris!");
114        let _ = std::fs::remove_dir_all(&dir);
115    }
116
117    #[test]
118    fn test_resolve() {
119        let base = Path::new("/project/src");
120        let rel = Path::new("../main.rs");
121        let abs = Path::new("/absolute/path.rs");
122        assert!(resolve(base, rel).ends_with("main.rs"));
123        assert_eq!(resolve(base, abs), abs);
124    }
125
126    #[test]
127    fn test_walk_files() {
128        let files = walk_files(Path::new("."), &["rs"]);
129        assert!(!files.is_empty());
130    }
131}