228-create-unified-config-system #229
196
src/util/error.rs
Normal file
196
src/util/error.rs
Normal file
@ -0,0 +1,196 @@
|
|||||||
|
#![feature(track_caller)]
|
||||||
|
|
||||||
|
use leptos::prelude::*;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use server_fn::codec::JsonEncoding;
|
||||||
|
use std::fmt::{Display, Formatter, Result as FmtResult};
|
||||||
|
use std::panic::Location;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
/// A location in the source code
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
|
struct ErrorLocation {
|
||||||
|
pub file: String,
|
||||||
|
pub line: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ErrorLocation {
|
||||||
|
/// Creates a new `ErrorLocation` with the file and line number of the caller.
|
||||||
|
#[track_caller]
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let location = Location::caller();
|
||||||
|
|
||||||
|
ErrorLocation {
|
||||||
|
file: location.file().to_string(),
|
||||||
|
line: location.line(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for ErrorLocation {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
|
||||||
|
write!(f, "{}:{}", self.file, self.line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A custom error type for backend errors
|
||||||
|
/// Contains the error, the location where it was created, and context added to the error.
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
|
pub struct BackendError {
|
||||||
|
/// The error type and data
|
||||||
|
error: BackendErrorType,
|
||||||
|
/// 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 BackendError {
|
||||||
|
/// Creates a new `BackendError` at this location with the given error type
|
||||||
|
#[track_caller]
|
||||||
|
fn new(error: BackendErrorType) -> Self {
|
||||||
|
BackendError {
|
||||||
|
error,
|
||||||
|
from: ErrorLocation::new(),
|
||||||
|
context: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Createa new `BackendError` from a `ServerFnErrorErr`
|
||||||
|
#[track_caller]
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn ServerFnError(error: ServerFnErrorErr) -> Self {
|
||||||
|
BackendError::new(BackendErrorType::ServerFnError(error))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a new `BackendError` from a Diesel error
|
||||||
|
#[track_caller]
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn DieselError(error: diesel::result::Error) -> Self {
|
||||||
|
BackendError::new(BackendErrorType::DieselError(error.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds a context message to the error
|
||||||
|
#[track_caller]
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn context(mut self, context: impl Into<String>) -> Self {
|
||||||
|
self.context.push((ErrorLocation::new(), context.into()));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Converts the error to a view component for display
|
||||||
|
pub fn to_component(mut self) -> impl IntoView {
|
||||||
|
use leptos_icons::*;
|
||||||
|
|
||||||
|
leptos::logging::error!("{}", self);
|
||||||
|
|
||||||
|
let error = self.error.to_string();
|
||||||
|
self.context.reverse(); // Reverse the context to show the most recent first
|
||||||
|
|
||||||
|
// Get the last context, if any, or the error message itself
|
||||||
|
let message = self
|
||||||
|
.context
|
||||||
|
.first()
|
||||||
|
.cloned()
|
||||||
|
.map(|(_location, message)| message)
|
||||||
|
.unwrap_or_else(|| error.clone());
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<div class="text-red-800">
|
||||||
|
<div class="grid grid-cols-[max-content_1fr] gap-1">
|
||||||
|
<Icon icon={icondata::BiErrorSolid} {..} class="self-center" />
|
||||||
|
<h1 class="self-center">{message}</h1>
|
||||||
|
</div>
|
||||||
|
{(!self.context.is_empty()).then(|| {
|
||||||
|
view! {
|
||||||
|
<details>
|
||||||
|
<summary class="cursor-pointer">{error.clone()}</summary>
|
||||||
|
<ul class="text-red-900">
|
||||||
|
{self.context.into_iter().map(|(location, message)| view! {
|
||||||
|
<li>{format!("{location}: {message}")}</li>
|
||||||
|
}).collect::<Vec<_>>()}
|
||||||
|
|
||||||
|
<li>{format!("{}: {}", self.from, error)}</li>
|
||||||
|
</ul>
|
||||||
|
</details>
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for BackendError {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
|
||||||
|
// Write the error type and its context
|
||||||
|
write!(f, "BackendError: {}", self.error)?;
|
||||||
|
|
||||||
|
if !self.context.is_empty() {
|
||||||
|
write!(f, "\nContext:")?;
|
||||||
|
|
||||||
|
for (location, message) in self.context.iter().rev() {
|
||||||
|
write!(f, "\n - {location}: {message}")?;
|
||||||
|
}
|
||||||
|
|
||||||
|
write!(f, "\n - {}: {}", self.from, self.error)?;
|
||||||
|
} else {
|
||||||
|
write!(f, "\nFrom: {}", self.from)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<BackendErrorType> for BackendError {
|
||||||
|
fn from(error: BackendErrorType) -> Self {
|
||||||
|
BackendError::new(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait Contextualize<T> {
|
||||||
|
/// Add context to the `Result` if it is an `Err`.
|
||||||
|
#[track_caller]
|
||||||
|
fn context(self, context: impl Into<String>) -> Result<T, BackendError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T, E: Into<BackendError>> Contextualize<T> for Result<T, E> {
|
||||||
|
#[track_caller]
|
||||||
|
fn context(self, context: impl Into<String>) -> Result<T, BackendError> {
|
||||||
|
match self {
|
||||||
|
Ok(value) => Ok(value),
|
||||||
|
Err(e) => {
|
||||||
|
// Convert the error into BackendError and add context
|
||||||
|
let backend_error = e.into().context(context);
|
||||||
|
Err(backend_error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The inner error type for `BackendError`
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, Error)]
|
||||||
|
enum BackendErrorType {
|
||||||
|
#[error("Server function error: {0}")]
|
||||||
|
ServerFnError(ServerFnErrorErr),
|
||||||
|
// Using string to represent Diesel errors,
|
||||||
|
// because Diesel's Error type is not `Serializable`.
|
||||||
|
#[error("Database error: {0}")]
|
||||||
|
DieselError(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromServerFnError for BackendError {
|
||||||
|
type Encoder = JsonEncoding;
|
||||||
|
|
||||||
|
#[track_caller]
|
||||||
|
fn from_server_fn_error(value: ServerFnErrorErr) -> Self {
|
||||||
|
BackendError::new(BackendErrorType::ServerFnError(value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "ssr")]
|
||||||
|
impl From<diesel::result::Error> for BackendError {
|
||||||
|
#[track_caller]
|
||||||
|
fn from(err: diesel::result::Error) -> Self {
|
||||||
|
BackendError::new(BackendErrorType::DieselError(format!("{err}")))
|
||||||
|
}
|
||||||
|
}
|
@ -13,6 +13,7 @@ cfg_if! {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub mod error;
|
||||||
pub mod img_fallback;
|
pub mod img_fallback;
|
||||||
pub mod serverfn_client;
|
pub mod serverfn_client;
|
||||||
pub mod state;
|
pub mod state;
|
||||||
|
Reference in New Issue
Block a user