use std::io::{self, Write};
use std::path::Path;
pub trait FileSystem: Send + Sync {
fn read_to_string(&self, path: &Path) -> io::Result<String>;
fn write(&self, path: &Path, content: &str) -> io::Result<()>;
fn exists(&self, path: &Path) -> bool;
fn modified(&self, path: &Path) -> io::Result<std::time::SystemTime>;
fn create_file(&self, path: &Path) -> io::Result<Box<dyn Write + Send>>;
}
#[derive(Debug, Default, Clone, Copy)]
pub struct RealFs;
impl RealFs {
pub fn new() -> Self {
Self
}
}
impl FileSystem for RealFs {
fn read_to_string(&self, path: &Path) -> io::Result<String> {
std::fs::read_to_string(path)
}
fn write(&self, path: &Path, content: &str) -> io::Result<()> {
std::fs::write(path, content)
}
fn exists(&self, path: &Path) -> bool {
path.exists()
}
fn modified(&self, path: &Path) -> io::Result<std::time::SystemTime> {
std::fs::metadata(path)?.modified()
}
fn create_file(&self, path: &Path) -> io::Result<Box<dyn Write + Send>> {
let file = std::fs::File::create(path)?;
Ok(Box::new(io::BufWriter::new(file)))
}
}
pub fn default_fs() -> &'static RealFs {
static INSTANCE: RealFs = RealFs;
&INSTANCE
}
#[cfg(test)]
pub mod mock {
use super::*;
use std::collections::HashMap;
use std::sync::RwLock;
#[derive(Debug, Default)]
pub struct MockFs {
files: RwLock<HashMap<String, String>>,
}
impl MockFs {
pub fn new() -> Self {
Self {
files: RwLock::new(HashMap::new()),
}
}
pub fn with_files<I, P, C>(files: I) -> Self
where
I: IntoIterator<Item = (P, C)>,
P: AsRef<Path>,
C: Into<String>,
{
let map: HashMap<String, String> = files
.into_iter()
.map(|(p, c)| (p.as_ref().to_string_lossy().to_string(), c.into()))
.collect();
Self {
files: RwLock::new(map),
}
}
pub fn files(&self) -> HashMap<String, String> {
self.files.read().unwrap().clone()
}
}
impl FileSystem for MockFs {
fn read_to_string(&self, path: &Path) -> io::Result<String> {
let key = path.to_string_lossy().to_string();
self.files
.read()
.unwrap()
.get(&key)
.cloned()
.ok_or_else(|| {
io::Error::new(io::ErrorKind::NotFound, format!("file not found: {}", key))
})
}
fn write(&self, path: &Path, content: &str) -> io::Result<()> {
let key = path.to_string_lossy().to_string();
self.files.write().unwrap().insert(key, content.to_string());
Ok(())
}
fn exists(&self, path: &Path) -> bool {
let key = path.to_string_lossy().to_string();
self.files.read().unwrap().contains_key(&key)
}
fn modified(&self, _path: &Path) -> io::Result<std::time::SystemTime> {
Ok(std::time::SystemTime::now())
}
fn create_file(&self, path: &Path) -> io::Result<Box<dyn Write + Send>> {
let key = path.to_string_lossy().to_string();
self.files.write().unwrap().insert(key, String::new());
Ok(Box::new(Vec::new()))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_mock_fs_read_write() {
let fs = MockFs::new();
let path = Path::new("/test/file.txt");
assert!(!fs.exists(path));
assert!(fs.read_to_string(path).is_err());
fs.write(path, "hello world").unwrap();
assert!(fs.exists(path));
assert_eq!(fs.read_to_string(path).unwrap(), "hello world");
}
#[test]
fn test_mock_fs_with_files() {
let fs = MockFs::with_files([
(Path::new("/a.txt"), "content a"),
(Path::new("/b.txt"), "content b"),
]);
assert_eq!(fs.read_to_string(Path::new("/a.txt")).unwrap(), "content a");
assert_eq!(fs.read_to_string(Path::new("/b.txt")).unwrap(), "content b");
}
}
}