Skip to main content

termdoc_core/
error.rs

1//! Errors and exit codes.
2//!
3//! The codes are fixed in docs/DESIGN.md ยง7 and verified by the CLI tests.
4
5use std::path::PathBuf;
6
7/// Process exit codes. `OK` is 0 even when warnings were emitted: a document rendered
8/// with one questionable table is still a success.
9pub mod exit {
10    pub const OK: i32 = 0;
11    pub const GENERIC: i32 = 1;
12    pub const USAGE: i32 = 2;
13    pub const UNSUPPORTED: i32 = 3;
14    pub const UNREADABLE: i32 = 4;
15    pub const PLUGIN: i32 = 5;
16    pub const INTERRUPTED: i32 = 130;
17}
18
19#[derive(Debug, thiserror::Error)]
20pub enum Error {
21    #[error("cannot read '{path}': {source}")]
22    Io {
23        path: PathBuf,
24        #[source]
25        source: std::io::Error,
26    },
27
28    #[error("{0}")]
29    Stdio(#[from] std::io::Error),
30
31    #[error("unsupported format: {0}")]
32    Unsupported(String),
33
34    #[error("corrupt document: {0}")]
35    Corrupt(String),
36
37    #[error("invalid encoding: {0}")]
38    Encoding(String),
39
40    #[error("{0}")]
41    Usage(String),
42
43    #[error("plugin '{name}' failed: {message}")]
44    Plugin { name: String, message: String },
45}
46
47impl Error {
48    /// The associated exit code. The single place where this is decided, so the CLI does
49    /// not end up with duplicated `match` arms that drift apart.
50    pub fn exit_code(&self) -> i32 {
51        match self {
52            Error::Usage(_) => exit::USAGE,
53            Error::Unsupported(_) => exit::UNSUPPORTED,
54            Error::Io { .. } | Error::Corrupt(_) | Error::Encoding(_) => exit::UNREADABLE,
55            Error::Plugin { .. } => exit::PLUGIN,
56            Error::Stdio(e) if e.kind() == std::io::ErrorKind::BrokenPipe => exit::OK,
57            Error::Stdio(_) => exit::GENERIC,
58        }
59    }
60
61    /// `true` when the error is simply "the consumer closed the pipe".
62    ///
63    /// That is not a failure: `termdoc doc.md | head -5` must finish silently.
64    pub fn is_broken_pipe(&self) -> bool {
65        matches!(self, Error::Stdio(e) if e.kind() == std::io::ErrorKind::BrokenPipe)
66    }
67}
68
69pub type Result<T> = std::result::Result<T, Error>;
70
71#[cfg(test)]
72mod tests {
73    use super::*;
74
75    #[test]
76    fn broken_pipe_is_not_a_failure() {
77        let e = Error::Stdio(std::io::Error::from(std::io::ErrorKind::BrokenPipe));
78        assert!(e.is_broken_pipe());
79        assert_eq!(e.exit_code(), exit::OK);
80    }
81
82    #[test]
83    fn exit_codes_match_the_design() {
84        assert_eq!(Error::Usage("x".into()).exit_code(), 2);
85        assert_eq!(Error::Unsupported("x".into()).exit_code(), 3);
86        assert_eq!(Error::Corrupt("x".into()).exit_code(), 4);
87        assert_eq!(
88            Error::Plugin {
89                name: "x".into(),
90                message: "y".into()
91            }
92            .exit_code(),
93            5
94        );
95    }
96}