io_extra/
context.rs

1use std::{error::Error, fmt, io, iter};
2
3/// Lazily attach a message to an [`io::Error`].
4/// This is particularly useful combined with [`Result::map_err`].
5///
6/// ```
7/// use io_extra::with;
8/// # fn _main() -> Result<(), std::io::Error> {
9/// # use std::fs::File;
10/// # let path = "foo";
11/// File::open(path).map_err(with("failed to open log file"))?;
12/// # Ok(())
13/// # }
14/// ```
15pub fn with(context: impl fmt::Display) -> impl FnOnce(io::Error) -> io::Error {
16    |e| self::context(e, context)
17}
18
19/// Attach a message to this [`io::Error`].
20///
21/// This is provided as a free function to not conflict with [`anyhow::Context`]
22///
23/// [`anyhow::Context`]: (https://docs.rs/anyhow/1/anyhow/trait.Context.html#method.context).
24pub fn context(e: io::Error, context: impl fmt::Display) -> io::Error {
25    let kind = e.kind();
26    let stringified = e.to_string();
27    let source = match (
28        e.raw_os_error(),
29        stringified == kind.to_string(),
30        e.into_inner(),
31    ) {
32        // ErrorData::Custom
33        (_, _, Some(source)) => Some(source),
34        // ErrorData::Os
35        (Some(code), _, None) => Some(Box::new(io::Error::from_raw_os_error(code)) as _),
36        // ErrorData::Simple
37        (None, true, None) => None,
38        // ErrorData::SimpleMessage
39        (None, false, None) => Some(Box::new(SimpleMessage(stringified)) as _),
40    };
41    io::Error::new(
42        kind,
43        Context {
44            context: context.to_string(),
45            source,
46        },
47    )
48}
49
50#[derive(Debug)]
51struct SimpleMessage(String);
52impl fmt::Display for SimpleMessage {
53    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
54        self.0.fmt(f)
55    }
56}
57impl Error for SimpleMessage {}
58
59#[derive(Debug)]
60struct Context {
61    context: String,
62    source: Option<Box<dyn Error + Send + Sync + 'static>>,
63}
64impl Error for Context {
65    fn source(&self) -> Option<&(dyn Error + 'static)> {
66        match &self.source {
67            Some(it) => Some(it.as_ref()),
68            None => None,
69        }
70    }
71}
72impl fmt::Display for Context {
73    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
74        self.context.fmt(f)?;
75        if f.alternate() {
76            for parent in Chain::new(self.source()) {
77                write!(f, ": {}", parent)?
78            }
79        }
80        Ok(())
81    }
82}
83
84/// An iterator of [`Error::source`]s.
85#[derive(Debug)]
86struct Chain<'a> {
87    #[allow(clippy::type_complexity)]
88    inner: iter::Successors<&'a dyn Error, fn(&&'a dyn Error) -> Option<&'a dyn Error>>,
89}
90
91impl<'a> Chain<'a> {
92    fn new(root: Option<&'a dyn Error>) -> Self {
93        Self {
94            inner: iter::successors(root, |e| (*e).source()),
95        }
96    }
97}
98
99impl<'a> Iterator for Chain<'a> {
100    type Item = &'a dyn Error;
101
102    fn next(&mut self) -> Option<Self::Item> {
103        self.inner.next()
104    }
105}