From a629950d4d44fb1f4acbd19ba70845b4bdbb9fba Mon Sep 17 00:00:00 2001 From: Paul Campbell Date: Wed, 30 Oct 2024 08:52:42 +0000 Subject: [PATCH] feat(fs)!: new fluent API --- src/fs/like.rs | 23 ---- src/fs/mod.rs | 27 +---- src/fs/real.rs | 273 +++++++++++++++++++++++++++++++++++++----------- src/fs/temp.rs | 6 +- src/tests/fs.rs | 126 +++++++++++----------- 5 files changed, 286 insertions(+), 169 deletions(-) delete mode 100644 src/fs/like.rs diff --git a/src/fs/like.rs b/src/fs/like.rs deleted file mode 100644 index 19ae0d9..0000000 --- a/src/fs/like.rs +++ /dev/null @@ -1,23 +0,0 @@ -use crate::fs::DirItem; - -use super::Result; - -use std::path::{Path, PathBuf}; - -pub trait FileSystemLike { - fn base(&self) -> &Path; - - fn dir_create(&self, path: &Path) -> Result<()>; - fn dir_create_all(&self, path: &Path) -> Result<()>; - - /// Reads the items in a directory and returns them as an iterator. - fn dir_read(&self, path: &Path) -> Result>>>; - - fn file_read_to_string(&self, path: &Path) -> Result; - fn file_write(&self, path: &Path, contents: &str) -> Result<()>; - - fn path_exists(&self, path: &Path) -> Result; - fn path_is_dir(&self, path: &Path) -> Result; - fn path_is_file(&self, path: &Path) -> Result; - fn path_of(&self, path: PathBuf) -> Result; -} diff --git a/src/fs/mod.rs b/src/fs/mod.rs index e2b369b..9a49d5d 100644 --- a/src/fs/mod.rs +++ b/src/fs/mod.rs @@ -2,15 +2,14 @@ use std::path::PathBuf; use derive_more::From; -use crate::fs::like::FileSystemLike; - -mod like; mod real; mod temp; mod dir_item; pub use dir_item::DirItem; pub use dir_item::DirItemIterator; +use real::FileSystem; +use temp::TempFileSystem; #[derive(Debug, From, derive_more::Display)] pub enum Error { @@ -32,25 +31,9 @@ impl std::error::Error for Error {} pub type Result = core::result::Result; pub const fn new(base: PathBuf) -> FileSystem { - FileSystem::Real(real::new(base)) + real::new(base) } -pub fn temp() -> Result { - temp::new().map(FileSystem::Temp) -} - -#[derive(Clone, Debug)] -pub enum FileSystem { - Real(real::RealFileSystem), - Temp(temp::TempFileSystem), -} -impl std::ops::Deref for FileSystem { - type Target = dyn FileSystemLike; - - fn deref(&self) -> &Self::Target { - match self { - Self::Real(fs) => fs, - Self::Temp(fs) => fs.deref(), - } - } +pub fn temp() -> Result { + temp::new() } diff --git a/src/fs/real.rs b/src/fs/real.rs index 52c8172..ca8a920 100644 --- a/src/fs/real.rs +++ b/src/fs/real.rs @@ -1,79 +1,44 @@ -use std::path::{Path, PathBuf}; +use std::{ + fmt::Display, + path::{Path, PathBuf}, +}; -use crate::fs::{DirItem, DirItemIterator}; +use crate::fs::DirItem; -pub const fn new(base: PathBuf) -> RealFileSystem { - RealFileSystem { base } +use super::{DirItemIterator, Result}; + +pub const fn new(base: PathBuf) -> FileSystem { + FileSystem { base } } #[derive(Clone, Debug)] -pub struct RealFileSystem { +pub struct FileSystem { base: PathBuf, } - -impl super::FileSystemLike for RealFileSystem { - fn base(&self) -> &Path { +impl FileSystem { + pub fn base(&self) -> &Path { &self.base } - fn dir_create(&self, path: &Path) -> super::Result<()> { - self.validate(path)?; - std::fs::create_dir(path).map_err(Into::into) - } - - fn dir_create_all(&self, path: &Path) -> super::Result<()> { - self.validate(path)?; - std::fs::create_dir_all(path).map_err(Into::into) - } - - fn dir_read( - &self, - path: &Path, - ) -> super::Result>>> { - self.validate(path)?; - if !self.path_is_dir(path)? { - return Err(super::Error::NotADirectory { - path: path.to_path_buf(), - }); - } - let read_dir = std::fs::read_dir(path)?; - Ok(Box::new(DirItemIterator::new(read_dir))) - } - - fn file_read_to_string(&self, path: &Path) -> super::Result { - self.validate(path)?; - std::fs::read_to_string(path).map_err(Into::into) - } - - fn file_write(&self, path: &Path, contents: &str) -> super::Result<()> { - self.validate(path)?; - std::fs::write(path, contents).map_err(Into::into) - } - - fn path_exists(&self, path: &Path) -> super::Result { - self.validate(path)?; - Ok(path.exists()) - } - - fn path_is_dir(&self, path: &Path) -> super::Result { - self.validate(path)?; - Ok(path.is_dir()) - } - - fn path_is_file(&self, path: &Path) -> super::Result { - self.validate(path)?; - Ok(path.is_file()) - } - - fn path_of(&self, path: PathBuf) -> super::Result { + pub fn path_of(&self, path: PathBuf) -> Result { let path_of = self.base.as_path().join(path); self.validate(&path_of)?; Ok(path_of) } -} -impl RealFileSystem { - fn validate(&self, path: &Path) -> super::Result<()> { + pub fn dir<'base, 'path>(&'base self, path: &'path Path) -> DirReal<'base, 'path> { + DirReal::new(self.base(), path) + } + + pub fn file<'base, 'path>(&'base self, path: &'path Path) -> FileReal<'base, 'path> { + FileReal::new(self.base(), path) + } + + pub fn path<'base, 'path>(&'base self, path: &'path Path) -> PathReal<'base, 'path> { + PathReal::new(self.base(), path) + } + + fn validate(&self, path: &Path) -> Result<()> { let path = self.clean_path(path)?; if !path.starts_with(&self.base) { return Err(super::Error::PathTraversal { @@ -84,7 +49,7 @@ impl RealFileSystem { Ok(()) } - fn clean_path(&self, path: &Path) -> super::Result { + fn clean_path(&self, path: &Path) -> Result { // let path = path.as_ref(); use path_clean::PathClean; let abs_path = if path.is_absolute() { @@ -96,3 +61,187 @@ impl RealFileSystem { Ok(abs_path) } } + +pub struct DirReal<'base, 'path> { + path: PathReal<'base, 'path>, +} +impl<'base, 'path> DirReal<'base, 'path> { + fn new(base: &'base Path, path: &'path Path) -> Self { + let mut path = PathReal::new(base, path); + if path.error.is_none() { + if let Ok(exists) = path.exists() { + if exists { + if let Ok(is_dir) = path.is_dir() { + if !is_dir { + path.put(super::Error::NotADirectory { + path: path.full_path(), + }) + } + } + } + } + } + Self { path } + } + + pub fn create(&mut self) -> Result<()> { + self.path.check_error()?; + std::fs::create_dir(self.path.full_path()).map_err(Into::into) + } + + pub fn create_all(&mut self) -> Result<()> { + self.path.check_error()?; + std::fs::create_dir_all(self.path.full_path()).map_err(Into::into) + } + + pub fn read(&mut self) -> Result>>> { + self.path.check_error()?; + let read_dir = std::fs::read_dir(self.path.full_path())?; + Ok(Box::new(DirItemIterator::new(read_dir))) + } + + pub fn exists(&mut self) -> Result { + self.path.check_error()?; + self.path.exists() + } + + pub fn is_dir(&mut self) -> Result { + self.path.check_error()?; + Ok(true) + } + + pub fn is_file(&mut self) -> Result { + self.path.check_error()?; + Ok(false) + } +} +pub struct FileReal<'base, 'path> { + path: PathReal<'base, 'path>, +} +impl<'base, 'path> FileReal<'base, 'path> { + fn new(base: &'base Path, path: &'path Path) -> Self { + Self { + path: PathReal::new(base, path), + } + } + + pub fn reader(&mut self) -> Result { + self.path.check_error()?; + ReaderReal::new(&self.path.full_path()) + } + + pub fn write(&mut self, contents: &str) -> Result<()> { + self.path.check_error()?; + std::fs::write(self.path.full_path(), contents).map_err(Into::into) + } + + pub fn exists(&mut self) -> Result { + self.path.check_error()?; + self.path.exists() + } + + pub fn is_dir(&mut self) -> Result { + self.path.check_error()?; + Ok(false) + } + + pub fn is_file(&mut self) -> Result { + self.path.check_error()?; + Ok(true) + } +} +#[derive(Debug)] +pub struct PathReal<'base, 'path> { + base: &'base Path, + path: &'path Path, + error: Option, +} +impl<'base, 'path> PathReal<'base, 'path> { + fn full_path(&self) -> PathBuf { + self.base.join(self.path) + } + fn put(&mut self, error: super::Error) { + if self.error.is_none() { + self.error.replace(error); + } + } + fn validate(base: &Path, path: &Path) -> Option { + match PathReal::clean_path(path) { + Err(error) => Some(error), + Ok(path) => { + if !path.starts_with(base) { + return Some(super::Error::PathTraversal { + base: base.to_path_buf(), + path, + }); + } + None + } + } + } + + fn clean_path(path: &Path) -> Result { + // let path = path.as_ref(); + use path_clean::PathClean; + let abs_path = if path.is_absolute() { + path.to_path_buf() + } else { + std::env::current_dir().expect("current_dir").join(path) + } + .clean(); + Ok(abs_path) + } + + fn new(base: &'base Path, path: &'path Path) -> Self { + Self { + base, + path, + error: PathReal::validate(base, path), + } + } + + pub fn exists(&mut self) -> Result { + self.check_error()?; + Ok(self.full_path().exists()) + } + + fn check_error(&mut self) -> Result<()> { + if let Some(error) = self.error.take() { + return Err(error); + } + Ok(()) + } + + pub fn is_dir(&mut self) -> Result { + self.check_error()?; + Ok(self.full_path().is_dir()) + } + + pub fn is_file(&mut self) -> Result { + self.check_error()?; + Ok(self.full_path().is_file()) + } +} +impl From> for PathBuf { + fn from(path: PathReal) -> Self { + path.base.join(path.path) + } +} +pub struct ReaderReal { + contents: String, +} +impl ReaderReal { + fn new(path: &Path) -> Result { + let contents = std::fs::read_to_string(path)?; + Ok(Self { contents }) + } + + pub fn as_str(&self) -> &str { + &self.contents + } +} +impl Display for ReaderReal { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.contents) + } +} diff --git a/src/fs/temp.rs b/src/fs/temp.rs index 5e1119b..eedd2b1 100644 --- a/src/fs/temp.rs +++ b/src/fs/temp.rs @@ -2,6 +2,8 @@ use std::sync::{Arc, Mutex}; use tempfile::TempDir; +use super::real::FileSystem; + pub(super) fn new() -> super::Result { let temp_dir = tempfile::tempdir()?; let base = temp_dir.path().to_path_buf(); @@ -16,12 +18,12 @@ pub(super) fn new() -> super::Result { #[derive(Clone, Debug)] pub struct TempFileSystem { - real: super::real::RealFileSystem, + real: FileSystem, _temp_dir: Arc>, } impl std::ops::Deref for TempFileSystem { - type Target = dyn super::FileSystemLike; + type Target = FileSystem; fn deref(&self) -> &Self::Target { &self.real diff --git a/src/tests/fs.rs b/src/tests/fs.rs index a5a6cdf..dcd8e26 100644 --- a/src/tests/fs.rs +++ b/src/tests/fs.rs @@ -6,9 +6,10 @@ type TestResult = Result<(), fs::Error>; mod path_of { use super::*; + #[test] fn validate_fails_on_path_traversal() -> TestResult { - let fs = fs::temp()?; + let fs = fs::temp().expect("temp fs"); let_assert!(Err(fs::Error::PathTraversal { base, path: _path }) = fs.path_of("..".into())); assert_eq!(base, fs.base()); @@ -18,10 +19,12 @@ mod path_of { #[test] fn matches_joins() -> TestResult { - let fs = fs::temp()?; + let fs = fs::temp().expect("temp fs"); let joined = fs.base().join("foo").join("bar"); - let path_of = fs.path_of("foo/bar".into())?; + let path_of = fs + .path_of("foo/bar".into()) + .expect("parse foo/bar into path"); assert_eq!(joined, path_of); @@ -31,23 +34,23 @@ mod path_of { mod file { use super::*; + #[test] /// Write to a file, read it, verify it exists, is a file and has the expected contents fn write_read_file_exists() -> TestResult { - let fs = fs::temp()?; + let fs = fs::temp().expect("temp fs"); let pathbuf = fs.base().join("foo"); - let_assert!(Ok(_) = fs.file_write(&pathbuf, "content")); - let_assert!( - Ok(c) = fs.file_read_to_string(&pathbuf), - "file_read_to_string" - ); + let mut file = fs.file(&pathbuf); + file.write("content").expect("write"); + let c = file.reader().expect("reader").to_string(); assert_eq!(c, "content"); - let_assert!(Ok(exists) = fs.path_exists(&pathbuf)); + let mut path = fs.path(&pathbuf); + let exists = path.exists().expect("exists"); assert!(exists); - let_assert!(Ok(is_file) = fs.path_is_file(&pathbuf)); + let is_file = path.is_file().expect("is_file"); assert!(is_file); Ok(()) @@ -58,28 +61,28 @@ mod dir_create { use super::*; #[test] fn should_create_a_dir() -> TestResult { - let fs = fs::temp()?; + let fs = fs::temp().expect("temp fs"); let pathbuf = fs.base().join("subdir"); - let_assert!(Ok(_) = fs.dir_create(&pathbuf)); + fs.dir(&pathbuf).create().expect("create"); - let_assert!(Ok(exists) = fs.path_exists(&pathbuf)); + let exists = fs.path(&pathbuf).exists().expect("exitss"); assert!(exists); - let_assert!(Ok(is_dir) = fs.path_is_dir(&pathbuf)); + let is_dir = fs.path(&pathbuf).is_dir().expect("is dir"); assert!(is_dir); Ok(()) } #[test] fn should_fail_on_path_traversal() -> TestResult { - let fs = fs::temp()?; + let fs = fs::temp().expect("temp fs"); let path = fs.base().join("..").join("foo"); let_assert!( Err(fs::Error::PathTraversal { base: _base, path: _path - }) = fs.dir_create(&path) + }) = fs.dir(&path).create() ); Ok(()) @@ -91,28 +94,29 @@ mod dir_create_all { #[test] fn should_create_a_dir() -> TestResult { - let fs = fs::temp()?; + let fs = fs::temp().expect("temp fs"); let pathbuf = fs.base().join("subdir").join("child"); - let_assert!(Ok(_) = fs.dir_create_all(&pathbuf)); + fs.dir(&pathbuf).create_all().expect("create_all"); - let_assert!(Ok(exists) = fs.path_exists(&pathbuf)); + let mut path = fs.path(&pathbuf); + let exists = path.exists().expect("exists"); assert!(exists, "path exists"); - let_assert!(Ok(is_dir) = fs.path_is_dir(&pathbuf)); + let is_dir = path.is_dir().expect("is_dir"); assert!(is_dir, "path is a directory"); Ok(()) } #[test] fn should_fail_on_path_traversal() -> TestResult { - let fs = fs::temp()?; + let fs = fs::temp().expect("temp fs"); let path = fs.base().join("..").join("foo"); let_assert!( Err(fs::Error::PathTraversal { base: _base, path: _path - }) = fs.dir_create_all(&path) + }) = fs.dir(&path).create_all() ); Ok(()) @@ -126,16 +130,18 @@ mod dir_dir_read { #[test] fn should_return_dir_items() -> TestResult { - let fs = fs::temp()?; + let fs = fs::temp().expect("temp fs"); let file1 = fs.base().join("file-1"); let dir = fs.base().join("dir"); let file2 = dir.join("file-2"); - fs.file_write(&file1, "file-1")?; - fs.dir_create(&dir)?; - fs.file_write(&file2, "file-2")?; + fs.file(&file1).write("file-1").expect("write: file-1"); + fs.dir(&dir).create().expect("create dir"); + fs.file(&file2).write("file-2").expect("write: file-2"); let items = fs - .dir_read(fs.base())? + .dir(fs.base()) + .read() + .expect("dir.read") .filter_map(|i| i.ok()) .collect::>(); @@ -147,23 +153,23 @@ mod dir_dir_read { } #[test] fn should_fail_on_not_a_dir() -> TestResult { - let fs = fs::temp()?; + let fs = fs::temp().expect("temp fs"); let path = fs.base().join("file"); - fs.file_write(&path, "contents")?; + fs.file(&path).write("contents").expect("write"); - let_assert!(Err(fs::Error::NotADirectory { path: _path }) = fs.dir_read(&path)); + let_assert!(Err(fs::Error::NotADirectory { path: _path }) = fs.dir(&path).read()); Ok(()) } #[test] fn should_fail_on_path_traversal() -> TestResult { - let fs = fs::temp()?; + let fs = fs::temp().expect("temp fs"); let path = fs.base().join("..").join("foo"); let_assert!( Err(fs::Error::PathTraversal { base: _base, path: _path - }) = fs.dir_read(&path) + }) = fs.dir(&path).read() ); Ok(()) @@ -174,32 +180,32 @@ mod path_exists { use super::*; #[test] fn should_be_true_when_it_exists() -> TestResult { - let fs = fs::temp()?; + let fs = fs::temp().expect("temp fs"); let path = fs.base().join("foo"); - let_assert!(Ok(_) = fs.file_write(&path, "bar")); - let_assert!(Ok(exists) = fs.path_exists(&path)); + fs.file(&path).write("bar").expect("write"); + let exists = fs.path(&path).exists().expect("exists"); assert!(exists); Ok(()) } #[test] fn should_be_false_when_it_does_not_exist() -> TestResult { - let fs = fs::temp()?; + let fs = fs::temp().expect("temp fs"); let path = fs.base().join("foo"); - let_assert!(Ok(exists) = fs.path_exists(&path)); + let exists = fs.path(&path).exists().expect("exists"); assert!(!exists); Ok(()) } #[test] fn should_fail_on_path_traversal() -> TestResult { - let fs = fs::temp()?; + let fs = fs::temp().expect("temp fs"); let path = fs.base().join("..").join("foo"); let_assert!( Err(fs::Error::PathTraversal { base: _base, path: _path - }) = fs.path_exists(&path) + }) = fs.path(&path).exists() ); Ok(()) @@ -210,20 +216,20 @@ mod path_is_dir { use super::*; #[test] fn should_be_true_when_is_a_dir() -> TestResult { - let fs = fs::temp()?; + let fs = fs::temp().expect("temp fs"); let path = fs.base().join("foo"); - let_assert!(Ok(_) = fs.dir_create(&path)); - let_assert!(Ok(is_dir) = fs.path_is_dir(&path)); + fs.dir(&path).create().expect("create"); + let is_dir = fs.path(&path).is_dir().expect("is_dir"); assert!(is_dir); Ok(()) } #[test] fn should_be_false_when_is_a_file() -> TestResult { - let fs = fs::temp()?; + let fs = fs::temp().expect("temp fs"); let path = fs.base().join("foo"); - let_assert!(Ok(_) = fs.file_write(&path, "bar")); - let_assert!(Ok(is_dir) = fs.path_is_dir(&path)); + fs.file(&path).write("bar").expect("write"); + let is_dir = fs.path(&path).is_dir().expect("is_dir"); assert!(!is_dir); Ok(()) @@ -231,24 +237,24 @@ mod path_is_dir { #[test] #[ignore] fn should_be_false_when_is_a_link() -> TestResult { - let fs = fs::temp()?; + let fs = fs::temp().expect("temp fs"); let path = fs.base().join("foo"); // TODO: (#38) create a link // let_assert!(Ok(_) = fs.file_write(&path, "bar")); - let_assert!(Ok(is_dir) = fs.path_is_dir(&path)); + let is_dir = fs.path(&path).is_dir().expect("is_dir"); assert!(!is_dir); Ok(()) } #[test] fn should_fail_on_path_traversal() -> TestResult { - let fs = fs::temp()?; + let fs = fs::temp().expect("temp fs"); let path = fs.base().join("..").join("foo"); let_assert!( Err(fs::Error::PathTraversal { base: _base, path: _path - }) = fs.path_is_dir(&path) + }) = fs.dir(&path).is_dir() ); Ok(()) @@ -259,21 +265,21 @@ mod path_is_file { use super::*; #[test] fn should_be_true_when_is_a_file() -> TestResult { - let fs = fs::temp()?; + let fs = fs::temp().expect("temp fs"); let path = fs.base().join("foo"); - let_assert!(Ok(_) = fs.file_write(&path, "bar")); + fs.file(&path).write("bar").expect("write"); - let_assert!(Ok(is_file) = fs.path_is_file(&path)); + let is_file = fs.path(&path).is_file().expect("is_file"); assert!(is_file); Ok(()) } #[test] fn should_be_false_when_is_a_dir() -> TestResult { - let fs = fs::temp()?; + let fs = fs::temp().expect("temp fs"); let path = fs.base().join("foo"); - let_assert!(Ok(_) = fs.dir_create(&path)); - let_assert!(Ok(is_file) = fs.path_is_file(&path)); + fs.dir(&path).create().expect("create"); + let is_file = fs.path(&path).is_file().expect("is_file"); assert!(!is_file); Ok(()) @@ -281,24 +287,24 @@ mod path_is_file { #[test] #[ignore] fn should_be_false_when_is_a_link() -> TestResult { - let fs = fs::temp()?; + let fs = fs::temp().expect("temp fs"); let path = fs.base().join("foo"); // TODO: (#38) create a link // let_assert!(Ok(_) = fs.file_write(&path, "bar")); - let_assert!(Ok(is_file) = fs.path_is_file(&path)); + let is_file = fs.path(&path).is_file().expect("is_file"); assert!(!is_file); Ok(()) } #[test] fn should_fail_on_path_traversal() -> TestResult { - let fs = fs::temp()?; + let fs = fs::temp().expect("temp fs"); let path = fs.base().join("..").join("foo"); let_assert!( Err(fs::Error::PathTraversal { base: _base, path: _path - }) = fs.path_is_file(&path) + }) = fs.file(&path).is_file() ); Ok(())