iris-core 1.1.3

Iris engine core: cross-platform windowing, async runtime, memory pool, IO and networking
Documentation
//! Async file I/O utilities built on Tokio.
//!
//! Provides convenience functions for reading/writing files in an async
//! context, path resolution helpers, and a file change detector.

use std::path::{Path, PathBuf};
use tokio::fs;
use tokio::io::{AsyncReadExt, AsyncWriteExt};

/// Read an entire file as a string (async).
///
/// Returns `Ok(content)` or an error message.
pub async fn read_to_string<P: AsRef<Path>>(path: P) -> Result<String, String> {
    let path = path.as_ref();
    let mut file = fs::File::open(path)
        .await
        .map_err(|e| format!("Failed to open {}: {}", path.display(), e))?;
    let mut contents = String::new();
    file.read_to_string(&mut contents)
        .await
        .map_err(|e| format!("Failed to read {}: {}", path.display(), e))?;
    Ok(contents)
}

/// Read an entire file as bytes (async).
pub async fn read_bytes<P: AsRef<Path>>(path: P) -> Result<Vec<u8>, String> {
    let path = path.as_ref();
    fs::read(path)
        .await
        .map_err(|e| format!("Failed to read {}: {}", path.display(), e))
}

/// Write a string to a file (async), creating parent directories if needed.
pub async fn write_string<P: AsRef<Path>>(path: P, content: &str) -> Result<(), String> {
    let path = path.as_ref();
    if let Some(parent) = path.parent() {
        fs::create_dir_all(parent)
            .await
            .map_err(|e| format!("Failed to create dir {}: {}", parent.display(), e))?;
    }
    let mut file = fs::File::create(path)
        .await
        .map_err(|e| format!("Failed to create {}: {}", path.display(), e))?;
    file.write_all(content.as_bytes())
        .await
        .map_err(|e| format!("Failed to write {}: {}", path.display(), e))
}

/// Write bytes to a file (async), creating parent directories if needed.
pub async fn write_bytes<P: AsRef<Path>>(path: P, data: &[u8]) -> Result<(), String> {
    let path = path.as_ref();
    if let Some(parent) = path.parent() {
        fs::create_dir_all(parent)
            .await
            .map_err(|e| format!("Failed to create dir {}: {}", parent.display(), e))?;
    }
    fs::write(path, data)
        .await
        .map_err(|e| format!("Failed to write {}: {}", path.display(), e))
}

/// Check if a path exists.
pub async fn exists<P: AsRef<Path>>(path: P) -> bool {
    fs::try_exists(path).await.unwrap_or(false)
}

/// Resolve a relative path against a base directory, canonicalizing the result.
pub fn resolve(base: &Path, relative: &Path) -> PathBuf {
    if relative.is_absolute() {
        relative.to_path_buf()
    } else {
        base.join(relative)
    }
}

/// Walk a directory recursively, yielding all file paths matching `extensions`.
///
/// Returns `Vec<PathBuf>` of matching files.
pub fn walk_files(dir: &Path, extensions: &[&str]) -> Vec<PathBuf> {
    let mut results = Vec::new();
    if !dir.is_dir() {
        return results;
    }
    walk_dir_recursive(dir, extensions, &mut results);
    results
}

fn walk_dir_recursive(dir: &Path, extensions: &[&str], results: &mut Vec<PathBuf>) {
    if let Ok(entries) = std::fs::read_dir(dir) {
        for entry in entries.flatten() {
            let path = entry.path();
            if path.is_dir() {
                walk_dir_recursive(&path, extensions, results);
            } else if let Some(ext) = path.extension() {
                if extensions.iter().any(|e| ext == *e) {
                    results.push(path);
                }
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[tokio::test]
    async fn test_write_and_read_string() {
        let dir = std::env::temp_dir().join("iris-io-test");
        let path = dir.join("hello.txt");
        write_string(&path, "Hello Iris!").await.unwrap();
        let content = read_to_string(&path).await.unwrap();
        assert_eq!(content, "Hello Iris!");
        let _ = std::fs::remove_dir_all(&dir);
    }

    #[test]
    fn test_resolve() {
        let base = Path::new("/project/src");
        let rel = Path::new("../main.rs");
        let abs = Path::new("/absolute/path.rs");
        assert!(resolve(base, rel).ends_with("main.rs"));
        assert_eq!(resolve(base, abs), abs);
    }

    #[test]
    fn test_walk_files() {
        let files = walk_files(Path::new("."), &["rs"]);
        assert!(!files.is_empty());
    }
}