//! Failure, classified once so every host renders it the same way. //! //! Decision 9 on the wiki note. A handler that cannot do what was asked returns //! a [`RouteError`] rather than a [`Response`](crate::Response) describing the //! problem. Two things fall out of that, and both are the reason the type //! exists. //! //! A class is not a message. The class is what the *host* acts on: axum turns //! it into a status code, the terminal and egui turn it into a banner. Handing //! back a plain response instead would have hosted axum answer 200 to //! everything, so caches, logs and monitoring could not tell a denied action //! from a completed one. //! //! The message is already UI. It carries a `makeover-layout` [`Notice`], so //! errors become something on screen in one place rather than once per handler. use makeover_layout::{Notice, Tone}; /// What kind of failure it was. /// /// Four, and each one is a different thing for a host to do. `NotFound` and /// `Denied` are separate because a host that conflates them cannot answer the /// question its own access log asks. `Conflict` is separate because it is the /// one failure the user can fix by looking at the screen again, which is a /// banner rather than a toast. /// /// `#[non_exhaustive]` for the reason `makeover-layout` puts it on the /// vocabularies renderers match against: growth must not be a lockstep event /// across every host adapter. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[non_exhaustive] pub enum Class { /// The thing addressed is not there. NotFound, /// The thing is there and this caller may not have it. Denied, /// The request was answerable and the current state refuses it. Conflict, /// We are broken. Internal, } impl Class { /// What the failure is saying, in `makeover-layout`'s vocabulary. /// /// Only `Internal` is [`Tone::Danger`]. The other three are conditions the /// user can understand and often act on, and spending the loudest tone on /// all of them is how a `Danger` stops meaning anything. #[must_use] pub const fn tone(self) -> Tone { match self { Self::NotFound | Self::Denied | Self::Conflict => Tone::Warning, Self::Internal => Tone::Danger, } } /// The status code an HTTP host answers with. /// /// A convenience rather than a leak: this is a `u16`, no host crate is /// imported to produce it, and every HTTP adapter we will write would /// otherwise hand-roll the same four-arm match. Hosts with no notion of a /// status code ignore it and read [`Class`] directly. #[must_use] pub const fn http_status(self) -> u16 { match self { Self::NotFound => 404, Self::Denied => 403, Self::Conflict => 409, Self::Internal => 500, } } /// Whether the failure is ours rather than the caller's. /// /// The line a host logs on. A `NotFound` at volume is a broken link /// somewhere; an `Internal` at any volume is a page. #[must_use] pub const fn is_ours(self) -> bool { matches!(self, Self::Internal) } } /// A route that could not answer. /// /// Carries a class for the host and a notice for the screen. The notice /// defaults to [`Notice::Banner`], because a failure is persistent by nature: /// it is dismissed by fixing the condition that caused it, which is exactly /// what `makeover-layout` says a banner is for. Call [`RouteError::as_toast`] /// where the failure really is transient. #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct RouteError { /// What kind of failure it was. pub class: Class, /// How the message sits on screen. pub notice: Notice, /// What to tell the user. Already user-facing text, not a debug string. pub message: String, } impl RouteError { /// A failure of the given class, shown as a banner. pub fn new(class: Class, message: impl Into) -> Self { Self { class, notice: Notice::Banner, message: message.into(), } } /// The thing addressed is not there. pub fn not_found(message: impl Into) -> Self { Self::new(Class::NotFound, message) } /// The caller may not have it. pub fn denied(message: impl Into) -> Self { Self::new(Class::Denied, message) } /// The current state refuses the request. pub fn conflict(message: impl Into) -> Self { Self::new(Class::Conflict, message) } /// We are broken. pub fn internal(message: impl Into) -> Self { Self::new(Class::Internal, message) } /// The same failure, shown as a toast instead of a banner. #[must_use] pub fn as_toast(mut self) -> Self { self.notice = Notice::Toast; self } /// What the failure is saying. Delegates to the class. #[must_use] pub const fn tone(&self) -> Tone { self.class.tone() } } impl std::fmt::Display for RouteError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{:?}: {}", self.class, self.message) } } impl std::error::Error for RouteError {}