From 1dcc5838f73451d722880c47903ef3f7cfdd2b98 Mon Sep 17 00:00:00 2001 From: Greg Johnston Date: Thu, 30 Jan 2025 21:42:39 -0500 Subject: [PATCH 01/17] `v0.7.5` --- Cargo.lock | 48 ++++++++++++++++---------------- Cargo.toml | 32 ++++++++++----------- either_of/Cargo.toml | 4 +-- leptos_macro/Cargo.toml | 2 +- meta/Cargo.toml | 2 +- reactive_graph/Cargo.toml | 2 +- reactive_stores/Cargo.toml | 2 +- reactive_stores_macro/Cargo.toml | 2 +- router/Cargo.toml | 2 +- router_macro/Cargo.toml | 2 +- tachys/Cargo.toml | 3 +- 11 files changed, 50 insertions(+), 51 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 60472dfb13..bd5b32e530 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -333,9 +333,9 @@ checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" [[package]] name = "async-trait" -version = "0.1.83" +version = "0.1.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" +checksum = "3f934833b4b7233644e5848f235df3f57ed8c80f1528a26c3dfa13d2147fa056" dependencies = [ "proc-macro2", "quote", @@ -879,7 +879,7 @@ checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" [[package]] name = "either_of" -version = "0.1.4" +version = "0.1.5" dependencies = [ "paste", "pin-project-lite", @@ -1718,7 +1718,7 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "leptos" -version = "0.7.4" +version = "0.7.5" dependencies = [ "any_spawner", "base64", @@ -1769,7 +1769,7 @@ dependencies = [ [[package]] name = "leptos_actix" -version = "0.7.4" +version = "0.7.5" dependencies = [ "actix-files", "actix-http", @@ -1794,7 +1794,7 @@ dependencies = [ [[package]] name = "leptos_axum" -version = "0.7.4" +version = "0.7.5" dependencies = [ "any_spawner", "axum", @@ -1817,7 +1817,7 @@ dependencies = [ [[package]] name = "leptos_config" -version = "0.7.4" +version = "0.7.5" dependencies = [ "config", "regex", @@ -1831,7 +1831,7 @@ dependencies = [ [[package]] name = "leptos_dom" -version = "0.7.4" +version = "0.7.5" dependencies = [ "js-sys", "leptos", @@ -1848,7 +1848,7 @@ dependencies = [ [[package]] name = "leptos_hot_reload" -version = "0.7.4" +version = "0.7.5" dependencies = [ "anyhow", "camino", @@ -1864,7 +1864,7 @@ dependencies = [ [[package]] name = "leptos_integration_utils" -version = "0.7.4" +version = "0.7.5" dependencies = [ "futures", "hydration_context", @@ -1877,7 +1877,7 @@ dependencies = [ [[package]] name = "leptos_macro" -version = "0.7.4" +version = "0.7.5" dependencies = [ "attribute-derive", "cfg-if", @@ -1896,7 +1896,7 @@ dependencies = [ "rstml", "serde", "server_fn", - "server_fn_macro 0.7.4", + "server_fn_macro 0.7.5", "syn 2.0.90", "tracing", "trybuild", @@ -1906,7 +1906,7 @@ dependencies = [ [[package]] name = "leptos_meta" -version = "0.7.4" +version = "0.7.5" dependencies = [ "futures", "indexmap", @@ -1921,7 +1921,7 @@ dependencies = [ [[package]] name = "leptos_router" -version = "0.7.4" +version = "0.7.5" dependencies = [ "any_spawner", "either_of", @@ -1945,7 +1945,7 @@ dependencies = [ [[package]] name = "leptos_router_macro" -version = "0.7.4" +version = "0.7.5" dependencies = [ "leptos_macro", "leptos_router", @@ -1957,7 +1957,7 @@ dependencies = [ [[package]] name = "leptos_server" -version = "0.7.4" +version = "0.7.5" dependencies = [ "any_spawner", "base64", @@ -2659,7 +2659,7 @@ dependencies = [ [[package]] name = "reactive_graph" -version = "0.1.4" +version = "0.1.5" dependencies = [ "any_spawner", "async-lock", @@ -2681,7 +2681,7 @@ dependencies = [ [[package]] name = "reactive_stores" -version = "0.1.3" +version = "0.1.5" dependencies = [ "any_spawner", "guardian", @@ -2698,7 +2698,7 @@ dependencies = [ [[package]] name = "reactive_stores_macro" -version = "0.1.0" +version = "0.1.5" dependencies = [ "convert_case 0.6.0", "proc-macro-error2", @@ -3155,7 +3155,7 @@ dependencies = [ [[package]] name = "server_fn" -version = "0.7.4" +version = "0.7.5" dependencies = [ "actix-web", "axum", @@ -3211,7 +3211,7 @@ dependencies = [ [[package]] name = "server_fn_macro" -version = "0.7.4" +version = "0.7.5" dependencies = [ "const_format", "convert_case 0.6.0", @@ -3223,9 +3223,9 @@ dependencies = [ [[package]] name = "server_fn_macro_default" -version = "0.7.4" +version = "0.7.5" dependencies = [ - "server_fn_macro 0.7.4", + "server_fn_macro 0.7.5", "syn 2.0.90", ] @@ -3419,7 +3419,7 @@ dependencies = [ [[package]] name = "tachys" -version = "0.1.4" +version = "0.1.5" dependencies = [ "any_spawner", "async-trait", diff --git a/Cargo.toml b/Cargo.toml index 4cee16ee7a..51ce42b271 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,7 +40,7 @@ members = [ exclude = ["benchmarks", "examples", "projects"] [workspace.package] -version = "0.7.4" +version = "0.7.5" edition = "2021" rust-version = "1.76" @@ -50,26 +50,26 @@ any_spawner = { path = "./any_spawner/", version = "0.2.0" } const_str_slice_concat = { path = "./const_str_slice_concat", version = "0.1" } either_of = { path = "./either_of/", version = "0.1.0" } hydration_context = { path = "./hydration_context", version = "0.2.0" } -leptos = { path = "./leptos", version = "0.7.4" } -leptos_config = { path = "./leptos_config", version = "0.7.4" } -leptos_dom = { path = "./leptos_dom", version = "0.7.4" } -leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.7.4" } -leptos_integration_utils = { path = "./integrations/utils", version = "0.7.4" } -leptos_macro = { path = "./leptos_macro", version = "0.7.4" } -leptos_router = { path = "./router", version = "0.7.4" } -leptos_router_macro = { path = "./router_macro", version = "0.7.4" } -leptos_server = { path = "./leptos_server", version = "0.7.4" } -leptos_meta = { path = "./meta", version = "0.7.4" } +leptos = { path = "./leptos", version = "0.7.5" } +leptos_config = { path = "./leptos_config", version = "0.7.5" } +leptos_dom = { path = "./leptos_dom", version = "0.7.5" } +leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.7.5" } +leptos_integration_utils = { path = "./integrations/utils", version = "0.7.5" } +leptos_macro = { path = "./leptos_macro", version = "0.7.5" } +leptos_router = { path = "./router", version = "0.7.5" } +leptos_router_macro = { path = "./router_macro", version = "0.7.5" } +leptos_server = { path = "./leptos_server", version = "0.7.5" } +leptos_meta = { path = "./meta", version = "0.7.5" } next_tuple = { path = "./next_tuple", version = "0.1.0" } oco_ref = { path = "./oco", version = "0.2.0" } or_poisoned = { path = "./or_poisoned", version = "0.1.0" } -reactive_graph = { path = "./reactive_graph", version = "0.1.4" } +reactive_graph = { path = "./reactive_graph", version = "0.1.5" } reactive_stores = { path = "./reactive_stores", version = "0.1.3" } reactive_stores_macro = { path = "./reactive_stores_macro", version = "0.1.0" } -server_fn = { path = "./server_fn", version = "0.7.4" } -server_fn_macro = { path = "./server_fn_macro", version = "0.7.4" } -server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.7.4" } -tachys = { path = "./tachys", version = "0.1.4" } +server_fn = { path = "./server_fn", version = "0.7.5" } +server_fn_macro = { path = "./server_fn_macro", version = "0.7.5" } +server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.7.5" } +tachys = { path = "./tachys", version = "0.1.5" } [profile.release] codegen-units = 1 diff --git a/either_of/Cargo.toml b/either_of/Cargo.toml index bc120cee45..15e0d145bf 100644 --- a/either_of/Cargo.toml +++ b/either_of/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "either_of" -version = "0.1.4" +version = "0.1.5" authors = ["Greg Johnston"] license = "MIT" readme = "../README.md" @@ -15,4 +15,4 @@ paste = "1.0.15" [features] default = ["no_std"] -no_std = [] \ No newline at end of file +no_std = [] diff --git a/leptos_macro/Cargo.toml b/leptos_macro/Cargo.toml index 9fd20ed7ea..e3f6a43872 100644 --- a/leptos_macro/Cargo.toml +++ b/leptos_macro/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "leptos_macro" -version = "0.7.4" +version = "0.7.5" authors = ["Greg Johnston"] license = "MIT" repository = "https://github.com/leptos-rs/leptos" diff --git a/meta/Cargo.toml b/meta/Cargo.toml index 1f0d0f3fb5..697f02ad49 100644 --- a/meta/Cargo.toml +++ b/meta/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "leptos_meta" -version = "0.7.4" +version = "0.7.5" authors = ["Greg Johnston"] license = "MIT" repository = "https://github.com/leptos-rs/leptos" diff --git a/reactive_graph/Cargo.toml b/reactive_graph/Cargo.toml index 53d3ebb3b5..165f999522 100644 --- a/reactive_graph/Cargo.toml +++ b/reactive_graph/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "reactive_graph" -version = "0.1.4" +version = "0.1.5" authors = ["Greg Johnston"] license = "MIT" readme = "../README.md" diff --git a/reactive_stores/Cargo.toml b/reactive_stores/Cargo.toml index fb2e31a671..4455a91988 100644 --- a/reactive_stores/Cargo.toml +++ b/reactive_stores/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "reactive_stores" -version = "0.1.3" +version = "0.1.5" authors = ["Greg Johnston"] license = "MIT" readme = "../README.md" diff --git a/reactive_stores_macro/Cargo.toml b/reactive_stores_macro/Cargo.toml index 5b79d5c7fd..b7a1823a13 100644 --- a/reactive_stores_macro/Cargo.toml +++ b/reactive_stores_macro/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "reactive_stores_macro" -version = "0.1.0" +version = "0.1.5" authors = ["Greg Johnston"] license = "MIT" readme = "../README.md" diff --git a/router/Cargo.toml b/router/Cargo.toml index 0251832e2e..04e8465521 100644 --- a/router/Cargo.toml +++ b/router/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "leptos_router" -version = "0.7.4" +version = "0.7.5" authors = ["Greg Johnston", "Ben Wishovich"] license = "MIT" readme = "../README.md" diff --git a/router_macro/Cargo.toml b/router_macro/Cargo.toml index df4e281bea..765324fb24 100644 --- a/router_macro/Cargo.toml +++ b/router_macro/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "leptos_router_macro" -version = "0.7.4" +version = "0.7.5" authors = ["Greg Johnston", "Ben Wishovich"] license = "MIT" readme = "../README.md" diff --git a/tachys/Cargo.toml b/tachys/Cargo.toml index f71678fd45..26b1f6f0f3 100644 --- a/tachys/Cargo.toml +++ b/tachys/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tachys" -version = "0.1.4" +version = "0.1.5" authors = ["Greg Johnston"] license = "MIT" readme = "../README.md" @@ -204,4 +204,3 @@ unexpected_cfgs = { level = "warn", check-cfg = [ 'cfg(leptos_debuginfo)', 'cfg(erase_components)', ] } - From e3010c7f1f49875f40379bfad0fa4df3c9dfa596 Mon Sep 17 00:00:00 2001 From: Serhii Stepanchuk <96484585+sstepanchuk@users.noreply.github.com> Date: Fri, 31 Jan 2025 19:33:55 +0200 Subject: [PATCH 02/17] feat: add `file_and_error_handler_with_context` (#3526) * add file_and_error_handler_with_context like in leptos_routes_with_context func * [autofix.ci] apply automated fixes --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- integrations/axum/src/lib.rs | 89 ++++++++++++++++++++++++------------ 1 file changed, 59 insertions(+), 30 deletions(-) diff --git a/integrations/axum/src/lib.rs b/integrations/axum/src/lib.rs index 80bc2da22b..485edd5f4b 100644 --- a/integrations/axum/src/lib.rs +++ b/integrations/axum/src/lib.rs @@ -1981,7 +1981,8 @@ where /// This is provided as a convenience, but is a fairly simple function. If you need to adapt it, /// simply reuse the source code of this function in your own application. #[cfg(feature = "default")] -pub fn file_and_error_handler( +pub fn file_and_error_handler_with_context( + additional_context: impl Fn() + 'static + Clone + Send, shell: fn(LeptosOptions) -> IV, ) -> impl Fn( Uri, @@ -1997,40 +1998,68 @@ where LeptosOptions: FromRef, { move |uri: Uri, State(state): State, req: Request| { - Box::pin(async move { - let options = LeptosOptions::from_ref(&state); - let res = get_static_file(uri, &options.site_root, req.headers()); - let res = res.await.unwrap(); - - if res.status() == StatusCode::OK { - res.into_response() - } else { - let mut res = handle_response_inner( - move || { - provide_context(state.clone()); - }, - move || shell(options), - req, - |app, chunks| { - Box::pin(async move { - let app = app - .to_html_stream_in_order() - .collect::() - .await; - let chunks = chunks(); - Box::pin(once(async move { app }).chain(chunks)) - as PinnedStream - }) - }, - ) - .await; - *res.status_mut() = StatusCode::NOT_FOUND; - res + Box::pin({ + let additional_context = additional_context.clone(); + async move { + let options = LeptosOptions::from_ref(&state); + let res = + get_static_file(uri, &options.site_root, req.headers()); + let res = res.await.unwrap(); + + if res.status() == StatusCode::OK { + res.into_response() + } else { + let mut res = handle_response_inner( + move || { + additional_context(); + provide_context(state.clone()); + }, + move || shell(options), + req, + |app, chunks| { + Box::pin(async move { + let app = app + .to_html_stream_in_order() + .collect::() + .await; + let chunks = chunks(); + Box::pin(once(async move { app }).chain(chunks)) + as PinnedStream + }) + }, + ) + .await; + *res.status_mut() = StatusCode::NOT_FOUND; + res + } } }) } } +/// A reasonable handler for serving static files (like JS/WASM/CSS) and 404 errors. +/// +/// This is provided as a convenience, but is a fairly simple function. If you need to adapt it, +/// simply reuse the source code of this function in your own application. +#[cfg(feature = "default")] +pub fn file_and_error_handler( + shell: fn(LeptosOptions) -> IV, +) -> impl Fn( + Uri, + State, + Request, +) -> Pin> + Send + 'static>> + + Clone + + Send + + 'static +where + IV: IntoView + 'static, + S: Send + Sync + Clone + 'static, + LeptosOptions: FromRef, +{ + file_and_error_handler_with_context(move || (), shell) +} + #[cfg(feature = "default")] async fn get_static_file( uri: Uri, From d9043e4f34371f14567133f8d0fc2347fbb6f949 Mon Sep 17 00:00:00 2001 From: Greg Johnston Date: Sat, 1 Feb 2025 09:37:38 -0500 Subject: [PATCH 03/17] feat: impl `From>` for `Field` (#3533) * feat: impl `From>` for `Field` * [autofix.ci] apply automated fixes --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- reactive_stores/src/field.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/reactive_stores/src/field.rs b/reactive_stores/src/field.rs index 644ab8ca3e..ad4fa09b09 100644 --- a/reactive_stores/src/field.rs +++ b/reactive_stores/src/field.rs @@ -82,6 +82,21 @@ where } } +impl From> for Field +where + T: 'static, + S: Storage>, +{ + #[track_caller] + fn from(value: ArcField) -> Self { + Field { + #[cfg(any(debug_assertions, leptos_debuginfo))] + defined_at: Location::caller(), + inner: ArenaItem::new_with_storage(value), + } + } +} + impl From> for Field where T: Send + Sync + 'static, From 32be3a023a5413fe17f81d9a0a76f719c1ad4e82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Radi=C4=8Dek?= Date: Sat, 1 Feb 2025 15:40:43 +0100 Subject: [PATCH 04/17] feat: implement `PatchField` for `Option<_>` (#3528) --- reactive_stores/src/option.rs | 114 +++++++++++++++++++++++++++++++++- reactive_stores/src/patch.rs | 29 +++++++++ 2 files changed, 142 insertions(+), 1 deletion(-) diff --git a/reactive_stores/src/option.rs b/reactive_stores/src/option.rs index 439eceb5b8..8343c584ea 100644 --- a/reactive_stores/src/option.rs +++ b/reactive_stores/src/option.rs @@ -77,11 +77,12 @@ where #[cfg(test)] mod tests { - use crate::{self as reactive_stores, Store}; + use crate::{self as reactive_stores, Patch as _, Store}; use reactive_graph::{ effect::Effect, traits::{Get, Read, ReadUntracked, Set, Write}, }; + use reactive_stores_macro::Patch; use std::sync::{ atomic::{AtomicUsize, Ordering}, Arc, @@ -237,4 +238,115 @@ mod tests { assert_eq!(parent_count.load(Ordering::Relaxed), 3); assert_eq!(inner_count.load(Ordering::Relaxed), 3); } + + #[tokio::test] + async fn patch() { + use crate::OptionStoreExt; + + #[derive(Debug, Clone, Store, Patch)] + struct Outer { + inner: Option, + } + + #[derive(Debug, Clone, Store, Patch)] + struct Inner { + first: String, + second: String, + } + + let store = Store::new(Outer { + inner: Some(Inner { + first: "A".to_owned(), + second: "B".to_owned(), + }), + }); + + _ = any_spawner::Executor::init_tokio(); + + let parent_count = Arc::new(AtomicUsize::new(0)); + let inner_first_count = Arc::new(AtomicUsize::new(0)); + let inner_second_count = Arc::new(AtomicUsize::new(0)); + + Effect::new_sync({ + let parent_count = Arc::clone(&parent_count); + move |prev: Option<()>| { + if prev.is_none() { + println!("parent: first run"); + } else { + println!("parent: next run"); + } + + println!(" value = {:?}", store.inner().get()); + parent_count.fetch_add(1, Ordering::Relaxed); + } + }); + Effect::new_sync({ + let inner_first_count = Arc::clone(&inner_first_count); + move |prev: Option<()>| { + if prev.is_none() { + println!("inner_first: first run"); + } else { + println!("inner_first: next run"); + } + + println!( + " value = {:?}", + store.inner().map(|inner| inner.first().get()) + ); + inner_first_count.fetch_add(1, Ordering::Relaxed); + } + }); + Effect::new_sync({ + let inner_second_count = Arc::clone(&inner_second_count); + move |prev: Option<()>| { + if prev.is_none() { + println!("inner_second: first run"); + } else { + println!("inner_second: next run"); + } + + println!( + " value = {:?}", + store.inner().map(|inner| inner.second().get()) + ); + inner_second_count.fetch_add(1, Ordering::Relaxed); + } + }); + + tick().await; + assert_eq!(parent_count.load(Ordering::Relaxed), 1); + assert_eq!(inner_first_count.load(Ordering::Relaxed), 1); + assert_eq!(inner_second_count.load(Ordering::Relaxed), 1); + + store.patch(Outer { + inner: Some(Inner { + first: "A".to_string(), + second: "C".to_string(), + }), + }); + + tick().await; + assert_eq!(parent_count.load(Ordering::Relaxed), 1); + assert_eq!(inner_first_count.load(Ordering::Relaxed), 1); + assert_eq!(inner_second_count.load(Ordering::Relaxed), 2); + + store.patch(Outer { inner: None }); + + tick().await; + assert_eq!(parent_count.load(Ordering::Relaxed), 2); + assert_eq!(inner_first_count.load(Ordering::Relaxed), 2); + assert_eq!(inner_second_count.load(Ordering::Relaxed), 3); + + store.patch(Outer { + inner: Some(Inner { + first: "A".to_string(), + second: "B".to_string(), + }), + }); + + tick().await; + assert_eq!(parent_count.load(Ordering::Relaxed), 3); + assert_eq!(inner_first_count.load(Ordering::Relaxed), 3); + assert_eq!(inner_second_count.load(Ordering::Relaxed), 4); + } } diff --git a/reactive_stores/src/patch.rs b/reactive_stores/src/patch.rs index f88fa8b585..7c65467115 100644 --- a/reactive_stores/src/patch.rs +++ b/reactive_stores/src/patch.rs @@ -114,6 +114,35 @@ patch_primitives! { NonZeroUsize } +impl PatchField for Option +where + T: PatchField, +{ + fn patch_field( + &mut self, + new: Self, + path: &StorePath, + notify: &mut dyn FnMut(&StorePath), + ) { + match (self, new) { + (None, None) => {} + (old @ Some(_), None) => { + old.take(); + notify(path); + } + (old @ None, new @ Some(_)) => { + *old = new; + notify(path); + } + (Some(old), Some(new)) => { + let mut new_path = path.to_owned(); + new_path.push(0); + old.patch_field(new, &new_path, notify); + } + } + } +} + impl PatchField for Vec where T: PatchField, From 6154199850b0eb78883128684bc9f70b1009cb94 Mon Sep 17 00:00:00 2001 From: Greg Johnston Date: Sat, 1 Feb 2025 11:32:32 -0500 Subject: [PATCH 05/17] fix: attribute type erasure nightly (closes #3536) (#3537) --- tachys/src/view/static_types.rs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/tachys/src/view/static_types.rs b/tachys/src/view/static_types.rs index 234b3409a0..c1d18c26a8 100644 --- a/tachys/src/view/static_types.rs +++ b/tachys/src/view/static_types.rs @@ -3,7 +3,12 @@ use super::{ RenderHtml, ToTemplate, }; use crate::{ - html::attribute::{Attribute, AttributeKey, AttributeValue, NextAttribute}, + html::attribute::{ + maybe_next_attr_erasure_macros::{ + next_attr_combine, next_attr_output_type, + }, + Attribute, AttributeKey, AttributeValue, NextAttribute, + }, hydration::Cursor, renderer::{CastFrom, Rndr}, }; @@ -111,13 +116,13 @@ impl NextAttribute for StaticAttr where K: AttributeKey, { - type Output = (Self, NewAttr); + next_attr_output_type!(Self, NewAttr); fn add_any_attr( self, new_attr: NewAttr, ) -> Self::Output { - (StaticAttr:: { ty: PhantomData }, new_attr) + next_attr_combine!(StaticAttr:: { ty: PhantomData }, new_attr) } } From c6de7c714ea8f4022798afa2a56df336a844cc93 Mon Sep 17 00:00:00 2001 From: Greg Johnston Date: Sat, 1 Feb 2025 15:29:40 -0500 Subject: [PATCH 06/17] fix: emit syntax errors in components rather than swallowing them (closes #3535) (#3538) --- leptos_macro/src/lib.rs | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/leptos_macro/src/lib.rs b/leptos_macro/src/lib.rs index 3599940884..dfd8851507 100644 --- a/leptos_macro/src/lib.rs +++ b/leptos_macro/src/lib.rs @@ -677,17 +677,21 @@ fn component_macro( #[allow(non_snake_case, dead_code, clippy::too_many_arguments, clippy::needless_lifetimes)] #unexpanded } - } else if let Ok(mut dummy) = dummy { - dummy.sig.ident = unmodified_fn_name_from_fn_name(&dummy.sig.ident); - quote! { - #[doc(hidden)] - #[allow(non_snake_case, dead_code, clippy::too_many_arguments, clippy::needless_lifetimes)] - #dummy - } } else { - quote! {} - } - .into() + match dummy { + Ok(mut dummy) => { + dummy.sig.ident = unmodified_fn_name_from_fn_name(&dummy.sig.ident); + quote! { + #[doc(hidden)] + #[allow(non_snake_case, dead_code, clippy::too_many_arguments, clippy::needless_lifetimes)] + #dummy + } + } + Err(e) => { + proc_macro_error2::abort!(e.span(), e); + } + } + }.into() } /// Annotates a struct so that it can be used with your Component as a `slot`. From 3adccf8e6db7b4578418e78537559eb67fe6acff Mon Sep 17 00:00:00 2001 From: Ryo Hirayama Date: Sat, 18 Jan 2025 03:30:12 +0900 Subject: [PATCH 07/17] feat: allow any type that implements FromServerFnError as a replacement of the ServerFnError in server_fn (#3274) --- Cargo.lock | 1 + examples/server_fns_axum/src/app.rs | 106 ++++++++--- integrations/actix/src/lib.rs | 1 - integrations/axum/src/lib.rs | 2 - leptos/src/form.rs | 16 +- leptos/src/lib.rs | 2 +- leptos_server/src/action.rs | 18 +- leptos_server/src/multi_action.rs | 12 +- server_fn/Cargo.toml | 1 + server_fn/src/client.rs | 40 ++-- server_fn/src/codec/cbor.rs | 64 ++++--- server_fn/src/codec/json.rs | 129 ++++++------- server_fn/src/codec/mod.rs | 64 +++---- server_fn/src/codec/msgpack.rs | 53 +++--- server_fn/src/codec/multipart.rs | 22 +-- server_fn/src/codec/postcard.rs | 53 +++--- server_fn/src/codec/rkyv.rs | 55 +++--- server_fn/src/codec/serde_lite.rs | 80 ++++---- server_fn/src/codec/stream.rs | 125 ++++++------- server_fn/src/codec/url.rs | 70 +++---- server_fn/src/error.rs | 280 +++++++++++++++++++--------- server_fn/src/lib.rs | 88 ++++----- server_fn/src/middleware/mod.rs | 71 ++++--- server_fn/src/request/actix.rs | 48 +++-- server_fn/src/request/axum.rs | 35 ++-- server_fn/src/request/browser.rs | 67 +++++-- server_fn/src/request/generic.rs | 24 +-- server_fn/src/request/mod.rs | 44 ++--- server_fn/src/request/reqwest.rs | 51 +++-- server_fn/src/request/spin.rs | 24 +-- server_fn/src/response/actix.rs | 36 ++-- server_fn/src/response/browser.rs | 42 ++--- server_fn/src/response/generic.rs | 51 +++-- server_fn/src/response/http.rs | 52 +++--- server_fn/src/response/mod.rs | 63 +++---- server_fn/src/response/reqwest.rs | 31 ++- server_fn_macro/src/lib.rs | 40 +--- 37 files changed, 1053 insertions(+), 908 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bd5b32e530..b48de82a7b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3159,6 +3159,7 @@ version = "0.7.5" dependencies = [ "actix-web", "axum", + "base64", "bytes", "ciborium", "const_format", diff --git a/examples/server_fns_axum/src/app.rs b/examples/server_fns_axum/src/app.rs index 75f5c8fe79..2dc26bacc3 100644 --- a/examples/server_fns_axum/src/app.rs +++ b/examples/server_fns_axum/src/app.rs @@ -9,8 +9,9 @@ use server_fn::{ MultipartFormData, Postcard, Rkyv, SerdeLite, StreamingText, TextStream, }, + error::{FromServerFnError, IntoAppError, ServerFnErrorErr}, request::{browser::BrowserRequest, ClientReq, Req}, - response::{browser::BrowserResponse, ClientRes, Res}, + response::{browser::BrowserResponse, ClientRes, TryRes}, }; use std::future::Future; #[cfg(feature = "ssr")] @@ -652,32 +653,72 @@ pub fn FileWatcher() -> impl IntoView { /// implementations if you'd like. However, it's much lighter weight to use something like `strum` /// simply to generate those trait implementations. #[server] -pub async fn ascii_uppercase( - text: String, -) -> Result> { +pub async fn ascii_uppercase(text: String) -> Result { + other_error()?; + Ok(ascii_uppercase_inner(text)?) +} + +pub fn other_error() -> Result<(), String> { + Ok(()) +} + +pub fn ascii_uppercase_inner(text: String) -> Result { if text.len() < 5 { - Err(InvalidArgument::TooShort.into()) + Err(InvalidArgument::TooShort) } else if text.len() > 15 { - Err(InvalidArgument::TooLong.into()) + Err(InvalidArgument::TooLong) } else if text.is_ascii() { Ok(text.to_ascii_uppercase()) } else { - Err(InvalidArgument::NotAscii.into()) + Err(InvalidArgument::NotAscii) } } +#[server] +pub async fn ascii_uppercase_classic( + text: String, +) -> Result> { + Ok(ascii_uppercase_inner(text)?) +} + // The EnumString and Display derive macros are provided by strum -#[derive(Debug, Clone, EnumString, Display)] +#[derive(Debug, Clone, Display, EnumString, Serialize, Deserialize)] pub enum InvalidArgument { TooShort, TooLong, NotAscii, } +#[derive(Debug, Clone, Display, Serialize, Deserialize)] +pub enum MyErrors { + InvalidArgument(InvalidArgument), + ServerFnError(ServerFnErrorErr), + Other(String), +} + +impl From for MyErrors { + fn from(value: InvalidArgument) -> Self { + MyErrors::InvalidArgument(value) + } +} + +impl From for MyErrors { + fn from(value: String) -> Self { + MyErrors::Other(value) + } +} + +impl FromServerFnError for MyErrors { + fn from_server_fn_error(value: ServerFnErrorErr) -> Self { + MyErrors::ServerFnError(value) + } +} + #[component] pub fn CustomErrorTypes() -> impl IntoView { let input_ref = NodeRef::::new(); let (result, set_result) = signal(None); + let (result_classic, set_result_classic) = signal(None); view! {

Using custom error types

@@ -692,14 +733,17 @@ pub fn CustomErrorTypes() -> impl IntoView {

{move || format!("{:?}", result.get())}

+

{move || format!("{:?}", result_classic.get())}

} } @@ -726,14 +770,12 @@ impl IntoReq for TomlEncoded where Request: ClientReq, T: Serialize, + Err: FromServerFnError, { - fn into_req( - self, - path: &str, - accepts: &str, - ) -> Result> { - let data = toml::to_string(&self.0) - .map_err(|e| ServerFnError::Serialization(e.to_string()))?; + fn into_req(self, path: &str, accepts: &str) -> Result { + let data = toml::to_string(&self.0).map_err(|e| { + ServerFnErrorErr::Serialization(e.to_string()).into_app_error() + })?; Request::try_new_post(path, Toml::CONTENT_TYPE, accepts, data) } } @@ -742,23 +784,26 @@ impl FromReq for TomlEncoded where Request: Req + Send, T: DeserializeOwned, + Err: FromServerFnError, { - async fn from_req(req: Request) -> Result> { + async fn from_req(req: Request) -> Result { let string_data = req.try_into_string().await?; toml::from_str::(&string_data) .map(TomlEncoded) - .map_err(|e| ServerFnError::Args(e.to_string())) + .map_err(|e| ServerFnErrorErr::Args(e.to_string()).into_app_error()) } } impl IntoRes for TomlEncoded where - Response: Res, + Response: TryRes, T: Serialize + Send, + Err: FromServerFnError, { - async fn into_res(self) -> Result> { - let data = toml::to_string(&self.0) - .map_err(|e| ServerFnError::Serialization(e.to_string()))?; + async fn into_res(self) -> Result { + let data = toml::to_string(&self.0).map_err(|e| { + ServerFnErrorErr::Serialization(e.to_string()).into_app_error() + })?; Response::try_from_string(Toml::CONTENT_TYPE, data) } } @@ -767,12 +812,13 @@ impl FromRes for TomlEncoded where Response: ClientRes + Send, T: DeserializeOwned, + Err: FromServerFnError, { - async fn from_res(res: Response) -> Result> { + async fn from_res(res: Response) -> Result { let data = res.try_into_string().await?; - toml::from_str(&data) - .map(TomlEncoded) - .map_err(|e| ServerFnError::Deserialization(e.to_string())) + toml::from_str(&data).map(TomlEncoded).map_err(|e| { + ServerFnErrorErr::Deserialization(e.to_string()).into_app_error() + }) } } @@ -835,7 +881,10 @@ pub fn CustomClientExample() -> impl IntoView { pub struct CustomClient; // Implement the `Client` trait for it. - impl Client for CustomClient { + impl Client for CustomClient + where + E: FromServerFnError, + { // BrowserRequest and BrowserResponse are the defaults used by other server functions. // They are wrappers for the underlying Web Fetch API types. type Request = BrowserRequest; @@ -844,8 +893,7 @@ pub fn CustomClientExample() -> impl IntoView { // Our custom `send()` implementation does all the work. fn send( req: Self::Request, - ) -> impl Future>> - + Send { + ) -> impl Future> + Send { // BrowserRequest derefs to the underlying Request type from gloo-net, // so we can get access to the headers here let headers = req.headers(); diff --git a/integrations/actix/src/lib.rs b/integrations/actix/src/lib.rs index bdf9d61a9d..b12cabb45f 100644 --- a/integrations/actix/src/lib.rs +++ b/integrations/actix/src/lib.rs @@ -369,7 +369,6 @@ pub fn handle_server_fns_with_context( // actually run the server fn let mut res = ActixResponse( service - .0 .run(ActixRequest::from((req, payload))) .await .take(), diff --git a/integrations/axum/src/lib.rs b/integrations/axum/src/lib.rs index 485edd5f4b..ca6a9086dc 100644 --- a/integrations/axum/src/lib.rs +++ b/integrations/axum/src/lib.rs @@ -368,8 +368,6 @@ async fn handle_server_fns_inner( additional_context: impl Fn() + 'static + Clone + Send, req: Request, ) -> impl IntoResponse { - use server_fn::middleware::Service; - let method = req.method().clone(); let path = req.uri().path().to_string(); let (req, parts) = generate_request_and_parts(req); diff --git a/leptos/src/form.rs b/leptos/src/form.rs index 37c483aba4..a29eaf8a9e 100644 --- a/leptos/src/form.rs +++ b/leptos/src/form.rs @@ -3,7 +3,11 @@ use leptos_dom::helpers::window; use leptos_server::{ServerAction, ServerMultiAction}; use serde::de::DeserializeOwned; use server_fn::{ - client::Client, codec::PostUrl, request::ClientReq, ServerFn, ServerFnError, + client::Client, + codec::PostUrl, + error::{IntoAppError, ServerFnErrorErr}, + request::ClientReq, + ServerFn, }; use tachys::{ either::Either, @@ -121,9 +125,10 @@ where "Error converting form field into server function \ arguments: {err:?}" ); - value.set(Some(Err(ServerFnError::Serialization( + value.set(Some(Err(ServerFnErrorErr::Serialization( err.to_string(), - )))); + ) + .into_app_error()))); version.update(|n| *n += 1); } } @@ -187,9 +192,10 @@ where action.dispatch(new_input); } Err(err) => { - action.dispatch_sync(Err(ServerFnError::Serialization( + action.dispatch_sync(Err(ServerFnErrorErr::Serialization( err.to_string(), - ))); + ) + .into_app_error())); } } }; diff --git a/leptos/src/lib.rs b/leptos/src/lib.rs index 57da2bafad..775da9271e 100644 --- a/leptos/src/lib.rs +++ b/leptos/src/lib.rs @@ -172,7 +172,7 @@ pub mod prelude { actions::*, computed::*, effect::*, graph::untrack, owner::*, signal::*, wrappers::read::*, }; - pub use server_fn::{self, ServerFnError}; + pub use server_fn::{self, error::ServerFnError}; pub use tachys::{ reactive_graph::{bind::BindAttribute, node_ref::*, Suspend}, view::{ diff --git a/leptos_server/src/action.rs b/leptos_server/src/action.rs index c66e751174..34ae0a2087 100644 --- a/leptos_server/src/action.rs +++ b/leptos_server/src/action.rs @@ -3,7 +3,7 @@ use reactive_graph::{ owner::use_context, traits::DefinedAt, }; -use server_fn::{error::ServerFnErrorSerde, ServerFn, ServerFnError}; +use server_fn::{error::FromServerFnError, ServerFn}; use std::{ops::Deref, panic::Location, sync::Arc}; /// An error that can be caused by a server action. @@ -42,7 +42,7 @@ where S: ServerFn + 'static, S::Output: 'static, { - inner: ArcAction>>, + inner: ArcAction>, #[cfg(any(debug_assertions, leptos_debuginfo))] defined_at: &'static Location<'static>, } @@ -52,13 +52,14 @@ where S: ServerFn + Clone + Send + Sync + 'static, S::Output: Send + Sync + 'static, S::Error: Send + Sync + 'static, + S::Error: FromServerFnError, { /// Creates a new [`ArcAction`] that will call the server function `S` when dispatched. #[track_caller] pub fn new() -> Self { let err = use_context::().and_then(|error| { (error.path() == S::PATH) - .then(|| ServerFnError::::de(error.err())) + .then(|| S::Error::de(error.err())) .map(Err) }); Self { @@ -76,7 +77,7 @@ where S: ServerFn + 'static, S::Output: 'static, { - type Target = ArcAction>>; + type Target = ArcAction>; fn deref(&self) -> &Self::Target { &self.inner @@ -131,7 +132,7 @@ where S: ServerFn + 'static, S::Output: 'static, { - inner: Action>>, + inner: Action>, #[cfg(any(debug_assertions, leptos_debuginfo))] defined_at: &'static Location<'static>, } @@ -146,7 +147,7 @@ where pub fn new() -> Self { let err = use_context::().and_then(|error| { (error.path() == S::PATH) - .then(|| ServerFnError::::de(error.err())) + .then(|| S::Error::de(error.err())) .map(Err) }); Self { @@ -182,15 +183,14 @@ where S::Output: Send + Sync + 'static, S::Error: Send + Sync + 'static, { - type Target = Action>>; + type Target = Action>; fn deref(&self) -> &Self::Target { &self.inner } } -impl From> - for Action>> +impl From> for Action> where S: ServerFn + 'static, S::Output: 'static, diff --git a/leptos_server/src/multi_action.rs b/leptos_server/src/multi_action.rs index e33a873bfd..19799ec26b 100644 --- a/leptos_server/src/multi_action.rs +++ b/leptos_server/src/multi_action.rs @@ -2,7 +2,7 @@ use reactive_graph::{ actions::{ArcMultiAction, MultiAction}, traits::DefinedAt, }; -use server_fn::{ServerFn, ServerFnError}; +use server_fn::ServerFn; use std::{ops::Deref, panic::Location}; /// An [`ArcMultiAction`] that can be used to call a server function. @@ -11,7 +11,7 @@ where S: ServerFn + 'static, S::Output: 'static, { - inner: ArcMultiAction>>, + inner: ArcMultiAction>, #[cfg(any(debug_assertions, leptos_debuginfo))] defined_at: &'static Location<'static>, } @@ -40,7 +40,7 @@ where S: ServerFn + 'static, S::Output: 'static, { - type Target = ArcMultiAction>>; + type Target = ArcMultiAction>; fn deref(&self) -> &Self::Target { &self.inner @@ -95,13 +95,13 @@ where S: ServerFn + 'static, S::Output: 'static, { - inner: MultiAction>>, + inner: MultiAction>, #[cfg(any(debug_assertions, leptos_debuginfo))] defined_at: &'static Location<'static>, } impl From> - for MultiAction>> + for MultiAction> where S: ServerFn + 'static, S::Output: 'static, @@ -152,7 +152,7 @@ where S::Output: 'static, S::Error: 'static, { - type Target = MultiAction>>; + type Target = MultiAction>; fn deref(&self) -> &Self::Target { &self.inner diff --git a/server_fn/Cargo.toml b/server_fn/Cargo.toml index 464341e886..99a205af47 100644 --- a/server_fn/Cargo.toml +++ b/server_fn/Cargo.toml @@ -53,6 +53,7 @@ bytes = "1.9" http-body-util = { version = "0.1.2", optional = true } rkyv = { version = "0.8.9", optional = true } rmp-serde = { version = "1.3.0", optional = true } +base64 = { version = "0.22.1" } # client gloo-net = { version = "0.6.0", optional = true } diff --git a/server_fn/src/client.rs b/server_fn/src/client.rs index 502fd4c4b7..c67a60207c 100644 --- a/server_fn/src/client.rs +++ b/server_fn/src/client.rs @@ -1,4 +1,4 @@ -use crate::{error::ServerFnError, request::ClientReq, response::ClientRes}; +use crate::{request::ClientReq, response::ClientRes}; use std::{future::Future, sync::OnceLock}; static ROOT_URL: OnceLock<&'static str> = OnceLock::new(); @@ -21,16 +21,16 @@ pub fn get_server_url() -> &'static str { /// This trait is implemented for things like a browser `fetch` request or for /// the `reqwest` trait. It should almost never be necessary to implement it /// yourself, unless you’re trying to use an alternative HTTP crate on the client side. -pub trait Client { +pub trait Client { /// The type of a request sent by this client. - type Request: ClientReq + Send; + type Request: ClientReq + Send; /// The type of a response received by this client. - type Response: ClientRes + Send; + type Response: ClientRes + Send; /// Sends the request and receives a response. fn send( req: Self::Request, - ) -> impl Future>> + Send; + ) -> impl Future> + Send; } #[cfg(feature = "browser")] @@ -38,24 +38,23 @@ pub trait Client { pub mod browser { use super::Client; use crate::{ - error::ServerFnError, + error::{FromServerFnError, IntoAppError, ServerFnErrorErr}, request::browser::{BrowserRequest, RequestInner}, response::browser::BrowserResponse, }; use send_wrapper::SendWrapper; use std::future::Future; - /// Implements [`Client`] for a `fetch` request in the browser. + /// Implements [`Client`] for a `fetch` request in the browser. pub struct BrowserClient; - impl Client for BrowserClient { + impl Client for BrowserClient { type Request = BrowserRequest; type Response = BrowserResponse; fn send( req: Self::Request, - ) -> impl Future>> - + Send { + ) -> impl Future> + Send { SendWrapper::new(async move { let req = req.0.take(); let RequestInner { @@ -66,7 +65,10 @@ pub mod browser { .send() .await .map(|res| BrowserResponse(SendWrapper::new(res))) - .map_err(|e| ServerFnError::Request(e.to_string())); + .map_err(|e| { + ServerFnErrorErr::Request(e.to_string()) + .into_app_error() + }); // at this point, the future has successfully resolved without being dropped, so we // can prevent the `AbortController` from firing @@ -83,7 +85,10 @@ pub mod browser { /// Implements [`Client`] for a request made by [`reqwest`]. pub mod reqwest { use super::Client; - use crate::{error::ServerFnError, request::reqwest::CLIENT}; + use crate::{ + error::{FromServerFnError, IntoAppError, ServerFnErrorErr}, + request::reqwest::CLIENT, + }; use futures::TryFutureExt; use reqwest::{Request, Response}; use std::future::Future; @@ -91,17 +96,16 @@ pub mod reqwest { /// Implements [`Client`] for a request made by [`reqwest`]. pub struct ReqwestClient; - impl Client for ReqwestClient { + impl Client for ReqwestClient { type Request = Request; type Response = Response; fn send( req: Self::Request, - ) -> impl Future>> - + Send { - CLIENT - .execute(req) - .map_err(|e| ServerFnError::Request(e.to_string())) + ) -> impl Future> + Send { + CLIENT.execute(req).map_err(|e| { + ServerFnErrorErr::Request(e.to_string()).into_app_error() + }) } } } diff --git a/server_fn/src/codec/cbor.rs b/server_fn/src/codec/cbor.rs index a5a91c8117..d9952a0a48 100644 --- a/server_fn/src/codec/cbor.rs +++ b/server_fn/src/codec/cbor.rs @@ -1,8 +1,8 @@ use super::{Encoding, FromReq, FromRes, IntoReq, IntoRes}; use crate::{ - error::ServerFnError, + error::{FromServerFnError, IntoAppError, ServerFnErrorErr}, request::{ClientReq, Req}, - response::{ClientRes, Res}, + response::{ClientRes, TryRes}, }; use bytes::Bytes; use http::Method; @@ -16,19 +16,17 @@ impl Encoding for Cbor { const METHOD: Method = Method::POST; } -impl IntoReq for T +impl IntoReq for T where - Request: ClientReq, + Request: ClientReq, T: Serialize + Send, + E: FromServerFnError, { - fn into_req( - self, - path: &str, - accepts: &str, - ) -> Result> { + fn into_req(self, path: &str, accepts: &str) -> Result { let mut buffer: Vec = Vec::new(); - ciborium::ser::into_writer(&self, &mut buffer) - .map_err(|e| ServerFnError::Serialization(e.to_string()))?; + ciborium::ser::into_writer(&self, &mut buffer).map_err(|e| { + ServerFnErrorErr::Serialization(e.to_string()).into_app_error() + })?; Request::try_new_post_bytes( path, accepts, @@ -38,40 +36,44 @@ where } } -impl FromReq for T +impl FromReq for T where - Request: Req + Send + 'static, + Request: Req + Send + 'static, T: DeserializeOwned, + E: FromServerFnError, { - async fn from_req(req: Request) -> Result> { + async fn from_req(req: Request) -> Result { let body_bytes = req.try_into_bytes().await?; ciborium::de::from_reader(body_bytes.as_ref()) - .map_err(|e| ServerFnError::Args(e.to_string())) + .map_err(|e| ServerFnErrorErr::Args(e.to_string()).into_app_error()) } } -impl IntoRes for T +impl IntoRes for T where - Response: Res, + Response: TryRes, T: Serialize + Send, + E: FromServerFnError, { - async fn into_res(self) -> Result> { + async fn into_res(self) -> Result { let mut buffer: Vec = Vec::new(); - ciborium::ser::into_writer(&self, &mut buffer) - .map_err(|e| ServerFnError::Serialization(e.to_string()))?; + ciborium::ser::into_writer(&self, &mut buffer).map_err(|e| { + ServerFnErrorErr::Serialization(e.to_string()).into_app_error() + })?; Response::try_from_bytes(Cbor::CONTENT_TYPE, Bytes::from(buffer)) } } -impl FromRes for T +impl FromRes for T where - Response: ClientRes + Send, + Response: ClientRes + Send, T: DeserializeOwned + Send, + E: FromServerFnError, { - async fn from_res(res: Response) -> Result> { + async fn from_res(res: Response) -> Result { let data = res.try_into_bytes().await?; ciborium::de::from_reader(data.as_ref()) - .map_err(|e| ServerFnError::Args(e.to_string())) + .map_err(|e| ServerFnErrorErr::Args(e.to_string()).into_app_error()) } } @@ -114,20 +116,20 @@ where ::Data: Send , ::Data: Send , { - async fn from_req(req: http::Request) -> Result> { + async fn from_req(req: http::Request) -> Result> { let (_parts, body) = req.into_parts(); let body_bytes = body .collect() .await .map(|c| c.to_bytes()) - .map_err(|e| ServerFnError::Deserialization(e.to_string()))?; + .map_err(|e| ServerFnErrorErr::Deserialization(e.to_string()).into())?; let data = ciborium::de::from_reader(body_bytes.as_ref()) - .map_err(|e| ServerFnError::Args(e.to_string()))?; + .map_err(|e| ServerFnErrorErr::Args(e.to_string()).into())?; Ok(data) } - async fn into_req(self) -> Result, ServerFnError> { + async fn into_req(self) -> Result, ServerFnError> { let mut buffer: Vec = Vec::new(); ciborium::ser::into_writer(&self, &mut buffer)?; let req = http::Request::builder() @@ -139,17 +141,17 @@ where .body(Body::from(buffer))?; Ok(req) } - async fn from_res(res: http::Response) -> Result> { + async fn from_res(res: http::Response) -> Result> { let (_parts, body) = res.into_parts(); let body_bytes = body .collect() .await .map(|c| c.to_bytes()) - .map_err(|e| ServerFnError::Deserialization(e.to_string()))?; + .map_err(|e| ServerFnErrorErr::Deserialization(e.to_string()).into())?; ciborium::de::from_reader(body_bytes.as_ref()) - .map_err(|e| ServerFnError::Args(e.to_string())) + .map_err(|e| ServerFnErrorErr::Args(e.to_string()).into()) } async fn into_res(self) -> http::Response { diff --git a/server_fn/src/codec/json.rs b/server_fn/src/codec/json.rs index 20872c467d..e67a2d2dee 100644 --- a/server_fn/src/codec/json.rs +++ b/server_fn/src/codec/json.rs @@ -1,8 +1,8 @@ use super::{Encoding, FromReq, FromRes, Streaming}; use crate::{ - error::{NoCustomError, ServerFnError}, + error::{FromServerFnError, IntoAppError, ServerFnErrorErr}, request::{ClientReq, Req}, - response::{ClientRes, Res}, + response::{ClientRes, TryRes}, IntoReq, IntoRes, }; use bytes::Bytes; @@ -18,55 +18,58 @@ impl Encoding for Json { const METHOD: Method = Method::POST; } -impl IntoReq for T +impl IntoReq for T where - Request: ClientReq, + Request: ClientReq, T: Serialize + Send, + E: FromServerFnError, { - fn into_req( - self, - path: &str, - accepts: &str, - ) -> Result> { - let data = serde_json::to_string(&self) - .map_err(|e| ServerFnError::Serialization(e.to_string()))?; + fn into_req(self, path: &str, accepts: &str) -> Result { + let data = serde_json::to_string(&self).map_err(|e| { + ServerFnErrorErr::Serialization(e.to_string()).into_app_error() + })?; Request::try_new_post(path, accepts, Json::CONTENT_TYPE, data) } } -impl FromReq for T +impl FromReq for T where - Request: Req + Send + 'static, + Request: Req + Send + 'static, T: DeserializeOwned, + E: FromServerFnError, { - async fn from_req(req: Request) -> Result> { + async fn from_req(req: Request) -> Result { let string_data = req.try_into_string().await?; serde_json::from_str::(&string_data) - .map_err(|e| ServerFnError::Args(e.to_string())) + .map_err(|e| ServerFnErrorErr::Args(e.to_string()).into_app_error()) } } -impl IntoRes for T +impl IntoRes for T where - Response: Res, + Response: TryRes, T: Serialize + Send, + E: FromServerFnError, { - async fn into_res(self) -> Result> { - let data = serde_json::to_string(&self) - .map_err(|e| ServerFnError::Serialization(e.to_string()))?; + async fn into_res(self) -> Result { + let data = serde_json::to_string(&self).map_err(|e| { + ServerFnErrorErr::Serialization(e.to_string()).into_app_error() + })?; Response::try_from_string(Json::CONTENT_TYPE, data) } } -impl FromRes for T +impl FromRes for T where - Response: ClientRes + Send, + Response: ClientRes + Send, T: DeserializeOwned + Send, + E: FromServerFnError, { - async fn from_res(res: Response) -> Result> { + async fn from_res(res: Response) -> Result { let data = res.try_into_string().await?; - serde_json::from_str(&data) - .map_err(|e| ServerFnError::Deserialization(e.to_string())) + serde_json::from_str(&data).map_err(|e| { + ServerFnErrorErr::Deserialization(e.to_string()).into_app_error() + }) } } @@ -102,35 +105,31 @@ impl Encoding for StreamingJson { /// end before the output will begin. /// /// Streaming requests are only allowed over HTTP2 or HTTP3. -pub struct JsonStream( - Pin>> + Send>>, -); +pub struct JsonStream(Pin> + Send>>); -impl std::fmt::Debug for JsonStream { +impl std::fmt::Debug for JsonStream { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_tuple("JsonStream").finish() } } -impl JsonStream { +impl JsonStream { /// Creates a new `ByteStream` from the given stream. pub fn new( - value: impl Stream> + Send + 'static, + value: impl Stream> + Send + 'static, ) -> Self { Self(Box::pin(value.map(|value| value))) } } -impl JsonStream { +impl JsonStream { /// Consumes the wrapper, returning a stream of text. - pub fn into_inner( - self, - ) -> impl Stream>> + Send { + pub fn into_inner(self) -> impl Stream> + Send { self.0 } } -impl From for JsonStream +impl From for JsonStream where S: Stream + Send + 'static, { @@ -139,18 +138,15 @@ where } } -impl IntoReq for S +impl IntoReq for S where - Request: ClientReq, + Request: ClientReq, S: Stream + Send + 'static, T: Serialize + 'static, + E: FromServerFnError + Serialize, { - fn into_req( - self, - path: &str, - accepts: &str, - ) -> Result> { - let data: JsonStream = self.into(); + fn into_req(self, path: &str, accepts: &str) -> Result { + let data: JsonStream = self.into(); Request::try_new_streaming( path, accepts, @@ -164,56 +160,61 @@ where } } -impl FromReq for S +impl FromReq for S where - Request: Req + Send + 'static, + Request: Req + Send + 'static, // The additional `Stream` bound is never used, but it is required to avoid an error where `T` is unconstrained - S: Stream + From> + Send + 'static, + S: Stream + From> + Send + 'static, T: DeserializeOwned + 'static, + E: FromServerFnError, { - async fn from_req(req: Request) -> Result> { + async fn from_req(req: Request) -> Result { let data = req.try_into_stream()?; let s = JsonStream::new(data.map(|chunk| { chunk.and_then(|bytes| { - serde_json::from_slice(bytes.as_ref()) - .map_err(|e| ServerFnError::Deserialization(e.to_string())) + serde_json::from_slice(bytes.as_ref()).map_err(|e| { + ServerFnErrorErr::Deserialization(e.to_string()) + .into_app_error() + }) }) })); Ok(s.into()) } } -impl IntoRes - for JsonStream +impl IntoRes for JsonStream where - Response: Res, - CustErr: 'static, + Response: TryRes, T: Serialize + 'static, + E: FromServerFnError, { - async fn into_res(self) -> Result> { + async fn into_res(self) -> Result { Response::try_from_stream( Streaming::CONTENT_TYPE, self.into_inner().map(|value| { - serde_json::to_vec(&value?) - .map(Bytes::from) - .map_err(|e| ServerFnError::Serialization(e.to_string())) + serde_json::to_vec(&value?).map(Bytes::from).map_err(|e| { + ServerFnErrorErr::Serialization(e.to_string()) + .into_app_error() + }) }), ) } } -impl FromRes - for JsonStream +impl FromRes for JsonStream where - Response: ClientRes + Send, + Response: ClientRes + Send, T: DeserializeOwned, + E: FromServerFnError, { - async fn from_res(res: Response) -> Result> { + async fn from_res(res: Response) -> Result { let stream = res.try_into_stream()?; Ok(JsonStream::new(stream.map(|chunk| { chunk.and_then(|bytes| { - serde_json::from_slice(bytes.as_ref()) - .map_err(|e| ServerFnError::Deserialization(e.to_string())) + serde_json::from_slice(bytes.as_ref()).map_err(|e| { + ServerFnErrorErr::Deserialization(e.to_string()) + .into_app_error() + }) }) }))) } diff --git a/server_fn/src/codec/mod.rs b/server_fn/src/codec/mod.rs index fc5bb37846..6b029f12ee 100644 --- a/server_fn/src/codec/mod.rs +++ b/server_fn/src/codec/mod.rs @@ -55,7 +55,6 @@ mod postcard; pub use postcard::*; mod stream; -use crate::error::ServerFnError; use futures::Future; use http::Method; pub use stream::*; @@ -71,31 +70,27 @@ pub use stream::*; /// For example, here’s the implementation for [`Json`]. /// /// ```rust,ignore -/// impl IntoReq for T +/// impl IntoReq for T /// where -/// Request: ClientReq, +/// Request: ClientReq, /// T: Serialize + Send, /// { /// fn into_req( /// self, /// path: &str, /// accepts: &str, -/// ) -> Result> { +/// ) -> Result { /// // try to serialize the data /// let data = serde_json::to_string(&self) -/// .map_err(|e| ServerFnError::Serialization(e.to_string()))?; +/// .map_err(|e| ServerFnErrorErr::Serialization(e.to_string()).into_app_error())?; /// // and use it as the body of a POST request /// Request::try_new_post(path, accepts, Json::CONTENT_TYPE, data) /// } /// } /// ``` -pub trait IntoReq { +pub trait IntoReq { /// Attempts to serialize the arguments into an HTTP request. - fn into_req( - self, - path: &str, - accepts: &str, - ) -> Result>; + fn into_req(self, path: &str, accepts: &str) -> Result; } /// Deserializes an HTTP request into the data type, on the server. @@ -109,32 +104,31 @@ pub trait IntoReq { /// For example, here’s the implementation for [`Json`]. /// /// ```rust,ignore -/// impl FromReq for T +/// impl FromReq for T /// where /// // require the Request implement `Req` -/// Request: Req + Send + 'static, +/// Request: Req + Send + 'static, /// // require that the type can be deserialized with `serde` /// T: DeserializeOwned, +/// E: FromServerFnError, /// { /// async fn from_req( /// req: Request, -/// ) -> Result> { +/// ) -> Result { /// // try to convert the body of the request into a `String` /// let string_data = req.try_into_string().await?; /// // deserialize the data -/// serde_json::from_str::(&string_data) -/// .map_err(|e| ServerFnError::Args(e.to_string())) +/// serde_json::from_str(&string_data) +/// .map_err(|e| ServerFnErrorErr::Args(e.to_string()).into_app_error()) /// } /// } /// ``` -pub trait FromReq +pub trait FromReq where Self: Sized, { /// Attempts to deserialize the arguments from a request. - fn from_req( - req: Request, - ) -> impl Future>> + Send; + fn from_req(req: Request) -> impl Future> + Send; } /// Serializes the data type into an HTTP response. @@ -148,25 +142,24 @@ where /// For example, here’s the implementation for [`Json`]. /// /// ```rust,ignore -/// impl IntoRes for T +/// impl IntoRes for T /// where -/// Response: Res, +/// Response: Res, /// T: Serialize + Send, +/// E: FromServerFnError, /// { -/// async fn into_res(self) -> Result> { +/// async fn into_res(self) -> Result { /// // try to serialize the data /// let data = serde_json::to_string(&self) -/// .map_err(|e| ServerFnError::Serialization(e.to_string()))?; +/// .map_err(|e| ServerFnErrorErr::Serialization(e.to_string()).into())?; /// // and use it as the body of a response /// Response::try_from_string(Json::CONTENT_TYPE, data) /// } /// } /// ``` -pub trait IntoRes { +pub trait IntoRes { /// Attempts to serialize the output into an HTTP response. - fn into_res( - self, - ) -> impl Future>> + Send; + fn into_res(self) -> impl Future> + Send; } /// Deserializes the data type from an HTTP response. @@ -181,30 +174,29 @@ pub trait IntoRes { /// For example, here’s the implementation for [`Json`]. /// /// ```rust,ignore -/// impl FromRes for T +/// impl FromRes for T /// where -/// Response: ClientRes + Send, +/// Response: ClientRes + Send, /// T: DeserializeOwned + Send, +/// E: FromServerFnError, /// { /// async fn from_res( /// res: Response, -/// ) -> Result> { +/// ) -> Result { /// // extracts the request body /// let data = res.try_into_string().await?; /// // and tries to deserialize it as JSON /// serde_json::from_str(&data) -/// .map_err(|e| ServerFnError::Deserialization(e.to_string())) +/// .map_err(|e| ServerFnErrorErr::Deserialization(e.to_string()).into_app_error()) /// } /// } /// ``` -pub trait FromRes +pub trait FromRes where Self: Sized, { /// Attempts to deserialize the outputs from a response. - fn from_res( - res: Response, - ) -> impl Future>> + Send; + fn from_res(res: Response) -> impl Future> + Send; } /// Defines a particular encoding format, which can be used for serializing or deserializing data. diff --git a/server_fn/src/codec/msgpack.rs b/server_fn/src/codec/msgpack.rs index c06789e1a6..339137f84a 100644 --- a/server_fn/src/codec/msgpack.rs +++ b/server_fn/src/codec/msgpack.rs @@ -1,8 +1,8 @@ use super::{Encoding, FromReq, FromRes, IntoReq, IntoRes}; use crate::{ - error::ServerFnError, + error::{FromServerFnError, IntoAppError, ServerFnErrorErr}, request::{ClientReq, Req}, - response::{ClientRes, Res}, + response::{ClientRes, TryRes}, }; use bytes::Bytes; use http::Method; @@ -16,18 +16,16 @@ impl Encoding for MsgPack { const METHOD: Method = Method::POST; } -impl IntoReq for T +impl IntoReq for T where - Request: ClientReq, + Request: ClientReq, T: Serialize, + E: FromServerFnError, { - fn into_req( - self, - path: &str, - accepts: &str, - ) -> Result> { - let data = rmp_serde::to_vec(&self) - .map_err(|e| ServerFnError::Serialization(e.to_string()))?; + fn into_req(self, path: &str, accepts: &str) -> Result { + let data = rmp_serde::to_vec(&self).map_err(|e| { + ServerFnErrorErr::Serialization(e.to_string()).into_app_error() + })?; Request::try_new_post_bytes( path, MsgPack::CONTENT_TYPE, @@ -37,38 +35,43 @@ where } } -impl FromReq for T +impl FromReq for T where - Request: Req + Send, + Request: Req + Send, T: DeserializeOwned, + E: FromServerFnError, { - async fn from_req(req: Request) -> Result> { + async fn from_req(req: Request) -> Result { let data = req.try_into_bytes().await?; rmp_serde::from_slice::(&data) - .map_err(|e| ServerFnError::Args(e.to_string())) + .map_err(|e| ServerFnErrorErr::Args(e.to_string()).into_app_error()) } } -impl IntoRes for T +impl IntoRes for T where - Response: Res, + Response: TryRes, T: Serialize + Send, + E: FromServerFnError, { - async fn into_res(self) -> Result> { - let data = rmp_serde::to_vec(&self) - .map_err(|e| ServerFnError::Serialization(e.to_string()))?; + async fn into_res(self) -> Result { + let data = rmp_serde::to_vec(&self).map_err(|e| { + ServerFnErrorErr::Serialization(e.to_string()).into_app_error() + })?; Response::try_from_bytes(MsgPack::CONTENT_TYPE, Bytes::from(data)) } } -impl FromRes for T +impl FromRes for T where - Response: ClientRes + Send, + Response: ClientRes + Send, T: DeserializeOwned, + E: FromServerFnError, { - async fn from_res(res: Response) -> Result> { + async fn from_res(res: Response) -> Result { let data = res.try_into_bytes().await?; - rmp_serde::from_slice(&data) - .map_err(|e| ServerFnError::Deserialization(e.to_string())) + rmp_serde::from_slice(&data).map_err(|e| { + ServerFnErrorErr::Deserialization(e.to_string()).into_app_error() + }) } } diff --git a/server_fn/src/codec/multipart.rs b/server_fn/src/codec/multipart.rs index 998e822260..75e8921b6b 100644 --- a/server_fn/src/codec/multipart.rs +++ b/server_fn/src/codec/multipart.rs @@ -1,6 +1,6 @@ use super::{Encoding, FromReq}; use crate::{ - error::ServerFnError, + error::FromServerFnError, request::{browser::BrowserFormData, ClientReq, Req}, IntoReq, }; @@ -56,16 +56,12 @@ impl From for MultipartData { } } -impl IntoReq for T +impl IntoReq for T where - Request: ClientReq, + Request: ClientReq, T: Into, { - fn into_req( - self, - path: &str, - accepts: &str, - ) -> Result> { + fn into_req(self, path: &str, accepts: &str) -> Result { let multi = self.into(); Request::try_new_multipart( path, @@ -75,20 +71,20 @@ where } } -impl FromReq for T +impl FromReq for T where - Request: Req + Send + 'static, + Request: Req + Send + 'static, T: From, - CustErr: 'static, + E: FromServerFnError, { - async fn from_req(req: Request) -> Result> { + async fn from_req(req: Request) -> Result { let boundary = req .to_content_type() .and_then(|ct| multer::parse_boundary(ct).ok()) .expect("couldn't parse boundary"); let stream = req.try_into_stream()?; let data = multer::Multipart::new( - stream.map(|data| data.map_err(|e| e.to_string())), + stream.map(|data| data.map_err(|e| e.ser())), boundary, ); Ok(MultipartData::Server(data).into()) diff --git a/server_fn/src/codec/postcard.rs b/server_fn/src/codec/postcard.rs index b78dae63b2..f1f4ede4e8 100644 --- a/server_fn/src/codec/postcard.rs +++ b/server_fn/src/codec/postcard.rs @@ -1,8 +1,8 @@ use super::{Encoding, FromReq, FromRes, IntoReq, IntoRes}; use crate::{ - error::ServerFnError, + error::{FromServerFnError, IntoAppError, ServerFnErrorErr}, request::{ClientReq, Req}, - response::{ClientRes, Res}, + response::{ClientRes, TryRes}, }; use bytes::Bytes; use http::Method; @@ -16,18 +16,16 @@ impl Encoding for Postcard { const METHOD: Method = Method::POST; } -impl IntoReq for T +impl IntoReq for T where - Request: ClientReq, + Request: ClientReq, T: Serialize, + E: FromServerFnError, { - fn into_req( - self, - path: &str, - accepts: &str, - ) -> Result> { - let data = postcard::to_allocvec(&self) - .map_err(|e| ServerFnError::Serialization(e.to_string()))?; + fn into_req(self, path: &str, accepts: &str) -> Result { + let data = postcard::to_allocvec(&self).map_err(|e| { + ServerFnErrorErr::Serialization(e.to_string()).into_app_error() + })?; Request::try_new_post_bytes( path, Postcard::CONTENT_TYPE, @@ -37,38 +35,43 @@ where } } -impl FromReq for T +impl FromReq for T where - Request: Req + Send, + Request: Req + Send, T: DeserializeOwned, + E: FromServerFnError, { - async fn from_req(req: Request) -> Result> { + async fn from_req(req: Request) -> Result { let data = req.try_into_bytes().await?; postcard::from_bytes::(&data) - .map_err(|e| ServerFnError::Args(e.to_string())) + .map_err(|e| ServerFnErrorErr::Args(e.to_string()).into_app_error()) } } -impl IntoRes for T +impl IntoRes for T where - Response: Res, + Response: TryRes, T: Serialize + Send, + E: FromServerFnError, { - async fn into_res(self) -> Result> { - let data = postcard::to_allocvec(&self) - .map_err(|e| ServerFnError::Serialization(e.to_string()))?; + async fn into_res(self) -> Result { + let data = postcard::to_allocvec(&self).map_err(|e| { + ServerFnErrorErr::Serialization(e.to_string()).into_app_error() + })?; Response::try_from_bytes(Postcard::CONTENT_TYPE, Bytes::from(data)) } } -impl FromRes for T +impl FromRes for T where - Response: ClientRes + Send, + Response: ClientRes + Send, T: DeserializeOwned, + E: FromServerFnError, { - async fn from_res(res: Response) -> Result> { + async fn from_res(res: Response) -> Result { let data = res.try_into_bytes().await?; - postcard::from_bytes(&data) - .map_err(|e| ServerFnError::Deserialization(e.to_string())) + postcard::from_bytes(&data).map_err(|e| { + ServerFnErrorErr::Deserialization(e.to_string()).into_app_error() + }) } } diff --git a/server_fn/src/codec/rkyv.rs b/server_fn/src/codec/rkyv.rs index 9aed0ff83a..8cfebca964 100644 --- a/server_fn/src/codec/rkyv.rs +++ b/server_fn/src/codec/rkyv.rs @@ -1,8 +1,8 @@ use super::{Encoding, FromReq, FromRes, IntoReq, IntoRes}; use crate::{ - error::ServerFnError, + error::{FromServerFnError, IntoAppError, ServerFnErrorErr}, request::{ClientReq, Req}, - response::{ClientRes, Res}, + response::{ClientRes, TryRes}, }; use bytes::Bytes; use futures::StreamExt; @@ -29,39 +29,38 @@ impl Encoding for Rkyv { const METHOD: Method = Method::POST; } -impl IntoReq for T +impl IntoReq for T where - Request: ClientReq, + Request: ClientReq, T: Archive + for<'a> Serialize>, T::Archived: Deserialize + for<'a> CheckBytes>, + E: FromServerFnError, { - fn into_req( - self, - path: &str, - accepts: &str, - ) -> Result> { - let encoded = rkyv::to_bytes::(&self) - .map_err(|e| ServerFnError::Serialization(e.to_string()))?; + fn into_req(self, path: &str, accepts: &str) -> Result { + let encoded = rkyv::to_bytes::(&self).map_err(|e| { + ServerFnErrorErr::Serialization(e.to_string()).into_app_error() + })?; let bytes = Bytes::copy_from_slice(encoded.as_ref()); Request::try_new_post_bytes(path, accepts, Rkyv::CONTENT_TYPE, bytes) } } -impl FromReq for T +impl FromReq for T where - Request: Req + Send + 'static, + Request: Req + Send + 'static, T: Archive + for<'a> Serialize>, T::Archived: Deserialize + for<'a> CheckBytes>, + E: FromServerFnError, { - async fn from_req(req: Request) -> Result> { + async fn from_req(req: Request) -> Result { let mut aligned = AlignedVec::<1024>::new(); let mut body_stream = Box::pin(req.try_into_stream()?); while let Some(chunk) = body_stream.next().await { match chunk { Err(e) => { - return Err(ServerFnError::Deserialization(e.to_string())) + return Err(e); } Ok(bytes) => { for byte in bytes { @@ -71,36 +70,40 @@ where } } rkyv::from_bytes::(aligned.as_ref()) - .map_err(|e| ServerFnError::Args(e.to_string())) + .map_err(|e| ServerFnErrorErr::Args(e.to_string()).into_app_error()) } } -impl IntoRes for T +impl IntoRes for T where - Response: Res, + Response: TryRes, T: Send, T: Archive + for<'a> Serialize>, T::Archived: Deserialize + for<'a> CheckBytes>, + E: FromServerFnError, { - async fn into_res(self) -> Result> { - let encoded = rkyv::to_bytes::(&self) - .map_err(|e| ServerFnError::Serialization(format!("{e:?}")))?; + async fn into_res(self) -> Result { + let encoded = rkyv::to_bytes::(&self).map_err(|e| { + ServerFnErrorErr::Serialization(format!("{e:?}")).into_app_error() + })?; let bytes = Bytes::copy_from_slice(encoded.as_ref()); Response::try_from_bytes(Rkyv::CONTENT_TYPE, bytes) } } -impl FromRes for T +impl FromRes for T where - Response: ClientRes + Send, + Response: ClientRes + Send, T: Archive + for<'a> Serialize>, T::Archived: Deserialize + for<'a> CheckBytes>, + E: FromServerFnError, { - async fn from_res(res: Response) -> Result> { + async fn from_res(res: Response) -> Result { let data = res.try_into_bytes().await?; - rkyv::from_bytes::(&data) - .map_err(|e| ServerFnError::Deserialization(e.to_string())) + rkyv::from_bytes::(&data).map_err(|e| { + ServerFnErrorErr::Deserialization(e.to_string()).into_app_error() + }) } } diff --git a/server_fn/src/codec/serde_lite.rs b/server_fn/src/codec/serde_lite.rs index b71b9390fe..e5bb4e7271 100644 --- a/server_fn/src/codec/serde_lite.rs +++ b/server_fn/src/codec/serde_lite.rs @@ -1,8 +1,8 @@ use super::{Encoding, FromReq, FromRes}; use crate::{ - error::ServerFnError, + error::{FromServerFnError, IntoAppError, ServerFnErrorErr}, request::{ClientReq, Req}, - response::{ClientRes, Res}, + response::{ClientRes, TryRes}, IntoReq, IntoRes, }; use http::Method; @@ -15,68 +15,68 @@ impl Encoding for SerdeLite { const METHOD: Method = Method::POST; } -impl IntoReq for T +impl IntoReq for T where - Request: ClientReq, + Request: ClientReq, T: Serialize + Send, + E: FromServerFnError, { - fn into_req( - self, - path: &str, - accepts: &str, - ) -> Result> { - let data = serde_json::to_string( - &self - .serialize() - .map_err(|e| ServerFnError::Serialization(e.to_string()))?, - ) - .map_err(|e| ServerFnError::Serialization(e.to_string()))?; + fn into_req(self, path: &str, accepts: &str) -> Result { + let data = serde_json::to_string(&self.serialize().map_err(|e| { + ServerFnErrorErr::Serialization(e.to_string()).into_app_error() + })?) + .map_err(|e| { + ServerFnErrorErr::Serialization(e.to_string()).into_app_error() + })?; Request::try_new_post(path, accepts, SerdeLite::CONTENT_TYPE, data) } } -impl FromReq for T +impl FromReq for T where - Request: Req + Send + 'static, + Request: Req + Send + 'static, T: Deserialize, + E: FromServerFnError, { - async fn from_req(req: Request) -> Result> { + async fn from_req(req: Request) -> Result { let string_data = req.try_into_string().await?; - Self::deserialize( - &serde_json::from_str(&string_data) - .map_err(|e| ServerFnError::Args(e.to_string()))?, - ) - .map_err(|e| ServerFnError::Args(e.to_string())) + Self::deserialize(&serde_json::from_str(&string_data).map_err(|e| { + ServerFnErrorErr::Args(e.to_string()).into_app_error() + })?) + .map_err(|e| ServerFnErrorErr::Args(e.to_string()).into_app_error()) } } -impl IntoRes for T +impl IntoRes for T where - Response: Res, + Response: TryRes, T: Serialize + Send, + E: FromServerFnError, { - async fn into_res(self) -> Result> { - let data = serde_json::to_string( - &self - .serialize() - .map_err(|e| ServerFnError::Serialization(e.to_string()))?, - ) - .map_err(|e| ServerFnError::Serialization(e.to_string()))?; + async fn into_res(self) -> Result { + let data = serde_json::to_string(&self.serialize().map_err(|e| { + ServerFnErrorErr::Serialization(e.to_string()).into_app_error() + })?) + .map_err(|e| { + ServerFnErrorErr::Serialization(e.to_string()).into_app_error() + })?; Response::try_from_string(SerdeLite::CONTENT_TYPE, data) } } -impl FromRes for T +impl FromRes for T where - Response: ClientRes + Send, + Response: ClientRes + Send, T: Deserialize + Send, + E: FromServerFnError, { - async fn from_res(res: Response) -> Result> { + async fn from_res(res: Response) -> Result { let data = res.try_into_string().await?; - Self::deserialize( - &serde_json::from_str(&data) - .map_err(|e| ServerFnError::Args(e.to_string()))?, - ) - .map_err(|e| ServerFnError::Deserialization(e.to_string())) + Self::deserialize(&serde_json::from_str(&data).map_err(|e| { + ServerFnErrorErr::Args(e.to_string()).into_app_error() + })?) + .map_err(|e| { + ServerFnErrorErr::Deserialization(e.to_string()).into_app_error() + }) } } diff --git a/server_fn/src/codec/stream.rs b/server_fn/src/codec/stream.rs index fd406209a3..f1d5418c74 100644 --- a/server_fn/src/codec/stream.rs +++ b/server_fn/src/codec/stream.rs @@ -1,9 +1,9 @@ use super::{Encoding, FromReq, FromRes, IntoReq}; use crate::{ - error::{NoCustomError, ServerFnError}, + error::{FromServerFnError, IntoAppError, ServerFnErrorErr}, request::{ClientReq, Req}, - response::{ClientRes, Res}, - IntoRes, + response::{ClientRes, TryRes}, + IntoRes, ServerFnError, }; use bytes::Bytes; use futures::{Stream, StreamExt}; @@ -29,26 +29,22 @@ impl Encoding for Streaming { const METHOD: Method = Method::POST; } -impl IntoReq for T +impl IntoReq for T where - Request: ClientReq, + Request: ClientReq, T: Stream + Send + Sync + 'static, { - fn into_req( - self, - path: &str, - accepts: &str, - ) -> Result> { + fn into_req(self, path: &str, accepts: &str) -> Result { Request::try_new_streaming(path, accepts, Streaming::CONTENT_TYPE, self) } } -impl FromReq for T +impl FromReq for T where - Request: Req + Send + 'static, - T: From + 'static, + Request: Req + Send + 'static, + T: From> + 'static, { - async fn from_req(req: Request) -> Result> { + async fn from_req(req: Request) -> Result { let data = req.try_into_stream()?; let s = ByteStream::new(data); Ok(s.into()) @@ -67,29 +63,25 @@ where /// end before the output will begin. /// /// Streaming requests are only allowed over HTTP2 or HTTP3. -pub struct ByteStream( - Pin>> + Send>>, -); +pub struct ByteStream(Pin> + Send>>); -impl ByteStream { +impl ByteStream { /// Consumes the wrapper, returning a stream of bytes. - pub fn into_inner( - self, - ) -> impl Stream>> + Send { + pub fn into_inner(self) -> impl Stream> + Send { self.0 } } -impl Debug for ByteStream { +impl Debug for ByteStream { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_tuple("ByteStream").finish() } } -impl ByteStream { +impl ByteStream { /// Creates a new `ByteStream` from the given stream. pub fn new( - value: impl Stream> + Send + 'static, + value: impl Stream> + Send + 'static, ) -> Self where T: Into, @@ -98,7 +90,7 @@ impl ByteStream { } } -impl From for ByteStream +impl From for ByteStream where S: Stream + Send + 'static, T: Into, @@ -108,22 +100,21 @@ where } } -impl IntoRes - for ByteStream +impl IntoRes for ByteStream where - Response: Res, - CustErr: 'static, + Response: TryRes, + E: 'static, { - async fn into_res(self) -> Result> { + async fn into_res(self) -> Result { Response::try_from_stream(Streaming::CONTENT_TYPE, self.into_inner()) } } -impl FromRes for ByteStream +impl FromRes for ByteStream where - Response: ClientRes + Send, + Response: ClientRes + Send, { - async fn from_res(res: Response) -> Result> { + async fn from_res(res: Response) -> Result { let stream = res.try_into_stream()?; Ok(ByteStream(Box::pin(stream))) } @@ -160,35 +151,33 @@ impl Encoding for StreamingText { /// end before the output will begin. /// /// Streaming requests are only allowed over HTTP2 or HTTP3. -pub struct TextStream( - Pin>> + Send>>, +pub struct TextStream( + Pin> + Send>>, ); -impl Debug for TextStream { +impl Debug for TextStream { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_tuple("TextStream").finish() } } -impl TextStream { +impl TextStream { /// Creates a new `ByteStream` from the given stream. pub fn new( - value: impl Stream> + Send + 'static, + value: impl Stream> + Send + 'static, ) -> Self { Self(Box::pin(value.map(|value| value))) } } -impl TextStream { +impl TextStream { /// Consumes the wrapper, returning a stream of text. - pub fn into_inner( - self, - ) -> impl Stream>> + Send { + pub fn into_inner(self) -> impl Stream> + Send { self.0 } } -impl From for TextStream +impl From for TextStream where S: Stream + Send + 'static, T: Into, @@ -198,16 +187,13 @@ where } } -impl IntoReq for T +impl IntoReq for T where - Request: ClientReq, - T: Into, + Request: ClientReq, + T: Into>, + E: 'static, { - fn into_req( - self, - path: &str, - accepts: &str, - ) -> Result> { + fn into_req(self, path: &str, accepts: &str) -> Result { let data = self.into(); Request::try_new_streaming( path, @@ -218,30 +204,32 @@ where } } -impl FromReq for T +impl FromReq for T where - Request: Req + Send + 'static, - T: From + 'static, + Request: Req + Send + 'static, + T: From> + 'static, + E: FromServerFnError, { - async fn from_req(req: Request) -> Result> { + async fn from_req(req: Request) -> Result { let data = req.try_into_stream()?; let s = TextStream::new(data.map(|chunk| { chunk.and_then(|bytes| { - String::from_utf8(bytes.to_vec()) - .map_err(|e| ServerFnError::Deserialization(e.to_string())) + String::from_utf8(bytes.to_vec()).map_err(|e| { + ServerFnErrorErr::Deserialization(e.to_string()) + .into_app_error() + }) }) })); Ok(s.into()) } } -impl IntoRes - for TextStream +impl IntoRes for TextStream where - Response: Res, - CustErr: 'static, + Response: TryRes, + E: 'static, { - async fn into_res(self) -> Result> { + async fn into_res(self) -> Result { Response::try_from_stream( Streaming::CONTENT_TYPE, self.into_inner().map(|stream| stream.map(Into::into)), @@ -249,16 +237,19 @@ where } } -impl FromRes for TextStream +impl FromRes for TextStream where - Response: ClientRes + Send, + Response: ClientRes + Send, + E: FromServerFnError, { - async fn from_res(res: Response) -> Result> { + async fn from_res(res: Response) -> Result { let stream = res.try_into_stream()?; Ok(TextStream(Box::pin(stream.map(|chunk| { chunk.and_then(|bytes| { - String::from_utf8(bytes.into()) - .map_err(|e| ServerFnError::Deserialization(e.to_string())) + String::from_utf8(bytes.into()).map_err(|e| { + ServerFnErrorErr::Deserialization(e.to_string()) + .into_app_error() + }) }) })))) } diff --git a/server_fn/src/codec/url.rs b/server_fn/src/codec/url.rs index 38e7894649..d5a3e7975c 100644 --- a/server_fn/src/codec/url.rs +++ b/server_fn/src/codec/url.rs @@ -1,6 +1,6 @@ use super::{Encoding, FromReq, IntoReq}; use crate::{ - error::ServerFnError, + error::{FromServerFnError, IntoAppError, ServerFnErrorErr}, request::{ClientReq, Req}, }; use http::Method; @@ -17,32 +17,33 @@ impl Encoding for GetUrl { const METHOD: Method = Method::GET; } -impl IntoReq for T +impl IntoReq for T where - Request: ClientReq, + Request: ClientReq, T: Serialize + Send, + E: FromServerFnError, { - fn into_req( - self, - path: &str, - accepts: &str, - ) -> Result> { - let data = serde_qs::to_string(&self) - .map_err(|e| ServerFnError::Serialization(e.to_string()))?; + fn into_req(self, path: &str, accepts: &str) -> Result { + let data = serde_qs::to_string(&self).map_err(|e| { + ServerFnErrorErr::Serialization(e.to_string()).into_app_error() + })?; Request::try_new_get(path, accepts, GetUrl::CONTENT_TYPE, &data) } } -impl FromReq for T +impl FromReq for T where - Request: Req + Send + 'static, + Request: Req + Send + 'static, T: DeserializeOwned, + E: FromServerFnError, { - async fn from_req(req: Request) -> Result> { + async fn from_req(req: Request) -> Result { let string_data = req.as_query().unwrap_or_default(); let args = serde_qs::Config::new(5, false) .deserialize_str::(string_data) - .map_err(|e| ServerFnError::Args(e.to_string()))?; + .map_err(|e| { + ServerFnErrorErr::Args(e.to_string()).into_app_error() + })?; Ok(args) } } @@ -52,32 +53,33 @@ impl Encoding for PostUrl { const METHOD: Method = Method::POST; } -impl IntoReq for T +impl IntoReq for T where - Request: ClientReq, + Request: ClientReq, T: Serialize + Send, + E: FromServerFnError, { - fn into_req( - self, - path: &str, - accepts: &str, - ) -> Result> { - let qs = serde_qs::to_string(&self) - .map_err(|e| ServerFnError::Serialization(e.to_string()))?; + fn into_req(self, path: &str, accepts: &str) -> Result { + let qs = serde_qs::to_string(&self).map_err(|e| { + ServerFnErrorErr::Serialization(e.to_string()).into_app_error() + })?; Request::try_new_post(path, accepts, PostUrl::CONTENT_TYPE, qs) } } -impl FromReq for T +impl FromReq for T where - Request: Req + Send + 'static, + Request: Req + Send + 'static, T: DeserializeOwned, + E: FromServerFnError, { - async fn from_req(req: Request) -> Result> { + async fn from_req(req: Request) -> Result { let string_data = req.try_into_string().await?; let args = serde_qs::Config::new(5, false) .deserialize_str::(&string_data) - .map_err(|e| ServerFnError::Args(e.to_string()))?; + .map_err(|e| { + ServerFnErrorErr::Args(e.to_string()).into_app_error() + })?; Ok(args) } } @@ -86,18 +88,18 @@ where impl Codec for T where T: DeserializeOwned + Serialize + Send, - Request: Req + Send, - Response: Res + Send, + Request: Req + Send, + Response: Res + Send, { - async fn from_req(req: Request) -> Result> { + async fn from_req(req: Request) -> Result> { let string_data = req.try_into_string()?; let args = serde_json::from_str::(&string_data) - .map_err(|e| ServerFnError::Args(e.to_string()))?; + .map_err(|e| ServerFnErrorErr::Args(e.to_string()).into())?; Ok(args) } - async fn into_req(self) -> Result> { + async fn into_req(self) -> Result> { /* let qs = serde_qs::to_string(&self)?; let req = http::Request::builder() .method("GET") @@ -110,7 +112,7 @@ where todo!() } - async fn from_res(res: Response) -> Result> { + async fn from_res(res: Response) -> Result> { todo!() /* let (_parts, body) = res.into_parts(); @@ -118,7 +120,7 @@ where .collect() .await .map(|c| c.to_bytes()) - .map_err(|e| ServerFnError::Deserialization(e.to_string()))?; + .map_err(|e| ServerFnErrorErr::Deserialization(e.to_string()).into())?; let string_data = String::from_utf8(body_bytes.to_vec())?; serde_json::from_str(&string_data) .map_err(|e| ServerFnError::Deserialization(e.to_string())) */ diff --git a/server_fn/src/error.rs b/server_fn/src/error.rs index c139dde062..432833b5f0 100644 --- a/server_fn/src/error.rs +++ b/server_fn/src/error.rs @@ -1,7 +1,9 @@ -use serde::{Deserialize, Serialize}; +#![allow(deprecated)] + +use base64::{engine::general_purpose::URL_SAFE, Engine as _}; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; use std::{ - fmt, - fmt::{Display, Write}, + fmt::{self, Display, Write}, str::FromStr, }; use thiserror::Error; @@ -13,7 +15,7 @@ pub const SERVER_FN_ERROR_HEADER: &str = "serverfnerror"; impl From for Error { fn from(e: ServerFnError) -> Self { - Error::from(ServerFnErrorErr::from(e)) + Error::from(ServerFnErrorWrapper(e)) } } @@ -35,6 +37,11 @@ impl From for Error { feature = "rkyv", derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize) )] +#[deprecated( + since = "0.8.0", + note = "Now server_fn can return any error type other than ServerFnError, \ + so the WrappedServerError variant will be removed in 0.9.0" +)] pub struct NoCustomError; // Implement `Display` for `NoCustomError` @@ -55,11 +62,21 @@ impl FromStr for NoCustomError { /// Wraps some error type, which may implement any of [`Error`](trait@std::error::Error), [`Clone`], or /// [`Display`]. #[derive(Debug)] +#[deprecated( + since = "0.8.0", + note = "Now server_fn can return any error type other than ServerFnError, \ + so the WrappedServerError variant will be removed in 0.9.0" +)] pub struct WrapError(pub T); /// A helper macro to convert a variety of different types into `ServerFnError`. /// This should mostly be used if you are implementing `From` for `YourError`. #[macro_export] +#[deprecated( + since = "0.8.0", + note = "Now server_fn can return any error type other than ServerFnError, \ + so the WrappedServerError variant will be removed in 0.9.0" +)] macro_rules! server_fn_error { () => {{ use $crate::{ViaError, WrapError}; @@ -75,6 +92,12 @@ macro_rules! server_fn_error { /// This trait serves as the conversion method between a variety of types /// and [`ServerFnError`]. +#[deprecated( + since = "0.8.0", + note = "Now server_fn can return any error type other than ServerFnError, \ + so users should place their custom error type instead of \ + ServerFnError" +)] pub trait ViaError { /// Converts something into an error. fn to_server_error(&self) -> ServerFnError; @@ -90,6 +113,7 @@ impl ViaError } // A type tag for ServerFnError so we can special case it +#[deprecated] pub(crate) trait ServerFnErrorKind {} impl ServerFnErrorKind for ServerFnError {} @@ -131,7 +155,8 @@ impl ViaError for WrapError { } } -/// Type for errors that can occur when using server functions. +/// A type that can be used as the return type of the server function for easy error conversion with `?` operator. +/// This type can be replaced with any other error type that implements `FromServerFnError`. /// /// Unlike [`ServerFnErrorErr`], this does not implement [`Error`](trait@std::error::Error). /// This means that other error types can easily be converted into it using the @@ -142,6 +167,12 @@ impl ViaError for WrapError { derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize) )] pub enum ServerFnError { + #[deprecated( + since = "0.8.0", + note = "Now server_fn can return any error type other than \ + ServerFnError, so users should place their custom error type \ + instead of ServerFnError" + )] /// A user-defined custom error type, which defaults to [`NoCustomError`]. WrappedServerError(E), /// Error while trying to register the server function (only occurs in case of poisoned RwLock). @@ -152,6 +183,8 @@ pub enum ServerFnError { Response(String), /// Occurs when there is an error while actually running the function on the server. ServerError(String), + /// Occurs when there is an error while actually running the middleware on the server. + MiddlewareError(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. @@ -198,6 +231,8 @@ where ), ServerFnError::ServerError(s) => format!("error running server function: {s}"), + ServerFnError::MiddlewareError(s) => + format!("error running middleware: {s}"), ServerFnError::Deserialization(s) => format!("error deserializing server function results: {s}"), ServerFnError::Serialization(s) => @@ -214,30 +249,45 @@ where } } -/// A serializable custom server function error type. -/// -/// This is implemented for all types that implement [`FromStr`] + [`Display`]. -/// -/// This means you do not necessarily need the overhead of `serde` for a custom error type. -/// Instead, you can use something like `strum` to derive `FromStr` and `Display` for your -/// custom error type. -/// -/// This is implemented for the default [`ServerFnError`], which uses [`NoCustomError`]. -pub trait ServerFnErrorSerde: Sized { - /// Converts the custom error type to a [`String`]. - fn ser(&self) -> Result; - - /// Deserializes the custom error type from a [`String`]. - fn de(data: &str) -> Self; -} - -impl ServerFnErrorSerde for ServerFnError +impl FromServerFnError for ServerFnError where - CustErr: FromStr + Display, + CustErr: std::fmt::Debug + + Display + + Serialize + + DeserializeOwned + + 'static + + FromStr + + Display, { - fn ser(&self) -> Result { + fn from_server_fn_error(value: ServerFnErrorErr) -> Self { + match value { + ServerFnErrorErr::Registration(value) => { + ServerFnError::Registration(value) + } + ServerFnErrorErr::Request(value) => ServerFnError::Request(value), + ServerFnErrorErr::ServerError(value) => { + ServerFnError::ServerError(value) + } + ServerFnErrorErr::MiddlewareError(value) => { + ServerFnError::MiddlewareError(value) + } + ServerFnErrorErr::Deserialization(value) => { + ServerFnError::Deserialization(value) + } + ServerFnErrorErr::Serialization(value) => { + ServerFnError::Serialization(value) + } + ServerFnErrorErr::Args(value) => ServerFnError::Args(value), + ServerFnErrorErr::MissingArg(value) => { + ServerFnError::MissingArg(value) + } + ServerFnErrorErr::Response(value) => ServerFnError::Response(value), + } + } + + fn ser(&self) -> String { let mut buf = String::new(); - match self { + let result = match self { ServerFnError::WrappedServerError(e) => { write!(&mut buf, "WrappedServerFn|{e}") } @@ -249,6 +299,9 @@ where ServerFnError::ServerError(e) => { write!(&mut buf, "ServerError|{e}") } + ServerFnError::MiddlewareError(e) => { + write!(&mut buf, "MiddlewareError|{e}") + } ServerFnError::Deserialization(e) => { write!(&mut buf, "Deserialization|{e}") } @@ -259,8 +312,11 @@ where ServerFnError::MissingArg(e) => { write!(&mut buf, "MissingArg|{e}") } - }?; - Ok(buf) + }; + match result { + Ok(()) => buf, + Err(_) => "Serialization|".to_string(), + } } fn de(data: &str) -> Self { @@ -311,20 +367,13 @@ where } } -/// Type for errors that can occur when using server functions. -/// -/// Unlike [`ServerFnError`], this implements [`std::error::Error`]. This means -/// it can be used in situations in which the `Error` trait is required, but it’s -/// 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, PartialEq, Eq)] -pub enum ServerFnErrorErr { - /// A user-defined custom error type, which defaults to [`NoCustomError`]. - #[error("internal error: {0}")] - WrappedServerError(E), +/// Type for errors that can occur when using server functions. If you need to return a custom error type from a server function, implement `FromServerFnError` for your custom error type. +#[derive(Error, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Archive, rkyv::Serialize, rkyv::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), @@ -334,6 +383,9 @@ pub enum ServerFnErrorErr { /// Occurs when there is an error while actually running the function on the server. #[error("error running server function: {0}")] ServerError(String), + /// Occurs when there is an error while actually running the middleware on the server. + #[error("error running middleware: {0}")] + MiddlewareError(String), /// Occurs on the client if there is an error deserializing the server's response. #[error("error deserializing server function results: {0}")] Deserialization(String), @@ -351,34 +403,6 @@ pub enum ServerFnErrorErr { Response(String), } -impl From> 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) - } - ServerFnError::WrappedServerError(value) => { - ServerFnErrorErr::WrappedServerError(value) - } - ServerFnError::Response(value) => ServerFnErrorErr::Response(value), - } - } -} - /// Associates a particular server function error with the server function /// found at a particular path. /// @@ -386,15 +410,15 @@ impl From> for ServerFnErrorErr { /// without JavaScript/WASM supported, by encoding it in the URL as a query string. /// This is useful for progressive enhancement. #[derive(Debug)] -pub struct ServerFnUrlError { +pub struct ServerFnUrlError { path: String, - error: ServerFnError, + error: E, } -impl ServerFnUrlError { +impl ServerFnUrlError { /// Creates a new structure associating the server function at some path /// with a particular error. - pub fn new(path: impl Display, error: ServerFnError) -> Self { + pub fn new(path: impl Display, error: E) -> Self { Self { path: path.to_string(), error, @@ -402,7 +426,7 @@ impl ServerFnUrlError { } /// The error itself. - pub fn error(&self) -> &ServerFnError { + pub fn error(&self) -> &E { &self.error } @@ -412,17 +436,11 @@ impl ServerFnUrlError { } /// Adds an encoded form of this server function error to the given base URL. - pub fn to_url(&self, base: &str) -> Result - where - CustErr: FromStr + Display, - { + pub fn to_url(&self, base: &str) -> Result { let mut url = Url::parse(base)?; url.query_pairs_mut() .append_pair("__path", &self.path) - .append_pair( - "__err", - &ServerFnErrorSerde::ser(&self.error).unwrap_or_default(), - ); + .append_pair("__err", &URL_SAFE.encode(self.error.ser())); Ok(url) } @@ -448,16 +466,102 @@ impl ServerFnUrlError { *path = url.to_string(); } } + + /// Decodes an error from a URL. + pub fn decode_err(err: &str) -> E { + let decoded = match URL_SAFE.decode(err) { + Ok(decoded) => decoded, + Err(err) => { + return ServerFnErrorErr::Deserialization(err.to_string()) + .into_app_error(); + } + }; + let s = match String::from_utf8(decoded) { + Ok(s) => s, + Err(err) => { + return ServerFnErrorErr::Deserialization(err.to_string()) + .into_app_error(); + } + }; + E::de(&s) + } } -impl From> for ServerFnError { - fn from(error: ServerFnUrlError) -> Self { +impl From> for ServerFnError { + fn from(error: ServerFnUrlError) -> Self { + error.error.into() + } +} + +impl From>> for ServerFnError { + fn from(error: ServerFnUrlError>) -> Self { error.error } } -impl From> for ServerFnErrorErr { - fn from(error: ServerFnUrlError) -> Self { - error.error.into() +#[derive(Debug)] +#[doc(hidden)] +/// Only used instantly only when a framework needs E: Error. +pub struct ServerFnErrorWrapper(pub E); + +impl Display for ServerFnErrorWrapper { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0.ser()) } } + +impl std::error::Error for ServerFnErrorWrapper { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + None + } +} + +/// A trait for types that can be returned from a server function. +pub trait FromServerFnError: + std::fmt::Debug + Serialize + DeserializeOwned + 'static +{ + /// Converts a [`ServerFnErrorErr`] into the application-specific custom error type. + fn from_server_fn_error(value: ServerFnErrorErr) -> Self; + + /// Converts the custom error type to a [`String`]. Defaults to serializing to JSON. + fn ser(&self) -> String { + serde_json::to_string(self).unwrap_or_else(|e| { + serde_json::to_string(&Self::from_server_fn_error( + ServerFnErrorErr::Serialization(e.to_string()), + )) + .expect( + "error serializing should success at least with the \ + Serialization error", + ) + }) + } + + /// Deserializes the custom error type from a [`&str`]. Defaults to deserializing from JSON. + fn de(data: &str) -> Self { + serde_json::from_str(data).unwrap_or_else(|e| { + ServerFnErrorErr::Deserialization(e.to_string()).into_app_error() + }) + } +} + +/// A helper trait for converting a [`ServerFnErrorErr`] into an application-specific custom error type that implements [`FromServerFnError`]. +pub trait IntoAppError { + /// Converts a [`ServerFnErrorErr`] into the application-specific custom error type. + fn into_app_error(self) -> E; +} + +impl IntoAppError for ServerFnErrorErr +where + E: FromServerFnError, +{ + fn into_app_error(self) -> E { + E::from_server_fn_error(self) + } +} + +#[test] +fn assert_from_server_fn_error_impl() { + fn assert_impl() {} + + assert_impl::(); +} diff --git a/server_fn/src/lib.rs b/server_fn/src/lib.rs index 5b656d33a1..1c3132ee18 100644 --- a/server_fn/src/lib.rs +++ b/server_fn/src/lib.rs @@ -132,15 +132,15 @@ use codec::{Encoding, FromReq, FromRes, IntoReq, IntoRes}; pub use const_format; use dashmap::DashMap; pub use error::ServerFnError; -use error::ServerFnErrorSerde; #[cfg(feature = "form-redirects")] use error::ServerFnUrlError; +use error::{FromServerFnError, ServerFnErrorErr}; use http::Method; -use middleware::{Layer, Service}; +use middleware::{BoxedService, Layer, Service}; use once_cell::sync::Lazy; use redirect::RedirectHook; use request::Req; -use response::{ClientRes, Res}; +use response::{ClientRes, Res, TryRes}; #[cfg(feature = "rkyv")] pub use rkyv; #[doc(hidden)] @@ -148,7 +148,7 @@ pub use serde; #[doc(hidden)] #[cfg(feature = "serde-lite")] pub use serde_lite; -use std::{fmt::Display, future::Future, pin::Pin, str::FromStr, sync::Arc}; +use std::{future::Future, pin::Pin, sync::Arc}; #[doc(hidden)] pub use xxhash_rust; @@ -203,7 +203,7 @@ where type ServerRequest: Req + Send; /// The type of the HTTP response returned by the server function on the server side. - type ServerResponse: Res + Send; + type ServerResponse: Res + TryRes + Send; /// The return type of the server function. /// @@ -222,9 +222,8 @@ where /// The [`Encoding`] used in the response for the result of the server function. type OutputEncoding: Encoding; - /// The type of the custom error on [`ServerFnError`], if any. (If there is no - /// custom error type, this can be `NoCustomError` by default.) - type Error: FromStr + Display; + /// The type of the error on the server function. Typically [`ServerFnError`], but allowed to be any type that implements [`FromServerFnError`]. + type Error: FromServerFnError; /// Returns [`Self::PATH`]. fn url() -> &'static str { @@ -240,7 +239,7 @@ where /// The body of the server function. This will only run on the server. fn run_body( self, - ) -> impl Future>> + Send; + ) -> impl Future> + Send; #[doc(hidden)] fn run_on_server( @@ -265,7 +264,10 @@ where .map(|res| (res, None)) .unwrap_or_else(|e| { ( - Self::ServerResponse::error_response(Self::PATH, &e), + Self::ServerResponse::error_response( + Self::PATH, + e.ser(), + ), Some(e), ) }); @@ -298,8 +300,7 @@ where #[doc(hidden)] fn run_on_client( self, - ) -> impl Future>> + Send - { + ) -> impl Future> + Send { async move { // create and send request on client let req = @@ -313,8 +314,7 @@ where fn run_on_client_with_req( req: >::Request, redirect_hook: Option<&RedirectHook>, - ) -> impl Future>> + Send - { + ) -> impl Future> + Send { async move { let res = Self::Client::send(req).await?; @@ -325,7 +325,7 @@ where // if it returns an error status, deserialize the error using FromStr let res = if (400..=599).contains(&status) { let text = res.try_into_string().await?; - Err(ServerFnError::::de(&text)) + Err(Self::Error::de(&text)) } else { // otherwise, deserialize the body as is Ok(Self::Output::from_res(res).await) @@ -345,9 +345,8 @@ where #[doc(hidden)] fn execute_on_server( req: Self::ServerRequest, - ) -> impl Future< - Output = Result>, - > + Send { + ) -> impl Future> + Send + { async { let this = Self::from_req(req).await?; let output = this.run_body().await?; @@ -387,21 +386,20 @@ pub struct ServerFnTraitObj { method: Method, handler: fn(Req) -> Pin + Send>>, middleware: fn() -> MiddlewareSet, + ser: fn(ServerFnErrorErr) -> String, } impl ServerFnTraitObj { /// Converts the relevant parts of a server function into a trait object. - pub const fn new( - path: &'static str, - method: Method, + pub const fn new>( handler: fn(Req) -> Pin + Send>>, - middleware: fn() -> MiddlewareSet, ) -> Self { Self { - path, - method, + path: S::PATH, + method: S::InputEncoding::METHOD, handler, - middleware, + middleware: S::middlewares, + ser: |e| S::Error::from_server_fn_error(e).ser(), } } @@ -424,6 +422,16 @@ impl ServerFnTraitObj { pub fn middleware(&self) -> MiddlewareSet { (self.middleware)() } + + /// Converts the server function into a boxed service. + pub fn boxed(self) -> BoxedService + where + Self: Service, + Req: 'static, + Res: 'static, + { + BoxedService::new(self.ser, self) + } } impl Service for ServerFnTraitObj @@ -431,7 +439,11 @@ where Req: Send + 'static, Res: 'static, { - fn run(&mut self, req: Req) -> Pin + Send>> { + fn run( + &mut self, + req: Req, + _ser: fn(ServerFnErrorErr) -> String, + ) -> Pin + Send>> { let handler = self.handler; Box::pin(async move { handler(req).await }) } @@ -444,6 +456,7 @@ impl Clone for ServerFnTraitObj { method: self.method.clone(), handler: self.handler, middleware: self.middleware, + ser: self.ser, } } } @@ -467,8 +480,8 @@ impl inventory::Collect #[cfg(feature = "axum-no-default")] pub mod axum { use crate::{ - middleware::{BoxedService, Service}, - Encoding, LazyServerFnMap, ServerFn, ServerFnTraitObj, + middleware::BoxedService, Encoding, LazyServerFnMap, ServerFn, + ServerFnTraitObj, }; use axum::body::Body; use http::{Method, Request, Response, StatusCode}; @@ -490,12 +503,7 @@ pub mod axum { { REGISTERED_SERVER_FUNCTIONS.insert( (T::PATH.into(), T::InputEncoding::METHOD), - ServerFnTraitObj::new( - T::PATH, - T::InputEncoding::METHOD, - |req| Box::pin(T::run_on_server(req)), - T::middlewares, - ), + ServerFnTraitObj::new::(|req| Box::pin(T::run_on_server(req))), ); } @@ -539,7 +547,7 @@ pub mod axum { let key = (path.into(), method); REGISTERED_SERVER_FUNCTIONS.get(&key).map(|server_fn| { let middleware = (server_fn.middleware)(); - let mut service = BoxedService::new(server_fn.clone()); + let mut service = server_fn.clone().boxed(); for middleware in middleware { service = middleware.layer(service); } @@ -578,12 +586,7 @@ pub mod actix { { REGISTERED_SERVER_FUNCTIONS.insert( (T::PATH.into(), T::InputEncoding::METHOD), - ServerFnTraitObj::new( - T::PATH, - T::InputEncoding::METHOD, - |req| Box::pin(T::run_on_server(req)), - T::middlewares, - ), + ServerFnTraitObj::new::(|req| Box::pin(T::run_on_server(req))), ); } @@ -603,7 +606,6 @@ pub mod actix { let method = req.method(); if let Some(mut service) = get_server_fn_service(path, method) { service - .0 .run(ActixRequest::from((req, payload))) .await .0 @@ -644,7 +646,7 @@ pub mod actix { REGISTERED_SERVER_FUNCTIONS.get(&(path.into(), method)).map( |server_fn| { let middleware = (server_fn.middleware)(); - let mut service = BoxedService::new(server_fn.clone()); + let mut service = server_fn.clone().boxed(); for middleware in middleware { service = middleware.layer(service); } diff --git a/server_fn/src/middleware/mod.rs b/server_fn/src/middleware/mod.rs index 0789bf719a..2c96ded6bb 100644 --- a/server_fn/src/middleware/mod.rs +++ b/server_fn/src/middleware/mod.rs @@ -1,3 +1,4 @@ +use crate::error::ServerFnErrorErr; use std::{future::Future, pin::Pin}; /// An abstraction over a middleware layer, which can be used to add additional @@ -8,12 +9,31 @@ pub trait Layer: Send + Sync + 'static { } /// A type-erased service, which takes an HTTP request and returns a response. -pub struct BoxedService(pub Box + Send>); +pub struct BoxedService { + /// A function that converts a [`ServerFnErrorErr`] into a string. + pub ser: fn(ServerFnErrorErr) -> String, + /// The inner service. + pub service: Box + Send>, +} impl BoxedService { /// Constructs a type-erased service from this service. - pub fn new(service: impl Service + Send + 'static) -> Self { - Self(Box::new(service)) + pub fn new( + ser: fn(ServerFnErrorErr) -> String, + service: impl Service + Send + 'static, + ) -> Self { + Self { + ser, + service: Box::new(service), + } + } + + /// Converts a request into a response by running the inner service. + pub fn run( + &mut self, + req: Req, + ) -> Pin + Send>> { + self.service.run(req, self.ser) } } @@ -23,37 +43,36 @@ pub trait Service { fn run( &mut self, req: Request, + ser: fn(ServerFnErrorErr) -> String, ) -> Pin + Send>>; } #[cfg(feature = "axum-no-default")] mod axum { use super::{BoxedService, Service}; - use crate::{response::Res, ServerFnError}; + use crate::{error::ServerFnErrorErr, response::Res, ServerFnError}; use axum::body::Body; use http::{Request, Response}; - use std::{ - fmt::{Debug, Display}, - future::Future, - pin::Pin, - }; + use std::{future::Future, pin::Pin}; impl super::Service, Response> for S where S: tower::Service, Response = Response>, S::Future: Send + 'static, - S::Error: Into + Send + Debug + Display + Sync + 'static, + S::Error: std::fmt::Display + Send + 'static, { fn run( &mut self, req: Request, + ser: fn(ServerFnErrorErr) -> String, ) -> Pin> + Send>> { let path = req.uri().path().to_string(); let inner = self.call(req); Box::pin(async move { inner.await.unwrap_or_else(|e| { - let err = ServerFnError::new(e); - Response::::error_response(&path, &err) + let err = + ser(ServerFnErrorErr::MiddlewareError(e.to_string())); + Response::::error_response(&path, err) }) }) } @@ -80,7 +99,7 @@ mod axum { } fn call(&mut self, req: Request) -> Self::Future { - let inner = self.0.run(req); + let inner = self.service.run(req, self.ser); Box::pin(async move { Ok(inner.await) }) } } @@ -97,7 +116,7 @@ mod axum { &self, inner: BoxedService, Response>, ) -> BoxedService, Response> { - BoxedService(Box::new(self.layer(inner))) + BoxedService::new(inner.ser, self.layer(inner)) } } } @@ -105,33 +124,31 @@ mod axum { #[cfg(feature = "actix")] mod actix { use crate::{ + error::ServerFnErrorErr, request::actix::ActixRequest, response::{actix::ActixResponse, Res}, - ServerFnError, }; use actix_web::{HttpRequest, HttpResponse}; - use std::{ - fmt::{Debug, Display}, - future::Future, - pin::Pin, - }; + use std::{future::Future, pin::Pin}; impl super::Service for S where S: actix_web::dev::Service, S::Future: Send + 'static, - S::Error: Into + Debug + Display + 'static, + S::Error: std::fmt::Display + Send + 'static, { fn run( &mut self, req: HttpRequest, + ser: fn(ServerFnErrorErr) -> String, ) -> Pin + Send>> { let path = req.uri().path().to_string(); let inner = self.call(req); Box::pin(async move { inner.await.unwrap_or_else(|e| { - let err = ServerFnError::new(e); - ActixResponse::error_response(&path, &err).take() + let err = + ser(ServerFnErrorErr::MiddlewareError(e.to_string())); + ActixResponse::error_response(&path, err).take() }) }) } @@ -141,18 +158,20 @@ mod actix { where S: actix_web::dev::Service, S::Future: Send + 'static, - S::Error: Into + Debug + Display + 'static, + S::Error: std::fmt::Display + Send + 'static, { fn run( &mut self, req: ActixRequest, + ser: fn(ServerFnErrorErr) -> String, ) -> Pin + Send>> { let path = req.0 .0.uri().path().to_string(); let inner = self.call(req.0.take().0); Box::pin(async move { ActixResponse::from(inner.await.unwrap_or_else(|e| { - let err = ServerFnError::new(e); - ActixResponse::error_response(&path, &err).take() + let err = + ser(ServerFnErrorErr::MiddlewareError(e.to_string())); + ActixResponse::error_response(&path, err).take() })) }) } diff --git a/server_fn/src/request/actix.rs b/server_fn/src/request/actix.rs index 9235eacfe6..3bc63bd12f 100644 --- a/server_fn/src/request/actix.rs +++ b/server_fn/src/request/actix.rs @@ -1,4 +1,7 @@ -use crate::{error::ServerFnError, request::Req}; +use crate::{ + error::{FromServerFnError, IntoAppError, ServerFnErrorErr}, + request::Req, +}; use actix_web::{web::Payload, HttpRequest}; use bytes::Bytes; use futures::{Stream, StreamExt}; @@ -33,9 +36,9 @@ impl From<(HttpRequest, Payload)> for ActixRequest { } } -impl Req for ActixRequest +impl Req for ActixRequest where - CustErr: 'static, + E: FromServerFnError, { fn as_query(&self) -> Option<&str> { self.0 .0.uri().query() @@ -53,44 +56,39 @@ where self.header("Referer") } - fn try_into_bytes( - self, - ) -> impl Future>> + Send - { + fn try_into_bytes(self) -> impl Future> + Send { // Actix is going to keep this on a single thread anyway so it's fine to wrap it // with SendWrapper, which makes it `Send` but will panic if it moves to another thread SendWrapper::new(async move { let payload = self.0.take().1; - payload - .to_bytes() - .await - .map_err(|e| ServerFnError::Deserialization(e.to_string())) + payload.to_bytes().await.map_err(|e| { + ServerFnErrorErr::Deserialization(e.to_string()) + .into_app_error() + }) }) } - fn try_into_string( - self, - ) -> impl Future>> + Send - { + fn try_into_string(self) -> impl Future> + Send { // Actix is going to keep this on a single thread anyway so it's fine to wrap it // with SendWrapper, which makes it `Send` but will panic if it moves to another thread SendWrapper::new(async move { let payload = self.0.take().1; - let bytes = payload - .to_bytes() - .await - .map_err(|e| ServerFnError::Deserialization(e.to_string()))?; - String::from_utf8(bytes.into()) - .map_err(|e| ServerFnError::Deserialization(e.to_string())) + let bytes = payload.to_bytes().await.map_err(|e| { + E::from_server_fn_error(ServerFnErrorErr::Deserialization( + e.to_string(), + )) + })?; + String::from_utf8(bytes.into()).map_err(|e| { + E::from_server_fn_error(ServerFnErrorErr::Deserialization( + e.to_string(), + )) + }) }) } fn try_into_stream( self, - ) -> Result< - impl Stream> + Send, - ServerFnError, - > { + ) -> Result> + Send, E> { let payload = self.0.take().1; let stream = payload.map(|res| { res.map_err(|e| ServerFnError::Deserialization(e.to_string())) diff --git a/server_fn/src/request/axum.rs b/server_fn/src/request/axum.rs index e26f7c7676..da02a535eb 100644 --- a/server_fn/src/request/axum.rs +++ b/server_fn/src/request/axum.rs @@ -1,4 +1,7 @@ -use crate::{error::ServerFnError, request::Req}; +use crate::{ + error::{FromServerFnError, IntoAppError, ServerFnErrorErr}, + request::Req, +}; use axum::body::{Body, Bytes}; use futures::{Stream, StreamExt}; use http::{ @@ -8,9 +11,9 @@ use http::{ use http_body_util::BodyExt; use std::borrow::Cow; -impl Req for Request +impl Req for Request where - CustErr: 'static, + E: FromServerFnError, { fn as_query(&self) -> Option<&str> { self.uri().query() @@ -34,29 +37,29 @@ where .map(|h| String::from_utf8_lossy(h.as_bytes())) } - async fn try_into_bytes(self) -> Result> { + async fn try_into_bytes(self) -> Result { let (_parts, body) = self.into_parts(); - body.collect() - .await - .map(|c| c.to_bytes()) - .map_err(|e| ServerFnError::Deserialization(e.to_string())) + body.collect().await.map(|c| c.to_bytes()).map_err(|e| { + ServerFnErrorErr::Deserialization(e.to_string()).into_app_error() + }) } - async fn try_into_string(self) -> Result> { + async fn try_into_string(self) -> Result { let bytes = self.try_into_bytes().await?; - String::from_utf8(bytes.to_vec()) - .map_err(|e| ServerFnError::Deserialization(e.to_string())) + String::from_utf8(bytes.to_vec()).map_err(|e| { + ServerFnErrorErr::Deserialization(e.to_string()).into_app_error() + }) } fn try_into_stream( self, - ) -> Result< - impl Stream> + Send + 'static, - ServerFnError, - > { + ) -> Result> + Send + 'static, E> { Ok(self.into_body().into_data_stream().map(|chunk| { - chunk.map_err(|e| ServerFnError::Deserialization(e.to_string())) + chunk.map_err(|e| { + ServerFnErrorErr::Deserialization(e.to_string()) + .into_app_error() + }) })) } } diff --git a/server_fn/src/request/browser.rs b/server_fn/src/request/browser.rs index 550b898cdf..839c4d8fe7 100644 --- a/server_fn/src/request/browser.rs +++ b/server_fn/src/request/browser.rs @@ -1,5 +1,8 @@ use super::ClientReq; -use crate::{client::get_server_url, error::ServerFnError}; +use crate::{ + client::get_server_url, + error::{FromServerFnError, ServerFnErrorErr}, +}; use bytes::Bytes; use futures::{Stream, StreamExt}; pub use gloo_net::http::Request; @@ -83,7 +86,10 @@ fn abort_signal() -> (Option, Option) { (ctrl.map(|ctrl| AbortOnDrop(Some(ctrl))), signal) } -impl ClientReq for BrowserRequest { +impl ClientReq for BrowserRequest +where + E: FromServerFnError, +{ type FormData = BrowserFormData; fn try_new_get( @@ -91,7 +97,7 @@ impl ClientReq for BrowserRequest { accepts: &str, content_type: &str, query: &str, - ) -> Result> { + ) -> Result { let (abort_ctrl, abort_signal) = abort_signal(); let server_url = get_server_url(); let mut url = String::with_capacity( @@ -107,7 +113,11 @@ impl ClientReq for BrowserRequest { .header("Accept", accepts) .abort_signal(abort_signal.as_ref()) .build() - .map_err(|e| ServerFnError::Request(e.to_string()))?, + .map_err(|e| { + E::from_server_fn_error(ServerFnErrorErr::Request( + e.to_string(), + )) + })?, abort_ctrl, }))) } @@ -117,7 +127,7 @@ impl ClientReq for BrowserRequest { accepts: &str, content_type: &str, body: String, - ) -> Result> { + ) -> Result { let (abort_ctrl, abort_signal) = abort_signal(); let server_url = get_server_url(); let mut url = String::with_capacity(server_url.len() + path.len()); @@ -129,7 +139,11 @@ impl ClientReq for BrowserRequest { .header("Accept", accepts) .abort_signal(abort_signal.as_ref()) .body(body) - .map_err(|e| ServerFnError::Request(e.to_string()))?, + .map_err(|e| { + E::from_server_fn_error(ServerFnErrorErr::Request( + e.to_string(), + )) + })?, abort_ctrl, }))) } @@ -139,7 +153,7 @@ impl ClientReq for BrowserRequest { accepts: &str, content_type: &str, body: Bytes, - ) -> Result> { + ) -> Result { let (abort_ctrl, abort_signal) = abort_signal(); let server_url = get_server_url(); let mut url = String::with_capacity(server_url.len() + path.len()); @@ -153,7 +167,11 @@ impl ClientReq for BrowserRequest { .header("Accept", accepts) .abort_signal(abort_signal.as_ref()) .body(body) - .map_err(|e| ServerFnError::Request(e.to_string()))?, + .map_err(|e| { + E::from_server_fn_error(ServerFnErrorErr::Request( + e.to_string(), + )) + })?, abort_ctrl, }))) } @@ -162,7 +180,7 @@ impl ClientReq for BrowserRequest { path: &str, accepts: &str, body: Self::FormData, - ) -> Result> { + ) -> Result { let (abort_ctrl, abort_signal) = abort_signal(); let server_url = get_server_url(); let mut url = String::with_capacity(server_url.len() + path.len()); @@ -173,7 +191,11 @@ impl ClientReq for BrowserRequest { .header("Accept", accepts) .abort_signal(abort_signal.as_ref()) .body(body.0.take()) - .map_err(|e| ServerFnError::Request(e.to_string()))?, + .map_err(|e| { + E::from_server_fn_error(ServerFnErrorErr::Request( + e.to_string(), + )) + })?, abort_ctrl, }))) } @@ -183,17 +205,17 @@ impl ClientReq for BrowserRequest { accepts: &str, content_type: &str, body: Self::FormData, - ) -> Result> { + ) -> Result { let (abort_ctrl, abort_signal) = abort_signal(); let form_data = body.0.take(); let url_params = UrlSearchParams::new_with_str_sequence_sequence(&form_data) .map_err(|e| { - ServerFnError::Serialization(e.as_string().unwrap_or_else( - || { + E::from_server_fn_error(ServerFnErrorErr::Serialization( + e.as_string().unwrap_or_else(|| { "Could not serialize FormData to URLSearchParams" .to_string() - }, + }), )) })?; Ok(Self(SendWrapper::new(RequestInner { @@ -202,7 +224,11 @@ impl ClientReq for BrowserRequest { .header("Accept", accepts) .abort_signal(abort_signal.as_ref()) .body(url_params) - .map_err(|e| ServerFnError::Request(e.to_string()))?, + .map_err(|e| { + E::from_server_fn_error(ServerFnErrorErr::Request( + e.to_string(), + )) + })?, abort_ctrl, }))) } @@ -212,11 +238,16 @@ impl ClientReq for BrowserRequest { accepts: &str, content_type: &str, body: impl Stream + 'static, - ) -> Result> { + ) -> Result { // TODO abort signal let (request, abort_ctrl) = - streaming_request(path, accepts, content_type, body) - .map_err(|e| ServerFnError::Request(format!("{e:?}")))?; + streaming_request(path, accepts, content_type, body).map_err( + |e| { + E::from_server_fn_error(ServerFnErrorErr::Request(format!( + "{e:?}" + ))) + }, + )?; Ok(Self(SendWrapper::new(RequestInner { request, abort_ctrl, diff --git a/server_fn/src/request/generic.rs b/server_fn/src/request/generic.rs index da1add07ff..99a2838577 100644 --- a/server_fn/src/request/generic.rs +++ b/server_fn/src/request/generic.rs @@ -12,7 +12,10 @@ //! * `wasm32-wasip*` integration crate `leptos_wasi` is using this //! crate under the hood. -use crate::request::Req; +use crate::{ + error::{FromServerFnError, IntoAppError, ServerFnErrorErr}, + request::Req, +}; use bytes::Bytes; use futures::{ stream::{self, Stream}, @@ -21,30 +24,23 @@ use futures::{ use http::Request; use std::borrow::Cow; -impl Req for Request +impl Req for Request where - CustErr: 'static, + E: FromServerFnError, { - async fn try_into_bytes( - self, - ) -> Result> { + async fn try_into_bytes(self) -> Result { Ok(self.into_body()) } - async fn try_into_string( - self, - ) -> Result> { + async fn try_into_string(self) -> Result { String::from_utf8(self.into_body().into()).map_err(|err| { - crate::ServerFnError::Deserialization(err.to_string()) + ServerFnErrorErr::Deserialization(err.to_string()).into_app_error() }) } fn try_into_stream( self, - ) -> Result< - impl Stream> + Send + 'static, - crate::ServerFnError, - > { + ) -> Result> + Send + 'static, E> { Ok(stream::iter(self.into_body()) .ready_chunks(16) .map(|chunk| Ok(Bytes::from(chunk)))) diff --git a/server_fn/src/request/mod.rs b/server_fn/src/request/mod.rs index 3a4c71d393..f8340414c5 100644 --- a/server_fn/src/request/mod.rs +++ b/server_fn/src/request/mod.rs @@ -1,4 +1,3 @@ -use crate::error::ServerFnError; use bytes::Bytes; use futures::Stream; use std::{borrow::Cow, future::Future}; @@ -19,7 +18,7 @@ pub mod generic; pub mod reqwest; /// Represents a request as made by the client. -pub trait ClientReq +pub trait ClientReq where Self: Sized, { @@ -32,7 +31,7 @@ where content_type: &str, accepts: &str, query: &str, - ) -> Result>; + ) -> Result; /// Attempts to construct a new `POST` request with a text body. fn try_new_post( @@ -40,7 +39,7 @@ where content_type: &str, accepts: &str, body: String, - ) -> Result>; + ) -> Result; /// Attempts to construct a new `POST` request with a binary body. fn try_new_post_bytes( @@ -48,7 +47,7 @@ where content_type: &str, accepts: &str, body: Bytes, - ) -> Result>; + ) -> Result; /// Attempts to construct a new `POST` request with form data as the body. fn try_new_post_form_data( @@ -56,14 +55,14 @@ where accepts: &str, content_type: &str, body: Self::FormData, - ) -> Result>; + ) -> Result; /// Attempts to construct a new `POST` request with a multipart body. fn try_new_multipart( path: &str, accepts: &str, body: Self::FormData, - ) -> Result>; + ) -> Result; /// Attempts to construct a new `POST` request with a streaming body. fn try_new_streaming( @@ -71,11 +70,11 @@ where accepts: &str, content_type: &str, body: impl Stream + Send + 'static, - ) -> Result>; + ) -> Result; } /// Represents the request as received by the server. -pub trait Req +pub trait Req where Self: Sized, { @@ -92,32 +91,22 @@ where fn referer(&self) -> Option>; /// Attempts to extract the body of the request into [`Bytes`]. - fn try_into_bytes( - self, - ) -> impl Future>> + Send; + fn try_into_bytes(self) -> impl Future> + Send; /// Attempts to convert the body of the request into a string. - fn try_into_string( - self, - ) -> impl Future>> + Send; + fn try_into_string(self) -> impl Future> + Send; /// Attempts to convert the body of the request into a stream of bytes. fn try_into_stream( self, - ) -> Result< - impl Stream> + Send + 'static, - ServerFnError, - >; + ) -> Result> + Send + 'static, E>; } /// A mocked request type that can be used in place of the actual server request, /// when compiling for the browser. pub struct BrowserMockReq; -impl Req for BrowserMockReq -where - CustErr: 'static, -{ +impl Req for BrowserMockReq { fn as_query(&self) -> Option<&str> { unreachable!() } @@ -133,20 +122,17 @@ where fn referer(&self) -> Option> { unreachable!() } - async fn try_into_bytes(self) -> Result> { + async fn try_into_bytes(self) -> Result { unreachable!() } - async fn try_into_string(self) -> Result> { + async fn try_into_string(self) -> Result { unreachable!() } fn try_into_stream( self, - ) -> Result< - impl Stream> + Send, - ServerFnError, - > { + ) -> Result> + Send, E> { Ok(futures::stream::once(async { unreachable!() })) } } diff --git a/server_fn/src/request/reqwest.rs b/server_fn/src/request/reqwest.rs index 1352da2fc1..e1ade8d76b 100644 --- a/server_fn/src/request/reqwest.rs +++ b/server_fn/src/request/reqwest.rs @@ -1,5 +1,8 @@ use super::ClientReq; -use crate::{client::get_server_url, error::ServerFnError}; +use crate::{ + client::get_server_url, + error::{FromServerFnError, IntoAppError, ServerFnErrorErr}, +}; use bytes::Bytes; use futures::Stream; use once_cell::sync::Lazy; @@ -8,7 +11,10 @@ pub use reqwest::{multipart::Form, Client, Method, Request, Url}; pub(crate) static CLIENT: Lazy = Lazy::new(Client::new); -impl ClientReq for Request { +impl ClientReq for Request +where + E: FromServerFnError, +{ type FormData = Form; fn try_new_get( @@ -16,17 +22,22 @@ impl ClientReq for Request { accepts: &str, content_type: &str, query: &str, - ) -> Result> { + ) -> Result { let url = format!("{}{}", get_server_url(), path); - let mut url = Url::try_from(url.as_str()) - .map_err(|e| ServerFnError::Request(e.to_string()))?; + let mut url = Url::try_from(url.as_str()).map_err(|e| { + E::from_server_fn_error(ServerFnErrorErr::Request(e.to_string())) + })?; url.set_query(Some(query)); let req = CLIENT .get(url) .header(CONTENT_TYPE, content_type) .header(ACCEPT, accepts) .build() - .map_err(|e| ServerFnError::Request(e.to_string()))?; + .map_err(|e| { + E::from_server_fn_error(ServerFnErrorErr::Request( + e.to_string(), + )) + })?; Ok(req) } @@ -35,7 +46,7 @@ impl ClientReq for Request { accepts: &str, content_type: &str, body: String, - ) -> Result> { + ) -> Result { let url = format!("{}{}", get_server_url(), path); CLIENT .post(url) @@ -43,7 +54,9 @@ impl ClientReq for Request { .header(ACCEPT, accepts) .body(body) .build() - .map_err(|e| ServerFnError::Request(e.to_string())) + .map_err(|e| { + ServerFnErrorErr::Request(e.to_string()).into_app_error() + }) } fn try_new_post_bytes( @@ -51,7 +64,7 @@ impl ClientReq for Request { accepts: &str, content_type: &str, body: Bytes, - ) -> Result> { + ) -> Result { let url = format!("{}{}", get_server_url(), path); CLIENT .post(url) @@ -59,20 +72,24 @@ impl ClientReq for Request { .header(ACCEPT, accepts) .body(body) .build() - .map_err(|e| ServerFnError::Request(e.to_string())) + .map_err(|e| { + ServerFnErrorErr::Request(e.to_string()).into_app_error() + }) } fn try_new_multipart( path: &str, accepts: &str, body: Self::FormData, - ) -> Result> { + ) -> Result { CLIENT .post(path) .header(ACCEPT, accepts) .multipart(body) .build() - .map_err(|e| ServerFnError::Request(e.to_string())) + .map_err(|e| { + ServerFnErrorErr::Request(e.to_string()).into_app_error() + }) } fn try_new_post_form_data( @@ -80,14 +97,16 @@ impl ClientReq for Request { accepts: &str, content_type: &str, body: Self::FormData, - ) -> Result> { + ) -> Result { CLIENT .post(path) .header(CONTENT_TYPE, content_type) .header(ACCEPT, accepts) .multipart(body) .build() - .map_err(|e| ServerFnError::Request(e.to_string())) + .map_err(|e| { + ServerFnErrorErr::Request(e.to_string()).into_app_error() + }) } fn try_new_streaming( @@ -95,7 +114,7 @@ impl ClientReq for Request { _accepts: &str, _content_type: &str, _body: impl Stream + 'static, - ) -> Result> { + ) -> Result { todo!("Streaming requests are not yet implemented for reqwest.") // We run into a fundamental issue here. // To be a reqwest body, the type must be Sync @@ -112,7 +131,7 @@ impl ClientReq for Request { .header(ACCEPT, accepts) .body(body) .build() - .map_err(|e| ServerFnError::Request(e.to_string())) + .map_err(|e| ServerFnErrorErr::Request(e.to_string()).into()) }*/ } } diff --git a/server_fn/src/request/spin.rs b/server_fn/src/request/spin.rs index 58781343d5..f819657a30 100644 --- a/server_fn/src/request/spin.rs +++ b/server_fn/src/request/spin.rs @@ -8,7 +8,7 @@ use http::{ use http_body_util::BodyExt; use std::borrow::Cow; -impl Req for IncomingRequest +impl Req for IncomingRequest where CustErr: 'static, { @@ -34,29 +34,31 @@ where .map(|h| String::from_utf8_lossy(h.as_bytes())) } - async fn try_into_bytes(self) -> Result> { + async fn try_into_bytes(self) -> Result { let (_parts, body) = self.into_parts(); - body.collect() - .await - .map(|c| c.to_bytes()) - .map_err(|e| ServerFnError::Deserialization(e.to_string())) + body.collect().await.map(|c| c.to_bytes()).map_err(|e| { + ServerFnErrorErr::Deserialization(e.to_string()).into() + }) } - async fn try_into_string(self) -> Result> { + async fn try_into_string(self) -> Result { let bytes = self.try_into_bytes().await?; - String::from_utf8(bytes.to_vec()) - .map_err(|e| ServerFnError::Deserialization(e.to_string())) + String::from_utf8(bytes.to_vec()).map_err(|e| { + ServerFnErrorErr::Deserialization(e.to_string()).into() + }) } fn try_into_stream( self, ) -> Result< impl Stream> + Send + 'static, - ServerFnError, + E, > { Ok(self.into_body().into_data_stream().map(|chunk| { - chunk.map_err(|e| ServerFnError::Deserialization(e.to_string())) + chunk.map_err(|e| { + ServerFnErrorErr::Deserialization(e.to_string()).into() + }) })) } } diff --git a/server_fn/src/response/actix.rs b/server_fn/src/response/actix.rs index 711268e717..3a168b3dad 100644 --- a/server_fn/src/response/actix.rs +++ b/server_fn/src/response/actix.rs @@ -1,6 +1,6 @@ -use super::Res; +use super::{Res, TryRes}; use crate::error::{ - ServerFnError, ServerFnErrorErr, ServerFnErrorSerde, SERVER_FN_ERROR_HEADER, + FromServerFnError, ServerFnErrorWrapper, SERVER_FN_ERROR_HEADER, }; use actix_web::{ http::{ @@ -13,10 +13,6 @@ use actix_web::{ use bytes::Bytes; use futures::{Stream, StreamExt}; use send_wrapper::SendWrapper; -use std::{ - fmt::{Debug, Display}, - str::FromStr, -}; /// A wrapped Actix response. /// @@ -38,14 +34,11 @@ impl From for ActixResponse { } } -impl Res for ActixResponse +impl TryRes for ActixResponse where - CustErr: FromStr + Display + Debug + 'static, + E: FromServerFnError, { - fn try_from_string( - content_type: &str, - data: String, - ) -> Result> { + fn try_from_string(content_type: &str, data: String) -> Result { let mut builder = HttpResponse::build(StatusCode::OK); Ok(ActixResponse(SendWrapper::new( builder @@ -54,10 +47,7 @@ where ))) } - fn try_from_bytes( - content_type: &str, - data: Bytes, - ) -> Result> { + fn try_from_bytes(content_type: &str, data: Bytes) -> Result { let mut builder = HttpResponse::build(StatusCode::OK); Ok(ActixResponse(SendWrapper::new( builder @@ -68,23 +58,23 @@ where fn try_from_stream( content_type: &str, - data: impl Stream>> + 'static, - ) -> Result> { + data: impl Stream> + 'static, + ) -> Result { let mut builder = HttpResponse::build(StatusCode::OK); Ok(ActixResponse(SendWrapper::new( builder .insert_header((header::CONTENT_TYPE, content_type)) - .streaming( - data.map(|data| data.map_err(ServerFnErrorErr::from)), - ), + .streaming(data.map(|data| data.map_err(ServerFnErrorWrapper))), ))) } +} - fn error_response(path: &str, err: &ServerFnError) -> Self { +impl Res for ActixResponse { + fn error_response(path: &str, err: String) -> Self { ActixResponse(SendWrapper::new( HttpResponse::build(StatusCode::INTERNAL_SERVER_ERROR) .append_header((SERVER_FN_ERROR_HEADER, path)) - .body(err.ser().unwrap_or_else(|_| err.to_string())), + .body(err), )) } diff --git a/server_fn/src/response/browser.rs b/server_fn/src/response/browser.rs index 6c4cfcc1b5..8f16f03de9 100644 --- a/server_fn/src/response/browser.rs +++ b/server_fn/src/response/browser.rs @@ -1,5 +1,8 @@ use super::ClientRes; -use crate::{error::ServerFnError, redirect::REDIRECT_HEADER}; +use crate::{ + error::{FromServerFnError, IntoAppError, ServerFnErrorErr}, + redirect::REDIRECT_HEADER, +}; use bytes::Bytes; use futures::{Stream, StreamExt}; pub use gloo_net::http::Response; @@ -12,48 +15,39 @@ use wasm_streams::ReadableStream; /// The response to a `fetch` request made in the browser. pub struct BrowserResponse(pub(crate) SendWrapper); -impl ClientRes for BrowserResponse { - fn try_into_string( - self, - ) -> impl Future>> + Send - { +impl ClientRes for BrowserResponse { + fn try_into_string(self) -> impl Future> + Send { // the browser won't send this async work between threads (because it's single-threaded) // so we can safely wrap this SendWrapper::new(async move { - self.0 - .text() - .await - .map_err(|e| ServerFnError::Deserialization(e.to_string())) + self.0.text().await.map_err(|e| { + ServerFnErrorErr::Deserialization(e.to_string()) + .into_app_error() + }) }) } - fn try_into_bytes( - self, - ) -> impl Future>> + Send - { + fn try_into_bytes(self) -> impl Future> + Send { // the browser won't send this async work between threads (because it's single-threaded) // so we can safely wrap this SendWrapper::new(async move { - self.0 - .binary() - .await - .map(Bytes::from) - .map_err(|e| ServerFnError::Deserialization(e.to_string())) + self.0.binary().await.map(Bytes::from).map_err(|e| { + ServerFnErrorErr::Deserialization(e.to_string()) + .into_app_error() + }) }) } fn try_into_stream( self, - ) -> Result< - impl Stream> + Send + 'static, - ServerFnError, - > { + ) -> Result> + Send + 'static, E> { let stream = ReadableStream::from_raw(self.0.body().unwrap()) .into_stream() .map(|data| match data { Err(e) => { web_sys::console::error_1(&e); - Err(ServerFnError::Request(format!("{e:?}"))) + Err(ServerFnErrorErr::Request(format!("{e:?}")) + .into_app_error()) } Ok(data) => { let data = data.unchecked_into::(); diff --git a/server_fn/src/response/generic.rs b/server_fn/src/response/generic.rs index f9e10b5f4c..cdb06093fb 100644 --- a/server_fn/src/response/generic.rs +++ b/server_fn/src/response/generic.rs @@ -12,18 +12,15 @@ //! * `wasm32-wasip*` integration crate `leptos_wasi` is using this //! crate under the hood. -use super::Res; +use super::{Res, TryRes}; use crate::error::{ - ServerFnError, ServerFnErrorErr, ServerFnErrorSerde, SERVER_FN_ERROR_HEADER, + FromServerFnError, IntoAppError, ServerFnErrorErr, ServerFnErrorWrapper, + SERVER_FN_ERROR_HEADER, }; use bytes::Bytes; use futures::{Stream, TryStreamExt}; use http::{header, HeaderValue, Response, StatusCode}; -use std::{ - fmt::{Debug, Display}, - pin::Pin, - str::FromStr, -}; +use std::pin::Pin; use throw_error::Error; /// The Body of a Response whose *execution model* can be @@ -44,55 +41,55 @@ impl From for Body { } } -impl Res for Response +impl TryRes for Response where - CustErr: Send + Sync + Debug + FromStr + Display + 'static, + E: Send + Sync + FromServerFnError, { - fn try_from_string( - content_type: &str, - data: String, - ) -> Result> { + fn try_from_string(content_type: &str, data: String) -> Result { let builder = http::Response::builder(); builder .status(200) .header(http::header::CONTENT_TYPE, content_type) .body(data.into()) - .map_err(|e| ServerFnError::Response(e.to_string())) + .map_err(|e| { + ServerFnErrorErr::Response(e.to_string()).into_app_error() + }) } - fn try_from_bytes( - content_type: &str, - data: Bytes, - ) -> Result> { + fn try_from_bytes(content_type: &str, data: Bytes) -> Result { let builder = http::Response::builder(); builder .status(200) .header(http::header::CONTENT_TYPE, content_type) .body(Body::Sync(data)) - .map_err(|e| ServerFnError::Response(e.to_string())) + .map_err(|e| { + ServerFnErrorErr::Response(e.to_string()).into_app_error() + }) } fn try_from_stream( content_type: &str, - data: impl Stream>> - + Send - + 'static, - ) -> Result> { + data: impl Stream> + Send + 'static, + ) -> Result { let builder = http::Response::builder(); builder .status(200) .header(http::header::CONTENT_TYPE, content_type) .body(Body::Async(Box::pin( - data.map_err(ServerFnErrorErr::from).map_err(Error::from), + data.map_err(ServerFnErrorWrapper).map_err(Error::from), ))) - .map_err(|e| ServerFnError::Response(e.to_string())) + .map_err(|e| { + ServerFnErrorErr::Response(e.to_string()).into_app_error() + }) } +} - fn error_response(path: &str, err: &ServerFnError) -> Self { +impl Res for Response { + fn error_response(path: &str, err: String) -> Self { Response::builder() .status(http::StatusCode::INTERNAL_SERVER_ERROR) .header(SERVER_FN_ERROR_HEADER, path) - .body(err.ser().unwrap_or_else(|_| err.to_string()).into()) + .body(err.into()) .unwrap() } diff --git a/server_fn/src/response/http.rs b/server_fn/src/response/http.rs index e8117f75cc..15caa5b95d 100644 --- a/server_fn/src/response/http.rs +++ b/server_fn/src/response/http.rs @@ -1,65 +1,61 @@ -use super::Res; +use super::{Res, TryRes}; use crate::error::{ - ServerFnError, ServerFnErrorErr, ServerFnErrorSerde, SERVER_FN_ERROR_HEADER, + FromServerFnError, IntoAppError, ServerFnErrorErr, ServerFnErrorWrapper, + SERVER_FN_ERROR_HEADER, }; use axum::body::Body; use bytes::Bytes; -use futures::{Stream, StreamExt}; +use futures::{Stream, TryStreamExt}; use http::{header, HeaderValue, Response, StatusCode}; -use std::{ - fmt::{Debug, Display}, - str::FromStr, -}; -impl Res for Response +impl TryRes for Response where - CustErr: Send + Sync + Debug + FromStr + Display + 'static, + E: Send + Sync + FromServerFnError, { - fn try_from_string( - content_type: &str, - data: String, - ) -> Result> { + fn try_from_string(content_type: &str, data: String) -> Result { let builder = http::Response::builder(); builder .status(200) .header(http::header::CONTENT_TYPE, content_type) .body(Body::from(data)) - .map_err(|e| ServerFnError::Response(e.to_string())) + .map_err(|e| { + ServerFnErrorErr::Response(e.to_string()).into_app_error() + }) } - fn try_from_bytes( - content_type: &str, - data: Bytes, - ) -> Result> { + fn try_from_bytes(content_type: &str, data: Bytes) -> Result { let builder = http::Response::builder(); builder .status(200) .header(http::header::CONTENT_TYPE, content_type) .body(Body::from(data)) - .map_err(|e| ServerFnError::Response(e.to_string())) + .map_err(|e| { + ServerFnErrorErr::Response(e.to_string()).into_app_error() + }) } fn try_from_stream( content_type: &str, - data: impl Stream>> - + Send - + 'static, - ) -> Result> { - let body = - Body::from_stream(data.map(|n| n.map_err(ServerFnErrorErr::from))); + data: impl Stream> + Send + 'static, + ) -> Result { + let body = Body::from_stream(data.map_err(|e| ServerFnErrorWrapper(e))); let builder = http::Response::builder(); builder .status(200) .header(http::header::CONTENT_TYPE, content_type) .body(body) - .map_err(|e| ServerFnError::Response(e.to_string())) + .map_err(|e| { + ServerFnErrorErr::Response(e.to_string()).into_app_error() + }) } +} - fn error_response(path: &str, err: &ServerFnError) -> Self { +impl Res for Response { + fn error_response(path: &str, err: String) -> Self { Response::builder() .status(http::StatusCode::INTERNAL_SERVER_ERROR) .header(SERVER_FN_ERROR_HEADER, path) - .body(err.ser().unwrap_or_else(|_| err.to_string()).into()) + .body(err.into()) .unwrap() } diff --git a/server_fn/src/response/mod.rs b/server_fn/src/response/mod.rs index 6a0f60bace..f479dbaa81 100644 --- a/server_fn/src/response/mod.rs +++ b/server_fn/src/response/mod.rs @@ -13,62 +13,49 @@ pub mod http; #[cfg(feature = "reqwest")] pub mod reqwest; -use crate::error::ServerFnError; use bytes::Bytes; use futures::Stream; use std::future::Future; /// Represents the response as created by the server; -pub trait Res +pub trait TryRes where Self: Sized, { /// Attempts to convert a UTF-8 string into an HTTP response. - fn try_from_string( - content_type: &str, - data: String, - ) -> Result>; + fn try_from_string(content_type: &str, data: String) -> Result; /// Attempts to convert a binary blob represented as bytes into an HTTP response. - fn try_from_bytes( - content_type: &str, - data: Bytes, - ) -> Result>; + fn try_from_bytes(content_type: &str, data: Bytes) -> Result; /// Attempts to convert a stream of bytes into an HTTP response. fn try_from_stream( content_type: &str, - data: impl Stream>> - + Send - + 'static, - ) -> Result>; + data: impl Stream> + Send + 'static, + ) -> Result; +} +/// Represents the response as created by the server; +pub trait Res { /// Converts an error into a response, with a `500` status code and the error text as its body. - fn error_response(path: &str, err: &ServerFnError) -> Self; + fn error_response(path: &str, err: String) -> Self; /// Redirect the response by setting a 302 code and Location header. fn redirect(&mut self, path: &str); } /// Represents the response as received by the client. -pub trait ClientRes { +pub trait ClientRes { /// Attempts to extract a UTF-8 string from an HTTP response. - fn try_into_string( - self, - ) -> impl Future>> + Send; + fn try_into_string(self) -> impl Future> + Send; /// Attempts to extract a binary blob from an HTTP response. - fn try_into_bytes( - self, - ) -> impl Future>> + Send; + fn try_into_bytes(self) -> impl Future> + Send; /// Attempts to extract a binary stream from an HTTP response. fn try_into_stream( self, - ) -> Result< - impl Stream> + Send + Sync + 'static, - ServerFnError, - >; + ) -> Result> + Send + Sync + 'static, E>; /// HTTP status code of the response. fn status(&self) -> u16; @@ -91,29 +78,25 @@ pub trait ClientRes { /// server response type when compiling for the client. pub struct BrowserMockRes; -impl Res for BrowserMockRes { - fn try_from_string( - _content_type: &str, - _data: String, - ) -> Result> { +impl TryRes for BrowserMockRes { + fn try_from_string(_content_type: &str, _data: String) -> Result { unreachable!() } - fn try_from_bytes( - _content_type: &str, - _data: Bytes, - ) -> Result> { + fn try_from_bytes(_content_type: &str, _data: Bytes) -> Result { unreachable!() } - fn error_response(_path: &str, _err: &ServerFnError) -> Self { + fn try_from_stream( + _content_type: &str, + _data: impl Stream>, + ) -> Result { unreachable!() } +} - fn try_from_stream( - _content_type: &str, - _data: impl Stream>>, - ) -> Result> { +impl Res for BrowserMockRes { + fn error_response(_path: &str, _err: String) -> Self { unreachable!() } diff --git a/server_fn/src/response/reqwest.rs b/server_fn/src/response/reqwest.rs index f60338e48d..7bbff5e8cc 100644 --- a/server_fn/src/response/reqwest.rs +++ b/server_fn/src/response/reqwest.rs @@ -1,31 +1,28 @@ use super::ClientRes; -use crate::error::ServerFnError; +use crate::error::{FromServerFnError, IntoAppError, ServerFnErrorErr}; use bytes::Bytes; use futures::{Stream, TryStreamExt}; use reqwest::Response; -impl ClientRes for Response { - async fn try_into_string(self) -> Result> { - self.text() - .await - .map_err(|e| ServerFnError::Deserialization(e.to_string())) +impl ClientRes for Response { + async fn try_into_string(self) -> Result { + self.text().await.map_err(|e| { + ServerFnErrorErr::Deserialization(e.to_string()).into_app_error() + }) } - async fn try_into_bytes(self) -> Result> { - self.bytes() - .await - .map_err(|e| ServerFnError::Deserialization(e.to_string())) + async fn try_into_bytes(self) -> Result { + self.bytes().await.map_err(|e| { + ServerFnErrorErr::Deserialization(e.to_string()).into_app_error() + }) } fn try_into_stream( self, - ) -> Result< - impl Stream> + Send + 'static, - ServerFnError, - > { - Ok(self - .bytes_stream() - .map_err(|e| ServerFnError::Response(e.to_string()))) + ) -> Result> + Send + 'static, E> { + Ok(self.bytes_stream().map_err(|e| { + ServerFnErrorErr::Response(e.to_string()).into_app_error() + })) } fn status(&self) -> u16 { diff --git a/server_fn_macro/src/lib.rs b/server_fn_macro/src/lib.rs index 864cba1204..c955418b68 100644 --- a/server_fn_macro/src/lib.rs +++ b/server_fn_macro/src/lib.rs @@ -382,13 +382,8 @@ pub fn server_macro_impl( quote! { #server_fn_path::inventory::submit! {{ use #server_fn_path::{ServerFn, codec::Encoding}; - #server_fn_path::ServerFnTraitObj::new( - #wrapped_struct_name_turbofish::PATH, - <#wrapped_struct_name as ServerFn>::InputEncoding::METHOD, - |req| { - Box::pin(#wrapped_struct_name_turbofish::run_on_server(req)) - }, - #wrapped_struct_name_turbofish::middlewares + #server_fn_path::ServerFnTraitObj::new::<#wrapped_struct_name>( + |req| Box::pin(#wrapped_struct_name_turbofish::run_on_server(req)), ) }} } @@ -730,12 +725,12 @@ fn output_type(return_ty: &Type) -> Result<&GenericArgument> { Err(syn::Error::new( return_ty.span(), - "server functions should return Result or Result>", + "server functions should return Result where E: \ + FromServerFnError", )) } -fn err_type(return_ty: &Type) -> Result> { +fn err_type(return_ty: &Type) -> Result> { if let syn::Type::Path(pat) = &return_ty { if pat.path.segments[0].ident == "Result" { if let PathArguments::AngleBracketed(args) = @@ -746,25 +741,8 @@ fn err_type(return_ty: &Type) -> Result> { return Ok(None); } // Result - else if let GenericArgument::Type(Type::Path(pat)) = - &args.args[1] - { - if let Some(segment) = pat.path.segments.last() { - if segment.ident == "ServerFnError" { - let args = &segment.arguments; - match args { - // Result - PathArguments::None => return Ok(None), - // Result> - PathArguments::AngleBracketed(args) => { - if args.args.len() == 1 { - return Ok(Some(&args.args[0])); - } - } - _ => {} - } - } - } + else if let GenericArgument::Type(ty) = &args.args[1] { + return Ok(Some(ty)); } } } @@ -772,8 +750,8 @@ fn err_type(return_ty: &Type) -> Result> { Err(syn::Error::new( return_ty.span(), - "server functions should return Result or Result>", + "server functions should return Result where E: \ + FromServerFnError", )) } From 3dc3366715d4a1a3d608db7b7ef5c927cbe99da9 Mon Sep 17 00:00:00 2001 From: Mario Carbajal <1387293+basro@users.noreply.github.com> Date: Fri, 17 Jan 2025 15:32:04 -0300 Subject: [PATCH 08/17] feat: impl Dispose for Callback types and add try_run to the Callable trait (#3371) * impl Dispose for Callback types and add try_run to the Callable trait * [autofix.ci] apply automated fixes --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- leptos/src/callback.rs | 50 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 46 insertions(+), 4 deletions(-) diff --git a/leptos/src/callback.rs b/leptos/src/callback.rs index 3db4d24cd1..db97578586 100644 --- a/leptos/src/callback.rs +++ b/leptos/src/callback.rs @@ -43,13 +43,20 @@ use reactive_graph::{ owner::{LocalStorage, StoredValue}, - traits::WithValue, + traits::{Dispose, WithValue}, }; use std::{fmt, rc::Rc, sync::Arc}; /// A wrapper trait for calling callbacks. pub trait Callable { /// calls the callback with the specified argument. + /// + /// Returns None if the callback has been disposed + fn try_run(&self, input: In) -> Option; + /// calls the callback with the specified argument. + /// + /// # Panics + /// Panics if you try to run a callback that has been disposed fn run(&self, input: In) -> Out; } @@ -72,6 +79,12 @@ impl Clone for UnsyncCallback { } } +impl Dispose for UnsyncCallback { + fn dispose(self) { + self.0.dispose(); + } +} + impl UnsyncCallback { /// Creates a new callback from the given function. pub fn new(f: F) -> UnsyncCallback @@ -93,6 +106,10 @@ impl UnsyncCallback { } impl Callable for UnsyncCallback { + fn try_run(&self, input: In) -> Option { + self.0.try_with_value(|fun| fun(input)) + } + fn run(&self, input: In) -> Out { self.0.with_value(|fun| fun(input)) } @@ -168,10 +185,12 @@ impl fmt::Debug for Callback { } impl Callable for Callback { + fn try_run(&self, input: In) -> Option { + self.0.try_with_value(|fun| fun(input)) + } + fn run(&self, input: In) -> Out { - self.0 - .try_with_value(|f| f(input)) - .expect("called a callback that has been disposed") + self.0.with_value(|f| f(input)) } } @@ -181,6 +200,12 @@ impl Clone for Callback { } } +impl Dispose for Callback { + fn dispose(self) { + self.0.dispose(); + } +} + impl Copy for Callback {} macro_rules! impl_callable_from_fn { @@ -239,7 +264,9 @@ impl Callback { #[cfg(test)] mod tests { + use super::Callable; use crate::callback::{Callback, UnsyncCallback}; + use reactive_graph::traits::Dispose; struct NoClone {} @@ -297,4 +324,19 @@ mod tests { let callback2 = UnsyncCallback::new(|x: i32| x + 1); assert!(!callback1.matches(&callback2)); } + + fn sync_callback_try_run() { + let callback = Callback::new(move |arg| arg); + assert_eq!(callback.try_run((0,)), Some((0,))); + callback.dispose(); + assert_eq!(callback.try_run((0,)), None); + } + + #[test] + fn unsync_callback_try_run() { + let callback = UnsyncCallback::new(move |arg| arg); + assert_eq!(callback.try_run((0,)), Some((0,))); + callback.dispose(); + assert_eq!(callback.try_run((0,)), None); + } } From ec7ada47fd6acfe67ff083e8def4971288b5f3b4 Mon Sep 17 00:00:00 2001 From: Greg Johnston Date: Fri, 17 Jan 2025 13:33:00 -0500 Subject: [PATCH 09/17] feat(breaking): allow make `PossibleRouteMatch` dyn-safe (#3421) --- router/src/matching/horizontal/mod.rs | 31 ++++++++++++++++++- .../src/matching/horizontal/param_segments.rs | 12 ++++++- .../src/matching/horizontal/static_segment.rs | 8 +++++ router/src/matching/horizontal/tuples.rs | 26 +++++++++++----- router/src/matching/nested/mod.rs | 2 +- 5 files changed, 68 insertions(+), 11 deletions(-) diff --git a/router/src/matching/horizontal/mod.rs b/router/src/matching/horizontal/mod.rs index 8bef7dcc8b..a8846831a2 100644 --- a/router/src/matching/horizontal/mod.rs +++ b/router/src/matching/horizontal/mod.rs @@ -1,4 +1,5 @@ use super::{PartialPathMatch, PathSegment}; +use std::sync::Arc; mod param_segments; mod static_segment; mod tuples; @@ -11,9 +12,37 @@ pub use static_segment::*; /// This is a "horizontal" matching: i.e., it treats a tuple of route segments /// as subsequent segments of the URL and tries to match them all. pub trait PossibleRouteMatch { - const OPTIONAL: bool = false; + fn optional(&self) -> bool; fn test<'a>(&self, path: &'a str) -> Option>; fn generate_path(&self, path: &mut Vec); } + +impl PossibleRouteMatch for Box { + fn optional(&self) -> bool { + (**self).optional() + } + + fn test<'a>(&self, path: &'a str) -> Option> { + (**self).test(path) + } + + fn generate_path(&self, path: &mut Vec) { + (**self).generate_path(path); + } +} + +impl PossibleRouteMatch for Arc { + fn optional(&self) -> bool { + (**self).optional() + } + + fn test<'a>(&self, path: &'a str) -> Option> { + (**self).test(path) + } + + fn generate_path(&self, path: &mut Vec) { + (**self).generate_path(path); + } +} diff --git a/router/src/matching/horizontal/param_segments.rs b/router/src/matching/horizontal/param_segments.rs index 35ead7c048..ddef8f02cc 100644 --- a/router/src/matching/horizontal/param_segments.rs +++ b/router/src/matching/horizontal/param_segments.rs @@ -35,6 +35,10 @@ use std::borrow::Cow; pub struct ParamSegment(pub &'static str); impl PossibleRouteMatch for ParamSegment { + fn optional(&self) -> bool { + false + } + fn test<'a>(&self, path: &'a str) -> Option> { let mut matched_len = 0; let mut param_offset = 0; @@ -121,6 +125,10 @@ impl PossibleRouteMatch for ParamSegment { pub struct WildcardSegment(pub &'static str); impl PossibleRouteMatch for WildcardSegment { + fn optional(&self) -> bool { + false + } + fn test<'a>(&self, path: &'a str) -> Option> { let mut matched_len = 0; let mut param_offset = 0; @@ -158,7 +166,9 @@ impl PossibleRouteMatch for WildcardSegment { pub struct OptionalParamSegment(pub &'static str); impl PossibleRouteMatch for OptionalParamSegment { - const OPTIONAL: bool = true; + fn optional(&self) -> bool { + true + } fn test<'a>(&self, path: &'a str) -> Option> { let mut matched_len = 0; diff --git a/router/src/matching/horizontal/static_segment.rs b/router/src/matching/horizontal/static_segment.rs index 5179efecfd..d0f9ed667c 100644 --- a/router/src/matching/horizontal/static_segment.rs +++ b/router/src/matching/horizontal/static_segment.rs @@ -2,6 +2,10 @@ use super::{PartialPathMatch, PathSegment, PossibleRouteMatch}; use std::fmt::Debug; impl PossibleRouteMatch for () { + fn optional(&self) -> bool { + false + } + fn test<'a>(&self, path: &'a str) -> Option> { Some(PartialPathMatch::new(path, vec![], "")) } @@ -54,6 +58,10 @@ impl AsPath for &'static str { pub struct StaticSegment(pub T); impl PossibleRouteMatch for StaticSegment { + fn optional(&self) -> bool { + false + } + fn test<'a>(&self, path: &'a str) -> Option> { let mut matched_len = 0; let mut test = path.chars().peekable(); diff --git a/router/src/matching/horizontal/tuples.rs b/router/src/matching/horizontal/tuples.rs index a4879fb1f0..a2ec7668bb 100644 --- a/router/src/matching/horizontal/tuples.rs +++ b/router/src/matching/horizontal/tuples.rs @@ -8,15 +8,21 @@ macro_rules! tuples { $first: PossibleRouteMatch, $($ty: PossibleRouteMatch),*, { + fn optional(&self) -> bool { + #[allow(non_snake_case)] + let ($first, $($ty,)*) = &self; + [$first.optional(), $($ty.optional()),*].into_iter().any(|n| n) + } + fn test<'a>(&self, path: &'a str) -> Option> { + #[allow(non_snake_case)] + let ($first, $($ty,)*) = &self; + // on the first run, include all optionals let mut include_optionals = { - [$first::OPTIONAL, $($ty::OPTIONAL),*].into_iter().filter(|n| *n).count() + [$first.optional(), $($ty.optional()),*].into_iter().filter(|n| *n).count() }; - #[allow(non_snake_case)] - let ($first, $($ty,)*) = &self; - loop { let mut nth_field = 0; let mut matched_len = 0; @@ -25,7 +31,7 @@ macro_rules! tuples { let mut p = Vec::new(); let mut m = String::new(); - if !$first::OPTIONAL || nth_field < include_optionals { + if !$first.optional() || nth_field < include_optionals { match $first.test(r) { None => { return None; @@ -40,16 +46,16 @@ macro_rules! tuples { matched_len += m.len(); $( - if $ty::OPTIONAL { + if $ty.optional() { nth_field += 1; } - if !$ty::OPTIONAL || nth_field < include_optionals { + if !$ty.optional() || nth_field < include_optionals { let PartialPathMatch { remaining, matched, params } = match $ty.test(r) { - None => if $ty::OPTIONAL { + None => if $ty.optional() { return None; } else { if include_optionals == 0 { @@ -90,6 +96,10 @@ where Self: core::fmt::Debug, A: PossibleRouteMatch, { + fn optional(&self) -> bool { + self.0.optional() + } + fn test<'a>(&self, path: &'a str) -> Option> { let remaining = path; let PartialPathMatch { diff --git a/router/src/matching/nested/mod.rs b/router/src/matching/nested/mod.rs index ea0b21dfa1..67ab2826e5 100644 --- a/router/src/matching/nested/mod.rs +++ b/router/src/matching/nested/mod.rs @@ -151,7 +151,7 @@ impl MatchNestedRoutes for NestedRoute where Self: 'static, - Segments: PossibleRouteMatch + std::fmt::Debug, + Segments: PossibleRouteMatch, Children: MatchNestedRoutes, Children::Match: MatchParams, Children: 'static, From e11664ff9535c37e0d9922cffb3387e130280670 Mon Sep 17 00:00:00 2001 From: Saber Haj Rabiee Date: Fri, 17 Jan 2025 10:38:37 -0800 Subject: [PATCH 10/17] chore: upgrade `axum` to `v0.8` (#3439) --- Cargo.lock | 18 ++++--- examples/axum_js_ssr/Cargo.toml | 25 ++++++--- examples/errors_axum/Cargo.toml | 2 +- examples/errors_axum/src/main.rs | 2 +- examples/hackernews_axum/Cargo.toml | 2 +- examples/hackernews_islands_axum/Cargo.toml | 2 +- examples/hackernews_js_fetch/Cargo.toml | 2 +- examples/islands/Cargo.toml | 7 +-- examples/islands_router/Cargo.toml | 7 +-- examples/server_fns_axum/Cargo.toml | 2 +- examples/ssr_modes_axum/Cargo.toml | 2 +- examples/static_routing/Cargo.toml | 4 +- examples/tailwind_axum/Cargo.toml | 2 +- examples/todo_app_sqlite_axum/Cargo.toml | 2 +- examples/todo_app_sqlite_axum/src/main.rs | 22 +++++--- examples/todo_app_sqlite_csr/Cargo.toml | 2 +- examples/todo_app_sqlite_csr/src/main.rs | 2 +- integrations/axum/Cargo.toml | 4 +- integrations/axum/src/lib.rs | 56 ++++++++++++--------- server_fn/Cargo.toml | 4 +- 20 files changed, 95 insertions(+), 74 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b48de82a7b..f50fe0b874 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -332,6 +332,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" [[package]] +<<<<<<< HEAD name = "async-trait" version = "0.1.85" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -343,6 +344,8 @@ dependencies = [ ] [[package]] +======= +>>>>>>> 35e6f179 (chore: upgrade `axum` to `v0.8` (#3439)) name = "atomic-polyfill" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -389,13 +392,13 @@ checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "axum" -version = "0.7.9" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" +checksum = "6d6fd624c75e18b3b4c6b9caf42b1afe24437daaee904069137d8bab077be8b8" dependencies = [ - "async-trait", "axum-core", "bytes", + "form_urlencoded", "futures-util", "http 1.2.0", "http-body", @@ -424,11 +427,10 @@ dependencies = [ [[package]] name = "axum-core" -version = "0.4.5" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" +checksum = "df1362f362fd16024ae199c1970ce98f9661bf5ef94b9808fee734bc3698b733" dependencies = [ - "async-trait", "bytes", "futures-util", "http 1.2.0", @@ -2064,9 +2066,9 @@ dependencies = [ [[package]] name = "matchit" -version = "0.7.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" [[package]] name = "memchr" diff --git a/examples/axum_js_ssr/Cargo.toml b/examples/axum_js_ssr/Cargo.toml index 1609805331..60a8c8d016 100644 --- a/examples/axum_js_ssr/Cargo.toml +++ b/examples/axum_js_ssr/Cargo.toml @@ -7,7 +7,7 @@ edition = "2021" crate-type = ["cdylib", "rlib"] [dependencies] -axum = { version = "0.7.5", optional = true } +axum = { version = "0.8.1", optional = true } console_error_panic_hook = "0.1.7" console_log = "1.0" gloo-utils = "0.2.0" @@ -20,18 +20,27 @@ leptos_axum = { path = "../../integrations/axum", optional = true } leptos_router = { path = "../../router" } serde = { version = "1.0", features = ["derive"] } thiserror = "1.0" -tokio = { version = "1.39", features = [ "rt-multi-thread", "macros", "time" ], optional = true } +tokio = { version = "1.39", features = [ + "rt-multi-thread", + "macros", + "time", +], optional = true } tower = { version = "0.4.13", optional = true } tower-http = { version = "0.5.2", features = ["fs"], optional = true } wasm-bindgen = "0.2.92" -web-sys = { version = "0.3.69", features = [ "AddEventListenerOptions", "Document", "Element", "Event", "EventListener", "EventTarget", "Performance", "Window" ], optional = true } +web-sys = { version = "0.3.69", features = [ + "AddEventListenerOptions", + "Document", + "Element", + "Event", + "EventListener", + "EventTarget", + "Performance", + "Window", +], optional = true } [features] -hydrate = [ - "leptos/hydrate", - "dep:js-sys", - "dep:web-sys", -] +hydrate = ["leptos/hydrate", "dep:js-sys", "dep:web-sys"] ssr = [ "dep:axum", "dep:http-body-util", diff --git a/examples/errors_axum/Cargo.toml b/examples/errors_axum/Cargo.toml index a66324ff0c..a18068cac2 100644 --- a/examples/errors_axum/Cargo.toml +++ b/examples/errors_axum/Cargo.toml @@ -13,7 +13,7 @@ leptos_axum = { path = "../../integrations/axum", optional = true } leptos_meta = { path = "../../meta" } leptos_router = { path = "../../router" } serde = { version = "1.0", features = ["derive"] } -axum = { version = "0.7.5", optional = true } +axum = { version = "0.8.1", optional = true } tower = { version = "0.4.13", optional = true } tower-http = { version = "0.5.2", features = ["fs"], optional = true } tokio = { version = "1.39", features = ["full"], optional = true } diff --git a/examples/errors_axum/src/main.rs b/examples/errors_axum/src/main.rs index 94d78728a9..f733388828 100644 --- a/examples/errors_axum/src/main.rs +++ b/examples/errors_axum/src/main.rs @@ -45,7 +45,7 @@ async fn main() { // build our application with a route let app = Router::new() - .route("/special/:id", get(custom_handler)) + .route("/special/{id}", get(custom_handler)) .leptos_routes(&leptos_options, routes, { let leptos_options = leptos_options.clone(); move || shell(leptos_options.clone()) diff --git a/examples/hackernews_axum/Cargo.toml b/examples/hackernews_axum/Cargo.toml index 4f64465641..482fac1932 100644 --- a/examples/hackernews_axum/Cargo.toml +++ b/examples/hackernews_axum/Cargo.toml @@ -20,7 +20,7 @@ serde = { version = "1.0", features = ["derive"] } tracing = "0.1.40" gloo-net = { version = "0.6.0", features = ["http"] } reqwest = { version = "0.12.5", features = ["json"] } -axum = { version = "0.7.5", optional = true } +axum = { version = "0.8.1", optional = true } tower = { version = "0.4.13", optional = true } tower-http = { version = "0.5.2", features = ["fs"], optional = true } tokio = { version = "1.39", features = ["full"], optional = true } diff --git a/examples/hackernews_islands_axum/Cargo.toml b/examples/hackernews_islands_axum/Cargo.toml index 933c131228..90314b3605 100644 --- a/examples/hackernews_islands_axum/Cargo.toml +++ b/examples/hackernews_islands_axum/Cargo.toml @@ -20,7 +20,7 @@ serde = { version = "1.0", features = ["derive"] } tracing = "0.1.40" gloo-net = { version = "0.6.0", features = ["http"] } reqwest = { version = "0.12.5", features = ["json"] } -axum = { version = "0.7.5", optional = true, features = ["http2"] } +axum = { version = "0.8.1", optional = true, features = ["http2"] } tower = { version = "0.4.13", optional = true } tower-http = { version = "0.5.2", features = [ "fs", diff --git a/examples/hackernews_js_fetch/Cargo.toml b/examples/hackernews_js_fetch/Cargo.toml index 53fb419a0f..68b3b96b21 100644 --- a/examples/hackernews_js_fetch/Cargo.toml +++ b/examples/hackernews_js_fetch/Cargo.toml @@ -23,7 +23,7 @@ serde = { version = "1.0", features = ["derive"] } tracing = "0.1.40" gloo-net = { version = "0.6.0", features = ["http"] } reqwest = { version = "0.12.5", features = ["json"] } -axum = { version = "0.7.5", default-features = false, optional = true } +axum = { version = "0.8.1", default-features = false, optional = true } tower = { version = "0.4.13", optional = true } http = { version = "1.1", optional = true } web-sys = { version = "0.3.70", features = [ diff --git a/examples/islands/Cargo.toml b/examples/islands/Cargo.toml index 68f53d2133..c05e0d3e70 100644 --- a/examples/islands/Cargo.toml +++ b/examples/islands/Cargo.toml @@ -10,15 +10,12 @@ crate-type = ["cdylib", "rlib"] console_error_panic_hook = "0.1.7" futures = "0.3.30" http = "1.1" -leptos = { path = "../../leptos", features = [ - "tracing", - "islands", -] } +leptos = { path = "../../leptos", features = ["tracing", "islands"] } server_fn = { path = "../../server_fn", features = ["serde-lite"] } leptos_axum = { path = "../../integrations/axum", optional = true } log = "0.4.22" serde = { version = "1.0", features = ["derive"] } -axum = { version = "0.7.5", optional = true } +axum = { version = "0.8.1", optional = true } tower = { version = "0.4.13", optional = true } tower-http = { version = "0.5.2", features = ["fs"], optional = true } tokio = { version = "1.39", features = ["full"], optional = true } diff --git a/examples/islands_router/Cargo.toml b/examples/islands_router/Cargo.toml index ec5abe77e1..31c8f921a9 100644 --- a/examples/islands_router/Cargo.toml +++ b/examples/islands_router/Cargo.toml @@ -10,10 +10,7 @@ crate-type = ["cdylib", "rlib"] console_error_panic_hook = "0.1.7" futures = "0.3.30" http = "1.1" -leptos = { path = "../../leptos", features = [ - "tracing", - "islands", -] } +leptos = { path = "../../leptos", features = ["tracing", "islands"] } leptos_router = { path = "../../router" } server_fn = { path = "../../server_fn", features = ["serde-lite"] } leptos_axum = { path = "../../integrations/axum", features = [ @@ -21,7 +18,7 @@ leptos_axum = { path = "../../integrations/axum", features = [ ], optional = true } log = "0.4.22" serde = { version = "1.0", features = ["derive"] } -axum = { version = "0.7.5", optional = true } +axum = { version = "0.8.1", optional = true } tower = { version = "0.4.13", optional = true } tower-http = { version = "0.5.2", features = ["fs"], optional = true } tokio = { version = "1.39", features = ["full"], optional = true } diff --git a/examples/server_fns_axum/Cargo.toml b/examples/server_fns_axum/Cargo.toml index 1a83c67c9c..ec4923c866 100644 --- a/examples/server_fns_axum/Cargo.toml +++ b/examples/server_fns_axum/Cargo.toml @@ -21,7 +21,7 @@ server_fn = { path = "../../server_fn", features = [ log = "0.4.22" simple_logger = "5.0" serde = { version = "1.0", features = ["derive"] } -axum = { version = "0.7.5", optional = true } +axum = { version = "0.8.1", optional = true } tower = { version = "0.4.13", optional = true } tower-http = { version = "0.5.2", features = [ "fs", diff --git a/examples/ssr_modes_axum/Cargo.toml b/examples/ssr_modes_axum/Cargo.toml index 5a8dd4f97c..2fc4130057 100644 --- a/examples/ssr_modes_axum/Cargo.toml +++ b/examples/ssr_modes_axum/Cargo.toml @@ -20,7 +20,7 @@ leptos_router = { path = "../../router" } log = "0.4.22" serde = { version = "1.0", features = ["derive"] } thiserror = "1.0" -axum = { version = "0.7.5", optional = true } +axum = { version = "0.8.1", optional = true } tower = { version = "0.4.13", optional = true } tower-http = { version = "0.5.2", features = ["fs"], optional = true } tokio = { version = "1.39", features = [ diff --git a/examples/static_routing/Cargo.toml b/examples/static_routing/Cargo.toml index 7df933f9be..6c642011e0 100644 --- a/examples/static_routing/Cargo.toml +++ b/examples/static_routing/Cargo.toml @@ -18,7 +18,7 @@ leptos_router = { path = "../../router" } log = "0.4.22" serde = { version = "1.0", features = ["derive"] } thiserror = "1.0" -axum = { version = "0.7.5", optional = true } +axum = { version = "0.8.1", optional = true } tower = { version = "0.4.13", optional = true } tower-http = { version = "0.5.2", features = ["fs"], optional = true } tokio = { version = "1.39", features = [ @@ -45,7 +45,7 @@ ssr = [ "dep:leptos_axum", "leptos_router/ssr", "dep:notify", - "dep:http" + "dep:http", ] [profile.release] diff --git a/examples/tailwind_axum/Cargo.toml b/examples/tailwind_axum/Cargo.toml index f9547a48ce..9b1961baf6 100644 --- a/examples/tailwind_axum/Cargo.toml +++ b/examples/tailwind_axum/Cargo.toml @@ -7,7 +7,7 @@ edition = "2021" crate-type = ["cdylib", "rlib"] [dependencies] -axum = { version = "0.7.5", optional = true } +axum = { version = "0.8.1", optional = true } console_error_panic_hook = "0.1.7" leptos = { path = "../../leptos" } leptos_meta = { path = "../../meta" } diff --git a/examples/todo_app_sqlite_axum/Cargo.toml b/examples/todo_app_sqlite_axum/Cargo.toml index b182c580c3..f9c754b978 100644 --- a/examples/todo_app_sqlite_axum/Cargo.toml +++ b/examples/todo_app_sqlite_axum/Cargo.toml @@ -16,7 +16,7 @@ leptos_axum = { path = "../../integrations/axum", optional = true } log = "0.4.22" simple_logger = "5.0" serde = { version = "1.0", features = ["derive"] } -axum = { version = "0.7.5", optional = true } +axum = { version = "0.8.1", optional = true } tower = { version = "0.4.13", optional = true } tower-http = { version = "0.5.2", features = ["fs"], optional = true } tokio = { version = "1.39", features = ["full"], optional = true } diff --git a/examples/todo_app_sqlite_axum/src/main.rs b/examples/todo_app_sqlite_axum/src/main.rs index aebbb42aad..554332cb84 100644 --- a/examples/todo_app_sqlite_axum/src/main.rs +++ b/examples/todo_app_sqlite_axum/src/main.rs @@ -1,4 +1,4 @@ -use crate::todo::*; +#[cfg(feature = "ssr")] use axum::{ body::Body, extract::Path, @@ -8,10 +8,9 @@ use axum::{ Router, }; use leptos::prelude::*; -use leptos_axum::{generate_route_list, LeptosRoutes}; use todo_app_sqlite_axum::*; - //Define a handler to test extractor with state +#[cfg(feature = "ssr")] async fn custom_handler( Path(id): Path, req: Request, @@ -20,14 +19,16 @@ async fn custom_handler( move || { provide_context(id.clone()); }, - TodoApp, + todo::TodoApp, ); handler(req).await.into_response() } +#[cfg(feature = "ssr")] #[tokio::main] async fn main() { - use crate::todo::ssr::db; + use crate::todo::{ssr::db, *}; + use leptos_axum::{generate_route_list, LeptosRoutes}; simple_logger::init_with_level(log::Level::Error) .expect("couldn't initialize logging"); @@ -45,7 +46,7 @@ async fn main() { // build our application with a route let app = Router::new() - .route("/special/:id", get(custom_handler)) + .route("/special/{id}", get(custom_handler)) .leptos_routes(&leptos_options, routes, { let leptos_options = leptos_options.clone(); move || shell(leptos_options.clone()) @@ -61,3 +62,12 @@ async fn main() { .await .unwrap(); } + +#[cfg(not(feature = "ssr"))] +pub fn main() { + use leptos::mount::mount_to_body; + + _ = console_log::init_with_level(log::Level::Debug); + console_error_panic_hook::set_once(); + mount_to_body(todo::TodoApp); +} diff --git a/examples/todo_app_sqlite_csr/Cargo.toml b/examples/todo_app_sqlite_csr/Cargo.toml index ac91721f94..c6b59d5c69 100644 --- a/examples/todo_app_sqlite_csr/Cargo.toml +++ b/examples/todo_app_sqlite_csr/Cargo.toml @@ -15,7 +15,7 @@ leptos_meta = { path = "../../meta" } leptos_router = { path = "../../router" } leptos_integration_utils = { path = "../../integrations/utils", optional = true } serde = { version = "1.0", features = ["derive"] } -axum = { version = "0.7.5", optional = true } +axum = { version = "0.8.1", optional = true } tower = { version = "0.5.1", features = ["util"], optional = true } tower-http = { version = "0.6.1", features = ["fs"], optional = true } tokio = { version = "1.39", features = ["full"], optional = true } diff --git a/examples/todo_app_sqlite_csr/src/main.rs b/examples/todo_app_sqlite_csr/src/main.rs index bc35de1681..fbbb8d26ef 100644 --- a/examples/todo_app_sqlite_csr/src/main.rs +++ b/examples/todo_app_sqlite_csr/src/main.rs @@ -34,7 +34,7 @@ async fn main() { // here, we're not actually doing server side rendering, so we set up a manual // handler for the server fns // this should include a get() handler if you have any GetUrl-based server fns - .route("/api/*fn_name", post(leptos_axum::handle_server_fns)) + .route("/api/{*fn_name}", post(leptos_axum::handle_server_fns)) .fallback(file_or_index_handler) .with_state(leptos_options); diff --git a/integrations/axum/Cargo.toml b/integrations/axum/Cargo.toml index 99e7c46a71..1c98f185ed 100644 --- a/integrations/axum/Cargo.toml +++ b/integrations/axum/Cargo.toml @@ -11,7 +11,7 @@ edition.workspace = true [dependencies] any_spawner = { workspace = true, features = ["tokio"] } hydration_context = { workspace = true } -axum = { version = "0.7.9", default-features = false, features = [ +axum = { version = "0.8.1", default-features = false, features = [ "matched-path", ] } dashmap = "6" @@ -30,7 +30,7 @@ tower-http = "0.6.2" tracing = { version = "0.1.41", optional = true } [dev-dependencies] -axum = "0.7.9" +axum = "0.8.1" tokio = { version = "1.41", features = ["net", "rt-multi-thread"] } [features] diff --git a/integrations/axum/src/lib.rs b/integrations/axum/src/lib.rs index ca6a9086dc..57a21c6d57 100644 --- a/integrations/axum/src/lib.rs +++ b/integrations/axum/src/lib.rs @@ -482,7 +482,7 @@ pub type PinnedHtmlStream = tracing::instrument(level = "trace", fields(error), skip_all) )] pub fn render_app_to_stream( - app_fn: impl Fn() -> IV + Clone + Send + 'static, + app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static, ) -> impl Fn( Request, ) -> Pin> + Send + 'static>> @@ -506,7 +506,7 @@ where )] pub fn render_route( paths: Vec, - app_fn: impl Fn() -> IV + Clone + Send + 'static, + app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static, ) -> impl Fn( State, Request, @@ -570,7 +570,7 @@ where tracing::instrument(level = "trace", fields(error), skip_all) )] pub fn render_app_to_stream_in_order( - app_fn: impl Fn() -> IV + Clone + Send + 'static, + app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static, ) -> impl Fn( Request, ) -> Pin> + Send + 'static>> @@ -623,13 +623,14 @@ where tracing::instrument(level = "trace", fields(error), skip_all) )] pub fn render_app_to_stream_with_context( - additional_context: impl Fn() + 'static + Clone + Send, - app_fn: impl Fn() -> IV + Clone + Send + 'static, + additional_context: impl Fn() + 'static + Clone + Send + Sync, + app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static, ) -> impl Fn( Request, ) -> Pin> + Send + 'static>> + Clone + Send + + Sync + 'static where IV: IntoView + 'static, @@ -652,8 +653,8 @@ where )] pub fn render_route_with_context( paths: Vec, - additional_context: impl Fn() + 'static + Clone + Send, - app_fn: impl Fn() -> IV + Clone + Send + 'static, + additional_context: impl Fn() + 'static + Clone + Send + Sync, + app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static, ) -> impl Fn( State, Request, @@ -754,14 +755,15 @@ where tracing::instrument(level = "trace", fields(error), skip_all) )] pub fn render_app_to_stream_with_context_and_replace_blocks( - additional_context: impl Fn() + 'static + Clone + Send, - app_fn: impl Fn() -> IV + Clone + Send + 'static, + additional_context: impl Fn() + 'static + Clone + Send + Sync, + app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static, replace_blocks: bool, ) -> impl Fn( Request, ) -> Pin> + Send + 'static>> + Clone + Send + + Sync + 'static where IV: IntoView + 'static, @@ -821,8 +823,8 @@ where tracing::instrument(level = "trace", fields(error), skip_all) )] pub fn render_app_to_stream_in_order_with_context( - additional_context: impl Fn() + 'static + Clone + Send, - app_fn: impl Fn() -> IV + Clone + Send + 'static, + additional_context: impl Fn() + 'static + Clone + Send + Sync, + app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static, ) -> impl Fn( Request, ) -> Pin> + Send + 'static>> @@ -845,13 +847,17 @@ where } fn handle_response( - additional_context: impl Fn() + 'static + Clone + Send, - app_fn: impl Fn() -> IV + Clone + Send + 'static, + additional_context: impl Fn() + 'static + Clone + Send + Sync, + app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static, stream_builder: fn( IV, BoxedFnOnce>, ) -> PinnedFuture>, -) -> impl Fn(Request) -> PinnedFuture> + Clone + Send + 'static +) -> impl Fn(Request) -> PinnedFuture> + + Clone + + Send + + Sync + + 'static where IV: IntoView + 'static, { @@ -978,7 +984,7 @@ fn provide_contexts( tracing::instrument(level = "trace", fields(error), skip_all) )] pub fn render_app_async( - app_fn: impl Fn() -> IV + Clone + Send + 'static, + app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static, ) -> impl Fn( Request, ) -> Pin> + Send + 'static>> @@ -1032,8 +1038,8 @@ where tracing::instrument(level = "trace", fields(error), skip_all) )] pub fn render_app_async_stream_with_context( - additional_context: impl Fn() + 'static + Clone + Send, - app_fn: impl Fn() -> IV + Clone + Send + 'static, + additional_context: impl Fn() + 'static + Clone + Send + Sync, + app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static, ) -> impl Fn( Request, ) -> Pin> + Send + 'static>> @@ -1099,8 +1105,8 @@ where tracing::instrument(level = "trace", fields(error), skip_all) )] pub fn render_app_async_with_context( - additional_context: impl Fn() + 'static + Clone + Send, - app_fn: impl Fn() -> IV + Clone + Send + 'static, + additional_context: impl Fn() + 'static + Clone + Send + Sync, + app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static, ) -> impl Fn( Request, ) -> Pin> + Send + 'static>> @@ -1639,7 +1645,7 @@ where self, options: &S, paths: Vec, - app_fn: impl Fn() -> IV + Clone + Send + 'static, + app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static, ) -> Self where IV: IntoView + 'static; @@ -1654,8 +1660,8 @@ where self, options: &S, paths: Vec, - additional_context: impl Fn() + 'static + Clone + Send, - app_fn: impl Fn() -> IV + Clone + Send + 'static, + additional_context: impl Fn() + 'static + Clone + Send + Sync, + app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static, ) -> Self where IV: IntoView + 'static; @@ -1725,7 +1731,7 @@ where self, state: &S, paths: Vec, - app_fn: impl Fn() -> IV + Clone + Send + 'static, + app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static, ) -> Self where IV: IntoView + 'static, @@ -1741,8 +1747,8 @@ where self, state: &S, paths: Vec, - additional_context: impl Fn() + 'static + Clone + Send, - app_fn: impl Fn() -> IV + Clone + Send + 'static, + additional_context: impl Fn() + 'static + Clone + Send + Sync, + app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static, ) -> Self where IV: IntoView + 'static, diff --git a/server_fn/Cargo.toml b/server_fn/Cargo.toml index 99a205af47..739b1bd5dd 100644 --- a/server_fn/Cargo.toml +++ b/server_fn/Cargo.toml @@ -30,7 +30,7 @@ once_cell = "1.20" actix-web = { version = "4.9", optional = true } # axum -axum = { version = "0.7.9", optional = true, default-features = false, features = [ +axum = { version = "0.8.1", optional = true, default-features = false, features = [ "multipart", ] } tower = { version = "0.5.1", optional = true } @@ -232,4 +232,4 @@ skip_feature_sets = [ ] [lints.rust] -unexpected_cfgs = { level = "warn", check-cfg = ['cfg(leptos_debuginfo)'] } +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(leptos_debuginfo)'] } \ No newline at end of file From 515d3109ed42e8beaa30316ae5cf207ccc15d78b Mon Sep 17 00:00:00 2001 From: Spencer Ferris <3319370+spencewenski@users.noreply.github.com> Date: Fri, 24 Jan 2025 20:11:38 -0800 Subject: [PATCH 11/17] feat: Add more options for generating server fn routes (#3438) * feat: Allow disabling server fn hash and customizing the default prefix Allow configuring the default prefix for server function API routes. This is useful to override the default prefix (`/api`) for all server functions without needing to manually specify via `#[server(prefix = "...")]` on every server function. Also, allow disabling appending the server functions' hashes to the end of their API names. This is useful when an app's client side needs a stable server API. For example, shipping the CSR WASM binary in a Tauri app. Tauri app releases are dependent on each platform's distribution method (e.g., the Apple App Store or the Google Play Store), which typically are much slower than the frequency at which a website can be updated. In addition, it's common for users to not have the latest app version installed. In these cases, the CSR WASM app would need to be able to continue calling the backend server function API, so the API path needs to be consistent and not have a hash appended. * Mark public structs as `#[non_exhaustive]` and add doc comments * Minor refactor to pull the fn hash logic out of the `path` statement * feat: Use module path in prefix for server fn API route Allow including the module path of the server function in the API route. This is an alternative strategy to prevent duplicate server function API routes (the default strategy is to add a hash to the end of the route). Each element of the module path will be separated by a `/`. For example, a server function with a fully qualified name of `parent::child::server_fn` would have an API route of `/api/parent/child/server_fn` (possibly with a different prefix and a hash suffix depending on the values of the other server fn configs). * Fix `enable_hash` if statement * Add missing import --- Cargo.lock | 7 ++++ leptos_config/src/lib.rs | 40 ++++++++++++++++++++ leptos_macro/src/lib.rs | 2 +- server_fn/Cargo.toml | 1 + server_fn/server_fn_macro_default/src/lib.rs | 2 +- server_fn/src/lib.rs | 2 + server_fn/src/request/actix.rs | 1 + server_fn_macro/src/lib.rs | 33 ++++++++++++++-- 8 files changed, 82 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f50fe0b874..932b6d3d22 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -681,6 +681,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "const-str" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3618cccc083bb987a415d85c02ca6c9994ea5b44731ec28b9ecf09658655fba9" + [[package]] name = "const_format" version = "0.2.34" @@ -3164,6 +3170,7 @@ dependencies = [ "base64", "bytes", "ciborium", + "const-str", "const_format", "dashmap", "futures", diff --git a/leptos_config/src/lib.rs b/leptos_config/src/lib.rs index 026d050dc7..e1a298d1df 100644 --- a/leptos_config/src/lib.rs +++ b/leptos_config/src/lib.rs @@ -14,6 +14,7 @@ use typed_builder::TypedBuilder; /// occur with LeptosOptions #[derive(Clone, Debug, serde::Deserialize, Default)] #[serde(rename_all = "kebab-case")] +#[non_exhaustive] pub struct ConfFile { pub leptos_options: LeptosOptions, } @@ -24,6 +25,7 @@ pub struct ConfFile { /// It shares keys with cargo-leptos, to allow for easy interoperability #[derive(TypedBuilder, Debug, Clone, serde::Deserialize)] #[serde(rename_all = "kebab-case")] +#[non_exhaustive] pub struct LeptosOptions { /// The name of the WASM and JS files generated by wasm-bindgen. Defaults to the crate name with underscores instead of dashes #[builder(setter(into), default=default_output_name())] @@ -78,6 +80,40 @@ pub struct LeptosOptions { #[builder(default = default_hash_files())] #[serde(default = "default_hash_files")] pub hash_files: bool, + /// The default prefix to use for server functions when generating API routes. Can be + /// overridden for individual functions using `#[server(prefix = "...")]` as usual. + /// + /// This is useful to override the default prefix (`/api`) for all server functions without + /// needing to manually specify via `#[server(prefix = "...")]` on every server function. + #[builder(default, setter(strip_option))] + #[serde(default)] + pub server_fn_prefix: Option, + /// Whether to disable appending the server functions' hashes to the end of their API names. + /// + /// This is useful when an app's client side needs a stable server API. For example, shipping + /// the CSR WASM binary in a Tauri app. Tauri app releases are dependent on each platform's + /// distribution method (e.g., the Apple App Store or the Google Play Store), which typically + /// are much slower than the frequency at which a website can be updated. In addition, it's + /// common for users to not have the latest app version installed. In these cases, the CSR WASM + /// app would need to be able to continue calling the backend server function API, so the API + /// path needs to be consistent and not have a hash appended. + /// + /// Note that the hash suffixes is intended as a way to ensure duplicate API routes are created. + /// Without the hash, server functions will need to have unique names to avoid creating + /// duplicate routes. Axum will throw an error if a duplicate route is added to the router, but + /// Actix will not. + #[builder(default)] + #[serde(default)] + pub disable_server_fn_hash: bool, + /// Include the module path of the server function in the API route. This is an alternative + /// strategy to prevent duplicate server function API routes (the default strategy is to add + /// a hash to the end of the route). Each element of the module path will be separated by a `/`. + /// For example, a server function with a fully qualified name of `parent::child::server_fn` + /// would have an API route of `/api/parent/child/server_fn` (possibly with a + /// different prefix and a hash suffix depending on the values of the other server fn configs). + #[builder(default)] + #[serde(default)] + pub server_fn_mod_path: bool, } impl LeptosOptions { @@ -120,6 +156,10 @@ impl LeptosOptions { hash_file: env_w_default("LEPTOS_HASH_FILE_NAME", "hash.txt")? .into(), hash_files: env_w_default("LEPTOS_HASH_FILES", "false")?.parse()?, + server_fn_prefix: env_wo_default("SERVER_FN_PREFIX")?, + disable_server_fn_hash: env_wo_default("DISABLE_SERVER_FN_HASH")? + .is_some(), + server_fn_mod_path: env_wo_default("SERVER_FN_MOD_PATH")?.is_some(), }) } } diff --git a/leptos_macro/src/lib.rs b/leptos_macro/src/lib.rs index dfd8851507..abce548c16 100644 --- a/leptos_macro/src/lib.rs +++ b/leptos_macro/src/lib.rs @@ -923,7 +923,7 @@ pub fn server(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream { args.into(), s.into(), Some(syn::parse_quote!(::leptos::server_fn)), - "/api", + option_env!("SERVER_FN_PREFIX").unwrap_or("/api"), None, None, ) { diff --git a/server_fn/Cargo.toml b/server_fn/Cargo.toml index 739b1bd5dd..dfb8b2a688 100644 --- a/server_fn/Cargo.toml +++ b/server_fn/Cargo.toml @@ -14,6 +14,7 @@ throw_error = { workspace = true } server_fn_macro_default = { workspace = true } # used for hashing paths in #[server] macro const_format = "0.2.33" +const-str = "0.5.7" xxhash-rust = { version = "0.8.12", features = ["const_xxh64"] } # used across multiple features serde = { version = "1.0", features = ["derive"] } diff --git a/server_fn/server_fn_macro_default/src/lib.rs b/server_fn/server_fn_macro_default/src/lib.rs index 2d15987057..6d48d9c92c 100644 --- a/server_fn/server_fn_macro_default/src/lib.rs +++ b/server_fn/server_fn_macro_default/src/lib.rs @@ -74,7 +74,7 @@ pub fn server(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream { args.into(), s.into(), Some(syn::parse_quote!(server_fns)), - "/api", + option_env!("SERVER_FN_PREFIX").unwrap_or("/api"), None, None, ) { diff --git a/server_fn/src/lib.rs b/server_fn/src/lib.rs index 1c3132ee18..48384309fe 100644 --- a/server_fn/src/lib.rs +++ b/server_fn/src/lib.rs @@ -130,6 +130,8 @@ use client::Client; use codec::{Encoding, FromReq, FromRes, IntoReq, IntoRes}; #[doc(hidden)] pub use const_format; +#[doc(hidden)] +pub use const_str; use dashmap::DashMap; pub use error::ServerFnError; #[cfg(feature = "form-redirects")] diff --git a/server_fn/src/request/actix.rs b/server_fn/src/request/actix.rs index 3bc63bd12f..6270330b22 100644 --- a/server_fn/src/request/actix.rs +++ b/server_fn/src/request/actix.rs @@ -1,6 +1,7 @@ use crate::{ error::{FromServerFnError, IntoAppError, ServerFnErrorErr}, request::Req, + ServerFnError, }; use actix_web::{web::Payload, HttpRequest}; use bytes::Bytes; diff --git a/server_fn_macro/src/lib.rs b/server_fn_macro/src/lib.rs index c955418b68..a5b478d236 100644 --- a/server_fn_macro/src/lib.rs +++ b/server_fn_macro/src/lib.rs @@ -230,6 +230,7 @@ pub fn server_macro_impl( None => Some("PostUrl".to_string()), _ => None, }; + let input = input .map(|n| { if builtin_encoding { @@ -604,20 +605,44 @@ pub fn server_macro_impl( } else { quote! { concat!("/", #fn_path) } }; + + let enable_server_fn_mod_path = option_env!("SERVER_FN_MOD_PATH").is_some(); + let mod_path = if enable_server_fn_mod_path { + quote! { + #server_fn_path::const_format::concatcp!( + #server_fn_path::const_str::replace!(module_path!(), "::", "/"), + "/" + ) + } + } else { + quote! { "" } + }; + + let enable_hash = option_env!("DISABLE_SERVER_FN_HASH").is_none(); + let hash = if enable_hash { + quote! { + #server_fn_path::xxhash_rust::const_xxh64::xxh64( + concat!(env!(#key_env_var), ":", file!(), ":", line!(), ":", column!()).as_bytes(), + 0 + ) + } + } else { + quote! { "" } + }; + let path = quote! { if #fn_path.is_empty() { #server_fn_path::const_format::concatcp!( #prefix, "/", + #mod_path, #fn_name_as_str, - #server_fn_path::xxhash_rust::const_xxh64::xxh64( - concat!(env!(#key_env_var), ":", file!(), ":", line!(), ":", column!()).as_bytes(), - 0 - ) + #hash ) } else { #server_fn_path::const_format::concatcp!( #prefix, + #mod_path, #fn_path ) } From 10168a9691b5b835a38d1f83ea7c4360a87c30d4 Mon Sep 17 00:00:00 2001 From: dcsturman Date: Fri, 24 Jan 2025 20:23:13 -0800 Subject: [PATCH 12/17] Enhanced docs for reactive_stores (#3508) Added docs on shadow traits, Option, Enum, Vec, and Box usage with Store. --- reactive_stores/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reactive_stores/src/lib.rs b/reactive_stores/src/lib.rs index 90d9f7dfce..3a1c759c33 100644 --- a/reactive_stores/src/lib.rs +++ b/reactive_stores/src/lib.rs @@ -98,7 +98,7 @@ //! # fn main() { //! # } //! ``` -//! +//! //! ### Additional field types //! //! Most of the time, your structs will have fields as in the example above: the struct is comprised From 5de63eb61e7e938d4b23b4aff5a089dce0bc2f79 Mon Sep 17 00:00:00 2001 From: Danik Vitek Date: Sat, 25 Jan 2025 07:15:29 +0200 Subject: [PATCH 13/17] feat (`either_of`): Extent API; Implement other iterator methods; Update deps (#3478) * Implement other iterator methods. Update deps * Formatting * Update Cargo.lock * [autofix.ci] apply automated fixes * Formatting * Move `Either` declaration into the `tuples` macro * Comment out non-MSRV-compliant methods * [autofix.ci] apply automated fixes * Formatting * Implement mapping functions * Fix clippy warnings * Impl `Error`; Impl `From> for Either` * Fix `Error` impl * Move `Error` impl under `#[cfg(not(feature="no_std"))] until MSRV >= 1.81 * [autofix.ci] apply automated fixes * Make `From` compliant with `EitherOr`. Add `impl EitherOr for Either` * fix: use fully-qualified name * fix: `EitherOf` test --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- Cargo.lock | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 932b6d3d22..cf53968a8a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -887,7 +887,11 @@ checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" [[package]] name = "either_of" +<<<<<<< HEAD version = "0.1.5" +======= +version = "0.1.4" +>>>>>>> c64d2059 (feat (`either_of`): Extent API; Implement other iterator methods; Update deps (#3478)) dependencies = [ "paste", "pin-project-lite", From e97fb38e925c56239fb09d634c450064a73b0fde Mon Sep 17 00:00:00 2001 From: benwis Date: Sun, 26 Jan 2025 09:46:19 -0800 Subject: [PATCH 14/17] Fix formatting --- reactive_stores/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reactive_stores/src/lib.rs b/reactive_stores/src/lib.rs index 3a1c759c33..90d9f7dfce 100644 --- a/reactive_stores/src/lib.rs +++ b/reactive_stores/src/lib.rs @@ -98,7 +98,7 @@ //! # fn main() { //! # } //! ``` -//! +//! //! ### Additional field types //! //! Most of the time, your structs will have fields as in the example above: the struct is comprised From 8e7c5c96ade2ad026a0bcdfbffa2eaf0162ad32e Mon Sep 17 00:00:00 2001 From: Greg Johnston Date: Thu, 30 Jan 2025 21:29:54 -0500 Subject: [PATCH 15/17] chore: update version numbers preparing for 0.8.0-alpha --- Cargo.lock | 72 +++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 30 ++++++++--------- leptos_macro/Cargo.toml | 2 +- meta/Cargo.toml | 4 +++ router/Cargo.toml | 4 +++ router_macro/Cargo.toml | 4 +++ 6 files changed, 100 insertions(+), 16 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cf53968a8a..58430d5dd2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -333,6 +333,9 @@ checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" [[package]] <<<<<<< HEAD +<<<<<<< HEAD +======= +>>>>>>> 0073ae7d (chore: update version numbers preparing for 0.8.0-alpha) name = "async-trait" version = "0.1.85" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -344,8 +347,11 @@ dependencies = [ ] [[package]] +<<<<<<< HEAD ======= >>>>>>> 35e6f179 (chore: upgrade `axum` to `v0.8` (#3439)) +======= +>>>>>>> 0073ae7d (chore: update version numbers preparing for 0.8.0-alpha) name = "atomic-polyfill" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1730,7 +1736,11 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "leptos" +<<<<<<< HEAD version = "0.7.5" +======= +version = "0.8.0-alpha" +>>>>>>> 0073ae7d (chore: update version numbers preparing for 0.8.0-alpha) dependencies = [ "any_spawner", "base64", @@ -1781,7 +1791,11 @@ dependencies = [ [[package]] name = "leptos_actix" +<<<<<<< HEAD version = "0.7.5" +======= +version = "0.8.0-alpha" +>>>>>>> 0073ae7d (chore: update version numbers preparing for 0.8.0-alpha) dependencies = [ "actix-files", "actix-http", @@ -1806,7 +1820,11 @@ dependencies = [ [[package]] name = "leptos_axum" +<<<<<<< HEAD version = "0.7.5" +======= +version = "0.8.0-alpha" +>>>>>>> 0073ae7d (chore: update version numbers preparing for 0.8.0-alpha) dependencies = [ "any_spawner", "axum", @@ -1829,7 +1847,11 @@ dependencies = [ [[package]] name = "leptos_config" +<<<<<<< HEAD version = "0.7.5" +======= +version = "0.8.0-alpha" +>>>>>>> 0073ae7d (chore: update version numbers preparing for 0.8.0-alpha) dependencies = [ "config", "regex", @@ -1843,7 +1865,11 @@ dependencies = [ [[package]] name = "leptos_dom" +<<<<<<< HEAD version = "0.7.5" +======= +version = "0.8.0-alpha" +>>>>>>> 0073ae7d (chore: update version numbers preparing for 0.8.0-alpha) dependencies = [ "js-sys", "leptos", @@ -1860,7 +1886,11 @@ dependencies = [ [[package]] name = "leptos_hot_reload" +<<<<<<< HEAD version = "0.7.5" +======= +version = "0.8.0-alpha" +>>>>>>> 0073ae7d (chore: update version numbers preparing for 0.8.0-alpha) dependencies = [ "anyhow", "camino", @@ -1876,7 +1906,11 @@ dependencies = [ [[package]] name = "leptos_integration_utils" +<<<<<<< HEAD version = "0.7.5" +======= +version = "0.8.0-alpha" +>>>>>>> 0073ae7d (chore: update version numbers preparing for 0.8.0-alpha) dependencies = [ "futures", "hydration_context", @@ -1889,7 +1923,11 @@ dependencies = [ [[package]] name = "leptos_macro" +<<<<<<< HEAD version = "0.7.5" +======= +version = "0.8.0-alpha" +>>>>>>> 0073ae7d (chore: update version numbers preparing for 0.8.0-alpha) dependencies = [ "attribute-derive", "cfg-if", @@ -1908,7 +1946,11 @@ dependencies = [ "rstml", "serde", "server_fn", +<<<<<<< HEAD "server_fn_macro 0.7.5", +======= + "server_fn_macro 0.8.0-alpha", +>>>>>>> 0073ae7d (chore: update version numbers preparing for 0.8.0-alpha) "syn 2.0.90", "tracing", "trybuild", @@ -1918,7 +1960,11 @@ dependencies = [ [[package]] name = "leptos_meta" +<<<<<<< HEAD version = "0.7.5" +======= +version = "0.8.0-alpha" +>>>>>>> 0073ae7d (chore: update version numbers preparing for 0.8.0-alpha) dependencies = [ "futures", "indexmap", @@ -1933,7 +1979,11 @@ dependencies = [ [[package]] name = "leptos_router" +<<<<<<< HEAD version = "0.7.5" +======= +version = "0.8.0-alpha" +>>>>>>> 0073ae7d (chore: update version numbers preparing for 0.8.0-alpha) dependencies = [ "any_spawner", "either_of", @@ -1957,7 +2007,11 @@ dependencies = [ [[package]] name = "leptos_router_macro" +<<<<<<< HEAD version = "0.7.5" +======= +version = "0.8.0-alpha" +>>>>>>> 0073ae7d (chore: update version numbers preparing for 0.8.0-alpha) dependencies = [ "leptos_macro", "leptos_router", @@ -1969,7 +2023,11 @@ dependencies = [ [[package]] name = "leptos_server" +<<<<<<< HEAD version = "0.7.5" +======= +version = "0.8.0-alpha" +>>>>>>> 0073ae7d (chore: update version numbers preparing for 0.8.0-alpha) dependencies = [ "any_spawner", "base64", @@ -3167,7 +3225,11 @@ dependencies = [ [[package]] name = "server_fn" +<<<<<<< HEAD version = "0.7.5" +======= +version = "0.8.0-alpha" +>>>>>>> 0073ae7d (chore: update version numbers preparing for 0.8.0-alpha) dependencies = [ "actix-web", "axum", @@ -3225,7 +3287,11 @@ dependencies = [ [[package]] name = "server_fn_macro" +<<<<<<< HEAD version = "0.7.5" +======= +version = "0.8.0-alpha" +>>>>>>> 0073ae7d (chore: update version numbers preparing for 0.8.0-alpha) dependencies = [ "const_format", "convert_case 0.6.0", @@ -3237,9 +3303,15 @@ dependencies = [ [[package]] name = "server_fn_macro_default" +<<<<<<< HEAD version = "0.7.5" dependencies = [ "server_fn_macro 0.7.5", +======= +version = "0.8.0-alpha" +dependencies = [ + "server_fn_macro 0.8.0-alpha", +>>>>>>> 0073ae7d (chore: update version numbers preparing for 0.8.0-alpha) "syn 2.0.90", ] diff --git a/Cargo.toml b/Cargo.toml index 51ce42b271..b90e5bb120 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,7 +40,7 @@ members = [ exclude = ["benchmarks", "examples", "projects"] [workspace.package] -version = "0.7.5" +version = "0.8.0-alpha" edition = "2021" rust-version = "1.76" @@ -50,26 +50,26 @@ any_spawner = { path = "./any_spawner/", version = "0.2.0" } const_str_slice_concat = { path = "./const_str_slice_concat", version = "0.1" } either_of = { path = "./either_of/", version = "0.1.0" } hydration_context = { path = "./hydration_context", version = "0.2.0" } -leptos = { path = "./leptos", version = "0.7.5" } -leptos_config = { path = "./leptos_config", version = "0.7.5" } -leptos_dom = { path = "./leptos_dom", version = "0.7.5" } -leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.7.5" } -leptos_integration_utils = { path = "./integrations/utils", version = "0.7.5" } -leptos_macro = { path = "./leptos_macro", version = "0.7.5" } -leptos_router = { path = "./router", version = "0.7.5" } -leptos_router_macro = { path = "./router_macro", version = "0.7.5" } -leptos_server = { path = "./leptos_server", version = "0.7.5" } -leptos_meta = { path = "./meta", version = "0.7.5" } +leptos = { path = "./leptos", version = "0.8.0-alpha" } +leptos_config = { path = "./leptos_config", version = "0.8.0-alpha" } +leptos_dom = { path = "./leptos_dom", version = "0.8.0-alpha" } +leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.8.0-alpha" } +leptos_integration_utils = { path = "./integrations/utils", version = "0.8.0-alpha" } +leptos_macro = { path = "./leptos_macro", version = "0.8.0-alpha" } +leptos_router = { path = "./router", version = "0.8.0-alpha" } +leptos_router_macro = { path = "./router_macro", version = "0.8.0-alpha" } +leptos_server = { path = "./leptos_server", version = "0.8.0-alpha" } +leptos_meta = { path = "./meta", version = "0.8.0-alpha" } next_tuple = { path = "./next_tuple", version = "0.1.0" } oco_ref = { path = "./oco", version = "0.2.0" } or_poisoned = { path = "./or_poisoned", version = "0.1.0" } reactive_graph = { path = "./reactive_graph", version = "0.1.5" } reactive_stores = { path = "./reactive_stores", version = "0.1.3" } reactive_stores_macro = { path = "./reactive_stores_macro", version = "0.1.0" } -server_fn = { path = "./server_fn", version = "0.7.5" } -server_fn_macro = { path = "./server_fn_macro", version = "0.7.5" } -server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.7.5" } -tachys = { path = "./tachys", version = "0.1.5" } +server_fn = { path = "./server_fn", version = "0.8.0-alpha" } +server_fn_macro = { path = "./server_fn_macro", version = "0.8.0-alpha" } +server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.8.0-alpha" } +tachys = { path = "./tachys", version = "0.1.4" } [profile.release] codegen-units = 1 diff --git a/leptos_macro/Cargo.toml b/leptos_macro/Cargo.toml index e3f6a43872..7799d515d1 100644 --- a/leptos_macro/Cargo.toml +++ b/leptos_macro/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "leptos_macro" -version = "0.7.5" +version = "0.8.0-alpha" authors = ["Greg Johnston"] license = "MIT" repository = "https://github.com/leptos-rs/leptos" diff --git a/meta/Cargo.toml b/meta/Cargo.toml index 697f02ad49..710af95d80 100644 --- a/meta/Cargo.toml +++ b/meta/Cargo.toml @@ -1,6 +1,10 @@ [package] name = "leptos_meta" +<<<<<<< HEAD version = "0.7.5" +======= +version = "0.8.0-alpha" +>>>>>>> 0073ae7d (chore: update version numbers preparing for 0.8.0-alpha) authors = ["Greg Johnston"] license = "MIT" repository = "https://github.com/leptos-rs/leptos" diff --git a/router/Cargo.toml b/router/Cargo.toml index 04e8465521..cb9e3fdde3 100644 --- a/router/Cargo.toml +++ b/router/Cargo.toml @@ -1,6 +1,10 @@ [package] name = "leptos_router" +<<<<<<< HEAD version = "0.7.5" +======= +version = "0.8.0-alpha" +>>>>>>> 0073ae7d (chore: update version numbers preparing for 0.8.0-alpha) authors = ["Greg Johnston", "Ben Wishovich"] license = "MIT" readme = "../README.md" diff --git a/router_macro/Cargo.toml b/router_macro/Cargo.toml index 765324fb24..782a77221f 100644 --- a/router_macro/Cargo.toml +++ b/router_macro/Cargo.toml @@ -1,6 +1,10 @@ [package] name = "leptos_router_macro" +<<<<<<< HEAD version = "0.7.5" +======= +version = "0.8.0-alpha" +>>>>>>> 0073ae7d (chore: update version numbers preparing for 0.8.0-alpha) authors = ["Greg Johnston", "Ben Wishovich"] license = "MIT" readme = "../README.md" From 157e4c5f3156bceb0e55c36a25e9c52c47559469 Mon Sep 17 00:00:00 2001 From: Greg Johnston Date: Fri, 31 Jan 2025 14:25:35 -0500 Subject: [PATCH 16/17] change: allow `IntoFuture` for `Suspend::new()` (closes #3509) (#3532) --- tachys/src/reactive_graph/suspense.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/tachys/src/reactive_graph/suspense.rs b/tachys/src/reactive_graph/suspense.rs index f918cdbd34..43dc5e538b 100644 --- a/tachys/src/reactive_graph/suspense.rs +++ b/tachys/src/reactive_graph/suspense.rs @@ -27,7 +27,7 @@ use reactive_graph::{ use std::{ cell::RefCell, fmt::Debug, - future::Future, + future::{Future, IntoFuture}, mem, pin::Pin, rc::Rc, @@ -115,11 +115,15 @@ impl ToAnySubscriber for SuspendSubscriber { impl Suspend { /// Creates a new suspended view. - pub fn new(fut: impl Future + Send + 'static) -> Self { + pub fn new(fut: Fut) -> Self + where + Fut: IntoFuture, + Fut::IntoFuture: Send + 'static, + { let subscriber = SuspendSubscriber::new(); let any_subscriber = subscriber.to_any_subscriber(); - let inner = - any_subscriber.with_observer(|| Box::pin(ScopedFuture::new(fut))); + let inner = any_subscriber + .with_observer(|| Box::pin(ScopedFuture::new(fut.into_future()))); Self { subscriber, inner } } } From 4691d84489e7da28b0052ae76f1bbf7dbb0a13e8 Mon Sep 17 00:00:00 2001 From: Chris <89366859+chrisp60@users.noreply.github.com> Date: Sat, 1 Feb 2025 09:41:44 -0500 Subject: [PATCH 17/17] fix: remove `Default` impl for `LeptosOptions` and `ConfFile` (#3522) * fix: remove `Default` impl for `LeptosOptions` and `ConfFile` * [autofix.ci] apply automated fixes --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- leptos_config/src/lib.rs | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/leptos_config/src/lib.rs b/leptos_config/src/lib.rs index e1a298d1df..d711f11ff6 100644 --- a/leptos_config/src/lib.rs +++ b/leptos_config/src/lib.rs @@ -12,7 +12,7 @@ use typed_builder::TypedBuilder; /// A Struct to allow us to parse LeptosOptions from the file. Not really needed, most interactions should /// occur with LeptosOptions -#[derive(Clone, Debug, serde::Deserialize, Default)] +#[derive(Clone, Debug, serde::Deserialize)] #[serde(rename_all = "kebab-case")] #[non_exhaustive] pub struct ConfFile { @@ -27,8 +27,12 @@ pub struct ConfFile { #[serde(rename_all = "kebab-case")] #[non_exhaustive] pub struct LeptosOptions { - /// The name of the WASM and JS files generated by wasm-bindgen. Defaults to the crate name with underscores instead of dashes - #[builder(setter(into), default=default_output_name())] + /// The name of the WASM and JS files generated by wasm-bindgen. + /// + /// This should match the name that will be output when building your application. + /// + /// You can easily set this using `env!("CARGO_CRATE_NAME")`. + #[builder(setter(into))] pub output_name: Arc, /// The path of the all the files generated by cargo-leptos. This defaults to '.' for convenience when integrating with other /// tools. @@ -164,16 +168,6 @@ impl LeptosOptions { } } -impl Default for LeptosOptions { - fn default() -> Self { - LeptosOptions::builder().build() - } -} - -fn default_output_name() -> Arc { - env!("CARGO_CRATE_NAME").replace('-', "_").into() -} - fn default_site_root() -> Arc { ".".into() }