Skip to main content

rustlavel_core/
error.rs

1use std::fmt;
2
3/// The framework-wide result type.
4pub type Result<T, E = Error> = std::result::Result<T, E>;
5
6/// Every failure the framework itself can produce.
7///
8/// Application errors are expected to convert into this through `From`, which
9/// is what lets a handler return `Result<T, E>` and still be a valid handler.
10pub enum Error {
11    /// A file could not be read or written.
12    Io(std::io::Error),
13    /// A `.env` or config file was malformed.
14    Config { file: String, line: usize, message: String },
15    /// A JSON document could not be parsed.
16    Json { line: usize, column: usize, message: String },
17    /// A template could not be parsed or rendered.
18    ///
19    /// Carries the position because a view is written by hand, often by
20    /// someone who is not the person reading the stack trace.
21    Template { file: String, line: usize, column: usize, message: String },
22    /// A malformed HTTP request reached the parser.
23    Protocol(String),
24    /// A dependency was not tried, because it is known to be failing.
25    ///
26    /// Distinct from a failure on purpose: nothing was sent, so nothing can
27    /// have had an effect, and a caller may safely fall back to a cache, a
28    /// default, or a degraded answer. Raised by the circuit breaker in
29    /// `rustlavel-client`.
30    Unavailable(String),
31    /// Anything raised by application code.
32    Message(String),
33}
34
35impl Error {
36    pub fn msg(message: impl Into<String>) -> Self {
37        Error::Message(message.into())
38    }
39
40    /// The HTTP status this error should surface as when it escapes a handler.
41    pub fn status(&self) -> u16 {
42        match self {
43            Error::Protocol(_) => 400,
44            // The upstream this request needed is known to be down, and the
45            // caller may reasonably try again later — which is what 503 means.
46            Error::Unavailable(_) => 503,
47            _ => 500,
48        }
49    }
50
51    /// A short, human title for the dev error page.
52    pub fn title(&self) -> &'static str {
53        match self {
54            Error::Io(_) => "I/O Error",
55            Error::Config { .. } => "Configuration Error",
56            Error::Json { .. } => "JSON Error",
57            Error::Template { .. } => "Template Error",
58            Error::Protocol(_) => "Protocol Error",
59            Error::Unavailable(_) => "Dependency Unavailable",
60            Error::Message(_) => "Application Error",
61        }
62    }
63
64    /// A suggestion shown on the dev error page — the Ignition touch.
65    pub fn hint(&self) -> Option<String> {
66        match self {
67            Error::Config { file, .. } => Some(format!(
68                "Check the syntax of `{file}`. Each line should look like `KEY=value`."
69            )),
70            Error::Json { .. } => {
71                Some("Verify the payload is valid JSON — trailing commas are not allowed.".into())
72            }
73            Error::Template { file, line, .. } => Some(format!(
74                "Look at `{file}` around line {line}. Every `@if`, `@foreach` and `@section` \
75                 needs its matching `@end...`."
76            )),
77            Error::Io(e) if e.kind() == std::io::ErrorKind::NotFound => {
78                Some("The file does not exist. Did you run `rustlavel new` in this directory?".into())
79            }
80            Error::Io(e) if e.kind() == std::io::ErrorKind::AddrInUse => {
81                Some("That port is already in use. Try `rustlavel serve --port 8001`.".into())
82            }
83            _ => None,
84        }
85    }
86}
87
88/// Deliberately the same as [`Display`](fmt::Display).
89///
90/// Rust prints the error a `main` returns with `Debug`, not `Display`, so the
91/// derived form is what people actually see when an application fails to
92/// start. It reads like this:
93///
94/// ```text
95/// Error: Io(Os { code: 48, kind: AddrInUse, message: "Address already in use" })
96/// ```
97///
98/// which names the struct that holds the problem rather than the problem, and
99/// tells somebody nothing they can act on. Delegating to `Display` gives them
100/// the sentence that was written for them instead. Test output improves for
101/// the same reason: `unwrap_err()` on a bad config now says which file and
102/// line, not which variant.
103impl fmt::Debug for Error {
104    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
105        fmt::Display::fmt(self, f)
106    }
107}
108
109impl fmt::Display for Error {
110    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
111        match self {
112            Error::Io(e) => write!(f, "{e}"),
113            Error::Config { file, line, message } => {
114                write!(f, "{file}:{line}: {message}")
115            }
116            Error::Json { line, column, message } => {
117                write!(f, "invalid JSON at line {line}, column {column}: {message}")
118            }
119            Error::Template { file, line, column, message } => {
120                write!(f, "{file}:{line}:{column}: {message}")
121            }
122            Error::Protocol(m) => write!(f, "malformed request: {m}"),
123            Error::Unavailable(m) => f.write_str(m),
124            Error::Message(m) => f.write_str(m),
125        }
126    }
127}
128
129impl std::error::Error for Error {
130    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
131        match self {
132            Error::Io(e) => Some(e),
133            _ => None,
134        }
135    }
136}
137
138impl From<std::io::Error> for Error {
139    fn from(e: std::io::Error) -> Self {
140        Error::Io(e)
141    }
142}
143
144impl From<String> for Error {
145    fn from(s: String) -> Self {
146        Error::Message(s)
147    }
148}
149
150impl From<&str> for Error {
151    fn from(s: &str) -> Self {
152        Error::Message(s.to_string())
153    }
154}
155
156#[cfg(test)]
157mod tests {
158    use super::*;
159
160    /// The failure this exists to stop: `Error: Io(Os { code: 48, kind:
161    /// AddrInUse, ... })` at the top of somebody's terminal, naming the struct
162    /// that holds the problem instead of the problem.
163    #[test]
164    fn the_debug_form_is_the_sentence_not_the_struct() {
165        let error = Error::Io(std::io::Error::new(
166            std::io::ErrorKind::AddrInUse,
167            "Address already in use",
168        ));
169        let shown = format!("{error:?}");
170        assert_eq!(shown, format!("{error}"));
171        assert!(!shown.contains("Io("), "the variant is leaking: {shown}");
172        assert!(!shown.contains("Os {"), "the os struct is leaking: {shown}");
173        assert!(shown.contains("Address already in use"), "{shown}");
174    }
175
176    #[test]
177    fn a_config_error_debugs_to_its_file_and_line() {
178        let error = Error::Config {
179            file: ".env".into(),
180            line: 4,
181            message: "expected KEY=value".into(),
182        };
183        assert_eq!(format!("{error:?}"), ".env:4: expected KEY=value");
184    }
185}