Compare commits
3 Commits
5f8d96b6ae
...
aba4556144
| Author | SHA1 | Date | |
|---|---|---|---|
|
aba4556144
|
|||
|
749b5e7864
|
|||
|
2b5f9d011f
|
77
Cargo.lock
generated
77
Cargo.lock
generated
@@ -280,6 +280,17 @@ version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
||||
|
||||
[[package]]
|
||||
name = "chacha20"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures 0.3.0",
|
||||
"rand_core 0.10.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "charset"
|
||||
version = "0.1.5"
|
||||
@@ -490,6 +501,15 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-utils"
|
||||
version = "0.8.21"
|
||||
@@ -1521,11 +1541,25 @@ dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"r-efi",
|
||||
"r-efi 5.3.0",
|
||||
"wasip2",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"r-efi 6.0.0",
|
||||
"rand_core 0.10.1",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gloo-net"
|
||||
version = "0.6.0"
|
||||
@@ -2059,7 +2093,9 @@ dependencies = [
|
||||
"diesel_migrations",
|
||||
"dioxus",
|
||||
"dotenvy",
|
||||
"getrandom 0.4.3",
|
||||
"lucide-dioxus",
|
||||
"rand 0.10.1",
|
||||
"serde",
|
||||
"thiserror 2.0.18",
|
||||
"tracing",
|
||||
@@ -2546,7 +2582,7 @@ dependencies = [
|
||||
"bytes",
|
||||
"getrandom 0.3.4",
|
||||
"lru-slab",
|
||||
"rand",
|
||||
"rand 0.9.4",
|
||||
"ring",
|
||||
"rustc-hash 2.1.2",
|
||||
"rustls",
|
||||
@@ -2587,6 +2623,12 @@ version = "5.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
|
||||
|
||||
[[package]]
|
||||
name = "r-efi"
|
||||
version = "6.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.9.4"
|
||||
@@ -2594,7 +2636,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea"
|
||||
dependencies = [
|
||||
"rand_chacha",
|
||||
"rand_core",
|
||||
"rand_core 0.9.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207"
|
||||
dependencies = [
|
||||
"chacha20",
|
||||
"getrandom 0.4.3",
|
||||
"rand_core 0.10.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2604,7 +2657,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
|
||||
dependencies = [
|
||||
"ppv-lite86",
|
||||
"rand_core",
|
||||
"rand_core 0.9.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2616,6 +2669,12 @@ dependencies = [
|
||||
"getrandom 0.3.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
|
||||
|
||||
[[package]]
|
||||
name = "raw-window-handle"
|
||||
version = "0.6.2"
|
||||
@@ -2932,7 +2991,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"cpufeatures 0.2.17",
|
||||
"digest",
|
||||
]
|
||||
|
||||
@@ -2943,7 +3002,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"cpufeatures 0.2.17",
|
||||
"digest",
|
||||
]
|
||||
|
||||
@@ -3511,7 +3570,7 @@ dependencies = [
|
||||
"http",
|
||||
"httparse",
|
||||
"log",
|
||||
"rand",
|
||||
"rand 0.9.4",
|
||||
"sha1",
|
||||
"thiserror 2.0.18",
|
||||
"utf-8",
|
||||
@@ -3528,7 +3587,7 @@ dependencies = [
|
||||
"http",
|
||||
"httparse",
|
||||
"log",
|
||||
"rand",
|
||||
"rand 0.9.4",
|
||||
"sha1",
|
||||
"thiserror 2.0.18",
|
||||
"utf-8",
|
||||
@@ -3545,7 +3604,7 @@ dependencies = [
|
||||
"http",
|
||||
"httparse",
|
||||
"log",
|
||||
"rand",
|
||||
"rand 0.9.4",
|
||||
"sha1",
|
||||
"thiserror 2.0.18",
|
||||
]
|
||||
|
||||
@@ -13,7 +13,8 @@ diesel = { version = "2.3.10", optional = true, features = [ "postgres" ] }
|
||||
diesel_migrations = { version = "2.3.2", optional = true, features = [ "postgres" ] }
|
||||
dioxus = { version = "0.7.9", features = ["router", "fullstack"] }
|
||||
dotenvy = { version = "0.15.7", optional = true }
|
||||
lucide-dioxus = "3.11.0"
|
||||
lucide-dioxus = { version = "3.11.0", features = ["notifications"] }
|
||||
rand = "0.10.1"
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
thiserror = "2.0.18"
|
||||
tracing = "0.1.44"
|
||||
@@ -31,3 +32,9 @@ server = [
|
||||
# Disabled until supported
|
||||
# desktop = ["dioxus/desktop"]
|
||||
# mobile = ["dioxus/mobile"]
|
||||
|
||||
# Enable wasm_js in getrandom when building for wasm32
|
||||
# This is a workaround for rand not exposing a wasm_js target
|
||||
# https://github.com/rust-random/rand/issues/1694#issuecomment-3846362044
|
||||
[target.'cfg(all(target_arch = "wasm32", target_os = "unknown"))'.dependencies]
|
||||
getrandom = { version = "0.4.3", features = ["wasm_js"] }
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
|
||||
pub mod error;
|
||||
|
||||
Reference in New Issue
Block a user