Create error type
Some checks failed
Push Workflows / rustfmt (push) Failing after 5s
Push Workflows / tailwind-build (push) Successful in 5s
Push Workflows / clippy (push) Failing after 15s
Push Workflows / test (push) Successful in 29s
Push Workflows / docs (push) Successful in 27s
Push Workflows / build (push) Failing after 36s
Push Workflows / nix-build (push) Successful in 4m59s

This commit is contained in:
2026-06-21 12:22:33 -04:00
parent 749b5e7864
commit aba4556144
2 changed files with 253 additions and 1 deletions

252
src/util/error.rs Normal file
View File

@@ -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<String>) -> 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::<String>();
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<R> {
/// Add context to the `Result` if it is an `Err`.
#[track_caller]
fn err_context(self, context: impl Into<String>) -> R;
}
impl<T, E: Into<Error>> Contextualize<Result<T, Error>> for Result<T, E> {
#[track_caller]
fn err_context(self, context: impl Into<String>) -> Result<T, Error> {
self.map_err(|e| e.into().with_context(context))
}
}
impl<E: Into<Error>> Contextualize<Error> for E {
#[track_caller]
fn err_context(self, context: impl Into<String>) -> Error {
self.into().with_context(context)
}
}
#[derive(Debug, Clone, thiserror::Error, Deserialize, Serialize)]
pub enum ErrorType {
}
impl From<ErrorType> for Error {
#[track_caller]
fn from(err: ErrorType) -> Self {
Error::new_here(err)
}
}