228-create-unified-config-system #229

Merged
eta357 merged 33 commits from 228-create-unified-config-system into main 2025-06-28 01:13:23 +00:00
7 changed files with 821 additions and 159 deletions
Showing only changes of commit cd39ec7252 - Show all commits

196
src/util/error.rs Normal file
View 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}")))
}
}

View File

@ -13,6 +13,7 @@ cfg_if! {
}
}
pub mod error;
pub mod img_fallback;
pub mod serverfn_client;
pub mod state;