diff --git a/justfile b/justfile index a6a5136..38c5e65 100644 --- a/justfile +++ b/justfile @@ -1,8 +1,10 @@ build: + cargo fmt cargo fmt --check cargo hack --feature-powerset clippy cargo hack --feature-powerset build cargo hack --feature-powerset test + cargo doc install-hooks: @echo "Installing git hooks" diff --git a/src/fs/dir.rs b/src/fs/dir.rs index c65de01..9f45ac3 100644 --- a/src/fs/dir.rs +++ b/src/fs/dir.rs @@ -1,22 +1,90 @@ // use crate::fs::{DirItem, DirItemIterator, Result}; -use super::path::{DirG, PathReal}; +use super::{ + path::{DirG, PathReal}, + FileG, PathG, +}; impl<'base, 'path> PathReal<'base, 'path, DirG> { - pub fn create(&mut self) -> Result<()> { + /// Creates a new, empty directory at the path + /// + /// Wrapper for [std::fs::create_dir] + /// + /// ``` + /// # fn try_main() -> kxio::fs::Result<()> { + /// let fs = kxio::fs::temp()?; + /// let path = fs.base().join("foo"); + /// let dir = fs.dir(&path); + /// dir.create()?; + /// # Ok(()) + /// # } + /// ``` + pub fn create(&self) -> Result<()> { self.check_error()?; std::fs::create_dir(self.as_pathbuf()).map_err(Into::into) } - pub fn create_all(&mut self) -> Result<()> { + /// Recursively create a directory and all of its parent components if they are missing. + /// + /// Wrapper for [std::fs::create_dir_all] + /// + /// ``` + /// # fn try_main() -> kxio::fs::Result<()> { + /// let fs = kxio::fs::temp()?; + /// let path = fs.base().join("foo").join("bar"); + /// let dir = fs.dir(&path); + /// dir.create_all()?; + /// # Ok(()) + /// # } + /// ``` + pub fn create_all(&self) -> Result<()> { self.check_error()?; std::fs::create_dir_all(self.as_pathbuf()).map_err(Into::into) } - pub fn read(&mut self) -> Result>>> { + /// Returns an iterator over the entries within a directory. + /// + /// Wrapper for [std::fs::read_dir] + /// + /// ``` + /// # fn try_main() -> kxio::fs::Result<()> { + /// let fs = kxio::fs::temp()?; + /// let path = fs.base().join("foo").join("bar"); + /// let dir = fs.dir(&path); + /// for entry in dir.read()? { /* ... */ } + /// # Ok(()) + /// # } + /// ``` + pub fn read(&self) -> Result>>> { self.check_error()?; let read_dir = std::fs::read_dir(self.as_pathbuf())?; Ok(Box::new(DirItemIterator::new(read_dir))) } } +impl<'base, 'path> TryFrom> for PathReal<'base, 'path, FileG> { + type Error = crate::fs::Error; + + fn try_from(path: PathReal<'base, 'path, PathG>) -> std::result::Result { + match path.as_file() { + Ok(Some(dir)) => Ok(dir.clone()), + Ok(None) => Err(crate::fs::Error::NotADirectory { + path: path.as_pathbuf(), + }), + Err(err) => Err(err), + } + } +} +impl<'base, 'path> TryFrom> for PathReal<'base, 'path, DirG> { + type Error = crate::fs::Error; + + fn try_from(path: PathReal<'base, 'path, PathG>) -> std::result::Result { + match path.as_dir() { + Ok(Some(dir)) => Ok(dir.clone()), + Ok(None) => Err(crate::fs::Error::NotADirectory { + path: path.as_pathbuf(), + }), + Err(err) => Err(err), + } + } +} diff --git a/src/fs/file.rs b/src/fs/file.rs index af11770..957fe97 100644 --- a/src/fs/file.rs +++ b/src/fs/file.rs @@ -3,16 +3,40 @@ use crate::fs::Result; use super::{ path::{FileG, PathReal}, - reader::ReaderReal, + reader::Reader, }; impl<'base, 'path> PathReal<'base, 'path, FileG> { - pub fn reader(&mut self) -> Result { + /// Returns a [Reader] for the file. + /// + /// ``` + /// # fn try_main() -> kxio::fs::Result<()> { + /// let fs = kxio::fs::temp()?; + /// let path = fs.base().join("foo").join("bar"); + /// let file = fs.file(&path); + /// let reader = file.reader()?; + /// # Ok(()) + /// # } + /// ``` + pub fn reader(&self) -> Result { self.check_error()?; - ReaderReal::new(&self.as_pathbuf()) + Reader::new(&self.as_pathbuf()) } - pub fn write(&mut self, contents: &str) -> Result<()> { + /// Writes a slice as the entire contents of a file. + /// + /// Wrapper for [std::fs::write] + /// + /// ``` + /// # fn try_main() -> kxio::fs::Result<()> { + /// let fs = kxio::fs::temp()?; + /// let path = fs.base().join("foo").join("bar"); + /// let file = fs.file(&path); + /// file.write("new file contents")?; + /// # Ok(()) + /// # } + /// ``` + pub fn write>(&self, contents: C) -> Result<()> { self.check_error()?; std::fs::write(self.as_pathbuf(), contents).map_err(Into::into) } diff --git a/src/fs/mod.rs b/src/fs/mod.rs index 3d6df74..fc187c6 100644 --- a/src/fs/mod.rs +++ b/src/fs/mod.rs @@ -17,6 +17,8 @@ mod system; mod temp; pub use dir_item::{DirItem, DirItemIterator}; +pub use path::*; +pub use reader::Reader; pub use result::{Error, Result}; pub use system::FileSystem; diff --git a/src/fs/path.rs b/src/fs/path.rs index 0a5b6a4..dc3459c 100644 --- a/src/fs/path.rs +++ b/src/fs/path.rs @@ -8,16 +8,22 @@ use crate::fs::{Error, Result}; pub trait PathType {} +#[derive(Clone, Debug)] pub struct PathG; impl PathType for PathG {} +#[derive(Clone, Debug)] pub struct FileG; impl PathType for FileG {} +#[derive(Clone, Debug)] pub struct DirG; impl PathType for DirG {} -#[derive(Debug)] +/// Represents a path in the filesystem. +/// +/// It can be a simple path, or it can be a file or a directory. +#[derive(Clone, Debug)] pub struct PathReal<'base, 'path, T: PathType> { base: &'base Path, path: &'path Path, @@ -34,6 +40,18 @@ impl<'base, 'path, T: PathType> PathReal<'base, 'path, T> { } } + /// Returns a [PathBuf] for the path. + /// + /// ``` + /// # use kxio::fs::Result; + /// # fn main() -> Result<()> { + /// let fs = kxio::fs::temp()?; + /// let path = fs.base().join("foo"); + /// let path = fs.path(&path); + /// let pathbuf = path.as_pathbuf(); + /// # Ok(()) + /// # } + /// ``` pub fn as_pathbuf(&self) -> PathBuf { self.base.join(self.path) } @@ -71,29 +89,84 @@ impl<'base, 'path, T: PathType> PathReal<'base, 'path, T> { Ok(abs_path) } - pub(super) fn check_error(&mut self) -> Result<()> { - if let Some(error) = self.error.take() { - return Err(error); + pub(super) fn check_error(&self) -> Result<()> { + if let Some(error) = &self.error { + Err(error.clone()) + } else { + Ok(()) } - Ok(()) } - pub fn exists(&mut self) -> Result { + /// Returns true if the path exists. + /// + /// N.B. If you have the path used to create the file or directory, you + /// should use [std::path::Path::exists] instead. + /// + /// ``` + /// # use kxio::fs::Result; + /// # fn main() -> Result<()> { + /// let fs = kxio::fs::temp()?; + /// let path = fs.base().join("foo"); + /// let dir = fs.dir(&path); + /// # dir.create()?; + /// if dir.exists()? { /* ... */ } + /// # Ok(()) + /// # } + /// ``` + pub fn exists(&self) -> Result { self.check_error()?; Ok(self.as_pathbuf().exists()) } - pub fn is_dir(&mut self) -> Result { + /// Returns true if the path is a directory. + /// + /// ``` + /// # use kxio::fs::Result; + /// # fn main() -> Result<()> { + /// let fs = kxio::fs::temp()?; + /// let path = fs.base().join("foo"); + /// # fs.dir(&path).create()?; + /// if path.is_dir() { /* ... */ } + /// # Ok(()) + /// # } + /// ``` + pub fn is_dir(&self) -> Result { self.check_error()?; Ok(self.as_pathbuf().is_dir()) } - pub fn is_file(&mut self) -> Result { + /// Returns true if the path is a file. + /// + /// ``` + /// # use kxio::fs::Result; + /// # fn main() -> Result<()> { + /// let fs = kxio::fs::temp()?; + /// let path = fs.base().join("foo"); + /// # fs.dir(&path).create()?; + /// if path.is_file() { /* ... */ } + /// # Ok(()) + /// # } + /// ``` + pub fn is_file(&self) -> Result { self.check_error()?; Ok(self.as_pathbuf().is_file()) } - pub fn as_dir(&mut self) -> Result>> { + /// Returns the path as a directory if it exists and is a directory, otherwise + /// it will return None. + /// + /// ``` + /// # use kxio::fs::Result; + /// # fn main() -> Result<()> { + /// let fs = kxio::fs::temp()?; + /// let path = fs.base().join("foo"); + /// # fs.dir(&path).create()?; + /// let file = fs.path(&path); + /// if let Ok(Some(dir)) = file.as_dir() { /* ... */ } + /// # Ok(()) + /// # } + /// ``` + pub fn as_dir(&self) -> Result>> { self.check_error()?; if self.as_pathbuf().is_dir() { Ok(Some(PathReal::new(self.base, self.path))) @@ -102,7 +175,21 @@ impl<'base, 'path, T: PathType> PathReal<'base, 'path, T> { } } - pub fn as_file(&mut self) -> Result>> { + /// Returns the path as a file if it exists and is a file, otherwise + /// it will return None. + /// + /// ``` + /// # use kxio::fs::Result; + /// # fn main() -> Result<()> { + /// let fs = kxio::fs::temp()?; + /// let path = fs.base().join("foo"); + /// # fs.dir(&path).create()?; + /// let file = fs.path(&path); + /// if let Ok(Some(file)) = file.as_file() { /* ... */ } + /// # Ok(()) + /// # } + /// ``` + pub fn as_file(&self) -> Result>> { self.check_error()?; if self.as_pathbuf().is_file() { Ok(Some(PathReal::new(self.base, self.path))) diff --git a/src/fs/reader.rs b/src/fs/reader.rs index c601159..a9b4d46 100644 --- a/src/fs/reader.rs +++ b/src/fs/reader.rs @@ -3,24 +3,68 @@ use std::{fmt::Display, path::Path, str::Lines}; use crate::fs::Result; -pub struct ReaderReal { +/// A reader for a file. +pub struct Reader { contents: String, } -impl ReaderReal { +impl Reader { pub(super) fn new(path: &Path) -> Result { let contents = std::fs::read_to_string(path)?; Ok(Self { contents }) } + /// Returns the contents of the file as a string. + /// + /// ``` + /// # use kxio::fs::Result; + /// # fn main() -> Result<()> { + /// let fs = kxio::fs::temp()?; + /// let path = fs.base().join("foo"); + /// let file = fs.file(&path); + /// # file.write("new file contents")?; + /// let contents = file.reader()?.contents(); + /// # Ok(()) + /// # } + /// ``` + pub fn contents(&self) -> &str { + &self.contents + } + + /// Returns the contents of the file as a string. + /// + /// ``` + /// # use kxio::fs::Result; + /// # fn main() -> Result<()> { + /// let fs = kxio::fs::temp()?; + /// let path = fs.base().join("foo"); + /// let file = fs.file(&path); + /// # file.write("new file contents")?; + /// let contents = file.reader()?.as_str(); + /// # Ok(()) + /// # } + /// ``` pub fn as_str(&self) -> &str { &self.contents } + /// Returns an iterator over the lines in the file. + /// + /// ``` + /// # use kxio::fs::Result; + /// # fn main() -> Result<()> { + /// let fs = kxio::fs::temp()?; + /// let path = fs.base().join("foo"); + /// let file = fs.file(&path); + /// # file.write("new file contents")?; + /// let lines = file.reader()?.lines(); + /// # Ok(()) + /// # } + /// ``` pub fn lines(&self) -> Lines<'_> { self.contents.lines() } } -impl Display for ReaderReal { +impl Display for Reader { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.contents) } diff --git a/src/fs/result.rs b/src/fs/result.rs index 6629818..bd38f1d 100644 --- a/src/fs/result.rs +++ b/src/fs/result.rs @@ -12,6 +12,8 @@ use derive_more::From; pub enum Error { Io(std::io::Error), + IoString(String), + #[display("Path access attempted outside of base ({base:?}): {path:?}")] PathTraversal { base: PathBuf, @@ -24,6 +26,19 @@ pub enum Error { }, } impl std::error::Error for Error {} +impl Clone for Error { + fn clone(&self) -> Self { + match self { + Error::Io(err) => Error::IoString(err.to_string()), + Error::IoString(err) => Error::IoString(err.clone()), + Error::PathTraversal { base, path } => Error::PathTraversal { + base: base.clone(), + path: path.clone(), + }, + Error::NotADirectory { path } => Error::NotADirectory { path: path.clone() }, + } + } +} /// Represents a success or a failure. /// diff --git a/src/fs/system.rs b/src/fs/system.rs index 820bbca..1fbe9c8 100644 --- a/src/fs/system.rs +++ b/src/fs/system.rs @@ -14,6 +14,7 @@ impl FileSystem { pub const fn new(base: PathBuf) -> Self { Self { base } } + pub fn base(&self) -> &Path { &self.base } @@ -24,6 +25,7 @@ impl FileSystem { Ok(path_of) } + /// Access the path as a directory. pub fn dir<'base, 'path>(&'base self, path: &'path Path) -> PathReal<'base, 'path, DirG> { let mut dir = PathReal::new(self.base(), path); diff --git a/tests/fs.rs b/tests/fs.rs index 4e80f82..ddac636 100644 --- a/tests/fs.rs +++ b/tests/fs.rs @@ -40,7 +40,7 @@ mod path { let fs = fs::temp().expect("temp fs"); let path = fs.base().join("foo"); - let mut dir = fs.dir(&path); + let dir = fs.dir(&path); dir.create().expect("create"); let_assert!(Ok(Some(as_dir)) = fs.path(&path).as_dir()); @@ -55,10 +55,10 @@ mod path { let fs = fs::temp().expect("temp fs"); let path = fs.base().join("foo"); - let mut file = fs.file(&path); + let file = fs.file(&path); file.write("contents").expect("create"); - let_assert!(Ok(Some(mut as_file)) = fs.path(&path).as_file()); + let_assert!(Ok(Some(as_file)) = fs.path(&path).as_file()); assert_eq!(file.as_pathbuf(), as_file.as_pathbuf()); assert_eq!(as_file.reader().expect("reader").to_string(), "contents"); @@ -70,7 +70,7 @@ mod path { let fs = fs::temp().expect("temp fs"); let path = fs.base().join("foo"); - let mut dir = fs.dir(&path); + let dir = fs.dir(&path); dir.create().expect("create"); let_assert!(Ok(None) = fs.path(&path).as_file()); @@ -82,7 +82,7 @@ mod path { let fs = fs::temp().expect("temp fs"); let path = fs.base().join("foo"); - let mut file = fs.file(&path); + let file = fs.file(&path); file.write("contents").expect("create"); let_assert!(Ok(None) = fs.path(&path).as_dir()); @@ -100,12 +100,12 @@ mod file { let fs = fs::temp().expect("temp fs"); let pathbuf = fs.base().join("foo"); - let mut file = fs.file(&pathbuf); + let file = fs.file(&pathbuf); file.write("content").expect("write"); let c = file.reader().expect("reader").to_string(); assert_eq!(c, "content"); - let mut path = fs.path(&pathbuf); + let path = fs.path(&pathbuf); let exists = path.exists().expect("exists"); assert!(exists); @@ -120,7 +120,7 @@ mod file { let fs = fs::temp().expect("temp fs"); let path = fs.base().join("foo"); - let mut file = fs.file(&path); + let file = fs.file(&path); file.write("line 1\nline 2").expect("write"); let reader = file.reader().expect("reader"); @@ -174,7 +174,7 @@ mod dir_create_all { fs.dir(&pathbuf).create_all().expect("create_all"); - let mut path = fs.path(&pathbuf); + let path = fs.path(&pathbuf); let exists = path.exists().expect("exists"); assert!(exists, "path exists");