Compare commits

...

8 Commits
main ... error

Author SHA1 Message Date
Greg Johnston 1a7344ca50 update other examples 2023-06-25 13:43:15 -04:00
Greg Johnston 921bb616b4 fmt 2023-06-25 13:31:48 -04:00
Greg Johnston 5bb7122b93 fmt 2023-06-24 17:06:58 -04:00
Greg Johnston ec2aa3e7a4 adapt example 2023-06-24 17:05:38 -04:00
Greg Johnston 43959a96c7 support server errors 2023-06-24 17:04:50 -04:00
Greg Johnston 7925fc4245 attempt at including server 2023-06-24 16:34:29 -04:00
Greg Johnston 2bd1ad0f11 fmt example 2023-06-23 13:54:42 -04:00
Greg Johnston dd730aa4ac feat: add an `anyhow`-like `Result` type for easier error handling 2023-06-23 13:25:16 -04:00
13 changed files with 238 additions and 122 deletions

View File

@ -1,4 +1,4 @@
use leptos::*;
use leptos::{error::Result, *};
use serde::{Deserialize, Serialize};
use thiserror::Error;
@ -8,30 +8,24 @@ pub struct Cat {
}
#[derive(Error, Clone, Debug)]
pub enum FetchError {
pub enum CatError {
#[error("Please request more than zero cats.")]
NonZeroCats,
#[error("Error loading data from serving.")]
Request,
#[error("Error deserializaing cat data from request.")]
Json,
}
type CatCount = usize;
async fn fetch_cats(count: CatCount) -> Result<Vec<String>, FetchError> {
async fn fetch_cats(count: CatCount) -> Result<Vec<String>> {
if count > 0 {
// make the request
let res = reqwasm::http::Request::get(&format!(
"https://api.thecatapi.com/v1/images/search?limit={count}",
))
.send()
.await
.map_err(|_| FetchError::Request)?
.await?
// convert it to JSON
.json::<Vec<Cat>>()
.await
.map_err(|_| FetchError::Json)?
.await?
// extract the URL field for each cat
.into_iter()
.take(count)
@ -39,7 +33,7 @@ async fn fetch_cats(count: CatCount) -> Result<Vec<String>, FetchError> {
.collect::<Vec<_>>();
Ok(res)
} else {
Err(FetchError::NonZeroCats)
Err(CatError::NonZeroCats.into())
}
}

View File

@ -163,12 +163,11 @@ pub async fn login(
let user: User = User::get_from_username(username, &pool)
.await
.ok_or("User does not exist.")
.map_err(|e| ServerFnError::ServerError(e.to_string()))?;
.ok_or_else(|| {
ServerFnError::ServerError("User does not exist.".into())
})?;
match verify(password, &user.password)
.map_err(|e| ServerFnError::ServerError(e.to_string()))?
{
match verify(password, &user.password)? {
true => {
auth.login_user(user.id);
auth.remember_user(remember.is_some());
@ -204,13 +203,16 @@ pub async fn signup(
.bind(username.clone())
.bind(password_hashed)
.execute(&pool)
.await
.map_err(|e| ServerFnError::ServerError(e.to_string()))?;
.await?;
let user = User::get_from_username(username, &pool)
.await
.ok_or("Signup failed: User does not exist.")
.map_err(|e| ServerFnError::ServerError(e.to_string()))?;
let user =
User::get_from_username(username, &pool)
.await
.ok_or_else(|| {
ServerFnError::ServerError(
"Signup failed: User does not exist.".into(),
)
})?;
auth.login_user(user.id);
auth.remember_user(remember.is_some());

View File

@ -21,14 +21,12 @@ if #[cfg(feature = "ssr")] {
pub fn pool(cx: Scope) -> Result<SqlitePool, ServerFnError> {
use_context::<SqlitePool>(cx)
.ok_or("Pool missing.")
.map_err(|e| ServerFnError::ServerError(e.to_string()))
.ok_or_else(|| ServerFnError::ServerError("Pool missing.".into()))
}
pub fn auth(cx: Scope) -> Result<AuthSession, ServerFnError> {
use_context::<AuthSession>(cx)
.ok_or("Auth session missing.")
.map_err(|e| ServerFnError::ServerError(e.to_string()))
.ok_or_else(|| ServerFnError::ServerError("Auth session missing.".into()))
}
#[derive(sqlx::FromRow, Clone)]
@ -64,11 +62,7 @@ pub async fn get_todos(cx: Scope) -> Result<Vec<Todo>, ServerFnError> {
let mut rows =
sqlx::query_as::<_, SqlTodo>("SELECT * FROM todos").fetch(&pool);
while let Some(row) = rows
.try_next()
.await
.map_err(|e| ServerFnError::ServerError(e.to_string()))?
{
while let Some(row) = rows.try_next().await? {
todos.push(row);
}
@ -117,12 +111,11 @@ pub async fn add_todo(cx: Scope, title: String) -> Result<(), ServerFnError> {
pub async fn delete_todo(cx: Scope, id: u16) -> Result<(), ServerFnError> {
let pool = pool(cx)?;
sqlx::query("DELETE FROM todos WHERE id = $1")
Ok(sqlx::query("DELETE FROM todos WHERE id = $1")
.bind(id)
.execute(&pool)
.await
.map(|_| ())
.map_err(|e| ServerFnError::ServerError(e.to_string()))
.map(|_| ())?)
}
#[component]

View File

@ -9,7 +9,7 @@ cfg_if! {
use sqlx::{Connection, SqliteConnection};
pub async fn db() -> Result<SqliteConnection, ServerFnError> {
SqliteConnection::connect("sqlite:Todos.db").await.map_err(|e| ServerFnError::ServerError(e.to_string()))
Ok(SqliteConnection::connect("sqlite:Todos.db").await?)
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow)]
@ -43,11 +43,7 @@ pub async fn get_todos(cx: Scope) -> Result<Vec<Todo>, ServerFnError> {
let mut todos = Vec::new();
let mut rows =
sqlx::query_as::<_, Todo>("SELECT * FROM todos").fetch(&mut conn);
while let Some(row) = rows
.try_next()
.await
.map_err(|e| ServerFnError::ServerError(e.to_string()))?
{
while let Some(row) = rows.try_next().await? {
todos.push(row);
}
@ -76,12 +72,11 @@ pub async fn add_todo(title: String) -> Result<(), ServerFnError> {
pub async fn delete_todo(id: u16) -> Result<(), ServerFnError> {
let mut conn = db().await?;
sqlx::query("DELETE FROM todos WHERE id = $1")
Ok(sqlx::query("DELETE FROM todos WHERE id = $1")
.bind(id)
.execute(&mut conn)
.await
.map(|_| ())
.map_err(|e| ServerFnError::ServerError(e.to_string()))
.map(|_| ())?)
}
#[component]

View File

@ -11,7 +11,7 @@ cfg_if! {
// use http::{header::SET_COOKIE, HeaderMap, HeaderValue, StatusCode};
pub async fn db() -> Result<SqliteConnection, ServerFnError> {
SqliteConnection::connect("sqlite:Todos.db").await.map_err(|e| ServerFnError::ServerError(e.to_string()))
Ok(SqliteConnection::connect("sqlite:Todos.db").await?)
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow)]
@ -47,11 +47,7 @@ pub async fn get_todos(cx: Scope) -> Result<Vec<Todo>, ServerFnError> {
let mut todos = Vec::new();
let mut rows =
sqlx::query_as::<_, Todo>("SELECT * FROM todos").fetch(&mut conn);
while let Some(row) = rows
.try_next()
.await
.map_err(|e| ServerFnError::ServerError(e.to_string()))?
{
while let Some(row) = rows.try_next().await? {
todos.push(row);
}
@ -93,12 +89,11 @@ pub async fn add_todo(title: String) -> Result<(), ServerFnError> {
pub async fn delete_todo(id: u16) -> Result<(), ServerFnError> {
let mut conn = db().await?;
sqlx::query("DELETE FROM todos WHERE id = $1")
Ok(sqlx::query("DELETE FROM todos WHERE id = $1")
.bind(id)
.execute(&mut conn)
.await
.map(|_| ())
.map_err(|e| ServerFnError::ServerError(e.to_string()))
.map(|_| ())?)
}
#[component]

View File

@ -11,7 +11,7 @@ cfg_if! {
// use http::{header::SET_COOKIE, HeaderMap, HeaderValue, StatusCode};
pub async fn db() -> Result<SqliteConnection, ServerFnError> {
SqliteConnection::connect("sqlite:Todos.db").await.map_err(|e| ServerFnError::ServerError(e.to_string()))
Ok(SqliteConnection::connect("sqlite:Todos.db").await?)
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow)]
@ -47,11 +47,7 @@ pub async fn get_todos(cx: Scope) -> Result<Vec<Todo>, ServerFnError> {
let mut todos = Vec::new();
let mut rows =
sqlx::query_as::<_, Todo>("SELECT * FROM todos").fetch(&mut conn);
while let Some(row) = rows
.try_next()
.await
.map_err(|e| ServerFnError::ServerError(e.to_string()))?
{
while let Some(row) = rows.try_next().await? {
todos.push(row);
}
@ -93,12 +89,11 @@ pub async fn add_todo(title: String) -> Result<(), ServerFnError> {
pub async fn delete_todo(id: u16) -> Result<(), ServerFnError> {
let mut conn = db().await?;
sqlx::query("DELETE FROM todos WHERE id = $1")
Ok(sqlx::query("DELETE FROM todos WHERE id = $1")
.bind(id)
.execute(&mut conn)
.await
.map(|_| ())
.map_err(|e| ServerFnError::ServerError(e.to_string()))
.map(|_| ())?)
}
#[component]

View File

@ -171,6 +171,11 @@ pub use leptos_dom::{
Class, CollectView, Errors, Fragment, HtmlElement, IntoAttribute,
IntoClass, IntoProperty, IntoStyle, IntoView, NodeRef, Property, View,
};
/// Types to make it easier to handle errors in your application.
pub mod error {
pub use server_fn::error::{Error, Result};
}
#[cfg(not(any(target_arch = "wasm32", feature = "template_macro")))]
pub use leptos_macro::view as template;
pub use leptos_macro::{component, server, slot, view, Params};
@ -178,6 +183,7 @@ pub use leptos_reactive::*;
pub use leptos_server::{
self, create_action, create_multi_action, create_server_action,
create_server_multi_action, Action, MultiAction, ServerFn, ServerFnError,
ServerFnErrorErr,
};
pub use server_fn::{self, ServerFn as _};
pub use typed_builder;

View File

@ -18,6 +18,7 @@ indexmap = "1.9"
itertools = "0.10"
js-sys = "0.3"
leptos_reactive = { workspace = true }
server_fn = { workspace = true }
once_cell = "1"
pad-adapter = "0.1"
paste = "1"

View File

@ -1,12 +1,13 @@
use crate::{HydrationCtx, IntoView};
use cfg_if::cfg_if;
use leptos_reactive::{signal_prelude::*, use_context, RwSignal};
use std::{borrow::Cow, collections::HashMap, error::Error, sync::Arc};
use server_fn::error::Error;
use std::{borrow::Cow, collections::HashMap};
/// A struct to hold all the possible errors that could be provided by child Views
#[derive(Debug, Clone, Default)]
#[repr(transparent)]
pub struct Errors(HashMap<ErrorKey, Arc<dyn Error + Send + Sync>>);
pub struct Errors(HashMap<ErrorKey, Error>);
/// A unique key for an error that occurs at a particular location in the user interface.
#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)]
@ -24,7 +25,7 @@ where
}
impl IntoIterator for Errors {
type Item = (ErrorKey, Arc<dyn Error + Send + Sync>);
type Item = (ErrorKey, Error);
type IntoIter = IntoIter;
#[inline(always)]
@ -35,15 +36,10 @@ impl IntoIterator for Errors {
/// An owning iterator over all the errors contained in the [Errors] struct.
#[repr(transparent)]
pub struct IntoIter(
std::collections::hash_map::IntoIter<
ErrorKey,
Arc<dyn Error + Send + Sync>,
>,
);
pub struct IntoIter(std::collections::hash_map::IntoIter<ErrorKey, Error>);
impl Iterator for IntoIter {
type Item = (ErrorKey, Arc<dyn Error + Send + Sync>);
type Item = (ErrorKey, Error);
#[inline(always)]
fn next(
@ -55,16 +51,10 @@ impl Iterator for IntoIter {
/// An iterator over all the errors contained in the [Errors] struct.
#[repr(transparent)]
pub struct Iter<'a>(
std::collections::hash_map::Iter<
'a,
ErrorKey,
Arc<dyn Error + Send + Sync>,
>,
);
pub struct Iter<'a>(std::collections::hash_map::Iter<'a, ErrorKey, Error>);
impl<'a> Iterator for Iter<'a> {
type Item = (&'a ErrorKey, &'a Arc<dyn Error + Send + Sync>);
type Item = (&'a ErrorKey, &'a Error);
#[inline(always)]
fn next(
@ -77,7 +67,7 @@ impl<'a> Iterator for Iter<'a> {
impl<T, E> IntoView for Result<T, E>
where
T: IntoView + 'static,
E: Error + Send + Sync + 'static,
E: Into<Error>,
{
fn into_view(self, cx: leptos_reactive::Scope) -> crate::View {
let id = ErrorKey(HydrationCtx::peek().fragment.to_string().into());
@ -92,6 +82,7 @@ where
stuff.into_view(cx)
}
Err(error) => {
let error = error.into();
match errors {
Some(errors) => {
errors.update({
@ -133,6 +124,7 @@ where
}
}
}
impl Errors {
/// Returns `true` if there are no errors.
#[inline(always)]
@ -143,24 +135,21 @@ impl Errors {
/// Add an error to Errors that will be processed by `<ErrorBoundary/>`
pub fn insert<E>(&mut self, key: ErrorKey, error: E)
where
E: Error + Send + Sync + 'static,
E: Into<Error>,
{
self.0.insert(key, Arc::new(error));
self.0.insert(key, error.into());
}
/// Add an error with the default key for errors outside the reactive system
pub fn insert_with_default_key<E>(&mut self, error: E)
where
E: Error + Send + Sync + 'static,
E: Into<Error>,
{
self.0.insert(Default::default(), Arc::new(error));
self.0.insert(Default::default(), error.into());
}
/// Remove an error to Errors that will be processed by `<ErrorBoundary/>`
pub fn remove(
&mut self,
key: &ErrorKey,
) -> Option<Arc<dyn Error + Send + Sync>> {
pub fn remove(&mut self, key: &ErrorKey) -> Option<Error> {
self.0.remove(key)
}

View File

@ -116,7 +116,9 @@
//! your app is not available.
use leptos_reactive::*;
pub use server_fn::{Encoding, Payload, ServerFnError};
pub use server_fn::{
error::ServerFnErrorErr, Encoding, Payload, ServerFnError,
};
mod action;
mod multi_action;

View File

@ -386,7 +386,7 @@ where
let e = ServerFnError::Request(e.to_string());
value.try_set(Some(Err(e.clone())));
if let Some(error) = error {
error.try_set(Some(Box::new(e)));
error.try_set(Some(Box::new(ServerFnErrorErr::from(e))));
}
});
});
@ -406,7 +406,8 @@ where
cx.batch(move || {
value.try_set(Some(Err(e.clone())));
if let Some(error) = error {
error.try_set(Some(Box::new(e)));
error
.try_set(Some(Box::new(ServerFnErrorErr::from(e))));
}
});
}
@ -472,7 +473,7 @@ where
error!("{e:?}");
if let Some(error) = error {
error.try_set(Some(Box::new(
ServerFnError::Request(
ServerFnErrorErr::Request(
e.as_string().unwrap_or_default(),
),
)));

167
server_fn/src/error.rs Normal file
View File

@ -0,0 +1,167 @@
use serde::{Deserialize, Serialize};
use std::{error, fmt, ops, sync::Arc};
use thiserror::Error;
/// This is a result type into which any error can be converted,
/// and which can be used directly in your `view`.
///
/// All errors will be stored as [`Error`].
pub type Result<T, E = Error> = core::result::Result<T, E>;
/// A generic wrapper for any error.
#[derive(Debug, Clone)]
#[repr(transparent)]
pub struct Error(Arc<dyn error::Error + Send + Sync>);
impl Error {
/// Converts the wrapper into the inner reference-counted error.
pub fn into_inner(self) -> Arc<dyn error::Error + Send + Sync> {
Arc::clone(&self.0)
}
}
impl ops::Deref for Error {
type Target = Arc<dyn error::Error + Send + Sync>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl<T> From<T> for Error
where
T: std::error::Error + Send + Sync + 'static,
{
fn from(value: T) -> Self {
Error(Arc::new(value))
}
}
impl From<ServerFnError> for Error {
fn from(e: ServerFnError) -> Self {
Error(Arc::new(ServerFnErrorErr::from(e)))
}
}
/// Type for errors that can occur when using server functions.
///
/// Unlike [`ServerFnErrorErr`], this does not implement [`std::error::Error`].
/// This means that other error types can easily be converted into it using the
/// `?` operator.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ServerFnError {
/// Error while trying to register the server function (only occurs in case of poisoned RwLock).
Registration(String),
/// Occurs on the client if there is a network error while trying to run function on server.
Request(String),
/// Occurs when there is an error while actually running the function on the server.
ServerError(String),
/// Occurs on the client if there is an error deserializing the server's response.
Deserialization(String),
/// Occurs on the client if there is an error serializing the server function arguments.
Serialization(String),
/// Occurs on the server if there is an error deserializing one of the arguments that's been sent.
Args(String),
/// Occurs on the server if there's a missing argument.
MissingArg(String),
}
impl std::fmt::Display for ServerFnError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}",
match self {
ServerFnError::Registration(s) => format!(
"error while trying to register the server function: {s}"
),
ServerFnError::Request(s) => format!(
"error reaching server to call server function: {s}"
),
ServerFnError::ServerError(s) =>
format!("error running server function: {s}"),
ServerFnError::Deserialization(s) =>
format!("error deserializing server function results: {s}"),
ServerFnError::Serialization(s) =>
format!("error serializing server function arguments: {s}"),
ServerFnError::Args(s) => format!(
"error deserializing server function arguments: {s}"
),
ServerFnError::MissingArg(s) => format!("missing argument {s}"),
}
)
}
}
impl<E> From<E> for ServerFnError
where
E: std::error::Error,
{
fn from(e: E) -> Self {
ServerFnError::ServerError(e.to_string())
}
}
/// Type for errors that can occur when using server functions.
///
/// Unlike [`ServerFnErrorErr`], this implements [`std::error::Error`]. This means
/// it can be used in situations in which the `Error` trait is required, but its
/// not possible to create a blanket implementation that converts other errors into
/// this type.
///
/// [`ServerFnError`] and [`ServerFnErrorErr`] mutually implement [`From`], so
/// it is easy to convert between the two types.
#[derive(Error, Debug, Clone, Serialize, Deserialize)]
pub enum ServerFnErrorErr {
/// Error while trying to register the server function (only occurs in case of poisoned RwLock).
#[error("error while trying to register the server function: {0}")]
Registration(String),
/// Occurs on the client if there is a network error while trying to run function on server.
#[error("error reaching server to call server function: {0}")]
Request(String),
/// Occurs when there is an error while actually running the function on the server.
#[error("error running server function: {0}")]
ServerError(String),
/// Occurs on the client if there is an error deserializing the server's response.
#[error("error deserializing server function results: {0}")]
Deserialization(String),
/// Occurs on the client if there is an error serializing the server function arguments.
#[error("error serializing server function arguments: {0}")]
Serialization(String),
/// Occurs on the server if there is an error deserializing one of the arguments that's been sent.
#[error("error deserializing server function arguments: {0}")]
Args(String),
/// Occurs on the server if there's a missing argument.
#[error("missing argument {0}")]
MissingArg(String),
}
impl From<ServerFnError> for ServerFnErrorErr {
fn from(value: ServerFnError) -> Self {
match value {
ServerFnError::Registration(value) => {
ServerFnErrorErr::Registration(value)
}
ServerFnError::Request(value) => ServerFnErrorErr::Request(value),
ServerFnError::ServerError(value) => {
ServerFnErrorErr::ServerError(value)
}
ServerFnError::Deserialization(value) => {
ServerFnErrorErr::Deserialization(value)
}
ServerFnError::Serialization(value) => {
ServerFnErrorErr::Serialization(value)
}
ServerFnError::Args(value) => ServerFnErrorErr::Args(value),
ServerFnError::MissingArg(value) => {
ServerFnErrorErr::MissingArg(value)
}
}
}
}

View File

@ -90,15 +90,17 @@ use quote::TokenStreamExt;
// used by the macro
#[doc(hidden)]
pub use serde;
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use serde::{de::DeserializeOwned, Serialize};
pub use server_fn_macro_default::server;
use std::{future::Future, pin::Pin, str::FromStr};
#[cfg(any(feature = "ssr", doc))]
use syn::parse_quote;
use thiserror::Error;
// used by the macro
#[doc(hidden)]
pub use xxhash_rust;
/// Error types used in server functions.
pub mod error;
pub use error::ServerFnError;
/// Default server function registry
pub mod default;
@ -451,32 +453,6 @@ where
}
}
/// Type for errors that can occur when using server functions.
#[derive(Error, Debug, Clone, Serialize, Deserialize)]
pub enum ServerFnError {
/// Error while trying to register the server function (only occurs in case of poisoned RwLock).
#[error("error while trying to register the server function: {0}")]
Registration(String),
/// Occurs on the client if there is a network error while trying to run function on server.
#[error("error reaching server to call server function: {0}")]
Request(String),
/// Occurs when there is an error while actually running the function on the server.
#[error("error running server function: {0}")]
ServerError(String),
/// Occurs on the client if there is an error deserializing the server's response.
#[error("error deserializing server function results {0}")]
Deserialization(String),
/// Occurs on the client if there is an error serializing the server function arguments.
#[error("error serializing server function arguments {0}")]
Serialization(String),
/// Occurs on the server if there is an error deserializing one of the arguments that's been sent.
#[error("error deserializing server function arguments {0}")]
Args(String),
/// Occurs on the server if there's a missing argument.
#[error("missing argument {0}")]
MissingArg(String),
}
/// Executes the HTTP call to call a server function from the client, given its URL and argument type.
#[cfg(not(feature = "ssr"))]
pub async fn call_server_fn<T, C: 'static>(
@ -649,7 +625,7 @@ where
// Lazily initialize the client to be reused for all server function calls.
#[cfg(any(all(not(feature = "ssr"), not(target_arch = "wasm32")), doc))]
static CLIENT: once_cell::sync::Lazy<reqwest::Client> =
once_cell::sync::Lazy::new(|| reqwest::Client::new());
once_cell::sync::Lazy::new(reqwest::Client::new);
#[cfg(any(all(not(feature = "ssr"), not(target_arch = "wasm32")), doc))]
static ROOT_URL: once_cell::sync::OnceCell<&'static str> =