joatmon/fs/
write.rs

1// Copyright (c) 2023 Richard Cook
2//
3// Permission is hereby granted, free of charge, to any person obtaining
4// a copy of this software and associated documentation files (the
5// "Software"), to deal in the Software without restriction, including
6// without limitation the rights to use, copy, modify, merge, publish,
7// distribute, sublicense, and/or sell copies of the Software, and to
8// permit persons to whom the Software is furnished to do so, subject to
9// the following conditions:
10//
11// The above copyright notice and this permission notice shall be
12// included in all copies or substantial portions of the Software.
13//
14// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21//
22use crate::error::HasOtherError;
23use anyhow::Error as AnyhowError;
24use std::error::Error as StdError;
25use std::fmt::{Debug, Display};
26use std::fs::{create_dir_all, write, File, OpenOptions};
27use std::io::{Error as IOError, Write};
28use std::path::{Path, PathBuf};
29use std::result::Result as StdResult;
30use thiserror::Error;
31
32#[allow(unused)]
33#[derive(Debug, PartialEq)]
34#[non_exhaustive]
35pub enum FileWriteErrorKind {
36    AlreadyExists,
37    Other,
38}
39
40#[derive(Debug, Error)]
41#[error(transparent)]
42pub struct FileWriteError(#[from] FileWriteErrorImpl);
43
44impl FileWriteError {
45    #[allow(unused)]
46    #[must_use]
47    pub const fn kind(&self) -> FileWriteErrorKind {
48        match self.0 {
49            FileWriteErrorImpl::AlreadyExists(_) => FileWriteErrorKind::AlreadyExists,
50            _ => FileWriteErrorKind::Other,
51        }
52    }
53
54    #[allow(unused)]
55    #[must_use]
56    pub fn is_already_exists(&self) -> bool {
57        self.kind() == FileWriteErrorKind::AlreadyExists
58    }
59
60    #[allow(unused)]
61    #[must_use]
62    pub fn is_other(&self) -> bool {
63        self.kind() == FileWriteErrorKind::Other
64    }
65
66    fn other<E>(e: E) -> Self
67    where
68        E: StdError + Send + Sync + 'static,
69    {
70        Self(FileWriteErrorImpl::Other(AnyhowError::new(e)))
71    }
72
73    fn convert(e: IOError, path: &Path) -> Self {
74        use std::io::ErrorKind::*;
75        match e.kind() {
76            AlreadyExists => Self(FileWriteErrorImpl::AlreadyExists(path.to_path_buf())),
77            _ => Self::other(e),
78        }
79    }
80}
81
82impl HasOtherError for FileWriteError {
83    fn is_other(&self) -> bool {
84        self.is_other()
85    }
86
87    fn downcast_other_ref<E>(&self) -> Option<&E>
88    where
89        E: Display + Debug + Send + Sync + 'static,
90    {
91        if let FileWriteErrorImpl::Other(ref inner) = self.0 {
92            inner.downcast_ref::<E>()
93        } else {
94            None
95        }
96    }
97}
98
99#[derive(Debug, Error)]
100enum FileWriteErrorImpl {
101    #[error("File {0} already exists")]
102    AlreadyExists(PathBuf),
103    #[error(transparent)]
104    Other(AnyhowError),
105}
106
107#[allow(unused)]
108pub fn safe_create_file(path: &Path, overwrite: bool) -> StdResult<File, FileWriteError> {
109    ensure_dir(path)?;
110
111    let mut options = OpenOptions::new();
112    options.write(true);
113    if overwrite {
114        options.create(true);
115    } else {
116        options.create_new(true);
117    }
118
119    options
120        .open(path)
121        .map_err(|e| FileWriteError::convert(e, path))
122}
123
124#[allow(unused)]
125pub fn safe_write_file<C>(
126    path: &Path,
127    contents: C,
128    overwrite: bool,
129) -> StdResult<(), FileWriteError>
130where
131    C: AsRef<[u8]>,
132{
133    ensure_dir(path)?;
134
135    if overwrite {
136        write(path, contents).map_err(|e| FileWriteError::convert(e, path))?;
137    } else {
138        let mut file = safe_create_file(path, overwrite)?;
139        file.write_all(contents.as_ref())
140            .map_err(|e| FileWriteError::convert(e, path))?;
141    }
142
143    Ok(())
144}
145
146fn ensure_dir(file_path: &Path) -> StdResult<(), FileWriteError> {
147    let mut dir = PathBuf::new();
148    dir.push(file_path);
149    dir.pop();
150    create_dir_all(&dir).map_err(FileWriteError::other)?;
151    Ok(())
152}
153
154#[cfg(test)]
155mod tests {
156    use super::{safe_create_file, safe_write_file, FileWriteErrorKind};
157    use anyhow::Result;
158    use std::fs::{read_to_string, write};
159    use std::io::Write;
160    use tempdir::TempDir;
161
162    #[test]
163    fn test_safe_create_file_no_overwrite_succeeds() -> Result<()> {
164        // Arrange
165        let temp_dir = TempDir::new("joatmon-test")?;
166        let path = temp_dir.path().join("file.txt");
167
168        // Act
169        let mut file = safe_create_file(&path, false)?;
170        file.write_all(b"hello-world")?;
171
172        // Assert
173        assert_eq!("hello-world", read_to_string(&path)?);
174        Ok(())
175    }
176
177    #[test]
178    fn test_safe_create_file_overwrite_succeeds() -> Result<()> {
179        // Arrange
180        let temp_dir = TempDir::new("joatmon-test")?;
181        let path = temp_dir.path().join("file.txt");
182
183        // Act
184        let mut file = safe_create_file(&path, true)?;
185        file.write_all(b"hello-world")?;
186
187        // Assert
188        assert_eq!("hello-world", read_to_string(&path)?);
189        Ok(())
190    }
191
192    #[test]
193    fn test_safe_create_file_exists_no_overwrite_fails() -> Result<()> {
194        // Arrange
195        let temp_dir = TempDir::new("joatmon-test")?;
196        let path = temp_dir.path().join("file.txt");
197        write(&path, "hello-world")?;
198
199        // Act
200        let e = match safe_create_file(&path, false) {
201            Ok(_) => panic!("safe_create_file must fail"),
202            Err(e) => e,
203        };
204
205        // Assert
206        assert_eq!(FileWriteErrorKind::AlreadyExists, e.kind());
207        assert!(e.is_already_exists());
208        assert!(!e.is_other());
209        let message = format!("{e}");
210        assert!(message.contains(path.to_str().expect("must be valid string")));
211        assert_eq!("hello-world", read_to_string(&path)?);
212        Ok(())
213    }
214
215    #[test]
216    fn test_safe_create_file_exists_overwrite_succeeds() -> Result<()> {
217        // Arrange
218        let temp_dir = TempDir::new("joatmon-test")?;
219        let path = temp_dir.path().join("file.txt");
220        write(&path, "hello-world")?;
221
222        // Act
223        let mut file = safe_create_file(&path, true)?;
224        file.write_all(b"something-else")?;
225
226        // Assert
227        assert_eq!("something-else", read_to_string(&path)?);
228        Ok(())
229    }
230
231    #[test]
232    fn test_safe_write_file_no_overwrite_succeeds() -> Result<()> {
233        // Arrange
234        let temp_dir = TempDir::new("joatmon-test")?;
235        let path = temp_dir.path().join("file.txt");
236
237        // Act
238        safe_write_file(&path, "hello-world", false)?;
239
240        // Assert
241        assert_eq!("hello-world", read_to_string(&path)?);
242        Ok(())
243    }
244
245    #[test]
246    fn test_safe_write_file_overwrite_succeeds() -> Result<()> {
247        // Arrange
248        let temp_dir = TempDir::new("joatmon-test")?;
249        let path = temp_dir.path().join("file.txt");
250
251        // Act
252        safe_write_file(&path, "hello-world", true)?;
253
254        // Assert
255        assert_eq!("hello-world", read_to_string(&path)?);
256        Ok(())
257    }
258
259    #[test]
260    fn test_safe_write_file_exists_no_overwrite_fails() -> Result<()> {
261        // Arrange
262        let temp_dir = TempDir::new("joatmon-test")?;
263        let path = temp_dir.path().join("file.txt");
264        write(&path, "hello-world")?;
265
266        // Act
267        let e = match safe_write_file(&path, "something-else", false) {
268            Ok(_) => panic!("safe_write_file must fail"),
269            Err(e) => e,
270        };
271
272        // Assert
273        assert_eq!(FileWriteErrorKind::AlreadyExists, e.kind());
274        assert!(e.is_already_exists());
275        assert!(!e.is_other());
276        let message = format!("{e}");
277        assert!(message.contains(path.to_str().expect("must be valid string")));
278        assert_eq!("hello-world", read_to_string(&path)?);
279        Ok(())
280    }
281
282    #[test]
283    fn test_safe_write_file_exists_overwrite_succeeds() -> Result<()> {
284        // Arrange
285        let temp_dir = TempDir::new("joatmon-test")?;
286        let path = temp_dir.path().join("file.txt");
287        write(&path, "hello-world")?;
288
289        // Act
290        safe_write_file(&path, "something-else", true)?;
291
292        // Assert
293        assert_eq!("something-else", read_to_string(&path)?);
294        Ok(())
295    }
296}