voa/
utils.rs

1//! Utilities for library and CLI.
2
3use std::{
4    path::{Path, PathBuf},
5    str::FromStr,
6};
7
8use crate::Error;
9
10/// Directory or regular file.
11#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
12pub enum DirOrFileType {
13    /// A directory.
14    Dir,
15    /// A regular file.
16    File,
17}
18
19/// A path that is guaranteed to be a directory or regular file.
20///
21/// Wraps a [`PathBuf`] and a [`DirOrFileType`] which indicates whether a directory or regular file
22/// is targeted.
23#[derive(Clone, Debug, Eq, PartialEq)]
24pub struct DirOrFile {
25    path: PathBuf,
26    /// The type of path (either a directory or a regular file).
27    pub typ: DirOrFileType,
28}
29
30impl AsRef<Path> for DirOrFile {
31    fn as_ref(&self) -> &Path {
32        &self.path
33    }
34}
35
36impl TryFrom<PathBuf> for DirOrFile {
37    type Error = Error;
38
39    /// Creates a [`DirOrFile`] from [`PathBuf`].
40    ///
41    /// # Errors
42    ///
43    /// Returns an error if `value` represents neither a directory, nor a regular file.
44    fn try_from(value: PathBuf) -> Result<Self, Self::Error> {
45        if value.is_dir() {
46            Ok(Self {
47                path: value,
48                typ: DirOrFileType::Dir,
49            })
50        } else if value.is_file() {
51            Ok(Self {
52                path: value,
53                typ: DirOrFileType::File,
54            })
55        } else {
56            Err(crate::Error::PathIsNotDirOrFile { path: value })
57        }
58    }
59}
60
61impl TryFrom<&Path> for DirOrFile {
62    type Error = Error;
63
64    /// Creates a [`DirOrFile`] from [`Path`] reference.
65    ///
66    /// # Errors
67    ///
68    /// Returns an error if `value` represents neither a directory, nor a regular file.
69    fn try_from(value: &Path) -> Result<Self, Self::Error> {
70        Self::try_from(value.to_path_buf())
71    }
72}
73
74impl FromStr for DirOrFile {
75    type Err = Error;
76
77    /// Creates a [`DirOrFile`] from a string slice.
78    ///
79    /// # Errors
80    ///
81    /// Returns an error if `value` represents neither a directory, nor a regular file.
82    fn from_str(s: &str) -> Result<Self, Self::Err> {
83        Self::try_from(PathBuf::from(s))
84    }
85}
86
87/// A path that is guaranteed to be a regular file.
88#[derive(Clone, Debug, Eq, Hash, PartialEq)]
89pub struct RegularFile(PathBuf);
90
91impl AsRef<Path> for RegularFile {
92    /// Returns a reference to the wrapped path.
93    fn as_ref(&self) -> &Path {
94        &self.0
95    }
96}
97
98impl TryFrom<PathBuf> for RegularFile {
99    type Error = Error;
100
101    /// Creates a [`RegularFile`] from a [`PathBuf`].
102    ///
103    /// # Errors
104    ///
105    /// Returns an error if `value` does not represent a regular file.
106    fn try_from(value: PathBuf) -> Result<Self, Self::Error> {
107        if !value.is_file() {
108            return Err(Error::PathIsNotAFile { path: value });
109        }
110
111        Ok(Self(value))
112    }
113}
114
115impl FromStr for RegularFile {
116    type Err = Error;
117
118    /// Creates a [`RegularFile`] from a string slice.
119    ///
120    /// # Errors
121    ///
122    /// Returns an error if `s` does not represent a regular file.
123    fn from_str(s: &str) -> Result<Self, Self::Err> {
124        Self::try_from(PathBuf::from(s))
125    }
126}
127
128#[cfg(test)]
129mod tests {
130    use tempfile::{NamedTempFile, TempDir};
131    use testresult::TestResult;
132
133    use super::*;
134
135    #[test]
136    fn dir_or_file_from_str_is_dir() -> TestResult {
137        let temp = TempDir::new()?;
138        let Some(path) = temp.path().to_str() else {
139            return Err("Could not convert temporary dir to string slice".into());
140        };
141
142        let dir = DirOrFile::from_str(path)?;
143        assert_eq!(dir.typ, DirOrFileType::Dir);
144        assert_eq!(dir.as_ref(), temp.path());
145        Ok(())
146    }
147
148    #[test]
149    fn dir_or_file_from_str_is_file() -> TestResult {
150        let temp = NamedTempFile::new()?;
151        let Some(path) = temp.path().to_str() else {
152            return Err("Could not convert temporary dir to string slice".into());
153        };
154
155        let dir = DirOrFile::from_str(path)?;
156        assert_eq!(dir.typ, DirOrFileType::File);
157        assert_eq!(dir.as_ref(), temp.path());
158        Ok(())
159    }
160
161    #[test]
162    #[cfg(target_os = "linux")]
163    fn dir_or_file_from_str_fails_on_not_a_dir_or_a_file() -> TestResult {
164        let result = DirOrFile::from_str("/dev/urandom");
165        match result {
166            Ok(path) => {
167                return Err(format!(
168                    "Succeeded to create a DirOrFile from {path:?} but should have failed"
169                )
170                .into());
171            }
172            Err(Error::PathIsNotDirOrFile { .. }) => {}
173            Err(error) => {
174                return Err(format!(
175                    "Should have returned Error::PathIsNotDirOrFile, but returned: {error}"
176                )
177                .into());
178            }
179        }
180        Ok(())
181    }
182
183    #[test]
184    fn regular_file_from_str_succeeds() -> TestResult {
185        let temp = NamedTempFile::new()?;
186        let Some(path) = temp.path().to_str() else {
187            return Err("Could not convert temporary dir to string slice".into());
188        };
189
190        let file = RegularFile::from_str(path)?;
191        assert_eq!(file.as_ref(), temp.path());
192        Ok(())
193    }
194
195    #[test]
196    fn regular_file_from_str_fails_on_dir() -> TestResult {
197        let temp = TempDir::new()?;
198        let Some(path) = temp.path().to_str() else {
199            return Err("Could not convert temporary dir to string slice".into());
200        };
201
202        let result = RegularFile::from_str(path);
203        match result {
204            Ok(path) => {
205                return Err(format!(
206                    "Succeeded to create a RegularFile from {path:?} but should have failed"
207                )
208                .into());
209            }
210            Err(Error::PathIsNotAFile { .. }) => {}
211            Err(error) => {
212                return Err(format!(
213                    "Should have returned Error::PathIsNotAFile, but returned: {error}"
214                )
215                .into());
216            }
217        }
218        Ok(())
219    }
220}