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
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:
252
src/util/error.rs
Normal file
252
src/util/error.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user