use atomicwrites::{AtomicFile, DisallowOverwrite};
use camino::{Utf8Path, Utf8PathBuf};
use std::{
fmt::{self, Display},
io::{self, Write},
};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum WriteStage {
Write,
Persist,
}
#[derive(Debug)]
pub struct WriteError {
path: Utf8PathBuf,
stage: WriteStage,
source: io::Error,
}
impl WriteError {
fn new(path: &Utf8Path, error: atomicwrites::Error<io::Error>) -> Self {
let (stage, source) = match error {
atomicwrites::Error::Internal(source) => {
(WriteStage::Persist, source)
}
atomicwrites::Error::User(source) => (WriteStage::Write, source),
};
WriteError { path: path.to_owned(), stage, source }
}
pub fn path(&self) -> &Utf8Path {
&self.path
}
pub fn stage(&self) -> WriteStage {
self.stage
}
}
impl fmt::Display for WriteError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.stage {
WriteStage::Write => write!(
f,
"writing value to temporary file for port file {}",
self.path
),
WriteStage::Persist => write!(
f,
"creating, syncing, or renaming temporary file for port \
file {}",
self.path
),
}
}
}
impl std::error::Error for WriteError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
Some(&self.source)
}
}
pub fn write<T: Display>(path: &Utf8Path, value: T) -> Result<(), WriteError> {
AtomicFile::new(path, DisallowOverwrite)
.write(|f| writeln!(f, "{value}"))
.map_err(|error| WriteError::new(path, error))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn user_error_is_write_stage() {
let path = Utf8Path::new("port");
let error = atomicwrites::Error::User(io::Error::from(
io::ErrorKind::StorageFull,
));
let err = WriteError::new(path, error);
assert_eq!(err.path(), path);
assert_eq!(err.stage(), WriteStage::Write);
assert!(
err.to_string().contains("writing value to temporary file"),
"unexpected error: {err}"
);
assert_eq!(err.source.kind(), io::ErrorKind::StorageFull);
}
#[test]
fn internal_error_is_persist_stage() {
let path = Utf8Path::new("port");
let error = atomicwrites::Error::Internal(io::Error::from(
io::ErrorKind::PermissionDenied,
));
let err = WriteError::new(path, error);
assert_eq!(err.path(), path);
assert_eq!(err.stage(), WriteStage::Persist);
assert!(
err.to_string()
.contains("creating, syncing, or renaming temporary file"),
"unexpected error: {err}"
);
assert_eq!(err.source.kind(), io::ErrorKind::PermissionDenied);
}
}