From aba45561443e762f73987e5bee896e5a7e86b4c7 Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Sun, 21 Jun 2026 12:22:33 -0400 Subject: [PATCH] Create error type --- src/util/error.rs | 252 ++++++++++++++++++++++++++++++++++++++++++++++ src/util/mod.rs | 2 +- 2 files changed, 253 insertions(+), 1 deletion(-) create mode 100644 src/util/error.rs diff --git a/src/util/error.rs b/src/util/error.rs new file mode 100644 index 0000000..57efcd7 --- /dev/null +++ b/src/util/error.rs @@ -0,0 +1,252 @@ +use std::fmt; +use std::panic::Location; + +use dioxus::prelude::*; +use serde::{Deserialize, Serialize}; + +/// A location in the source code +/// A thin wrapper over `std::panic::Location`, which isn't `Serialize` or `Deserialize` +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ErrorLocation { + file: String, + line: u32, +} + +impl ErrorLocation { + /// Creates a new `ErrorLocation` with the file and line number of the caller. + #[track_caller] + pub fn here() -> Self { + let location = Location::caller(); + + ErrorLocation { + file: location.file().to_string(), + line: location.line(), + } + } + + /// Get a link to the source code based on the repository URL from Cargo and the Git commit + /// from the build script. Uses a format supported by GitHub, Gitea, and GitLab. + pub fn source_link(&self) -> String { + format!( + "{}/blob/{}/{}#L{}", + env!("CARGO_PKG_REPOSITORY"), + env!("GIT_REV"), + self.file, + self.line + ) + } +} + +impl fmt::Display for ErrorLocation { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}:{}", self.file, self.line) + } +} + +#[derive(Debug, Clone, Deserialize, Serialize, thiserror::Error)] +pub struct Error { + #[source] + /// The error type and data + source: ErrorType, + + /// The location where the error was created + from: ErrorLocation, + + /// Context added to the error, and location where it was added + context: Vec<(ErrorLocation, String)>, +} + +impl Error { + /// Creates a new `Error` at this location with the given error type + #[track_caller] + pub fn new_here(source: ErrorType) -> Self { + Error::new(source, ErrorLocation::here()) + } + + /// Creates a new `Error` with the given location and error type + pub fn new(source: ErrorType, from: ErrorLocation) -> Self { + Error { + source, + from, + context: Vec::new(), + } + } + + /// Adds a context message to the error + #[track_caller] + pub fn with_context(mut self, context: impl Into) -> Self { + self.context.push((ErrorLocation::here(), context.into())); + self + } + + /// Retrieve the "top" message of the error. Uses either the most recent context entry, or the + /// source message if no context has been added. + pub fn top_message(&self) -> String { + if let Some((_location, message)) = self.context.last() { + message.clone() + } else { + self.source.to_string() + } + } + + /// Display this error as a modal dialog activated by a checkbox with the given id + pub fn as_modal(&self, id: String) -> Element { + rsx! { + input { + r#type: "checkbox", + class: "modal-toggle", + id: &id, + } + + div { + class: "modal", + role: "dialog", + + div { + class: "modal-box border border-error bg-soft-error max-w-200", + + h2 { + class: "flex items-center gap-3 text-lg", + + lucide_dioxus::CircleAlert { + class: "shrink-0", + } + {self.top_message()} + } + + p { + class: "text-base-content/70 py-3", + "Details" + } + + div { + class: "md:grid md:grid-cols-[fit-content(calc(var(--spacing)*30))_auto] md:gap-1 md:gap-x-2 mb-6", + + for (location, message) in self.context.iter().rev().chain(std::iter::once(&( + self.from.clone(), + self.source.to_string(), + ))) { + a { + class: "text-base-content/80 interact underline", + target: "_blank", + href: location.source_link(), + {location.to_string()} + } + + p { + class: "ml-3 md:ml-0", + {message.to_string()} + } + } + } + + a { + class: "text-base-content/50 interact underline", + target: "_blank", + href: format!("{}/issues/new", env!("CARGO_PKG_REPOSITORY")), + "Report an issue" + } + + label { + class: "absolute right-1 top-1 interact hover:bg-base-100/70 p-1 rounded-full", + r#for: &id, + lucide_dioxus::X { + class: "size-7 md:size-5", + } + } + } + + label { + class: "modal-backdrop cursor-pointer", + r#for: id, + "Close", + } + } + } + } + + /// Convert this error to a toast message, which opens a modal when clicked + pub fn as_toast(&self) -> Element { + // Generate a random string to use as an id for the modal + // This allows multiple toast/modal to be present + let modal_id = { + use rand::RngExt; + + let mut rng = rand::rng(); + + let random_str = (0..5) + .map(|_| rng.sample(rand::distr::Alphanumeric) as char) + .collect::(); + + format!("err-modal-{random_str}") + }; + + rsx! { + {self.as_modal(modal_id.clone())} + + div { + class: "toast", + + label { + class: "alert alert-error alert-soft cursor-pointer", + role: "alert", + r#for: modal_id, + lucide_dioxus::CircleAlert {} + + p { + class: "max-w-120 text-ellipsis line-clamp-3", + {self.top_message()} + } + } + } + } + } +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // Write the error type and its context + writeln!(f, "Error: {}", self.top_message())?; + write!(f, "Context:")?; + + for (location, message) in self.context.iter().rev().chain(std::iter::once(&( + self.from.clone(), + self.source.to_string(), + ))) { + write!(f, "\n - {location}: {message}")?; + } + + Ok(()) + } +} + +pub trait Contextualize { + /// Add context to the `Result` if it is an `Err`. + #[track_caller] + fn err_context(self, context: impl Into) -> R; +} + +impl> Contextualize> for Result { + #[track_caller] + fn err_context(self, context: impl Into) -> Result { + self.map_err(|e| e.into().with_context(context)) + } +} + +impl> Contextualize for E { + #[track_caller] + fn err_context(self, context: impl Into) -> Error { + self.into().with_context(context) + } +} + +#[derive(Debug, Clone, thiserror::Error, Deserialize, Serialize)] +pub enum ErrorType { +} + +impl From for Error { + #[track_caller] + fn from(err: ErrorType) -> Self { + Error::new_here(err) + } +} diff --git a/src/util/mod.rs b/src/util/mod.rs index 8b13789..a91e735 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -1 +1 @@ - +pub mod error;