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 serverfn_client;
|
||||
pub mod state;
|
||||
|
Reference in New Issue
Block a user