From cadd2170787a50983d9034764132f65080f69a42 Mon Sep 17 00:00:00 2001 From: Daniel Santana Date: Sun, 31 Dec 2023 00:05:17 +0000 Subject: [PATCH 001/100] Update integration with support for axum 0.7 (#2082) * chore: update to axum 0.7 Removed http, since it's included in axum, and replaced hyper by http-body-util, which is a smaller. * chore: update samples to work with nre axum Missing sessions_axum_auth, pending PR merge. * chore: all dependencies update to axum 0.7 * chore: cargo fmt * chore: fix doctests * chore: Fix example that in reality doesn't use axum. Fixed anyway. * chore: more examples support for axum 0.7 * Small tweak --- examples/errors_axum/Cargo.toml | 22 +-- examples/errors_axum/src/fallback.rs | 6 +- examples/errors_axum/src/main.rs | 4 +- examples/hackernews_axum/Cargo.toml | 26 +-- examples/hackernews_axum/src/fallback.rs | 6 +- examples/hackernews_axum/src/handlers.rs | 13 +- examples/hackernews_axum/src/main.rs | 4 +- examples/hackernews_islands_axum/Cargo.toml | 26 +-- .../hackernews_islands_axum/src/fallback.rs | 6 +- .../hackernews_islands_axum/src/handlers.rs | 12 +- examples/hackernews_islands_axum/src/main.rs | 4 +- examples/hackernews_js_fetch/Cargo.toml | 26 +-- examples/hackernews_js_fetch/src/fallback.rs | 4 +- examples/hackernews_js_fetch/src/lib.rs | 2 +- examples/session_auth_axum/Cargo.toml | 39 ++--- examples/session_auth_axum/src/fallback.rs | 6 +- examples/session_auth_axum/src/main.rs | 4 +- examples/ssr_modes_axum/Cargo.toml | 6 +- examples/ssr_modes_axum/src/fallback.rs | 6 +- examples/ssr_modes_axum/src/main.rs | 4 +- examples/tailwind_axum/Cargo.toml | 18 +-- examples/tailwind_axum/src/fallback.rs | 6 +- examples/tailwind_axum/src/main.rs | 4 +- examples/todo_app_sqlite_axum/Cargo.toml | 26 +-- examples/todo_app_sqlite_axum/Todos.db | Bin 16384 -> 16384 bytes examples/todo_app_sqlite_axum/src/fallback.rs | 6 +- examples/todo_app_sqlite_axum/src/main.rs | 8 +- examples/todo_app_sqlite_csr/Cargo.toml | 24 +-- examples/todo_app_sqlite_csr/src/fallback.rs | 6 +- examples/todo_app_sqlite_csr/src/main.rs | 6 +- integrations/axum/Cargo.toml | 19 ++- integrations/axum/src/lib.rs | 149 +++++++----------- 32 files changed, 233 insertions(+), 265 deletions(-) diff --git a/examples/errors_axum/Cargo.toml b/examples/errors_axum/Cargo.toml index 28f48dae93..30748b1709 100644 --- a/examples/errors_axum/Cargo.toml +++ b/examples/errors_axum/Cargo.toml @@ -7,22 +7,22 @@ edition = "2021" crate-type = ["cdylib", "rlib"] [dependencies] -console_log = "1.0.0" -console_error_panic_hook = "0.1.7" -cfg-if = "1.0.0" +console_log = "1.0" +console_error_panic_hook = "0.1" +cfg-if = "1.0" leptos = { path = "../../leptos", features = ["nightly"] } leptos_axum = { path = "../../integrations/axum", optional = true } leptos_meta = { path = "../../meta" } leptos_router = { path = "../../router" } -log = "0.4.17" +log = "0.4" serde = { version = "1", features = ["derive"] } -simple_logger = "4.0.0" -axum = { version = "0.6.1", optional = true } -tower = { version = "0.4.13", optional = true } -tower-http = { version = "0.4", features = ["fs"], optional = true } -tokio = { version = "1.22.0", features = ["full"], optional = true } -http = { version = "0.2.8" } -thiserror = "1.0.38" +simple_logger = "4.0" +axum = { version = "0.7", optional = true } +tower = { version = "0.4", optional = true } +tower-http = { version = "0.5", features = ["fs"], optional = true } +tokio = { version = "1", features = ["full"], optional = true } +http = { version = "1.0" } +thiserror = "1.0" wasm-bindgen = "0.2" [features] diff --git a/examples/errors_axum/src/fallback.rs b/examples/errors_axum/src/fallback.rs index 176336beb1..ba7ae7a58f 100644 --- a/examples/errors_axum/src/fallback.rs +++ b/examples/errors_axum/src/fallback.rs @@ -2,7 +2,7 @@ use cfg_if::cfg_if; cfg_if! { if #[cfg(feature = "ssr")] { use axum::{ - body::{boxed, Body, BoxBody}, + body::{Body}, extract::State, response::IntoResponse, http::{Request, Response, StatusCode, Uri}, @@ -28,12 +28,12 @@ cfg_if! { if #[cfg(feature = "ssr")] { } } - async fn get_static_file(uri: Uri, root: &str) -> Result, (StatusCode, String)> { + async fn get_static_file(uri: Uri, root: &str) -> Result, (StatusCode, String)> { let req = Request::builder().uri(uri.clone()).body(Body::empty()).unwrap(); // `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot` // This path is relative to the cargo root match ServeDir::new(root).oneshot(req).await { - Ok(res) => Ok(res.map(boxed)), + Ok(res) => Ok(res.into_response()), Err(err) => Err(( StatusCode::INTERNAL_SERVER_ERROR, format!("Something went wrong: {err}"), diff --git a/examples/errors_axum/src/main.rs b/examples/errors_axum/src/main.rs index 4783ab6ff5..8179e5fe36 100644 --- a/examples/errors_axum/src/main.rs +++ b/examples/errors_axum/src/main.rs @@ -61,8 +61,8 @@ async fn main() { // run our app with hyper // `axum::Server` is a re-export of `hyper::Server` log!("listening on http://{}", &addr); - axum::Server::bind(&addr) - .serve(app.into_make_service()) + let listener = tokio::net::TcpListener::bind(&addr).await.unwrap(); + axum::serve(listener, app.into_make_service()) .await .unwrap(); } diff --git a/examples/hackernews_axum/Cargo.toml b/examples/hackernews_axum/Cargo.toml index 69ad0ce9a5..41b5a01bcb 100644 --- a/examples/hackernews_axum/Cargo.toml +++ b/examples/hackernews_axum/Cargo.toml @@ -11,24 +11,24 @@ codegen-units = 1 lto = true [dependencies] -console_log = "1.0.0" -console_error_panic_hook = "0.1.7" -cfg-if = "1.0.0" +console_log = "1.0" +console_error_panic_hook = "0.1" +cfg-if = "1.0" leptos = { path = "../../leptos", features = ["nightly"] } leptos_axum = { path = "../../integrations/axum", optional = true } leptos_meta = { path = "../../meta", features = ["nightly"] } leptos_router = { path = "../../router", features = ["nightly"] } -log = "0.4.17" -simple_logger = "4.0.0" -serde = { version = "1.0.148", features = ["derive"] } +log = "0.4" +simple_logger = "4.0" +serde = { version = "1.0", features = ["derive"] } tracing = "0.1" -gloo-net = { version = "0.2.5", features = ["http"] } -reqwest = { version = "0.11.13", features = ["json"] } -axum = { version = "0.6.1", optional = true } -tower = { version = "0.4.13", optional = true } -tower-http = { version = "0.4", features = ["fs"], optional = true } -tokio = { version = "1.22.0", features = ["full"], optional = true } -http = { version = "0.2.11", optional = true } +gloo-net = { version = "0.4", features = ["http"] } +reqwest = { version = "0.11", features = ["json"] } +axum = { version = "0.7", optional = true } +tower = { version = "0.4", optional = true } +tower-http = { version = "0.5", features = ["fs"], optional = true } +tokio = { version = "1", features = ["full"], optional = true } +http = { version = "1.0", optional = true } web-sys = { version = "0.3", features = ["AbortController", "AbortSignal"] } wasm-bindgen = "0.2" diff --git a/examples/hackernews_axum/src/fallback.rs b/examples/hackernews_axum/src/fallback.rs index 69ec7586c2..dbdcd85ed9 100644 --- a/examples/hackernews_axum/src/fallback.rs +++ b/examples/hackernews_axum/src/fallback.rs @@ -3,7 +3,7 @@ use cfg_if::cfg_if; cfg_if! { if #[cfg(feature = "ssr")] { use axum::{ - body::{boxed, Body, BoxBody}, + body::Body, extract::State, response::IntoResponse, http::{Request, Response, StatusCode, Uri}, @@ -26,12 +26,12 @@ if #[cfg(feature = "ssr")] { } } - async fn get_static_file(uri: Uri, root: &str) -> Result, (StatusCode, String)> { + async fn get_static_file(uri: Uri, root: &str) -> Result, (StatusCode, String)> { let req = Request::builder().uri(uri.clone()).body(Body::empty()).unwrap(); // `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot` // This path is relative to the cargo root match ServeDir::new(root).oneshot(req).await { - Ok(res) => Ok(res.map(boxed)), + Ok(res) => Ok(res.into_response()), Err(err) => Err(( StatusCode::INTERNAL_SERVER_ERROR, format!("Something went wrong: {}", err), diff --git a/examples/hackernews_axum/src/handlers.rs b/examples/hackernews_axum/src/handlers.rs index 4ed9ff7b28..4554b1f191 100644 --- a/examples/hackernews_axum/src/handlers.rs +++ b/examples/hackernews_axum/src/handlers.rs @@ -3,13 +3,14 @@ use cfg_if::cfg_if; cfg_if! { if #[cfg(feature = "ssr")] { use axum::{ - body::{boxed, Body, BoxBody}, + body::Body, http::{Request, Response, StatusCode, Uri}, + response::IntoResponse, }; use tower::ServiceExt; use tower_http::services::ServeDir; - pub async fn file_handler(uri: Uri) -> Result, (StatusCode, String)> { + pub async fn file_handler(uri: Uri) -> Result, (StatusCode, String)> { let res = get_static_file(uri.clone(), "/pkg").await?; if res.status() == StatusCode::NOT_FOUND { @@ -24,7 +25,7 @@ if #[cfg(feature = "ssr")] { } } - pub async fn get_static_file_handler(uri: Uri) -> Result, (StatusCode, String)> { + pub async fn get_static_file_handler(uri: Uri) -> Result, (StatusCode, String)> { let res = get_static_file(uri.clone(), "/static").await?; if res.status() == StatusCode::NOT_FOUND { @@ -34,14 +35,14 @@ if #[cfg(feature = "ssr")] { } } - async fn get_static_file(uri: Uri, base: &str) -> Result, (StatusCode, String)> { + async fn get_static_file(uri: Uri, base: &str) -> Result, (StatusCode, String)> { let req = Request::builder().uri(&uri).body(Body::empty()).unwrap(); // `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot` // When run normally, the root should be the crate root if base == "/static" { match ServeDir::new("./static").oneshot(req).await { - Ok(res) => Ok(res.map(boxed)), + Ok(res) => Ok(res.into_response()), Err(err) => Err(( StatusCode::INTERNAL_SERVER_ERROR, format!("Something went wrong: {}", err), @@ -49,7 +50,7 @@ if #[cfg(feature = "ssr")] { } } else if base == "/pkg" { match ServeDir::new("./pkg").oneshot(req).await { - Ok(res) => Ok(res.map(boxed)), + Ok(res) => Ok(res.into_response()), Err(err) => Err(( StatusCode::INTERNAL_SERVER_ERROR, format!("Something went wrong: {}", err), diff --git a/examples/hackernews_axum/src/main.rs b/examples/hackernews_axum/src/main.rs index 786fad53f4..da4db51648 100644 --- a/examples/hackernews_axum/src/main.rs +++ b/examples/hackernews_axum/src/main.rs @@ -32,8 +32,8 @@ if #[cfg(feature = "ssr")] { // run our app with hyper // `axum::Server` is a re-export of `hyper::Server` log!("listening on {}", addr); - axum::Server::bind(&addr) - .serve(app.into_make_service()) + let listener = tokio::net::TcpListener::bind(&addr).await.unwrap(); + axum::serve(listener, app.into_make_service()) .await .unwrap(); } diff --git a/examples/hackernews_islands_axum/Cargo.toml b/examples/hackernews_islands_axum/Cargo.toml index 51d14613f4..aea1fae068 100644 --- a/examples/hackernews_islands_axum/Cargo.toml +++ b/examples/hackernews_islands_axum/Cargo.toml @@ -11,9 +11,9 @@ codegen-units = 1 lto = true [dependencies] -console_log = "1.0.0" -console_error_panic_hook = "0.1.7" -cfg-if = "1.0.0" +console_log = "1.0" +console_error_panic_hook = "0.1" +cfg-if = "1.0" leptos = { path = "../../leptos", features = [ "nightly", "experimental-islands", @@ -23,20 +23,20 @@ leptos_axum = { path = "../../integrations/axum", optional = true, features = [ ] } leptos_meta = { path = "../../meta", features = ["nightly"] } leptos_router = { path = "../../router", features = ["nightly"] } -log = "0.4.17" -simple_logger = "4.0.0" -serde = { version = "1.0.148", features = ["derive"] } +log = "0.4" +simple_logger = "4.0" +serde = { version = "1.0", features = ["derive"] } tracing = "0.1" -gloo-net = { version = "0.2.5", features = ["http"] } -reqwest = { version = "0.11.13", features = ["json"] } -axum = { version = "0.6.1", optional = true, features = ["http2"] } -tower = { version = "0.4.13", optional = true } -tower-http = { version = "0.4", features = [ +gloo-net = { version = "0.4", features = ["http"] } +reqwest = { version = "0.11", features = ["json"] } +axum = { version = "0.7", optional = true, features = ["http2"] } +tower = { version = "0.4", optional = true } +tower-http = { version = "0.5", features = [ "fs", "compression-br", ], optional = true } -tokio = { version = "1.22.0", features = ["full"], optional = true } -http = { version = "0.2.11", optional = true } +tokio = { version = "1", features = ["full"], optional = true } +http = { version = "1.0", optional = true } web-sys = { version = "0.3", features = ["AbortController", "AbortSignal"] } wasm-bindgen = "0.2" lazy_static = "1.4.0" diff --git a/examples/hackernews_islands_axum/src/fallback.rs b/examples/hackernews_islands_axum/src/fallback.rs index 69ec7586c2..dbdcd85ed9 100644 --- a/examples/hackernews_islands_axum/src/fallback.rs +++ b/examples/hackernews_islands_axum/src/fallback.rs @@ -3,7 +3,7 @@ use cfg_if::cfg_if; cfg_if! { if #[cfg(feature = "ssr")] { use axum::{ - body::{boxed, Body, BoxBody}, + body::Body, extract::State, response::IntoResponse, http::{Request, Response, StatusCode, Uri}, @@ -26,12 +26,12 @@ if #[cfg(feature = "ssr")] { } } - async fn get_static_file(uri: Uri, root: &str) -> Result, (StatusCode, String)> { + async fn get_static_file(uri: Uri, root: &str) -> Result, (StatusCode, String)> { let req = Request::builder().uri(uri.clone()).body(Body::empty()).unwrap(); // `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot` // This path is relative to the cargo root match ServeDir::new(root).oneshot(req).await { - Ok(res) => Ok(res.map(boxed)), + Ok(res) => Ok(res.into_response()), Err(err) => Err(( StatusCode::INTERNAL_SERVER_ERROR, format!("Something went wrong: {}", err), diff --git a/examples/hackernews_islands_axum/src/handlers.rs b/examples/hackernews_islands_axum/src/handlers.rs index 4ed9ff7b28..1526447f30 100644 --- a/examples/hackernews_islands_axum/src/handlers.rs +++ b/examples/hackernews_islands_axum/src/handlers.rs @@ -3,13 +3,13 @@ use cfg_if::cfg_if; cfg_if! { if #[cfg(feature = "ssr")] { use axum::{ - body::{boxed, Body, BoxBody}, + body::Body, http::{Request, Response, StatusCode, Uri}, }; use tower::ServiceExt; use tower_http::services::ServeDir; - pub async fn file_handler(uri: Uri) -> Result, (StatusCode, String)> { + pub async fn file_handler(uri: Uri) -> Result, (StatusCode, String)> { let res = get_static_file(uri.clone(), "/pkg").await?; if res.status() == StatusCode::NOT_FOUND { @@ -24,7 +24,7 @@ if #[cfg(feature = "ssr")] { } } - pub async fn get_static_file_handler(uri: Uri) -> Result, (StatusCode, String)> { + pub async fn get_static_file_handler(uri: Uri) -> Result, (StatusCode, String)> { let res = get_static_file(uri.clone(), "/static").await?; if res.status() == StatusCode::NOT_FOUND { @@ -34,14 +34,14 @@ if #[cfg(feature = "ssr")] { } } - async fn get_static_file(uri: Uri, base: &str) -> Result, (StatusCode, String)> { + async fn get_static_file(uri: Uri, base: &str) -> Result, (StatusCode, String)> { let req = Request::builder().uri(&uri).body(Body::empty()).unwrap(); // `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot` // When run normally, the root should be the crate root if base == "/static" { match ServeDir::new("./static").oneshot(req).await { - Ok(res) => Ok(res.map(boxed)), + Ok(res) => Ok(res.into_response()), Err(err) => Err(( StatusCode::INTERNAL_SERVER_ERROR, format!("Something went wrong: {}", err), @@ -49,7 +49,7 @@ if #[cfg(feature = "ssr")] { } } else if base == "/pkg" { match ServeDir::new("./pkg").oneshot(req).await { - Ok(res) => Ok(res.map(boxed)), + Ok(res) => Ok(res.into_response()), Err(err) => Err(( StatusCode::INTERNAL_SERVER_ERROR, format!("Something went wrong: {}", err), diff --git a/examples/hackernews_islands_axum/src/main.rs b/examples/hackernews_islands_axum/src/main.rs index 64f75ed898..0191d06781 100644 --- a/examples/hackernews_islands_axum/src/main.rs +++ b/examples/hackernews_islands_axum/src/main.rs @@ -27,8 +27,8 @@ async fn main() { // run our app with hyper // `axum::Server` is a re-export of `hyper::Server` logging::log!("listening on {}", addr); - axum::Server::bind(&addr) - .serve(app.into_make_service()) + let listener = tokio::net::TcpListener::bind(&addr).await.unwrap(); + axum::serve(listener, app.into_make_service()) .await .unwrap(); } diff --git a/examples/hackernews_js_fetch/Cargo.toml b/examples/hackernews_js_fetch/Cargo.toml index 56f0ea8d0b..e69cff15d2 100644 --- a/examples/hackernews_js_fetch/Cargo.toml +++ b/examples/hackernews_js_fetch/Cargo.toml @@ -11,22 +11,22 @@ codegen-units = 1 lto = true [dependencies] -console_log = "1.0.0" -console_error_panic_hook = "0.1.7" -cfg-if = "1.0.0" +console_log = "1.0" +console_error_panic_hook = "0.1" +cfg-if = "1.0" leptos = { path = "../../leptos", features = ["nightly"] } leptos_axum = { path = "../../integrations/axum", default-features = false, optional = true } leptos_meta = { path = "../../meta", features = ["nightly"] } leptos_router = { path = "../../router", features = ["nightly"] } -log = "0.4.17" -simple_logger = "4.0.0" -serde = { version = "1.0.148", features = ["derive"] } +log = "0.4" +simple_logger = "4.0" +serde = { version = "1.0", features = ["derive"] } tracing = "0.1" -gloo-net = { version = "0.4.0", features = ["http"] } -reqwest = { version = "0.11.13", features = ["json"] } -axum = { version = "0.6", default-features = false, optional = true } -tower = { version = "0.4.13", optional = true } -http = { version = "0.2.11", optional = true } +gloo-net = { version = "0.4", features = ["http"] } +reqwest = { version = "0.11", features = ["json"] } +axum = { version = "0.7", default-features = false, optional = true } +tower = { version = "0.4", optional = true } +http = { version = "1.0", optional = true } web-sys = { version = "0.3", features = [ "AbortController", "AbortSignal", @@ -34,10 +34,10 @@ web-sys = { version = "0.3", features = [ "Response", ] } wasm-bindgen = "0.2" -wasm-bindgen-futures = { version = "0.4.37", features = [ +wasm-bindgen-futures = { version = "0.4", features = [ "futures-core-03-stream", ], optional = true } -axum-js-fetch = { version = "0.2.1", optional = true } +axum-js-fetch = { version = "0.2", optional = true } lazy_static = "1.4.0" [features] diff --git a/examples/hackernews_js_fetch/src/fallback.rs b/examples/hackernews_js_fetch/src/fallback.rs index 1628db4ca1..099d5ee254 100644 --- a/examples/hackernews_js_fetch/src/fallback.rs +++ b/examples/hackernews_js_fetch/src/fallback.rs @@ -3,7 +3,7 @@ use cfg_if::cfg_if; cfg_if! { if #[cfg(feature = "ssr")] { use axum::{ - body::{Body, BoxBody}, + body::Body, extract::State, response::IntoResponse, http::{Request, Response, StatusCode, Uri}, @@ -25,7 +25,7 @@ if #[cfg(feature = "ssr")] { } } - async fn get_static_file(uri: Uri, root: &str) -> Result, (StatusCode, String)> { + async fn get_static_file(uri: Uri, root: &str) -> Result, (StatusCode, String)> { let req = Request::builder().uri(uri.clone()).body(Body::empty()).unwrap(); // `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot` // This path is relative to the cargo root diff --git a/examples/hackernews_js_fetch/src/lib.rs b/examples/hackernews_js_fetch/src/lib.rs index 014a1f32c3..f1bbeeb5e9 100644 --- a/examples/hackernews_js_fetch/src/lib.rs +++ b/examples/hackernews_js_fetch/src/lib.rs @@ -62,7 +62,7 @@ cfg_if! { let routes = generate_route_list(App); // build our application with a route - let app: axum::Router<(), axum::body::Body> = Router::new() + let app: axum::Router = Router::new() .leptos_routes(&leptos_options, routes, || view! { } ) .route("/api/*fn_name", post(leptos_axum::handle_server_fns)) .with_state(leptos_options); diff --git a/examples/session_auth_axum/Cargo.toml b/examples/session_auth_axum/Cargo.toml index ec4180ecf2..31ae3bdd07 100644 --- a/examples/session_auth_axum/Cargo.toml +++ b/examples/session_auth_axum/Cargo.toml @@ -7,40 +7,41 @@ edition = "2021" crate-type = ["cdylib", "rlib"] [dependencies] -anyhow = "1.0.66" -console_log = "1.0.0" -rand = { version = "0.8.5", features = ["min_const_gen"], optional = true } -console_error_panic_hook = "0.1.7" -futures = "0.3.25" -cfg-if = "1.0.0" +anyhow = "1.0" +console_log = "1.0" +rand = { version = "0.8", features = ["min_const_gen"], optional = true } +console_error_panic_hook = "0.1" +futures = "0.3" +cfg-if = "1.0" leptos = { path = "../../leptos", features = ["nightly"] } leptos_meta = { path = "../../meta", features = ["nightly"] } leptos_axum = { path = "../../integrations/axum", optional = true } leptos_router = { path = "../../router", features = ["nightly"] } -log = "0.4.17" -simple_logger = "4.0.0" -serde = { version = "1.0.148", features = ["derive"] } -axum = { version = "0.6.1", optional = true, features=["macros"] } -tower = { version = "0.4.13", optional = true } -tower-http = { version = "0.4", features = ["fs"], optional = true } -tokio = { version = "1.22.0", features = ["full"], optional = true } -http = { version = "0.2.11" } +log = "0.4" +simple_logger = "4.0" +serde = { version = "1.0", features = ["derive"] } +axum = { version = "0.7", optional = true, features=["macros"] } +tower = { version = "0.4", optional = true } +tower-http = { version = "0.5", features = ["fs"], optional = true } +tokio = { version = "1", features = ["full"], optional = true } +http = { version = "1.0" } sqlx = { version = "0.7.2", features = [ "runtime-tokio-rustls", "sqlite", ], optional = true } -thiserror = "1.0.38" +thiserror = "1.0" wasm-bindgen = "0.2" -axum_session_auth = { version = "0.9.0", features = [ +axum_session_auth = { version = "0.10", features = [ "sqlite-rustls", ], optional = true } -axum_session = { version = "0.9.0", features = [ +axum_session = { version = "0.10", features = [ "sqlite-rustls", ], optional = true } -bcrypt = { version = "0.14", optional = true } -async-trait = { version = "0.1.64", optional = true } +bcrypt = { version = "0.15", optional = true } +async-trait = { version = "0.1", optional = true } [features] +default = ["ssr"] hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"] ssr = [ "dep:axum", diff --git a/examples/session_auth_axum/src/fallback.rs b/examples/session_auth_axum/src/fallback.rs index 635d4de9dd..9eb77ac5fd 100644 --- a/examples/session_auth_axum/src/fallback.rs +++ b/examples/session_auth_axum/src/fallback.rs @@ -3,7 +3,7 @@ use cfg_if::cfg_if; cfg_if! { if #[cfg(feature = "ssr")] { use axum::{ - body::{boxed, Body, BoxBody}, + body::Body, extract::State, response::IntoResponse, http::{Request, Response, StatusCode, Uri}, @@ -29,12 +29,12 @@ if #[cfg(feature = "ssr")] { } } - async fn get_static_file(uri: Uri, root: &str) -> Result, (StatusCode, String)> { + async fn get_static_file(uri: Uri, root: &str) -> Result, (StatusCode, String)> { let req = Request::builder().uri(uri.clone()).body(Body::empty()).unwrap(); // `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot` // This path is relative to the cargo root match ServeDir::new(root).oneshot(req).await { - Ok(res) => Ok(res.map(boxed)), + Ok(res) => Ok(res.into_response()), Err(err) => Err(( StatusCode::INTERNAL_SERVER_ERROR, format!("Something went wrong: {err}"), diff --git a/examples/session_auth_axum/src/main.rs b/examples/session_auth_axum/src/main.rs index 8912e2fe45..a321729fd6 100644 --- a/examples/session_auth_axum/src/main.rs +++ b/examples/session_auth_axum/src/main.rs @@ -100,8 +100,8 @@ if #[cfg(feature = "ssr")] { // run our app with hyper // `axum::Server` is a re-export of `hyper::Server` log!("listening on http://{}", &addr); - axum::Server::bind(&addr) - .serve(app.into_make_service()) + let listener = tokio::net::TcpListener::bind(&addr).await.unwrap(); + axum::serve(listener, app.into_make_service()) .await .unwrap(); } diff --git a/examples/ssr_modes_axum/Cargo.toml b/examples/ssr_modes_axum/Cargo.toml index 1c350dc959..e57730e9c6 100644 --- a/examples/ssr_modes_axum/Cargo.toml +++ b/examples/ssr_modes_axum/Cargo.toml @@ -18,9 +18,9 @@ leptos_router = { path = "../../router", features = ["nightly"] } log = "0.4" serde = { version = "1", features = ["derive"] } thiserror = "1" -axum = { version = "0.6.1", optional = true } -tower = { version = "0.4.13", optional = true } -tower-http = { version = "0.4", features = ["fs"], optional = true } +axum = { version = "0.7", optional = true } +tower = { version = "0.4", optional = true } +tower-http = { version = "0.5", features = ["fs"], optional = true } tokio = { version = "1", features = ["time"], optional = true } wasm-bindgen = "0.2" diff --git a/examples/ssr_modes_axum/src/fallback.rs b/examples/ssr_modes_axum/src/fallback.rs index 99403cd2ff..1d6f95333c 100644 --- a/examples/ssr_modes_axum/src/fallback.rs +++ b/examples/ssr_modes_axum/src/fallback.rs @@ -2,7 +2,7 @@ use cfg_if::cfg_if; cfg_if! { if #[cfg(feature = "ssr")] { use axum::{ - body::{boxed, Body, BoxBody}, + body::Body, extract::State, response::IntoResponse, http::{Request, Response, StatusCode, Uri}, @@ -28,12 +28,12 @@ cfg_if! { if #[cfg(feature = "ssr")] { } } - async fn get_static_file(uri: Uri, root: &str) -> Result, (StatusCode, String)> { + async fn get_static_file(uri: Uri, root: &str) -> Result, (StatusCode, String)> { let req = Request::builder().uri(uri.clone()).body(Body::empty()).unwrap(); // `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot` // This path is relative to the cargo root match ServeDir::new(root).oneshot(req).await { - Ok(res) => Ok(res.map(boxed)), + Ok(res) => Ok(res.into_response()), Err(err) => Err(( StatusCode::INTERNAL_SERVER_ERROR, format!("Something went wrong: {err}"), diff --git a/examples/ssr_modes_axum/src/main.rs b/examples/ssr_modes_axum/src/main.rs index 641f76cdff..0c2b04aef5 100644 --- a/examples/ssr_modes_axum/src/main.rs +++ b/examples/ssr_modes_axum/src/main.rs @@ -27,8 +27,8 @@ async fn main() { // run our app with hyper // `axum::Server` is a re-export of `hyper::Server` log!("listening on http://{}", &addr); - axum::Server::bind(&addr) - .serve(app.into_make_service()) + let listener = tokio::net::TcpListener::bind(&addr).await.unwrap(); + axum::serve(listener, app.into_make_service()) .await .unwrap(); } diff --git a/examples/tailwind_axum/Cargo.toml b/examples/tailwind_axum/Cargo.toml index 94616dbb3d..e4bd1aaba7 100644 --- a/examples/tailwind_axum/Cargo.toml +++ b/examples/tailwind_axum/Cargo.toml @@ -7,8 +7,8 @@ edition = "2021" crate-type = ["cdylib", "rlib"] [dependencies] -axum = { version = "0.6.18", optional = true } -console_error_panic_hook = "0.1.7" +axum = { version = "0.7", optional = true } +console_error_panic_hook = "0.1" console_log = "1" cfg-if = "1" leptos = { path = "../../leptos", features = ["nightly"] } @@ -17,13 +17,13 @@ leptos_axum = { path = "../../integrations/axum", optional = true } leptos_router = { path = "../../router", features = ["nightly"] } log = "0.4.17" simple_logger = "4" -tokio = { version = "1.28.1", optional = true } -tower = { version = "0.4.13", optional = true } -tower-http = { version = "0.4", features = ["fs"], optional = true } -wasm-bindgen = "0.2.84" -thiserror = "1.0.40" -tracing = { version = "0.1.37", optional = true } -http = "0.2.11" +tokio = { version = "1", optional = true } +tower = { version = "0.4", optional = true } +tower-http = { version = "0.5", features = ["fs"], optional = true } +wasm-bindgen = "0.2" +thiserror = "1.0" +tracing = { version = "0.1", optional = true } +http = "1.0" [features] hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"] diff --git a/examples/tailwind_axum/src/fallback.rs b/examples/tailwind_axum/src/fallback.rs index 99403cd2ff..1d6f95333c 100644 --- a/examples/tailwind_axum/src/fallback.rs +++ b/examples/tailwind_axum/src/fallback.rs @@ -2,7 +2,7 @@ use cfg_if::cfg_if; cfg_if! { if #[cfg(feature = "ssr")] { use axum::{ - body::{boxed, Body, BoxBody}, + body::Body, extract::State, response::IntoResponse, http::{Request, Response, StatusCode, Uri}, @@ -28,12 +28,12 @@ cfg_if! { if #[cfg(feature = "ssr")] { } } - async fn get_static_file(uri: Uri, root: &str) -> Result, (StatusCode, String)> { + async fn get_static_file(uri: Uri, root: &str) -> Result, (StatusCode, String)> { let req = Request::builder().uri(uri.clone()).body(Body::empty()).unwrap(); // `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot` // This path is relative to the cargo root match ServeDir::new(root).oneshot(req).await { - Ok(res) => Ok(res.map(boxed)), + Ok(res) => Ok(res.into_response()), Err(err) => Err(( StatusCode::INTERNAL_SERVER_ERROR, format!("Something went wrong: {err}"), diff --git a/examples/tailwind_axum/src/main.rs b/examples/tailwind_axum/src/main.rs index 0bed160c5c..d179ac888a 100644 --- a/examples/tailwind_axum/src/main.rs +++ b/examples/tailwind_axum/src/main.rs @@ -31,8 +31,8 @@ async fn main() { // run our app with hyper // `axum::Server` is a re-export of `hyper::Server` info!("listening on http://{}", &addr); - axum::Server::bind(&addr) - .serve(app.into_make_service()) + let listener = tokio::net::TcpListener::bind(&addr).await.unwrap(); + axum::serve(listener, app.into_make_service()) .await .unwrap(); } diff --git a/examples/todo_app_sqlite_axum/Cargo.toml b/examples/todo_app_sqlite_axum/Cargo.toml index ac3eda9d7c..a01215d397 100644 --- a/examples/todo_app_sqlite_axum/Cargo.toml +++ b/examples/todo_app_sqlite_axum/Cargo.toml @@ -7,27 +7,27 @@ edition = "2021" crate-type = ["cdylib", "rlib"] [dependencies] -console_log = "1.0.0" -console_error_panic_hook = "0.1.7" -futures = "0.3.25" -cfg-if = "1.0.0" +console_log = "1.0" +console_error_panic_hook = "0.1" +futures = "0.3" +cfg-if = "1.0" +http = "1.0" leptos = { path = "../../leptos", features = ["nightly"] } leptos_axum = { path = "../../integrations/axum", optional = true } leptos_meta = { path = "../../meta", features = ["nightly"] } leptos_router = { path = "../../router", features = ["nightly"] } -log = "0.4.17" -simple_logger = "4.0.0" +log = "0.4" +simple_logger = "4.0" serde = { version = "1", features = ["derive"] } -axum = { version = "0.6.1", optional = true } -tower = { version = "0.4.13", optional = true } -tower-http = { version = "0.4", features = ["fs"], optional = true } -tokio = { version = "1.22.0", features = ["full"], optional = true } -http = { version = "0.2.11" } -sqlx = { version = "0.6.2", features = [ +axum = { version = "0.7", optional = true } +tower = { version = "0.4", optional = true } +tower-http = { version = "0.5", features = ["fs"], optional = true } +tokio = { version = "1", features = ["full"], optional = true } +sqlx = { version = "0.7", features = [ "runtime-tokio-rustls", "sqlite", ], optional = true } -thiserror = "1.0.38" +thiserror = "1.0" wasm-bindgen = "0.2" [features] diff --git a/examples/todo_app_sqlite_axum/Todos.db b/examples/todo_app_sqlite_axum/Todos.db index c9e91811510280b8e0884336ca7221cd3be5151a..ec81f62b999d4d0b6675e6c5b4550d572cafc1ca 100644 GIT binary patch delta 92 zcmZo@U~Fh$oFL68G*QNxRfs{aXu-ymx%`6sFBzElF9H$&9WXq)Sx{gzzXlf@3xgzw gM`}(^z6B=>3xha>&B+R9mMAhXFo Result, (StatusCode, String)> { + async fn get_static_file(uri: Uri, root: &str) -> Result, (StatusCode, String)> { let req = Request::builder().uri(uri.clone()).body(Body::empty()).unwrap(); // `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot` // This path is relative to the cargo root match ServeDir::new(root).oneshot(req).await { - Ok(res) => Ok(res.map(boxed)), + Ok(res) => Ok(res.into_response()), Err(err) => Err(( StatusCode::INTERNAL_SERVER_ERROR, format!("Something went wrong: {err}"), diff --git a/examples/todo_app_sqlite_axum/src/main.rs b/examples/todo_app_sqlite_axum/src/main.rs index f4b74c8270..a21789c36e 100644 --- a/examples/todo_app_sqlite_axum/src/main.rs +++ b/examples/todo_app_sqlite_axum/src/main.rs @@ -10,14 +10,14 @@ cfg_if! { response::{IntoResponse, Response}, Router, }; - use axum::body::Body as AxumBody; + use axum::body::Body; use crate::todo::*; use todo_app_sqlite_axum::*; use crate::fallback::file_and_error_handler; use leptos_axum::{generate_route_list, LeptosRoutes}; //Define a handler to test extractor with state - async fn custom_handler(Path(id): Path, State(options): State, req: Request) -> Response{ + async fn custom_handler(Path(id): Path, State(options): State, req: Request) -> Response{ let handler = leptos_axum::render_app_to_stream_with_context(options, move || { provide_context(id.clone()); @@ -60,9 +60,9 @@ cfg_if! { // run our app with hyper // `axum::Server` is a re-export of `hyper::Server` + let listener = tokio::net::TcpListener::bind(&addr).await.unwrap(); logging::log!("listening on http://{}", &addr); - axum::Server::bind(&addr) - .serve(app.into_make_service()) + axum::serve(listener, app.into_make_service()) .await .unwrap(); } diff --git a/examples/todo_app_sqlite_csr/Cargo.toml b/examples/todo_app_sqlite_csr/Cargo.toml index fd5c74e3a8..b10978f71f 100644 --- a/examples/todo_app_sqlite_csr/Cargo.toml +++ b/examples/todo_app_sqlite_csr/Cargo.toml @@ -7,27 +7,27 @@ edition = "2021" crate-type = ["cdylib", "rlib"] [dependencies] -console_log = "1.0.0" -console_error_panic_hook = "0.1.7" -futures = "0.3.25" +console_log = "1.0" +console_error_panic_hook = "0.1" +futures = "0.3" leptos = { path = "../../leptos", features = ["nightly"] } leptos_axum = { path = "../../integrations/axum", optional = true } leptos_meta = { path = "../../meta", features = ["nightly"] } leptos_router = { path = "../../router", features = ["nightly"] } leptos_integration_utils = { path = "../../integrations/utils", optional = true } -log = "0.4.17" -simple_logger = "4.0.0" +log = "0.4" +simple_logger = "4.0" serde = { version = "1", features = ["derive"] } -axum = { version = "0.6.1", optional = true } -tower = { version = "0.4.13", optional = true } -tower-http = { version = "0.4", features = ["fs"], optional = true } -tokio = { version = "1.22.0", features = ["full"], optional = true } -http = { version = "0.2.11" } -sqlx = { version = "0.6.2", features = [ +axum = { version = "0.7", optional = true } +tower = { version = "0.4", optional = true } +tower-http = { version = "0.5", features = ["fs"], optional = true } +tokio = { version = "1", features = ["full"], optional = true } +http = { version = "1.0" } +sqlx = { version = "0.7", features = [ "runtime-tokio-rustls", "sqlite", ], optional = true } -thiserror = "1.0.38" +thiserror = "1.0" wasm-bindgen = "0.2" [features] diff --git a/examples/todo_app_sqlite_csr/src/fallback.rs b/examples/todo_app_sqlite_csr/src/fallback.rs index 129f69733e..e799d04bcc 100644 --- a/examples/todo_app_sqlite_csr/src/fallback.rs +++ b/examples/todo_app_sqlite_csr/src/fallback.rs @@ -1,5 +1,5 @@ use axum::{ - body::{boxed, Body, BoxBody}, + body::Body, extract::State, http::{Request, Response, StatusCode, Uri}, response::{Html, IntoResponse, Response as AxumResponse}, @@ -28,7 +28,7 @@ pub async fn file_or_index_handler( async fn get_static_file( uri: Uri, root: &str, -) -> Result, (StatusCode, String)> { +) -> Result, (StatusCode, String)> { let req = Request::builder() .uri(uri.clone()) .body(Body::empty()) @@ -36,7 +36,7 @@ async fn get_static_file( // `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot` // This path is relative to the cargo root match ServeDir::new(root).oneshot(req).await { - Ok(res) => Ok(res.map(boxed)), + Ok(res) => Ok(res.into_response()), Err(err) => Err(( StatusCode::INTERNAL_SERVER_ERROR, format!("Something went wrong: {err}"), diff --git a/examples/todo_app_sqlite_csr/src/main.rs b/examples/todo_app_sqlite_csr/src/main.rs index 13eb02506f..0d83232c38 100644 --- a/examples/todo_app_sqlite_csr/src/main.rs +++ b/examples/todo_app_sqlite_csr/src/main.rs @@ -39,8 +39,10 @@ async fn main() { // run our app with hyper // `axum::Server` is a re-export of `hyper::Server` logging::log!("listening on http://{}", &addr); - axum::Server::bind(&addr) - .serve(app.into_make_service()) + let listener = tokio::net::TcpListener::bind(&addr) + .await + .expect("couldn't bind to address"); + axum::serve(listener, app.into_make_service()) .await .unwrap(); } diff --git a/integrations/axum/Cargo.toml b/integrations/axum/Cargo.toml index f6091a8917..91fc936e8e 100644 --- a/integrations/axum/Cargo.toml +++ b/integrations/axum/Cargo.toml @@ -8,23 +8,26 @@ repository = "https://github.com/leptos-rs/leptos" description = "Axum integrations for the Leptos web framework." [dependencies] -axum = { version = "0.6", default-features = false, features = [ +axum = { version = "0.7", default-features = false, features = [ "matched-path", ] } futures = "0.3" -http = "0.2.11" -hyper = "0.14.23" +http-body-util = "0.1" leptos = { workspace = true, features = ["ssr"] } leptos_meta = { workspace = true, features = ["ssr"] } leptos_router = { workspace = true, features = ["ssr"] } leptos_integration_utils = { workspace = true } +parking_lot = "0.12" serde_json = "1" tokio = { version = "1", default-features = false } -parking_lot = "0.12.1" -tokio-util = { version = "0.7.7", features = ["rt"] } -tracing = "0.1.37" -once_cell = "1.17" -cfg-if = "1.0.0" +tokio-util = { version = "0.7", features = ["rt"] } +tracing = "0.1" +once_cell = "1.18" +cfg-if = "1.0" + +[dev-dependencies] +axum = "0.7" +tokio = { version = "1", features = ["net"] } [features] nonce = ["leptos/nonce"] diff --git a/integrations/axum/src/lib.rs b/integrations/axum/src/lib.rs index 26baee5b08..3d019a888e 100644 --- a/integrations/axum/src/lib.rs +++ b/integrations/axum/src/lib.rs @@ -34,11 +34,15 @@ //! directory in the Leptos repository. use axum::{ - body::{Body, Bytes, Full, StreamBody}, + body::{Body, Bytes}, extract::{FromRef, FromRequestParts, MatchedPath, Path, RawQuery}, http::{ - header::{HeaderName, HeaderValue}, - HeaderMap, Request, StatusCode, + header::{self, HeaderName, HeaderValue}, + method::Method, + request::Parts, + uri::Uri, + version::Version, + HeaderMap, Request, Response, StatusCode, }, response::IntoResponse, routing::{delete, get, patch, post, put}, @@ -47,11 +51,7 @@ use futures::{ channel::mpsc::{Receiver, Sender}, Future, SinkExt, Stream, StreamExt, }; -use http::{ - header, method::Method, request::Parts, uri::Uri, version::Version, - Response, -}; -use hyper::body; +use http_body_util::BodyExt; use leptos::{ leptos_server::{server_fn_by_path, Payload}, server_fn::Encoding, @@ -162,7 +162,7 @@ pub async fn generate_request_and_parts( ) -> (Request, RequestParts) { // provide request headers as context in server scope let (parts, body) = req.into_parts(); - let body = body::to_bytes(body).await.unwrap_or_default(); + let body = body.collect().await.unwrap_or_default().to_bytes(); let request_parts = RequestParts { method: parts.method.clone(), uri: parts.uri.clone(), @@ -196,9 +196,8 @@ pub async fn generate_request_and_parts( /// .route("/api/*fn_name", post(leptos_axum::handle_server_fns)); /// /// // run our app with hyper -/// // `axum::Server` is a re-export of `hyper::Server` -/// axum::Server::bind(&addr) -/// .serve(app.into_make_service()) +/// let listener = tokio::net::TcpListener::bind(&addr).await.unwrap(); +/// axum::serve(listener, app.into_make_service()) /// .await /// .unwrap(); /// } @@ -358,21 +357,21 @@ async fn handle_server_fns_inner( match serialized { Payload::Binary(data) => res .header("Content-Type", "application/cbor") - .body(Full::from(data)), + .body(Body::from(data)), Payload::Url(data) => res .header( "Content-Type", "application/x-www-form-urlencoded", ) - .body(Full::from(data)), + .body(Body::from(data)), Payload::Json(data) => res .header("Content-Type", "application/json") - .body(Full::from(data)), + .body(Body::from(data)), } } Err(e) => Response::builder() .status(StatusCode::INTERNAL_SERVER_ERROR) - .body(Full::from( + .body(Body::from( serde_json::to_string(&e) .unwrap_or_else(|_| e.to_string()), )), @@ -382,7 +381,7 @@ async fn handle_server_fns_inner( res } else { Response::builder().status(StatusCode::BAD_REQUEST).body( - Full::from(format!( + Body::from(format!( "Could not find a server function at the route \ {fn_name}. \n\nIt's likely that either 1. The API prefix you specify in the `#[server]` \ @@ -442,9 +441,8 @@ pub type PinnedHtmlStream = /// )); /// /// // run our app with hyper -/// // `axum::Server` is a re-export of `hyper::Server` -/// axum::Server::bind(&addr) -/// .serve(app.into_make_service()) +/// let listener = tokio::net::TcpListener::bind(&addr).await.unwrap(); +/// axum::serve(listener, app.into_make_service()) /// .await /// .unwrap(); /// } @@ -463,13 +461,8 @@ pub fn render_app_to_stream( app_fn: impl Fn() -> IV + Clone + Send + 'static, ) -> impl Fn( Request, -) -> Pin< - Box< - dyn Future>> - + Send - + 'static, - >, -> + Clone +) -> Pin> + Send + 'static>> + + Clone + Send + 'static where @@ -490,13 +483,8 @@ pub fn render_route( app_fn: impl Fn() -> IV + Clone + Send + 'static, ) -> impl Fn( Request, -) -> Pin< - Box< - dyn Future>> - + Send - + 'static, - >, -> + Clone +) -> Pin> + Send + 'static>> + + Clone + Send + 'static where @@ -504,6 +492,7 @@ where { render_route_with_context(options, paths, || {}, app_fn) } + /// Returns an Axum [Handler](axum::handler::Handler) that listens for a `GET` request and tries /// to route it using [leptos_router], serving an in-order HTML stream of your application. /// This stream will pause at each `` node and wait for it to resolve before @@ -543,9 +532,8 @@ where /// )); /// /// // run our app with hyper -/// // `axum::Server` is a re-export of `hyper::Server` -/// axum::Server::bind(&addr) -/// .serve(app.into_make_service()) +/// let listener = tokio::net::TcpListener::bind(&addr).await.unwrap(); +/// axum::serve(listener, app.into_make_service()) /// .await /// .unwrap(); /// } @@ -564,13 +552,8 @@ pub fn render_app_to_stream_in_order( app_fn: impl Fn() -> IV + Clone + Send + 'static, ) -> impl Fn( Request, -) -> Pin< - Box< - dyn Future>> - + Send - + 'static, - >, -> + Clone +) -> Pin> + Send + 'static>> + + Clone + Send + 'static where @@ -611,13 +594,8 @@ pub fn render_app_to_stream_with_context( app_fn: impl Fn() -> IV + Clone + Send + 'static, ) -> impl Fn( Request, -) -> Pin< - Box< - dyn Future>> - + Send - + 'static, - >, -> + Clone +) -> Pin> + Send + 'static>> + + Clone + Send + 'static where @@ -644,13 +622,8 @@ pub fn render_route_with_context( app_fn: impl Fn() -> IV + Clone + Send + 'static, ) -> impl Fn( Request, -) -> Pin< - Box< - dyn Future>> - + Send - + 'static, - >, -> + Clone +) -> Pin> + Send + 'static>> + + Clone + Send + 'static where @@ -704,6 +677,7 @@ where } } } + /// Returns an Axum [Handler](axum::handler::Handler) that listens for a `GET` request and tries /// to route it using [leptos_router], serving an HTML stream of your application. /// @@ -732,13 +706,8 @@ pub fn render_app_to_stream_with_context_and_replace_blocks( replace_blocks: bool, ) -> impl Fn( Request, -) -> Pin< - Box< - dyn Future>> - + Send - + 'static, - >, -> + Clone +) -> Pin> + Send + 'static>> + + Clone + Send + 'static where @@ -791,7 +760,7 @@ where async fn generate_response( res_options: ResponseOptions, rx: Receiver, -) -> Response> { +) -> Response { let mut stream = Box::pin(rx.map(|html| Ok(Bytes::from(html)))); // Get the first and second chunks in the stream, which renders the app shell, and thus allows Resources to run @@ -806,9 +775,9 @@ async fn generate_response( futures::stream::iter([first_chunk.unwrap(), second_chunk.unwrap()]) .chain(stream); - let mut res = Response::new(StreamBody::new( - Box::pin(complete_stream) as PinnedHtmlStream - )); + let mut res = + Body::from_stream(Box::pin(complete_stream) as PinnedHtmlStream) + .into_response(); if let Some(status) = res_options.status { *res.status_mut() = status @@ -819,11 +788,11 @@ async fn generate_response( let mut res_headers = res_options.headers.clone(); headers.extend(res_headers.drain()); - if !headers.contains_key(http::header::CONTENT_TYPE) { + if !headers.contains_key(header::CONTENT_TYPE) { // Set the Content Type headers on all responses. This makes Firefox show the page source // without complaining headers.insert( - http::header::CONTENT_TYPE, + header::CONTENT_TYPE, HeaderValue::from_str("text/html; charset=utf-8").unwrap(), ); } @@ -897,13 +866,8 @@ pub fn render_app_to_stream_in_order_with_context( app_fn: impl Fn() -> IV + Clone + Send + 'static, ) -> impl Fn( Request, -) -> Pin< - Box< - dyn Future>> - + Send - + 'static, - >, -> + Clone +) -> Pin> + Send + 'static>> + + Clone + Send + 'static where @@ -1012,8 +976,9 @@ fn provide_contexts( /// /// // run our app with hyper /// // `axum::Server` is a re-export of `hyper::Server` -/// axum::Server::bind(&addr) -/// .serve(app.into_make_service()) +/// let listener = +/// tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(); +/// axum::serve(listener, app.into_make_service()) /// .await /// .unwrap(); /// } @@ -1075,13 +1040,8 @@ pub fn render_app_async_stream_with_context( app_fn: impl Fn() -> IV + Clone + Send + 'static, ) -> impl Fn( Request, -) -> Pin< - Box< - dyn Future>> - + Send - + 'static, - >, -> + Clone +) -> Pin> + Send + 'static>> + + Clone + Send + 'static where @@ -1149,10 +1109,10 @@ where let complete_stream = futures::stream::iter([Ok(Bytes::from(html))]); - let mut res = Response::new(StreamBody::new(Box::pin( - complete_stream, + let mut res = Body::from_stream( + Box::pin(complete_stream) as PinnedHtmlStream ) - as PinnedHtmlStream)); + .into_response(); if let Some(status) = res_options.status { *res.status_mut() = status } @@ -1162,11 +1122,11 @@ where headers.extend(res_headers.drain()); // This one doesn't use generate_response(), so we need to do this seperately - if !headers.contains_key(http::header::CONTENT_TYPE) { + if !headers.contains_key(header::CONTENT_TYPE) { // Set the Content Type headers on all responses. This makes Firefox show the page source // without complaining headers.insert( - http::header::CONTENT_TYPE, + header::CONTENT_TYPE, HeaderValue::from_str("text/html; charset=utf-8") .unwrap(), ); @@ -1291,6 +1251,7 @@ where }) } } + /// Generates a list of all routes defined in Leptos's Router in your app. We can then use this to automatically /// create routes in Axum's Router without having to use wildcard matching or fallbacks. Takes in your root app Element /// as an argument so it can walk you app tree. This version is tailored to generate Axum compatible paths. @@ -1464,7 +1425,7 @@ where handler: H, ) -> Self where - H: axum::handler::Handler, + H: axum::handler::Handler, T: 'static; } @@ -1803,7 +1764,7 @@ where handler: H, ) -> Self where - H: axum::handler::Handler, + H: axum::handler::Handler, T: 'static, { let mut router = self; From c7422cd96e18073c733d46021222de874b825121 Mon Sep 17 00:00:00 2001 From: benwis Date: Sun, 31 Dec 2023 12:09:38 -0800 Subject: [PATCH 002/100] First commit, checkpoint for cyclical dependency error --- Cargo.toml | 2 +- examples/session_auth_axum/flake.lock | 18 +- flake.lock | 12 +- integrations/pavex/Cargo.toml | 24 + .../pavex}/Makefile.toml | 0 integrations/pavex/src/lib.rs | 1554 +++++++++++++++++ server_fn/Cargo.toml | 106 +- server_fn/Makefile.toml | 1 - server_fn/server_fn_macro_default/src/lib.rs | 69 - server_fn/src/client.rs | 65 + server_fn/src/codec/cbor.rs | 160 ++ server_fn/src/codec/json.rs | 59 + server_fn/src/codec/mod.rs | 53 + server_fn/src/codec/multipart.rs | 71 + server_fn/src/codec/rkyv.rs | 72 + server_fn/src/codec/stream.rs | 128 ++ server_fn/src/codec/url.rs | 131 ++ server_fn/src/default.rs | 117 -- server_fn/src/error.rs | 191 +- server_fn/src/lib.rs | 821 +++------ server_fn/src/middleware/mod.rs | 109 ++ server_fn/src/redirect.rs | 13 + server_fn/src/request/actix.rs | 50 + server_fn/src/request/axum.rs | 43 + server_fn/src/request/browser.rs | 93 + server_fn/src/request/mod.rs | 103 ++ server_fn/src/request/reqwest.rs | 93 + server_fn/src/response/actix.rs | 51 + server_fn/src/response/browser.rs | 73 + server_fn/src/response/http.rs | 50 + server_fn/src/response/mod.rs | 85 + server_fn/src/response/reqwest.rs | 45 + server_fn_macro/Cargo.toml | 8 +- server_fn_macro/Makefile.toml | 1 - server_fn_macro/src/lib.rs | 763 +++++--- .../Cargo.toml | 9 +- server_fn_macro_default/src/lib.rs | 19 + 37 files changed, 4163 insertions(+), 1099 deletions(-) create mode 100644 integrations/pavex/Cargo.toml rename {server_fn/server_fn_macro_default => integrations/pavex}/Makefile.toml (100%) create mode 100644 integrations/pavex/src/lib.rs delete mode 100644 server_fn/Makefile.toml delete mode 100644 server_fn/server_fn_macro_default/src/lib.rs create mode 100644 server_fn/src/client.rs create mode 100644 server_fn/src/codec/cbor.rs create mode 100644 server_fn/src/codec/json.rs create mode 100644 server_fn/src/codec/mod.rs create mode 100644 server_fn/src/codec/multipart.rs create mode 100644 server_fn/src/codec/rkyv.rs create mode 100644 server_fn/src/codec/stream.rs create mode 100644 server_fn/src/codec/url.rs delete mode 100644 server_fn/src/default.rs create mode 100644 server_fn/src/middleware/mod.rs create mode 100644 server_fn/src/redirect.rs create mode 100644 server_fn/src/request/actix.rs create mode 100644 server_fn/src/request/axum.rs create mode 100644 server_fn/src/request/browser.rs create mode 100644 server_fn/src/request/mod.rs create mode 100644 server_fn/src/request/reqwest.rs create mode 100644 server_fn/src/response/actix.rs create mode 100644 server_fn/src/response/browser.rs create mode 100644 server_fn/src/response/http.rs create mode 100644 server_fn/src/response/mod.rs create mode 100644 server_fn/src/response/reqwest.rs delete mode 100644 server_fn_macro/Makefile.toml rename {server_fn/server_fn_macro_default => server_fn_macro_default}/Cargo.toml (63%) create mode 100644 server_fn_macro_default/src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index 7cccce8d38..d31ab7d9d3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,7 @@ members = [ "leptos_server", "server_fn", "server_fn_macro", - "server_fn/server_fn_macro_default", + "server_fn_macro_default", # integrations "integrations/actix", diff --git a/examples/session_auth_axum/flake.lock b/examples/session_auth_axum/flake.lock index 18fbbc6d61..c6c5cacc00 100644 --- a/examples/session_auth_axum/flake.lock +++ b/examples/session_auth_axum/flake.lock @@ -5,11 +5,11 @@ "systems": "systems" }, "locked": { - "lastModified": 1681202837, - "narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=", + "lastModified": 1701680307, + "narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=", "owner": "numtide", "repo": "flake-utils", - "rev": "cfacdce06f30d2b68473a46042957675eebb3401", + "rev": "4022d587cbbfd70fe950c1e2083a02621806a725", "type": "github" }, "original": { @@ -38,11 +38,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1672580127, - "narHash": "sha256-3lW3xZslREhJogoOkjeZtlBtvFMyxHku7I/9IVehhT8=", + "lastModified": 1685573264, + "narHash": "sha256-Zffu01pONhs/pqH07cjlF10NnMDLok8ix5Uk4rhOnZQ=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "0874168639713f547c05947c76124f78441ea46c", + "rev": "380be19fbd2d9079f677978361792cb25e8a3635", "type": "github" }, "original": { @@ -67,11 +67,11 @@ ] }, "locked": { - "lastModified": 1681525152, - "narHash": "sha256-KzI+ILcmU03iFWtB+ysPqtNmp8TP8v1BBReTuPP8MJY=", + "lastModified": 1703902408, + "narHash": "sha256-qXdWvu+tlgNjeoz8yQMRKSom6QyRROfgpmeOhwbujqw=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "b6f8d87208336d7cb85003b2e439fc707c38f92a", + "rev": "319f57cd2c34348c55970a4bf2b35afe82088681", "type": "github" }, "original": { diff --git a/flake.lock b/flake.lock index 9842fad562..36554d16b1 100644 --- a/flake.lock +++ b/flake.lock @@ -38,11 +38,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1702312524, - "narHash": "sha256-gkZJRDBUCpTPBvQk25G0B7vfbpEYM5s5OZqghkjZsnE=", + "lastModified": 1703637592, + "narHash": "sha256-8MXjxU0RfFfzl57Zy3OfXCITS0qWDNLzlBAdwxGZwfY=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "a9bf124c46ef298113270b1f84a164865987a91c", + "rev": "cfc3698c31b1fb9cdcf10f36c9643460264d0ca8", "type": "github" }, "original": { @@ -81,11 +81,11 @@ "nixpkgs": "nixpkgs_2" }, "locked": { - "lastModified": 1702433821, - "narHash": "sha256-Kxv+dRbzj1fLQG0fyF/H6nswda6cN48r6kjctysnY4o=", + "lastModified": 1703902408, + "narHash": "sha256-qXdWvu+tlgNjeoz8yQMRKSom6QyRROfgpmeOhwbujqw=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "cb9016d3a569100a609bb92c0a45beb9e23cd4eb", + "rev": "319f57cd2c34348c55970a4bf2b35afe82088681", "type": "github" }, "original": { diff --git a/integrations/pavex/Cargo.toml b/integrations/pavex/Cargo.toml new file mode 100644 index 0000000000..5ac8c3d8d4 --- /dev/null +++ b/integrations/pavex/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "leptos_pavex" +version = { workspace = true } +edition = "2021" +authors = ["Ben Wishovich"] +license = "MIT" +repository = "https://github.com/leptos-rs/leptos" +description = "Pavex integrations for the Leptos web framework." + +[dependencies] +pavex = { git = "https://github.com/LukeMathWalker/pavex", branch = "main" } +futures = "0.3" +leptos = { workspace = true, features = ["ssr"] } +leptos_meta = { workspace = true, features = ["ssr"] } +leptos_router = { workspace = true, features = ["ssr"] } +leptos_integration_utils = { workspace = true } +parking_lot = "0.12.1" +regex = "1.7.0" +tracing = "0.1.37" +tokio = { version = "1", features = ["full"] } + +[features] +nonce = ["leptos/nonce"] +experimental-islands = ["leptos_integration_utils/experimental-islands"] diff --git a/server_fn/server_fn_macro_default/Makefile.toml b/integrations/pavex/Makefile.toml similarity index 100% rename from server_fn/server_fn_macro_default/Makefile.toml rename to integrations/pavex/Makefile.toml diff --git a/integrations/pavex/src/lib.rs b/integrations/pavex/src/lib.rs new file mode 100644 index 0000000000..a21f110f9a --- /dev/null +++ b/integrations/pavex/src/lib.rs @@ -0,0 +1,1554 @@ +#![forbid(unsafe_code)] + +//! Provides functions to easily integrate Leptos with Actix. +//! +//! For more details on how to use the integrations, see the +//! [`examples`](https://github.com/leptos-rs/leptos/tree/main/examples) +//! directory in the Leptos repository. + +//use actix_web::{ +// body::BoxBody, +// dev::{ServiceFactory, ServiceRequest}, +// http::header, +// web::{Bytes, ServiceConfig}, +// *, +//}; +use futures::{Stream, StreamExt}; +use leptos::{ + leptos_server::{server_fn_by_path, Payload}, + server_fn::Encoding, + ssr::render_to_stream_with_prefix_undisposed_with_context_and_block_replacement, + *, +}; +use leptos_integration_utils::{build_async_response, html_parts_separated}; +use leptos_meta::*; +use leptos_router::*; +use parking_lot::RwLock; +use pavex::{http::header::{self, HeaderMap, HeaderName, HeaderValue}, request::{RequestHead, body::RawIncomingBody}}; +use pavex::http::StatusCode; +use pavex::request::Request; +use pavex::response::body::raw::BoxBody; +use pavex::response::Response; +use regex::Regex; +use std::{ + fmt::{Debug, Display}, + future::Future, + pin::Pin, + sync::Arc, +}; +#[cfg(debug_assertions)] +use tracing::instrument; +/// This struct lets you define headers and override the status of the Response from an Element or a Server Function +/// Typically contained inside of a ResponseOptions. Setting this is useful for cookies and custom responses. +#[derive(Debug, Clone, Default)] +pub struct ResponseParts { + pub headers: HeaderMap, + pub status: Option, +} + +impl ResponseParts { + /// Insert a header, overwriting any previous value with the same key + pub fn insert_header( + &mut self, + key: header::HeaderName, + value: header::HeaderValue, + ) { + self.headers.insert(key, value); + } + /// Append a header, leaving any header with the same key intact + pub fn append_header( + &mut self, + key: header::HeaderName, + value: header::HeaderValue, + ) { + self.headers.append(key, value); + } +} + +/// Allows you to override details of the HTTP response like the status code and add Headers/Cookies. +#[derive(Debug, Clone, Default)] +pub struct ResponseOptions(pub Arc>); + +impl ResponseOptions { + /// A simpler way to overwrite the contents of `ResponseOptions` with a new `ResponseParts`. + pub fn overwrite(&self, parts: ResponseParts) { + let mut writable = self.0.write(); + *writable = parts + } + /// Set the status of the returned Response. + pub fn set_status(&self, status: StatusCode) { + let mut writeable = self.0.write(); + let res_parts = &mut *writeable; + res_parts.status = Some(status); + } + /// Insert a header, overwriting any previous value with the same key. + pub fn insert_header( + &self, + key: header::HeaderName, + value: header::HeaderValue, + ) { + let mut writeable = self.0.write(); + let res_parts = &mut *writeable; + res_parts.headers.insert(key, value); + } + /// Append a header, leaving any header with the same key intact. + pub fn append_header( + &self, + key: header::HeaderName, + value: header::HeaderValue, + ) { + let mut writeable = self.0.write(); + let res_parts = &mut *writeable; + res_parts.headers.append(key, value); + } +} +/// We're creating this type because Pavex doesn't provide one, and this seems +/// easier than trying to convert into the other ones +#[derive(Debug, Clone)] +pub struct PavexRequest{ +head: RequestHead, +body: RawIncomingBody, +} + +impl PavexRequest{ + // Tell Pavex how to create this type + pub fn extract(head: &RequestHead, body: RawIncomingBody) -> PavexRequest{ + PavexRequest { head, body } + } +} + +/// Provides an easy way to redirect the user from within a server function. Mimicking the Remix `redirect()`, +/// it sets a [StatusCode] of 302 and a [LOCATION](header::LOCATION) header with the provided value. +/// If looking to redirect from the client, `leptos_router::use_navigate()` should be used instead. +#[tracing::instrument(level = "trace", fields(error), skip_all)] +pub fn redirect(path: &str) { + if let Some(response_options) = use_context::() { + response_options.set_status(StatusCode::FOUND); + response_options.insert_header( + header::LOCATION, + header::HeaderValue::from_str(path) + .expect("Failed to create HeaderValue"), + ); + } +} + +/// An Actix [struct@Route](actix_web::Route) that listens for a `POST` request with +/// Leptos server function arguments in the body, runs the server function if found, +/// and returns the resulting [HttpResponse]. +/// +/// This can then be set up at an appropriate route in your application: +/// +/// ``` +/// use actix_web::*; +/// +/// fn register_server_functions() { +/// // call ServerFn::register() for each of the server functions you've defined +/// } +/// +/// # if false { // don't actually try to run a server in a doctest... +/// #[actix_web::main] +/// async fn main() -> std::io::Result<()> { +/// // make sure you actually register your server functions +/// register_server_functions(); +/// +/// HttpServer::new(|| { +/// App::new() +/// // "/api" should match the prefix, if any, declared when defining server functions +/// // {tail:.*} passes the remainder of the URL as the server function name +/// .route("/api/{tail:.*}", leptos_actix::handle_server_fns()) +/// }) +/// .bind(("127.0.0.1", 8080))? +/// .run() +/// .await +/// } +/// # } +/// ``` +/// +/// ## Provided Context Types +/// This function always provides context values including the following types: +/// - [ResponseOptions] +/// - [HttpRequest](actix_web::HttpRequest) +#[tracing::instrument(level = "trace", fields(error), skip_all)] +pub fn handle_server_fns() -> Route { + handle_server_fns_with_context(|| {}) +} + +/// An Actix [struct@Route](actix_web::Route) that listens for `GET` or `POST` requests with +/// Leptos server function arguments in the URL (`GET`) or body (`POST`), +/// runs the server function if found, and returns the resulting [HttpResponse]. +/// +/// This can then be set up at an appropriate route in your application: +/// +/// This version allows you to pass in a closure that adds additional route data to the +/// context, allowing you to pass in info about the route or user from Actix, or other info. +/// +/// **NOTE**: If your server functions expect a context, make sure to provide it both in +/// [`handle_server_fns_with_context`] **and** in [`leptos_routes_with_context`] (or whatever +/// rendering method you are using). During SSR, server functions are called by the rendering +/// method, while subsequent calls from the client are handled by the server function handler. +/// The same context needs to be provided to both handlers. +/// +/// ## Provided Context Types +/// This function always provides context values including the following types: +/// - [ResponseOptions] +/// - [HttpRequest](actix_web::HttpRequest) +#[tracing::instrument(level = "trace", fields(error), skip_all)] +pub fn handle_server_fns_with_context( + additional_context: impl Fn() + 'static + Clone + Send, +) -> Route { + web::to( + move |req: HttpRequest, params: web::Path, body: web::Bytes| { + let additional_context = additional_context.clone(); + async move { + let additional_context = additional_context.clone(); + + let path = params.into_inner(); + let accept_header = req + .headers() + .get("Accept") + .and_then(|value| value.to_str().ok()); + + if let Some(server_fn) = server_fn_by_path(path.as_str()) { + let body_ref: &[u8] = &body; + + let runtime = create_runtime(); + + // Add additional info to the context of the server function + additional_context(); + let res_options = ResponseOptions::default(); + + // provide HttpRequest as context in server scope + provide_context(req.clone()); + provide_context(res_options.clone()); + + // we consume the body here (using the web::Bytes extractor), but it is required for things + // like MultipartForm + if req + .headers() + .get("Content-Type") + .and_then(|value| value.to_str().ok()) + .map(|value| { + value.starts_with("multipart/form-data; boundary=") + }) + == Some(true) + { + provide_context(body.clone()); + } + + let query = req.query_string().as_bytes(); + + let data = match &server_fn.encoding() { + Encoding::Url | Encoding::Cbor => body_ref, + Encoding::GetJSON | Encoding::GetCBOR => query, + }; + + let res = match server_fn.call((), data).await { + Ok(serialized) => { + let res_options = + use_context::().unwrap(); + + let mut res: HttpResponseBuilder = + HttpResponse::Ok(); + let res_parts = res_options.0.write(); + + // if accept_header isn't set to one of these, it's a form submit + // redirect back to the referrer if not redirect has been set + if accept_header != Some("application/json") + && accept_header + != Some("application/x-www-form-urlencoded") + && accept_header != Some("application/cbor") + { + // Location will already be set if redirect() has been used + let has_location_set = + res_parts.headers.get("Location").is_some(); + if !has_location_set { + let referer = req + .headers() + .get("Referer") + .and_then(|value| value.to_str().ok()) + .unwrap_or("/"); + res = HttpResponse::SeeOther(); + res.insert_header(("Location", referer)) + .content_type("application/json"); + } + }; + // Override StatusCode if it was set in a Resource or Element + if let Some(status) = res_parts.status { + res.status(status); + } + + // Use provided ResponseParts headers if they exist + let _count = res_parts + .headers + .clone() + .into_iter() + .map(|(k, v)| { + res.append_header((k, v)); + }) + .count(); + + match serialized { + Payload::Binary(data) => { + res.content_type("application/cbor"); + res.body(Bytes::from(data)) + } + Payload::Url(data) => { + res.content_type( + "application/x-www-form-urlencoded", + ); + res.body(data) + } + Payload::Json(data) => { + res.content_type("application/json"); + res.body(data) + } + } + } + Err(e) => HttpResponse::InternalServerError().body( + serde_json::to_string(&e) + .unwrap_or_else(|_| e.to_string()), + ), + }; + // clean up the scope + runtime.dispose(); + res + } else { + HttpResponse::BadRequest().body(format!( + "Could not find a server function at the route {:?}. \ + \n\nIt's likely that either + 1. The API prefix you specify in the `#[server]` \ + macro doesn't match the prefix at which your server \ + function handler is mounted, or \n2. You are on a \ + platform that doesn't support automatic server \ + function registration and you need to call \ + ServerFn::register_explicit() on the server function \ + type, somewhere in your `main` function.", + req.path() + )) + } + } + }, + ) +} + +/// Returns an Actix [struct@Route](actix_web::Route) that listens for a `GET` request and tries +/// to route it using [leptos_router], serving an HTML stream of your application. The stream +/// will include fallback content for any `` nodes, and be immediately interactive, +/// but requires some client-side JavaScript. +/// +/// The provides a [MetaContext] and a [RouterIntegrationContext] to app’s context before +/// rendering it, and includes any meta tags injected using [leptos_meta]. +/// +/// The HTML stream is rendered using [render_to_stream](leptos::ssr::render_to_stream), and +/// includes everything described in the documentation for that function. +/// +/// This can then be set up at an appropriate route in your application: +/// ``` +/// use actix_web::{App, HttpServer}; +/// use leptos::*; +/// use leptos_router::Method; +/// use std::{env, net::SocketAddr}; +/// +/// #[component] +/// fn MyApp() -> impl IntoView { +/// view! {
"Hello, world!"
} +/// } +/// +/// # if false { // don't actually try to run a server in a doctest... +/// #[actix_web::main] +/// async fn main() -> std::io::Result<()> { +/// let conf = get_configuration(Some("Cargo.toml")).await.unwrap(); +/// let addr = conf.leptos_options.site_addr.clone(); +/// HttpServer::new(move || { +/// let leptos_options = &conf.leptos_options; +/// +/// App::new() +/// // {tail:.*} passes the remainder of the URL as the route +/// // the actual routing will be handled by `leptos_router` +/// .route( +/// "/{tail:.*}", +/// leptos_actix::render_app_to_stream( +/// leptos_options.to_owned(), +/// || view! { }, +/// Method::Get, +/// ), +/// ) +/// }) +/// .bind(&addr)? +/// .run() +/// .await +/// } +/// # } +/// ``` +/// +/// ## Provided Context Types +/// This function always provides context values including the following types: +/// - [ResponseOptions] +/// - [HttpRequest](actix_web::HttpRequest) +/// - [MetaContext](leptos_meta::MetaContext) +/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext) +#[tracing::instrument(level = "trace", fields(error), skip_all)] +pub fn render_app_to_stream( + options: LeptosOptions, + app_fn: impl Fn() -> IV + Clone + 'static, + method: Method, +) -> Route +where + IV: IntoView, +{ + render_app_to_stream_with_context(options, || {}, app_fn, method) +} + +/// Returns an Actix [struct@Route](actix_web::Route) that listens for a `GET` request and tries +/// to route it using [leptos_router], serving an in-order HTML stream of your application. +/// This stream will pause at each `` node and wait for it to resolve before +/// sending down its HTML. The app will become interactive once it has fully loaded. +/// +/// The provides a [MetaContext] and a [RouterIntegrationContext] to app’s context before +/// rendering it, and includes any meta tags injected using [leptos_meta]. +/// +/// The HTML stream is rendered using +/// [render_to_stream_in_order](leptos::ssr::render_to_stream_in_order), +/// and includes everything described in the documentation for that function. +/// +/// This can then be set up at an appropriate route in your application: +/// ``` +/// use actix_web::{App, HttpServer}; +/// use leptos::*; +/// use leptos_router::Method; +/// use std::{env, net::SocketAddr}; +/// +/// #[component] +/// fn MyApp() -> impl IntoView { +/// view! {
"Hello, world!"
} +/// } +/// +/// # if false { // don't actually try to run a server in a doctest... +/// #[actix_web::main] +/// async fn main() -> std::io::Result<()> { +/// let conf = get_configuration(Some("Cargo.toml")).await.unwrap(); +/// let addr = conf.leptos_options.site_addr.clone(); +/// HttpServer::new(move || { +/// let leptos_options = &conf.leptos_options; +/// +/// App::new() +/// // {tail:.*} passes the remainder of the URL as the route +/// // the actual routing will be handled by `leptos_router` +/// .route( +/// "/{tail:.*}", +/// leptos_actix::render_app_to_stream_in_order( +/// leptos_options.to_owned(), +/// || view! { }, +/// Method::Get, +/// ), +/// ) +/// }) +/// .bind(&addr)? +/// .run() +/// .await +/// } +/// # } +/// ``` +/// +/// ## Provided Context Types +/// This function always provides context values including the following types: +/// - [ResponseOptions] +/// - [HttpRequest](actix_web::HttpRequest) +/// - [MetaContext](leptos_meta::MetaContext) +/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext) +#[tracing::instrument(level = "trace", fields(error), skip_all)] +pub fn render_app_to_stream_in_order( + options: LeptosOptions, + app_fn: impl Fn() -> IV + Clone + 'static, + method: Method, +) -> Route +where + IV: IntoView, +{ + render_app_to_stream_in_order_with_context(options, || {}, app_fn, method) +} + +/// Returns an Actix [struct@Route](actix_web::Route) that listens for a `GET` request and tries +/// to route it using [leptos_router], asynchronously rendering an HTML page after all +/// `async` [Resource](leptos::Resource)s have loaded. +/// +/// The provides a [MetaContext] and a [RouterIntegrationContext] to the app’s context before +/// rendering it, and includes any meta tags injected using [leptos_meta]. +/// +/// The HTML stream is rendered using [render_to_string_async](leptos::ssr::render_to_string_async), and +/// includes everything described in the documentation for that function. +/// +/// This can then be set up at an appropriate route in your application: +/// ``` +/// use actix_web::{App, HttpServer}; +/// use leptos::*; +/// use leptos_router::Method; +/// use std::{env, net::SocketAddr}; +/// +/// #[component] +/// fn MyApp() -> impl IntoView { +/// view! {
"Hello, world!"
} +/// } +/// +/// # if false { // don't actually try to run a server in a doctest... +/// #[actix_web::main] +/// async fn main() -> std::io::Result<()> { +/// let conf = get_configuration(Some("Cargo.toml")).await.unwrap(); +/// let addr = conf.leptos_options.site_addr.clone(); +/// HttpServer::new(move || { +/// let leptos_options = &conf.leptos_options; +/// +/// App::new() +/// // {tail:.*} passes the remainder of the URL as the route +/// // the actual routing will be handled by `leptos_router` +/// .route( +/// "/{tail:.*}", +/// leptos_actix::render_app_async( +/// leptos_options.to_owned(), +/// || view! { }, +/// Method::Get, +/// ), +/// ) +/// }) +/// .bind(&addr)? +/// .run() +/// .await +/// } +/// # } +/// ``` +/// +/// ## Provided Context Types +/// This function always provides context values including the following types: +/// - [ResponseOptions] +/// - [HttpRequest](actix_web::HttpRequest) +/// - [MetaContext](leptos_meta::MetaContext) +/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext) +#[tracing::instrument(level = "trace", fields(error), skip_all)] +pub fn render_app_async( + options: LeptosOptions, + app_fn: impl Fn() -> IV + Clone + 'static, + method: Method, +) -> Route +where + IV: IntoView, +{ + render_app_async_with_context(options, || {}, app_fn, method) +} + +/// Returns an Actix [struct@Route] that listens for a `GET` request and tries +/// to route it using [leptos_router], serving an HTML stream of your application. +/// +/// This function allows you to provide additional information to Leptos for your route. +/// It could be used to pass in Path Info, Connection Info, or anything your heart desires. +/// +/// ## Provided Context Types +/// This function always provides context values including the following types: +/// - [ResponseOptions] +/// - [HttpRequest](actix_web::HttpRequest) +/// - [MetaContext](leptos_meta::MetaContext) +/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext) +#[tracing::instrument(level = "trace", fields(error), skip_all)] +pub fn render_app_to_stream_with_context( + options: LeptosOptions, + additional_context: impl Fn() + 'static + Clone + Send, + app_fn: impl Fn() -> IV + Clone + 'static, + method: Method, +) -> Route +where + IV: IntoView, +{ + render_app_to_stream_with_context_and_replace_blocks( + options, + additional_context, + app_fn, + method, + false, + ) +} + +/// Returns an Actix [struct@Route](actix_web::Route) that listens for a `GET` request and tries +/// to route it using [leptos_router], serving an HTML stream of your application. +/// +/// This function allows you to provide additional information to Leptos for your route. +/// It could be used to pass in Path Info, Connection Info, or anything your heart desires. +/// +/// `replace_blocks` additionally lets you specify whether `` fragments that read +/// from blocking resources should be retrojected into the HTML that's initially served, rather +/// than dynamically inserting them with JavaScript on the client. This means you will have +/// better support if JavaScript is not enabled, in exchange for a marginally slower response time. +/// +/// ## Provided Context Types +/// This function always provides context values including the following types: +/// - [ResponseOptions] +/// - [HttpRequest](actix_web::HttpRequest) +/// - [MetaContext](leptos_meta::MetaContext) +/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext) +#[tracing::instrument(level = "trace", fields(error), skip_all)] +pub fn render_app_to_stream_with_context_and_replace_blocks( + options: LeptosOptions, + additional_context: impl Fn() + 'static + Clone + Send, + app_fn: impl Fn() -> IV + Clone + 'static, + method: Method, + replace_blocks: bool, +) -> Route +where + IV: IntoView, +{ + let handler = move |req: HttpRequest| { + let options = options.clone(); + let app_fn = app_fn.clone(); + let additional_context = additional_context.clone(); + let res_options = ResponseOptions::default(); + + async move { + let app = { + let app_fn = app_fn.clone(); + let res_options = res_options.clone(); + move || { + provide_contexts(&req, res_options); + (app_fn)().into_view() + } + }; + + stream_app( + &options, + app, + res_options, + additional_context, + replace_blocks, + ) + .await + } + }; + match method { + Method::Get => web::get().to(handler), + Method::Post => web::post().to(handler), + Method::Put => web::put().to(handler), + Method::Delete => web::delete().to(handler), + Method::Patch => web::patch().to(handler), + } +} + +/// Returns an Actix [struct@Route](actix_web::Route) that listens for a `GET` request and tries +/// to route it using [leptos_router], serving an in-order HTML stream of your application. +/// +/// This function allows you to provide additional information to Leptos for your route. +/// It could be used to pass in Path Info, Connection Info, or anything your heart desires. +/// +/// ## Provided Context Types +/// This function always provides context values including the following types: +/// - [ResponseOptions] +/// - [HttpRequest](actix_web::HttpRequest) +/// - [MetaContext](leptos_meta::MetaContext) +/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext) +#[tracing::instrument(level = "trace", fields(error), skip_all)] +pub fn render_app_to_stream_in_order_with_context( + options: LeptosOptions, + additional_context: impl Fn() + 'static + Clone + Send, + app_fn: impl Fn() -> IV + Clone + 'static, + method: Method, +) -> Route +where + IV: IntoView, +{ + let handler = move |req: HttpRequest| { + let options = options.clone(); + let app_fn = app_fn.clone(); + let additional_context = additional_context.clone(); + let res_options = ResponseOptions::default(); + + async move { + let app = { + let app_fn = app_fn.clone(); + let res_options = res_options.clone(); + move || { + provide_contexts(&req, res_options); + (app_fn)().into_view() + } + }; + + stream_app_in_order(&options, app, res_options, additional_context) + .await + } + }; + match method { + Method::Get => web::get().to(handler), + Method::Post => web::post().to(handler), + Method::Put => web::put().to(handler), + Method::Delete => web::delete().to(handler), + Method::Patch => web::patch().to(handler), + } +} + +/// Returns an Actix [struct@Route](actix_web::Route) that listens for a `GET` request and tries +/// to route it using [leptos_router], asynchronously serving the page once all `async` +/// [Resource](leptos::Resource)s have loaded. +/// +/// This function allows you to provide additional information to Leptos for your route. +/// It could be used to pass in Path Info, Connection Info, or anything your heart desires. +/// +/// ## Provided Context Types +/// This function always provides context values including the following types: +/// - [ResponseOptions] +/// - [HttpRequest](actix_web::HttpRequest) +/// - [MetaContext](leptos_meta::MetaContext) +/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext) +#[tracing::instrument(level = "trace", fields(error), skip_all)] +pub fn render_app_async_with_context( + options: LeptosOptions, + additional_context: impl Fn() + 'static + Clone + Send, + app_fn: impl Fn() -> IV + Clone + 'static, + method: Method, +) -> Route +where + IV: IntoView, +{ + let handler = move |req: HttpRequest| { + let options = options.clone(); + let app_fn = app_fn.clone(); + let additional_context = additional_context.clone(); + let res_options = ResponseOptions::default(); + + async move { + let app = { + let app_fn = app_fn.clone(); + let res_options = res_options.clone(); + move || { + provide_contexts(&req, res_options); + (app_fn)().into_view() + } + }; + + render_app_async_helper( + &options, + app, + res_options, + additional_context, + ) + .await + } + }; + match method { + Method::Get => web::get().to(handler), + Method::Post => web::post().to(handler), + Method::Put => web::put().to(handler), + Method::Delete => web::delete().to(handler), + Method::Patch => web::patch().to(handler), + } +} + +#[tracing::instrument(level = "trace", fields(error), skip_all)] +fn provide_contexts(req: &HttpRequest, res_options: ResponseOptions) { + let path = leptos_corrected_path(req); + + let integration = ServerIntegration { path }; + provide_context(RouterIntegrationContext::new(integration)); + provide_context(MetaContext::new()); + provide_context(res_options); + provide_context(req.clone()); + provide_server_redirect(redirect); + #[cfg(feature = "nonce")] + leptos::nonce::provide_nonce(); +} + +fn leptos_corrected_path(req: &HttpRequest) -> String { + let path = req.path(); + let query = req.query_string(); + if query.is_empty() { + "http://leptos".to_string() + path + } else { + "http://leptos".to_string() + path + "?" + query + } +} +#[tracing::instrument(level = "trace", fields(error), skip_all)] +async fn stream_app( + options: &LeptosOptions, + app: impl FnOnce() -> View + 'static, + res_options: ResponseOptions, + additional_context: impl Fn() + 'static + Clone + Send, + replace_blocks: bool, +) -> Response { + let (stream, runtime) = + render_to_stream_with_prefix_undisposed_with_context_and_block_replacement( + app, + move || generate_head_metadata_separated().1.into(), + additional_context, + replace_blocks + ); + + build_stream_response(options, res_options, stream, runtime).await +} +#[cfg_attr( + any(debug_assertions, feature = "ssr"), + instrument(level = "trace", skip_all,) +)] +async fn stream_app_in_order( + options: &LeptosOptions, + app: impl FnOnce() -> View + 'static, + res_options: ResponseOptions, + additional_context: impl Fn() + 'static + Clone + Send, +) -> Response { + let (stream, runtime) = + leptos::ssr::render_to_stream_in_order_with_prefix_undisposed_with_context( + app, + move || { + generate_head_metadata_separated().1.into() + }, + additional_context, + ); + + build_stream_response(options, res_options, stream, runtime).await +} +#[tracing::instrument(level = "trace", fields(error), skip_all)] +async fn build_stream_response( + options: &LeptosOptions, + res_options: ResponseOptions, + stream: impl Stream + 'static, + runtime: RuntimeId, +) -> Response { + let mut stream = Box::pin(stream); + + // wait for any blocking resources to load before pulling metadata + let first_app_chunk = stream.next().await.unwrap_or_default(); + + let (head, tail) = + html_parts_separated(options, use_context::().as_ref()); + + let mut stream = Box::pin( + futures::stream::once(async move { head.clone() }) + .chain( + futures::stream::once(async move { first_app_chunk }) + .chain(stream), + ) + .map(|html| Ok(web::Bytes::from(html)) as Result), + ); + + // Get the first and second in the stream, which renders the app shell, and thus allows Resources to run + + let first_chunk = stream.next().await; + + let second_chunk = stream.next().await; + + let res_options = res_options.0.read(); + + let (status, headers) = (res_options.status, res_options.headers.clone()); + let status = status.unwrap_or_default(); + + let complete_stream = + futures::stream::iter([first_chunk.unwrap(), second_chunk.unwrap()]) + .chain(stream) + .chain( + futures::stream::once(async move { + runtime.dispose(); + tail.to_string() + }) + .map(|html| Ok(web::Bytes::from(html)) as Result), + ); + let mut res = Response::ok() + .content_type("text/html") + .streaming(complete_stream); + + // Add headers manipulated in the response + for (key, value) in headers.into_iter() { + res.append_header(key, value); + } + + // Set status to what is returned in the function + res.set_status(status); + // Return the response + res +} +#[tracing::instrument(level = "trace", fields(error), skip_all)] +async fn render_app_async_helper( + options: &LeptosOptions, + app: impl FnOnce() -> View + 'static, + res_options: ResponseOptions, + additional_context: impl Fn() + 'static + Clone + Send, +) -> Response { + let (stream, runtime) = + leptos::ssr::render_to_stream_in_order_with_prefix_undisposed_with_context( + app, + move || "".into(), + additional_context, + ); + + let html = build_async_response(stream, options, runtime).await; + + let res_options = res_options.0.read(); + + let (status, headers) = (res_options.status, res_options.headers.clone()); + let status = status.unwrap_or_default(); + + let mut res = Response::ok().set_typed_body(html); + + // Add headers manipulated in the response + for (key, value) in headers.into_iter() { + res.headers_mut().append(key, value); + } + + // Set status to what is returned in the function + res.set_status(status); + // Return the response + res +} + +/// Generates a list of all routes defined in Leptos's Router in your app. We can then use this to automatically +/// create routes in Actix's App without having to use wildcard matching or fallbacks. Takes in your root app Element +/// as an argument so it can walk you app tree. This version is tailored to generated Actix compatible paths. +pub fn generate_route_list( + app_fn: impl Fn() -> IV + 'static + Clone, +) -> Vec +where + IV: IntoView + 'static, +{ + generate_route_list_with_exclusions_and_ssg(app_fn, None).0 +} + +/// Generates a list of all routes defined in Leptos's Router in your app. We can then use this to automatically +/// create routes in Actix's App without having to use wildcard matching or fallbacks. Takes in your root app Element +/// as an argument so it can walk you app tree. This version is tailored to generated Actix compatible paths. +pub fn generate_route_list_with_ssg( + app_fn: impl Fn() -> IV + 'static + Clone, +) -> (Vec, StaticDataMap) +where + IV: IntoView + 'static, +{ + generate_route_list_with_exclusions_and_ssg(app_fn, None) +} + +/// Generates a list of all routes defined in Leptos's Router in your app. We can then use this to automatically +/// create routes in Actix's App without having to use wildcard matching or fallbacks. Takes in your root app Element +/// as an argument so it can walk you app tree. This version is tailored to generated Actix compatible paths. Adding excluded_routes +/// to this function will stop `.leptos_routes()` from generating a route for it, allowing a custom handler. These need to be in Actix path format +pub fn generate_route_list_with_exclusions( + app_fn: impl Fn() -> IV + 'static + Clone, + excluded_routes: Option>, +) -> Vec +where + IV: IntoView + 'static, +{ + generate_route_list_with_exclusions_and_ssg(app_fn, excluded_routes).0 +} + +/// Generates a list of all routes defined in Leptos's Router in your app. We can then use this to automatically +/// create routes in Actix's App without having to use wildcard matching or fallbacks. Takes in your root app Element +/// as an argument so it can walk you app tree. This version is tailored to generated Actix compatible paths. Adding excluded_routes +/// to this function will stop `.leptos_routes()` from generating a route for it, allowing a custom handler. These need to be in Actix path format +pub fn generate_route_list_with_exclusions_and_ssg( + app_fn: impl Fn() -> IV + 'static + Clone, + excluded_routes: Option>, +) -> (Vec, StaticDataMap) +where + IV: IntoView + 'static, +{ + generate_route_list_with_exclusions_and_ssg_and_context( + app_fn, + excluded_routes, + || {}, + ) +} + +/// Generates a list of all routes defined in Leptos's Router in your app. We can then use this to automatically +/// create routes in Actix's App without having to use wildcard matching or fallbacks. Takes in your root app Element +/// as an argument so it can walk you app tree. This version is tailored to generated Actix compatible paths. Adding excluded_routes +/// to this function will stop `.leptos_routes()` from generating a route for it, allowing a custom handler. These need to be in Actix path format. +/// Additional context will be provided to the app Element. +pub fn generate_route_list_with_exclusions_and_ssg_and_context( + app_fn: impl Fn() -> IV + 'static + Clone, + excluded_routes: Option>, + additional_context: impl Fn() + 'static + Clone, +) -> (Vec, StaticDataMap) +where + IV: IntoView + 'static, +{ + let (mut routes, static_data_map) = + leptos_router::generate_route_list_inner_with_context( + app_fn, + additional_context, + ); + + // Actix's Router doesn't follow Leptos's + // Match `*` or `*someword` to replace with replace it with "/{tail.*} + let wildcard_re = Regex::new(r"\*.*").unwrap(); + // Match `:some_word` but only capture `some_word` in the groups to replace with `{some_word}` + let capture_re = Regex::new(r":((?:[^.,/]+)+)[^/]?").unwrap(); + + // Empty strings screw with Actix pathing, they need to be "/" + routes = routes + .into_iter() + .map(|listing| { + let path = listing.path(); + if path.is_empty() { + return RouteListing::new( + "/".to_string(), + listing.path(), + listing.mode(), + listing.methods(), + listing.static_mode(), + ); + } + RouteListing::new( + listing.path(), + listing.path(), + listing.mode(), + listing.methods(), + listing.static_mode(), + ) + }) + .map(|listing| { + let path = wildcard_re + .replace_all(listing.path(), "{tail:.*}") + .to_string(); + let path = capture_re.replace_all(&path, "{$1}").to_string(); + RouteListing::new( + path, + listing.path(), + listing.mode(), + listing.methods(), + listing.static_mode(), + ) + }) + .collect::>(); + + ( + if routes.is_empty() { + vec![RouteListing::new( + "/", + "", + Default::default(), + [Method::Get], + None, + )] + } else { + // Routes to exclude from auto generation + if let Some(excluded_routes) = excluded_routes { + routes + .retain(|p| !excluded_routes.iter().any(|e| e == p.path())) + } + routes + }, + static_data_map, + ) +} + +//pub enum DataResponse { +// Data(T), +// Response(Response), +//} + +//fn handle_static_response<'a, IV>( +// path: &'a str, +// options: &'a LeptosOptions, +// app_fn: &'a (impl Fn() -> IV + Clone + Send + 'static), +// additional_context: &'a (impl Fn() + 'static + Clone + Send), +// res: StaticResponse, +//) -> Pin + 'a>> +//where +// IV: IntoView + 'static, +//{ +// Box::pin(async move { +// match res { +// StaticResponse::ReturnResponse { +// body, +// status, +// content_type, +// } => { +// let mut res = Response::new(match status { +// StaticStatusCode::Ok => StatusCode::OK, +// StaticStatusCode::NotFound => StatusCode::NOT_FOUND, +// StaticStatusCode::InternalServerError => { +// StatusCode::INTERNAL_SERVER_ERROR +// } +// }); +// if let Some(v) = content_type { +// res.headers_mut().insert( +// HeaderName::from_static("content-type"), +// HeaderValue::from_static(v), +// ); +// } +// res.set_body(body) +// } +// StaticResponse::RenderDynamic => { +// handle_static_response( +// path, +// options, +// app_fn, +// additional_context, +// render_dynamic( +// path, +// options, +// app_fn.clone(), +// additional_context.clone(), +// ) +// .await, +// ) +// .await +// } +// StaticResponse::RenderNotFound => { +// handle_static_response( +// path, +// options, +// app_fn, +// additional_context, +// not_found_page( +// tokio::fs::read_to_string(not_found_path(options)) +// .await, +// ), +// ) +// .await +// } +// StaticResponse::WriteFile { body, path } => { +// if let Some(path) = path.parent() { +// if let Err(e) = std::fs::create_dir_all(path) { +// tracing::error!( +// "encountered error {} writing directories {}", +// e, +// path.display() +// ); +// } +// } +// if let Err(e) = std::fs::write(&path, &body) { +// tracing::error!( +// "encountered error {} writing file {}", +// e, +// path.display() +// ); +// } +// handle_static_response( +// path.to_str().unwrap(), +// options, +// app_fn, +// additional_context, +// StaticResponse::ReturnResponse { +// body, +// status: StaticStatusCode::Ok, +// content_type: Some("text/html"), +// }, +// ) +// .await +// } +// } +// }) +//} +// +//fn static_route( +// options: LeptosOptions, +// app_fn: impl Fn() -> IV + Clone + Send + 'static, +// additional_context: impl Fn() + 'static + Clone + Send, +// method: Method, +// mode: StaticMode, +//) -> Route +//where +// IV: IntoView + 'static, +//{ +// match mode { +// StaticMode::Incremental => { +// let handler = move |req: HttpRequest| { +// Box::pin({ +// let options = options.clone(); +// let app_fn = app_fn.clone(); +// let additional_context = additional_context.clone(); +// async move { +// handle_static_response( +// req.path(), +// &options, +// &app_fn, +// &additional_context, +// incremental_static_route( +// tokio::fs::read_to_string(static_file_path( +// &options, +// req.path(), +// )) +// .await, +// ), +// ) +// .await +// } +// }) +// }; +// match method { +// Method::Get => web::get().to(handler), +// Method::Post => web::post().to(handler), +// Method::Put => web::put().to(handler), +// Method::Delete => web::delete().to(handler), +// Method::Patch => web::patch().to(handler), +// } +// } +// StaticMode::Upfront => { +// let handler = move |req: HttpRequest| { +// Box::pin({ +// let options = options.clone(); +// let app_fn = app_fn.clone(); +// let additional_context = additional_context.clone(); +// async move { +// handle_static_response( +// req.path(), +// &options, +// &app_fn, +// &additional_context, +// upfront_static_route( +// tokio::fs::read_to_string(static_file_path( +// &options, +// req.path(), +// )) +// .await, +// ), +// ) +// .await +// } +// }) +// }; +// match method { +// Method::Get => web::get().to(handler), +// Method::Post => web::post().to(handler), +// Method::Put => web::put().to(handler), +// Method::Delete => web::delete().to(handler), +// Method::Patch => web::patch().to(handler), +// } +// } +// } +//} + +/// This trait allows one to pass a list of routes and a render function to Actix's router, letting us avoid +/// having to use wildcards or manually define all routes in multiple places. +pub trait LeptosRoutes { + fn leptos_routes( + self, + options: LeptosOptions, + paths: Vec, + app_fn: impl Fn() -> IV + Clone + Send + 'static, + ) -> Self + where + IV: IntoView + 'static; + + fn leptos_routes_with_context( + self, + options: LeptosOptions, + paths: Vec, + additional_context: impl Fn() + 'static + Clone + Send, + app_fn: impl Fn() -> IV + Clone + Send + 'static, + ) -> Self + where + IV: IntoView + 'static; +} + +/// The default implementation of `LeptosRoutes` which takes in a list of paths, and dispatches GET requests +/// to those paths to Leptos's renderer. +impl LeptosRoutes for actix_web::App +where + T: ServiceFactory< + ServiceRequest, + Config = (), + Error = Error, + InitError = (), + >, +{ + #[tracing::instrument(level = "trace", fields(error), skip_all)] + fn leptos_routes( + self, + options: LeptosOptions, + paths: Vec, + app_fn: impl Fn() -> IV + Clone + Send + 'static, + ) -> Self + where + IV: IntoView + 'static, + { + self.leptos_routes_with_context(options, paths, || {}, app_fn) + } + + #[tracing::instrument(level = "trace", fields(error), skip_all)] + fn leptos_routes_with_context( + self, + options: LeptosOptions, + paths: Vec, + additional_context: impl Fn() + 'static + Clone + Send, + app_fn: impl Fn() -> IV + Clone + Send + 'static, + ) -> Self + where + IV: IntoView + 'static, + { + let mut router = self; + for listing in paths.iter() { + let path = listing.path(); + let mode = listing.mode(); + + for method in listing.methods() { + router = if let Some(static_mode) = listing.static_mode() { + router.route( + path, + static_route( + options.clone(), + app_fn.clone(), + additional_context.clone(), + method, + static_mode, + ), + ) + } else { + router.route( + path, + match mode { + SsrMode::OutOfOrder => { + render_app_to_stream_with_context( + options.clone(), + additional_context.clone(), + app_fn.clone(), + method, + ) + } + SsrMode::PartiallyBlocked => { + render_app_to_stream_with_context_and_replace_blocks( + options.clone(), + additional_context.clone(), + app_fn.clone(), + method, + true, + ) + } + SsrMode::InOrder => { + render_app_to_stream_in_order_with_context( + options.clone(), + additional_context.clone(), + app_fn.clone(), + method, + ) + } + SsrMode::Async => render_app_async_with_context( + options.clone(), + additional_context.clone(), + app_fn.clone(), + method, + ), + }, + ) + }; + } + } + router + } +} + +/// The default implementation of `LeptosRoutes` which takes in a list of paths, and dispatches GET requests +/// to those paths to Leptos's renderer. +impl LeptosRoutes for &mut ServiceConfig { + #[tracing::instrument(level = "trace", fields(error), skip_all)] + fn leptos_routes( + self, + options: LeptosOptions, + paths: Vec, + app_fn: impl Fn() -> IV + Clone + Send + 'static, + ) -> Self + where + IV: IntoView + 'static, + { + self.leptos_routes_with_context(options, paths, || {}, app_fn) + } + + #[tracing::instrument(level = "trace", fields(error), skip_all)] + fn leptos_routes_with_context( + self, + options: LeptosOptions, + paths: Vec, + additional_context: impl Fn() + 'static + Clone + Send, + app_fn: impl Fn() -> IV + Clone + Send + 'static, + ) -> Self + where + IV: IntoView + 'static, + { + let mut router = self; + for listing in paths.iter() { + let path = listing.path(); + let mode = listing.mode(); + + for method in listing.methods() { + router = router.route( + path, + match mode { + SsrMode::OutOfOrder => { + render_app_to_stream_with_context( + options.clone(), + additional_context.clone(), + app_fn.clone(), + method, + ) + } + SsrMode::PartiallyBlocked => { + render_app_to_stream_with_context_and_replace_blocks( + options.clone(), + additional_context.clone(), + app_fn.clone(), + method, + true, + ) + } + SsrMode::InOrder => { + render_app_to_stream_in_order_with_context( + options.clone(), + additional_context.clone(), + app_fn.clone(), + method, + ) + } + SsrMode::Async => render_app_async_with_context( + options.clone(), + additional_context.clone(), + app_fn.clone(), + method, + ), + }, + ); + } + } + router + } +} + +/// A helper to make it easier to use Actix extractors in server functions. This takes +/// a handler function as its argument. The handler follows similar rules to an Actix +/// [Handler]: it is an async function that receives arguments that +/// will be extracted from the request and returns some value. +/// +/// ```rust,ignore +/// use leptos::*; +/// use serde::Deserialize; +/// #[derive(Deserialize)] +/// struct Search { +/// q: String, +/// } +/// +/// #[server(ExtractoServerFn, "/api")] +/// pub async fn extractor_server_fn() -> Result { +/// use actix_web::dev::ConnectionInfo; +/// use actix_web::web::{Data, Query}; +/// +/// extract( +/// |data: Data, search: Query, connection: ConnectionInfo| async move { +/// format!( +/// "data = {}\nsearch = {}\nconnection = {:?}", +/// data.into_inner(), +/// search.q, +/// connection +/// ) +/// }, +/// ) +/// .await +/// } +/// ``` +//pub async fn extract( +// f: F, +//) -> Result<<>::Future as Future>::Output, ServerFnError> +//where +// F: Extractor, +// E: actix_web::FromRequest, +// ::Error: Display, +// >::Future: Future, +//{ +// let req = use_context::() +// .expect("HttpRequest should have been provided via context"); +// +// let input = if let Some(body) = use_context::() { +// let (_, mut payload) = actix_http::h1::Payload::create(false); +// payload.unread_data(body); +// E::from_request(&req, &mut dev::Payload::from(payload)) +// } else { +// E::extract(&req) +// } +// .await +// .map_err(|e| ServerFnError::ServerError(e.to_string()))?; +// +// Ok(f.call(input).await) +//} +// +///// A helper to make it easier to use Axum extractors in server functions, with a +///// simpler API than [`extract()`]. +///// +///// It is generic over some type `T` that implements [`FromRequest`] and can +///// therefore be used in an extractor. The compiler can often infer this type. +///// +///// Any error that occurs during extraction is converted to a [`ServerFnError`]. +///// +///// ```rust,ignore +///// // MyQuery is some type that implements `Deserialize + Serialize` +///// #[server] +///// pub async fn query_extract() -> Result { +///// use actix_web::web::Query; +///// use leptos_actix::*; +///// let Query(data) = extractor().await?; +///// Ok(data) +///// } +///// ``` +//pub async fn extractor() -> Result +//where +// T: actix_web::FromRequest, +// ::Error: Debug, +//{ +// let req = use_context::() +// .expect("HttpRequest should have been provided via context"); +// +// if let Some(body) = use_context::() { +// let (_, mut payload) = actix_http::h1::Payload::create(false); +// payload.unread_data(body); +// T::from_request(&req, &mut dev::Payload::from(payload)) +// } else { +// T::extract(&req) +// } +// .await +// .map_err(|e| ServerFnError::ServerError(format!("{e:?}"))) +//} +// +///// A macro that makes it easier to use extractors in server functions. The macro +///// takes a type or types, and extracts them from the request, returning from the +///// server function with an `Err(_)` if there is an error during extraction. +///// ```rust,ignore +///// let info = extract!(ConnectionInfo); +///// let Query(data) = extract!(Query); +///// let (info, Query(data)) = extract!(ConnectionInfo, Query); +///// ``` +//#[macro_export] +//macro_rules! extract { +// ($($x:ty),+) => { +// $crate::extract(|fields: ($($x),+)| async move { fields }).await? +// }; +//} +// +//// Drawn from the Actix Handler implementation +//// https://github.com/actix/actix-web/blob/19c9d858f25e8262e14546f430d713addb397e96/actix-web/src/handler.rs#L124 +//pub trait Extractor { +// type Future; +// +// fn call(self, args: T) -> Self::Future; +//} +// +//macro_rules! factory_tuple ({ $($param:ident)* } => { +// impl Extractor<($($param,)*)> for Func +// where +// Func: FnOnce($($param),*) -> Fut + Clone + 'static, +// Fut: Future, +// { +// type Future = Fut; +// +// #[inline] +// #[allow(non_snake_case)] +// fn call(self, ($($param,)*): ($($param,)*)) -> Self::Future { +// (self)($($param,)*) +// } +// } +//}); +// +//factory_tuple! {} +//factory_tuple! { A } +//factory_tuple! { A B } +//factory_tuple! { A B C } +//factory_tuple! { A B C D } +//factory_tuple! { A B C D E } +//factory_tuple! { A B C D E F } +//factory_tuple! { A B C D E F G } +//factory_tuple! { A B C D E F G H } +//factory_tuple! { A B C D E F G H I } +//factory_tuple! { A B C D E F G H I J } +//factory_tuple! { A B C D E F G H I J K } +//factory_tuple! { A B C D E F G H I J K L } +//factory_tuple! { A B C D E F G H I J K L M } +//factory_tuple! { A B C D E F G H I J K L M N } +//factory_tuple! { A B C D E F G H I J K L M N O } +//factory_tuple! { A B C D E F G H I J K L M N O P } diff --git a/server_fn/Cargo.toml b/server_fn/Cargo.toml index c85e6aa6c5..8e683fd263 100644 --- a/server_fn/Cargo.toml +++ b/server_fn/Cargo.toml @@ -1,42 +1,92 @@ [package] name = "server_fn" -version = { workspace = true } +version = "0.6.0" edition = "2021" -authors = ["Greg Johnston"] -license = "MIT" -repository = "https://github.com/leptos-rs/leptos" -description = "RPC for any web framework." -readme = "../README.md" [dependencies] -server_fn_macro_default = { workspace = true } -serde = { version = "1", features = ["derive"] } -serde_qs = "0.12" -thiserror = "1" -serde_json = "1" -quote = "1" -syn = { version = "2", features = ["full", "parsing", "extra-traits"] } -proc-macro2 = "1" -ciborium = "0.2" -xxhash-rust = { version = "0.8", features = ["const_xxh64"] } +# used for hashing paths in #[server] macro const_format = "0.2" -inventory = { version = "0.3", optional = true } -lazy_static = "1" - -[dev-dependencies] -server_fn = { version = "0.2" } -serde = "1" +xxhash-rust = { version = "0.8", features = ["const_xxh64"] } -[target.'cfg(target_arch = "wasm32")'.dependencies] -gloo-net = "0.2" -js-sys = "0.3" +# used across multiple featurs +serde = { version = "1", features = ["derive"] } +send_wrapper = { version = "0.6", features = ["futures"], optional = true } -[target.'cfg(not(target_arch = "wasm32"))'.dependencies] -reqwest = { version = "0.11", default-features = false } +# registration system +inventory = {version="0.3",optional=true} +dashmap = "5" once_cell = "1" +## servers +# actix +actix-web = { version = "4", optional = true } + +# axum +axum = { version = "0.7", optional = true, features = ["multipart"] } +tower = { version = "0.4", optional = true } +tower-layer = { version = "0.3", optional = true } + +## input encodings +serde_qs = { version = "0.12", optional = true } +multer = { version = "3", optional = true } + +## output encodings +# serde +serde_json = { version = "1", optional = true } +futures = "0.3" +http = { version = "1", optional = true } +ciborium = { version = "0.2", optional = true } +hyper = { version = "1", optional = true } +bytes = "1" +thiserror = "1" +http-body-util = { version = "0.1.0", optional = true } +rkyv = { version = "0.7", features = [ + "validation", + "uuid", + "strict", +], optional = true } + +# client +gloo-net = { version = "0.5", optional = true } +js-sys = { version = "0.3", optional = true } +wasm-bindgen-futures = { version = "0.4", optional = true } +wasm-streams = { version = "0.4", optional = true } +web-sys = { version = "0.3", optional = true, features = [ + "console", + "ReadableStream", + "ReadableStreamDefaultReader", +] } + +# reqwest client +reqwest = { version = "0.11", default-features = false, optional = true, features = [ + "multipart", + "stream", +] } + [features] +actix = ["dep:actix-web", "dep:send_wrapper"] +axum = [ + "dep:axum", + "dep:http", + "dep:hyper", + "dep:http-body-util", + "dep:tower", + "dep:tower-layer", +] +browser = [ + "dep:gloo-net", + "dep:js-sys", + "dep:send_wrapper", + "dep:web-sys", + "dep:wasm-streams", + "dep:wasm-bindgen-futures", +] +json = ["dep:serde_json"] +multipart = ["dep:multer"] +url = ["dep:serde_qs"] +cbor = ["dep:ciborium"] +rkyv = ["dep:rkyv"] default-tls = ["reqwest/default-tls"] rustls = ["reqwest/rustls-tls"] +reqwest = ["dep:http", "dep:reqwest"] ssr = ["inventory"] -nightly = ["server_fn_macro_default/nightly"] diff --git a/server_fn/Makefile.toml b/server_fn/Makefile.toml deleted file mode 100644 index 3d822c68da..0000000000 --- a/server_fn/Makefile.toml +++ /dev/null @@ -1 +0,0 @@ -extend = { path = "../cargo-make/main.toml" } diff --git a/server_fn/server_fn_macro_default/src/lib.rs b/server_fn/server_fn_macro_default/src/lib.rs deleted file mode 100644 index 69b8b27af0..0000000000 --- a/server_fn/server_fn_macro_default/src/lib.rs +++ /dev/null @@ -1,69 +0,0 @@ -#![cfg_attr(feature = "nightly", feature(proc_macro_span))] -//! This crate contains the default implementation of the #[macro@crate::server] macro without a context from the server. See the [server_fn_macro] crate for more information. -#![forbid(unsafe_code)] -// to prevent warnings from popping up when a nightly feature is stabilized -#![allow(stable_features)] - -use proc_macro::TokenStream; -use server_fn_macro::server_macro_impl; -use syn::__private::ToTokens; - -/// Declares that a function is a [server function](https://docs.rs/server_fn/). -/// This means that its body will only run on the server, i.e., when the `ssr` -/// feature is enabled. -/// -/// You can specify one, two, three, or four arguments to the server function: -/// 1. **Required**: A type name that will be used to identify and register the server function -/// (e.g., `MyServerFn`). -/// 2. *Optional*: A URL prefix at which the function will be mounted when it’s registered -/// (e.g., `"/api"`). Defaults to `"/"`. -/// 3. *Optional*: The encoding for the server function (`"Url"`, `"Cbor"`, `"GetJson"`, or `"GetCbor`". See **Server Function Encodings** below.) -/// 4. *Optional*: A specific endpoint path to be used in the URL. (By default, a unique path will be generated.) -/// -/// ```rust,ignore -/// // will generate a server function at `/api-prefix/hello` -/// #[server(MyServerFnType, "/api-prefix", "Url", "hello")] -/// ``` -/// -/// The server function itself can take any number of arguments, each of which should be serializable -/// and deserializable with `serde`. -/// -/// ```ignore -/// # use server_fn::*; use serde::{Serialize, Deserialize}; -/// # #[derive(Serialize, Deserialize)] -/// # pub struct Post { } -/// #[server(ReadPosts, "/api")] -/// pub async fn read_posts(how_many: u8, query: String) -> Result, ServerFnError> { -/// // do some work on the server to access the database -/// todo!() -/// } -/// ``` -/// -/// Note the following: -/// - **Server functions must be `async`.** Even if the work being done inside the function body -/// can run synchronously on the server, from the client’s perspective it involves an asynchronous -/// function call. -/// - **Server functions must return `Result`.** Even if the work being done -/// inside the function body can’t fail, the processes of serialization/deserialization and the -/// network call are fallible. -/// - **Return types must implement [Serialize](https://docs.rs/serde/latest/serde/trait.Serialize.html).** -/// This should be fairly obvious: we have to serialize arguments to send them to the server, and we -/// need to deserialize the result to return it to the client. -/// - **Arguments must be implement [`Serialize`](https://docs.rs/serde/latest/serde/trait.Serialize.html) -/// and [`DeserializeOwned`](https://docs.rs/serde/latest/serde/de/trait.DeserializeOwned.html).** -/// They are serialized as an `application/x-www-form-urlencoded` -/// form data using [`serde_qs`](https://docs.rs/serde_qs/latest/serde_qs/) or as `application/cbor` -/// using [`cbor`](https://docs.rs/cbor/latest/cbor/). -#[proc_macro_attribute] -pub fn server(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream { - match server_macro_impl( - args.into(), - s.into(), - syn::parse_quote!(server_fn::default::DefaultServerFnTraitObj), - None, - Some(syn::parse_quote!(server_fn)), - ) { - Err(e) => e.to_compile_error().into(), - Ok(s) => s.to_token_stream().into(), - } -} diff --git a/server_fn/src/client.rs b/server_fn/src/client.rs new file mode 100644 index 0000000000..d171131509 --- /dev/null +++ b/server_fn/src/client.rs @@ -0,0 +1,65 @@ +use crate::{error::ServerFnError, request::ClientReq, response::ClientRes}; +use std::future::Future; + +pub trait Client { + type Request: ClientReq + Send; + type Response: ClientRes + Send; + + fn send( + req: Self::Request, + ) -> impl Future>> + Send; +} + +#[cfg(feature = "browser")] +pub mod browser { + use super::Client; + use crate::{ + error::ServerFnError, request::browser::BrowserRequest, response::browser::BrowserResponse, + }; + use send_wrapper::SendWrapper; + use std::future::Future; + + pub struct BrowserClient; + + impl Client for BrowserClient { + type Request = BrowserRequest; + type Response = BrowserResponse; + + fn send( + req: Self::Request, + ) -> impl Future>> + Send { + SendWrapper::new(async move { + req.0 + .take() + .send() + .await + .map(|res| BrowserResponse(SendWrapper::new(res))) + .map_err(|e| ServerFnError::Request(e.to_string())) + }) + } + } +} + +#[cfg(feature = "reqwest")] +pub mod reqwest { + use super::Client; + use crate::{error::ServerFnError, request::reqwest::CLIENT}; + use futures::TryFutureExt; + use reqwest::{Request, Response}; + use std::future::Future; + + pub struct 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())) + } + } +} diff --git a/server_fn/src/codec/cbor.rs b/server_fn/src/codec/cbor.rs new file mode 100644 index 0000000000..e6ca6ea323 --- /dev/null +++ b/server_fn/src/codec/cbor.rs @@ -0,0 +1,160 @@ +use super::{Encoding, FromReq, FromRes, IntoReq, IntoRes}; +use crate::error::ServerFnError; +use crate::request::{ClientReq, Req}; +use crate::response::{ClientRes, Res}; +use bytes::Bytes; +use serde::de::DeserializeOwned; +use serde::Serialize; + +/// Pass arguments and receive responses using `cbor` in a `POST` request. +pub struct Cbor; + +impl Encoding for Cbor { + const CONTENT_TYPE: &'static str = "application/cbor"; +} + +impl IntoReq for T +where + Request: ClientReq, + T: Serialize + Send, +{ + 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()))?; + Request::try_new_post_bytes(path, accepts, Cbor::CONTENT_TYPE, Bytes::from(buffer)) + } +} + +impl FromReq for T +where + Request: Req + Send + 'static, + T: DeserializeOwned, +{ + 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())) + } +} + +impl IntoRes for T +where + Response: Res, + T: Serialize + Send, +{ + 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()))?; + Response::try_from_bytes(Cbor::CONTENT_TYPE, Bytes::from(buffer)) + } +} + +impl FromRes for T +where + Response: ClientRes + Send, + T: DeserializeOwned + Send, +{ + 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())) + } +} + +/* use std::fmt::Display; + +use super::{Codec, Encoding}; +use crate::error::{ServerFnError, IntoErrorResponse}; +use async_trait::async_trait; +use axum::body::{Body, HttpBody}; +use http_body_util::BodyExt; +use serde::de::DeserializeOwned; +use serde::Serialize; +/// Pass argument as JSON in the body of a POST Request +pub struct PostCbor; + +impl Encoding for PostCbor { + const REQUEST_CONTENT_TYPE: &'static str = "application/cbor"; + const RESPONSE_CONTENT_TYPE: &'static str = "application/cbor"; +} + +#[async_trait] +impl + Codec< + RequestBody, + ResponseBody, + http::Request, + http::Response, + Body, + Body, + http::Request, + http::Response, + PostCbor, + > for T +where + T: DeserializeOwned + Serialize + Send, + for<'a> RequestBody: HttpBody + Send + 'a, + ::Error: Display + Send , + ::Error: Display + Send , + for<'a> ResponseBody: HttpBody + Send + 'a, + ::Data: Send , + ::Data: Send , +{ + 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()))?; + let data = ciborium::de::from_reader(body_bytes.as_ref()) + .map_err(|e| ServerFnError::Args(e.to_string()))?; + Ok(data) + } + + 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() + .method("POST") + .header( + http::header::CONTENT_TYPE, + ::REQUEST_CONTENT_TYPE, + ) + .body(Body::from(buffer))?; + Ok(req) + } + 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()))?; + + ciborium::de::from_reader(body_bytes.as_ref()) + .map_err(|e| ServerFnError::Args(e.to_string())) + } + + async fn into_res(self) -> http::Response { + let mut buffer: Vec = Vec::new(); + match ciborium::ser::into_writer(&self, &mut buffer) { + Ok(_) => (), + Err(e) => return e.into_err_res(), + }; + + let res = http::Response::builder() + .status(200) + .header( + http::header::CONTENT_TYPE, + ::REQUEST_CONTENT_TYPE, + ) + .body(Body::from(buffer)) + .unwrap(); + res + } +} + */ diff --git a/server_fn/src/codec/json.rs b/server_fn/src/codec/json.rs new file mode 100644 index 0000000000..fc944453e9 --- /dev/null +++ b/server_fn/src/codec/json.rs @@ -0,0 +1,59 @@ +use super::{Encoding, FromReq, FromRes}; +use crate::error::ServerFnError; +use crate::request::{ClientReq, Req}; +use crate::response::{ClientRes, Res}; +use crate::{IntoReq, IntoRes}; +use serde::de::DeserializeOwned; +use serde::Serialize; +/// Pass arguments and receive responses as JSON in the body of a `POST` request. +pub struct Json; + +impl Encoding for Json { + const CONTENT_TYPE: &'static str = "application/json"; +} + +impl IntoReq for T +where + Request: ClientReq, + T: Serialize + Send, +{ + fn into_req(self, path: &str, accepts: &str) -> Result> { + let data = serde_json::to_string(&self) + .map_err(|e| ServerFnError::Serialization(e.to_string()))?; + Request::try_new_post(path, accepts, Json::CONTENT_TYPE, data) + } +} + +impl FromReq for T +where + Request: Req + Send + 'static, + T: DeserializeOwned, +{ + 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())) + } +} + +impl IntoRes for T +where + Response: Res, + T: Serialize + Send, +{ + async fn into_res(self) -> Result> { + let data = serde_json::to_string(&self) + .map_err(|e| ServerFnError::Serialization(e.to_string()))?; + Response::try_from_string(Json::CONTENT_TYPE, data) + } +} + +impl FromRes for T +where + Response: ClientRes + Send, + T: DeserializeOwned + Send, +{ + 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())) + } +} diff --git a/server_fn/src/codec/mod.rs b/server_fn/src/codec/mod.rs new file mode 100644 index 0000000000..7883ec3321 --- /dev/null +++ b/server_fn/src/codec/mod.rs @@ -0,0 +1,53 @@ +#[cfg(feature = "cbor")] +mod cbor; +#[cfg(feature = "cbor")] +pub use cbor::*; +#[cfg(feature = "json")] +mod json; +#[cfg(feature = "json")] +pub use json::*; +#[cfg(feature = "rkyv")] +mod rkyv; +#[cfg(feature = "rkyv")] +pub use rkyv::*; +#[cfg(feature = "url")] +mod url; +use crate::error::ServerFnError; +use futures::Future; +#[cfg(feature = "url")] +pub use url::*; + +#[cfg(feature = "multipart")] +mod multipart; +#[cfg(feature = "multipart")] +pub use multipart::*; + +mod stream; +pub use stream::*; + +pub trait FromReq +where + Self: Sized, +{ + fn from_req(req: Request) -> impl Future>> + Send; +} + +pub trait IntoReq { + fn into_req(self, path: &str, accepts: &str) -> Result>; +} + +pub trait FromRes +where + Self: Sized, +{ + fn from_res(res: Response) + -> impl Future>> + Send; +} + +pub trait IntoRes { + fn into_res(self) -> impl Future>> + Send; +} + +pub trait Encoding { + const CONTENT_TYPE: &'static str; +} diff --git a/server_fn/src/codec/multipart.rs b/server_fn/src/codec/multipart.rs new file mode 100644 index 0000000000..987902184d --- /dev/null +++ b/server_fn/src/codec/multipart.rs @@ -0,0 +1,71 @@ +use super::{Encoding, FromReq}; +use crate::error::ServerFnError; +use crate::request::browser::BrowserFormData; +use crate::request::{ClientReq, Req}; +use crate::IntoReq; +use futures::StreamExt; +use multer::Multipart; +use web_sys::FormData; + +pub struct MultipartFormData; + +impl Encoding for MultipartFormData { + const CONTENT_TYPE: &'static str = "multipart/form-data"; +} + +#[derive(Debug)] +pub enum MultipartData { + Client(BrowserFormData), + Server(multer::Multipart<'static>), +} + +impl MultipartData { + pub fn into_client_data(self) -> Option { + match self { + MultipartData::Client(data) => Some(data), + MultipartData::Server(_) => None, + } + } + + pub fn into_data(self) -> Option> { + match self { + MultipartData::Client(_) => None, + MultipartData::Server(data) => Some(data), + } + } +} + +impl From for MultipartData { + fn from(value: FormData) -> Self { + MultipartData::Client(value.into()) + } +} + +impl IntoReq for T +where + Request: ClientReq, + T: Into, +{ + fn into_req(self, path: &str, accepts: &str) -> Result> { + let multi = self.into(); + Request::try_new_multipart(path, accepts, multi.into_client_data().unwrap()) + } +} + +impl FromReq for T +where + Request: Req + Send + 'static, + T: From, + CustErr: 'static, +{ + 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())), boundary); + Ok(MultipartData::Server(data).into()) + } +} diff --git a/server_fn/src/codec/rkyv.rs b/server_fn/src/codec/rkyv.rs new file mode 100644 index 0000000000..8fc0981d6f --- /dev/null +++ b/server_fn/src/codec/rkyv.rs @@ -0,0 +1,72 @@ +use rkyv::{ + de::deserializers::SharedDeserializeMap, ser::serializers::AllocSerializer, + validation::validators::DefaultValidator, Archive, CheckBytes, Deserialize, Serialize, +}; + +use super::{Encoding, FromReq, FromRes, IntoReq, IntoRes}; +use crate::error::ServerFnError; +use crate::request::{ClientReq, Req}; +use crate::response::{ClientRes, Res}; +use bytes::Bytes; + +/// Pass arguments and receive responses using `rkyv` in a `POST` request. +pub struct Rkyv; + +impl Encoding for Rkyv { + const CONTENT_TYPE: &'static str = "application/rkyv"; +} + +impl IntoReq for T +where + Request: ClientReq, + T: Serialize> + Send, + T: Archive, + T::Archived: for<'a> CheckBytes> + Deserialize, +{ + fn into_req(self, path: &str, accepts: &str) -> Result> { + let encoded = rkyv::to_bytes::(&self)?; + let bytes = Bytes::copy_from_slice(encoded.as_ref()); + Request::try_new_post_bytes(path, accepts, Rkyv::CONTENT_TYPE, bytes) + } +} + +impl FromReq for T +where + Request: Req + Send + 'static, + T: Serialize> + Send, + T: Archive, + T::Archived: for<'a> CheckBytes> + Deserialize, +{ + async fn from_req(req: Request) -> Result> { + let body_bytes = req.try_into_bytes().await?; + rkyv::from_bytes::(&body_bytes).map_err(|e| ServerFnError::Args(e.to_string())) + } +} + +impl IntoRes for T +where + Response: Res, + T: Serialize> + Send, + T: Archive, + T::Archived: for<'a> CheckBytes> + Deserialize, +{ + async fn into_res(self) -> Result> { + let encoded = rkyv::to_bytes::(&self) + .map_err(|e| ServerFnError::Serialization(e.to_string()))?; + let bytes = Bytes::copy_from_slice(encoded.as_ref()); + Response::try_from_bytes(Rkyv::CONTENT_TYPE, bytes) + } +} + +impl FromRes for T +where + Response: ClientRes + Send, + T: Serialize> + Send, + T: Archive, + T::Archived: for<'a> CheckBytes> + Deserialize, +{ + 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())) + } +} diff --git a/server_fn/src/codec/stream.rs b/server_fn/src/codec/stream.rs new file mode 100644 index 0000000000..7999a8caf0 --- /dev/null +++ b/server_fn/src/codec/stream.rs @@ -0,0 +1,128 @@ +use std::pin::Pin; + +use super::{Encoding, FromRes}; +use crate::error::{NoCustomError, ServerFnError}; +use crate::response::{ClientRes, Res}; +use crate::IntoRes; +use bytes::Bytes; +use futures::{Stream, StreamExt}; + +pub struct Streaming; + +impl Encoding for Streaming { + const CONTENT_TYPE: &'static str = "application/octet-stream"; +} + +/* impl IntoReq for T +where + Request: ClientReq, + T: Stream + Send, +{ + fn into_req(self, path: &str, accepts: &str) -> Result> { + Request::try_new_stream(path, ByteStream::CONTENT_TYPE, self) + } +} */ + +/* impl FromReq for T +where + Request: Req + Send + 'static, + T: Stream + Send, +{ + async fn from_req(req: Request) -> Result> { + req.try_into_stream().await + } +} */ + +pub struct ByteStream( + Pin>> + Send>>, +); + +impl ByteStream { + pub fn into_inner(self) -> impl Stream>> + Send { + self.0 + } +} + +impl From for ByteStream +where + S: Stream + Send + 'static, + T: Into, +{ + fn from(value: S) -> Self { + Self(Box::pin(value.map(|data| Ok(data.into())))) + } +} + +impl IntoRes for ByteStream +where + Response: Res, + CustErr: 'static, +{ + async fn into_res(self) -> Result> { + Response::try_from_stream(Streaming::CONTENT_TYPE, self.into_inner()) + } +} + +impl FromRes for ByteStream +where + Response: ClientRes + Send, +{ + async fn from_res(res: Response) -> Result> { + let stream = res.try_into_stream()?; + Ok(ByteStream(Box::pin(stream))) + } +} + +pub struct StreamingText; + +impl Encoding for StreamingText { + const CONTENT_TYPE: &'static str = "text/plain"; +} + +pub struct TextStream( + Pin>> + Send>>, +); + +impl TextStream { + pub fn into_inner(self) -> impl Stream>> + Send { + self.0 + } +} + +impl From for TextStream +where + S: Stream + Send + 'static, + T: Into, +{ + fn from(value: S) -> Self { + Self(Box::pin(value.map(|data| Ok(data.into())))) + } +} + +impl IntoRes for TextStream +where + Response: Res, + CustErr: 'static, +{ + async fn into_res(self) -> Result> { + Response::try_from_stream( + Streaming::CONTENT_TYPE, + self.into_inner().map(|stream| stream.map(Into::into)), + ) + } +} + +impl FromRes for TextStream +where + Response: ClientRes + Send, +{ + 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())) + }) + })))) + } +} diff --git a/server_fn/src/codec/url.rs b/server_fn/src/codec/url.rs new file mode 100644 index 0000000000..fb298d2805 --- /dev/null +++ b/server_fn/src/codec/url.rs @@ -0,0 +1,131 @@ +use super::{Encoding, FromReq, IntoReq}; +use crate::error::ServerFnError; +use crate::request::{ClientReq, Req}; +use serde::de::DeserializeOwned; +use serde::Serialize; + +/// Pass arguments as a URL-encoded query string of a `GET` request. +pub struct GetUrl; + +/// Pass arguments as the URL-encoded body of a `POST` request. +pub struct PostUrl; + +impl Encoding for GetUrl { + const CONTENT_TYPE: &'static str = "application/x-www-form-urlencoded"; +} + +impl IntoReq for T +where + Request: ClientReq, + T: Serialize + Send, +{ + fn into_req(self, path: &str, accepts: &str) -> Result> { + let data = + serde_qs::to_string(&self).map_err(|e| ServerFnError::Serialization(e.to_string()))?; + Request::try_new_get(path, accepts, GetUrl::CONTENT_TYPE, &data) + } +} + +impl FromReq for T +where + Request: Req + Send + 'static, + T: DeserializeOwned, +{ + async fn from_req(req: Request) -> Result> { + let string_data = req.as_query().unwrap_or_default(); + let args = serde_qs::from_str::(string_data) + .map_err(|e| ServerFnError::Args(e.to_string()))?; + Ok(args) + } +} + +impl Encoding for PostUrl { + const CONTENT_TYPE: &'static str = "application/x-www-form-urlencoded"; +} + +impl IntoReq for T +where + Request: ClientReq, + T: Serialize + Send, +{ + fn into_req(self, path: &str, accepts: &str) -> Result> { + let qs = + serde_qs::to_string(&self).map_err(|e| ServerFnError::Serialization(e.to_string()))?; + Request::try_new_post(path, accepts, PostUrl::CONTENT_TYPE, qs) + } +} + +impl FromReq for T +where + Request: Req + Send + 'static, + T: DeserializeOwned, +{ + async fn from_req(req: Request) -> Result> { + let string_data = req.try_into_string().await?; + let args = serde_qs::from_str::(&string_data) + .map_err(|e| ServerFnError::Args(e.to_string()))?; + Ok(args) + } +} + +/* #[async_trait] +impl Codec for T +where + T: DeserializeOwned + Serialize + Send, + Request: Req + Send, + Response: Res + Send, +{ + 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()))?; + Ok(args) + } + + async fn into_req(self) -> Result> { + /* let qs = serde_qs::to_string(&self)?; + let req = http::Request::builder() + .method("GET") + .header( + http::header::CONTENT_TYPE, + ::REQUEST_CONTENT_TYPE, + ) + .body(Body::from(qs))?; + Ok(req) */ + todo!() + } + + async fn from_res(res: Response) -> Result> { + todo!() + /* 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()))?; + let string_data = String::from_utf8(body_bytes.to_vec())?; + serde_json::from_str(&string_data) + .map_err(|e| ServerFnError::Deserialization(e.to_string())) */ + } + + async fn into_res(self) -> Response { + // Need to catch and err or here, or handle Errors at a higher level + let data = match serde_json::to_string(&self) { + Ok(d) => d, + Err(e) => return e.into_err_res(), + }; + let builder = http::Response::builder(); + let res = builder + .status(200) + .header( + http::header::CONTENT_TYPE, + ::RESPONSE_CONTENT_TYPE, + ) + .body(Body::from(data)) + .unwrap(); + res + } +} + */ diff --git a/server_fn/src/default.rs b/server_fn/src/default.rs deleted file mode 100644 index 0875ba4d84..0000000000 --- a/server_fn/src/default.rs +++ /dev/null @@ -1,117 +0,0 @@ -#[cfg(any(feature = "ssr", doc))] -use crate::ServerFnTraitObj; -pub use server_fn_macro_default::server; -#[cfg(any(feature = "ssr", doc))] -use std::{ - collections::HashMap, - sync::{Arc, RwLock}, -}; - -#[cfg(any(feature = "ssr", doc))] -lazy_static::lazy_static! { - static ref REGISTERED_SERVER_FUNCTIONS: Arc>> = { - let mut map = HashMap::new(); - for server_fn in inventory::iter:: { - map.insert(server_fn.0.url(), server_fn); - } - Arc::new(RwLock::new(map)) - }; -} - -#[cfg(feature = "ssr")] -inventory::collect!(DefaultServerFnTraitObj); - -/// Attempts to find a server function registered at the given path. -/// -/// This can be used by a server to handle the requests, as in the following example (using [`actix-web`]). -/// -/// ```rust, ignore -/// #[post("{tail:.*}")] -/// async fn handle_server_fns( -/// req: HttpRequest, -/// params: web::Path, -/// body: web::Bytes, -/// ) -> impl Responder { -/// let path = params.into_inner(); -/// let accept_header = req -/// .headers() -/// .get("Accept") -/// .and_then(|value| value.to_str().ok()); -/// -/// if let Some(server_fn) = server_fn_by_path(path.as_str()) { -/// let body: &[u8] = &body; -/// match server_fn(&body).await { -/// Ok(serialized) => { -/// // if this is Accept: application/json then send a serialized JSON response -/// if let Some("application/json") = accept_header { -/// HttpResponse::Ok().body(serialized) -/// } -/// // otherwise, it's probably a
submit or something: redirect back to the referrer -/// else { -/// HttpResponse::SeeOther() -/// .insert_header(("Location", "/")) -/// .content_type("application/json") -/// .body(serialized) -/// } -/// } -/// Err(e) => { -/// eprintln!("server function error: {e:#?}"); -/// HttpResponse::InternalServerError().body(e.to_string()) -/// } -/// } -/// } else { -/// HttpResponse::BadRequest().body(format!("Could not find a server function at that route.")) -/// } -/// } -/// ``` -/// -/// [`actix-web`]: -#[cfg(any(feature = "ssr", doc))] -pub fn server_fn_by_path( - path: &str, -) -> Option<&'static DefaultServerFnTraitObj> { - REGISTERED_SERVER_FUNCTIONS - .read() - .expect("Server function registry is poisoned") - .get(path) - .copied() -} - -/// Returns the set of currently-registered server function paths, for debugging purposes. -#[cfg(any(feature = "ssr", doc))] -pub fn server_fns_by_path() -> Vec<&'static str> { - REGISTERED_SERVER_FUNCTIONS - .read() - .expect("Server function registry is poisoned") - .keys() - .copied() - .collect() -} - -#[cfg(any(feature = "ssr", doc))] -/// A server function that can be called from the client without any context from the server. -pub struct DefaultServerFnTraitObj(ServerFnTraitObj<()>); - -#[cfg(any(feature = "ssr", doc))] -impl DefaultServerFnTraitObj { - /// Creates a new server function with the given prefix, URL, encoding, and function. - pub const fn from_generic_server_fn(f: ServerFnTraitObj<()>) -> Self { - Self(f) - } -} - -#[cfg(any(feature = "ssr", doc))] -impl std::ops::Deref for DefaultServerFnTraitObj { - type Target = ServerFnTraitObj<()>; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -#[cfg(any(feature = "ssr", doc))] -impl std::ops::DerefMut for DefaultServerFnTraitObj { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } -} diff --git a/server_fn/src/error.rs b/server_fn/src/error.rs index 8d3ec910ed..00c855669d 100644 --- a/server_fn/src/error.rs +++ b/server_fn/src/error.rs @@ -1,65 +1,107 @@ +use core::fmt::{self, Display}; + use serde::{Deserialize, Serialize}; -use std::{error, fmt, ops, sync::Arc}; use thiserror::Error; -/// This is a result type into which any error can be converted, -/// and which can be used directly in your `view`. -/// -/// All errors will be stored as [`struct@Error`]. -pub type Result = core::result::Result; - -/// A generic wrapper for any error. -#[derive(Debug, Clone)] -#[repr(transparent)] -pub struct Error(Arc); - -impl Error { - /// Converts the wrapper into the inner reference-counted error. - pub fn into_inner(self) -> Arc { - Arc::clone(&self.0) +/// An empty value indicating that there is no custom error type associated +/// with this server function. +#[derive(Debug, Deserialize, Serialize)] +pub struct NoCustomError(()); + +// Implement `Display` for `NoCustomError` +impl fmt::Display for NoCustomError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "Unit Type Displayed") } } -impl ops::Deref for Error { - type Target = Arc; +#[derive(Debug)] +pub struct WrapError(pub T); + +/// This helper macro lets you call the gnarly autoref-specialization call +/// without having to worry about things like how many & you need. +/// Mostly used when you impl From for YourError +#[macro_export] +macro_rules! server_fn_error { + () => {{ + use $crate::{ViaError, WrapError}; + (&&&&&WrapError(())).to_server_error() + }}; + ($err:expr) => {{ + use $crate::error::{ViaError, WrapError}; + match $err { + error => (&&&&&WrapError(error)).to_server_error(), + } + }}; +} + +/// This trait serves as the conversion method between a variety of types +/// and [`ServerFnError`]. +pub trait ViaError { + fn to_server_error(&self) -> ServerFnError; +} - fn deref(&self) -> &Self::Target { - &self.0 +// This impl should catch if you fed it a [`ServerFnError`] already. +impl ViaError + for &&&&WrapError> +{ + fn to_server_error(&self) -> ServerFnError { + self.0.clone() } } -impl fmt::Display for Error { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) +// A type tag for ServerFnError so we can special case it +pub(crate) trait ServerFnErrorKind {} + +impl ServerFnErrorKind for ServerFnError {} + +// This impl should catch passing () or nothing to server_fn_error +impl ViaError<()> for &&&WrapError<()> { + fn to_server_error(&self) -> ServerFnError<()> { + ServerFnError::WrappedServerError(self.0.clone()) } } -impl From for Error -where - T: std::error::Error + Send + Sync + 'static, -{ - fn from(value: T) -> Self { - Error(Arc::new(value)) +// This impl will catch any type that implements any type that impls +// Error and Clone, so that it can be wrapped into ServerFnError +impl ViaError for &&WrapError { + fn to_server_error(&self) -> ServerFnError { + ServerFnError::WrappedServerError(self.0.clone()) } } -impl From for Error { - fn from(e: ServerFnError) -> Self { - Error(Arc::new(ServerFnErrorErr::from(e))) +// If it doesn't impl Error, but does impl Display and Clone, +// we can still wrap it in String form +impl ViaError for &WrapError { + fn to_server_error(&self) -> ServerFnError { + ServerFnError::WrappedServerError(self.0.clone()) + } +} + +// This is what happens if someone tries to pass in something that does +// not meet the above criteria +impl ViaError for WrapError { + #[track_caller] + fn to_server_error(&self) -> ServerFnError { + panic!("At {}, you call `to_server_error()` or use `server_fn_error!` with a value that does not implement `Clone` and either `Error` or `Display`.", std::panic::Location::caller()); } } /// Type for errors that can occur when using server functions. /// -/// Unlike [`ServerFnErrorErr`], this does not implement [`std::error::Error`]. +/// Unlike [`ServerFnErrorErr`], this does not implement [`Error`](std::error::Error). /// This means that other error types can easily be converted into it using the /// `?` operator. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -pub enum ServerFnError { +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "type")] +pub enum ServerFnError { + WrappedServerError(E), /// Error while trying to register the server function (only occurs in case of poisoned RwLock). Registration(String), /// Occurs on the client if there is a network error while trying to run function on server. Request(String), + /// Occurs on the server if there is an error creating an HTTP response. + Response(String), /// Occurs when there is an error while actually running the function on the server. ServerError(String), /// Occurs on the client if there is an error deserializing the server's response. @@ -72,39 +114,49 @@ pub enum ServerFnError { MissingArg(String), } -impl core::fmt::Display for ServerFnError { +impl From for ServerFnError { + fn from(value: CustErr) -> Self { + ServerFnError::WrappedServerError(value) + } +} + +impl Display for ServerFnError +where + CustErr: Display, +{ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( f, "{}", match self { - ServerFnError::Registration(s) => format!( - "error while trying to register the server function: {s}" - ), - ServerFnError::Request(s) => format!( - "error reaching server to call server function: {s}" - ), - ServerFnError::ServerError(s) => - format!("error running server function: {s}"), + ServerFnError::Registration(s) => + format!("error while trying to register the server function: {s}"), + ServerFnError::Request(s) => + format!("error reaching server to call server function: {s}"), + ServerFnError::ServerError(s) => format!("error running server function: {s}"), ServerFnError::Deserialization(s) => format!("error deserializing server function results: {s}"), ServerFnError::Serialization(s) => format!("error serializing server function arguments: {s}"), - ServerFnError::Args(s) => format!( - "error deserializing server function arguments: {s}" - ), + ServerFnError::Args(s) => + format!("error deserializing server function arguments: {s}"), ServerFnError::MissingArg(s) => format!("missing argument {s}"), + ServerFnError::Response(s) => format!("error generating HTTP response: {s}"), + ServerFnError::WrappedServerError(e) => format!("{}", e), } ) } } - -impl From for ServerFnError +impl std::error::Error for ServerFnError where - E: std::error::Error, + E: std::error::Error + 'static, + ServerFnError: std::fmt::Display, { - fn from(e: E) -> Self { - ServerFnError::ServerError(e.to_string()) + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + ServerFnError::WrappedServerError(e) => Some(e), + _ => None, + } } } @@ -117,8 +169,10 @@ where /// /// [`ServerFnError`] and [`ServerFnErrorErr`] mutually implement [`From`], so /// it is easy to convert between the two types. -#[derive(Error, Debug, Clone, Serialize, Deserialize)] -pub enum ServerFnErrorErr { +#[derive(Error, Debug, Clone)] +pub enum ServerFnErrorErr { + #[error("internal error: {0}")] + WrappedServerError(E), /// 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), @@ -140,28 +194,23 @@ pub enum ServerFnErrorErr { /// Occurs on the server if there's a missing argument. #[error("missing argument {0}")] MissingArg(String), + /// Occurs on the server if there is an error creating an HTTP response. + #[error("error creating response {0}")] + Response(String), } -impl From for ServerFnErrorErr { - fn from(value: ServerFnError) -> Self { +impl From> for ServerFnErrorErr { + fn from(value: ServerFnError) -> Self { match value { - ServerFnError::Registration(value) => { - ServerFnErrorErr::Registration(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::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::MissingArg(value) => ServerFnErrorErr::MissingArg(value), + ServerFnError::WrappedServerError(value) => ServerFnErrorErr::WrappedServerError(value), + ServerFnError::Response(value) => ServerFnErrorErr::Response(value), } } } diff --git a/server_fn/src/lib.rs b/server_fn/src/lib.rs index 1d9a3a0e09..4010e03f5c 100644 --- a/server_fn/src/lib.rs +++ b/server_fn/src/lib.rs @@ -1,632 +1,291 @@ -#![forbid(unsafe_code)] -#![deny(missing_docs)] - -//! # Server Functions -//! -//! This package is based on a simple idea: sometimes it’s useful to write functions -//! that will only run on the server, and call them from the client. -//! -//! If you’re creating anything beyond a toy app, you’ll need to do this all the time: -//! reading from or writing to a database that only runs on the server, running expensive -//! computations using libraries you don’t want to ship down to the client, accessing -//! APIs that need to be called from the server rather than the client for CORS reasons -//! or because you need a secret API key that’s stored on the server and definitely -//! shouldn’t be shipped down to a user’s browser. -//! -//! Traditionally, this is done by separating your server and client code, and by setting -//! up something like a REST API or GraphQL API to allow your client to fetch and mutate -//! data on the server. This is fine, but it requires you to write and maintain your code -//! in multiple separate places (client-side code for fetching, server-side functions to run), -//! as well as creating a third thing to manage, which is the API contract between the two. -//! -//! This package provides two simple primitives that allow you instead to write co-located, -//! isomorphic server functions. (*Co-located* means you can write them in your app code so -//! that they are “located alongside” the client code that calls them, rather than separating -//! the client and server sides. *Isomorphic* means you can call them from the client as if -//! you were simply calling a function; the function call has the “same shape” on the client -//! as it does on the server.) -//! -//! ### `#[server]` -//! -//! The [`#[server]`][server] macro allows you to annotate a function to -//! indicate that it should only run on the server (i.e., when you have an `ssr` feature in your -//! crate that is enabled). -//! -//! **Important**: Before calling a server function on a non-web platform, you must set the server URL by calling [`set_server_url`]. -//! -//! ```rust,ignore -//! #[server(ReadFromDB)] -//! async fn read_posts(how_many: usize, query: String) -> Result, ServerFnError> { -//! // do some server-only work here to access the database -//! let posts = ...; -//! Ok(posts) -//! } -//! -//! // call the function -//! # #[tokio::main] -//! # async fn main() { -//! async { -//! let posts = read_posts(3, "my search".to_string()).await; -//! log::debug!("posts = {posts:#?}"); -//! } -//! # } -//! -//! // make sure you've registered it somewhere in main -//! fn main() { -//! // for non-web apps, you must set the server URL manually -//! server_fn::set_server_url("http://localhost:3000"); -//! _ = ReadFromDB::register(); -//! } -//! ``` -//! -//! If you call this function from the client, it will serialize the function arguments and `POST` -//! them to the server as if they were the inputs in ``. -//! -//! Here’s what you need to remember: -//! - **Server functions must be `async`.** Even if the work being done inside the function body -//! can run synchronously on the server, from the client’s perspective it involves an asynchronous -//! function call. -//! - **Server functions must return `Result`.** Even if the work being done -//! inside the function body can’t fail, the processes of serialization/deserialization and the -//! network call are fallible. -//! - **Return types must implement [`serde::Serialize`].** -//! This should be fairly obvious: we have to serialize arguments to send them to the server, and we -//! need to deserialize the result to return it to the client. -//! - **Arguments must be implement [`serde::Serialize`].** They are serialized as an `application/x-www-form-urlencoded` -//! form data using [`serde_qs`] or as `application/cbor` using [`cbor`]. -//! -//! [server]: -//! [`serde_qs`]: -//! [`cbor`]: - -// used by the macro +pub mod client; +pub mod codec; +#[macro_use] +pub mod error; +pub mod middleware; +pub mod redirect; +pub mod request; +pub mod response; + +use client::Client; +use codec::{Encoding, FromReq, FromRes, IntoReq, IntoRes}; +use dashmap::DashMap; +pub use error::ServerFnError; +use middleware::{Layer, Service}; +use once_cell::sync::Lazy; +use request::Req; +use response::{ClientRes, Res}; +use serde::{de::DeserializeOwned, Serialize}; +use std::{future::Future, pin::Pin, sync::Arc}; + +// reexports for the sake of the macro #[doc(hidden)] pub use const_format; -// used by the macro -#[cfg(feature = "ssr")] -#[doc(hidden)] -pub use inventory; -#[cfg(any(feature = "ssr", doc))] -use proc_macro2::TokenStream; -#[cfg(any(feature = "ssr", doc))] -use quote::TokenStreamExt; -// used by the macro #[doc(hidden)] pub use serde; -use serde::{de::DeserializeOwned, Serialize}; -pub use server_fn_macro_default::server; -use std::{future::Future, pin::Pin, str::FromStr}; -#[cfg(any(feature = "ssr", doc))] -use syn::parse_quote; -// used by the macro #[doc(hidden)] pub use xxhash_rust; -/// Error types used in server functions. -pub mod error; -pub use error::ServerFnError; -/// Default server function registry -pub mod default; - -/// Something that can register a server function. -pub trait ServerFunctionRegistry { - /// An error that can occur when registering a server function. - type Error: std::error::Error; - - /// Server functions are automatically registered on most platforms, (including Linux, macOS, - /// iOS, FreeBSD, Android, and Windows). If you are on another platform, like a WASM server runtime, - /// this will explicitly register server functions. - fn register_explicit( - prefix: &'static str, - url: &'static str, - server_function: SerializedFnTraitObj, - encoding: Encoding, - ) -> Result<(), Self::Error>; - - /// Returns the server function registered at the given URL, or `None` if no function is registered at that URL. - fn get(url: &str) -> Option>; - - /// Returns the server function registered at the given URL, or `None` if no function is registered at that URL. - fn get_trait_obj(url: &str) -> Option>; - /// Returns the encoding of the server FN at the given URL, or `None` if no function is - /// registered at that URL - fn get_encoding(url: &str) -> Option; - /// Returns a list of all registered server functions. - fn paths_registered() -> Vec<&'static str>; -} +pub trait ServerFn +where + Self: Send + + FromReq + + IntoReq>::Request, Self::InputEncoding>, +{ + const PATH: &'static str; -/// A server function that can be called from the client. -pub type SerializedFnTraitObj = - fn( - T, - &[u8], - ) -> Pin>>>; - -/// A server function that can be called from the client. -#[derive(Clone)] -pub struct ServerFnTraitObj { - pub(crate) prefix: &'static str, - pub(crate) url: &'static str, - pub(crate) encoding: Encoding, - pub(crate) run: SerializedFnTraitObj, -} + /// The type of the HTTP client that will send the request from the client side. + /// + /// For example, this might be `gloo-net` in the browser, or `reqwest` for a desktop app. + type Client: Client; -impl ServerFnTraitObj { - /// Creates a new server function with the given prefix, URL, encoding, and function. - pub const fn new( - prefix: &'static str, - url: &'static str, - encoding: Encoding, - run: SerializedFnTraitObj, - ) -> Self { - Self { - prefix, - url, - encoding, - run, - } - } + /// The type of the HTTP request when received by the server function on the server side. + type ServerRequest: Req + Send; - /// Runs the server function with the given server agruments and serialized buffer from the client. - pub fn call( - &self, - args: T, - buffer: &[u8], - ) -> Pin>>> { - (self.run)(args, buffer) - } + /// The type of the HTTP response returned by the server function on the server side. + type ServerResponse: Res + Send; - /// Returns the prefix of the server function. - pub fn prefix(&self) -> &str { - self.prefix - } + /// The return type of the server function. + /// + /// This needs to be converted into `ServerResponse` on the server side, and converted + /// *from* `ClientResponse` when received by the client. + type Output: IntoRes + + FromRes>::Response, Self::OutputEncoding> + + Send; - /// Returns the URL of the server function. - pub fn url(&self) -> &str { - self.url - } + /// The [`Encoding`] used in the request for arguments into the server function. + type InputEncoding: Encoding; - /// Returns the encoding of the server function. - pub fn encoding(&self) -> Encoding { - self.encoding - } -} + /// The [`Encoding`] used in the response for the result of the server function. + type OutputEncoding: Encoding; -/// A dual type to hold the possible Response datatypes -#[derive(Debug)] -pub enum Payload { - ///Encodes Data using CBOR - Binary(Vec), - ///Encodes data in the URL - Url(String), - ///Encodes Data using Json - Json(String), -} + /// 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: Serialize + DeserializeOwned; -/// Attempts to find a server function registered at the given path. -/// -/// This can be used by a server to handle the requests, as in the following example (using [`actix-web`]) -/// -/// ```rust, ignore -/// #[post("{tail:.*}")] -/// async fn handle_server_fns( -/// req: HttpRequest, -/// params: web::Path, -/// body: web::Bytes, -/// ) -> impl Responder { -/// let path = params.into_inner(); -/// let accept_header = req -/// .headers() -/// .get("Accept") -/// .and_then(|value| value.to_str().ok()); -/// -/// if let Some(server_fn) = server_fn_by_path::(path.as_str()) { -/// let body: &[u8] = &body; -/// match (server_fn.trait_obj)(&body).await { -/// Ok(serialized) => { -/// // if this is Accept: application/json then send a serialized JSON response -/// if let Some("application/json") = accept_header { -/// HttpResponse::Ok().body(serialized) -/// } -/// // otherwise, it's probably a submit or something: redirect back to the referrer -/// else { -/// HttpResponse::SeeOther() -/// .insert_header(("Location", "/")) -/// .content_type("application/json") -/// .body(serialized) -/// } -/// } -/// Err(e) => { -/// eprintln!("server function error: {e:#?}"); -/// HttpResponse::InternalServerError().body(e.to_string()) -/// } -/// } -/// } else { -/// HttpResponse::BadRequest().body(format!("Could not find a server function at that route.")) -/// } -/// } -/// ``` -/// -/// [`actix-web`]: -#[cfg(any(feature = "ssr", doc))] -pub fn server_fn_by_path>( - path: &str, -) -> Option> { - R::get(path) -} + /// Middleware that should be applied to this server function. + fn middlewares() -> Vec>> { + Vec::new() + } -/// Returns a trait obj of the server fn for calling purposes -#[cfg(any(feature = "ssr", doc))] -pub fn server_fn_trait_obj_by_path>( - path: &str, -) -> Option> { - R::get_trait_obj(path) -} + // The body of the server function. This will only run on the server. + fn run_body( + self, + ) -> impl Future>> + Send; -/// Returns the Encoding of the server fn at a particular path -#[cfg(any(feature = "ssr", doc))] -pub fn server_fn_encoding_by_path>( - path: &str, -) -> Option { - R::get_encoding(path) -} + fn run_on_server( + req: Self::ServerRequest, + ) -> impl Future + Send { + async { + Self::execute_on_server(req) + .await + .unwrap_or_else(Self::ServerResponse::error_response) + } + } -/// Returns the set of currently-registered server function paths, for debugging purposes. -#[cfg(any(feature = "ssr", doc))] -pub fn server_fns_by_path>( -) -> Vec<&'static str> { - R::paths_registered() -} + fn run_on_client( + self, + ) -> impl Future>> + Send { + async move { + // create and send request on client + let req = self.into_req(Self::PATH, Self::OutputEncoding::CONTENT_TYPE)?; + let res = Self::Client::send(req).await?; + + let status = res.status(); + let location = res.location(); + + // if it returns an error status, deserialize the error + // this is the same logic as the current implementation of server fns + // TODO I don't love that this requires shipping `serde_json` for errors + let res = if (400..=599).contains(&status) { + let status_text = res.status_text(); + let text = res.try_into_string().await?; + match serde_json::from_str(&text) { + Ok(e) => Err(e), + Err(_) => Err(ServerFnError::ServerError(if text.is_empty() { + format!("{} {}", status, status_text) + } else { + format!("{} {}: {}", status, status_text, text) + })), + } + } else { + // otherwise, deserialize the body as is + Self::Output::from_res(res).await + }; -/// Holds the current options for encoding types. -/// More could be added, but they need to be serde -#[derive(Debug, Clone, Copy, Default, PartialEq)] -pub enum Encoding { - /// A Binary Encoding Scheme Called Cbor - Cbor, - /// The Default URL-encoded encoding method - #[default] - Url, - /// Pass arguments to server fns as part of the query string. Cacheable. Returns JSON - GetJSON, - /// Pass arguments to server fns as part of the query string. Cacheable. Returns CBOR - GetCBOR, -} + // if redirected, call the redirect hook (if that's been set) + if (300..=399).contains(&status) { + redirect::call_redirect_hook(&location); + } -impl FromStr for Encoding { - type Err = (); + res + } + } - fn from_str(input: &str) -> Result { - match input { - "URL" => Ok(Encoding::Url), - "Cbor" => Ok(Encoding::Cbor), - "GetCbor" => Ok(Encoding::GetCBOR), - "GetJson" => Ok(Encoding::GetJSON), - _ => Err(()), + #[doc(hidden)] + fn execute_on_server( + req: Self::ServerRequest, + ) -> impl Future>> + Send { + async { + let this = Self::from_req(req).await?; + let output = this.run_body().await?; + let res = output.into_res().await?; + Ok(res) } } -} -#[cfg(any(feature = "ssr", doc))] -impl quote::ToTokens for Encoding { - fn to_tokens(&self, tokens: &mut TokenStream) { - let option: syn::Ident = match *self { - Encoding::Cbor => parse_quote!(Cbor), - Encoding::Url => parse_quote!(Url), - Encoding::GetJSON => parse_quote!(GetJSON), - Encoding::GetCBOR => parse_quote!(GetCBOR), - }; - let expansion: syn::Ident = syn::parse_quote! { - Encoding::#option - }; - tokens.append(expansion); + fn url() -> &'static str { + Self::PATH } } -/// Defines a "server function." A server function can be called from the server or the client, -/// but the body of its code will only be run on the server, i.e., if a crate feature `ssr` (server-side-rendering) is enabled. -/// -/// Server functions are created using the `server` macro. -/// -/// The set of server functions can be queried on the server for routing purposes by calling [`server_fn_by_path`]. -/// -/// Technically, the trait is implemented on a type that describes the server function's arguments. -pub trait ServerFn -where - Self: Serialize + DeserializeOwned + Sized + 'static, -{ - /// The return type of the function. - type Output: serde::Serialize; - - /// URL prefix that should be prepended by the client to the generated URL. - fn prefix() -> &'static str; - - /// The path at which the server function can be reached on the server. - fn url() -> &'static str; - - /// The path at which the server function can be reached on the server. - fn encoding() -> Encoding; - - /// Runs the function on the server. - #[cfg(any(feature = "ssr", doc))] - fn call_fn( - self, - cx: T, - ) -> Pin>>>; - - /// Runs the function on the client by sending an HTTP request to the server. - #[cfg(any(not(feature = "ssr"), doc))] - fn call_fn_client( - self, - cx: T, - ) -> Pin>>>; - - /// Returns a trait object that can be used to call the server function. - #[cfg(any(feature = "ssr", doc))] - fn call_from_bytes( - cx: T, - data: &[u8], - ) -> Pin>>> { - // decode the args - let value = match Self::encoding() { - Encoding::Url | Encoding::GetJSON | Encoding::GetCBOR => { - serde_qs::Config::new(5, false) - .deserialize_bytes(data) - .map_err(|e| ServerFnError::Deserialization(e.to_string())) - } - Encoding::Cbor => ciborium::de::from_reader(data) - .map_err(|e| ServerFnError::Deserialization(e.to_string())), - }; - Box::pin(async move { - let value: Self = match value { - Ok(v) => v, - Err(e) => return Err(e), - }; +#[doc(hidden)] +pub use inventory; - // call the function - let result = match value.call_fn(cx).await { - Ok(r) => r, - Err(e) => return Err(e), - }; +#[macro_export] +macro_rules! initialize_server_fn_map { + ($req:ty, $res:ty) => { + once_cell::sync::Lazy::new(|| { + $crate::inventory::iter::> + .into_iter() + .map(|obj| (obj.path(), *obj)) + .collect() + }) + }; +} - // serialize the output - let result = match Self::encoding() { - Encoding::Url | Encoding::GetJSON => { - match serde_json::to_string(&result).map_err(|e| { - ServerFnError::Serialization(e.to_string()) - }) { - Ok(r) => Payload::Url(r), - Err(e) => return Err(e), - } - } - Encoding::Cbor | Encoding::GetCBOR => { - let mut buffer: Vec = Vec::new(); - match ciborium::ser::into_writer(&result, &mut buffer) - .map_err(|e| { - ServerFnError::Serialization(e.to_string()) - }) { - Ok(_) => Payload::Binary(buffer), - Err(e) => return Err(e), - } - } - }; +pub struct ServerFnTraitObj { + path: &'static str, + handler: fn(Req) -> Pin + Send>>, + middleware: fn() -> Vec>>, +} - Ok(result) - }) - as Pin>>> +impl ServerFnTraitObj { + pub const fn new( + path: &'static str, + handler: fn(Req) -> Pin + Send>>, + middleware: fn() -> Vec>>, + ) -> Self { + Self { + path, + handler, + middleware, + } } - /// Registers the server function explicitly on platforms that require it, - /// allowing the server to query it by URL. - #[cfg(any(feature = "ssr", doc,))] - fn register_in_explicit>( - ) -> Result<(), ServerFnError> { - // store it in the hashmap - R::register_explicit( - Self::prefix(), - Self::url(), - Self::call_from_bytes, - Self::encoding(), - ) - .map_err(|e| ServerFnError::Registration(e.to_string())) + pub fn path(&self) -> &'static str { + self.path } } -/// Executes the HTTP call to call a server function from the client, given its URL and argument type. -#[cfg(not(feature = "ssr"))] -pub async fn call_server_fn( - url: &str, - args: impl ServerFn, - enc: Encoding, -) -> Result +impl Service for ServerFnTraitObj where - T: serde::Serialize + serde::de::DeserializeOwned + Sized, + Req: Send + 'static, + Res: 'static, { - use ciborium::ser::into_writer; - use serde_json::Deserializer as JSONDeserializer; - #[cfg(not(target_arch = "wasm32"))] - let url = format!("{}{}", get_server_url(), url); - - #[derive(Debug)] - enum Payload { - Binary(Vec), - Url(String), + fn run(&mut self, req: Req) -> Pin + Send>> { + let handler = self.handler; + Box::pin(async move { handler(req).await }) } - let args_encoded = match &enc { - Encoding::Url | Encoding::GetJSON | Encoding::GetCBOR => Payload::Url( - serde_qs::to_string(&args) - .map_err(|e| ServerFnError::Serialization(e.to_string()))?, - ), - Encoding::Cbor => { - let mut buffer: Vec = Vec::new(); - into_writer(&args, &mut buffer) - .map_err(|e| ServerFnError::Serialization(e.to_string()))?; - Payload::Binary(buffer) - } - }; +} - let content_type_header = match &enc { - Encoding::Url | Encoding::GetJSON | Encoding::GetCBOR => { - "application/x-www-form-urlencoded" - } - Encoding::Cbor => "application/cbor", - }; +impl Clone for ServerFnTraitObj { + fn clone(&self) -> Self { + *self + } +} - let accept_header = match &enc { - Encoding::Url | Encoding::GetJSON => { - "application/x-www-form-urlencoded" - } - Encoding::Cbor | Encoding::GetCBOR => "application/cbor", - }; +impl Copy for ServerFnTraitObj {} - #[cfg(target_arch = "wasm32")] - let resp = match &enc { - Encoding::Url | Encoding::Cbor => match args_encoded { - Payload::Binary(b) => { - let slice_ref: &[u8] = &b; - let js_array = js_sys::Uint8Array::from(slice_ref).buffer(); - gloo_net::http::Request::post(url) - .header("Content-Type", content_type_header) - .header("Accept", accept_header) - .body(js_array) - .send() - .await - .map_err(|e| ServerFnError::Request(e.to_string()))? - } - Payload::Url(s) => gloo_net::http::Request::post(url) - .header("Content-Type", content_type_header) - .header("Accept", accept_header) - .body(s) - .send() - .await - .map_err(|e| ServerFnError::Request(e.to_string()))?, - }, - Encoding::GetCBOR | Encoding::GetJSON => match args_encoded { - Payload::Binary(_) => panic!( - "Binary data cannot be transferred via GET request in a query \ - string. Please try using the CBOR encoding." - ), - Payload::Url(s) => { - let full_url = format!("{url}?{s}"); - gloo_net::http::Request::get(&full_url) - .header("Content-Type", content_type_header) - .header("Accept", accept_header) - .send() - .await - .map_err(|e| ServerFnError::Request(e.to_string()))? - } - }, +type LazyServerFnMap = Lazy>>; + +// Axum integration +#[cfg(feature = "axum")] +pub mod axum { + use crate::{ + middleware::{BoxedService, Layer, Service}, + LazyServerFnMap, ServerFn, ServerFnTraitObj, }; - #[cfg(not(target_arch = "wasm32"))] - let resp = match &enc { - Encoding::Url | Encoding::Cbor => match args_encoded { - Payload::Binary(b) => CLIENT - .post(url) - .header("Content-Type", content_type_header) - .header("Accept", accept_header) - .body(b) - .send() - .await - .map_err(|e| ServerFnError::Request(e.to_string()))?, - Payload::Url(s) => CLIENT - .post(url) - .header("Content-Type", content_type_header) - .header("Accept", accept_header) - .body(s) - .send() - .await - .map_err(|e| ServerFnError::Request(e.to_string()))?, - }, - Encoding::GetJSON | Encoding::GetCBOR => match args_encoded { - Payload::Binary(_) => panic!( - "Binary data cannot be transferred via GET request in a query \ - string. Please try using the CBOR encoding." + use axum::body::Body; + use http::{Request, Response, StatusCode}; + + inventory::collect!(ServerFnTraitObj, Response>); + + static REGISTERED_SERVER_FUNCTIONS: LazyServerFnMap, Response> = + initialize_server_fn_map!(Request, Response); + + pub fn register_explicit() + where + T: ServerFn, ServerResponse = Response> + 'static, + { + REGISTERED_SERVER_FUNCTIONS.insert( + T::PATH, + ServerFnTraitObj::new( + T::PATH, + |req| Box::pin(T::run_on_server(req)), + T::middlewares, ), + ); + } - Payload::Url(s) => { - let full_url = format!("{url}?{s}"); - CLIENT - .get(full_url) - .header("Content-Type", content_type_header) - .header("Accept", accept_header) - .send() - .await - .map_err(|e| ServerFnError::Request(e.to_string()))? - } - }, - }; + pub async fn handle_server_fn(req: Request) -> Response { + let path = req.uri().path(); - // check for error status - let status = resp.status(); - #[cfg(not(target_arch = "wasm32"))] - let status = status.as_u16(); - if (400..=599).contains(&status) { - let text = resp.text().await.unwrap_or_default(); - return Err(match serde_json::from_str(&text) { - Ok(e) => e, - Err(_) => { - #[cfg(target_arch = "wasm32")] - let status_text = resp.status_text(); - #[cfg(not(target_arch = "wasm32"))] - let status_text = status.to_string(); - ServerFnError::ServerError(if text.is_empty() { - format!("{} {}", status, status_text) - } else { - format!("{} {}: {}", status, status_text, text) - }) + if let Some(server_fn) = REGISTERED_SERVER_FUNCTIONS.get(path) { + let middleware = (server_fn.middleware)(); + let mut service = BoxedService::new(*server_fn); + for middleware in middleware { + service = middleware.layer(service); } - }); - } - - // Decoding the body of the request - if (enc == Encoding::Cbor) || (enc == Encoding::GetCBOR) { - #[cfg(target_arch = "wasm32")] - let binary = resp - .binary() - .await - .map_err(|e| ServerFnError::Deserialization(e.to_string()))?; - #[cfg(target_arch = "wasm32")] - let binary = binary.as_slice(); - #[cfg(not(target_arch = "wasm32"))] - let binary = resp - .bytes() - .await - .map_err(|e| ServerFnError::Deserialization(e.to_string()))?; - #[cfg(not(target_arch = "wasm32"))] - let binary = binary.as_ref(); - - ciborium::de::from_reader(binary) - .map_err(|e| ServerFnError::Deserialization(e.to_string())) - } else { - let text = resp - .text() - .await - .map_err(|e| ServerFnError::Deserialization(e.to_string()))?; - - let mut deserializer = JSONDeserializer::from_str(&text); - T::deserialize(&mut deserializer) - .map_err(|e| ServerFnError::Deserialization(e.to_string())) + service.run(req).await + } else { + Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Body::from(format!( + "Could not find a server function at the route {path}. \n\nIt's likely that either\n 1. The API prefix you specify in the `#[server]` macro doesn't match the prefix at which your server function handler is mounted, or \n2. You are on a platform that doesn't support automatic server function registration and you need to call ServerFn::register_explicit() on the server function type, somewhere in your `main` function.", + ))) + .unwrap() + } } } -// Lazily initialize the client to be reused for all server function calls. -#[cfg(any(all(not(feature = "ssr"), not(target_arch = "wasm32")), doc))] -static CLIENT: once_cell::sync::Lazy = - once_cell::sync::Lazy::new(reqwest::Client::new); - -#[cfg(any(all(not(feature = "ssr"), not(target_arch = "wasm32")), doc))] -static ROOT_URL: once_cell::sync::OnceCell<&'static str> = - once_cell::sync::OnceCell::new(); - -#[cfg(any(all(not(feature = "ssr"), not(target_arch = "wasm32")), doc))] -/// Set the root server url that all server function paths are relative to for the client. On WASM this will default to the origin. -pub fn set_server_url(url: &'static str) { - ROOT_URL.set(url).unwrap(); -} +// Actix integration +#[cfg(feature = "actix")] +pub mod actix { + use actix_web::{HttpRequest, HttpResponse}; + use send_wrapper::SendWrapper; + + use crate::request::actix::ActixRequest; + use crate::response::actix::ActixResponse; + use crate::{LazyServerFnMap, ServerFn, ServerFnTraitObj}; + + inventory::collect!(ServerFnTraitObj); + + static REGISTERED_SERVER_FUNCTIONS: LazyServerFnMap = + initialize_server_fn_map!(ActixRequest, ActixResponse); + + pub fn register_explicit() + where + T: ServerFn + 'static, + { + REGISTERED_SERVER_FUNCTIONS.insert( + T::PATH, + ServerFnTraitObj::new(T::PATH, |req| Box::pin(T::run_on_server(req))), + ); + } -#[cfg(all(not(feature = "ssr"), not(target_arch = "wasm32")))] -fn get_server_url() -> &'static str { - ROOT_URL - .get() - .expect("Call set_root_url before calling a server function.") + pub async fn handle_server_fn(req: HttpRequest) -> HttpResponse { + let path = req.uri().path(); + if let Some(server_fn) = REGISTERED_SERVER_FUNCTIONS.get(path) { + server_fn + .run(ActixRequest(SendWrapper::new(req))) + .await + .0 + .take() + } else { + HttpResponse::BadRequest().body(format!( + "Could not find a server function at the route {path}. \n\nIt's likely that either\n 1. The API prefix you specify in the `#[server]` macro doesn't match the prefix at which your server function handler is mounted, or \n2. You are on a platform that doesn't support automatic server function registration and you need to call ServerFn::register_explicit() on the server function type, somewhere in your `main` function.", + )) + } + } } diff --git a/server_fn/src/middleware/mod.rs b/server_fn/src/middleware/mod.rs new file mode 100644 index 0000000000..1f16ec299a --- /dev/null +++ b/server_fn/src/middleware/mod.rs @@ -0,0 +1,109 @@ +use std::{future::Future, pin::Pin}; + +pub trait Layer: Send + Sync + 'static { + fn layer(&self, inner: BoxedService) -> BoxedService; +} + +pub struct BoxedService(pub Box + Send>); + +impl BoxedService { + pub fn new(service: impl Service + Send + 'static) -> Self { + Self(Box::new(service)) + } +} + +pub trait Service { + fn run(&mut self, req: Request) -> Pin + Send>>; +} + +#[cfg(feature = "axum")] +mod axum { + use crate::{response::Res, ServerFnError}; + use axum::body::Body; + use http::{Request, Response}; + use std::fmt::{Debug, Display}; + use std::future::Future; + use std::pin::Pin; + + use super::{BoxedService, Service}; + + impl super::Service, Response> for S + where + S: tower::Service, Response = Response>, + S::Future: Send + 'static, + S::Error: Into + Send + Debug + Display + Sync + 'static, + { + fn run( + &mut self, + req: Request, + ) -> Pin> + Send>> { + let inner = self.call(req); + Box::pin(async move { + inner.await.unwrap_or_else(|e| { + let err = ServerFnError::from(e); + Response::::error_response(err) + }) + }) + } + } + + impl tower::Service> for BoxedService, Response> { + type Response = Response; + type Error = ServerFnError; + type Future = + Pin> + Send>>; + + fn poll_ready( + &mut self, + _cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + Ok(()).into() + } + + fn call(&mut self, req: Request) -> Self::Future { + let inner = self.0.run(req); + Box::pin(async move { Ok(inner.await) }) + } + } + + impl super::Layer, Response> for L + where + L: tower_layer::Layer, Response>> + Sync + Send + 'static, + L::Service: Service, Response> + Send + 'static, + { + fn layer( + &self, + inner: BoxedService, Response>, + ) -> BoxedService, Response> { + BoxedService(Box::new(self.layer(inner))) + } + } +} + +#[cfg(feature = "actix")] +mod actix { + use crate::{ + response::{actix::ActixResponse, Res}, + ServerFnError, + }; + use actix_web::{HttpRequest, HttpResponse}; + use std::fmt::{Debug, Display}; + 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, + { + fn run(&mut self, req: HttpRequest) -> Pin + Send>> { + let inner = self.call(req); + Box::pin(async move { + inner.await.unwrap_or_else(|e| { + let err = ServerFnError::from(e); + ActixResponse::error_response(err).into_inner() + }) + }) + } + } +} diff --git a/server_fn/src/redirect.rs b/server_fn/src/redirect.rs new file mode 100644 index 0000000000..42fc805241 --- /dev/null +++ b/server_fn/src/redirect.rs @@ -0,0 +1,13 @@ +use std::sync::OnceLock; + +static REDIRECT_HOOK: OnceLock> = OnceLock::new(); + +pub fn set_redirect_hook(hook: impl Fn(&str) + Send + Sync + 'static) { + REDIRECT_HOOK.set(Box::new(hook)); +} + +pub fn call_redirect_hook(path: &str) { + if let Some(hook) = REDIRECT_HOOK.get() { + hook(path) + } +} diff --git a/server_fn/src/request/actix.rs b/server_fn/src/request/actix.rs new file mode 100644 index 0000000000..9354793205 --- /dev/null +++ b/server_fn/src/request/actix.rs @@ -0,0 +1,50 @@ +use crate::{error::ServerFnError, request::Req}; +use actix_web::{FromRequest, HttpRequest}; +use bytes::Bytes; +use futures::Stream; +use send_wrapper::SendWrapper; +use std::future::Future; + +pub struct ActixRequest(pub(crate) SendWrapper); + +impl Req for ActixRequest { + fn as_query(&self) -> Option<&str> { + self.0.uri().query() + } + + fn to_content_type(&self) -> Option { + self.0 + .headers() + .get("Content-Type") + .map(|h| String::from_utf8_lossy(h.as_bytes()).to_string()) + } + + 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 { + Bytes::extract(&self.0) + .await + .map_err(|e| ServerFnError::Deserialization(e.to_string())) + }) + } + + 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 { + String::extract(&self.0) + .await + .map_err(|e| ServerFnError::Deserialization(e.to_string())) + }) + } + + fn try_into_stream( + self, + ) -> Result> + Send, ServerFnError> + { + Ok(futures::stream::once(async { todo!() })) + } +} diff --git a/server_fn/src/request/axum.rs b/server_fn/src/request/axum.rs new file mode 100644 index 0000000000..457fe650f2 --- /dev/null +++ b/server_fn/src/request/axum.rs @@ -0,0 +1,43 @@ +use crate::{error::ServerFnError, request::Req}; +use axum::body::{Body, Bytes}; +use futures::{Stream, StreamExt}; +use http::{header::CONTENT_TYPE, Request}; +use http_body_util::BodyExt; + +impl Req for Request { + fn as_query(&self) -> Option<&str> { + self.uri().query() + } + + fn to_content_type(&self) -> Option { + self.headers() + .get(CONTENT_TYPE) + .map(|h| String::from_utf8_lossy(h.as_bytes()).to_string()) + } + + 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())) + } + + async fn try_into_string(self) -> Result> { + let bytes = self.try_into_bytes().await?; + let body = String::from_utf8(bytes.to_vec()) + .map_err(|e| ServerFnError::Deserialization(e.to_string())); + body + } + + fn try_into_stream( + self, + ) -> Result> + Send, ServerFnError> + { + Ok(self + .into_body() + .into_data_stream() + .map(|chunk| chunk.map_err(|e| ServerFnError::Deserialization(e.to_string())))) + } +} diff --git a/server_fn/src/request/browser.rs b/server_fn/src/request/browser.rs new file mode 100644 index 0000000000..f985f1ef41 --- /dev/null +++ b/server_fn/src/request/browser.rs @@ -0,0 +1,93 @@ +use crate::error::ServerFnError; + +use super::ClientReq; +use bytes::Bytes; +pub use gloo_net::http::Request; +use js_sys::Uint8Array; +use send_wrapper::SendWrapper; +use web_sys::FormData; + +#[derive(Debug)] +pub struct BrowserRequest(pub(crate) SendWrapper); + +impl From for BrowserRequest { + fn from(value: Request) -> Self { + Self(SendWrapper::new(value)) + } +} + +#[derive(Debug)] +pub struct BrowserFormData(pub(crate) SendWrapper); + +impl From for BrowserFormData { + fn from(value: FormData) -> Self { + Self(SendWrapper::new(value)) + } +} + +impl ClientReq for BrowserRequest { + type FormData = BrowserFormData; + + fn try_new_get( + path: &str, + accepts: &str, + content_type: &str, + query: &str, + ) -> Result> { + let mut url = path.to_owned(); + url.push('?'); + url.push_str(query); + Ok(Self(SendWrapper::new( + Request::get(&url) + .header("Content-Type", content_type) + .header("Accept", accepts) + .build() + .map_err(|e| ServerFnError::Request(e.to_string()))?, + ))) + } + + fn try_new_post( + path: &str, + accepts: &str, + content_type: &str, + body: String, + ) -> Result> { + Ok(Self(SendWrapper::new( + Request::post(path) + .header("Content-Type", content_type) + .header("Accept", accepts) + .body(body) + .map_err(|e| ServerFnError::Request(e.to_string()))?, + ))) + } + + fn try_new_post_bytes( + path: &str, + accepts: &str, + content_type: &str, + body: Bytes, + ) -> Result> { + let body: &[u8] = &body; + let body = Uint8Array::from(body).buffer(); + Ok(Self(SendWrapper::new( + Request::post(path) + .header("Content-Type", content_type) + .header("Accept", accepts) + .body(body) + .map_err(|e| ServerFnError::Request(e.to_string()))?, + ))) + } + + fn try_new_multipart( + path: &str, + accepts: &str, + body: Self::FormData, + ) -> Result> { + Ok(Self(SendWrapper::new( + Request::post(path) + .header("Accept", accepts) + .body(body.0.take()) + .map_err(|e| ServerFnError::Request(e.to_string()))?, + ))) + } +} diff --git a/server_fn/src/request/mod.rs b/server_fn/src/request/mod.rs new file mode 100644 index 0000000000..ee5a3f60df --- /dev/null +++ b/server_fn/src/request/mod.rs @@ -0,0 +1,103 @@ +use crate::error::ServerFnError; +use bytes::Bytes; +use futures::Stream; +use std::future::Future; + +#[cfg(feature = "actix")] +pub mod actix; +#[cfg(feature = "axum")] +pub mod axum; +#[cfg(feature = "browser")] +pub mod browser; +#[cfg(feature = "reqwest")] +pub mod reqwest; + +/// Represents a request as made by the client. +pub trait ClientReq +where + Self: Sized, +{ + type FormData; + + fn try_new_get( + path: &str, + content_type: &str, + accepts: &str, + query: &str, + ) -> Result>; + + fn try_new_post( + path: &str, + content_type: &str, + accepts: &str, + body: String, + ) -> Result>; + + fn try_new_post_bytes( + path: &str, + content_type: &str, + accepts: &str, + body: Bytes, + ) -> Result>; + + fn try_new_multipart( + path: &str, + accepts: &str, + body: Self::FormData, + ) -> Result>; +} + +/// Represents the request as received by the server. +pub trait Req +where + Self: Sized, +{ + /// Returns the query string of the request’s URL, starting after the `?`. + fn as_query(&self) -> Option<&str>; + + /// Returns the `Content-Type` header, if any. + fn to_content_type(&self) -> Option; + + /// Attempts to extract the body of the request into [`Bytes`]. + 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; + + /// Attempts to convert the body of the request into a string. + fn try_into_stream( + self, + ) -> Result> + Send, ServerFnError>; +} + +/// 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 { + fn as_query(&self) -> Option<&str> { + unreachable!() + } + + fn to_content_type(&self) -> Option { + unreachable!() + } + + fn try_into_bytes(self) -> impl Future>> + Send { + async { unreachable!() } + } + + fn try_into_string( + self, + ) -> impl Future>> + Send { + async { unreachable!() } + } + + fn try_into_stream( + self, + ) -> Result> + Send, ServerFnError> + { + Ok(futures::stream::once(async { unreachable!() })) + } +} diff --git a/server_fn/src/request/reqwest.rs b/server_fn/src/request/reqwest.rs new file mode 100644 index 0000000000..b03a26db4b --- /dev/null +++ b/server_fn/src/request/reqwest.rs @@ -0,0 +1,93 @@ +use std::sync::OnceLock; + +use crate::error::ServerFnError; + +use super::ClientReq; +use bytes::Bytes; +use once_cell::sync::Lazy; +use reqwest::header::{ACCEPT, CONTENT_TYPE}; +pub use reqwest::{multipart::Form, Client, Method, Request, Url}; + +pub(crate) static CLIENT: Lazy = Lazy::new(Client::new); +static ROOT_URL: OnceLock<&'static str> = OnceLock::new(); + +/// Set the root server url that all server function paths are relative to for the client. +/// +/// If this is not set, it defaults to the origin. +pub fn set_server_url(url: &'static str) { + ROOT_URL.set(url).unwrap(); +} + +fn get_server_url() -> &'static str { + ROOT_URL + .get() + .expect("Call `set_root_url` before calling a server function.") +} + +impl ClientReq for Request { + type FormData = Form; + + fn try_new_get( + path: &str, + accepts: &str, + content_type: &str, + query: &str, + ) -> 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()))?; + 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()))?; + Ok(req) + } + + fn try_new_post( + path: &str, + accepts: &str, + content_type: &str, + body: String, + ) -> Result> { + let url = format!("{}{}", get_server_url(), path); + Ok(CLIENT + .post(url) + .header(CONTENT_TYPE, content_type) + .header(ACCEPT, accepts) + .body(body) + .build() + .map_err(|e| ServerFnError::Request(e.to_string()))?) + } + + fn try_new_post_bytes( + path: &str, + accepts: &str, + content_type: &str, + body: Bytes, + ) -> Result> { + let url = format!("{}{}", get_server_url(), path); + Ok(CLIENT + .post(url) + .header(CONTENT_TYPE, content_type) + .header(ACCEPT, accepts) + .body(body) + .build() + .map_err(|e| ServerFnError::Request(e.to_string()))?) + } + + fn try_new_multipart( + path: &str, + accepts: &str, + body: Self::FormData, + ) -> Result> { + Ok(CLIENT + .post(path) + .header(ACCEPT, accepts) + .multipart(body) + .build() + .map_err(|e| ServerFnError::Request(e.to_string()))?) + } +} diff --git a/server_fn/src/response/actix.rs b/server_fn/src/response/actix.rs new file mode 100644 index 0000000000..9472113cec --- /dev/null +++ b/server_fn/src/response/actix.rs @@ -0,0 +1,51 @@ +use super::Res; +use crate::error::ServerFnError; +use actix_web::{http::header, http::StatusCode, HttpResponse}; +use bytes::Bytes; +use futures::Stream; +use send_wrapper::SendWrapper; +use std::fmt::Display; + +pub struct ActixResponse(pub(crate) SendWrapper); + +impl ActixResponse { + pub fn into_inner(self) -> HttpResponse { + self.0.take() + } +} + +impl Res for ActixResponse +where + CustErr: Display, +{ + fn try_from_string(content_type: &str, data: String) -> Result> { + let mut builder = HttpResponse::build(StatusCode::OK); + Ok(ActixResponse(SendWrapper::new( + builder + .insert_header((header::CONTENT_TYPE, content_type)) + .body(data), + ))) + } + + fn try_from_bytes(content_type: &str, data: Bytes) -> Result> { + let mut builder = HttpResponse::build(StatusCode::OK); + Ok(ActixResponse(SendWrapper::new( + builder + .insert_header((header::CONTENT_TYPE, content_type)) + .body(data), + ))) + } + + fn error_response(err: ServerFnError) -> Self { + ActixResponse(SendWrapper::new( + HttpResponse::build(StatusCode::INTERNAL_SERVER_ERROR).body(err.to_string()), + )) + } + + fn try_from_stream( + content_type: &str, + data: impl Stream>>, + ) -> Result> { + todo!() + } +} diff --git a/server_fn/src/response/browser.rs b/server_fn/src/response/browser.rs new file mode 100644 index 0000000000..8132ae8813 --- /dev/null +++ b/server_fn/src/response/browser.rs @@ -0,0 +1,73 @@ +use crate::error::ServerFnError; + +use super::ClientRes; +use bytes::Bytes; +use futures::{Stream, StreamExt}; +pub use gloo_net::http::Response; +use js_sys::{wasm_bindgen::JsCast, Uint8Array}; +use send_wrapper::SendWrapper; +use std::future::Future; +use wasm_streams::ReadableStream; + +pub struct BrowserResponse(pub(crate) SendWrapper); + +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())) + }) + } + + 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())) + }) + } + + fn try_into_stream( + self, + ) -> Result< + impl Stream> + Send + 'static, + ServerFnError, + > { + let stream = ReadableStream::from_raw(self.0.body().unwrap()) + .into_stream() + .map(|data| { + let data = data.unwrap().unchecked_into::(); + let mut buf = Vec::new(); + let length = data.length(); + buf.resize(length as usize, 0); + data.copy_to(&mut buf); + Ok(Bytes::from(buf)) + }); + Ok(SendWrapper::new(stream)) + } + + fn status(&self) -> u16 { + self.0.status() + } + + fn status_text(&self) -> String { + self.0.status_text() + } + + fn location(&self) -> String { + self.0 + .headers() + .get("Location") + .unwrap_or_else(|| self.0.url()) + } +} diff --git a/server_fn/src/response/http.rs b/server_fn/src/response/http.rs new file mode 100644 index 0000000000..1731f3715e --- /dev/null +++ b/server_fn/src/response/http.rs @@ -0,0 +1,50 @@ +use super::Res; +use crate::error::{ServerFnError, ServerFnErrorErr}; +use axum::body::Body; +use bytes::Bytes; +use futures::{Stream, StreamExt}; +use http::Response; +use std::fmt::{Debug, Display}; + +impl Res for Response +where + CustErr: Send + Sync + Debug + Display + 'static, +{ + 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())) + } + + 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())) + } + + 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))); + 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())) + } + + fn error_response(err: ServerFnError) -> Self { + Response::builder() + .status(http::StatusCode::INTERNAL_SERVER_ERROR) + .body(Body::from(err.to_string())) + .unwrap() + } +} diff --git a/server_fn/src/response/mod.rs b/server_fn/src/response/mod.rs new file mode 100644 index 0000000000..d58c94feea --- /dev/null +++ b/server_fn/src/response/mod.rs @@ -0,0 +1,85 @@ +#[cfg(feature = "actix")] +pub mod actix; +#[cfg(feature = "browser")] +pub mod browser; +#[cfg(feature = "axum")] +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 +where + Self: Sized, +{ + /// Attempts to convert a UTF-8 string into an HTTP response. + 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>; + + /// Attempts to convert a stream of bytes into an HTTP response. + fn try_from_stream( + content_type: &str, + data: impl Stream>> + Send + 'static, + ) -> Result>; + + fn error_response(err: ServerFnError) -> Self; +} + +/// Represents the response as received by the client. +pub trait ClientRes { + /// Attempts to extract a UTF-8 string from an HTTP response. + 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; + + /// Attempts to extract a binary stream from an HTTP response. + fn try_into_stream( + self, + ) -> Result< + impl Stream> + Send + 'static, + ServerFnError, + >; + + /// HTTP status code of the response. + fn status(&self) -> u16; + + /// Status text for the status code. + fn status_text(&self) -> String; + + /// The `Location` header or (if none is set), the URL of the response. + fn location(&self) -> String; +} + +/// A mocked response type that can be used in place of the actual server response, +/// when compiling for the browser. +pub struct BrowserMockRes; + +impl Res for BrowserMockRes { + fn try_from_string(content_type: &str, data: String) -> Result> { + unreachable!() + } + + fn try_from_bytes(content_type: &str, data: Bytes) -> Result> { + unreachable!() + } + + fn error_response(err: ServerFnError) -> Self { + unreachable!() + } + + fn try_from_stream( + content_type: &str, + data: impl Stream>>, + ) -> Result> { + todo!() + } +} diff --git a/server_fn/src/response/reqwest.rs b/server_fn/src/response/reqwest.rs new file mode 100644 index 0000000000..d3c697cb04 --- /dev/null +++ b/server_fn/src/response/reqwest.rs @@ -0,0 +1,45 @@ +use super::ClientRes; +use crate::error::ServerFnError; +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())) + } + + async fn try_into_bytes(self) -> Result> { + self.bytes() + .await + .map_err(|e| ServerFnError::Deserialization(e.to_string())) + } + + fn try_into_stream( + self, + ) -> Result< + impl Stream> + Send + 'static, + ServerFnError, + > { + Ok(self + .bytes_stream() + .map_err(|e| ServerFnError::Response(e.to_string()))) + } + + fn status(&self) -> u16 { + self.status().as_u16() + } + + fn status_text(&self) -> String { + self.status().to_string() + } + + fn location(&self) -> String { + self.headers() + .get("Location") + .map(|value| String::from_utf8_lossy(value.as_bytes()).to_string()) + .unwrap_or_else(|| self.url().to_string()) + } +} diff --git a/server_fn_macro/Cargo.toml b/server_fn_macro/Cargo.toml index 226f7eb7fc..5fb3b6b561 100644 --- a/server_fn_macro/Cargo.toml +++ b/server_fn_macro/Cargo.toml @@ -1,22 +1,22 @@ [package] name = "server_fn_macro" -version = { workspace = true } +version = "0.6.0" edition = "2021" authors = ["Greg Johnston"] license = "MIT" -repository = "https://github.com/leptos-rs/leptos" description = "RPC for any web framework." readme = "../README.md" [dependencies] -serde = { version = "1", features = ["derive"] } quote = "1" syn = { version = "2", features = ["full", "parsing", "extra-traits"] } proc-macro2 = "1" -proc-macro-error = { version = "1", default-features = false } xxhash-rust = { version = "0.8.6", features = ["const_xxh64"] } const_format = "0.2.30" +convert_case = "0.6.0" [features] nightly = [] ssr = [] +actix = [] +axum = [] diff --git a/server_fn_macro/Makefile.toml b/server_fn_macro/Makefile.toml deleted file mode 100644 index 3d822c68da..0000000000 --- a/server_fn_macro/Makefile.toml +++ /dev/null @@ -1 +0,0 @@ -extend = { path = "../cargo-make/main.toml" } diff --git a/server_fn_macro/src/lib.rs b/server_fn_macro/src/lib.rs index 18f8a36d29..c4a1201bdc 100644 --- a/server_fn_macro/src/lib.rs +++ b/server_fn_macro/src/lib.rs @@ -1,16 +1,14 @@ #![cfg_attr(feature = "nightly", feature(proc_macro_span))] #![forbid(unsafe_code)] #![deny(missing_docs)] -// to prevent warnings from popping up when a nightly feature is stabilized -#![allow(stable_features)] //! Implementation of the `server_fn` macro. //! //! This crate contains the implementation of the `server_fn` macro. [`server_macro_impl`] can be used to implement custom versions of the macro for different frameworks that allow users to pass a custom context from the server to the server function. +use convert_case::{Case, Converter}; use proc_macro2::{Literal, Span, TokenStream as TokenStream2}; -use proc_macro_error::abort; -use quote::{quote, quote_spanned}; +use quote::{quote, quote_spanned, ToTokens}; use syn::{ parse::{Parse, ParseStream}, punctuated::Punctuated, @@ -18,30 +16,6 @@ use syn::{ *, }; -/// Describes the custom context from the server that passed to the server function. Optionally, the first argument of a server function -/// can be a custom context of this type. This context can be used to access the server's state within the server function. -pub struct ServerContext { - /// The type of the context. - pub ty: Ident, - /// The path to the context type. Used to reference the context type in the generated code. - pub path: Path, -} - -fn fn_arg_is_cx(f: &syn::FnArg, server_context: &ServerContext) -> bool { - if let FnArg::Typed(t) = f { - if let Type::Path(path) = &*t.ty { - path.path - .segments - .iter() - .any(|segment| segment.ident == server_context.ty) - } else { - false - } - } else { - false - } -} - /// The implementation of the `server_fn` macro. /// To allow the macro to accept a custom context from the server, pass a custom server context to this function. /// **The Context comes from the server.** Optionally, the first argument of a server function @@ -53,14 +27,9 @@ fn fn_arg_is_cx(f: &syn::FnArg, server_context: &ServerContext) -> bool { /// ```ignore /// #[proc_macro_attribute] /// pub fn server(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream { -/// let server_context = Some(ServerContext { -/// ty: syn::parse_quote!(MyContext), -/// path: syn::parse_quote!(my_crate::prelude::MyContext), -/// }); /// match server_macro_impl( /// args.into(), /// s.into(), -/// Some(server_context), /// Some(syn::parse_quote!(my_crate::exports::server_fn)), /// ) { /// Err(e) => e.to_compile_error().into(), @@ -72,21 +41,57 @@ pub fn server_macro_impl( args: TokenStream2, body: TokenStream2, trait_obj_wrapper: Type, - server_context: Option, server_fn_path: Option, + default_path: &str, ) -> Result { - let ServerFnName { + let mut body = syn::parse::(body.into())?; + + // extract all #[middleware] attributes, removing them from signature of dummy + let mut middlewares: Vec = vec![]; + body.attrs.retain(|attr| { + if attr.meta.path().is_ident("middleware") { + if let Ok(middleware) = attr.parse_args() { + middlewares.push(middleware); + false + } else { + true + } + } else { + true + } + }); + + let dummy = body.to_dummy_output(); + let dummy_name = body.to_dummy_ident(); + let args = syn::parse::(args.into())?; + + // default values for args + let ServerFnArgs { struct_name, prefix, - encoding, + input, + output, fn_path, - .. - } = syn::parse2::(args)?; - let prefix = prefix.unwrap_or_else(|| Literal::string("")); + } = args; + let prefix = prefix.unwrap_or_else(|| Literal::string(default_path)); let fn_path = fn_path.unwrap_or_else(|| Literal::string("")); - let encoding = quote!(#server_fn_path::#encoding); + let input = input.unwrap_or_else(|| syn::parse_quote!(PostUrl)); + let input_is_rkyv = input == "Rkyv"; + let input_is_multipart = input == "MultipartFormData"; + let input = codec_ident(server_fn_path.as_ref(), input); + let output = output.unwrap_or_else(|| syn::parse_quote!(Json)); + let output = codec_ident(server_fn_path.as_ref(), output); + // default to PascalCase version of function name if no struct name given + let struct_name = struct_name.unwrap_or_else(|| { + let upper_camel_case_name = Converter::new() + .from_case(Case::Snake) + .to_case(Case::UpperCamel) + .convert(body.ident.to_string()); + Ident::new(&upper_camel_case_name, body.ident.span()) + }); - let mut body = syn::parse::(body.into())?; + // build struct for type + let mut body = body; let fn_name = &body.ident; let fn_name_as_str = body.ident.to_string(); let vis = body.vis; @@ -96,23 +101,17 @@ pub fn server_macro_impl( let fields = body .inputs .iter_mut() - .filter(|f| { - if let Some(ctx) = &server_context { - !fn_arg_is_cx(f, ctx) - } else { - true - } - }) .map(|f| { let typed_arg = match f { FnArg::Receiver(_) => { - abort!( - f, - "cannot use receiver types in server function macro" - ) + return Err(syn::Error::new( + f.span(), + "cannot use receiver types in server function macro", + )) } FnArg::Typed(t) => t, }; + // allow #[server(default)] on fields — TODO is this documented? let mut default = false; let mut other_attrs = Vec::new(); for attr in typed_arg.attrs.iter() { @@ -141,87 +140,76 @@ pub fn server_macro_impl( }) .collect::>>()?; - let cx_arg = body.inputs.iter().next().and_then(|f| { - server_context - .as_ref() - .and_then(|ctx| fn_arg_is_cx(f, ctx).then_some(f)) - }); - let cx_fn_arg = if cx_arg.is_some() { - quote! { cx, } - } else { - quote! {} - }; + let fn_args = body + .inputs + .iter() + .filter_map(|f| match f { + FnArg::Receiver(_) => None, + FnArg::Typed(t) => Some(t), + }) + .collect::>(); - let fn_args = body.inputs.iter().map(|f| { - let typed_arg = match f { - FnArg::Receiver(_) => { - abort!(f, "cannot use receiver types in server function macro") - } - FnArg::Typed(t) => t, - }; - let is_cx = if let Some(ctx) = &server_context { - fn_arg_is_cx(f, ctx) - } else { - false - }; - if is_cx { + let field_names = body + .inputs + .iter() + .filter_map(|f| match f { + FnArg::Receiver(_) => None, + FnArg::Typed(t) => Some(&t.pat), + }) + .collect::>(); + + // if there's exactly one field, impl From for the struct + let first_field = body + .inputs + .iter() + .filter_map(|f| match f { + FnArg::Receiver(_) => None, + FnArg::Typed(t) => Some((&t.pat, &t.ty)), + }) + .next(); + let from_impl = + (body.inputs.len() == 1 && first_field.is_some()).then(|| { + let field = first_field.unwrap(); + let (name, ty) = field; quote! { - #[allow(unused)] - #typed_arg - } - } else { - quote! { #typed_arg } - } - }); - let fn_args_2 = fn_args.clone(); - - let field_names = body.inputs.iter().filter_map(|f| match f { - FnArg::Receiver(_) => todo!(), - FnArg::Typed(t) => { - if let Some(ctx) = &server_context { - if fn_arg_is_cx(f, ctx) { - None - } else { - Some(&t.pat) + impl From<#struct_name> for #ty { + fn from(value: #struct_name) -> Self { + let #struct_name { #name } = value; + #name + } } - } else { - Some(&t.pat) - } - } - }); - let field_names_2 = field_names.clone(); - let field_names_3 = field_names.clone(); - let field_names_4 = field_names.clone(); - let field_names_5 = field_names.clone(); + impl From<#ty> for #struct_name { + fn from(#name: #ty) -> Self { + #struct_name { #name } + } + } + } + }); + // check output type let output_arrow = body.output_arrow; let return_ty = body.return_ty; - let output_ty = 'output_ty: { - if let syn::Type::Path(pat) = &return_ty { - if pat.path.segments[0].ident == "Result" { - if let PathArguments::AngleBracketed(args) = - &pat.path.segments[0].arguments - { - break 'output_ty &args.args[0]; - } + let output_ty = output_type(&return_ty)?; + let error_ty = err_type(&return_ty)?; + let error_ty = + error_ty.map(ToTokens::to_token_stream).unwrap_or_else(|| { + quote! { + #server_fn_path::error::NoCustomError } - } - - abort!( - return_ty, - "server functions should return Result" - ); - }; - - let server_ctx_path = if let Some(ctx) = &server_context { - let path = &ctx.path; - quote!(#path) - } else { - quote!(()) - }; + }); + // build server fn path + let serde_path = server_fn_path.as_ref().map(|path| { + let path = path + .segments + .iter() + .map(|segment| segment.ident.to_string()) + .collect::>(); + let path = path.join("::"); + format!("{path}::serde") + }); let server_fn_path = server_fn_path .map(|path| quote!(#path)) .unwrap_or_else(|| quote! { server_fn }); @@ -239,43 +227,49 @@ pub fn server_macro_impl( #[doc = #link_to_server_fn] }; + // pass through docs let docs = body .docs .iter() .map(|(doc, span)| quote_spanned!(*span=> #[doc = #doc])) .collect::(); + // auto-registration with inventory let inventory = if cfg!(feature = "ssr") { quote! { - #server_fn_path::inventory::submit! { - #trait_obj_wrapper::from_generic_server_fn(#server_fn_path::ServerFnTraitObj::new( - #struct_name::PREFIX, - #struct_name::URL, - #struct_name::ENCODING, - <#struct_name as #server_fn_path::ServerFn<#server_ctx_path>>::call_from_bytes, - )) - } + #server_fn_path::inventory::submit! {{ + use #server_fn_path::ServerFn; + #server_fn_path::ServerFnTraitObj::new( + #struct_name::PATH, + |req| { + Box::pin(#struct_name::run_on_server(req)) + }, + #struct_name::middlewares + ) + }} } } else { quote! {} }; - let call_fn = if cfg!(feature = "ssr") { + // run_body in the trait implementation + let run_body = if cfg!(feature = "ssr") { quote! { - fn call_fn(self, cx: #server_ctx_path) -> std::pin::Pin>>> { + async fn run_body(self) -> #return_ty { let #struct_name { #(#field_names),* } = self; - Box::pin(async move { #fn_name( #cx_fn_arg #(#field_names_2),*).await }) + #dummy_name(#(#field_names),*).await } } } else { quote! { - fn call_fn_client(self, cx: #server_ctx_path) -> std::pin::Pin>>> { - let #struct_name { #(#field_names_3),* } = self; - Box::pin(async move { #fn_name( #cx_fn_arg #(#field_names_4),*).await }) + #[allow(unused_variables)] + async fn run_body(self) -> #return_ty { + unreachable!() } } }; + // the actual function definition let func = if cfg!(feature = "ssr") { quote! { #docs @@ -289,114 +283,414 @@ pub fn server_macro_impl( #docs #(#attrs)* #[allow(unused_variables)] - #vis async fn #fn_name(#(#fn_args_2),*) #output_arrow #return_ty { - #server_fn_path::call_server_fn( - &{ - let prefix = #struct_name::PREFIX.to_string(); - prefix + "/" + #struct_name::URL - }, - #struct_name { #(#field_names_5),* }, - #encoding - ).await + #vis async fn #fn_name(#(#fn_args),*) #output_arrow #return_ty { + use #server_fn_path::ServerFn; + let data = #struct_name { #(#field_names),* }; + data.run_on_client().await } } }; + // TODO rkyv derives + let derives = if input_is_multipart { + quote! {} + } else if input_is_rkyv { + todo!("implement derives for Rkyv") + } else { + quote! { + Clone, #server_fn_path::serde::Serialize, #server_fn_path::serde::Deserialize + } + }; + let serde_path = (!input_is_multipart && !input_is_rkyv).then(|| { + quote! { + #[serde(crate = #serde_path)] + } + }); + + // TODO reqwest + let client = quote! { + #server_fn_path::client::browser::BrowserClient + }; + + // TODO Actix etc + let req = if !cfg!(feature = "ssr") { + quote! { + #server_fn_path::request::BrowserMockReq + } + } else if cfg!(feature = "axum") { + quote! { + ::axum::http::Request<::axum::body::Body> + } + } else if cfg!(feature = "actix") { + quote! { + ::actix_web::HttpRequest + } + } else { + return Err(syn::Error::new( + Span::call_site(), + "If the `ssr` feature is enabled, either the `actix` or `axum` \ + features should also be enabled.", + )); + }; + let res = if !cfg!(feature = "ssr") { + quote! { + #server_fn_path::response::BrowserMockRes + } + } else if cfg!(feature = "axum") { + quote! { + ::axum::http::Response<::axum::body::Body> + } + } else if cfg!(feature = "actix") { + quote! { + ::actix_web::HttpResponse + } + } else { + return Err(syn::Error::new( + Span::call_site(), + "If the `ssr` feature is enabled, either the `actix` or `axum` \ + features should also be enabled.", + )); + }; + + // generate path + let path = quote! { + if #fn_path.is_empty() { + #server_fn_path::const_format::concatcp!( + #prefix, + "/", + #fn_name_as_str, + #server_fn_path::xxhash_rust::const_xxh64::xxh64( + concat!(env!(#key_env_var), ":", file!(), ":", line!(), ":", column!()).as_bytes(), + 0 + ) + ) + } else { + #server_fn_path::const_format::concatcp!( + #prefix, + #fn_path + ) + } + }; + + // only emit the dummy (unmodified server-only body) for the server build + let dummy = cfg!(feature = "ssr").then_some(dummy); + let middlewares = if cfg!(feature = "ssr") { + quote! { + vec![ + #( + std::sync::Arc::new(#middlewares), + ),* + ] + } + } else { + quote! { vec![] } + }; + Ok(quote::quote! { #args_docs #docs - #[derive(Clone, Debug, #server_fn_path::serde::Serialize, #server_fn_path::serde::Deserialize)] + #[derive(Debug, #derives)] + #serde_path pub struct #struct_name { #(#fields),* } - impl #struct_name { - const URL: &'static str = if #fn_path.is_empty() { - #server_fn_path::const_format::concatcp!( - #fn_name_as_str, - #server_fn_path::xxhash_rust::const_xxh64::xxh64( - concat!(env!(#key_env_var), ":", file!(), ":", line!(), ":", column!()).as_bytes(), - 0 - ) - ) - } else { - #fn_path - }; - const PREFIX: &'static str = #prefix; - const ENCODING: #server_fn_path::Encoding = #encoding; - } + #from_impl - #inventory + impl #server_fn_path::ServerFn for #struct_name { + // TODO prefix + const PATH: &'static str = #path; - impl #server_fn_path::ServerFn<#server_ctx_path> for #struct_name { + type Client = #client; + type ServerRequest = #req; + type ServerResponse = #res; type Output = #output_ty; + type InputEncoding = #input; + type OutputEncoding = #output; + type Error = #error_ty; - fn prefix() -> &'static str { - Self::PREFIX + fn middlewares() -> Vec>> { + #middlewares } - fn url() -> &'static str { - Self::URL - } + #run_body + } - fn encoding() -> #server_fn_path::Encoding { - Self::ENCODING + #inventory + + #func + + #dummy + }) +} + +#[derive(Debug)] +struct Middleware { + expr: syn::Expr, +} + +impl ToTokens for Middleware { + fn to_tokens(&self, tokens: &mut TokenStream2) { + let expr = &self.expr; + tokens.extend(quote::quote! { + #expr + }); + } +} + +impl Parse for Middleware { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + let arg: syn::Expr = input.parse()?; + Ok(Middleware { expr: arg }) + } +} + +fn output_type(return_ty: &Type) -> Result<&GenericArgument> { + if let syn::Type::Path(pat) = &return_ty { + if pat.path.segments[0].ident == "Result" { + if pat.path.segments.is_empty() { + panic!("{:#?}", pat.path); + } else if let PathArguments::AngleBracketed(args) = + &pat.path.segments[0].arguments + { + return Ok(&args.args[0]); } + } + }; - #call_fn + Err(syn::Error::new( + return_ty.span(), + "server functions should return Result or 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) = + &pat.path.segments[0].arguments + { + // Result + if args.args.len() == 1 { + return Ok(None); + } + // Result + else if let GenericArgument::Type(Type::Path(pat)) = + &args.args[1] + { + if pat.path.segments[0].ident == "ServerFnError" { + let args = &pat.path.segments[0].arguments; + match args { + // Result + PathArguments::None => return Ok(None), + // Result> + PathArguments::AngleBracketed(args) => { + if args.args.len() == 1 { + return Ok(Some(&args.args[0])); + } + } + _ => {} + } + } + } + } } + }; - #func - }) + Err(syn::Error::new( + return_ty.span(), + "server functions should return Result or Result>", + )) } -struct ServerFnName { - _attrs: Vec, - struct_name: Ident, - _comma: Option, +#[derive(Debug)] +struct ServerFnArgs { + struct_name: Option, prefix: Option, - _comma2: Option, - encoding: Path, - _comma3: Option, + input: Option, + output: Option, fn_path: Option, } -impl Parse for ServerFnName { - fn parse(input: ParseStream) -> syn::Result { - let _attrs: Vec = input.call(Attribute::parse_outer)?; - let struct_name = input.parse()?; - let _comma = input.parse()?; - let prefix = input.parse()?; - let _comma2 = input.parse()?; - let encoding = input - .parse::() - .map(|encoding| { - match encoding.to_string().to_lowercase().as_str() { - "\"url\"" => syn::parse_quote!(Encoding::Url), - "\"cbor\"" => syn::parse_quote!(Encoding::Cbor), - "\"getcbor\"" => syn::parse_quote!(Encoding::GetCBOR), - "\"getjson\"" => syn::parse_quote!(Encoding::GetJSON), - _ => abort!(encoding, "Encoding Not Found"), +impl Parse for ServerFnArgs { + fn parse(stream: ParseStream) -> syn::Result { + // legacy 4-part arguments + let mut struct_name: Option = None; + let mut prefix: Option = None; + let mut encoding: Option = None; + let mut fn_path: Option = None; + + // new arguments: can only be keyed by name + let mut input: Option = None; + let mut output: Option = None; + + let mut use_key_and_value = false; + let mut arg_pos = 0; + + while !stream.is_empty() { + arg_pos += 1; + let lookahead = stream.lookahead1(); + if lookahead.peek(Ident) { + let key_or_value: Ident = stream.parse()?; + + let lookahead = stream.lookahead1(); + if lookahead.peek(Token![=]) { + stream.parse::()?; + let key = key_or_value; + use_key_and_value = true; + if key == "name" { + if struct_name.is_some() { + return Err(syn::Error::new( + key.span(), + "keyword argument repeated: `name`", + )); + } + struct_name = Some(stream.parse()?); + } else if key == "prefix" { + if prefix.is_some() { + return Err(syn::Error::new( + key.span(), + "keyword argument repeated: `prefix`", + )); + } + prefix = Some(stream.parse()?); + } else if key == "encoding" { + if encoding.is_some() { + return Err(syn::Error::new( + key.span(), + "keyword argument repeated: `encoding`", + )); + } + encoding = Some(stream.parse()?); + } else if key == "endpoint" { + if fn_path.is_some() { + return Err(syn::Error::new( + key.span(), + "keyword argument repeated: `endpoint`", + )); + } + fn_path = Some(stream.parse()?); + } else if key == "input" { + if encoding.is_some() { + return Err(syn::Error::new( + key.span(), + "`encoding` and `input` should not both be \ + specified", + )); + } else if input.is_some() { + return Err(syn::Error::new( + key.span(), + "keyword argument repeated: `input`", + )); + } + input = Some(stream.parse()?); + } else if key == "output" { + if encoding.is_some() { + return Err(syn::Error::new( + key.span(), + "`encoding` and `output` should not both be \ + specified", + )); + } else if output.is_some() { + return Err(syn::Error::new( + key.span(), + "keyword argument repeated: `output`", + )); + } + output = Some(stream.parse()?); + } else { + return Err(lookahead.error()); + } + } else { + let value = key_or_value; + if use_key_and_value { + return Err(syn::Error::new( + value.span(), + "positional argument follows keyword argument", + )); + } + if arg_pos == 1 { + struct_name = Some(value) + } else { + return Err(syn::Error::new( + value.span(), + "expected string literal", + )); + } } - }) - .unwrap_or_else(|_| syn::parse_quote!(Encoding::Url)); - let _comma3 = input.parse()?; - let fn_path = input.parse()?; + } else if lookahead.peek(LitStr) { + let value: Literal = stream.parse()?; + if use_key_and_value { + return Err(syn::Error::new( + value.span(), + "If you use keyword arguments (e.g., `name` = \ + Something), then you can no longer use arguments \ + without a keyword.", + )); + } + match arg_pos { + 1 => return Err(lookahead.error()), + 2 => prefix = Some(value), + 3 => encoding = Some(value), + 4 => fn_path = Some(value), + _ => { + return Err(syn::Error::new( + value.span(), + "unexpected extra argument", + )) + } + } + } else { + return Err(lookahead.error()); + } + + if !stream.is_empty() { + stream.parse::()?; + } + } + + // parse legacy encoding into input/output + if let Some(encoding) = encoding { + match encoding.to_string().to_lowercase().as_str() { + "\"url\"" => { + input = syn::parse_quote!(PostUrl); + output = syn::parse_quote!(Json); + } + "\"cbor\"" => { + input = syn::parse_quote!(Cbor); + output = syn::parse_quote!(Cbor); + } + "\"getcbor\"" => { + input = syn::parse_quote!(GetUrl); + output = syn::parse_quote!(Cbor); + } + "\"getjson\"" => { + input = syn::parse_quote!(GetUrl); + output = syn::parse_quote!(Json); + } + _ => { + return Err(syn::Error::new( + encoding.span(), + "Encoding not found.", + )) + } + } + } Ok(Self { _attrs, struct_name, - _comma, prefix, - _comma2, - encoding, - _comma3, + input, + output, fn_path, }) } } -#[allow(unused)] +#[derive(Debug)] struct ServerFnBody { pub attrs: Vec, pub vis: syn::Visibility, @@ -408,11 +702,10 @@ struct ServerFnBody { pub inputs: Punctuated, pub output_arrow: Token![->], pub return_ty: syn::Type, - pub block: Box, + pub block: TokenStream2, pub docs: Vec<(String, Span)>, } -/// The custom rusty variant of parsing rsx! impl Parse for ServerFnBody { fn parse(input: ParseStream) -> Result { let mut attrs: Vec = input.call(Attribute::parse_outer)?; @@ -478,3 +771,57 @@ impl Parse for ServerFnBody { }) } } + +impl ServerFnBody { + fn to_dummy_ident(&self) -> Ident { + Ident::new(&format!("__{}", self.ident), self.ident.span()) + } + + fn to_dummy_output(&self) -> TokenStream2 { + let ident = self.to_dummy_ident(); + let Self { + attrs, + vis, + async_token, + fn_token, + generics, + inputs, + output_arrow, + return_ty, + block, + .. + } = &self; + quote! { + #[doc(hidden)] + #(#attrs)* + #vis #async_token #fn_token #ident #generics ( #inputs ) #output_arrow #return_ty + #block + } + } +} + +/// Returns either the path of the codec (if it's a builtin) or the +/// original ident. +fn codec_ident(server_fn_path: Option<&Path>, ident: Ident) -> TokenStream2 { + if let Some(server_fn_path) = server_fn_path { + let str = ident.to_string(); + if [ + "GetUrl", + "PostUrl", + "Cbor", + "Json", + "Rkyv", + "Streaming", + "StreamingText", + "MultipartFormData", + ] + .contains(&str.as_str()) + { + return quote! { + #server_fn_path::codec::#ident + }; + } + } + + ident.into_token_stream() +} diff --git a/server_fn/server_fn_macro_default/Cargo.toml b/server_fn_macro_default/Cargo.toml similarity index 63% rename from server_fn/server_fn_macro_default/Cargo.toml rename to server_fn_macro_default/Cargo.toml index abab7610c9..2712cf6e47 100644 --- a/server_fn/server_fn_macro_default/Cargo.toml +++ b/server_fn_macro_default/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "server_fn_macro_default" -version = { workspace = true } +version = "0.1.0" edition = "2021" authors = ["Greg Johnston"] license = "MIT" @@ -11,8 +11,11 @@ description = "The default implementation of the server_fn macro without a conte proc-macro = true [dependencies] -syn = { version = "2", features = ["full"] } -server_fn_macro = { workspace = true } +syn = { version = "2" } +server_fn_macro = { path = "../server_fn_macro" } [features] nightly = ["server_fn_macro/nightly"] +ssr = ["server_fn_macro/ssr"] +actix = ["server_fn_macro/actix"] +axum = ["server_fn_macro/axum"] diff --git a/server_fn_macro_default/src/lib.rs b/server_fn_macro_default/src/lib.rs new file mode 100644 index 0000000000..84aa276213 --- /dev/null +++ b/server_fn_macro_default/src/lib.rs @@ -0,0 +1,19 @@ +#![forbid(unsafe_code)] + +use proc_macro::TokenStream; +use server_fn_macro::server_macro_impl; +use syn::__private::ToTokens; + +#[proc_macro_attribute] +pub fn server(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream { + match server_macro_impl( + args.into(), + s.into(), + syn::parse_quote!(server_fn::default::DefaultServerFnTraitObj), + Some(syn::parse_quote!(server_fns)), + "/api", + ) { + Err(e) => e.to_compile_error().into(), + Ok(s) => s.to_token_stream().into(), + } +} From c9627bfeb4ff5358ce25568a470b3b8769331dd7 Mon Sep 17 00:00:00 2001 From: benwis Date: Sun, 31 Dec 2023 12:31:22 -0800 Subject: [PATCH 003/100] Setup folder structure as before. Got a cyclical dependency though --- Cargo.toml | 2 +- server_fn/Cargo.toml | 2 +- .../server_fn_macro_default}/Cargo.toml | 4 ++-- .../server_fn_macro_default}/src/lib.rs | 0 4 files changed, 4 insertions(+), 4 deletions(-) rename {server_fn_macro_default => server_fn/server_fn_macro_default}/Cargo.toml (86%) rename {server_fn_macro_default => server_fn/server_fn_macro_default}/src/lib.rs (100%) diff --git a/Cargo.toml b/Cargo.toml index d31ab7d9d3..7cccce8d38 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,7 @@ members = [ "leptos_server", "server_fn", "server_fn_macro", - "server_fn_macro_default", + "server_fn/server_fn_macro_default", # integrations "integrations/actix", diff --git a/server_fn/Cargo.toml b/server_fn/Cargo.toml index 8e683fd263..9a126aa18e 100644 --- a/server_fn/Cargo.toml +++ b/server_fn/Cargo.toml @@ -4,10 +4,10 @@ version = "0.6.0" edition = "2021" [dependencies] +server_fn_macro_default = { workspace = true} # used for hashing paths in #[server] macro const_format = "0.2" xxhash-rust = { version = "0.8", features = ["const_xxh64"] } - # used across multiple featurs serde = { version = "1", features = ["derive"] } send_wrapper = { version = "0.6", features = ["futures"], optional = true } diff --git a/server_fn_macro_default/Cargo.toml b/server_fn/server_fn_macro_default/Cargo.toml similarity index 86% rename from server_fn_macro_default/Cargo.toml rename to server_fn/server_fn_macro_default/Cargo.toml index 2712cf6e47..5e9beac223 100644 --- a/server_fn_macro_default/Cargo.toml +++ b/server_fn/server_fn_macro_default/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "server_fn_macro_default" -version = "0.1.0" +version = "0.6.0" edition = "2021" authors = ["Greg Johnston"] license = "MIT" @@ -12,7 +12,7 @@ proc-macro = true [dependencies] syn = { version = "2" } -server_fn_macro = { path = "../server_fn_macro" } +server_fn_macro = { workspace = true } [features] nightly = ["server_fn_macro/nightly"] diff --git a/server_fn_macro_default/src/lib.rs b/server_fn/server_fn_macro_default/src/lib.rs similarity index 100% rename from server_fn_macro_default/src/lib.rs rename to server_fn/server_fn_macro_default/src/lib.rs From 2a5c8555959f60495f2f3cfdd422f18a0beb1f5e Mon Sep 17 00:00:00 2001 From: benwis Date: Sun, 31 Dec 2023 13:19:09 -0800 Subject: [PATCH 004/100] It starts to compile! --- leptos_server/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/leptos_server/Cargo.toml b/leptos_server/Cargo.toml index f440c2ae0b..60047b6026 100644 --- a/leptos_server/Cargo.toml +++ b/leptos_server/Cargo.toml @@ -27,7 +27,7 @@ default-tls = ["server_fn/default-tls"] hydrate = ["leptos_reactive/hydrate", "leptos_macro/hydrate"] rustls = ["server_fn/rustls"] ssr = ["leptos_reactive/ssr", "server_fn/ssr", "leptos_macro/ssr"] -nightly = ["leptos_reactive/nightly", "server_fn/nightly"] +nightly = ["leptos_reactive/nightly"] [package.metadata.cargo-all-features] denylist = ["nightly"] From 197edebd5119aa604167a7bbefcead72d9cd1770 Mon Sep 17 00:00:00 2001 From: benwis Date: Tue, 2 Jan 2024 15:06:49 -0800 Subject: [PATCH 005/100] Made some progress, started work on pavex integration as well --- examples/pavex_demo/.gitignore | 3 + examples/pavex_demo/Cargo.toml | 92 +++ examples/pavex_demo/README.md | 71 ++ examples/pavex_demo/flake.lock | 119 +++ examples/pavex_demo/flake.nix | 129 ++++ examples/pavex_demo/leptos_app/Cargo.toml | 21 + .../leptos_app/src/error_template.rs | 73 ++ examples/pavex_demo/leptos_app/src/lib.rs | 45 ++ examples/pavex_demo/leptos_front/Cargo.toml | 8 + examples/pavex_demo/leptos_front/src/lib.rs | 13 + examples/pavex_demo/style/main.scss | 4 + .../todo_app_sqlite_pavex/Cargo.toml | 22 + .../todo_app_sqlite_pavex/src/bin/bp.rs | 17 + .../todo_app_sqlite_pavex/src/blueprint.rs | 98 +++ .../src/configuration.rs | 32 + .../todo_app_sqlite_pavex/src/file_handler.rs | 45 ++ .../todo_app_sqlite_pavex/src/leptos.rs | 19 + .../todo_app_sqlite_pavex/src/lib.rs | 7 + .../todo_app_sqlite_pavex/src/routes/greet.rs | 21 + .../todo_app_sqlite_pavex/src/routes/mod.rs | 3 + .../src/routes/status.rs | 7 + .../todo_app_sqlite_pavex/src/telemetry.rs | 84 ++ .../todo_app_sqlite_pavex/src/user_agent.rs | 27 + .../todo_app_sqlite_pavex_server/Cargo.toml | 29 + .../configuration/base.yml | 3 + .../configuration/dev.yml | 6 + .../configuration/prod.yml | 3 + .../configuration/test.yml | 8 + .../src/bin/api.rs | 49 ++ .../src/configuration.rs | 140 ++++ .../todo_app_sqlite_pavex_server/src/lib.rs | 2 + .../src/telemetry.rs | 40 + .../tests/integration/greet.rs | 37 + .../tests/integration/helpers.rs | 52 ++ .../tests/integration/main.rs | 4 + .../tests/integration/ping.rs | 11 + .../Cargo.toml | 21 + .../blueprint.ron | 233 ++++++ .../src/lib.rs | 254 ++++++ flake.lock | 12 +- leptos_macro/src/server.rs | 8 +- leptos_server/src/action.rs | 3 +- leptos_server/src/lib.rs | 720 +++++++++--------- leptos_server/src/multi_action.rs | 2 +- server_fn/Cargo.toml | 4 +- server_fn/src/error.rs | 103 ++- 46 files changed, 2315 insertions(+), 389 deletions(-) create mode 100644 examples/pavex_demo/.gitignore create mode 100644 examples/pavex_demo/Cargo.toml create mode 100644 examples/pavex_demo/README.md create mode 100644 examples/pavex_demo/flake.lock create mode 100644 examples/pavex_demo/flake.nix create mode 100644 examples/pavex_demo/leptos_app/Cargo.toml create mode 100644 examples/pavex_demo/leptos_app/src/error_template.rs create mode 100644 examples/pavex_demo/leptos_app/src/lib.rs create mode 100644 examples/pavex_demo/leptos_front/Cargo.toml create mode 100644 examples/pavex_demo/leptos_front/src/lib.rs create mode 100644 examples/pavex_demo/style/main.scss create mode 100644 examples/pavex_demo/todo_app_sqlite_pavex/Cargo.toml create mode 100644 examples/pavex_demo/todo_app_sqlite_pavex/src/bin/bp.rs create mode 100644 examples/pavex_demo/todo_app_sqlite_pavex/src/blueprint.rs create mode 100644 examples/pavex_demo/todo_app_sqlite_pavex/src/configuration.rs create mode 100644 examples/pavex_demo/todo_app_sqlite_pavex/src/file_handler.rs create mode 100644 examples/pavex_demo/todo_app_sqlite_pavex/src/leptos.rs create mode 100644 examples/pavex_demo/todo_app_sqlite_pavex/src/lib.rs create mode 100644 examples/pavex_demo/todo_app_sqlite_pavex/src/routes/greet.rs create mode 100644 examples/pavex_demo/todo_app_sqlite_pavex/src/routes/mod.rs create mode 100644 examples/pavex_demo/todo_app_sqlite_pavex/src/routes/status.rs create mode 100644 examples/pavex_demo/todo_app_sqlite_pavex/src/telemetry.rs create mode 100644 examples/pavex_demo/todo_app_sqlite_pavex/src/user_agent.rs create mode 100644 examples/pavex_demo/todo_app_sqlite_pavex_server/Cargo.toml create mode 100644 examples/pavex_demo/todo_app_sqlite_pavex_server/configuration/base.yml create mode 100644 examples/pavex_demo/todo_app_sqlite_pavex_server/configuration/dev.yml create mode 100644 examples/pavex_demo/todo_app_sqlite_pavex_server/configuration/prod.yml create mode 100644 examples/pavex_demo/todo_app_sqlite_pavex_server/configuration/test.yml create mode 100644 examples/pavex_demo/todo_app_sqlite_pavex_server/src/bin/api.rs create mode 100644 examples/pavex_demo/todo_app_sqlite_pavex_server/src/configuration.rs create mode 100644 examples/pavex_demo/todo_app_sqlite_pavex_server/src/lib.rs create mode 100644 examples/pavex_demo/todo_app_sqlite_pavex_server/src/telemetry.rs create mode 100644 examples/pavex_demo/todo_app_sqlite_pavex_server/tests/integration/greet.rs create mode 100644 examples/pavex_demo/todo_app_sqlite_pavex_server/tests/integration/helpers.rs create mode 100644 examples/pavex_demo/todo_app_sqlite_pavex_server/tests/integration/main.rs create mode 100644 examples/pavex_demo/todo_app_sqlite_pavex_server/tests/integration/ping.rs create mode 100644 examples/pavex_demo/todo_app_sqlite_pavex_server_sdk/Cargo.toml create mode 100644 examples/pavex_demo/todo_app_sqlite_pavex_server_sdk/blueprint.ron create mode 100644 examples/pavex_demo/todo_app_sqlite_pavex_server_sdk/src/lib.rs diff --git a/examples/pavex_demo/.gitignore b/examples/pavex_demo/.gitignore new file mode 100644 index 0000000000..b6be732f92 --- /dev/null +++ b/examples/pavex_demo/.gitignore @@ -0,0 +1,3 @@ +/target +.env +.direnv diff --git a/examples/pavex_demo/Cargo.toml b/examples/pavex_demo/Cargo.toml new file mode 100644 index 0000000000..f46abb11a5 --- /dev/null +++ b/examples/pavex_demo/Cargo.toml @@ -0,0 +1,92 @@ +[workspace] +members = ["todo_app_sqlite_pavex", "todo_app_sqlite_pavex_server_sdk", "todo_app_sqlite_pavex_server", "leptos_app"] +# By setting `todo_app_sqlite_pavex_server` as the default member, `cargo run` will default to running the server binary +# when executed from the root of the workspace. +# Otherwise, you would have to use `cargo run --bin api` to run the server binary. +default-members = ["todo_app_sqlite_pavex_server"] +resolver = "2" + +# need to be applied only to wasm build +[profile.wasm_release] +codegen-units = 1 +lto = true +opt-level = 'z' + +[workspace.dependencies] +leptos = { version = "0.5", features = ["nightly"] } +leptos_meta = { version = "0.5", features = ["nightly"] } +leptos_router = { version = "0.5", features = ["nightly"] } +leptos_pavex = { version = "0.5" } +cfg_if = "1" +thiserror = "1" + +# See https://github.com/akesson/cargo-leptos for documentation of all the parameters. + +# A leptos project defines which workspace members +# that are used together frontend (lib) & server (bin) +[[workspace.metadata.leptos]] +# this name is used for the wasm, js and css file names +name = "start-pavex-workspace" + +# the package in the workspace that contains the server binary (binary crate) +bin-package = "server" + +# the package in the workspace that contains the frontend wasm binary (library crate) +lib-package = "leptos_frontend" + +# The site root folder is where cargo-leptos generate all output. WARNING: all content of this folder will be erased on a rebuild. Use it in your server setup. +site-root = "target/site" + +# The site-root relative folder where all compiled output (JS, WASM and CSS) is written +# Defaults to pkg +site-pkg-dir = "pkg" + +# [Optional] The source CSS file. If it ends with .sass or .scss then it will be compiled by dart-sass into CSS. The CSS is optimized by Lightning CSS before being written to //app.css +style-file = "style/main.scss" + +# Assets source dir. All files found here will be copied and synchronized to site-root. +# The assets-dir cannot have a sub directory with the same name/path as site-pkg-dir. +# +# Optional. Env: LEPTOS_ASSETS_DIR. +assets-dir = "public" + +# The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup. +site-addr = "127.0.0.1:3000" + +# The port to use for automatic reload monitoring +reload-port = 3001 + +# [Optional] Command to use when running end2end tests. It will run in the end2end dir. +# [Windows] for non-WSL use "npx.cmd playwright test" +# This binary name can be checked in Powershell with Get-Command npx +end2end-cmd = "npx playwright test" +end2end-dir = "end2end" + +# The browserlist query used for optimizing the CSS. +browserquery = "defaults" + +# Set by cargo-leptos watch when building with that tool. Controls whether autoreload JS will be included in the head +watch = false + +# The environment Leptos will run in, usually either "DEV" or "PROD" +env = "DEV" + +# The features to use when compiling the bin target +# +# Optional. Can be over-ridden with the command line parameter --bin-features +bin-features = [] + +# If the --no-default-features flag should be used when compiling the bin target +# +# Optional. Defaults to false. +bin-default-features = false + +# The features to use when compiling the lib target +# +# Optional. Can be over-ridden with the command line parameter --lib-features +lib-features = [] + +# If the --no-default-features flag should be used when compiling the lib target +# +# Optional. Defaults to false. +lib-default-features = false diff --git a/examples/pavex_demo/README.md b/examples/pavex_demo/README.md new file mode 100644 index 0000000000..72141d8ffb --- /dev/null +++ b/examples/pavex_demo/README.md @@ -0,0 +1,71 @@ +# todo_app_sqlite_pavex + +# Getting started + +## Prerequisites + +- Rust (see [here](https://www.rust-lang.org/tools/install) for instructions) +- `cargo-px`: + ```bash + cargo install --locked cargo-px --version="~0.1" + ``` +- [Pavex](https://pavex.dev) + +## Useful commands + +`todo_app_sqlite_pavex` is built using the [Pavex](https://pavex.dev) web framework, which relies on code generation. +You need to use the `cargo px` command instead of `cargo`: it ensures that the +`todo_app_sqlite_pavex_server_sdk` crate is correctly regenerated when the +application blueprint changes. + +`cargo px` is a wrapper around `cargo` that will automatically regenerate the +server SDK when needed. Check out its [documentation](https://github.com/LukeMathWalker/cargo-px) +for more details. + +### Build + +```bash +cargo px build +``` + +### Run + +```bash +cargo px run +``` + +### Test + +```bash +cargo px test +``` + +## Configuration + +All configurable parameters are listed in `todo_app_sqlite_pavex/src/configuration.rs`. + +Configuration values are loaded from two sources: + +- Configuration files +- Environment variables + +Environment variables take precedence over configuration files. + +All configuration files are in the `todo_app_sqlite_pavex_server/configuration` folder. +The application can be run in three different profiles: `dev`, `test` and `prod`. +The settings that you want to share across all profiles should be placed in `todo_app_sqlite_pavex_server/configuration/base.yml`. +Profile-specific configuration files can be then used +to override or supply additional values on top of the default settings (e.g. `todo_app_sqlite_pavex_server/configuration/dev.yml`). + +You can specify the app profile that you want to use by setting the `APP_PROFILE` environment variable; e.g.: + +```bash +APP_PROFILE=prod cargo px run +``` + +for running the application with the `prod` profile. + +By default, the `dev` profile is used since `APP_PROFILE` is set to `dev` in the `.env` file at the root of the project. +The `.env` file should not be committed to version control: it is meant to be used for local development only, +so that each developer can specify their own environment variables for secret values (e.g. database credentials) +that shouldn't be stored in configuration files (given their sensitive nature). diff --git a/examples/pavex_demo/flake.lock b/examples/pavex_demo/flake.lock new file mode 100644 index 0000000000..c219f46f50 --- /dev/null +++ b/examples/pavex_demo/flake.lock @@ -0,0 +1,119 @@ +{ + "nodes": { + "cargo-pavex-git": { + "flake": false, + "locked": { + "lastModified": 1703610192, + "narHash": "sha256-+oM6VGRRt/DQdhEFWJFIpKfY29w72V0vRpud8NsOI7c=", + "owner": "LukeMathWalker", + "repo": "pavex", + "rev": "e302f99e3641a55fe5624ba6c8154ce64e732a89", + "type": "github" + }, + "original": { + "owner": "LukeMathWalker", + "repo": "pavex", + "type": "github" + } + }, + "cargo-px-git": { + "flake": false, + "locked": { + "lastModified": 1702137928, + "narHash": "sha256-FbwHEOQnIYKhxp4Ne9XBIUJXu1o+ak6y9MhzRenIW40=", + "owner": "LukeMathWalker", + "repo": "cargo-px", + "rev": "d1bb9075c4993130f31f31c95642567a2255bd8e", + "type": "github" + }, + "original": { + "owner": "LukeMathWalker", + "repo": "cargo-px", + "type": "github" + } + }, + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1701680307, + "narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "4022d587cbbfd70fe950c1e2083a02621806a725", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1703499205, + "narHash": "sha256-lF9rK5mSUfIZJgZxC3ge40tp1gmyyOXZ+lRY3P8bfbg=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "e1fa12d4f6c6fe19ccb59cac54b5b3f25e160870", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "cargo-pavex-git": "cargo-pavex-git", + "cargo-px-git": "cargo-px-git", + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs", + "rust-overlay": "rust-overlay" + } + }, + "rust-overlay": { + "inputs": { + "flake-utils": [ + "flake-utils" + ], + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1703643208, + "narHash": "sha256-UL4KO8JxnD5rOycwHqBAf84lExF1/VnYMDC7b/wpPDU=", + "owner": "oxalica", + "repo": "rust-overlay", + "rev": "ce117f3e0de8262be8cd324ee6357775228687cf", + "type": "github" + }, + "original": { + "owner": "oxalica", + "repo": "rust-overlay", + "type": "github" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/examples/pavex_demo/flake.nix b/examples/pavex_demo/flake.nix new file mode 100644 index 0000000000..4452c5f02a --- /dev/null +++ b/examples/pavex_demo/flake.nix @@ -0,0 +1,129 @@ +{ + description = "Build Pavex tools"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; + + flake-utils.url = "github:numtide/flake-utils"; + + cargo-px-git = { + url = "github:/LukeMathWalker/cargo-px"; + flake = false; + }; + cargo-pavex-git = { + url = "github:LukeMathWalker/pavex"; + flake = false; + }; + + rust-overlay = { + url = "github:oxalica/rust-overlay"; + inputs = { + nixpkgs.follows = "nixpkgs"; + flake-utils.follows = "flake-utils"; + }; + }; + }; + + outputs = { self, nixpkgs, flake-utils, rust-overlay, ... } @inputs: + flake-utils.lib.eachDefaultSystem + (system: + let + pkgs = import nixpkgs { + inherit system; + overlays = [ (import rust-overlay) ]; + }; + inherit (pkgs) lib; + rustTarget = pkgs.rust-bin.selectLatestNightlyWith( toolchain: toolchain.default.override { + extensions = [ "rust-src" "rust-analyzer" "rustc-codegen-cranelift-preview" "rust-docs-json"]; + targets = [ "wasm32-unknown-unknown" ]; + }); + + + cargo-pavex_cli-git = pkgs.rustPlatform.buildRustPackage rec { + pname = "cargo-pavex-cli"; + version = "0.2.22"; + #buildFeatures = ["no_downloads"]; # cargo-leptos will try to download Ruby and other things without this feature + + src = inputs.cargo-pavex-git; + sourceRoot = "source/libs"; + cargoLock = { + lockFile = inputs.cargo-pavex-git + "/libs/Cargo.lock"; + outputHashes = { + "matchit-0.7.3" = "sha256-1bhbWvLlDb6/UJ4j2FqoG7j3DD1dTOLl6RaiY9kasmQ="; + #"pavex-0.1.0" = "sha256-NC7T1pcXJiWPtAWeiMUNzf2MUsYaRYxjLIL9fCqhExo="; + }; + }; + #buildAndTestSubdir = "libs"; + cargoSha256 = ""; + nativeBuildInputs = [pkgs.pkg-config pkgs.openssl pkgs.git]; + + buildInputs = with pkgs; + [openssl pkg-config git] + ++ lib.optionals stdenv.isDarwin [ + Security + ]; + + doCheck = false; # integration tests depend on changing cargo config + + meta = with lib; { + description = "An easy-to-use Rust framework for building robust and performant APIs"; + homepage = "https://github.com/LukeMatthewWalker/pavex"; + changelog = "https://github.com/LukeMatthewWalker/pavex/blob/v${version}/CHANGELOG.md"; + license = with licenses; [mit]; + maintainers = with maintainers; [benwis]; + }; + }; + cargo-px-git = pkgs.rustPlatform.buildRustPackage rec { + pname = "cargo-px"; + version = "0.2.22"; + #buildFeatures = ["no_downloads"]; # cargo-leptos will try to download Ruby and other things without this feature + + src = inputs.cargo-px-git; + + cargoSha256 ="sha256-+pyeqh0IoZ1JMgbhWxhEJw1MPgG7XeocVrqJoSNjgDA="; + + nativeBuildInputs = [pkgs.pkg-config pkgs.openssl pkgs.git]; + + buildInputs = with pkgs; + [openssl pkg-config git] + ++ lib.optionals stdenv.isDarwin [ + Security + ]; + + doCheck = false; # integration tests depend on changing cargo config + + meta = with lib; { + description = "A cargo subcommand that extends cargo's capabilities when it comes to code generation."; + homepage = "https://github.com/LukeMatthewWalker/cargo-px"; + changelog = "https://github.com/LukeMatthewWalker/cargo-px/blob/v${version}/CHANGELOG.md"; + license = with licenses; [mit]; + maintainers = with maintainers; [benwis]; + }; + }; + in + { + + devShells.default = pkgs.mkShell { + + # Extra inputs can be added here + nativeBuildInputs = with pkgs; [ + #rustTarget + rustup + openssl + pkg-config + clang + tailwindcss + mold-wrapped + cargo-px-git + cargo-pavex_cli-git + ]; + #RUST_SRC_PATH = "${rustTarget}/lib/rustlib/src/rust/library"; + MOLD_PATH = "${pkgs.mold-wrapped}/bin/mold"; + + shellHook = '' + sed -i -e '/rustflags = \["-C", "link-arg=-fuse-ld=/ s|ld=.*|ld=${pkgs.mold-wrapped}/bin/mold"]|' .cargo/config.toml + ''; + LIBCLANG_PATH = "${pkgs.llvmPackages.libclang.lib}/lib"; + }; + }); +} diff --git a/examples/pavex_demo/leptos_app/Cargo.toml b/examples/pavex_demo/leptos_app/Cargo.toml new file mode 100644 index 0000000000..2a455b47dc --- /dev/null +++ b/examples/pavex_demo/leptos_app/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "leptos_app" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +leptos.workspace = true +leptos_meta.workspace = true +leptos_router.workspace = true +leptos_pavex = { workspace = true, optional = true } + +#http.workspace = true +cfg_if.workspace = true +thiserror.workspace = true + +[features] +default = [] +hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"] +ssr = ["leptos/ssr", "leptos_meta/ssr", "leptos_router/ssr", "dep:leptos_pavex"] diff --git a/examples/pavex_demo/leptos_app/src/error_template.rs b/examples/pavex_demo/leptos_app/src/error_template.rs new file mode 100644 index 0000000000..f22e4687b9 --- /dev/null +++ b/examples/pavex_demo/leptos_app/src/error_template.rs @@ -0,0 +1,73 @@ +use cfg_if::cfg_if; +use http::status::StatusCode; +use leptos::*; +#[cfg(feature = "ssr")] +use leptos_axum::ResponseOptions; +use thiserror::Error; + +#[derive(Clone, Debug, Error)] +pub enum AppError { + #[error("Not Found")] + NotFound, +} + +impl AppError { + pub fn status_code(&self) -> StatusCode { + match self { + AppError::NotFound => StatusCode::NOT_FOUND, + } + } +} + +// A basic function to display errors served by the error boundaries. +// Feel free to do more complicated things here than just displaying the error. +#[component] +pub fn ErrorTemplate( + #[prop(optional)] outside_errors: Option, + #[prop(optional)] errors: Option>, +) -> impl IntoView { + let errors = match outside_errors { + Some(e) => create_rw_signal(e), + None => match errors { + Some(e) => e, + None => panic!("No Errors found and we expected errors!"), + }, + }; + // Get Errors from Signal + let errors = errors.get_untracked(); + + // Downcast lets us take a type that implements `std::error::Error` + let errors: Vec = errors + .into_iter() + .filter_map(|(_k, v)| v.downcast_ref::().cloned()) + .collect(); + println!("Errors: {errors:#?}"); + + // Only the response code for the first error is actually sent from the server + // this may be customized by the specific application + cfg_if! { if #[cfg(feature="ssr")] { + let response = use_context::(); + if let Some(response) = response { + response.set_status(errors[0].status_code()); + } + }} + + view! { +

{if errors.len() > 1 { "Errors" } else { "Error" }}

+ {error_code.to_string()} +

"Error: " {error_string}

+ } + } + /> + } +} diff --git a/examples/pavex_demo/leptos_app/src/lib.rs b/examples/pavex_demo/leptos_app/src/lib.rs new file mode 100644 index 0000000000..f68dcf688f --- /dev/null +++ b/examples/pavex_demo/leptos_app/src/lib.rs @@ -0,0 +1,45 @@ +use crate::error_template::{AppError, ErrorTemplate}; +use leptos::*; +use leptos_meta::*; +use leptos_router::*; + +pub mod error_template; + +#[component] +pub fn App() -> impl IntoView { + // Provides context that manages stylesheets, titles, meta tags, etc. + provide_meta_context(); + + view! { + + + // sets the document title + + + // content for this welcome page + <Router fallback=|| { + let mut outside_errors = Errors::default(); + outside_errors.insert_with_default_key(AppError::NotFound); + view! { <ErrorTemplate outside_errors/> }.into_view() + }> + <main> + <Routes> + <Route path="" view=HomePage/> + </Routes> + </main> + </Router> + } +} + +/// Renders the home page of your application. +#[component] +fn HomePage() -> impl IntoView { + // Creates a reactive value to update the button + let (count, set_count) = create_signal(0); + let on_click = move |_| set_count.update(|count| *count += 1); + + view! { + <h1>"Welcome to Leptos on Pavex!"</h1> + <button on:click=on_click>"Click Me: " {count}</button> + } +} diff --git a/examples/pavex_demo/leptos_front/Cargo.toml b/examples/pavex_demo/leptos_front/Cargo.toml new file mode 100644 index 0000000000..da9f88d9f0 --- /dev/null +++ b/examples/pavex_demo/leptos_front/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "leptos_front" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/examples/pavex_demo/leptos_front/src/lib.rs b/examples/pavex_demo/leptos_front/src/lib.rs new file mode 100644 index 0000000000..8e25fd20e5 --- /dev/null +++ b/examples/pavex_demo/leptos_front/src/lib.rs @@ -0,0 +1,13 @@ +use leptos::*; +use leptos_app::*; +use wasm_bindgen::prelude::wasm_bindgen; + +#[wasm_bindgen] +pub fn hydrate() { + // initializes logging using the `log` crate + _ = console_log::init_with_level(log::Level::Debug); + console_error_panic_hook::set_once(); + + leptos::mount_to_body(App); +} + diff --git a/examples/pavex_demo/style/main.scss b/examples/pavex_demo/style/main.scss new file mode 100644 index 0000000000..2ef54d3348 --- /dev/null +++ b/examples/pavex_demo/style/main.scss @@ -0,0 +1,4 @@ +body { + font-family: sans-serif; + text-align: center; +} diff --git a/examples/pavex_demo/todo_app_sqlite_pavex/Cargo.toml b/examples/pavex_demo/todo_app_sqlite_pavex/Cargo.toml new file mode 100644 index 0000000000..fc53c56663 --- /dev/null +++ b/examples/pavex_demo/todo_app_sqlite_pavex/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "todo_app_sqlite_pavex" +version = "0.1.0" +edition = "2021" + +[[bin]] +path = "src/bin/bp.rs" +name = "bp" + +[dependencies] +cargo_px_env = "0.1" +pavex = { git = "https://github.com/LukeMathWalker/pavex", branch = "main" } +pavex_cli_client = { git = "https://github.com/LukeMathWalker/pavex", branch = "main" } +tracing = "0.1" + +# Configuration +serde = { version = "1", features = ["derive"] } +serde-aux = "4" + +# Leptos +leptos_pavex.workspace = true + diff --git a/examples/pavex_demo/todo_app_sqlite_pavex/src/bin/bp.rs b/examples/pavex_demo/todo_app_sqlite_pavex/src/bin/bp.rs new file mode 100644 index 0000000000..7a75bb9a6b --- /dev/null +++ b/examples/pavex_demo/todo_app_sqlite_pavex/src/bin/bp.rs @@ -0,0 +1,17 @@ +use cargo_px_env::generated_pkg_manifest_path; +use todo_app_sqlite_pavex::blueprint; +use pavex_cli_client::Client; +use std::error::Error; + +/// Generate the `todo_app_sqlite_pavex_server_sdk` crate using Pavex's CLI. +/// +/// Pavex will automatically wire all our routes, constructors and error handlers +/// into the a "server SDK" that can be used by the final API server binary to launch +/// the application. +fn main() -> Result<(), Box<dyn Error>> { + let generated_dir = generated_pkg_manifest_path()?.parent().unwrap().into(); + Client::new() + .generate(blueprint(), generated_dir) + .execute()?; + Ok(()) +} diff --git a/examples/pavex_demo/todo_app_sqlite_pavex/src/blueprint.rs b/examples/pavex_demo/todo_app_sqlite_pavex/src/blueprint.rs new file mode 100644 index 0000000000..7ea7275a95 --- /dev/null +++ b/examples/pavex_demo/todo_app_sqlite_pavex/src/blueprint.rs @@ -0,0 +1,98 @@ +use leptos_pavex::{LeptosOptions, RouteListing}; +use pavex::{ + blueprint::{ + constructor::{CloningStrategy, Lifecycle}, + router::{ANY, GET}, + Blueprint, + }, + f, +}; +/// The main blueprint, containing all the routes, constructors and error handlers +/// required by our API. +pub fn blueprint() -> Blueprint { + let mut bp = Blueprint::new(); + register_common_constructors(&mut bp); + + bp.constructor( + f!(crate::user_agent::UserAgent::extract), + Lifecycle::RequestScoped, + ) + .error_handler(f!(crate::user_agent::invalid_user_agent)); + + add_telemetry_middleware(&mut bp); + + bp.route(GET, "/test/ping", f!(crate::routes::status::ping)); + bp.route(GET, "/test/greet/:name", f!(crate::routes::greet::greet)); + // Handle all /api requests as those are Leptos server fns + bp.route(ANY, "/api/*fn_name", f!(leptos_pavex::handle_server_fns)); + bp.route(ANY, "/"); + bp.fallback(f!(file_handler)); + bp +} + +/// Common constructors used by all routes. +fn register_common_constructors(bp: &mut Blueprint) { + // Configuration Options + bp.constructor( + f!(crate::leptos::get_cargo_leptos_conf(), Lifecycle::Singleton), + Lifecycle::Singleton, + ); + // List of Routes + bp.constructor( + f!(crate::leptos::get_app_route_listing(), Lifecycle::Singleton), + Lifecycle::Singleton, + ); + + bp.constructor( + f!(leptos_pavex::PavexRequest::extract), + LifeCycle::RequestScoped, + ); + // Query parameters + bp.constructor( + f!(pavex::request::query::QueryParams::extract), + Lifecycle::RequestScoped, + ) + .error_handler(f!( + pavex::request::query::errors::ExtractQueryParamsError::into_response + )); + + // Route parameters + bp.constructor( + f!(pavex::request::route::RouteParams::extract), + Lifecycle::RequestScoped, + ) + .error_handler(f!( + pavex::request::route::errors::ExtractRouteParamsError::into_response + )); + + // Json body + bp.constructor( + f!(pavex::request::body::JsonBody::extract), + Lifecycle::RequestScoped, + ) + .error_handler(f!( + pavex::request::body::errors::ExtractJsonBodyError::into_response + )); + bp.constructor( + f!(pavex::request::body::BufferedBody::extract), + Lifecycle::RequestScoped, + ) + .error_handler(f!( + pavex::request::body::errors::ExtractBufferedBodyError::into_response + )); + bp.constructor( + f!(<pavex::request::body::BodySizeLimit as std::default::Default>::default), + Lifecycle::RequestScoped, + ); +} + +/// Add the telemetry middleware, as well as the constructors of its dependencies. +fn add_telemetry_middleware(bp: &mut Blueprint) { + bp.constructor( + f!(crate::telemetry::RootSpan::new), + Lifecycle::RequestScoped, + ) + .cloning(CloningStrategy::CloneIfNecessary); + + bp.wrap(f!(crate::telemetry::logger)); +} diff --git a/examples/pavex_demo/todo_app_sqlite_pavex/src/configuration.rs b/examples/pavex_demo/todo_app_sqlite_pavex/src/configuration.rs new file mode 100644 index 0000000000..ad8bc7b385 --- /dev/null +++ b/examples/pavex_demo/todo_app_sqlite_pavex/src/configuration.rs @@ -0,0 +1,32 @@ +use pavex::server::IncomingStream; +use serde_aux::field_attributes::deserialize_number_from_string; +use std::net::SocketAddr; + +#[derive(serde::Deserialize)] +/// The top-level configuration, holding all the values required +/// to configure the entire application. +pub struct Config { + pub server: ServerConfig, +} + +#[derive(serde::Deserialize, Clone)] +/// Configuration for the HTTP server used to expose our API +/// to users. +pub struct ServerConfig { + /// The port that the server must listen on. + #[serde(deserialize_with = "deserialize_number_from_string")] + pub port: u16, + /// The network interface that the server must be bound to. + /// + /// E.g. `0.0.0.0` for listening to incoming requests from + /// all sources. + pub ip: std::net::IpAddr, +} + +impl ServerConfig { + /// Bind a TCP listener according to the specified parameters. + pub async fn listener(&self) -> Result<IncomingStream, std::io::Error> { + let addr = SocketAddr::new(self.ip, self.port); + IncomingStream::bind(addr).await + } +} \ No newline at end of file diff --git a/examples/pavex_demo/todo_app_sqlite_pavex/src/file_handler.rs b/examples/pavex_demo/todo_app_sqlite_pavex/src/file_handler.rs new file mode 100644 index 0000000000..98b7d8f8a9 --- /dev/null +++ b/examples/pavex_demo/todo_app_sqlite_pavex/src/file_handler.rs @@ -0,0 +1,45 @@ +use app::error_template::AppError; +use app::error_template::ErrorTemplate; +use app::App; +use axum::response::Response as AxumResponse; +use axum::{ + body::{boxed, Body, BoxBody}, + extract::State, + http::{Request, Response, StatusCode, Uri}, + response::IntoResponse, +}; +use leptos::*; +use tower::ServiceExt; +use tower_http::services::ServeDir; + +pub async fn file_and_error_handler( + uri: Uri, + State(options): State<LeptosOptions>, + req: Request<Body>, +) -> AxumResponse { + let root = options.site_root.clone(); + let res = get_static_file(uri.clone(), &root).await.unwrap(); + + if res.status() == StatusCode::OK { + res.into_response() + } else { + let handler = leptos_axum::render_app_to_stream(options.to_owned(), move || view! { <App/> }); + handler(req).await.into_response() + } +} + +async fn get_static_file(uri: Uri, root: &str) -> Result<Response<BoxBody>, (StatusCode, String)> { + let req = Request::builder() + .uri(uri.clone()) + .body(Body::empty()) + .unwrap(); + // `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot` + // This path is relative to the cargo root + match ServeDir::new(root).oneshot(req).await { + Ok(res) => Ok(res.map(boxed)), + Err(err) => Err(( + StatusCode::INTERNAL_SERVER_ERROR, + format!("Something went wrong: {err}"), + )), + } +} diff --git a/examples/pavex_demo/todo_app_sqlite_pavex/src/leptos.rs b/examples/pavex_demo/todo_app_sqlite_pavex/src/leptos.rs new file mode 100644 index 0000000000..1c774ae86c --- /dev/null +++ b/examples/pavex_demo/todo_app_sqlite_pavex/src/leptos.rs @@ -0,0 +1,19 @@ +use leptos::{get_configuration, leptos_config::ConfFile}; +use leptos_pavex::generate_route_list; +use leptos_router::RouteListing; +use pavex::{ + http::header::{ToStrError, USER_AGENT}, + request::RequestHead, + response::Response, +}; + +/// Easier to do this to avoid having to register things with Blueprints +/// Provide LeptosOptions via env vars provided by cargo-leptos or the user +pub fn get_cargo_leptos_conf() -> ConfFile { + get_configuration(None) +} + +/// Generate all possible non server fn routes for our app +pub fn get_app_route_listing() -> Vec<RouteListing> { + generate_route_list(TodoApp) +} diff --git a/examples/pavex_demo/todo_app_sqlite_pavex/src/lib.rs b/examples/pavex_demo/todo_app_sqlite_pavex/src/lib.rs new file mode 100644 index 0000000000..bea4f0fb94 --- /dev/null +++ b/examples/pavex_demo/todo_app_sqlite_pavex/src/lib.rs @@ -0,0 +1,7 @@ +mod blueprint; +pub mod configuration; +pub mod leptos; +pub mod routes; +pub mod telemetry; +pub mod user_agent; +pub use blueprint::blueprint; diff --git a/examples/pavex_demo/todo_app_sqlite_pavex/src/routes/greet.rs b/examples/pavex_demo/todo_app_sqlite_pavex/src/routes/greet.rs new file mode 100644 index 0000000000..1ac11bc56d --- /dev/null +++ b/examples/pavex_demo/todo_app_sqlite_pavex/src/routes/greet.rs @@ -0,0 +1,21 @@ +use crate::user_agent::UserAgent; +use pavex::{request::route::RouteParams, response::Response}; + +#[RouteParams] +pub struct GreetParams { + pub name: String, +} +pub fn greet( + params: RouteParams<GreetParams>, + user_agent: UserAgent, +) -> Response { + if let UserAgent::Unknown = user_agent { + return Response::unauthorized() + .set_typed_body("You must provide a `User-Agent` header") + .box_body(); + } + let GreetParams { name } = params.0; + Response::ok() + .set_typed_body(format!("Hello, {name}!")) + .box_body() +} diff --git a/examples/pavex_demo/todo_app_sqlite_pavex/src/routes/mod.rs b/examples/pavex_demo/todo_app_sqlite_pavex/src/routes/mod.rs new file mode 100644 index 0000000000..47f146cc9d --- /dev/null +++ b/examples/pavex_demo/todo_app_sqlite_pavex/src/routes/mod.rs @@ -0,0 +1,3 @@ +pub mod greet; +pub mod status; + diff --git a/examples/pavex_demo/todo_app_sqlite_pavex/src/routes/status.rs b/examples/pavex_demo/todo_app_sqlite_pavex/src/routes/status.rs new file mode 100644 index 0000000000..acec3eefe4 --- /dev/null +++ b/examples/pavex_demo/todo_app_sqlite_pavex/src/routes/status.rs @@ -0,0 +1,7 @@ +use pavex::http::StatusCode; + +/// Respond with a `200 OK` status code to indicate that the server is alive +/// and ready to accept new requests. +pub fn ping() -> StatusCode { + StatusCode::OK +} diff --git a/examples/pavex_demo/todo_app_sqlite_pavex/src/telemetry.rs b/examples/pavex_demo/todo_app_sqlite_pavex/src/telemetry.rs new file mode 100644 index 0000000000..6867b98767 --- /dev/null +++ b/examples/pavex_demo/todo_app_sqlite_pavex/src/telemetry.rs @@ -0,0 +1,84 @@ +use pavex::request::route::MatchedRouteTemplate; +use pavex::http::Version; +use pavex::middleware::Next; +use pavex::request::RequestHead; +use pavex::response::Response; +use std::borrow::Cow; +use std::future::IntoFuture; +use tracing::Instrument; + +/// A logging middleware that wraps the request pipeline in the root span. +/// It takes care to record key information about the request and the response. +pub async fn logger<T>(next: Next<T>, root_span: RootSpan) -> Response + where + T: IntoFuture<Output = Response>, +{ + let response = next + .into_future() + .instrument(root_span.clone().into_inner()) + .await; + root_span.record_response_data(&response); + response +} + +/// A root span is the top-level *logical* span for an incoming request. +/// +/// It is not necessarily the top-level *physical* span, as it may be a child of +/// another span (e.g. a span representing the underlying HTTP connection). +/// +/// We use the root span to attach as much information as possible about the +/// incoming request, and to record the final outcome of the request (success or +/// failure). +#[derive(Debug, Clone)] +pub struct RootSpan(tracing::Span); + +impl RootSpan { + /// Create a new root span for the given request. + /// + /// We follow OpenTelemetry's HTTP semantic conventions as closely as + /// possible for field naming. + pub fn new(request_head: &RequestHead, matched_route: MatchedRouteTemplate) -> Self { + let user_agent = request_head + .headers + .get("User-Agent") + .map(|h| h.to_str().unwrap_or_default()) + .unwrap_or_default(); + + let span = tracing::info_span!( + "HTTP request", + http.method = %request_head.method, + http.flavor = %http_flavor(request_head.version), + user_agent.original = %user_agent, + http.response.status_code = tracing::field::Empty, + http.route = %matched_route, + http.target = %request_head.uri.path_and_query().map(|p| p.as_str()).unwrap_or(""), + ); + Self(span) + } + + pub fn record_response_data(&self, response: &Response) { + self.0 + .record("http.response.status_code", &response.status().as_u16()); + } + + /// Get a reference to the underlying [`tracing::Span`]. + pub fn inner(&self) -> &tracing::Span { + &self.0 + } + + /// Deconstruct the root span into its underlying [`tracing::Span`]. + pub fn into_inner(self) -> tracing::Span { + self.0 + } +} + +fn http_flavor(version: Version) -> Cow<'static, str> { + match version { + Version::HTTP_09 => "0.9".into(), + Version::HTTP_10 => "1.0".into(), + Version::HTTP_11 => "1.1".into(), + Version::HTTP_2 => "2.0".into(), + Version::HTTP_3 => "3.0".into(), + other => format!("{other:?}").into(), + } +} \ No newline at end of file diff --git a/examples/pavex_demo/todo_app_sqlite_pavex/src/user_agent.rs b/examples/pavex_demo/todo_app_sqlite_pavex/src/user_agent.rs new file mode 100644 index 0000000000..1e08618e1e --- /dev/null +++ b/examples/pavex_demo/todo_app_sqlite_pavex/src/user_agent.rs @@ -0,0 +1,27 @@ +use pavex::{ + http::header::{ToStrError, USER_AGENT}, + request::RequestHead, + response::Response, +}; + +pub enum UserAgent { + /// No User-Agent header was provided + Unknown, + /// The value of the 'User-Agent' header for the incoming request + Known(String), +} +impl UserAgent { + pub fn extract(request_head: &RequestHead) -> Result<Self, ToStrError> { + let Some(user_agent) = request_head.headers.get(USER_AGENT) else { + return Ok(UserAgent::Unknown); + }; + + user_agent.to_str().map(|s| UserAgent::Known(s.into())) + } +} + +pub fn invalid_user_agent(_e: &ToStrError) -> Response { + Response::bad_request() + .set_typed_body("The `User-Agent` header must be a valid UTF-8 string") + .box_body() +} diff --git a/examples/pavex_demo/todo_app_sqlite_pavex_server/Cargo.toml b/examples/pavex_demo/todo_app_sqlite_pavex_server/Cargo.toml new file mode 100644 index 0000000000..52d1462184 --- /dev/null +++ b/examples/pavex_demo/todo_app_sqlite_pavex_server/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "todo_app_sqlite_pavex_server" +version = "0.1.0" +edition = "2021" + +[[bin]] +path = "src/bin/api.rs" +name = "api" + +[dependencies] +anyhow = "1" +pavex = { git = "https://github.com/LukeMathWalker/pavex", branch = "main" } +tokio = { version = "1", features = ["full"] } +todo_app_sqlite_pavex_server_sdk = { path = "../todo_app_sqlite_pavex_server_sdk" } +todo_app_sqlite_pavex = { path = "../todo_app_sqlite_pavex" } + +# Configuration +dotenvy = "0.15" +figment = { version = "0.10", features = ["env", "yaml"] } +serde = { version = "1", features = ["derive"]} + +# Telemetry +tracing = "0.1" +tracing-bunyan-formatter = "0.3" +tracing-panic = "0.1" +tracing-subscriber = { version = "0.3", default-features = false, features = ["env-filter", "registry", "smallvec", "std", "tracing-log"] } + +[dev-dependencies] +reqwest = "0.11" \ No newline at end of file diff --git a/examples/pavex_demo/todo_app_sqlite_pavex_server/configuration/base.yml b/examples/pavex_demo/todo_app_sqlite_pavex_server/configuration/base.yml new file mode 100644 index 0000000000..2e6c1938e4 --- /dev/null +++ b/examples/pavex_demo/todo_app_sqlite_pavex_server/configuration/base.yml @@ -0,0 +1,3 @@ +server: + ip: "0.0.0.0" + port: 8000 \ No newline at end of file diff --git a/examples/pavex_demo/todo_app_sqlite_pavex_server/configuration/dev.yml b/examples/pavex_demo/todo_app_sqlite_pavex_server/configuration/dev.yml new file mode 100644 index 0000000000..fe1171b8cd --- /dev/null +++ b/examples/pavex_demo/todo_app_sqlite_pavex_server/configuration/dev.yml @@ -0,0 +1,6 @@ +# This file contains the configuration for the dev environment. +# None of the values here are actually secret, so it's fine +# to commit this file to the repository. +server: + ip: "127.0.0.1" + port: 8000 diff --git a/examples/pavex_demo/todo_app_sqlite_pavex_server/configuration/prod.yml b/examples/pavex_demo/todo_app_sqlite_pavex_server/configuration/prod.yml new file mode 100644 index 0000000000..2a9375957f --- /dev/null +++ b/examples/pavex_demo/todo_app_sqlite_pavex_server/configuration/prod.yml @@ -0,0 +1,3 @@ +server: + ip: "0.0.0.0" + port: 8000 diff --git a/examples/pavex_demo/todo_app_sqlite_pavex_server/configuration/test.yml b/examples/pavex_demo/todo_app_sqlite_pavex_server/configuration/test.yml new file mode 100644 index 0000000000..4e7a868f5e --- /dev/null +++ b/examples/pavex_demo/todo_app_sqlite_pavex_server/configuration/test.yml @@ -0,0 +1,8 @@ +# This file contains the configuration for the API when spawned +# in black-box tests. +# None of the values here are actually secret, so it's fine +# to commit this file to the repository. +server: + ip: "127.0.0.1" + # The OS will assign a random port to the test server. + port: 0 \ No newline at end of file diff --git a/examples/pavex_demo/todo_app_sqlite_pavex_server/src/bin/api.rs b/examples/pavex_demo/todo_app_sqlite_pavex_server/src/bin/api.rs new file mode 100644 index 0000000000..d5fd21d3d6 --- /dev/null +++ b/examples/pavex_demo/todo_app_sqlite_pavex_server/src/bin/api.rs @@ -0,0 +1,49 @@ +use anyhow::Context; +use todo_app_sqlite_pavex_server::{ + configuration::load_configuration, + telemetry::{get_subscriber, init_telemetry}, +}; +use todo_app_sqlite_pavex_server_sdk::{build_application_state, run}; +use pavex::server::Server; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + let subscriber = get_subscriber("todo_app_sqlite_pavex".into(), "info".into(), std::io::stdout); + init_telemetry(subscriber)?; + + // We isolate all the server setup and launch logic in a separate function + // in order to have a single choke point where we make sure to log fatal errors + // that will cause the application to exit. + if let Err(e) = _main().await { + tracing::error!( + error.msg = %e, + error.error_chain = ?e, + "The application is exiting due to an error" + ) + } + + Ok(()) +} + +async fn _main() -> anyhow::Result<()> { + // Load environment variables from a .env file, if it exists. + let _ = dotenvy::dotenv(); + + let config = load_configuration(None)?; + let application_state = build_application_state() + .await; + + let tcp_listener = config + .server + .listener() + .await + .context("Failed to bind the server TCP listener")?; + let address = tcp_listener + .local_addr() + .context("The server TCP listener doesn't have a local socket address")?; + let server_builder = Server::new().listen(tcp_listener); + + tracing::info!("Starting to listen for incoming requests at {}", address); + run(server_builder, application_state).await; + Ok(()) +} diff --git a/examples/pavex_demo/todo_app_sqlite_pavex_server/src/configuration.rs b/examples/pavex_demo/todo_app_sqlite_pavex_server/src/configuration.rs new file mode 100644 index 0000000000..093377b596 --- /dev/null +++ b/examples/pavex_demo/todo_app_sqlite_pavex_server/src/configuration.rs @@ -0,0 +1,140 @@ +use std::env::VarError; + +use anyhow::Context; +use todo_app_sqlite_pavex::configuration::Config; +use figment::{ + providers::{Env, Format, Yaml}, + Figment, +}; + +/// Retrieve the application configuration by merging together multiple configuration sources. +/// +/// # Application profiles +/// +/// We use the concept of application profiles to allow for +/// different configuration values depending on the type of environment +/// the application is running in. +/// +/// We don't rely on `figment`'s built-in support for profiles because +/// we want to make sure that values for different profiles are not co-located in +/// the same configuration file. +/// This makes it easier to avoid leaking sensitive information by mistake (e.g. +/// by committing configuration values for the `dev` profile to the repository). +/// +/// You primary mechanism to specify the desired application profile is the `APP_PROFILE` +/// environment variable. +/// You can pass a `default_profile` value that will be used if the environment variable +/// is not set. +/// +/// # Hierarchy +/// +/// The configuration sources are: +/// +/// 1. `base.yml` - Contains the default configuration values, common to all profiles. +/// 2. `<profile>.yml` - Contains the configuration values specific to the desired profile. +/// 3. Environment variables - Contains the configuration values specific to the current environment. +/// +/// The configuration sources are listed in priority order, i.e. +/// the last source in the list will override any previous source. +/// +/// For example, if the same configuration key is defined in both +/// the YAML file and the environment, the value from the environment +/// will be used. +pub fn load_configuration( + default_profile: Option<ApplicationProfile>, +) -> Result<Config, anyhow::Error> { + let application_profile = load_app_profile(default_profile) + .context("Failed to load the desired application profile")?; + + let configuration_dir = { + let manifest_dir = env!( + "CARGO_MANIFEST_DIR", + "`CARGO_MANIFEST_DIR` was not set. Are you using a custom build system?" + ); + std::path::Path::new(manifest_dir).join("configuration") + }; + + let base_filepath = configuration_dir.join("base.yml"); + + let profile_filename = format!("{}.yml", application_profile.as_str()); + let profile_filepath = configuration_dir.join(profile_filename); + + let figment = Figment::new() + .merge(Yaml::file(base_filepath)) + .merge(Yaml::file(profile_filepath)) + .merge(Env::prefixed("APP_")); + + let configuration: Config = figment + .extract() + .context("Failed to load hierarchical configuration")?; + Ok(configuration) +} + +/// Load the application profile from the `APP_PROFILE` environment variable. +fn load_app_profile( + default_profile: Option<ApplicationProfile>, +) -> Result<ApplicationProfile, anyhow::Error> { + static PROFILE_ENV_VAR: &str = "APP_PROFILE"; + + match std::env::var(PROFILE_ENV_VAR) { + Ok(raw_value) => raw_value.parse().with_context(|| { + format!("Failed to parse the `{PROFILE_ENV_VAR}` environment variable") + }), + Err(VarError::NotPresent) if default_profile.is_some() => Ok(default_profile.unwrap()), + Err(e) => Err(anyhow::anyhow!(e).context(format!( + "Failed to read the `{PROFILE_ENV_VAR}` environment variable" + ))), + } +} + +/// The application profile, i.e. the type of environment the application is running in. +/// See [`load_configuration`] for more details. +pub enum ApplicationProfile { + /// Test profile. + /// + /// This is the profile used by the integration test suite. + Test, + /// Local development profile. + /// + /// This is the profile you should use when running the application locally + /// for exploratory testing. + /// + /// The corresponding configuration file is `dev.yml` and it's *never* committed to the repository. + Dev, + /// Production profile. + /// + /// This is the profile you should use when running the application in production—e.g. + /// when deploying it to a staging or production environment, exposed to live traffic. + /// + /// The corresponding configuration file is `prod.yml`. + /// It's committed to the repository, but it's meant to contain exclusively + /// non-sensitive configuration values. + Prod, +} + +impl ApplicationProfile { + /// Return the environment as a string. + pub fn as_str(&self) -> &'static str { + match self { + ApplicationProfile::Test => "test", + ApplicationProfile::Dev => "dev", + ApplicationProfile::Prod => "prod", + } + } +} + +impl std::str::FromStr for ApplicationProfile { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + match s.to_lowercase().as_str() { + "test" => Ok(ApplicationProfile::Test), + "dev" | "development" => Ok(ApplicationProfile::Dev), + "prod" | "production" => Ok(ApplicationProfile::Prod), + s => Err(anyhow::anyhow!( + "`{}` is not a valid application profile.\nValid options are: `test`, `dev`, `prod`.", + s + )), + } + } +} diff --git a/examples/pavex_demo/todo_app_sqlite_pavex_server/src/lib.rs b/examples/pavex_demo/todo_app_sqlite_pavex_server/src/lib.rs new file mode 100644 index 0000000000..39bc2ff72c --- /dev/null +++ b/examples/pavex_demo/todo_app_sqlite_pavex_server/src/lib.rs @@ -0,0 +1,2 @@ +pub mod configuration; +pub mod telemetry; \ No newline at end of file diff --git a/examples/pavex_demo/todo_app_sqlite_pavex_server/src/telemetry.rs b/examples/pavex_demo/todo_app_sqlite_pavex_server/src/telemetry.rs new file mode 100644 index 0000000000..848bc1884c --- /dev/null +++ b/examples/pavex_demo/todo_app_sqlite_pavex_server/src/telemetry.rs @@ -0,0 +1,40 @@ +use anyhow::Context; +use tracing::subscriber::set_global_default; +use tracing::Subscriber; +use tracing_bunyan_formatter::{BunyanFormattingLayer, JsonStorageLayer}; +use tracing_subscriber::fmt::MakeWriter; +use tracing_subscriber::{layer::SubscriberExt, EnvFilter, Registry}; + +/// Perform all the required setup steps for our telemetry: +/// +/// - Register a subscriber as global default to process span data +/// - Register a panic hook to capture any panic and record its details +/// +/// It should only be called once! +pub fn init_telemetry(subscriber: impl Subscriber + Sync + Send) -> Result<(), anyhow::Error> { + std::panic::set_hook(Box::new(tracing_panic::panic_hook)); + set_global_default(subscriber).context("Failed to set a `tracing` global subscriber") +} + +/// Compose multiple layers into a `tracing`'s subscriber. +/// +/// # Implementation Notes +/// +/// We are using `impl Subscriber` as return type to avoid having to spell out the actual +/// type of the returned subscriber, which is indeed quite complex. +pub fn get_subscriber<Sink>( + application_name: String, + default_env_filter: String, + sink: Sink, +) -> impl Subscriber + Sync + Send + where + Sink: for<'a> MakeWriter<'a> + Send + Sync + 'static, +{ + let env_filter = + EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(default_env_filter)); + let formatting_layer = BunyanFormattingLayer::new(application_name, sink); + Registry::default() + .with(env_filter) + .with(JsonStorageLayer) + .with(formatting_layer) +} \ No newline at end of file diff --git a/examples/pavex_demo/todo_app_sqlite_pavex_server/tests/integration/greet.rs b/examples/pavex_demo/todo_app_sqlite_pavex_server/tests/integration/greet.rs new file mode 100644 index 0000000000..9ba0dd35cc --- /dev/null +++ b/examples/pavex_demo/todo_app_sqlite_pavex_server/tests/integration/greet.rs @@ -0,0 +1,37 @@ +use crate::helpers::TestApi; +use pavex::http::StatusCode; + +#[tokio::test] +async fn greet_happy_path() { + let api = TestApi::spawn().await; + let name = "Ursula"; + + let response = api + .api_client + .get(&format!("{}/api/greet/{name}", &api.api_address)) + .header("User-Agent", "Test runner") + .send() + .await + .expect("Failed to execute request."); + assert_eq!(response.status().as_u16(), StatusCode::OK.as_u16()); + assert_eq!(response.text().await.unwrap(), "Hello, Ursula!"); +} + +#[tokio::test] +async fn non_utf8_agent_is_rejected() { + let api = TestApi::spawn().await; + let name = "Ursula"; + + let response = api + .api_client + .get(&format!("{}/api/greet/{name}", &api.api_address)) + .header("User-Agent", b"hello\xfa".as_slice()) + .send() + .await + .expect("Failed to execute request."); + assert_eq!(response.status().as_u16(), StatusCode::BAD_REQUEST.as_u16()); + assert_eq!( + response.text().await.unwrap(), + "The `User-Agent` header must be a valid UTF-8 string" + ); +} diff --git a/examples/pavex_demo/todo_app_sqlite_pavex_server/tests/integration/helpers.rs b/examples/pavex_demo/todo_app_sqlite_pavex_server/tests/integration/helpers.rs new file mode 100644 index 0000000000..3eaff3713d --- /dev/null +++ b/examples/pavex_demo/todo_app_sqlite_pavex_server/tests/integration/helpers.rs @@ -0,0 +1,52 @@ +use todo_app_sqlite_pavex_server::configuration::{load_configuration, ApplicationProfile}; +use todo_app_sqlite_pavex_server_sdk::{build_application_state, run}; +use todo_app_sqlite_pavex::configuration::Config; +use pavex::server::Server; + +pub struct TestApi { + pub api_address: String, + pub api_client: reqwest::Client, +} + +impl TestApi { + pub async fn spawn() -> Self { + let config = Self::get_config(); + + let application_state = build_application_state().await; + + let tcp_listener = config + .server + .listener() + .await + .expect("Failed to bind the server TCP listener"); + let address = tcp_listener + .local_addr() + .expect("The server TCP listener doesn't have a local socket address"); + let server_builder = Server::new().listen(tcp_listener); + + tokio::spawn(async move { + run(server_builder, application_state).await + }); + + TestApi { + api_address: format!("http://{}:{}", config.server.ip, address.port()), + api_client: reqwest::Client::new(), + } + } + + fn get_config() -> Config { + load_configuration(Some(ApplicationProfile::Test)).expect("Failed to load test configuration") + } +} + +/// Convenient methods for calling the API under test. +impl TestApi { + pub async fn get_ping(&self) -> reqwest::Response + { + self.api_client + .get(&format!("{}/api/ping", &self.api_address)) + .send() + .await + .expect("Failed to execute request.") + } +} diff --git a/examples/pavex_demo/todo_app_sqlite_pavex_server/tests/integration/main.rs b/examples/pavex_demo/todo_app_sqlite_pavex_server/tests/integration/main.rs new file mode 100644 index 0000000000..67767e05fc --- /dev/null +++ b/examples/pavex_demo/todo_app_sqlite_pavex_server/tests/integration/main.rs @@ -0,0 +1,4 @@ +mod greet; +mod helpers; +mod ping; + diff --git a/examples/pavex_demo/todo_app_sqlite_pavex_server/tests/integration/ping.rs b/examples/pavex_demo/todo_app_sqlite_pavex_server/tests/integration/ping.rs new file mode 100644 index 0000000000..c79eb0eb76 --- /dev/null +++ b/examples/pavex_demo/todo_app_sqlite_pavex_server/tests/integration/ping.rs @@ -0,0 +1,11 @@ +use crate::helpers::TestApi; +use pavex::http::StatusCode; + +#[tokio::test] +async fn ping_works() { + let api = TestApi::spawn().await; + + let response = api.get_ping().await; + + assert_eq!(response.status().as_u16(), StatusCode::OK.as_u16()); +} diff --git a/examples/pavex_demo/todo_app_sqlite_pavex_server_sdk/Cargo.toml b/examples/pavex_demo/todo_app_sqlite_pavex_server_sdk/Cargo.toml new file mode 100644 index 0000000000..336485067e --- /dev/null +++ b/examples/pavex_demo/todo_app_sqlite_pavex_server_sdk/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "todo_app_sqlite_pavex_server_sdk" +version = "0.1.0" +edition = "2021" + +[package.metadata.px.generate] +generator_type = "cargo_workspace_binary" +generator_name = "bp" + +[lints] +clippy = { all = "allow" } + +[dependencies] +bytes = { version = "1.5.0", package = "bytes" } +http = { version = "1.0.0", package = "http" } +http_body_util = { version = "0.1.0", package = "http-body-util" } +hyper = { version = "1.1.0", package = "hyper" } +matchit = { version = "0.7.3", git = "https://github.com/ibraheemdev/matchit", branch = "master", package = "matchit" } +pavex = { version = "0.1.0", git = "https://github.com/LukeMathWalker/pavex", branch = "main", package = "pavex" } +thiserror = { version = "1.0.52", package = "thiserror" } +todo_app_sqlite_pavex = { version = "0.1.0", path = "../todo_app_sqlite_pavex", package = "todo_app_sqlite_pavex" } diff --git a/examples/pavex_demo/todo_app_sqlite_pavex_server_sdk/blueprint.ron b/examples/pavex_demo/todo_app_sqlite_pavex_server_sdk/blueprint.ron new file mode 100644 index 0000000000..39906399ea --- /dev/null +++ b/examples/pavex_demo/todo_app_sqlite_pavex_server_sdk/blueprint.ron @@ -0,0 +1,233 @@ +( + creation_location: ( + line: 13, + column: 18, + file: "todo_app_sqlite_pavex/src/blueprint.rs", + ), + constructors: [ + ( + constructor: ( + callable: ( + registered_at: "todo_app_sqlite_pavex", + import_path: "pavex::request::query::QueryParams::extract", + ), + location: ( + line: 32, + column: 8, + file: "todo_app_sqlite_pavex/src/blueprint.rs", + ), + ), + lifecycle: RequestScoped, + cloning_strategy: None, + error_handler: Some(( + callable: ( + registered_at: "todo_app_sqlite_pavex", + import_path: "pavex::request::query::errors::ExtractQueryParamsError::into_response", + ), + location: ( + line: 36, + column: 6, + file: "todo_app_sqlite_pavex/src/blueprint.rs", + ), + )), + ), + ( + constructor: ( + callable: ( + registered_at: "todo_app_sqlite_pavex", + import_path: "pavex::request::route::RouteParams::extract", + ), + location: ( + line: 41, + column: 8, + file: "todo_app_sqlite_pavex/src/blueprint.rs", + ), + ), + lifecycle: RequestScoped, + cloning_strategy: None, + error_handler: Some(( + callable: ( + registered_at: "todo_app_sqlite_pavex", + import_path: "pavex::request::route::errors::ExtractRouteParamsError::into_response", + ), + location: ( + line: 45, + column: 6, + file: "todo_app_sqlite_pavex/src/blueprint.rs", + ), + )), + ), + ( + constructor: ( + callable: ( + registered_at: "todo_app_sqlite_pavex", + import_path: "pavex::request::body::JsonBody::extract", + ), + location: ( + line: 50, + column: 8, + file: "todo_app_sqlite_pavex/src/blueprint.rs", + ), + ), + lifecycle: RequestScoped, + cloning_strategy: None, + error_handler: Some(( + callable: ( + registered_at: "todo_app_sqlite_pavex", + import_path: "pavex::request::body::errors::ExtractJsonBodyError::into_response", + ), + location: ( + line: 54, + column: 6, + file: "todo_app_sqlite_pavex/src/blueprint.rs", + ), + )), + ), + ( + constructor: ( + callable: ( + registered_at: "todo_app_sqlite_pavex", + import_path: "pavex::request::body::BufferedBody::extract", + ), + location: ( + line: 57, + column: 8, + file: "todo_app_sqlite_pavex/src/blueprint.rs", + ), + ), + lifecycle: RequestScoped, + cloning_strategy: None, + error_handler: Some(( + callable: ( + registered_at: "todo_app_sqlite_pavex", + import_path: "pavex::request::body::errors::ExtractBufferedBodyError::into_response", + ), + location: ( + line: 61, + column: 6, + file: "todo_app_sqlite_pavex/src/blueprint.rs", + ), + )), + ), + ( + constructor: ( + callable: ( + registered_at: "todo_app_sqlite_pavex", + import_path: "<pavex::request::body::BodySizeLimit as std::default::Default>::default", + ), + location: ( + line: 64, + column: 8, + file: "todo_app_sqlite_pavex/src/blueprint.rs", + ), + ), + lifecycle: RequestScoped, + cloning_strategy: None, + error_handler: None, + ), + ( + constructor: ( + callable: ( + registered_at: "todo_app_sqlite_pavex", + import_path: "crate::user_agent::UserAgent::extract", + ), + location: ( + line: 16, + column: 8, + file: "todo_app_sqlite_pavex/src/blueprint.rs", + ), + ), + lifecycle: RequestScoped, + cloning_strategy: None, + error_handler: Some(( + callable: ( + registered_at: "todo_app_sqlite_pavex", + import_path: "crate::user_agent::invalid_user_agent", + ), + location: ( + line: 20, + column: 6, + file: "todo_app_sqlite_pavex/src/blueprint.rs", + ), + )), + ), + ( + constructor: ( + callable: ( + registered_at: "todo_app_sqlite_pavex", + import_path: "crate::telemetry::RootSpan::new", + ), + location: ( + line: 72, + column: 8, + file: "todo_app_sqlite_pavex/src/blueprint.rs", + ), + ), + lifecycle: RequestScoped, + cloning_strategy: Some(CloneIfNecessary), + error_handler: None, + ), + ], + middlewares: [ + ( + middleware: ( + callable: ( + registered_at: "todo_app_sqlite_pavex", + import_path: "crate::telemetry::logger", + ), + location: ( + line: 78, + column: 8, + file: "todo_app_sqlite_pavex/src/blueprint.rs", + ), + ), + error_handler: None, + ), + ], + routes: [ + ( + path: "/api/ping", + method_guard: ( + inner: Some(( + bitset: 256, + extensions: [], + )), + ), + request_handler: ( + callable: ( + registered_at: "todo_app_sqlite_pavex", + import_path: "crate::routes::status::ping", + ), + location: ( + line: 24, + column: 8, + file: "todo_app_sqlite_pavex/src/blueprint.rs", + ), + ), + error_handler: None, + ), + ( + path: "/api/greet/:name", + method_guard: ( + inner: Some(( + bitset: 256, + extensions: [], + )), + ), + request_handler: ( + callable: ( + registered_at: "todo_app_sqlite_pavex", + import_path: "crate::routes::greet::greet", + ), + location: ( + line: 25, + column: 8, + file: "todo_app_sqlite_pavex/src/blueprint.rs", + ), + ), + error_handler: None, + ), + ], + fallback_request_handler: None, + nested_blueprints: [], +) \ No newline at end of file diff --git a/examples/pavex_demo/todo_app_sqlite_pavex_server_sdk/src/lib.rs b/examples/pavex_demo/todo_app_sqlite_pavex_server_sdk/src/lib.rs new file mode 100644 index 0000000000..a76065bfb3 --- /dev/null +++ b/examples/pavex_demo/todo_app_sqlite_pavex_server_sdk/src/lib.rs @@ -0,0 +1,254 @@ +//! Do NOT edit this code. +//! It was automatically generated by Pavex. +//! All manual edits will be lost next time the code is generated. +extern crate alloc; +struct ServerState { + router: matchit::Router<u32>, + #[allow(dead_code)] + application_state: ApplicationState, +} +pub struct ApplicationState {} +pub async fn build_application_state() -> crate::ApplicationState { + crate::ApplicationState {} +} +pub fn run( + server_builder: pavex::server::Server, + application_state: ApplicationState, +) -> pavex::server::ServerHandle { + let server_state = std::sync::Arc::new(ServerState { + router: build_router(), + application_state, + }); + server_builder.serve(route_request, server_state) +} +fn build_router() -> matchit::Router<u32> { + let mut router = matchit::Router::new(); + router.insert("/api/greet/:name", 0u32).unwrap(); + router.insert("/api/ping", 1u32).unwrap(); + router +} +async fn route_request( + request: http::Request<hyper::body::Incoming>, + server_state: std::sync::Arc<ServerState>, +) -> pavex::response::Response { + let (request_head, request_body) = request.into_parts(); + #[allow(unused)] + let request_body = pavex::request::body::RawIncomingBody::from(request_body); + let request_head: pavex::request::RequestHead = request_head.into(); + let matched_route = match server_state.router.at(&request_head.uri.path()) { + Ok(m) => m, + Err(_) => { + let allowed_methods: pavex::router::AllowedMethods = pavex::router::MethodAllowList::from_iter( + vec![], + ) + .into(); + let matched_route_template = pavex::request::route::MatchedRouteTemplate::new( + "*", + ); + return route_2::middleware_0( + matched_route_template, + &allowed_methods, + &request_head, + ) + .await; + } + }; + let route_id = matched_route.value; + #[allow(unused)] + let url_params: pavex::request::route::RawRouteParams<'_, '_> = matched_route + .params + .into(); + match route_id { + 0u32 => { + let matched_route_template = pavex::request::route::MatchedRouteTemplate::new( + "/api/greet/:name", + ); + match &request_head.method { + &pavex::http::Method::GET => { + route_1::middleware_0( + matched_route_template, + url_params, + &request_head, + ) + .await + } + _ => { + let allowed_methods: pavex::router::AllowedMethods = pavex::router::MethodAllowList::from_iter([ + pavex::http::Method::GET, + ]) + .into(); + route_2::middleware_0( + matched_route_template, + &allowed_methods, + &request_head, + ) + .await + } + } + } + 1u32 => { + let matched_route_template = pavex::request::route::MatchedRouteTemplate::new( + "/api/ping", + ); + match &request_head.method { + &pavex::http::Method::GET => { + route_0::middleware_0(matched_route_template, &request_head).await + } + _ => { + let allowed_methods: pavex::router::AllowedMethods = pavex::router::MethodAllowList::from_iter([ + pavex::http::Method::GET, + ]) + .into(); + route_2::middleware_0( + matched_route_template, + &allowed_methods, + &request_head, + ) + .await + } + } + } + i => unreachable!("Unknown route id: {}", i), + } +} +pub mod route_0 { + pub async fn middleware_0( + v0: pavex::request::route::MatchedRouteTemplate, + v1: &pavex::request::RequestHead, + ) -> pavex::response::Response { + let v2 = todo_app_sqlite_pavex::telemetry::RootSpan::new(v1, v0); + let v3 = crate::route_0::Next0 { + next: handler, + }; + let v4 = pavex::middleware::Next::new(v3); + todo_app_sqlite_pavex::telemetry::logger(v4, v2).await + } + pub async fn handler() -> pavex::response::Response { + let v0 = todo_app_sqlite_pavex::routes::status::ping(); + <http::StatusCode as pavex::response::IntoResponse>::into_response(v0) + } + pub struct Next0<T> + where + T: std::future::Future<Output = pavex::response::Response>, + { + next: fn() -> T, + } + impl<T> std::future::IntoFuture for Next0<T> + where + T: std::future::Future<Output = pavex::response::Response>, + { + type Output = pavex::response::Response; + type IntoFuture = T; + fn into_future(self) -> Self::IntoFuture { + (self.next)() + } + } +} +pub mod route_1 { + pub async fn middleware_0( + v0: pavex::request::route::MatchedRouteTemplate, + v1: pavex::request::route::RawRouteParams<'_, '_>, + v2: &pavex::request::RequestHead, + ) -> pavex::response::Response { + let v3 = todo_app_sqlite_pavex::telemetry::RootSpan::new(v2, v0); + let v4 = crate::route_1::Next0 { + s_0: v1, + s_1: v2, + next: handler, + }; + let v5 = pavex::middleware::Next::new(v4); + todo_app_sqlite_pavex::telemetry::logger(v5, v3).await + } + pub async fn handler( + v0: pavex::request::route::RawRouteParams<'_, '_>, + v1: &pavex::request::RequestHead, + ) -> pavex::response::Response { + let v2 = todo_app_sqlite_pavex::user_agent::UserAgent::extract(v1); + let v3 = match v2 { + Ok(ok) => ok, + Err(v3) => { + return { + let v4 = todo_app_sqlite_pavex::user_agent::invalid_user_agent(&v3); + <pavex::response::Response as pavex::response::IntoResponse>::into_response( + v4, + ) + }; + } + }; + let v4 = pavex::request::route::RouteParams::extract(v0); + let v5 = match v4 { + Ok(ok) => ok, + Err(v5) => { + return { + let v6 = pavex::request::route::errors::ExtractRouteParamsError::into_response( + &v5, + ); + <pavex::response::Response< + http_body_util::Full<bytes::Bytes>, + > as pavex::response::IntoResponse>::into_response(v6) + }; + } + }; + let v6 = todo_app_sqlite_pavex::routes::greet::greet(v5, v3); + <pavex::response::Response as pavex::response::IntoResponse>::into_response(v6) + } + pub struct Next0<'a, 'b, 'c, T> + where + T: std::future::Future<Output = pavex::response::Response>, + { + s_0: pavex::request::route::RawRouteParams<'a, 'b>, + s_1: &'c pavex::request::RequestHead, + next: fn( + pavex::request::route::RawRouteParams<'a, 'b>, + &'c pavex::request::RequestHead, + ) -> T, + } + impl<'a, 'b, 'c, T> std::future::IntoFuture for Next0<'a, 'b, 'c, T> + where + T: std::future::Future<Output = pavex::response::Response>, + { + type Output = pavex::response::Response; + type IntoFuture = T; + fn into_future(self) -> Self::IntoFuture { + (self.next)(self.s_0, self.s_1) + } + } +} +pub mod route_2 { + pub async fn middleware_0( + v0: pavex::request::route::MatchedRouteTemplate, + v1: &pavex::router::AllowedMethods, + v2: &pavex::request::RequestHead, + ) -> pavex::response::Response { + let v3 = todo_app_sqlite_pavex::telemetry::RootSpan::new(v2, v0); + let v4 = crate::route_2::Next0 { + s_0: v1, + next: handler, + }; + let v5 = pavex::middleware::Next::new(v4); + todo_app_sqlite_pavex::telemetry::logger(v5, v3).await + } + pub async fn handler( + v0: &pavex::router::AllowedMethods, + ) -> pavex::response::Response { + let v1 = pavex::router::default_fallback(v0).await; + <pavex::response::Response as pavex::response::IntoResponse>::into_response(v1) + } + pub struct Next0<'a, T> + where + T: std::future::Future<Output = pavex::response::Response>, + { + s_0: &'a pavex::router::AllowedMethods, + next: fn(&'a pavex::router::AllowedMethods) -> T, + } + impl<'a, T> std::future::IntoFuture for Next0<'a, T> + where + T: std::future::Future<Output = pavex::response::Response>, + { + type Output = pavex::response::Response; + type IntoFuture = T; + fn into_future(self) -> Self::IntoFuture { + (self.next)(self.s_0) + } + } +} diff --git a/flake.lock b/flake.lock index 36554d16b1..ffc538721d 100644 --- a/flake.lock +++ b/flake.lock @@ -38,11 +38,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1703637592, - "narHash": "sha256-8MXjxU0RfFfzl57Zy3OfXCITS0qWDNLzlBAdwxGZwfY=", + "lastModified": 1703961334, + "narHash": "sha256-M1mV/Cq+pgjk0rt6VxoyyD+O8cOUiai8t9Q6Yyq4noY=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "cfc3698c31b1fb9cdcf10f36c9643460264d0ca8", + "rev": "b0d36bd0a420ecee3bc916c91886caca87c894e9", "type": "github" }, "original": { @@ -81,11 +81,11 @@ "nixpkgs": "nixpkgs_2" }, "locked": { - "lastModified": 1703902408, - "narHash": "sha256-qXdWvu+tlgNjeoz8yQMRKSom6QyRROfgpmeOhwbujqw=", + "lastModified": 1704075545, + "narHash": "sha256-L3zgOuVKhPjKsVLc3yTm2YJ6+BATyZBury7wnhyc8QU=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "319f57cd2c34348c55970a4bf2b35afe82088681", + "rev": "a0df72e106322b67e9c6e591fe870380bd0da0d5", "type": "github" }, "original": { diff --git a/leptos_macro/src/server.rs b/leptos_macro/src/server.rs index cb2cbc197f..035d2300ab 100644 --- a/leptos_macro/src/server.rs +++ b/leptos_macro/src/server.rs @@ -50,17 +50,21 @@ pub fn server_impl(args: TokenStream, s: TokenStream) -> TokenStream { if args.prefix.is_none() { args.prefix = Some(Literal::string("/api")); } + let args_prefix = match &args.prefix { + Some(s) => s.to_string(), + None => "/api".to_string(), + }; // default to "Url" if no encoding given if args.encoding.is_none() { args.encoding = Some(Literal::string("Url")); } - + // Either this match is wrong, or the impl in the macro crate is wrong match server_fn_macro::server_macro_impl( quote::quote!(#args), mapped_body, syn::parse_quote!(::leptos::leptos_server::ServerFnTraitObj), - None, Some(syn::parse_quote!(::leptos::server_fn)), + &args_prefix, ) { Err(e) => e.to_compile_error().into(), Ok(s) => s.to_token_stream().into(), diff --git a/leptos_server/src/action.rs b/leptos_server/src/action.rs index b068198fd3..6052f6401e 100644 --- a/leptos_server/src/action.rs +++ b/leptos_server/src/action.rs @@ -1,8 +1,9 @@ -use crate::{ServerFn, ServerFnError}; +//use crate::{ServerFn, ServerFnError}; use leptos_reactive::{ batch, create_rw_signal, is_suppressing_resource_load, signal_prelude::*, spawn_local, store_value, ReadSignal, RwSignal, StoredValue, }; +use server_fn::{ServerFn, ServerFnError}; use std::{cell::Cell, future::Future, pin::Pin, rc::Rc}; /// An action synchronizes an imperative `async` call to the synchronous reactive system. diff --git a/leptos_server/src/lib.rs b/leptos_server/src/lib.rs index 6aa6773193..8d925fedae 100644 --- a/leptos_server/src/lib.rs +++ b/leptos_server/src/lib.rs @@ -1,120 +1,120 @@ -#![deny(missing_docs)] +//#![deny(missing_docs)] #![forbid(unsafe_code)] -//! # Leptos Server Functions -//! -//! This package is based on a simple idea: sometimes it’s useful to write functions -//! that will only run on the server, and call them from the client. -//! -//! If you’re creating anything beyond a toy app, you’ll need to do this all the time: -//! reading from or writing to a database that only runs on the server, running expensive -//! computations using libraries you don’t want to ship down to the client, accessing -//! APIs that need to be called from the server rather than the client for CORS reasons -//! or because you need a secret API key that’s stored on the server and definitely -//! shouldn’t be shipped down to a user’s browser. -//! -//! Traditionally, this is done by separating your server and client code, and by setting -//! up something like a REST API or GraphQL API to allow your client to fetch and mutate -//! data on the server. This is fine, but it requires you to write and maintain your code -//! in multiple separate places (client-side code for fetching, server-side functions to run), -//! as well as creating a third thing to manage, which is the API contract between the two. -//! -//! This package provides two simple primitives that allow you instead to write co-located, -//! isomorphic server functions. (*Co-located* means you can write them in your app code so -//! that they are “located alongside” the client code that calls them, rather than separating -//! the client and server sides. *Isomorphic* means you can call them from the client as if -//! you were simply calling a function; the function call has the “same shape” on the client -//! as it does on the server.) -//! -//! ### `#[server]` -//! -//! The [`#[server]`](https://docs.rs/leptos/latest/leptos/attr.server.html) macro allows you to annotate a function to -//! indicate that it should only run on the server (i.e., when you have an `ssr` feature in your -//! crate that is enabled). -//! -//! ```rust,ignore -//! use leptos::*; -//! #[server(ReadFromDB)] -//! async fn read_posts(how_many: usize, query: String) -> Result<Vec<Posts>, ServerFnError> { -//! // do some server-only work here to access the database -//! let posts = todo!();; -//! Ok(posts) -//! } -//! -//! // call the function -//! spawn_local(async { -//! let posts = read_posts(3, "my search".to_string()).await; -//! log::debug!("posts = {posts:#?}"); -//! }); -//! ``` -//! -//! If you call this function from the client, it will serialize the function arguments and `POST` -//! them to the server as if they were the inputs in `<form method="POST">`. -//! -//! Here’s what you need to remember: -//! - **Server functions must be `async`.** Even if the work being done inside the function body -//! can run synchronously on the server, from the client’s perspective it involves an asynchronous -//! function call. -//! - **Server functions must return `Result<T, ServerFnError>`.** Even if the work being done -//! inside the function body can’t fail, the processes of serialization/deserialization and the -//! network call are fallible. -//! - **Return types must be [Serializable](leptos_reactive::Serializable).** -//! This should be fairly obvious: we have to serialize arguments to send them to the server, and we -//! need to deserialize the result to return it to the client. -//! - **Arguments must be implement [serde::Serialize].** They are serialized as an `application/x-www-form-urlencoded` -//! form data using [`serde_qs`](https://docs.rs/serde_qs/latest/serde_qs/) or as `application/cbor` -//! using [`cbor`](https://docs.rs/cbor/latest/cbor/). **Note**: You should explicitly include `serde` with the -//! `derive` feature enabled in your `Cargo.toml`. You can do this by running `cargo add serde --features=derive`. -//! - Context comes from the server. [`use_context`](leptos_reactive::use_context) can be used to access specific -//! server-related data, as documented in the server integrations. This allows accessing things like HTTP request -//! headers as needed. However, server functions *not* have access to reactive state that exists in the client. -//! -//! ## Server Function Encodings -//! -//! By default, the server function call is a `POST` request that serializes the arguments as URL-encoded form data in the body -//! of the request. But there are a few other methods supported. Optionally, we can provide another argument to the `#[server]` -//! macro to specify an alternate encoding: -//! -//! ```rust,ignore -//! #[server(AddTodo, "/api", "Url")] -//! #[server(AddTodo, "/api", "GetJson")] -//! #[server(AddTodo, "/api", "Cbor")] -//! #[server(AddTodo, "/api", "GetCbor")] -//! ``` -//! -//! The four options use different combinations of HTTP verbs and encoding methods: -//! -//! | Name | Method | Request | Response | -//! | ----------------- | ------ | ----------- | -------- | -//! | **Url** (default) | POST | URL encoded | JSON | -//! | **GetJson** | GET | URL encoded | JSON | -//! | **Cbor** | POST | CBOR | CBOR | -//! | **GetCbor** | GET | URL encoded | CBOR | -//! -//! In other words, you have two choices: -//! -//! - `GET` or `POST`? This has implications for things like browser or CDN caching; while `POST` requests should not be cached, -//! `GET` requests can be. -//! - Plain text (arguments sent with URL/form encoding, results sent as JSON) or a binary format (CBOR, encoded as a base64 -//! string)? -//! -//! ## Why not `PUT` or `DELETE`? Why URL/form encoding, and not JSON?** -//! -//! These are reasonable questions. Much of the web is built on REST API patterns that encourage the use of semantic HTTP -//! methods like `DELETE` to delete an item from a database, and many devs are accustomed to sending data to APIs in the -//! JSON format. -//! -//! The reason we use `POST` or `GET` with URL-encoded data by default is the `<form>` support. For better or for worse, -//! HTML forms don’t support `PUT` or `DELETE`, and they don’t support sending JSON. This means that if you use anything -//! but a `GET` or `POST` request with URL-encoded data, it can only work once WASM has loaded. -//! -//! The CBOR encoding is suported for historical reasons; an earlier version of server functions used a URL encoding that -//! didn’t support nested objects like structs or vectors as server function arguments, which CBOR did. But note that the -//! CBOR forms encounter the same issue as `PUT`, `DELETE`, or JSON: they do not degrade gracefully if the WASM version of -//! your app is not available. +////! # Leptos Server Functions +////! +////! This package is based on a simple idea: sometimes it’s useful to write functions +////! that will only run on the server, and call them from the client. +////! +////! If you’re creating anything beyond a toy app, you’ll need to do this all the time: +////! reading from or writing to a database that only runs on the server, running expensive +////! computations using libraries you don’t want to ship down to the client, accessing +////! APIs that need to be called from the server rather than the client for CORS reasons +////! or because you need a secret API key that’s stored on the server and definitely +////! shouldn’t be shipped down to a user’s browser. +////! +////! Traditionally, this is done by separating your server and client code, and by setting +////! up something like a REST API or GraphQL API to allow your client to fetch and mutate +////! data on the server. This is fine, but it requires you to write and maintain your code +////! in multiple separate places (client-side code for fetching, server-side functions to run), +////! as well as creating a third thing to manage, which is the API contract between the two. +////! +////! This package provides two simple primitives that allow you instead to write co-located, +////! isomorphic server functions. (*Co-located* means you can write them in your app code so +////! that they are “located alongside” the client code that calls them, rather than separating +////! the client and server sides. *Isomorphic* means you can call them from the client as if +////! you were simply calling a function; the function call has the “same shape” on the client +////! as it does on the server.) +////! +////! ### `#[server]` +////! +////! The [`#[server]`](https://docs.rs/leptos/latest/leptos/attr.server.html) macro allows you to annotate a function to +////! indicate that it should only run on the server (i.e., when you have an `ssr` feature in your +////! crate that is enabled). +////! +////! ```rust,ignore +////! use leptos::*; +////! #[server(ReadFromDB)] +////! async fn read_posts(how_many: usize, query: String) -> Result<Vec<Posts>, ServerFnError> { +////! // do some server-only work here to access the database +////! let posts = todo!();; +////! Ok(posts) +////! } +////! +////! // call the function +////! spawn_local(async { +////! let posts = read_posts(3, "my search".to_string()).await; +////! log::debug!("posts = {posts:#?}"); +////! }); +////! ``` +////! +////! If you call this function from the client, it will serialize the function arguments and `POST` +////! them to the server as if they were the inputs in `<form method="POST">`. +////! +////! Here’s what you need to remember: +////! - **Server functions must be `async`.** Even if the work being done inside the function body +////! can run synchronously on the server, from the client’s perspective it involves an asynchronous +////! function call. +////! - **Server functions must return `Result<T, ServerFnError>`.** Even if the work being done +////! inside the function body can’t fail, the processes of serialization/deserialization and the +////! network call are fallible. +////! - **Return types must be [Serializable](leptos_reactive::Serializable).** +////! This should be fairly obvious: we have to serialize arguments to send them to the server, and we +////! need to deserialize the result to return it to the client. +////! - **Arguments must be implement [serde::Serialize].** They are serialized as an `application/x-www-form-urlencoded` +////! form data using [`serde_qs`](https://docs.rs/serde_qs/latest/serde_qs/) or as `application/cbor` +////! using [`cbor`](https://docs.rs/cbor/latest/cbor/). **Note**: You should explicitly include `serde` with the +////! `derive` feature enabled in your `Cargo.toml`. You can do this by running `cargo add serde --features=derive`. +////! - Context comes from the server. [`use_context`](leptos_reactive::use_context) can be used to access specific +////! server-related data, as documented in the server integrations. This allows accessing things like HTTP request +////! headers as needed. However, server functions *not* have access to reactive state that exists in the client. +////! +////! ## Server Function Encodings +////! +////! By default, the server function call is a `POST` request that serializes the arguments as URL-encoded form data in the body +////! of the request. But there are a few other methods supported. Optionally, we can provide another argument to the `#[server]` +////! macro to specify an alternate encoding: +////! +////! ```rust,ignore +////! #[server(AddTodo, "/api", "Url")] +////! #[server(AddTodo, "/api", "GetJson")] +////! #[server(AddTodo, "/api", "Cbor")] +////! #[server(AddTodo, "/api", "GetCbor")] +////! ``` +////! +////! The four options use different combinations of HTTP verbs and encoding methods: +////! +////! | Name | Method | Request | Response | +////! | ----------------- | ------ | ----------- | -------- | +////! | **Url** (default) | POST | URL encoded | JSON | +////! | **GetJson** | GET | URL encoded | JSON | +////! | **Cbor** | POST | CBOR | CBOR | +////! | **GetCbor** | GET | URL encoded | CBOR | +////! +////! In other words, you have two choices: +////! +////! - `GET` or `POST`? This has implications for things like browser or CDN caching; while `POST` requests should not be cached, +////! `GET` requests can be. +////! - Plain text (arguments sent with URL/form encoding, results sent as JSON) or a binary format (CBOR, encoded as a base64 +////! string)? +////! +////! ## Why not `PUT` or `DELETE`? Why URL/form encoding, and not JSON?** +////! +////! These are reasonable questions. Much of the web is built on REST API patterns that encourage the use of semantic HTTP +////! methods like `DELETE` to delete an item from a database, and many devs are accustomed to sending data to APIs in the +////! JSON format. +////! +////! The reason we use `POST` or `GET` with URL-encoded data by default is the `<form>` support. For better or for worse, +////! HTML forms don’t support `PUT` or `DELETE`, and they don’t support sending JSON. This means that if you use anything +////! but a `GET` or `POST` request with URL-encoded data, it can only work once WASM has loaded. +////! +////! The CBOR encoding is suported for historical reasons; an earlier version of server functions used a URL encoding that +////! didn’t support nested objects like structs or vectors as server function arguments, which CBOR did. But note that the +////! CBOR forms encounter the same issue as `PUT`, `DELETE`, or JSON: they do not degrade gracefully if the WASM version of +////! your app is not available. pub use server_fn::{ - error::ServerFnErrorErr, Encoding, Payload, ServerFnError, + error::ServerFnErrorErr, ServerFnError, }; mod action; @@ -123,250 +123,250 @@ pub use action::*; pub use multi_action::*; extern crate tracing; -#[cfg(any(feature = "ssr", doc))] -use std::{ - collections::HashMap, - sync::{Arc, RwLock}, -}; - -#[cfg(any(feature = "ssr", doc))] -/// A concrete type for a server function. -#[derive(Clone)] -pub struct ServerFnTraitObj(pub server_fn::ServerFnTraitObj<()>); - -#[cfg(any(feature = "ssr", doc))] -impl std::ops::Deref for ServerFnTraitObj { - type Target = server_fn::ServerFnTraitObj<()>; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -#[cfg(any(feature = "ssr", doc))] -impl std::ops::DerefMut for ServerFnTraitObj { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } -} - -#[cfg(any(feature = "ssr", doc))] -impl ServerFnTraitObj { - /// Create a new `ServerFnTraitObj` from a `server_fn::ServerFnTraitObj`. - pub const fn from_generic_server_fn( - server_fn: server_fn::ServerFnTraitObj<()>, - ) -> Self { - Self(server_fn) - } -} - -#[cfg(feature = "ssr")] -inventory::collect!(ServerFnTraitObj); - -#[allow(unused)] -type ServerFunction = server_fn::ServerFnTraitObj<()>; - -#[cfg(any(feature = "ssr", doc))] -lazy_static::lazy_static! { - static ref REGISTERED_SERVER_FUNCTIONS: Arc<RwLock<HashMap<&'static str, ServerFnTraitObj>>> = { - let mut map = HashMap::new(); - for server_fn in inventory::iter::<ServerFnTraitObj> { - map.insert(server_fn.0.url(), server_fn.clone()); - } - Arc::new(RwLock::new(map)) - }; -} - -#[cfg(any(feature = "ssr", doc))] -/// The registry of all Leptos server functions. -pub struct LeptosServerFnRegistry; - -#[cfg(any(feature = "ssr", doc))] -impl server_fn::ServerFunctionRegistry<()> for LeptosServerFnRegistry { - type Error = ServerRegistrationFnError; - - /// Server functions are automatically registered on most platforms, (including Linux, macOS, - /// iOS, FreeBSD, Android, and Windows). If you are on another platform, like a WASM server runtime, - /// you should register server functions by calling this `T::register_explicit()`. - fn register_explicit( - prefix: &'static str, - url: &'static str, - server_function: server_fn::SerializedFnTraitObj<()>, - encoding: Encoding, - ) -> Result<(), Self::Error> { - // store it in the hashmap - let mut func_write = REGISTERED_SERVER_FUNCTIONS - .write() - .map_err(|e| ServerRegistrationFnError::Poisoned(e.to_string()))?; - let prev = func_write.insert( - url, - ServerFnTraitObj(server_fn::ServerFnTraitObj::new( - prefix, - url, - encoding, - server_function, - )), - ); - - // if there was already a server function with this key, - // return Err - match prev { - Some(_) => { - Err(ServerRegistrationFnError::AlreadyRegistered(format!( - "There was already a server function registered at {:?}. \ - This can happen if you use the same server function name \ - in two different modules - on `stable` or in `release` mode.", - url - ))) - } - None => Ok(()), - } - } - - /// Returns the server function registered at the given URL, or `None` if no function is registered at that URL. - fn get(url: &str) -> Option<server_fn::ServerFnTraitObj<()>> { - REGISTERED_SERVER_FUNCTIONS - .read() - .ok() - .and_then(|fns| fns.get(url).map(|sf| sf.0.clone())) - } - - /// Returns the server function trait obj registered at the given URL, or `None` if no function is registered at that URL. - fn get_trait_obj(url: &str) -> Option<server_fn::ServerFnTraitObj<()>> { - REGISTERED_SERVER_FUNCTIONS - .read() - .ok() - .and_then(|fns| fns.get(url).map(|sf| sf.0.clone())) - } - /// Return the - fn get_encoding(url: &str) -> Option<Encoding> { - REGISTERED_SERVER_FUNCTIONS - .read() - .ok() - .and_then(|fns| fns.get(url).map(|sf| sf.encoding())) - } - - /// Returns a list of all registered server functions. - fn paths_registered() -> Vec<&'static str> { - REGISTERED_SERVER_FUNCTIONS - .read() - .ok() - .map(|fns| fns.keys().cloned().collect()) - .unwrap_or_default() - } -} - -#[cfg(any(feature = "ssr", doc))] -/// Errors that can occur when registering a server function. -#[derive( - thiserror::Error, Debug, Clone, serde::Serialize, serde::Deserialize, -)] -pub enum ServerRegistrationFnError { - /// The server function is already registered. - #[error("The server function {0} is already registered")] - AlreadyRegistered(String), - /// The server function registry is poisoned. - #[error("The server function registry is poisoned: {0}")] - Poisoned(String), -} - -/// Get a ServerFunction struct containing info about the server fn -#[cfg(any(feature = "ssr", doc))] -pub fn server_fn_by_path(path: &str) -> Option<ServerFnTraitObj> { - REGISTERED_SERVER_FUNCTIONS - .read() - .expect("Server function registry is poisoned") - .get(path) - .cloned() -} - -/// Attempts to find a server function registered at the given path. -/// -/// This can be used by a server to handle the requests, as in the following example (using `actix-web`) -/// -/// ```rust, ignore -/// #[post("{tail:.*}")] -/// async fn handle_server_fns( -/// req: HttpRequest, -/// params: web::Path<String>, -/// body: web::Bytes, -/// ) -> impl Responder { -/// let path = params.into_inner(); -/// let accept_header = req -/// .headers() -/// .get("Accept") -/// .and_then(|value| value.to_str().ok()); -/// if let Some(server_fn) = server_fn_by_path(path.as_str()) { -/// let query = req.query_string().as_bytes(); -/// let data = match &server_fn.encoding { -/// Encoding::Url | Encoding::Cbor => &body, -/// Encoding::GetJSON | Encoding::GetCBOR => query, -/// }; -/// match (server_fn.trait_obj)(data).await { -/// Ok(serialized) => { -/// // if this is Accept: application/json then send a serialized JSON response -/// if let Some("application/json") = accept_header { -/// HttpResponse::Ok().body(serialized) -/// } -/// // otherwise, it's probably a <form> submit or something: redirect back to the referrer -/// else { -/// HttpResponse::SeeOther() -/// .insert_header(("Location", "/")) -/// .content_type("application/json") -/// .body(serialized) -/// } -/// } -/// Err(e) => { -/// eprintln!("server function error: {e:#?}"); -/// HttpResponse::InternalServerError().body(e.to_string()) -/// } -/// } -/// } else { -/// HttpResponse::BadRequest().body(format!("Could not find a server function at that route.")) -/// } -/// } -/// ``` -#[cfg(any(feature = "ssr", doc))] -pub fn server_fn_trait_obj_by_path(path: &str) -> Option<ServerFnTraitObj> { - server_fn::server_fn_trait_obj_by_path::<(), LeptosServerFnRegistry>(path) - .map(ServerFnTraitObj::from_generic_server_fn) -} - -/// Get the Encoding of a server fn if one is registered at that path. Otherwise, return None -#[cfg(any(feature = "ssr", doc))] -pub fn server_fn_encoding_by_path(path: &str) -> Option<Encoding> { - server_fn::server_fn_encoding_by_path::<(), LeptosServerFnRegistry>(path) -} - -/// Returns the set of currently-registered server function paths, for debugging purposes. -#[cfg(any(feature = "ssr", doc))] -pub fn server_fns_by_path() -> Vec<&'static str> { - server_fn::server_fns_by_path::<(), LeptosServerFnRegistry>() -} - -/// Defines a "server function." A server function can be called from the server or the client, -/// but the body of its code will only be run on the server, i.e., if a crate feature `ssr` is enabled. -/// -/// (This follows the same convention as the Leptos framework's distinction between `ssr` for server-side rendering, -/// and `csr` and `hydrate` for client-side rendering and hydration, respectively.) -/// -/// Server functions are created using the `server` macro. -/// -/// The function should be registered by calling `ServerFn::register()`. The set of server functions -/// can be queried on the server for routing purposes by calling [server_fn_by_path]. -/// -/// Technically, the trait is implemented on a type that describes the server function's arguments. -pub trait ServerFn: server_fn::ServerFn<()> { - #[cfg(any(feature = "ssr", doc))] - /// Explicitly registers the server function on platforms that require it, - /// allowing the server to query it by URL. - /// - /// Explicit server function registration is no longer required on most platforms - /// (including Linux, macOS, iOS, FreeBSD, Android, and Windows) - fn register_explicit() -> Result<(), ServerFnError> { - Self::register_in_explicit::<LeptosServerFnRegistry>() - } -} - -impl<T> ServerFn for T where T: server_fn::ServerFn<()> {} +//#[cfg(any(feature = "ssr", doc))] +//use std::{ +// collections::HashMap, +// sync::{Arc, RwLock}, +//}; +// +//#[cfg(any(feature = "ssr", doc))] +///// A concrete type for a server function. +//#[derive(Clone)] +//pub struct ServerFnTraitObj(pub server_fn::ServerFnTraitObj<()>); +// +//#[cfg(any(feature = "ssr", doc))] +//impl std::ops::Deref for ServerFnTraitObj { +// type Target = server_fn::ServerFnTraitObj<()>; +// +// fn deref(&self) -> &Self::Target { +// &self.0 +// } +//} +// +//#[cfg(any(feature = "ssr", doc))] +//impl std::ops::DerefMut for ServerFnTraitObj { +// fn deref_mut(&mut self) -> &mut Self::Target { +// &mut self.0 +// } +//} +// +//#[cfg(any(feature = "ssr", doc))] +//impl ServerFnTraitObj { +// /// Create a new `ServerFnTraitObj` from a `server_fn::ServerFnTraitObj`. +// pub const fn from_generic_server_fn( +// server_fn: server_fn::ServerFnTraitObj<()>, +// ) -> Self { +// Self(server_fn) +// } +//} +// +//#[cfg(feature = "ssr")] +//inventory::collect!(ServerFnTraitObj); +// +//#[allow(unused)] +//type ServerFunction = server_fn::ServerFnTraitObj<()>; +// +//#[cfg(any(feature = "ssr", doc))] +//lazy_static::lazy_static! { +// static ref REGISTERED_SERVER_FUNCTIONS: Arc<RwLock<HashMap<&'static str, ServerFnTraitObj>>> = { +// let mut map = HashMap::new(); +// for server_fn in inventory::iter::<ServerFnTraitObj> { +// map.insert(server_fn.0.url(), server_fn.clone()); +// } +// Arc::new(RwLock::new(map)) +// }; +//} +// +//#[cfg(any(feature = "ssr", doc))] +///// The registry of all Leptos server functions. +//pub struct LeptosServerFnRegistry; +// +//#[cfg(any(feature = "ssr", doc))] +//impl server_fn::ServerFunctionRegistry<()> for LeptosServerFnRegistry { +// type Error = ServerRegistrationFnError; +// +// /// Server functions are automatically registered on most platforms, (including Linux, macOS, +// /// iOS, FreeBSD, Android, and Windows). If you are on another platform, like a WASM server runtime, +// /// you should register server functions by calling this `T::register_explicit()`. +// fn register_explicit( +// prefix: &'static str, +// url: &'static str, +// server_function: server_fn::SerializedFnTraitObj<()>, +// encoding: Encoding, +// ) -> Result<(), Self::Error> { +// // store it in the hashmap +// let mut func_write = REGISTERED_SERVER_FUNCTIONS +// .write() +// .map_err(|e| ServerRegistrationFnError::Poisoned(e.to_string()))?; +// let prev = func_write.insert( +// url, +// ServerFnTraitObj(server_fn::ServerFnTraitObj::new( +// prefix, +// url, +// encoding, +// server_function, +// )), +// ); +// +// // if there was already a server function with this key, +// // return Err +// match prev { +// Some(_) => { +// Err(ServerRegistrationFnError::AlreadyRegistered(format!( +// "There was already a server function registered at {:?}. \ +// This can happen if you use the same server function name \ +// in two different modules +// on `stable` or in `release` mode.", +// url +// ))) +// } +// None => Ok(()), +// } +// } +// +// /// Returns the server function registered at the given URL, or `None` if no function is registered at that URL. +// fn get(url: &str) -> Option<server_fn::ServerFnTraitObj<()>> { +// REGISTERED_SERVER_FUNCTIONS +// .read() +// .ok() +// .and_then(|fns| fns.get(url).map(|sf| sf.0.clone())) +// } +// +// /// Returns the server function trait obj registered at the given URL, or `None` if no function is registered at that URL. +// fn get_trait_obj(url: &str) -> Option<server_fn::ServerFnTraitObj<()>> { +// REGISTERED_SERVER_FUNCTIONS +// .read() +// .ok() +// .and_then(|fns| fns.get(url).map(|sf| sf.0.clone())) +// } +// /// Return the +// fn get_encoding(url: &str) -> Option<Encoding> { +// REGISTERED_SERVER_FUNCTIONS +// .read() +// .ok() +// .and_then(|fns| fns.get(url).map(|sf| sf.encoding())) +// } +// +// /// Returns a list of all registered server functions. +// fn paths_registered() -> Vec<&'static str> { +// REGISTERED_SERVER_FUNCTIONS +// .read() +// .ok() +// .map(|fns| fns.keys().cloned().collect()) +// .unwrap_or_default() +// } +//} +// +//#[cfg(any(feature = "ssr", doc))] +///// Errors that can occur when registering a server function. +//#[derive( +// thiserror::Error, Debug, Clone, serde::Serialize, serde::Deserialize, +//)] +//pub enum ServerRegistrationFnError { +// /// The server function is already registered. +// #[error("The server function {0} is already registered")] +// AlreadyRegistered(String), +// /// The server function registry is poisoned. +// #[error("The server function registry is poisoned: {0}")] +// Poisoned(String), +//} +// +///// Get a ServerFunction struct containing info about the server fn +//#[cfg(any(feature = "ssr", doc))] +//pub fn server_fn_by_path(path: &str) -> Option<ServerFnTraitObj> { +// REGISTERED_SERVER_FUNCTIONS +// .read() +// .expect("Server function registry is poisoned") +// .get(path) +// .cloned() +//} +// +///// Attempts to find a server function registered at the given path. +///// +///// This can be used by a server to handle the requests, as in the following example (using `actix-web`) +///// +///// ```rust, ignore +///// #[post("{tail:.*}")] +///// async fn handle_server_fns( +///// req: HttpRequest, +///// params: web::Path<String>, +///// body: web::Bytes, +///// ) -> impl Responder { +///// let path = params.into_inner(); +///// let accept_header = req +///// .headers() +///// .get("Accept") +///// .and_then(|value| value.to_str().ok()); +///// if let Some(server_fn) = server_fn_by_path(path.as_str()) { +///// let query = req.query_string().as_bytes(); +///// let data = match &server_fn.encoding { +///// Encoding::Url | Encoding::Cbor => &body, +///// Encoding::GetJSON | Encoding::GetCBOR => query, +///// }; +///// match (server_fn.trait_obj)(data).await { +///// Ok(serialized) => { +///// // if this is Accept: application/json then send a serialized JSON response +///// if let Some("application/json") = accept_header { +///// HttpResponse::Ok().body(serialized) +///// } +///// // otherwise, it's probably a <form> submit or something: redirect back to the referrer +///// else { +///// HttpResponse::SeeOther() +///// .insert_header(("Location", "/")) +///// .content_type("application/json") +///// .body(serialized) +///// } +///// } +///// Err(e) => { +///// eprintln!("server function error: {e:#?}"); +///// HttpResponse::InternalServerError().body(e.to_string()) +///// } +///// } +///// } else { +///// HttpResponse::BadRequest().body(format!("Could not find a server function at that route.")) +///// } +///// } +///// ``` +//#[cfg(any(feature = "ssr", doc))] +//pub fn server_fn_trait_obj_by_path(path: &str) -> Option<ServerFnTraitObj> { +// server_fn::server_fn_trait_obj_by_path::<(), LeptosServerFnRegistry>(path) +// .map(ServerFnTraitObj::from_generic_server_fn) +//} +// +///// Get the Encoding of a server fn if one is registered at that path. Otherwise, return None +//#[cfg(any(feature = "ssr", doc))] +//pub fn server_fn_encoding_by_path(path: &str) -> Option<Encoding> { +// server_fn::server_fn_encoding_by_path::<(), LeptosServerFnRegistry>(path) +//} +// +///// Returns the set of currently-registered server function paths, for debugging purposes. +//#[cfg(any(feature = "ssr", doc))] +//pub fn server_fns_by_path() -> Vec<&'static str> { +// server_fn::server_fns_by_path::<(), LeptosServerFnRegistry>() +//} +// +///// Defines a "server function." A server function can be called from the server or the client, +///// but the body of its code will only be run on the server, i.e., if a crate feature `ssr` is enabled. +///// +///// (This follows the same convention as the Leptos framework's distinction between `ssr` for server-side rendering, +///// and `csr` and `hydrate` for client-side rendering and hydration, respectively.) +///// +///// Server functions are created using the `server` macro. +///// +///// The function should be registered by calling `ServerFn::register()`. The set of server functions +///// can be queried on the server for routing purposes by calling [server_fn_by_path]. +///// +///// Technically, the trait is implemented on a type that describes the server function's arguments. +//pub trait ServerFn: server_fn::ServerFn<()> { +// #[cfg(any(feature = "ssr", doc))] +// /// Explicitly registers the server function on platforms that require it, +// /// allowing the server to query it by URL. +// /// +// /// Explicit server function registration is no longer required on most platforms +// /// (including Linux, macOS, iOS, FreeBSD, Android, and Windows) +// fn register_explicit() -> Result<(), ServerFnError> { +// Self::register_in_explicit::<LeptosServerFnRegistry>() +// } +//} +// +//impl<T> ServerFn for T where T: server_fn::ServerFn<()> {} diff --git a/leptos_server/src/multi_action.rs b/leptos_server/src/multi_action.rs index c6c3c23b99..18d480344d 100644 --- a/leptos_server/src/multi_action.rs +++ b/leptos_server/src/multi_action.rs @@ -1,4 +1,4 @@ -use crate::{ServerFn, ServerFnError}; +use server_fn::{ServerFn, ServerFnError}; use leptos_reactive::{ create_rw_signal, is_suppressing_resource_load, signal_prelude::*, spawn_local, store_value, untrack, ReadSignal, RwSignal, StoredValue, diff --git a/server_fn/Cargo.toml b/server_fn/Cargo.toml index 9a126aa18e..affd31e13f 100644 --- a/server_fn/Cargo.toml +++ b/server_fn/Cargo.toml @@ -32,7 +32,7 @@ multer = { version = "3", optional = true } ## output encodings # serde -serde_json = { version = "1", optional = true } +serde_json = "1" futures = "0.3" http = { version = "1", optional = true } ciborium = { version = "0.2", optional = true } @@ -81,7 +81,7 @@ browser = [ "dep:wasm-streams", "dep:wasm-bindgen-futures", ] -json = ["dep:serde_json"] +#json = ["dep:serde_json"] multipart = ["dep:multer"] url = ["dep:serde_qs"] cbor = ["dep:ciborium"] diff --git a/server_fn/src/error.rs b/server_fn/src/error.rs index 00c855669d..e517c52c74 100644 --- a/server_fn/src/error.rs +++ b/server_fn/src/error.rs @@ -1,8 +1,55 @@ -use core::fmt::{self, Display}; - +use core::fmt::Display; use serde::{Deserialize, Serialize}; +use std::{error, fmt, ops, sync::Arc}; use thiserror::Error; +/// This is a result type into which any error can be converted, +/// and which can be used directly in your `view`. +/// +/// All errors will be stored as [`struct@Error`]. +pub type Result<T, E = Error> = core::result::Result<T, E>; + +/// A generic wrapper for any error. +#[derive(Debug, Clone)] +#[repr(transparent)] +pub struct Error(Arc<dyn error::Error + Send + Sync>); + +impl Error { + /// Converts the wrapper into the inner reference-counted error. + pub fn into_inner(self) -> Arc<dyn error::Error + Send + Sync> { + Arc::clone(&self.0) + } +} + +impl ops::Deref for Error { + type Target = Arc<dyn error::Error + Send + Sync>; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +impl<T> From<T> for Error +where + T: std::error::Error + Send + Sync + 'static, +{ + fn from(value: T) -> Self { + Error(Arc::new(value)) + } +} + +impl From<ServerFnError> for Error { + fn from(e: ServerFnError) -> Self { + Error(Arc::new(ServerFnErrorErr::from(e))) + } +} + /// An empty value indicating that there is no custom error type associated /// with this server function. #[derive(Debug, Deserialize, Serialize)] @@ -83,7 +130,12 @@ impl<E: Display + Clone> ViaError<E> for &WrapError<E> { impl<E> ViaError<E> for WrapError<E> { #[track_caller] fn to_server_error(&self) -> ServerFnError<E> { - panic!("At {}, you call `to_server_error()` or use `server_fn_error!` with a value that does not implement `Clone` and either `Error` or `Display`.", std::panic::Location::caller()); + panic!( + "At {}, you call `to_server_error()` or use `server_fn_error!` \ + with a value that does not implement `Clone` and either `Error` \ + or `Display`.", + std::panic::Location::caller() + ); } } @@ -129,19 +181,24 @@ where f, "{}", match self { - ServerFnError::Registration(s) => - format!("error while trying to register the server function: {s}"), - ServerFnError::Request(s) => - format!("error reaching server to call server function: {s}"), - ServerFnError::ServerError(s) => format!("error running server function: {s}"), + ServerFnError::Registration(s) => format!( + "error while trying to register the server function: {s}" + ), + ServerFnError::Request(s) => format!( + "error reaching server to call server function: {s}" + ), + ServerFnError::ServerError(s) => + format!("error running server function: {s}"), ServerFnError::Deserialization(s) => format!("error deserializing server function results: {s}"), ServerFnError::Serialization(s) => format!("error serializing server function arguments: {s}"), - ServerFnError::Args(s) => - format!("error deserializing server function arguments: {s}"), + ServerFnError::Args(s) => format!( + "error deserializing server function arguments: {s}" + ), ServerFnError::MissingArg(s) => format!("missing argument {s}"), - ServerFnError::Response(s) => format!("error generating HTTP response: {s}"), + ServerFnError::Response(s) => + format!("error generating HTTP response: {s}"), ServerFnError::WrappedServerError(e) => format!("{}", e), } ) @@ -202,14 +259,26 @@ pub enum ServerFnErrorErr<E = NoCustomError> { impl<CustErr> From<ServerFnError<CustErr>> for ServerFnErrorErr<CustErr> { fn from(value: ServerFnError<CustErr>) -> Self { match value { - ServerFnError::Registration(value) => ServerFnErrorErr::Registration(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::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::MissingArg(value) => { + ServerFnErrorErr::MissingArg(value) + } + ServerFnError::WrappedServerError(value) => { + ServerFnErrorErr::WrappedServerError(value) + } ServerFnError::Response(value) => ServerFnErrorErr::Response(value), } } From d5b9e84f36f0441ab89305fee2e3fac49b0b1111 Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Tue, 2 Jan 2024 21:34:09 -0500 Subject: [PATCH 006/100] properly gate `inventory` --- server_fn/src/lib.rs | 105 ++++++++++++++++++++++++++++++------------- 1 file changed, 73 insertions(+), 32 deletions(-) diff --git a/server_fn/src/lib.rs b/server_fn/src/lib.rs index 4010e03f5c..0b8eab6d08 100644 --- a/server_fn/src/lib.rs +++ b/server_fn/src/lib.rs @@ -9,20 +9,19 @@ pub mod response; use client::Client; use codec::{Encoding, FromReq, FromRes, IntoReq, IntoRes}; +// reexports for the sake of the macro +#[doc(hidden)] +pub use const_format; use dashmap::DashMap; pub use error::ServerFnError; use middleware::{Layer, Service}; use once_cell::sync::Lazy; use request::Req; use response::{ClientRes, Res}; -use serde::{de::DeserializeOwned, Serialize}; -use std::{future::Future, pin::Pin, sync::Arc}; - -// reexports for the sake of the macro -#[doc(hidden)] -pub use const_format; #[doc(hidden)] pub use serde; +use serde::{de::DeserializeOwned, Serialize}; +use std::{future::Future, pin::Pin, sync::Arc}; #[doc(hidden)] pub use xxhash_rust; @@ -30,7 +29,11 @@ pub trait ServerFn where Self: Send + FromReq<Self::Error, Self::ServerRequest, Self::InputEncoding> - + IntoReq<Self::Error, <Self::Client as Client<Self::Error>>::Request, Self::InputEncoding>, + + IntoReq< + Self::Error, + <Self::Client as Client<Self::Error>>::Request, + Self::InputEncoding, + >, { const PATH: &'static str; @@ -50,8 +53,11 @@ where /// This needs to be converted into `ServerResponse` on the server side, and converted /// *from* `ClientResponse` when received by the client. type Output: IntoRes<Self::Error, Self::ServerResponse, Self::OutputEncoding> - + FromRes<Self::Error, <Self::Client as Client<Self::Error>>::Response, Self::OutputEncoding> - + Send; + + FromRes< + Self::Error, + <Self::Client as Client<Self::Error>>::Response, + Self::OutputEncoding, + > + Send; /// The [`Encoding`] used in the request for arguments into the server function. type InputEncoding: Encoding; @@ -64,7 +70,8 @@ where type Error: Serialize + DeserializeOwned; /// Middleware that should be applied to this server function. - fn middlewares() -> Vec<Arc<dyn Layer<Self::ServerRequest, Self::ServerResponse>>> { + fn middlewares( + ) -> Vec<Arc<dyn Layer<Self::ServerRequest, Self::ServerResponse>>> { Vec::new() } @@ -85,10 +92,12 @@ where fn run_on_client( self, - ) -> impl Future<Output = Result<Self::Output, ServerFnError<Self::Error>>> + Send { + ) -> impl Future<Output = Result<Self::Output, ServerFnError<Self::Error>>> + Send + { async move { // create and send request on client - let req = self.into_req(Self::PATH, Self::OutputEncoding::CONTENT_TYPE)?; + let req = + self.into_req(Self::PATH, Self::OutputEncoding::CONTENT_TYPE)?; let res = Self::Client::send(req).await?; let status = res.status(); @@ -102,11 +111,13 @@ where let text = res.try_into_string().await?; match serde_json::from_str(&text) { Ok(e) => Err(e), - Err(_) => Err(ServerFnError::ServerError(if text.is_empty() { - format!("{} {}", status, status_text) - } else { - format!("{} {}: {}", status, status_text, text) - })), + Err(_) => { + Err(ServerFnError::ServerError(if text.is_empty() { + format!("{} {}", status, status_text) + } else { + format!("{} {}: {}", status, status_text, text) + })) + } } } else { // otherwise, deserialize the body as is @@ -125,7 +136,9 @@ where #[doc(hidden)] fn execute_on_server( req: Self::ServerRequest, - ) -> impl Future<Output = Result<Self::ServerResponse, ServerFnError<Self::Error>>> + Send { + ) -> impl Future< + Output = Result<Self::ServerResponse, ServerFnError<Self::Error>>, + > + Send { async { let this = Self::from_req(req).await?; let output = this.run_body().await?; @@ -139,6 +152,7 @@ where } } +#[cfg(feature = "ssr")] #[doc(hidden)] pub use inventory; @@ -197,7 +211,8 @@ impl<Req, Res> Clone for ServerFnTraitObj<Req, Res> { impl<Req, Res> Copy for ServerFnTraitObj<Req, Res> {} -type LazyServerFnMap<Req, Res> = Lazy<DashMap<&'static str, ServerFnTraitObj<Req, Res>>>; +type LazyServerFnMap<Req, Res> = + Lazy<DashMap<&'static str, ServerFnTraitObj<Req, Res>>>; // Axum integration #[cfg(feature = "axum")] @@ -211,12 +226,17 @@ pub mod axum { inventory::collect!(ServerFnTraitObj<Request<Body>, Response<Body>>); - static REGISTERED_SERVER_FUNCTIONS: LazyServerFnMap<Request<Body>, Response<Body>> = - initialize_server_fn_map!(Request<Body>, Response<Body>); + static REGISTERED_SERVER_FUNCTIONS: LazyServerFnMap< + Request<Body>, + Response<Body>, + > = initialize_server_fn_map!(Request<Body>, Response<Body>); pub fn register_explicit<T>() where - T: ServerFn<ServerRequest = Request<Body>, ServerResponse = Response<Body>> + 'static, + T: ServerFn< + ServerRequest = Request<Body>, + ServerResponse = Response<Body>, + > + 'static, { REGISTERED_SERVER_FUNCTIONS.insert( T::PATH, @@ -242,7 +262,14 @@ pub mod axum { Response::builder() .status(StatusCode::BAD_REQUEST) .body(Body::from(format!( - "Could not find a server function at the route {path}. \n\nIt's likely that either\n 1. The API prefix you specify in the `#[server]` macro doesn't match the prefix at which your server function handler is mounted, or \n2. You are on a platform that doesn't support automatic server function registration and you need to call ServerFn::register_explicit() on the server function type, somewhere in your `main` function.", + "Could not find a server function at the route {path}. \ + \n\nIt's likely that either\n 1. The API prefix you \ + specify in the `#[server]` macro doesn't match the \ + prefix at which your server function handler is mounted, \ + or \n2. You are on a platform that doesn't support \ + automatic server function registration and you need to \ + call ServerFn::register_explicit() on the server \ + function type, somewhere in your `main` function.", ))) .unwrap() } @@ -252,25 +279,32 @@ pub mod axum { // Actix integration #[cfg(feature = "actix")] pub mod actix { + use crate::{ + request::actix::ActixRequest, response::actix::ActixResponse, + LazyServerFnMap, ServerFn, ServerFnTraitObj, + }; use actix_web::{HttpRequest, HttpResponse}; use send_wrapper::SendWrapper; - use crate::request::actix::ActixRequest; - use crate::response::actix::ActixResponse; - use crate::{LazyServerFnMap, ServerFn, ServerFnTraitObj}; - inventory::collect!(ServerFnTraitObj<ActixRequest, ActixResponse>); - static REGISTERED_SERVER_FUNCTIONS: LazyServerFnMap<ActixRequest, ActixResponse> = - initialize_server_fn_map!(ActixRequest, ActixResponse); + static REGISTERED_SERVER_FUNCTIONS: LazyServerFnMap< + ActixRequest, + ActixResponse, + > = initialize_server_fn_map!(ActixRequest, ActixResponse); pub fn register_explicit<T>() where - T: ServerFn<ServerRequest = ActixRequest, ServerResponse = ActixResponse> + 'static, + T: ServerFn< + ServerRequest = ActixRequest, + ServerResponse = ActixResponse, + > + 'static, { REGISTERED_SERVER_FUNCTIONS.insert( T::PATH, - ServerFnTraitObj::new(T::PATH, |req| Box::pin(T::run_on_server(req))), + ServerFnTraitObj::new(T::PATH, |req| { + Box::pin(T::run_on_server(req)) + }), ); } @@ -284,7 +318,14 @@ pub mod actix { .take() } else { HttpResponse::BadRequest().body(format!( - "Could not find a server function at the route {path}. \n\nIt's likely that either\n 1. The API prefix you specify in the `#[server]` macro doesn't match the prefix at which your server function handler is mounted, or \n2. You are on a platform that doesn't support automatic server function registration and you need to call ServerFn::register_explicit() on the server function type, somewhere in your `main` function.", + "Could not find a server function at the route {path}. \ + \n\nIt's likely that either\n 1. The API prefix you specify \ + in the `#[server]` macro doesn't match the prefix at which \ + your server function handler is mounted, or \n2. You are on \ + a platform that doesn't support automatic server function \ + registration and you need to call \ + ServerFn::register_explicit() on the server function type, \ + somewhere in your `main` function.", )) } } From 8fae76828e1dc1e3ccc40b9471879720db3f05c4 Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Tue, 2 Jan 2024 22:03:54 -0500 Subject: [PATCH 007/100] `FromStr`-based lightweight `ServerFnError` deserialization --- server_fn/src/error.rs | 88 ++++++++++++++++++++++++++++++++++++++++-- server_fn/src/lib.rs | 26 ++++--------- 2 files changed, 92 insertions(+), 22 deletions(-) diff --git a/server_fn/src/error.rs b/server_fn/src/error.rs index e517c52c74..02fe783ff4 100644 --- a/server_fn/src/error.rs +++ b/server_fn/src/error.rs @@ -1,6 +1,6 @@ -use core::fmt::Display; +use core::fmt::{self, Display}; use serde::{Deserialize, Serialize}; -use std::{error, fmt, ops, sync::Arc}; +use std::{error, fmt, fmt::Write, ops, str::FromStr, sync::Arc}; use thiserror::Error; /// This is a result type into which any error can be converted, @@ -53,7 +53,7 @@ impl From<ServerFnError> for Error { /// An empty value indicating that there is no custom error type associated /// with this server function. #[derive(Debug, Deserialize, Serialize)] -pub struct NoCustomError(()); +pub struct NoCustomError; // Implement `Display` for `NoCustomError` impl fmt::Display for NoCustomError { @@ -62,6 +62,14 @@ impl fmt::Display for NoCustomError { } } +impl FromStr for NoCustomError { + type Err = (); + + fn from_str(s: &str) -> Result<Self, Self::Err> { + Ok(NoCustomError) + } +} + #[derive(Debug)] pub struct WrapError<T>(pub T); @@ -204,6 +212,80 @@ where ) } } + +pub trait ServerFnErrorSerde: Sized { + fn ser(&self) -> String; + + fn de(data: &str) -> Self; +} + +impl<CustErr> ServerFnErrorSerde for ServerFnError<CustErr> +where + CustErr: FromStr + Display, +{ + fn ser(&self) -> String { + let mut buf = String::new(); + match self { + ServerFnError::WrappedServerError(e) => { + write!(&mut buf, "WrappedServerFn|{}", e) + } + ServerFnError::Registration(e) => { + write!(&mut buf, "Registration|{}", e) + } + ServerFnError::Request(e) => write!(&mut buf, "Request|{}", e), + ServerFnError::Response(e) => write!(&mut buf, "Response|{}", e), + ServerFnError::ServerError(e) => { + write!(&mut buf, "ServerError|{}", e) + } + ServerFnError::Deserialization(e) => { + write!(&mut buf, "Deserialization|{}", e) + } + ServerFnError::Serialization(e) => { + write!(&mut buf, "Serialization|{}", e) + } + ServerFnError::Args(e) => write!(&mut buf, "Args|{}", e), + ServerFnError::MissingArg(e) => { + write!(&mut buf, "MissingArg|{}", e) + } + }; + buf + } + + fn de(data: &str) -> Self { + data.split_once('|') + .and_then(|(ty, data)| match ty { + "WrappedServerFn" => match CustErr::from_str(data) { + Ok(d) => Some(ServerFnError::WrappedServerError(d)), + Err(_) => None, + }, + "Registration" => { + Some(ServerFnError::Registration(data.to_string())) + } + "Request" => Some(ServerFnError::Request(data.to_string())), + "Response" => Some(ServerFnError::Response(data.to_string())), + "ServerError" => { + Some(ServerFnError::ServerError(data.to_string())) + } + "Deserialization" => { + Some(ServerFnError::Deserialization(data.to_string())) + } + "Serialization" => { + Some(ServerFnError::Serialization(data.to_string())) + } + "Args" => Some(ServerFnError::Args(data.to_string())), + "MissingArg" => { + Some(ServerFnError::MissingArg(data.to_string())) + } + _ => None, + }) + .unwrap_or_else(|| { + ServerFnError::Deserialization(format!( + "Could not deserialize error {data:?}" + )) + }) + } +} + impl<E> std::error::Error for ServerFnError<E> where E: std::error::Error + 'static, diff --git a/server_fn/src/lib.rs b/server_fn/src/lib.rs index 0b8eab6d08..5fc1d1659a 100644 --- a/server_fn/src/lib.rs +++ b/server_fn/src/lib.rs @@ -14,14 +14,14 @@ use codec::{Encoding, FromReq, FromRes, IntoReq, IntoRes}; pub use const_format; use dashmap::DashMap; pub use error::ServerFnError; +use error::ServerFnErrorSerde; use middleware::{Layer, Service}; use once_cell::sync::Lazy; use request::Req; use response::{ClientRes, Res}; #[doc(hidden)] pub use serde; -use serde::{de::DeserializeOwned, Serialize}; -use std::{future::Future, pin::Pin, sync::Arc}; +use std::{fmt::Display, future::Future, pin::Pin, str::FromStr, sync::Arc}; #[doc(hidden)] pub use xxhash_rust; @@ -67,7 +67,7 @@ where /// 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: Serialize + DeserializeOwned; + type Error: ServerFnErrorSerde; /// Middleware that should be applied to this server function. fn middlewares( @@ -103,26 +103,14 @@ where let status = res.status(); let location = res.location(); - // if it returns an error status, deserialize the error - // this is the same logic as the current implementation of server fns - // TODO I don't love that this requires shipping `serde_json` for errors + // if it returns an error status, deserialize the error using FromStr let res = if (400..=599).contains(&status) { - let status_text = res.status_text(); let text = res.try_into_string().await?; - match serde_json::from_str(&text) { - Ok(e) => Err(e), - Err(_) => { - Err(ServerFnError::ServerError(if text.is_empty() { - format!("{} {}", status, status_text) - } else { - format!("{} {}: {}", status, status_text, text) - })) - } - } + Err(Self::Error::de(&text)) } else { // otherwise, deserialize the body as is - Self::Output::from_res(res).await - }; + Ok(Self::Output::from_res(res).await) + }?; // if redirected, call the redirect hook (if that's been set) if (300..=399).contains(&status) { From 7d45e6bb13cfce8da8b2a19c83795358db47c520 Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Tue, 2 Jan 2024 22:10:59 -0500 Subject: [PATCH 008/100] clean up my mistake --- server_fn/src/error.rs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/server_fn/src/error.rs b/server_fn/src/error.rs index 02fe783ff4..dbab984483 100644 --- a/server_fn/src/error.rs +++ b/server_fn/src/error.rs @@ -1,6 +1,11 @@ -use core::fmt::{self, Display}; use serde::{Deserialize, Serialize}; -use std::{error, fmt, fmt::Write, ops, str::FromStr, sync::Arc}; +use std::{ + error, fmt, + fmt::{Display, Write}, + ops, + str::FromStr, + sync::Arc, +}; use thiserror::Error; /// This is a result type into which any error can be converted, @@ -65,7 +70,7 @@ impl fmt::Display for NoCustomError { impl FromStr for NoCustomError { type Err = (); - fn from_str(s: &str) -> Result<Self, Self::Err> { + fn from_str(_s: &str) -> Result<Self, Self::Err> { Ok(NoCustomError) } } From 6a1685936b2854a3e35460703bbed00402ddd1cf Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Wed, 3 Jan 2024 07:15:54 -0500 Subject: [PATCH 009/100] fix rkyv --- server_fn/src/codec/rkyv.rs | 43 ++++++++++++++++++++++++------------- 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/server_fn/src/codec/rkyv.rs b/server_fn/src/codec/rkyv.rs index 8fc0981d6f..02c4aae2aa 100644 --- a/server_fn/src/codec/rkyv.rs +++ b/server_fn/src/codec/rkyv.rs @@ -1,14 +1,16 @@ +use super::{Encoding, FromReq, FromRes, IntoReq, IntoRes}; +use crate::{ + error::ServerFnError, + request::{ClientReq, Req}, + response::{ClientRes, Res}, +}; +use bytes::Bytes; use rkyv::{ de::deserializers::SharedDeserializeMap, ser::serializers::AllocSerializer, - validation::validators::DefaultValidator, Archive, CheckBytes, Deserialize, Serialize, + validation::validators::DefaultValidator, Archive, CheckBytes, Deserialize, + Serialize, }; -use super::{Encoding, FromReq, FromRes, IntoReq, IntoRes}; -use crate::error::ServerFnError; -use crate::request::{ClientReq, Req}; -use crate::response::{ClientRes, Res}; -use bytes::Bytes; - /// Pass arguments and receive responses using `rkyv` in a `POST` request. pub struct Rkyv; @@ -21,10 +23,16 @@ where Request: ClientReq<CustErr>, T: Serialize<AllocSerializer<1024>> + Send, T: Archive, - T::Archived: for<'a> CheckBytes<DefaultValidator<'a>> + Deserialize<T, SharedDeserializeMap>, + T::Archived: for<'a> CheckBytes<DefaultValidator<'a>> + + Deserialize<T, SharedDeserializeMap>, { - fn into_req(self, path: &str, accepts: &str) -> Result<Request, ServerFnError<CustErr>> { - let encoded = rkyv::to_bytes::<T, 1024>(&self)?; + fn into_req( + self, + path: &str, + accepts: &str, + ) -> Result<Request, ServerFnError<CustErr>> { + let encoded = rkyv::to_bytes::<T, 1024>(&self) + .map_err(|e| ServerFnError::Serialization(e.to_string()))?; let bytes = Bytes::copy_from_slice(encoded.as_ref()); Request::try_new_post_bytes(path, accepts, Rkyv::CONTENT_TYPE, bytes) } @@ -35,11 +43,13 @@ where Request: Req<CustErr> + Send + 'static, T: Serialize<AllocSerializer<1024>> + Send, T: Archive, - T::Archived: for<'a> CheckBytes<DefaultValidator<'a>> + Deserialize<T, SharedDeserializeMap>, + T::Archived: for<'a> CheckBytes<DefaultValidator<'a>> + + Deserialize<T, SharedDeserializeMap>, { async fn from_req(req: Request) -> Result<Self, ServerFnError<CustErr>> { let body_bytes = req.try_into_bytes().await?; - rkyv::from_bytes::<T>(&body_bytes).map_err(|e| ServerFnError::Args(e.to_string())) + rkyv::from_bytes::<T>(&body_bytes) + .map_err(|e| ServerFnError::Args(e.to_string())) } } @@ -48,7 +58,8 @@ where Response: Res<CustErr>, T: Serialize<AllocSerializer<1024>> + Send, T: Archive, - T::Archived: for<'a> CheckBytes<DefaultValidator<'a>> + Deserialize<T, SharedDeserializeMap>, + T::Archived: for<'a> CheckBytes<DefaultValidator<'a>> + + Deserialize<T, SharedDeserializeMap>, { async fn into_res(self) -> Result<Response, ServerFnError<CustErr>> { let encoded = rkyv::to_bytes::<T, 1024>(&self) @@ -63,10 +74,12 @@ where Response: ClientRes<CustErr> + Send, T: Serialize<AllocSerializer<1024>> + Send, T: Archive, - T::Archived: for<'a> CheckBytes<DefaultValidator<'a>> + Deserialize<T, SharedDeserializeMap>, + T::Archived: for<'a> CheckBytes<DefaultValidator<'a>> + + Deserialize<T, SharedDeserializeMap>, { async fn from_res(res: Response) -> Result<Self, ServerFnError<CustErr>> { let data = res.try_into_bytes().await?; - rkyv::from_bytes::<T>(&data).map_err(|e| ServerFnError::Deserialization(e.to_string())) + rkyv::from_bytes::<T>(&data) + .map_err(|e| ServerFnError::Deserialization(e.to_string())) } } From 9c258219dd3ed80da811b0ec2ad4b92dbd58b17c Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Wed, 3 Jan 2024 07:22:06 -0500 Subject: [PATCH 010/100] fix Actix implementation with middleware --- server_fn/src/lib.rs | 29 +++++++++++++++++++---------- server_fn/src/request/actix.rs | 20 ++++++++++++++++---- 2 files changed, 35 insertions(+), 14 deletions(-) diff --git a/server_fn/src/lib.rs b/server_fn/src/lib.rs index 5fc1d1659a..96ade28639 100644 --- a/server_fn/src/lib.rs +++ b/server_fn/src/lib.rs @@ -268,8 +268,9 @@ pub mod axum { #[cfg(feature = "actix")] pub mod actix { use crate::{ - request::actix::ActixRequest, response::actix::ActixResponse, - LazyServerFnMap, ServerFn, ServerFnTraitObj, + middleware::BoxedService, request::actix::ActixRequest, + response::actix::ActixResponse, LazyServerFnMap, ServerFn, + ServerFnTraitObj, }; use actix_web::{HttpRequest, HttpResponse}; use send_wrapper::SendWrapper; @@ -290,20 +291,28 @@ pub mod actix { { REGISTERED_SERVER_FUNCTIONS.insert( T::PATH, - ServerFnTraitObj::new(T::PATH, |req| { - Box::pin(T::run_on_server(req)) - }), + ServerFnTraitObj::new( + T::PATH, + |req| Box::pin(T::run_on_server(req)), + T::middlewares, + ), ); } pub async fn handle_server_fn(req: HttpRequest) -> HttpResponse { let path = req.uri().path(); if let Some(server_fn) = REGISTERED_SERVER_FUNCTIONS.get(path) { - server_fn - .run(ActixRequest(SendWrapper::new(req))) - .await - .0 - .take() + let middleware = (server_fn.middleware)(); + let mut service = BoxedService::new(*server_fn); + for middleware in middleware { + service = middleware.layer(service); + } + service.0.run(ActixRequest::from(req)).await.0.take() + /*server_fn + .run(ActixRequest(SendWrapper::new(req))) + .await + .0 + .take()*/ } else { HttpResponse::BadRequest().body(format!( "Could not find a server function at the route {path}. \ diff --git a/server_fn/src/request/actix.rs b/server_fn/src/request/actix.rs index 9354793205..c8f2213d21 100644 --- a/server_fn/src/request/actix.rs +++ b/server_fn/src/request/actix.rs @@ -7,6 +7,12 @@ use std::future::Future; pub struct ActixRequest(pub(crate) SendWrapper<HttpRequest>); +impl From<HttpRequest> for ActixRequest { + fn from(value: HttpRequest) -> Self { + ActixRequest(SendWrapper::new(value)) + } +} + impl<CustErr> Req<CustErr> for ActixRequest { fn as_query(&self) -> Option<&str> { self.0.uri().query() @@ -19,7 +25,10 @@ impl<CustErr> Req<CustErr> for ActixRequest { .map(|h| String::from_utf8_lossy(h.as_bytes()).to_string()) } - fn try_into_bytes(self) -> impl Future<Output = Result<Bytes, ServerFnError<CustErr>>> + Send { + fn try_into_bytes( + self, + ) -> impl Future<Output = Result<Bytes, ServerFnError<CustErr>>> + 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 { @@ -31,7 +40,8 @@ impl<CustErr> Req<CustErr> for ActixRequest { fn try_into_string( self, - ) -> impl Future<Output = Result<String, ServerFnError<CustErr>>> + Send { + ) -> impl Future<Output = Result<String, ServerFnError<CustErr>>> + 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 { @@ -43,8 +53,10 @@ impl<CustErr> Req<CustErr> for ActixRequest { fn try_into_stream( self, - ) -> Result<impl Stream<Item = Result<Bytes, ServerFnError>> + Send, ServerFnError<CustErr>> - { + ) -> Result< + impl Stream<Item = Result<Bytes, ServerFnError>> + Send, + ServerFnError<CustErr>, + > { Ok(futures::stream::once(async { todo!() })) } } From dd368a845cc6341a19cfe3feb80ac68afa1fd505 Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Wed, 3 Jan 2024 07:31:03 -0500 Subject: [PATCH 011/100] @ealmloff changes to reexport actix/axum --- server_fn/src/lib.rs | 7 ++++++- server_fn_macro/src/lib.rs | 8 ++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/server_fn/src/lib.rs b/server_fn/src/lib.rs index 96ade28639..57056ebcfa 100644 --- a/server_fn/src/lib.rs +++ b/server_fn/src/lib.rs @@ -7,9 +7,14 @@ pub mod redirect; pub mod request; pub mod response; +#[cfg(feature = "actix")] +#[doc(hidden)] +pub use ::actix_web as actix_export; +#[cfg(feature = "axum")] +#[doc(hidden)] +pub use ::axum as axum_export; use client::Client; use codec::{Encoding, FromReq, FromRes, IntoReq, IntoRes}; -// reexports for the sake of the macro #[doc(hidden)] pub use const_format; use dashmap::DashMap; diff --git a/server_fn_macro/src/lib.rs b/server_fn_macro/src/lib.rs index c4a1201bdc..60ef83c03b 100644 --- a/server_fn_macro/src/lib.rs +++ b/server_fn_macro/src/lib.rs @@ -319,11 +319,11 @@ pub fn server_macro_impl( } } else if cfg!(feature = "axum") { quote! { - ::axum::http::Request<::axum::body::Body> + #server_fn_path::axum_export::http::Request<#server_fn_path::axum_export::body::Body> } } else if cfg!(feature = "actix") { quote! { - ::actix_web::HttpRequest + #server_fn_path::actix_export::HttpRequest } } else { return Err(syn::Error::new( @@ -338,11 +338,11 @@ pub fn server_macro_impl( } } else if cfg!(feature = "axum") { quote! { - ::axum::http::Response<::axum::body::Body> + #server_fn_path::axum_export::http::Response<#server_fn_path::axum_export::body::Body> } } else if cfg!(feature = "actix") { quote! { - ::actix_web::HttpResponse + #server_fn_path::actix_export::HttpResponse } } else { return Err(syn::Error::new( From 2dbc5899f3f9da76c105778faad693d9b1f57231 Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Wed, 3 Jan 2024 07:31:13 -0500 Subject: [PATCH 012/100] cargo fmt --- leptos_server/src/lib.rs | 4 +-- leptos_server/src/multi_action.rs | 2 +- server_fn/src/client.rs | 9 ++++-- server_fn/src/codec/cbor.rs | 27 ++++++++++++------ server_fn/src/codec/json.rs | 25 +++++++++++------ server_fn/src/codec/mod.rs | 19 +++++++++---- server_fn/src/codec/multipart.rs | 27 ++++++++++++------ server_fn/src/codec/stream.rs | 25 +++++++++++------ server_fn/src/codec/url.rs | 29 ++++++++++++------- server_fn/src/middleware/mod.rs | 46 ++++++++++++++++++++++--------- server_fn/src/redirect.rs | 3 +- server_fn/src/request/axum.rs | 13 +++++---- server_fn/src/request/browser.rs | 3 +- server_fn/src/request/mod.rs | 28 +++++++++++++------ server_fn/src/request/reqwest.rs | 10 +++---- server_fn/src/response/actix.rs | 18 +++++++++--- server_fn/src/response/browser.rs | 11 +++++--- server_fn/src/response/http.rs | 17 +++++++++--- server_fn/src/response/mod.rs | 33 ++++++++++++++++------ 19 files changed, 237 insertions(+), 112 deletions(-) diff --git a/leptos_server/src/lib.rs b/leptos_server/src/lib.rs index 8d925fedae..6055e13936 100644 --- a/leptos_server/src/lib.rs +++ b/leptos_server/src/lib.rs @@ -113,9 +113,7 @@ ////! CBOR forms encounter the same issue as `PUT`, `DELETE`, or JSON: they do not degrade gracefully if the WASM version of ////! your app is not available. -pub use server_fn::{ - error::ServerFnErrorErr, ServerFnError, -}; +pub use server_fn::{error::ServerFnErrorErr, ServerFnError}; mod action; mod multi_action; diff --git a/leptos_server/src/multi_action.rs b/leptos_server/src/multi_action.rs index 18d480344d..bd9a7fa72c 100644 --- a/leptos_server/src/multi_action.rs +++ b/leptos_server/src/multi_action.rs @@ -1,8 +1,8 @@ -use server_fn::{ServerFn, ServerFnError}; use leptos_reactive::{ create_rw_signal, is_suppressing_resource_load, signal_prelude::*, spawn_local, store_value, untrack, ReadSignal, RwSignal, StoredValue, }; +use server_fn::{ServerFn, ServerFnError}; use std::{future::Future, pin::Pin, rc::Rc}; /// An action that synchronizes multiple imperative `async` calls to the reactive system, diff --git a/server_fn/src/client.rs b/server_fn/src/client.rs index d171131509..cd3599e63f 100644 --- a/server_fn/src/client.rs +++ b/server_fn/src/client.rs @@ -14,7 +14,8 @@ pub trait Client<CustErr> { pub mod browser { use super::Client; use crate::{ - error::ServerFnError, request::browser::BrowserRequest, response::browser::BrowserResponse, + error::ServerFnError, request::browser::BrowserRequest, + response::browser::BrowserResponse, }; use send_wrapper::SendWrapper; use std::future::Future; @@ -27,7 +28,8 @@ pub mod browser { fn send( req: Self::Request, - ) -> impl Future<Output = Result<Self::Response, ServerFnError<CustErr>>> + Send { + ) -> impl Future<Output = Result<Self::Response, ServerFnError<CustErr>>> + + Send { SendWrapper::new(async move { req.0 .take() @@ -56,7 +58,8 @@ pub mod reqwest { fn send( req: Self::Request, - ) -> impl Future<Output = Result<Self::Response, ServerFnError<CustErr>>> + Send { + ) -> impl Future<Output = Result<Self::Response, ServerFnError<CustErr>>> + + Send { CLIENT .execute(req) .map_err(|e| ServerFnError::Request(e.to_string())) diff --git a/server_fn/src/codec/cbor.rs b/server_fn/src/codec/cbor.rs index e6ca6ea323..e9acb50f56 100644 --- a/server_fn/src/codec/cbor.rs +++ b/server_fn/src/codec/cbor.rs @@ -1,10 +1,11 @@ use super::{Encoding, FromReq, FromRes, IntoReq, IntoRes}; -use crate::error::ServerFnError; -use crate::request::{ClientReq, Req}; -use crate::response::{ClientRes, Res}; +use crate::{ + error::ServerFnError, + request::{ClientReq, Req}, + response::{ClientRes, Res}, +}; use bytes::Bytes; -use serde::de::DeserializeOwned; -use serde::Serialize; +use serde::{de::DeserializeOwned, Serialize}; /// Pass arguments and receive responses using `cbor` in a `POST` request. pub struct Cbor; @@ -18,11 +19,20 @@ where Request: ClientReq<CustErr>, T: Serialize + Send, { - fn into_req(self, path: &str, accepts: &str) -> Result<Request, ServerFnError<CustErr>> { + fn into_req( + self, + path: &str, + accepts: &str, + ) -> Result<Request, ServerFnError<CustErr>> { let mut buffer: Vec<u8> = Vec::new(); ciborium::ser::into_writer(&self, &mut buffer) .map_err(|e| ServerFnError::Serialization(e.to_string()))?; - Request::try_new_post_bytes(path, accepts, Cbor::CONTENT_TYPE, Bytes::from(buffer)) + Request::try_new_post_bytes( + path, + accepts, + Cbor::CONTENT_TYPE, + Bytes::from(buffer), + ) } } @@ -58,7 +68,8 @@ where { async fn from_res(res: Response) -> Result<Self, ServerFnError<CustErr>> { let data = res.try_into_bytes().await?; - ciborium::de::from_reader(data.as_ref()).map_err(|e| ServerFnError::Args(e.to_string())) + ciborium::de::from_reader(data.as_ref()) + .map_err(|e| ServerFnError::Args(e.to_string())) } } diff --git a/server_fn/src/codec/json.rs b/server_fn/src/codec/json.rs index fc944453e9..f90f420a6d 100644 --- a/server_fn/src/codec/json.rs +++ b/server_fn/src/codec/json.rs @@ -1,10 +1,11 @@ use super::{Encoding, FromReq, FromRes}; -use crate::error::ServerFnError; -use crate::request::{ClientReq, Req}; -use crate::response::{ClientRes, Res}; -use crate::{IntoReq, IntoRes}; -use serde::de::DeserializeOwned; -use serde::Serialize; +use crate::{ + error::ServerFnError, + request::{ClientReq, Req}, + response::{ClientRes, Res}, + IntoReq, IntoRes, +}; +use serde::{de::DeserializeOwned, Serialize}; /// Pass arguments and receive responses as JSON in the body of a `POST` request. pub struct Json; @@ -17,7 +18,11 @@ where Request: ClientReq<CustErr>, T: Serialize + Send, { - fn into_req(self, path: &str, accepts: &str) -> Result<Request, ServerFnError<CustErr>> { + fn into_req( + self, + path: &str, + accepts: &str, + ) -> Result<Request, ServerFnError<CustErr>> { let data = serde_json::to_string(&self) .map_err(|e| ServerFnError::Serialization(e.to_string()))?; Request::try_new_post(path, accepts, Json::CONTENT_TYPE, data) @@ -31,7 +36,8 @@ where { async fn from_req(req: Request) -> Result<Self, ServerFnError<CustErr>> { let string_data = req.try_into_string().await?; - serde_json::from_str::<Self>(&string_data).map_err(|e| ServerFnError::Args(e.to_string())) + serde_json::from_str::<Self>(&string_data) + .map_err(|e| ServerFnError::Args(e.to_string())) } } @@ -54,6 +60,7 @@ where { async fn from_res(res: Response) -> Result<Self, ServerFnError<CustErr>> { 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| ServerFnError::Deserialization(e.to_string())) } } diff --git a/server_fn/src/codec/mod.rs b/server_fn/src/codec/mod.rs index 7883ec3321..56ab0bf70d 100644 --- a/server_fn/src/codec/mod.rs +++ b/server_fn/src/codec/mod.rs @@ -29,23 +29,32 @@ pub trait FromReq<CustErr, Request, Encoding> where Self: Sized, { - fn from_req(req: Request) -> impl Future<Output = Result<Self, ServerFnError<CustErr>>> + Send; + fn from_req( + req: Request, + ) -> impl Future<Output = Result<Self, ServerFnError<CustErr>>> + Send; } pub trait IntoReq<CustErr, Request, Encoding> { - fn into_req(self, path: &str, accepts: &str) -> Result<Request, ServerFnError<CustErr>>; + fn into_req( + self, + path: &str, + accepts: &str, + ) -> Result<Request, ServerFnError<CustErr>>; } pub trait FromRes<CustErr, Response, Encoding> where Self: Sized, { - fn from_res(res: Response) - -> impl Future<Output = Result<Self, ServerFnError<CustErr>>> + Send; + fn from_res( + res: Response, + ) -> impl Future<Output = Result<Self, ServerFnError<CustErr>>> + Send; } pub trait IntoRes<CustErr, Response, Encoding> { - fn into_res(self) -> impl Future<Output = Result<Response, ServerFnError<CustErr>>> + Send; + fn into_res( + self, + ) -> impl Future<Output = Result<Response, ServerFnError<CustErr>>> + Send; } pub trait Encoding { diff --git a/server_fn/src/codec/multipart.rs b/server_fn/src/codec/multipart.rs index 987902184d..aa11fcd6f6 100644 --- a/server_fn/src/codec/multipart.rs +++ b/server_fn/src/codec/multipart.rs @@ -1,8 +1,9 @@ use super::{Encoding, FromReq}; -use crate::error::ServerFnError; -use crate::request::browser::BrowserFormData; -use crate::request::{ClientReq, Req}; -use crate::IntoReq; +use crate::{ + error::ServerFnError, + request::{browser::BrowserFormData, ClientReq, Req}, + IntoReq, +}; use futures::StreamExt; use multer::Multipart; use web_sys::FormData; @@ -46,9 +47,17 @@ where Request: ClientReq<CustErr, FormData = BrowserFormData>, T: Into<MultipartData>, { - fn into_req(self, path: &str, accepts: &str) -> Result<Request, ServerFnError<CustErr>> { + fn into_req( + self, + path: &str, + accepts: &str, + ) -> Result<Request, ServerFnError<CustErr>> { let multi = self.into(); - Request::try_new_multipart(path, accepts, multi.into_client_data().unwrap()) + Request::try_new_multipart( + path, + accepts, + multi.into_client_data().unwrap(), + ) } } @@ -64,8 +73,10 @@ where .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())), boundary); + let data = multer::Multipart::new( + stream.map(|data| data.map_err(|e| e.to_string())), + boundary, + ); Ok(MultipartData::Server(data).into()) } } diff --git a/server_fn/src/codec/stream.rs b/server_fn/src/codec/stream.rs index 7999a8caf0..8158261ac4 100644 --- a/server_fn/src/codec/stream.rs +++ b/server_fn/src/codec/stream.rs @@ -1,11 +1,12 @@ -use std::pin::Pin; - use super::{Encoding, FromRes}; -use crate::error::{NoCustomError, ServerFnError}; -use crate::response::{ClientRes, Res}; -use crate::IntoRes; +use crate::{ + error::{NoCustomError, ServerFnError}, + response::{ClientRes, Res}, + IntoRes, +}; use bytes::Bytes; use futures::{Stream, StreamExt}; +use std::pin::Pin; pub struct Streaming; @@ -38,7 +39,9 @@ pub struct ByteStream<CustErr = NoCustomError>( ); impl<CustErr> ByteStream<CustErr> { - pub fn into_inner(self) -> impl Stream<Item = Result<Bytes, ServerFnError<CustErr>>> + Send { + pub fn into_inner( + self, + ) -> impl Stream<Item = Result<Bytes, ServerFnError<CustErr>>> + Send { self.0 } } @@ -53,7 +56,8 @@ where } } -impl<CustErr, Response> IntoRes<CustErr, Response, Streaming> for ByteStream<CustErr> +impl<CustErr, Response> IntoRes<CustErr, Response, Streaming> + for ByteStream<CustErr> where Response: Res<CustErr>, CustErr: 'static, @@ -84,7 +88,9 @@ pub struct TextStream<CustErr = NoCustomError>( ); impl<CustErr> TextStream<CustErr> { - pub fn into_inner(self) -> impl Stream<Item = Result<String, ServerFnError<CustErr>>> + Send { + pub fn into_inner( + self, + ) -> impl Stream<Item = Result<String, ServerFnError<CustErr>>> + Send { self.0 } } @@ -99,7 +105,8 @@ where } } -impl<CustErr, Response> IntoRes<CustErr, Response, StreamingText> for TextStream<CustErr> +impl<CustErr, Response> IntoRes<CustErr, Response, StreamingText> + for TextStream<CustErr> where Response: Res<CustErr>, CustErr: 'static, diff --git a/server_fn/src/codec/url.rs b/server_fn/src/codec/url.rs index fb298d2805..e133066e28 100644 --- a/server_fn/src/codec/url.rs +++ b/server_fn/src/codec/url.rs @@ -1,8 +1,9 @@ use super::{Encoding, FromReq, IntoReq}; -use crate::error::ServerFnError; -use crate::request::{ClientReq, Req}; -use serde::de::DeserializeOwned; -use serde::Serialize; +use crate::{ + error::ServerFnError, + request::{ClientReq, Req}, +}; +use serde::{de::DeserializeOwned, Serialize}; /// Pass arguments as a URL-encoded query string of a `GET` request. pub struct GetUrl; @@ -19,9 +20,13 @@ where Request: ClientReq<CustErr>, T: Serialize + Send, { - fn into_req(self, path: &str, accepts: &str) -> Result<Request, ServerFnError<CustErr>> { - let data = - serde_qs::to_string(&self).map_err(|e| ServerFnError::Serialization(e.to_string()))?; + fn into_req( + self, + path: &str, + accepts: &str, + ) -> Result<Request, ServerFnError<CustErr>> { + let data = serde_qs::to_string(&self) + .map_err(|e| ServerFnError::Serialization(e.to_string()))?; Request::try_new_get(path, accepts, GetUrl::CONTENT_TYPE, &data) } } @@ -48,9 +53,13 @@ where Request: ClientReq<CustErr>, T: Serialize + Send, { - fn into_req(self, path: &str, accepts: &str) -> Result<Request, ServerFnError<CustErr>> { - let qs = - serde_qs::to_string(&self).map_err(|e| ServerFnError::Serialization(e.to_string()))?; + fn into_req( + self, + path: &str, + accepts: &str, + ) -> Result<Request, ServerFnError<CustErr>> { + let qs = serde_qs::to_string(&self) + .map_err(|e| ServerFnError::Serialization(e.to_string()))?; Request::try_new_post(path, accepts, PostUrl::CONTENT_TYPE, qs) } } diff --git a/server_fn/src/middleware/mod.rs b/server_fn/src/middleware/mod.rs index 1f16ec299a..7c28720e7d 100644 --- a/server_fn/src/middleware/mod.rs +++ b/server_fn/src/middleware/mod.rs @@ -13,19 +13,23 @@ impl<Req, Res> BoxedService<Req, Res> { } pub trait Service<Request, Response> { - fn run(&mut self, req: Request) -> Pin<Box<dyn Future<Output = Response> + Send>>; + fn run( + &mut self, + req: Request, + ) -> Pin<Box<dyn Future<Output = Response> + Send>>; } #[cfg(feature = "axum")] mod axum { + use super::{BoxedService, Service}; use crate::{response::Res, ServerFnError}; use axum::body::Body; use http::{Request, Response}; - use std::fmt::{Debug, Display}; - use std::future::Future; - use std::pin::Pin; - - use super::{BoxedService, Service}; + use std::{ + fmt::{Debug, Display}, + future::Future, + pin::Pin, + }; impl<S> super::Service<Request<Body>, Response<Body>> for S where @@ -47,11 +51,18 @@ mod axum { } } - impl tower::Service<Request<Body>> for BoxedService<Request<Body>, Response<Body>> { + impl tower::Service<Request<Body>> + for BoxedService<Request<Body>, Response<Body>> + { type Response = Response<Body>; type Error = ServerFnError; - type Future = - Pin<Box<dyn std::future::Future<Output = Result<Self::Response, Self::Error>> + Send>>; + type Future = Pin< + Box< + dyn std::future::Future< + Output = Result<Self::Response, Self::Error>, + > + Send, + >, + >; fn poll_ready( &mut self, @@ -68,7 +79,10 @@ mod axum { impl<L> super::Layer<Request<Body>, Response<Body>> for L where - L: tower_layer::Layer<BoxedService<Request<Body>, Response<Body>>> + Sync + Send + 'static, + L: tower_layer::Layer<BoxedService<Request<Body>, Response<Body>>> + + Sync + + Send + + 'static, L::Service: Service<Request<Body>, Response<Body>> + Send + 'static, { fn layer( @@ -87,8 +101,11 @@ mod actix { ServerFnError, }; use actix_web::{HttpRequest, HttpResponse}; - use std::fmt::{Debug, Display}; - use std::{future::Future, pin::Pin}; + use std::{ + fmt::{Debug, Display}, + future::Future, + pin::Pin, + }; impl<S> super::Service<HttpRequest, HttpResponse> for S where @@ -96,7 +113,10 @@ mod actix { S::Future: Send + 'static, S::Error: Into<ServerFnError> + Debug + Display + 'static, { - fn run(&mut self, req: HttpRequest) -> Pin<Box<dyn Future<Output = HttpResponse> + Send>> { + fn run( + &mut self, + req: HttpRequest, + ) -> Pin<Box<dyn Future<Output = HttpResponse> + Send>> { let inner = self.call(req); Box::pin(async move { inner.await.unwrap_or_else(|e| { diff --git a/server_fn/src/redirect.rs b/server_fn/src/redirect.rs index 42fc805241..67a9cfec62 100644 --- a/server_fn/src/redirect.rs +++ b/server_fn/src/redirect.rs @@ -1,6 +1,7 @@ use std::sync::OnceLock; -static REDIRECT_HOOK: OnceLock<Box<dyn Fn(&str) + Send + Sync>> = OnceLock::new(); +static REDIRECT_HOOK: OnceLock<Box<dyn Fn(&str) + Send + Sync>> = + OnceLock::new(); pub fn set_redirect_hook(hook: impl Fn(&str) + Send + Sync + 'static) { REDIRECT_HOOK.set(Box::new(hook)); diff --git a/server_fn/src/request/axum.rs b/server_fn/src/request/axum.rs index 457fe650f2..41f3230627 100644 --- a/server_fn/src/request/axum.rs +++ b/server_fn/src/request/axum.rs @@ -33,11 +33,12 @@ impl<CustErr> Req<CustErr> for Request<Body> { fn try_into_stream( self, - ) -> Result<impl Stream<Item = Result<Bytes, ServerFnError>> + Send, ServerFnError<CustErr>> - { - Ok(self - .into_body() - .into_data_stream() - .map(|chunk| chunk.map_err(|e| ServerFnError::Deserialization(e.to_string())))) + ) -> Result< + impl Stream<Item = Result<Bytes, ServerFnError>> + Send, + ServerFnError<CustErr>, + > { + Ok(self.into_body().into_data_stream().map(|chunk| { + chunk.map_err(|e| ServerFnError::Deserialization(e.to_string())) + })) } } diff --git a/server_fn/src/request/browser.rs b/server_fn/src/request/browser.rs index f985f1ef41..228feee1d8 100644 --- a/server_fn/src/request/browser.rs +++ b/server_fn/src/request/browser.rs @@ -1,6 +1,5 @@ -use crate::error::ServerFnError; - use super::ClientReq; +use crate::error::ServerFnError; use bytes::Bytes; pub use gloo_net::http::Request; use js_sys::Uint8Array; diff --git a/server_fn/src/request/mod.rs b/server_fn/src/request/mod.rs index ee5a3f60df..500b853efe 100644 --- a/server_fn/src/request/mod.rs +++ b/server_fn/src/request/mod.rs @@ -59,16 +59,22 @@ where fn to_content_type(&self) -> Option<String>; /// Attempts to extract the body of the request into [`Bytes`]. - fn try_into_bytes(self) -> impl Future<Output = Result<Bytes, ServerFnError<CustErr>>> + Send; + fn try_into_bytes( + self, + ) -> impl Future<Output = Result<Bytes, ServerFnError<CustErr>>> + Send; /// Attempts to convert the body of the request into a string. - fn try_into_string(self) - -> impl Future<Output = Result<String, ServerFnError<CustErr>>> + Send; + fn try_into_string( + self, + ) -> impl Future<Output = Result<String, ServerFnError<CustErr>>> + Send; /// Attempts to convert the body of the request into a string. fn try_into_stream( self, - ) -> Result<impl Stream<Item = Result<Bytes, ServerFnError>> + Send, ServerFnError<CustErr>>; + ) -> Result< + impl Stream<Item = Result<Bytes, ServerFnError>> + Send, + ServerFnError<CustErr>, + >; } /// A mocked request type that can be used in place of the actual server request, @@ -84,20 +90,26 @@ impl<CustErr> Req<CustErr> for BrowserMockReq { unreachable!() } - fn try_into_bytes(self) -> impl Future<Output = Result<Bytes, ServerFnError<CustErr>>> + Send { + fn try_into_bytes( + self, + ) -> impl Future<Output = Result<Bytes, ServerFnError<CustErr>>> + Send + { async { unreachable!() } } fn try_into_string( self, - ) -> impl Future<Output = Result<String, ServerFnError<CustErr>>> + Send { + ) -> impl Future<Output = Result<String, ServerFnError<CustErr>>> + Send + { async { unreachable!() } } fn try_into_stream( self, - ) -> Result<impl Stream<Item = Result<Bytes, ServerFnError>> + Send, ServerFnError<CustErr>> - { + ) -> Result< + impl Stream<Item = Result<Bytes, ServerFnError>> + Send, + ServerFnError<CustErr>, + > { Ok(futures::stream::once(async { unreachable!() })) } } diff --git a/server_fn/src/request/reqwest.rs b/server_fn/src/request/reqwest.rs index b03a26db4b..c705f966ee 100644 --- a/server_fn/src/request/reqwest.rs +++ b/server_fn/src/request/reqwest.rs @@ -1,12 +1,10 @@ -use std::sync::OnceLock; - -use crate::error::ServerFnError; - use super::ClientReq; +use crate::error::ServerFnError; use bytes::Bytes; use once_cell::sync::Lazy; use reqwest::header::{ACCEPT, CONTENT_TYPE}; pub use reqwest::{multipart::Form, Client, Method, Request, Url}; +use std::sync::OnceLock; pub(crate) static CLIENT: Lazy<Client> = Lazy::new(Client::new); static ROOT_URL: OnceLock<&'static str> = OnceLock::new(); @@ -34,8 +32,8 @@ impl<CustErr> ClientReq<CustErr> for Request { query: &str, ) -> Result<Self, ServerFnError<CustErr>> { 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| ServerFnError::Request(e.to_string()))?; url.set_query(Some(query)); let req = CLIENT .get(url) diff --git a/server_fn/src/response/actix.rs b/server_fn/src/response/actix.rs index 9472113cec..97581999d7 100644 --- a/server_fn/src/response/actix.rs +++ b/server_fn/src/response/actix.rs @@ -1,6 +1,9 @@ use super::Res; use crate::error::ServerFnError; -use actix_web::{http::header, http::StatusCode, HttpResponse}; +use actix_web::{ + http::{header, StatusCode}, + HttpResponse, +}; use bytes::Bytes; use futures::Stream; use send_wrapper::SendWrapper; @@ -18,7 +21,10 @@ impl<CustErr> Res<CustErr> for ActixResponse where CustErr: Display, { - fn try_from_string(content_type: &str, data: String) -> Result<Self, ServerFnError<CustErr>> { + fn try_from_string( + content_type: &str, + data: String, + ) -> Result<Self, ServerFnError<CustErr>> { let mut builder = HttpResponse::build(StatusCode::OK); Ok(ActixResponse(SendWrapper::new( builder @@ -27,7 +33,10 @@ where ))) } - fn try_from_bytes(content_type: &str, data: Bytes) -> Result<Self, ServerFnError<CustErr>> { + fn try_from_bytes( + content_type: &str, + data: Bytes, + ) -> Result<Self, ServerFnError<CustErr>> { let mut builder = HttpResponse::build(StatusCode::OK); Ok(ActixResponse(SendWrapper::new( builder @@ -38,7 +47,8 @@ where fn error_response(err: ServerFnError<CustErr>) -> Self { ActixResponse(SendWrapper::new( - HttpResponse::build(StatusCode::INTERNAL_SERVER_ERROR).body(err.to_string()), + HttpResponse::build(StatusCode::INTERNAL_SERVER_ERROR) + .body(err.to_string()), )) } diff --git a/server_fn/src/response/browser.rs b/server_fn/src/response/browser.rs index 8132ae8813..d1d5ebfdc0 100644 --- a/server_fn/src/response/browser.rs +++ b/server_fn/src/response/browser.rs @@ -1,6 +1,5 @@ -use crate::error::ServerFnError; - use super::ClientRes; +use crate::error::ServerFnError; use bytes::Bytes; use futures::{Stream, StreamExt}; pub use gloo_net::http::Response; @@ -14,7 +13,8 @@ pub struct BrowserResponse(pub(crate) SendWrapper<Response>); impl<CustErr> ClientRes<CustErr> for BrowserResponse { fn try_into_string( self, - ) -> impl Future<Output = Result<String, ServerFnError<CustErr>>> + Send { + ) -> impl Future<Output = Result<String, ServerFnError<CustErr>>> + 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 { @@ -25,7 +25,10 @@ impl<CustErr> ClientRes<CustErr> for BrowserResponse { }) } - fn try_into_bytes(self) -> impl Future<Output = Result<Bytes, ServerFnError<CustErr>>> + Send { + fn try_into_bytes( + self, + ) -> impl Future<Output = Result<Bytes, ServerFnError<CustErr>>> + 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 { diff --git a/server_fn/src/response/http.rs b/server_fn/src/response/http.rs index 1731f3715e..09844e72b4 100644 --- a/server_fn/src/response/http.rs +++ b/server_fn/src/response/http.rs @@ -10,7 +10,10 @@ impl<CustErr> Res<CustErr> for Response<Body> where CustErr: Send + Sync + Debug + Display + 'static, { - fn try_from_string(content_type: &str, data: String) -> Result<Self, ServerFnError<CustErr>> { + fn try_from_string( + content_type: &str, + data: String, + ) -> Result<Self, ServerFnError<CustErr>> { let builder = http::Response::builder(); builder .status(200) @@ -19,7 +22,10 @@ where .map_err(|e| ServerFnError::Response(e.to_string())) } - fn try_from_bytes(content_type: &str, data: Bytes) -> Result<Self, ServerFnError<CustErr>> { + fn try_from_bytes( + content_type: &str, + data: Bytes, + ) -> Result<Self, ServerFnError<CustErr>> { let builder = http::Response::builder(); builder .status(200) @@ -30,9 +36,12 @@ where fn try_from_stream( content_type: &str, - data: impl Stream<Item = Result<Bytes, ServerFnError<CustErr>>> + Send + 'static, + data: impl Stream<Item = Result<Bytes, ServerFnError<CustErr>>> + + Send + + 'static, ) -> Result<Self, ServerFnError<CustErr>> { - let body = Body::from_stream(data.map(|n| n.map_err(ServerFnErrorErr::from))); + let body = + Body::from_stream(data.map(|n| n.map_err(ServerFnErrorErr::from))); let builder = http::Response::builder(); builder .status(200) diff --git a/server_fn/src/response/mod.rs b/server_fn/src/response/mod.rs index d58c94feea..e49f58e549 100644 --- a/server_fn/src/response/mod.rs +++ b/server_fn/src/response/mod.rs @@ -18,15 +18,23 @@ where Self: Sized, { /// Attempts to convert a UTF-8 string into an HTTP response. - fn try_from_string(content_type: &str, data: String) -> Result<Self, ServerFnError<CustErr>>; + fn try_from_string( + content_type: &str, + data: String, + ) -> Result<Self, ServerFnError<CustErr>>; /// Attempts to convert a binary blob represented as bytes into an HTTP response. - fn try_from_bytes(content_type: &str, data: Bytes) -> Result<Self, ServerFnError<CustErr>>; + fn try_from_bytes( + content_type: &str, + data: Bytes, + ) -> Result<Self, ServerFnError<CustErr>>; /// Attempts to convert a stream of bytes into an HTTP response. fn try_from_stream( content_type: &str, - data: impl Stream<Item = Result<Bytes, ServerFnError<CustErr>>> + Send + 'static, + data: impl Stream<Item = Result<Bytes, ServerFnError<CustErr>>> + + Send + + 'static, ) -> Result<Self, ServerFnError<CustErr>>; fn error_response(err: ServerFnError<CustErr>) -> Self; @@ -35,11 +43,14 @@ where /// Represents the response as received by the client. pub trait ClientRes<CustErr> { /// Attempts to extract a UTF-8 string from an HTTP response. - fn try_into_string(self) - -> impl Future<Output = Result<String, ServerFnError<CustErr>>> + Send; + fn try_into_string( + self, + ) -> impl Future<Output = Result<String, ServerFnError<CustErr>>> + Send; /// Attempts to extract a binary blob from an HTTP response. - fn try_into_bytes(self) -> impl Future<Output = Result<Bytes, ServerFnError<CustErr>>> + Send; + fn try_into_bytes( + self, + ) -> impl Future<Output = Result<Bytes, ServerFnError<CustErr>>> + Send; /// Attempts to extract a binary stream from an HTTP response. fn try_into_stream( @@ -64,11 +75,17 @@ pub trait ClientRes<CustErr> { pub struct BrowserMockRes; impl<CustErr> Res<CustErr> for BrowserMockRes { - fn try_from_string(content_type: &str, data: String) -> Result<Self, ServerFnError<CustErr>> { + fn try_from_string( + content_type: &str, + data: String, + ) -> Result<Self, ServerFnError<CustErr>> { unreachable!() } - fn try_from_bytes(content_type: &str, data: Bytes) -> Result<Self, ServerFnError<CustErr>> { + fn try_from_bytes( + content_type: &str, + data: Bytes, + ) -> Result<Self, ServerFnError<CustErr>> { unreachable!() } From dec17fc65be0040b839c916fba7f477048ad4082 Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Wed, 3 Jan 2024 07:38:43 -0500 Subject: [PATCH 013/100] fix server actions and server multi actions --- leptos_server/src/action.rs | 16 ++++++---------- leptos_server/src/multi_action.rs | 13 ++++--------- 2 files changed, 10 insertions(+), 19 deletions(-) diff --git a/leptos_server/src/action.rs b/leptos_server/src/action.rs index 6052f6401e..ef2eeae89c 100644 --- a/leptos_server/src/action.rs +++ b/leptos_server/src/action.rs @@ -212,17 +212,17 @@ where any(debug_assertions, feature = "ssr"), tracing::instrument(level = "trace", skip_all,) )] - pub fn server() -> Action<I, Result<I::Output, ServerFnError>> + pub fn server() -> Action<I, Result<I::Output, ServerFnError<I::Error>>> where I: ServerFn<Output = O> + Clone, { // The server is able to call the function directly #[cfg(feature = "ssr")] - let action_function = |args: &I| I::call_fn(args.clone(), ()); + let action_function = |args: &I| I::run_body(args.clone()); // When not on the server send a fetch to request the fn call. #[cfg(not(feature = "ssr"))] - let action_function = |args: &I| I::call_fn_client(args.clone(), ()); + let action_function = |args: &I| I::run_on_client(args.clone()); // create the action Action::new(action_function).using_server_fn::<I>() @@ -267,13 +267,8 @@ where tracing::instrument(level = "trace", skip_all,) )] pub fn using_server_fn<T: ServerFn>(self) -> Self { - let prefix = T::prefix(); self.0.update_value(|state| { - state.url = if prefix.is_empty() { - Some(T::url().to_string()) - } else { - Some(prefix.to_string() + "/" + T::url()) - }; + state.url = Some(T::url().to_string()); }); self } @@ -483,7 +478,8 @@ where any(debug_assertions, feature = "ssr"), tracing::instrument(level = "trace", skip_all,) )] -pub fn create_server_action<S>() -> Action<S, Result<S::Output, ServerFnError>> +pub fn create_server_action<S>( +) -> Action<S, Result<S::Output, ServerFnError<S::Error>>> where S: Clone + ServerFn, { diff --git a/leptos_server/src/multi_action.rs b/leptos_server/src/multi_action.rs index bd9a7fa72c..232e36b64c 100644 --- a/leptos_server/src/multi_action.rs +++ b/leptos_server/src/multi_action.rs @@ -133,13 +133,8 @@ where tracing::instrument(level = "trace", skip_all,) )] pub fn using_server_fn<T: ServerFn>(self) -> Self { - let prefix = T::prefix(); self.0.update_value(|a| { - a.url = if prefix.is_empty() { - Some(T::url().to_string()) - } else { - Some(prefix.to_string() + "/" + T::url()) - }; + a.url = Some(T::url().to_string()); }); self @@ -343,13 +338,13 @@ where tracing::instrument(level = "trace", skip_all,) )] pub fn create_server_multi_action<S>( -) -> MultiAction<S, Result<S::Output, ServerFnError>> +) -> MultiAction<S, Result<S::Output, ServerFnError<S::Error>>> where S: Clone + ServerFn, { #[cfg(feature = "ssr")] - let c = move |args: &S| S::call_fn(args.clone(), ()); + let c = move |args: &S| S::run_body(args.clone()); #[cfg(not(feature = "ssr"))] - let c = move |args: &S| S::call_fn_client(args.clone(), ()); + let c = move |args: &S| S::run_on_client(args.clone()); create_multi_action(c).using_server_fn::<S>() } From c3e3ce7878c27157cfdfcf3992070d854bcd0272 Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Wed, 3 Jan 2024 20:15:38 -0500 Subject: [PATCH 014/100] changes to get `todo_app_sqlite_axum` example working --- examples/todo_app_sqlite_axum/Cargo.toml | 1 + examples/todo_app_sqlite_axum/Todos.db | Bin 16384 -> 16384 bytes examples/todo_app_sqlite_axum/src/todo.rs | 23 +- integrations/axum/Cargo.toml | 2 + integrations/axum/src/lib.rs | 447 +++++-------------- leptos/Cargo.toml | 2 +- leptos/src/lib.rs | 2 +- leptos_macro/Cargo.toml | 2 + leptos_macro/src/lib.rs | 11 +- leptos_macro/src/server.rs | 210 --------- router/src/components/form.rs | 40 +- server_fn/Cargo.toml | 2 +- server_fn/server_fn_macro_default/src/lib.rs | 1 - server_fn/src/codec/mod.rs | 12 +- server_fn/src/error.rs | 8 +- server_fn/src/lib.rs | 24 +- server_fn_macro/src/lib.rs | 1 - 17 files changed, 177 insertions(+), 611 deletions(-) delete mode 100644 leptos_macro/src/server.rs diff --git a/examples/todo_app_sqlite_axum/Cargo.toml b/examples/todo_app_sqlite_axum/Cargo.toml index a01215d397..5e4d187d56 100644 --- a/examples/todo_app_sqlite_axum/Cargo.toml +++ b/examples/todo_app_sqlite_axum/Cargo.toml @@ -13,6 +13,7 @@ futures = "0.3" cfg-if = "1.0" http = "1.0" leptos = { path = "../../leptos", features = ["nightly"] } +server_fn = { path = "../../server_fn", features = ["url", "json"] } leptos_axum = { path = "../../integrations/axum", optional = true } leptos_meta = { path = "../../meta", features = ["nightly"] } leptos_router = { path = "../../router", features = ["nightly"] } diff --git a/examples/todo_app_sqlite_axum/Todos.db b/examples/todo_app_sqlite_axum/Todos.db index ec81f62b999d4d0b6675e6c5b4550d572cafc1ca..263ef90a1294c8770642036723c764eba4591d78 100644 GIT binary patch delta 82 zcmZo@U~Fh$oFL68Gf~EwQD$SpLViI81_l=X2Q2(Q`9JYL;D5>ga<ibpX8y@X<)sx^ gIawI^Ib2E<S-C(AXM1~Ab`Zmfl?_BWE>chc0FW#akN^Mx delta 82 zcmZo@U~Fh$oFL68G*QNxQD|erLViL1mkdn&7lDZX4j3NYENHNqfAUdzX$5f(kJOx; h{1Qb51_p6q4tslh9!3@hSq`VtN`>6aoa{vk3IOny7W)7I diff --git a/examples/todo_app_sqlite_axum/src/todo.rs b/examples/todo_app_sqlite_axum/src/todo.rs index fd40ed29c9..1ae2092e35 100644 --- a/examples/todo_app_sqlite_axum/src/todo.rs +++ b/examples/todo_app_sqlite_axum/src/todo.rs @@ -26,9 +26,11 @@ cfg_if! { #[server(GetTodos, "/api")] pub async fn get_todos() -> Result<Vec<Todo>, ServerFnError> { + use http::{header::SET_COOKIE, request::Parts, HeaderValue, StatusCode}; + use leptos_axum::ResponseOptions; + // this is just an example of how to access server context injected in the handlers - // http::Request doesn't implement Clone, so more work will be needed to do use_context() on this - let req_parts = use_context::<leptos_axum::RequestParts>(); + let req_parts = use_context::<Parts>(); if let Some(req_parts) = req_parts { println!("Uri = {:?}", req_parts.uri); @@ -45,19 +47,10 @@ pub async fn get_todos() -> Result<Vec<Todo>, ServerFnError> { todos.push(row); } - // Add a random header(because why not) - // let mut res_headers = HeaderMap::new(); - // res_headers.insert(SET_COOKIE, HeaderValue::from_str("fizz=buzz").unwrap()); - - // let res_parts = leptos_axum::ResponseParts { - // headers: res_headers, - // status: Some(StatusCode::IM_A_TEAPOT), - // }; - - // let res_options_outer = use_context::<leptos_axum::ResponseOptions>(); - // if let Some(res_options) = res_options_outer { - // res_options.overwrite(res_parts).await; - // } + // Lines below show how to set status code and headers on the response + // let resp = expect_context::<ResponseOptions>(); + // resp.set_status(StatusCode::IM_A_TEAPOT); + // resp.insert_header(SET_COOKIE, HeaderValue::from_str("fizz=buzz").unwrap()); Ok(todos) } diff --git a/integrations/axum/Cargo.toml b/integrations/axum/Cargo.toml index 91fc936e8e..5469d2793a 100644 --- a/integrations/axum/Cargo.toml +++ b/integrations/axum/Cargo.toml @@ -14,6 +14,8 @@ axum = { version = "0.7", default-features = false, features = [ futures = "0.3" http-body-util = "0.1" leptos = { workspace = true, features = ["ssr"] } +server_fn = { workspace = true, features = ["axum"] } +leptos_macro = { workspace = true, features = ["axum"] } leptos_meta = { workspace = true, features = ["ssr"] } leptos_router = { workspace = true, features = ["ssr"] } leptos_integration_utils = { workspace = true } diff --git a/integrations/axum/src/lib.rs b/integrations/axum/src/lib.rs index 3d019a888e..b90abfa219 100644 --- a/integrations/axum/src/lib.rs +++ b/integrations/axum/src/lib.rs @@ -35,61 +35,32 @@ use axum::{ body::{Body, Bytes}, - extract::{FromRef, FromRequestParts, MatchedPath, Path, RawQuery}, + extract::{FromRef, FromRequestParts, MatchedPath}, http::{ header::{self, HeaderName, HeaderValue}, - method::Method, request::Parts, - uri::Uri, - version::Version, HeaderMap, Request, Response, StatusCode, }, response::IntoResponse, routing::{delete, get, patch, post, put}, + RequestPartsExt, }; use futures::{ channel::mpsc::{Receiver, Sender}, Future, SinkExt, Stream, StreamExt, }; -use http_body_util::BodyExt; -use leptos::{ - leptos_server::{server_fn_by_path, Payload}, - server_fn::Encoding, - ssr::*, - *, -}; +use leptos::{ssr::*, *}; use leptos_integration_utils::{build_async_response, html_parts_separated}; use leptos_meta::{generate_head_metadata_separated, MetaContext}; use leptos_router::*; use once_cell::sync::OnceCell; use parking_lot::RwLock; -use std::{fmt::Debug, io, pin::Pin, sync::Arc, thread::available_parallelism}; +use std::{ + error::Error, fmt::Debug, io, pin::Pin, sync::Arc, + thread::available_parallelism, +}; use tokio_util::task::LocalPoolHandle; use tracing::Instrument; -/// A struct to hold the parts of the incoming Request. Since `http::Request` isn't cloneable, we're forced -/// to construct this for Leptos to use in Axum -#[derive(Debug, Clone)] -pub struct RequestParts { - pub version: Version, - pub method: Method, - pub uri: Uri, - pub headers: HeaderMap<HeaderValue>, - pub body: Bytes, -} - -/// Convert http::Parts to RequestParts(and vice versa). Body and Extensions will -/// be lost in the conversion -impl From<Parts> for RequestParts { - fn from(parts: Parts) -> Self { - Self { - version: parts.version, - method: parts.method, - uri: parts.uri, - headers: parts.headers, - body: Bytes::default(), - } - } -} /// This struct lets you define headers and override the status of the Response from an Element or a Server Function /// Typically contained inside of a ResponseOptions. Setting this is useful for cookies and custom responses. @@ -157,22 +128,12 @@ pub fn redirect(path: &str) { /// Decomposes an HTTP request into its parts, allowing you to read its headers /// and other data without consuming the body. Creates a new Request from the /// original parts for further processing -pub async fn generate_request_and_parts( +pub fn generate_request_and_parts( req: Request<Body>, -) -> (Request<Body>, RequestParts) { - // provide request headers as context in server scope +) -> (Request<Body>, Parts) { let (parts, body) = req.into_parts(); - let body = body.collect().await.unwrap_or_default().to_bytes(); - let request_parts = RequestParts { - method: parts.method.clone(), - uri: parts.uri.clone(), - headers: parts.headers.clone(), - version: parts.version, - body: body.clone(), - }; - let request = Request::from_parts(parts, body.into()); - - (request, request_parts) + let parts2 = parts.clone(); + (Request::from_parts(parts, body), parts2) } /// An Axum handlers to listens for a request with Leptos server function arguments in the body, @@ -208,16 +169,11 @@ pub async fn generate_request_and_parts( /// /// ## Provided Context Types /// This function always provides context values including the following types: -/// - [RequestParts] -/// - [ResponseOptions] +/// - [`Parts`] +/// - [`ResponseOptions`] #[tracing::instrument(level = "trace", fields(error), skip_all)] -pub async fn handle_server_fns( - Path(fn_name): Path<String>, - headers: HeaderMap, - RawQuery(query): RawQuery, - req: Request<Body>, -) -> impl IntoResponse { - handle_server_fns_inner(fn_name, headers, query, || {}, req).await +pub async fn handle_server_fns(req: Request<Body>) -> impl IntoResponse { + handle_server_fns_inner(|| {}, req).await } /// Leptos pool causes wasm to panic and leptos_reactive::spawn::spawn_local causes native @@ -262,139 +218,66 @@ macro_rules! spawn_task { /// - [ResponseOptions] #[tracing::instrument(level = "trace", fields(error), skip_all)] pub async fn handle_server_fns_with_context( - Path(fn_name): Path<String>, - headers: HeaderMap, - RawQuery(query): RawQuery, additional_context: impl Fn() + 'static + Clone + Send, req: Request<Body>, ) -> impl IntoResponse { - handle_server_fns_inner(fn_name, headers, query, additional_context, req) - .await + handle_server_fns_inner(additional_context, req).await } -#[tracing::instrument(level = "trace", fields(error), skip_all)] + async fn handle_server_fns_inner( - fn_name: String, - headers: HeaderMap, - query: Option<String>, additional_context: impl Fn() + 'static + Clone + Send, req: Request<Body>, ) -> impl IntoResponse { - // Axum Path extractor doesn't remove the first slash from the path, while Actix does - let fn_name = fn_name - .strip_prefix('/') - .map(|fn_name| fn_name.to_string()) - .unwrap_or(fn_name); + use server_fn::middleware::Service; let (tx, rx) = futures::channel::oneshot::channel(); spawn_task!(async move { - let res = - if let Some(server_fn) = server_fn_by_path(fn_name.as_str()) { - let runtime = create_runtime(); + let path = req.uri().path().to_string(); + let (req, parts) = generate_request_and_parts(req); - additional_context(); + let res = if let Some(mut service) = + server_fn::axum::get_server_fn_service(&path) + { + let runtime = create_runtime(); - let (req, req_parts) = generate_request_and_parts(req).await; + additional_context(); + provide_context(parts); + // Add this so that we can set headers and status of the response + provide_context(ResponseOptions::default()); - provide_context(req_parts.clone()); - provide_context(ExtractorHelper::from(req)); - // Add this so that we can set headers and status of the response - provide_context(ResponseOptions::default()); + let mut res = service.run(req).await; - let query: &Bytes = &query.unwrap_or("".to_string()).into(); - let data = match &server_fn.encoding() { - Encoding::Url | Encoding::Cbor => &req_parts.body, - Encoding::GetJSON | Encoding::GetCBOR => query, - }; - let res = match server_fn.call((), data).await { - Ok(serialized) => { - // If ResponseOptions are set, add the headers and status to the request - let res_options = use_context::<ResponseOptions>(); - - // if this is Accept: application/json then send a serialized JSON response - let accept_header = headers - .get("Accept") - .and_then(|value| value.to_str().ok()); - let mut res = Response::builder(); - - // Add headers from ResponseParts if they exist. These should be added as long - // as the server function returns an OK response - let res_options_outer = res_options.unwrap().0; - let res_options_inner = res_options_outer.read(); - let (status, mut res_headers) = ( - res_options_inner.status, - res_options_inner.headers.clone(), - ); + let res_options = expect_context::<ResponseOptions>().0; + let res_options_inner = res_options.read(); + let (status, mut res_headers) = + (res_options_inner.status, res_options_inner.headers.clone()); - if accept_header == Some("application/json") - || accept_header - == Some("application/x-www-form-urlencoded") - || accept_header == Some("application/cbor") - { - res = res.status(StatusCode::OK); - } - // otherwise, it's probably a <form> submit or something: redirect back to the referrer - else { - let referer = headers - .get("Referer") - .and_then(|value| value.to_str().ok()) - .unwrap_or("/"); - - res = res - .status(StatusCode::SEE_OTHER) - .header("Location", referer); - } - // Override StatusCode if it was set in a Resource or Element - res = match status { - Some(status) => res.status(status), - None => res, - }; - // This must be after the default referrer - // redirect so that it overwrites the one above - if let Some(header_ref) = res.headers_mut() { - header_ref.extend(res_headers.drain()); - }; - match serialized { - Payload::Binary(data) => res - .header("Content-Type", "application/cbor") - .body(Body::from(data)), - Payload::Url(data) => res - .header( - "Content-Type", - "application/x-www-form-urlencoded", - ) - .body(Body::from(data)), - Payload::Json(data) => res - .header("Content-Type", "application/json") - .body(Body::from(data)), - } - } - Err(e) => Response::builder() - .status(StatusCode::INTERNAL_SERVER_ERROR) - .body(Body::from( - serde_json::to_string(&e) - .unwrap_or_else(|_| e.to_string()), - )), - }; - // clean up the scope - runtime.dispose(); - res - } else { - Response::builder().status(StatusCode::BAD_REQUEST).body( - Body::from(format!( - "Could not find a server function at the route \ - {fn_name}. \n\nIt's likely that either - 1. The API prefix you specify in the `#[server]` \ - macro doesn't match the prefix at which your server \ - function handler is mounted, or \n2. You are on a \ - platform that doesn't support automatic server \ - function registration and you need to call \ - ServerFn::register_explicit() on the server function \ - type, somewhere in your `main` function.", - )), - ) + // apply status code and headers if used changed them + if let Some(status) = status { + *res.status_mut() = status; } - .expect("could not build Response"); + res.headers_mut().extend(res_headers.drain()); + + // clean up the scope + runtime.dispose(); + Ok(res) + } else { + Response::builder().status(StatusCode::BAD_REQUEST).body( + Body::from(format!( + "Could not find a server function at the route {path}. \ + \n\nIt's likely that either + 1. The API prefix you specify in the `#[server]` \ + macro doesn't match the prefix at which your server \ + function handler is mounted, or \n2. You are on a \ + platform that doesn't support automatic server function \ + registration and you need to call \ + ServerFn::register_explicit() on the server function \ + type, somewhere in your `main` function.", + )), + ) + } + .expect("could not build Response"); _ = tx.send(res); }); @@ -542,7 +425,7 @@ where /// /// ## Provided Context Types /// This function always provides context values including the following types: -/// - [RequestParts] +/// - [Parts] /// - [ResponseOptions] /// - [MetaContext](leptos_meta::MetaContext) /// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext) @@ -583,7 +466,7 @@ where /// /// ## Provided Context Types /// This function always provides context values including the following types: -/// - [RequestParts] +/// - [Parts] /// - [ResponseOptions] /// - [MetaContext](leptos_meta::MetaContext) /// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext) @@ -694,7 +577,7 @@ where /// /// ## Provided Context Types /// This function always provides context values including the following types: -/// - [RequestParts] +/// - [Parts] /// - [ResponseOptions] /// - [MetaContext](leptos_meta::MetaContext) /// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext) @@ -732,9 +615,9 @@ where let path = req.uri().path_and_query().unwrap().as_str(); let full_path = format!("http://leptos.dev{path}"); - let (req, req_parts) = generate_request_and_parts(req).await; + let (req, req_parts) = generate_request_and_parts(req); move || { - provide_contexts(full_path, req_parts, req.into(), default_res_options); + provide_contexts(full_path, req_parts, default_res_options); app_fn().into_view() } }; @@ -855,7 +738,7 @@ async fn forward_stream( /// /// ## Provided Context Types /// This function always provides context values including the following types: -/// - [RequestParts] +/// - [Parts] /// - [ResponseOptions] /// - [MetaContext](leptos_meta::MetaContext) /// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext) @@ -895,9 +778,9 @@ where spawn_task!(async move { let app = { let full_path = full_path.clone(); - let (req, req_parts) = generate_request_and_parts(req).await; + let (parts, _) = req.into_parts(); move || { - provide_contexts(full_path, req_parts, req.into(), default_res_options); + provide_contexts(full_path, parts, default_res_options); app_fn().into_view() } }; @@ -923,15 +806,13 @@ where #[tracing::instrument(level = "trace", fields(error), skip_all)] fn provide_contexts( path: String, - req_parts: RequestParts, - extractor: ExtractorHelper, + parts: Parts, default_res_options: ResponseOptions, ) { let integration = ServerIntegration { path }; provide_context(RouterIntegrationContext::new(integration)); provide_context(MetaContext::new()); - provide_context(req_parts); - provide_context(extractor); + provide_context(parts); provide_context(default_res_options); provide_server_redirect(redirect); #[cfg(feature = "nonce")] @@ -987,7 +868,7 @@ fn provide_contexts( /// /// ## Provided Context Types /// This function always provides context values including the following types: -/// - [RequestParts] +/// - [Parts] /// - [ResponseOptions] /// - [MetaContext](leptos_meta::MetaContext) /// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext) @@ -1029,7 +910,7 @@ where /// /// ## Provided Context Types /// This function always provides context values including the following types: -/// - [RequestParts] +/// - [Parts] /// - [ResponseOptions] /// - [MetaContext](leptos_meta::MetaContext) /// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext) @@ -1068,13 +949,11 @@ where spawn_task!(async move { let app = { let full_path = full_path.clone(); - let (req, req_parts) = - generate_request_and_parts(req).await; + let (req, req_parts) = generate_request_and_parts(req); move || { provide_contexts( full_path, req_parts, - req.into(), default_res_options, ); app_fn().into_view() @@ -1160,7 +1039,7 @@ where /// /// ## Provided Context Types /// This function always provides context values including the following types: -/// - [RequestParts] +/// - [Parts] /// - [ResponseOptions] /// - [MetaContext](leptos_meta::MetaContext) /// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext) @@ -1200,13 +1079,11 @@ where spawn_task!(async move { let app = { let full_path = full_path.clone(); - let (req, req_parts) = - generate_request_and_parts(req).await; + let (req, req_parts) = generate_request_and_parts(req); move || { provide_contexts( full_path, req_parts, - req.into(), default_res_options, ); app_fn().into_view() @@ -1800,78 +1677,7 @@ fn get_leptos_pool() -> LocalPoolHandle { .clone() } -#[derive(Clone, Debug)] -struct ExtractorHelper { - parts: Arc<tokio::sync::Mutex<Parts>>, -} - -impl ExtractorHelper { - pub fn new(parts: Parts) -> Self { - Self { - parts: Arc::new(tokio::sync::Mutex::new(parts)), - } - } - - pub async fn extract<F, T, U, S>( - &self, - f: F, - s: S, - ) -> Result<U, T::Rejection> - where - S: Sized, - F: Extractor<T, U, S>, - T: core::fmt::Debug + Send + FromRequestParts<S> + 'static, - T::Rejection: core::fmt::Debug + Send + 'static, - { - let mut parts = self.parts.lock().await; - let data = T::from_request_parts(&mut parts, &s).await?; - Ok(f.call(data).await) - } -} - -impl<B> From<Request<B>> for ExtractorHelper { - fn from(req: Request<B>) -> Self { - // TODO provide body for extractors there, too? - let (parts, _) = req.into_parts(); - ExtractorHelper::new(parts) - } -} - -/// A helper to make it easier to use Axum extractors in server functions. This takes -/// a handler function as its argument. The handler rules similar to Axum -/// [handlers](https://docs.rs/axum/latest/axum/extract/index.html#intro): it is an async function -/// whose arguments are “extractors.” -/// -/// ```rust,ignore -/// #[server(QueryExtract, "/api")] -/// pub async fn query_extract() -> Result<String, ServerFnError> { -/// use axum::{extract::Query, http::Method}; -/// use leptos_axum::extract; -/// -/// extract(|method: Method, res: Query<MyQuery>| async move { -/// format!("{method:?} and {}", res.q) -/// }, -/// ) -/// .await -/// .map_err(|e| ServerFnError::ServerError("Could not extract method and query...".to_string())) -/// } -/// ``` -/// -/// > This function only supports extractors for -/// which the state is `()`. If the state is not `()`, use [`extract_with_state`]. -#[tracing::instrument(level = "trace", fields(error), skip_all)] -pub async fn extract<T, U>( - f: impl Extractor<T, U, ()>, -) -> Result<U, T::Rejection> -where - T: core::fmt::Debug + Send + FromRequestParts<()> + 'static, - T::Rejection: core::fmt::Debug + Send + 'static, -{ - extract_with_state((), f).await -} - -/// A helper to make it easier to use Axum extractors in server functions, with a -/// simpler API than [`extract`]. +/// A helper to make it easier to use Axum extractors in server functions. /// /// It is generic over some type `T` that implements [`FromRequestParts`] and can /// therefore be used in an extractor. The compiler can often infer this type. @@ -1889,98 +1695,49 @@ where /// Ok(query) /// } /// ``` -pub async fn extractor<T>() -> Result<T, ServerFnError> +pub async fn extractor<T, CustErr>() -> Result<T, ServerFnError> where T: Sized + FromRequestParts<()>, T::Rejection: Debug, + CustErr: Error + 'static, { - let ctx = use_context::<ExtractorHelper>().expect( - "should have had ExtractorHelper provided by the leptos_axum \ - integration", - ); - let mut parts = ctx.parts.lock().await; - T::from_request_parts(&mut parts, &()) - .await - .map_err(|e| ServerFnError::ServerError(format!("{e:?}"))) + extractor_with_state::<T, (), CustErr>(&()).await } -/// A helper to make it easier to use Axum extractors in server functions. This takes -/// a handler function and state as its arguments. The handler rules similar to Axum -/// [handlers](https://docs.rs/axum/latest/axum/extract/index.html#intro): it is an async function -/// whose arguments are “extractors.” +/// A helper to make it easier to use Axum extractors in server functions. This +/// function is compatible with extractors that require access to `State`. +/// +/// It is generic over some type `T` that implements [`FromRequestParts`] and can +/// therefore be used in an extractor. The compiler can often infer this type. +/// +/// Any error that occurs during extraction is converted to a [`ServerFnError`]. /// /// ```rust,ignore -/// #[server(QueryExtract, "/api")] -/// pub async fn query_extract() -> Result<String, ServerFnError> { +/// // MyQuery is some type that implements `Deserialize + Serialize` +/// #[server] +/// pub async fn query_extract() -> Result<MyQuery, ServerFnError> { /// use axum::{extract::Query, http::Method}; -/// use leptos_axum::extract; -/// let state: ServerState = use_context::<crate::ServerState>() -/// .ok_or(ServerFnError::ServerError("No server state".to_string()))?; +/// use leptos_axum::*; +/// let Query(query) = extractor().await?; /// -/// extract_with_state(&state, |method: Method, res: Query<MyQuery>| async move { -/// format!("{method:?} and {}", res.q) -/// }, -/// ) -/// .await -/// .map_err(|e| ServerFnError::ServerError("Could not extract method and query...".to_string())) +/// Ok(query) /// } /// ``` -#[tracing::instrument(level = "trace", fields(error), skip_all)] -pub async fn extract_with_state<T, U, S>( - state: S, - f: impl Extractor<T, U, S>, -) -> Result<U, T::Rejection> +pub async fn extractor_with_state<T, S, CustErr>( + state: &S, +) -> Result<T, ServerFnError> where - S: Sized, - T: core::fmt::Debug + Send + FromRequestParts<S> + 'static, - T::Rejection: core::fmt::Debug + Send + 'static, + T: Sized + FromRequestParts<S>, + T::Rejection: Debug, + CustErr: Error + 'static, { - use_context::<ExtractorHelper>() - .expect( - "should have had ExtractorHelper provided by the leptos_axum \ - integration", + let mut parts = use_context::<Parts>().ok_or_else(|| { + ServerFnError::ServerError::<CustErr>( + "should have had Parts provided by the leptos_axum integration" + .to_string(), ) - .extract(f, state) + })?; + T::from_request_parts(&mut parts, &state) .await + .map_err(|e| ServerFnError::ServerError(format!("{e:?}"))) } - -pub trait Extractor<T, U, S> -where - S: Sized, - T: FromRequestParts<S>, -{ - fn call(self, args: T) -> Pin<Box<dyn Future<Output = U>>>; -} - -macro_rules! factory_tuple ({ $($param:ident)* } => { - impl<Func, Fut, U, S, $($param,)*> Extractor<($($param,)*), U, S> for Func - where - $($param: FromRequestParts<S> + Send,)* - Func: FnOnce($($param),*) -> Fut + 'static, - Fut: Future<Output = U> + 'static, - S: Sized + Send + Sync - { - #[inline] - #[allow(non_snake_case)] - fn call(self, ($($param,)*): ($($param,)*)) -> Pin<Box<dyn Future<Output = U>>> { - Box::pin((self)($($param,)*)) - } - } -}); - -factory_tuple! { A } -factory_tuple! { A B } -factory_tuple! { A B C } -factory_tuple! { A B C D } -factory_tuple! { A B C D E } -factory_tuple! { A B C D E F } -factory_tuple! { A B C D E F G } -factory_tuple! { A B C D E F G H } -factory_tuple! { A B C D E F G H I } -factory_tuple! { A B C D E F G H I J } -factory_tuple! { A B C D E F G H I J K } -factory_tuple! { A B C D E F G H I J K L } -factory_tuple! { A B C D E F G H I J K L M } -factory_tuple! { A B C D E F G H I J K L M N } -factory_tuple! { A B C D E F G H I J K L M N O } -factory_tuple! { A B C D E F G H I J K L M N O P } diff --git a/leptos/Cargo.toml b/leptos/Cargo.toml index 30be04993e..78f47ad3b5 100644 --- a/leptos/Cargo.toml +++ b/leptos/Cargo.toml @@ -20,7 +20,7 @@ typed-builder = "0.18" typed-builder-macro = "0.18" serde = { version = "1", optional = true } serde_json = { version = "1", optional = true } -server_fn = { workspace = true } +server_fn = { workspace = true, features = ["browser"] } web-sys = { version = "0.3.63", features = [ "ShadowRoot", "ShadowRootInit", diff --git a/leptos/src/lib.rs b/leptos/src/lib.rs index 0d208a21a4..1d1004d5e7 100644 --- a/leptos/src/lib.rs +++ b/leptos/src/lib.rs @@ -184,7 +184,7 @@ pub use leptos_macro::{component, island, server, slice, slot, view, Params}; pub use leptos_reactive::*; pub use leptos_server::{ self, create_action, create_multi_action, create_server_action, - create_server_multi_action, Action, MultiAction, ServerFn, ServerFnError, + create_server_multi_action, Action, MultiAction, ServerFnError, ServerFnErrorErr, }; pub use server_fn::{self, ServerFn as _}; diff --git a/leptos_macro/Cargo.toml b/leptos_macro/Cargo.toml index 3fb1ef1e14..bb4033b5d7 100644 --- a/leptos_macro/Cargo.toml +++ b/leptos_macro/Cargo.toml @@ -44,6 +44,8 @@ nightly = ["server_fn_macro/nightly"] tracing = [] experimental-islands = [] trace-component-props = [] +actix = ["server_fn_macro/actix"] +axum = ["server_fn_macro/axum"] [package.metadata.cargo-all-features] denylist = ["nightly", "tracing", "trace-component-props"] diff --git a/leptos_macro/src/lib.rs b/leptos_macro/src/lib.rs index b9c6ceead2..04c8a41d2f 100644 --- a/leptos_macro/src/lib.rs +++ b/leptos_macro/src/lib.rs @@ -37,7 +37,6 @@ mod view; use crate::component::unmodified_fn_name_from_fn_name; use view::{client_template::render_template, render_view}; mod component; -mod server; mod slice; mod slot; @@ -971,7 +970,15 @@ pub fn slot(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream { #[proc_macro_attribute] #[proc_macro_error] pub fn server(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream { - server::server_impl(args, s) + match server_fn_macro::server_macro_impl( + args.into(), + s.into(), + Some(syn::parse_quote!(::leptos::server_fn)), + "/api", + ) { + Err(e) => e.to_compile_error().into(), + Ok(s) => s.to_token_stream().into(), + } } /// Derives a trait that parses a map of string keys and values into a typed diff --git a/leptos_macro/src/server.rs b/leptos_macro/src/server.rs deleted file mode 100644 index 035d2300ab..0000000000 --- a/leptos_macro/src/server.rs +++ /dev/null @@ -1,210 +0,0 @@ -use convert_case::{Case, Converter}; -use proc_macro::TokenStream; -use proc_macro2::Literal; -use quote::{ToTokens, __private::TokenStream as TokenStream2}; -use syn::{ - parse::{Parse, ParseStream}, - Attribute, Ident, ItemFn, LitStr, Token, -}; - -pub fn server_impl(args: TokenStream, s: TokenStream) -> TokenStream { - let function: syn::ItemFn = match syn::parse(s.clone()) { - Ok(f) => f, - // Returning the original input stream in the case of a parsing - // error helps IDEs and rust-analyzer with auto-completion. - Err(_) => return s, - }; - let ItemFn { - attrs, - vis, - sig, - block, - } = function; - // TODO apply middleware: https://github.com/leptos-rs/leptos/issues/1461 - let mapped_body = quote::quote! { - #(#attrs)* - #vis #sig { - #block - } - }; - - let mut args: ServerFnArgs = match syn::parse(args) { - Ok(args) => args, - Err(e) => return e.to_compile_error().into(), - }; - args.docs = attrs - .iter() - .filter(|attr| attr.meta.path().is_ident("doc")) - .cloned() - .collect(); - // default to PascalCase version of function name if no struct name given - if args.struct_name.is_none() { - let upper_camel_case_name = Converter::new() - .from_case(Case::Snake) - .to_case(Case::UpperCamel) - .convert(sig.ident.to_string()); - args.struct_name = - Some(Ident::new(&upper_camel_case_name, sig.ident.span())); - } - // default to "/api" if no prefix given - if args.prefix.is_none() { - args.prefix = Some(Literal::string("/api")); - } - let args_prefix = match &args.prefix { - Some(s) => s.to_string(), - None => "/api".to_string(), - }; - // default to "Url" if no encoding given - if args.encoding.is_none() { - args.encoding = Some(Literal::string("Url")); - } - // Either this match is wrong, or the impl in the macro crate is wrong - match server_fn_macro::server_macro_impl( - quote::quote!(#args), - mapped_body, - syn::parse_quote!(::leptos::leptos_server::ServerFnTraitObj), - Some(syn::parse_quote!(::leptos::server_fn)), - &args_prefix, - ) { - Err(e) => e.to_compile_error().into(), - Ok(s) => s.to_token_stream().into(), - } -} - -struct ServerFnArgs { - docs: Vec<Attribute>, - struct_name: Option<Ident>, - prefix: Option<Literal>, - encoding: Option<Literal>, - fn_path: Option<Literal>, -} - -impl ToTokens for ServerFnArgs { - fn to_tokens(&self, tokens: &mut TokenStream2) { - let struct_name = - self.struct_name.as_ref().map(|s| quote::quote! { #s, }); - let prefix = self.prefix.as_ref().map(|p| quote::quote! { #p, }); - let encoding = self.encoding.as_ref().map(|e| quote::quote! { #e, }); - let fn_path = self.fn_path.as_ref().map(|f| quote::quote! { #f }); - let docs = &self.docs; - tokens.extend(quote::quote! { - #(#docs)* - #struct_name - #prefix - #encoding - #fn_path - }) - } -} - -impl Parse for ServerFnArgs { - fn parse(input: ParseStream) -> syn::Result<Self> { - let mut struct_name: Option<Ident> = None; - let mut prefix: Option<Literal> = None; - let mut encoding: Option<Literal> = None; - let mut fn_path: Option<Literal> = None; - - let mut use_key_and_value = false; - let mut arg_pos = 0; - - while !input.is_empty() { - arg_pos += 1; - let lookahead = input.lookahead1(); - if lookahead.peek(Ident) { - let key_or_value: Ident = input.parse()?; - - let lookahead = input.lookahead1(); - if lookahead.peek(Token![=]) { - input.parse::<Token![=]>()?; - let key = key_or_value; - use_key_and_value = true; - if key == "name" { - if struct_name.is_some() { - return Err(syn::Error::new( - key.span(), - "keyword argument repeated: name", - )); - } - struct_name = Some(input.parse()?); - } else if key == "prefix" { - if prefix.is_some() { - return Err(syn::Error::new( - key.span(), - "keyword argument repeated: prefix", - )); - } - prefix = Some(input.parse()?); - } else if key == "encoding" { - if encoding.is_some() { - return Err(syn::Error::new( - key.span(), - "keyword argument repeated: encoding", - )); - } - encoding = Some(input.parse()?); - } else if key == "endpoint" { - if fn_path.is_some() { - return Err(syn::Error::new( - key.span(), - "keyword argument repeated: endpoint", - )); - } - fn_path = Some(input.parse()?); - } else { - return Err(lookahead.error()); - } - } else { - let value = key_or_value; - if use_key_and_value { - return Err(syn::Error::new( - value.span(), - "positional argument follows keyword argument", - )); - } - if arg_pos == 1 { - struct_name = Some(value) - } else { - return Err(syn::Error::new( - value.span(), - "expected string literal", - )); - } - } - } else if lookahead.peek(LitStr) { - let value: Literal = input.parse()?; - if use_key_and_value { - return Err(syn::Error::new( - value.span(), - "positional argument follows keyword argument", - )); - } - match arg_pos { - 1 => return Err(lookahead.error()), - 2 => prefix = Some(value), - 3 => encoding = Some(value), - 4 => fn_path = Some(value), - _ => { - return Err(syn::Error::new( - value.span(), - "unexpected extra argument", - )) - } - } - } else { - return Err(lookahead.error()); - } - - if !input.is_empty() { - input.parse::<Token![,]>()?; - } - } - - Ok(Self { - docs: vec![], - struct_name, - prefix, - encoding, - fn_path, - }) - } -} diff --git a/router/src/components/form.rs b/router/src/components/form.rs index 2177c442eb..ffa50ab9ce 100644 --- a/router/src/components/form.rs +++ b/router/src/components/form.rs @@ -2,7 +2,7 @@ use crate::{ hooks::has_router, use_navigate, use_resolved_path, NavigateOptions, ToHref, Url, }; -use leptos::{html::form, logging::*, *}; +use leptos::{html::form, logging::*, server_fn::ServerFn, *}; use serde::{de::DeserializeOwned, Serialize}; use std::{error::Error, rc::Rc}; use wasm_bindgen::{JsCast, UnwrapThrowExt}; @@ -411,10 +411,10 @@ fn current_window_origin() -> String { tracing::instrument(level = "trace", skip_all,) )] #[component] -pub fn ActionForm<I, O>( +pub fn ActionForm<I, O, Enc>( /// The action from which to build the form. This should include a URL, which can be generated - /// by default using [create_server_action](leptos_server::create_server_action) or added - /// manually using [leptos_server::Action::using_server_fn]. + /// by default using [`create_server_action`](l:eptos_server::create_server_action) or added + /// manually using [`using_server_fn`](leptos_server::Action::using_server_fn). action: Action<I, Result<O, ServerFnError>>, /// Sets the `class` attribute on the underlying `<form>` tag, making it easier to style. #[prop(optional, into)] @@ -435,8 +435,9 @@ pub fn ActionForm<I, O>( children: Children, ) -> impl IntoView where - I: Clone + ServerFn + 'static, + I: Clone + DeserializeOwned + ServerFn<InputEncoding = Enc> + 'static, O: Clone + Serialize + DeserializeOwned + 'static, + ServerFnError<I::Error>: Clone, // Enc: FormDataEncoding, { let action_url = if let Some(url) = action.url() { url @@ -452,14 +453,15 @@ where let input = action.input(); let on_error = Rc::new(move |e: &gloo_net::Error| { - batch(move || { + // TODO + /* batch(move || { action.set_pending(false); let e = ServerFnError::Request(e.to_string()); value.try_set(Some(Err(e.clone()))); if let Some(error) = error { error.try_set(Some(Box::new(ServerFnErrorErr::from(e)))); } - }); + });*/ }); let on_form_data = Rc::new(move |form_data: &web_sys::FormData| { @@ -472,7 +474,8 @@ where }); } Err(e) => { - error!("{e}"); + // TODO + /* error!("{e}"); let e = ServerFnError::Serialization(e.to_string()); batch(move || { value.try_set(Some(Err(e.clone()))); @@ -480,7 +483,7 @@ where error .try_set(Some(Box::new(ServerFnErrorErr::from(e)))); } - }); + }); */ } } }); @@ -547,13 +550,14 @@ where } Err(e) => { error!("{e:?}"); - if let Some(error) = error { + // TODO + /* if let Some(error) = error { error.try_set(Some(Box::new( ServerFnErrorErr::Request( e.as_string().unwrap_or_default(), ), ))); - } + }*/ } }; batch(move || { @@ -564,18 +568,6 @@ where }); let class = class.map(|bx| bx.into_attribute_boxed()); - #[cfg(debug_assertions)] - { - if I::encoding() != server_fn::Encoding::Url { - leptos::logging::warn!( - "<ActionForm/> only supports the `Url` encoding for server \ - functions, but {} uses {:?}.", - std::any::type_name::<I>(), - I::encoding() - ); - } - } - let mut props = FormProps::builder() .action(action_url) .version(version) @@ -622,7 +614,7 @@ pub fn MultiActionForm<I, O>( children: Children, ) -> impl IntoView where - I: Clone + ServerFn + 'static, + I: Clone + ServerFn + DeserializeOwned + 'static, O: Clone + Serializable + 'static, { let multi_action = action; diff --git a/server_fn/Cargo.toml b/server_fn/Cargo.toml index affd31e13f..8fb8edca25 100644 --- a/server_fn/Cargo.toml +++ b/server_fn/Cargo.toml @@ -81,7 +81,7 @@ browser = [ "dep:wasm-streams", "dep:wasm-bindgen-futures", ] -#json = ["dep:serde_json"] +json = [] multipart = ["dep:multer"] url = ["dep:serde_qs"] cbor = ["dep:ciborium"] diff --git a/server_fn/server_fn_macro_default/src/lib.rs b/server_fn/server_fn_macro_default/src/lib.rs index 84aa276213..e5230df260 100644 --- a/server_fn/server_fn_macro_default/src/lib.rs +++ b/server_fn/server_fn_macro_default/src/lib.rs @@ -9,7 +9,6 @@ pub fn server(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream { match server_macro_impl( args.into(), s.into(), - syn::parse_quote!(server_fn::default::DefaultServerFnTraitObj), Some(syn::parse_quote!(server_fns)), "/api", ) { diff --git a/server_fn/src/codec/mod.rs b/server_fn/src/codec/mod.rs index 56ab0bf70d..9fba70edf5 100644 --- a/server_fn/src/codec/mod.rs +++ b/server_fn/src/codec/mod.rs @@ -12,7 +12,7 @@ mod rkyv; pub use rkyv::*; #[cfg(feature = "url")] mod url; -use crate::error::ServerFnError; +use crate::{client::Client, error::ServerFnError, request::ClientReq}; use futures::Future; #[cfg(feature = "url")] pub use url::*; @@ -60,3 +60,13 @@ pub trait IntoRes<CustErr, Response, Encoding> { pub trait Encoding { const CONTENT_TYPE: &'static str; } + +pub trait FormDataEncoding<Client, CustErr, Request> +where + Self: Sized, + Client: ClientReq<CustErr>, +{ + fn form_data_into_req( + form_data: Client::FormData, + ) -> Result<Self, ServerFnError<CustErr>>; +} diff --git a/server_fn/src/error.rs b/server_fn/src/error.rs index dbab984483..a36c4e275b 100644 --- a/server_fn/src/error.rs +++ b/server_fn/src/error.rs @@ -57,7 +57,7 @@ impl From<ServerFnError> for Error { /// An empty value indicating that there is no custom error type associated /// with this server function. -#[derive(Debug, Deserialize, Serialize)] +#[derive(Debug, Deserialize, Serialize, Clone, Copy)] pub struct NoCustomError; // Implement `Display` for `NoCustomError` @@ -185,6 +185,12 @@ impl<CustErr> From<CustErr> for ServerFnError<CustErr> { } } +impl<E: std::error::Error> From<E> for ServerFnError { + fn from(value: E) -> Self { + ServerFnError::ServerError(value.to_string()) + } +} + impl<CustErr> Display for ServerFnError<CustErr> where CustErr: Display, diff --git a/server_fn/src/lib.rs b/server_fn/src/lib.rs index 57056ebcfa..68c712dbf6 100644 --- a/server_fn/src/lib.rs +++ b/server_fn/src/lib.rs @@ -72,7 +72,7 @@ where /// 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: ServerFnErrorSerde; + type Error: FromStr + Display; /// Middleware that should be applied to this server function. fn middlewares( @@ -111,7 +111,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(Self::Error::de(&text)) + Err(ServerFnError::<Self::Error>::de(&text)) } else { // otherwise, deserialize the body as is Ok(Self::Output::from_res(res).await) @@ -244,12 +244,7 @@ pub mod axum { pub async fn handle_server_fn(req: Request<Body>) -> Response<Body> { let path = req.uri().path(); - if let Some(server_fn) = REGISTERED_SERVER_FUNCTIONS.get(path) { - let middleware = (server_fn.middleware)(); - let mut service = BoxedService::new(*server_fn); - for middleware in middleware { - service = middleware.layer(service); - } + if let Some(mut service) = get_server_fn_service(&path) { service.run(req).await } else { Response::builder() @@ -267,6 +262,19 @@ pub mod axum { .unwrap() } } + + pub fn get_server_fn_service( + path: &str, + ) -> Option<BoxedService<Request<Body>, Response<Body>>> { + REGISTERED_SERVER_FUNCTIONS.get(path).map(|server_fn| { + let middleware = (server_fn.middleware)(); + let mut service = BoxedService::new(*server_fn); + for middleware in middleware { + service = middleware.layer(service); + } + service + }) + } } // Actix integration diff --git a/server_fn_macro/src/lib.rs b/server_fn_macro/src/lib.rs index 60ef83c03b..bbc4249782 100644 --- a/server_fn_macro/src/lib.rs +++ b/server_fn_macro/src/lib.rs @@ -40,7 +40,6 @@ use syn::{ pub fn server_macro_impl( args: TokenStream2, body: TokenStream2, - trait_obj_wrapper: Type, server_fn_path: Option<Path>, default_path: &str, ) -> Result<TokenStream2> { From af62d2e900abbac48834d5d8450d1fadb38b23b9 Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Wed, 3 Jan 2024 20:45:27 -0500 Subject: [PATCH 015/100] automatically include server function handler in `.leptos_router()` --- examples/todo_app_sqlite_axum/Todos.db | Bin 16384 -> 16384 bytes examples/todo_app_sqlite_axum/src/main.rs | 1 - integrations/axum/src/lib.rs | 30 +++++++++++++++-- server_fn/Cargo.toml | 5 ++- server_fn/src/codec/cbor.rs | 2 ++ server_fn/src/codec/json.rs | 2 ++ server_fn/src/codec/mod.rs | 2 ++ server_fn/src/codec/multipart.rs | 2 ++ server_fn/src/codec/rkyv.rs | 2 ++ server_fn/src/codec/stream.rs | 3 ++ server_fn/src/codec/url.rs | 3 ++ server_fn/src/lib.rs | 39 ++++++++++++++++------ server_fn_macro/src/lib.rs | 3 +- 13 files changed, 77 insertions(+), 17 deletions(-) diff --git a/examples/todo_app_sqlite_axum/Todos.db b/examples/todo_app_sqlite_axum/Todos.db index 263ef90a1294c8770642036723c764eba4591d78..536e991ee4b5fb5365ee0ded31402ceaaf029a99 100644 GIT binary patch delta 79 zcmZo@U~Fh$oFL7pFj2;tQDI}kLViL1mkccYH(B_9@;~7J#DA0j=4L^I&HR&(%8N*| g@~|-Qb2zhdf+&{~MFs{2b|By0o|Sv^7kM590E^EQ5C8xG delta 79 zcmZo@U~Fh$oFL68Gf~EwQD$SpLViI81_l=X2Q2(Q`9JYL;D5>ga<ibpX8y@X<wc}( ha`HJ@Sr}M3Ss3^^TuKyKxj+nOdwW*)&0pks6aaSm6e$1z diff --git a/examples/todo_app_sqlite_axum/src/main.rs b/examples/todo_app_sqlite_axum/src/main.rs index a21789c36e..62631b84f2 100644 --- a/examples/todo_app_sqlite_axum/src/main.rs +++ b/examples/todo_app_sqlite_axum/src/main.rs @@ -52,7 +52,6 @@ cfg_if! { // build our application with a route let app = Router::new() - .route("/api/*fn_name", post(leptos_axum::handle_server_fns)) .route("/special/:id", get(custom_handler)) .leptos_routes(&leptos_options, routes, || view! { <TodoApp/> } ) .fallback(file_and_error_handler) diff --git a/integrations/axum/src/lib.rs b/integrations/axum/src/lib.rs index b90abfa219..d0fb8a7186 100644 --- a/integrations/axum/src/lib.rs +++ b/integrations/axum/src/lib.rs @@ -39,11 +39,10 @@ use axum::{ http::{ header::{self, HeaderName, HeaderValue}, request::Parts, - HeaderMap, Request, Response, StatusCode, + HeaderMap, Method, Request, Response, StatusCode, }, response::IntoResponse, routing::{delete, get, patch, post, put}, - RequestPartsExt, }; use futures::{ channel::mpsc::{Receiver, Sender}, @@ -1540,6 +1539,8 @@ where IV: IntoView + 'static, { let mut router = self; + + // register router paths for listing in paths.iter() { let path = listing.path(); @@ -1631,6 +1632,31 @@ where }; } } + + // register server functions + for (path, method) in server_fn::axum::server_fn_paths() { + let additional_context = additional_context.clone(); + let handler = move |req: Request<Body>| async move { + handle_server_fns_with_context(additional_context, req).await + }; + router = router.route( + path, + match method { + Method::GET => get(handler), + Method::POST => post(handler), + Method::PUT => put(handler), + Method::DELETE => delete(handler), + Method::PATCH => patch(handler), + _ => { + panic!( + "Unsupported server function HTTP method: \ + {method:?}" + ); + } + }, + ); + } + router } diff --git a/server_fn/Cargo.toml b/server_fn/Cargo.toml index 8fb8edca25..498443cfc4 100644 --- a/server_fn/Cargo.toml +++ b/server_fn/Cargo.toml @@ -34,7 +34,7 @@ multer = { version = "3", optional = true } # serde serde_json = "1" futures = "0.3" -http = { version = "1", optional = true } +http = { version = "1" } ciborium = { version = "0.2", optional = true } hyper = { version = "1", optional = true } bytes = "1" @@ -67,7 +67,6 @@ reqwest = { version = "0.11", default-features = false, optional = true, feature actix = ["dep:actix-web", "dep:send_wrapper"] axum = [ "dep:axum", - "dep:http", "dep:hyper", "dep:http-body-util", "dep:tower", @@ -88,5 +87,5 @@ cbor = ["dep:ciborium"] rkyv = ["dep:rkyv"] default-tls = ["reqwest/default-tls"] rustls = ["reqwest/rustls-tls"] -reqwest = ["dep:http", "dep:reqwest"] +reqwest = ["dep:reqwest"] ssr = ["inventory"] diff --git a/server_fn/src/codec/cbor.rs b/server_fn/src/codec/cbor.rs index e9acb50f56..9f0e8d6ab7 100644 --- a/server_fn/src/codec/cbor.rs +++ b/server_fn/src/codec/cbor.rs @@ -5,6 +5,7 @@ use crate::{ response::{ClientRes, Res}, }; use bytes::Bytes; +use http::Method; use serde::{de::DeserializeOwned, Serialize}; /// Pass arguments and receive responses using `cbor` in a `POST` request. @@ -12,6 +13,7 @@ pub struct Cbor; impl Encoding for Cbor { const CONTENT_TYPE: &'static str = "application/cbor"; + const METHOD: Method = Method::POST; } impl<CustErr, T, Request> IntoReq<CustErr, Request, Cbor> for T diff --git a/server_fn/src/codec/json.rs b/server_fn/src/codec/json.rs index f90f420a6d..0dcb062d21 100644 --- a/server_fn/src/codec/json.rs +++ b/server_fn/src/codec/json.rs @@ -5,12 +5,14 @@ use crate::{ response::{ClientRes, Res}, IntoReq, IntoRes, }; +use http::Method; use serde::{de::DeserializeOwned, Serialize}; /// Pass arguments and receive responses as JSON in the body of a `POST` request. pub struct Json; impl Encoding for Json { const CONTENT_TYPE: &'static str = "application/json"; + const METHOD: Method = Method::POST; } impl<CustErr, T, Request> IntoReq<CustErr, Request, Json> for T diff --git a/server_fn/src/codec/mod.rs b/server_fn/src/codec/mod.rs index 9fba70edf5..426f24b6b6 100644 --- a/server_fn/src/codec/mod.rs +++ b/server_fn/src/codec/mod.rs @@ -4,6 +4,7 @@ mod cbor; pub use cbor::*; #[cfg(feature = "json")] mod json; +use http::Method; #[cfg(feature = "json")] pub use json::*; #[cfg(feature = "rkyv")] @@ -59,6 +60,7 @@ pub trait IntoRes<CustErr, Response, Encoding> { pub trait Encoding { const CONTENT_TYPE: &'static str; + const METHOD: Method; } pub trait FormDataEncoding<Client, CustErr, Request> diff --git a/server_fn/src/codec/multipart.rs b/server_fn/src/codec/multipart.rs index aa11fcd6f6..a3c254e6ed 100644 --- a/server_fn/src/codec/multipart.rs +++ b/server_fn/src/codec/multipart.rs @@ -5,6 +5,7 @@ use crate::{ IntoReq, }; use futures::StreamExt; +use http::Method; use multer::Multipart; use web_sys::FormData; @@ -12,6 +13,7 @@ pub struct MultipartFormData; impl Encoding for MultipartFormData { const CONTENT_TYPE: &'static str = "multipart/form-data"; + const METHOD: Method = Method::POST; } #[derive(Debug)] diff --git a/server_fn/src/codec/rkyv.rs b/server_fn/src/codec/rkyv.rs index 02c4aae2aa..17435d342b 100644 --- a/server_fn/src/codec/rkyv.rs +++ b/server_fn/src/codec/rkyv.rs @@ -5,6 +5,7 @@ use crate::{ response::{ClientRes, Res}, }; use bytes::Bytes; +use http::Method; use rkyv::{ de::deserializers::SharedDeserializeMap, ser::serializers::AllocSerializer, validation::validators::DefaultValidator, Archive, CheckBytes, Deserialize, @@ -16,6 +17,7 @@ pub struct Rkyv; impl Encoding for Rkyv { const CONTENT_TYPE: &'static str = "application/rkyv"; + const METHOD: Method = Method::POST; } impl<CustErr, T, Request> IntoReq<CustErr, Request, Rkyv> for T diff --git a/server_fn/src/codec/stream.rs b/server_fn/src/codec/stream.rs index 8158261ac4..020bfdb546 100644 --- a/server_fn/src/codec/stream.rs +++ b/server_fn/src/codec/stream.rs @@ -6,12 +6,14 @@ use crate::{ }; use bytes::Bytes; use futures::{Stream, StreamExt}; +use http::Method; use std::pin::Pin; pub struct Streaming; impl Encoding for Streaming { const CONTENT_TYPE: &'static str = "application/octet-stream"; + const METHOD: Method = Method::POST; } /* impl<CustErr, T, Request> IntoReq<CustErr, Request, ByteStream> for T @@ -81,6 +83,7 @@ pub struct StreamingText; impl Encoding for StreamingText { const CONTENT_TYPE: &'static str = "text/plain"; + const METHOD: Method = Method::POST; } pub struct TextStream<CustErr = NoCustomError>( diff --git a/server_fn/src/codec/url.rs b/server_fn/src/codec/url.rs index e133066e28..e75953c6ad 100644 --- a/server_fn/src/codec/url.rs +++ b/server_fn/src/codec/url.rs @@ -3,6 +3,7 @@ use crate::{ error::ServerFnError, request::{ClientReq, Req}, }; +use http::Method; use serde::{de::DeserializeOwned, Serialize}; /// Pass arguments as a URL-encoded query string of a `GET` request. @@ -13,6 +14,7 @@ pub struct PostUrl; impl Encoding for GetUrl { const CONTENT_TYPE: &'static str = "application/x-www-form-urlencoded"; + const METHOD: Method = Method::GET; } impl<CustErr, T, Request> IntoReq<CustErr, Request, GetUrl> for T @@ -46,6 +48,7 @@ where impl Encoding for PostUrl { const CONTENT_TYPE: &'static str = "application/x-www-form-urlencoded"; + const METHOD: Method = Method::POST; } impl<CustErr, T, Request> IntoReq<CustErr, Request, PostUrl> for T diff --git a/server_fn/src/lib.rs b/server_fn/src/lib.rs index 68c712dbf6..2078af8f4c 100644 --- a/server_fn/src/lib.rs +++ b/server_fn/src/lib.rs @@ -20,6 +20,7 @@ pub use const_format; use dashmap::DashMap; pub use error::ServerFnError; use error::ServerFnErrorSerde; +use http::Method; use middleware::{Layer, Service}; use once_cell::sync::Lazy; use request::Req; @@ -155,7 +156,7 @@ macro_rules! initialize_server_fn_map { once_cell::sync::Lazy::new(|| { $crate::inventory::iter::<ServerFnTraitObj<$req, $res>> .into_iter() - .map(|obj| (obj.path(), *obj)) + .map(|obj| (obj.path(), obj.clone())) .collect() }) }; @@ -163,6 +164,7 @@ macro_rules! initialize_server_fn_map { pub struct ServerFnTraitObj<Req, Res> { path: &'static str, + method: Method, handler: fn(Req) -> Pin<Box<dyn Future<Output = Res> + Send>>, middleware: fn() -> Vec<Arc<dyn Layer<Req, Res>>>, } @@ -170,11 +172,13 @@ pub struct ServerFnTraitObj<Req, Res> { impl<Req, Res> ServerFnTraitObj<Req, Res> { pub const fn new( path: &'static str, + method: Method, handler: fn(Req) -> Pin<Box<dyn Future<Output = Res> + Send>>, middleware: fn() -> Vec<Arc<dyn Layer<Req, Res>>>, ) -> Self { Self { path, + method, handler, middleware, } @@ -183,6 +187,10 @@ impl<Req, Res> ServerFnTraitObj<Req, Res> { pub fn path(&self) -> &'static str { self.path } + + pub fn method(&self) -> Method { + self.method.clone() + } } impl<Req, Res> Service<Req, Res> for ServerFnTraitObj<Req, Res> @@ -198,12 +206,15 @@ where impl<Req, Res> Clone for ServerFnTraitObj<Req, Res> { fn clone(&self) -> Self { - *self + Self { + path: self.path, + method: self.method.clone(), + handler: self.handler, + middleware: self.middleware, + } } } -impl<Req, Res> Copy for ServerFnTraitObj<Req, Res> {} - type LazyServerFnMap<Req, Res> = Lazy<DashMap<&'static str, ServerFnTraitObj<Req, Res>>>; @@ -212,10 +223,10 @@ type LazyServerFnMap<Req, Res> = pub mod axum { use crate::{ middleware::{BoxedService, Layer, Service}, - LazyServerFnMap, ServerFn, ServerFnTraitObj, + Encoding, LazyServerFnMap, ServerFn, ServerFnTraitObj, }; use axum::body::Body; - use http::{Request, Response, StatusCode}; + use http::{Method, Request, Response, StatusCode}; inventory::collect!(ServerFnTraitObj<Request<Body>, Response<Body>>); @@ -235,12 +246,19 @@ pub mod axum { T::PATH, ServerFnTraitObj::new( T::PATH, + T::InputEncoding::METHOD, |req| Box::pin(T::run_on_server(req)), T::middlewares, ), ); } + pub fn server_fn_paths() -> impl Iterator<Item = (&'static str, Method)> { + REGISTERED_SERVER_FUNCTIONS + .iter() + .map(|item| (item.path(), item.method())) + } + pub async fn handle_server_fn(req: Request<Body>) -> Response<Body> { let path = req.uri().path(); @@ -268,7 +286,7 @@ pub mod axum { ) -> Option<BoxedService<Request<Body>, Response<Body>>> { REGISTERED_SERVER_FUNCTIONS.get(path).map(|server_fn| { let middleware = (server_fn.middleware)(); - let mut service = BoxedService::new(*server_fn); + let mut service = BoxedService::new(server_fn.clone()); for middleware in middleware { service = middleware.layer(service); } @@ -282,11 +300,10 @@ pub mod axum { pub mod actix { use crate::{ middleware::BoxedService, request::actix::ActixRequest, - response::actix::ActixResponse, LazyServerFnMap, ServerFn, + response::actix::ActixResponse, Encoding, LazyServerFnMap, ServerFn, ServerFnTraitObj, }; use actix_web::{HttpRequest, HttpResponse}; - use send_wrapper::SendWrapper; inventory::collect!(ServerFnTraitObj<ActixRequest, ActixResponse>); @@ -306,6 +323,7 @@ pub mod actix { T::PATH, ServerFnTraitObj::new( T::PATH, + T::InputEncoding::METHOD, |req| Box::pin(T::run_on_server(req)), T::middlewares, ), @@ -316,7 +334,8 @@ pub mod actix { let path = req.uri().path(); if let Some(server_fn) = REGISTERED_SERVER_FUNCTIONS.get(path) { let middleware = (server_fn.middleware)(); - let mut service = BoxedService::new(*server_fn); + // http::Method is the only non-Copy type here + let mut service = BoxedService::new(server_fn.clone()); for middleware in middleware { service = middleware.layer(service); } diff --git a/server_fn_macro/src/lib.rs b/server_fn_macro/src/lib.rs index bbc4249782..06fdae874d 100644 --- a/server_fn_macro/src/lib.rs +++ b/server_fn_macro/src/lib.rs @@ -237,9 +237,10 @@ pub fn server_macro_impl( let inventory = if cfg!(feature = "ssr") { quote! { #server_fn_path::inventory::submit! {{ - use #server_fn_path::ServerFn; + use #server_fn_path::{ServerFn, codec::Encoding}; #server_fn_path::ServerFnTraitObj::new( #struct_name::PATH, + <#struct_name as ServerFn>::InputEncoding::METHOD, |req| { Box::pin(#struct_name::run_on_server(req)) }, From db4158f5c36b4e58d8756e1cc85ba61d2242244a Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Thu, 4 Jan 2024 17:11:17 -0500 Subject: [PATCH 016/100] clear up warnings --- server_fn/src/codec/mod.rs | 2 +- server_fn/src/error.rs | 14 +++++++------- server_fn/src/lib.rs | 11 +++++++---- server_fn/src/redirect.rs | 13 +++++++++---- server_fn/src/request/axum.rs | 5 ++--- server_fn/src/request/mod.rs | 14 ++++---------- server_fn/src/request/reqwest.rs | 12 ++++++------ server_fn/src/response/actix.rs | 19 +++++++++++++------ server_fn/src/response/mod.rs | 18 +++++++++++------- server_fn_macro/src/lib.rs | 18 +++++++----------- 10 files changed, 67 insertions(+), 59 deletions(-) diff --git a/server_fn/src/codec/mod.rs b/server_fn/src/codec/mod.rs index 426f24b6b6..aacb15953d 100644 --- a/server_fn/src/codec/mod.rs +++ b/server_fn/src/codec/mod.rs @@ -13,7 +13,7 @@ mod rkyv; pub use rkyv::*; #[cfg(feature = "url")] mod url; -use crate::{client::Client, error::ServerFnError, request::ClientReq}; +use crate::{error::ServerFnError, request::ClientReq}; use futures::Future; #[cfg(feature = "url")] pub use url::*; diff --git a/server_fn/src/error.rs b/server_fn/src/error.rs index a36c4e275b..ec0d4f38c4 100644 --- a/server_fn/src/error.rs +++ b/server_fn/src/error.rs @@ -116,9 +116,9 @@ pub(crate) trait ServerFnErrorKind {} impl ServerFnErrorKind for ServerFnError {} // This impl should catch passing () or nothing to server_fn_error -impl ViaError<()> for &&&WrapError<()> { - fn to_server_error(&self) -> ServerFnError<()> { - ServerFnError::WrappedServerError(self.0.clone()) +impl ViaError<NoCustomError> for &&&WrapError<()> { + fn to_server_error(&self) -> ServerFnError { + ServerFnError::WrappedServerError(NoCustomError) } } @@ -225,7 +225,7 @@ where } pub trait ServerFnErrorSerde: Sized { - fn ser(&self) -> String; + fn ser(&self) -> Result<String, std::fmt::Error>; fn de(data: &str) -> Self; } @@ -234,7 +234,7 @@ impl<CustErr> ServerFnErrorSerde for ServerFnError<CustErr> where CustErr: FromStr + Display, { - fn ser(&self) -> String { + fn ser(&self) -> Result<String, std::fmt::Error> { let mut buf = String::new(); match self { ServerFnError::WrappedServerError(e) => { @@ -258,8 +258,8 @@ where ServerFnError::MissingArg(e) => { write!(&mut buf, "MissingArg|{}", e) } - }; - buf + }?; + Ok(buf) } fn de(data: &str) -> Self { diff --git a/server_fn/src/lib.rs b/server_fn/src/lib.rs index 2078af8f4c..2a6179a78b 100644 --- a/server_fn/src/lib.rs +++ b/server_fn/src/lib.rs @@ -162,11 +162,13 @@ macro_rules! initialize_server_fn_map { }; } +pub type MiddlewareSet<Req, Res> = Vec<Arc<dyn Layer<Req, Res>>>; + pub struct ServerFnTraitObj<Req, Res> { path: &'static str, method: Method, handler: fn(Req) -> Pin<Box<dyn Future<Output = Res> + Send>>, - middleware: fn() -> Vec<Arc<dyn Layer<Req, Res>>>, + middleware: fn() -> MiddlewareSet<Req, Res>, } impl<Req, Res> ServerFnTraitObj<Req, Res> { @@ -174,7 +176,7 @@ impl<Req, Res> ServerFnTraitObj<Req, Res> { path: &'static str, method: Method, handler: fn(Req) -> Pin<Box<dyn Future<Output = Res> + Send>>, - middleware: fn() -> Vec<Arc<dyn Layer<Req, Res>>>, + middleware: fn() -> MiddlewareSet<Req, Res>, ) -> Self { Self { path, @@ -215,6 +217,7 @@ impl<Req, Res> Clone for ServerFnTraitObj<Req, Res> { } } +#[allow(unused)] // used by server integrations type LazyServerFnMap<Req, Res> = Lazy<DashMap<&'static str, ServerFnTraitObj<Req, Res>>>; @@ -222,7 +225,7 @@ type LazyServerFnMap<Req, Res> = #[cfg(feature = "axum")] pub mod axum { use crate::{ - middleware::{BoxedService, Layer, Service}, + middleware::{BoxedService, Service}, Encoding, LazyServerFnMap, ServerFn, ServerFnTraitObj, }; use axum::body::Body; @@ -262,7 +265,7 @@ pub mod axum { pub async fn handle_server_fn(req: Request<Body>) -> Response<Body> { let path = req.uri().path(); - if let Some(mut service) = get_server_fn_service(&path) { + if let Some(mut service) = get_server_fn_service(path) { service.run(req).await } else { Response::builder() diff --git a/server_fn/src/redirect.rs b/server_fn/src/redirect.rs index 67a9cfec62..178ef3bec6 100644 --- a/server_fn/src/redirect.rs +++ b/server_fn/src/redirect.rs @@ -1,10 +1,15 @@ use std::sync::OnceLock; -static REDIRECT_HOOK: OnceLock<Box<dyn Fn(&str) + Send + Sync>> = - OnceLock::new(); +pub type RedirectHook = Box<dyn Fn(&str) + Send + Sync>; -pub fn set_redirect_hook(hook: impl Fn(&str) + Send + Sync + 'static) { - REDIRECT_HOOK.set(Box::new(hook)); +// allowed: not in a public API, and pretty straightforward +#[allow(clippy::type_complexity)] +static REDIRECT_HOOK: OnceLock<RedirectHook> = OnceLock::new(); + +pub fn set_redirect_hook( + hook: impl Fn(&str) + Send + Sync + 'static, +) -> Result<(), RedirectHook> { + REDIRECT_HOOK.set(Box::new(hook)) } pub fn call_redirect_hook(path: &str) { diff --git a/server_fn/src/request/axum.rs b/server_fn/src/request/axum.rs index 41f3230627..799c82ecc3 100644 --- a/server_fn/src/request/axum.rs +++ b/server_fn/src/request/axum.rs @@ -26,9 +26,8 @@ impl<CustErr> Req<CustErr> for Request<Body> { async fn try_into_string(self) -> Result<String, ServerFnError<CustErr>> { let bytes = self.try_into_bytes().await?; - let body = String::from_utf8(bytes.to_vec()) - .map_err(|e| ServerFnError::Deserialization(e.to_string())); - body + String::from_utf8(bytes.to_vec()) + .map_err(|e| ServerFnError::Deserialization(e.to_string())) } fn try_into_stream( diff --git a/server_fn/src/request/mod.rs b/server_fn/src/request/mod.rs index 500b853efe..a62349eac7 100644 --- a/server_fn/src/request/mod.rs +++ b/server_fn/src/request/mod.rs @@ -90,18 +90,12 @@ impl<CustErr> Req<CustErr> for BrowserMockReq { unreachable!() } - fn try_into_bytes( - self, - ) -> impl Future<Output = Result<Bytes, ServerFnError<CustErr>>> + Send - { - async { unreachable!() } + async fn try_into_bytes(self) -> Result<Bytes, ServerFnError<CustErr>> { + unreachable!() } - fn try_into_string( - self, - ) -> impl Future<Output = Result<String, ServerFnError<CustErr>>> + Send - { - async { unreachable!() } + async fn try_into_string(self) -> Result<String, ServerFnError<CustErr>> { + unreachable!() } fn try_into_stream( diff --git a/server_fn/src/request/reqwest.rs b/server_fn/src/request/reqwest.rs index c705f966ee..c1ecb649fe 100644 --- a/server_fn/src/request/reqwest.rs +++ b/server_fn/src/request/reqwest.rs @@ -51,13 +51,13 @@ impl<CustErr> ClientReq<CustErr> for Request { body: String, ) -> Result<Self, ServerFnError<CustErr>> { let url = format!("{}{}", get_server_url(), path); - Ok(CLIENT + CLIENT .post(url) .header(CONTENT_TYPE, content_type) .header(ACCEPT, accepts) .body(body) .build() - .map_err(|e| ServerFnError::Request(e.to_string()))?) + .map_err(|e| ServerFnError::Request(e.to_string())) } fn try_new_post_bytes( @@ -67,13 +67,13 @@ impl<CustErr> ClientReq<CustErr> for Request { body: Bytes, ) -> Result<Self, ServerFnError<CustErr>> { let url = format!("{}{}", get_server_url(), path); - Ok(CLIENT + CLIENT .post(url) .header(CONTENT_TYPE, content_type) .header(ACCEPT, accepts) .body(body) .build() - .map_err(|e| ServerFnError::Request(e.to_string()))?) + .map_err(|e| ServerFnError::Request(e.to_string())) } fn try_new_multipart( @@ -81,11 +81,11 @@ impl<CustErr> ClientReq<CustErr> for Request { accepts: &str, body: Self::FormData, ) -> Result<Self, ServerFnError<CustErr>> { - Ok(CLIENT + CLIENT .post(path) .header(ACCEPT, accepts) .multipart(body) .build() - .map_err(|e| ServerFnError::Request(e.to_string()))?) + .map_err(|e| ServerFnError::Request(e.to_string())) } } diff --git a/server_fn/src/response/actix.rs b/server_fn/src/response/actix.rs index 97581999d7..f4014cd533 100644 --- a/server_fn/src/response/actix.rs +++ b/server_fn/src/response/actix.rs @@ -1,13 +1,13 @@ use super::Res; -use crate::error::ServerFnError; +use crate::error::{ServerFnError, ServerFnErrorErr}; use actix_web::{ http::{header, StatusCode}, HttpResponse, }; use bytes::Bytes; -use futures::Stream; +use futures::{Stream, StreamExt}; use send_wrapper::SendWrapper; -use std::fmt::Display; +use std::fmt::{Debug, Display}; pub struct ActixResponse(pub(crate) SendWrapper<HttpResponse>); @@ -19,7 +19,7 @@ impl ActixResponse { impl<CustErr> Res<CustErr> for ActixResponse where - CustErr: Display, + CustErr: Display + Debug + 'static, { fn try_from_string( content_type: &str, @@ -54,8 +54,15 @@ where fn try_from_stream( content_type: &str, - data: impl Stream<Item = Result<Bytes, ServerFnError<CustErr>>>, + data: impl Stream<Item = Result<Bytes, ServerFnError<CustErr>>> + 'static, ) -> Result<Self, ServerFnError<CustErr>> { - todo!() + 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)), + ), + ))) } } diff --git a/server_fn/src/response/mod.rs b/server_fn/src/response/mod.rs index e49f58e549..6458a05820 100644 --- a/server_fn/src/response/mod.rs +++ b/server_fn/src/response/mod.rs @@ -72,30 +72,34 @@ pub trait ClientRes<CustErr> { /// A mocked response type that can be used in place of the actual server response, /// when compiling for the browser. +/// +/// ## Panics +/// This always panics if its methods are called. It is used solely to stub out the +/// server response type when compiling for the client. pub struct BrowserMockRes; impl<CustErr> Res<CustErr> for BrowserMockRes { fn try_from_string( - content_type: &str, - data: String, + _content_type: &str, + _data: String, ) -> Result<Self, ServerFnError<CustErr>> { unreachable!() } fn try_from_bytes( - content_type: &str, - data: Bytes, + _content_type: &str, + _data: Bytes, ) -> Result<Self, ServerFnError<CustErr>> { unreachable!() } - fn error_response(err: ServerFnError<CustErr>) -> Self { + fn error_response(_err: ServerFnError<CustErr>) -> Self { unreachable!() } fn try_from_stream( - content_type: &str, - data: impl Stream<Item = Result<Bytes, ServerFnError<CustErr>>>, + _content_type: &str, + _data: impl Stream<Item = Result<Bytes, ServerFnError<CustErr>>>, ) -> Result<Self, ServerFnError<CustErr>> { todo!() } diff --git a/server_fn_macro/src/lib.rs b/server_fn_macro/src/lib.rs index 06fdae874d..8ce1c8efe0 100644 --- a/server_fn_macro/src/lib.rs +++ b/server_fn_macro/src/lib.rs @@ -158,14 +158,10 @@ pub fn server_macro_impl( .collect::<Vec<_>>(); // if there's exactly one field, impl From<T> for the struct - let first_field = body - .inputs - .iter() - .filter_map(|f| match f { - FnArg::Receiver(_) => None, - FnArg::Typed(t) => Some((&t.pat, &t.ty)), - }) - .next(); + let first_field = body.inputs.iter().find_map(|f| match f { + FnArg::Receiver(_) => None, + FnArg::Typed(t) => Some((&t.pat, &t.ty)), + }); let from_impl = (body.inputs.len() == 1 && first_field.is_some()).then(|| { let field = first_field.unwrap(); @@ -698,7 +694,7 @@ struct ServerFnBody { pub fn_token: Token![fn], pub ident: Ident, pub generics: Generics, - pub paren_token: token::Paren, + pub _paren_token: token::Paren, pub inputs: Punctuated<FnArg, Token![,]>, pub output_arrow: Token![->], pub return_ty: syn::Type, @@ -718,7 +714,7 @@ impl Parse for ServerFnBody { let generics: Generics = input.parse()?; let content; - let paren_token = syn::parenthesized!(content in input); + let _paren_token = syn::parenthesized!(content in input); let inputs = syn::punctuated::Punctuated::parse_terminated(&content)?; @@ -761,7 +757,7 @@ impl Parse for ServerFnBody { fn_token, ident, generics, - paren_token, + _paren_token, inputs, output_arrow, return_ty, From 60efaefff41fb0fd1fda83d34d22ab20d4800d6d Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Thu, 4 Jan 2024 17:23:09 -0500 Subject: [PATCH 017/100] start Actix work --- examples/todo_app_sqlite/Cargo.toml | 1 + integrations/actix/Cargo.toml | 2 ++ server_fn/Cargo.toml | 4 +++- 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/examples/todo_app_sqlite/Cargo.toml b/examples/todo_app_sqlite/Cargo.toml index f3977ca2c4..c5d2ccdf64 100644 --- a/examples/todo_app_sqlite/Cargo.toml +++ b/examples/todo_app_sqlite/Cargo.toml @@ -20,6 +20,7 @@ leptos = { path = "../../leptos", features = ["nightly"] } leptos_actix = { path = "../../integrations/actix", optional = true } leptos_meta = { path = "../../meta", features = ["nightly"] } leptos_router = { path = "../../router", features = ["nightly"] } +server_fn = { path = "../../server_fn", features = ["cbor"] } log = "0.4.17" simple_logger = "4.0.0" gloo = { git = "https://github.com/rustwasm/gloo" } diff --git a/integrations/actix/Cargo.toml b/integrations/actix/Cargo.toml index 8c21885a11..aafd392755 100644 --- a/integrations/actix/Cargo.toml +++ b/integrations/actix/Cargo.toml @@ -12,9 +12,11 @@ actix-http = "3" actix-web = "4" futures = "0.3" leptos = { workspace = true, features = ["ssr"] } +leptos_macro = { workspace = true, features = ["actix"] } leptos_meta = { workspace = true, features = ["ssr"] } leptos_router = { workspace = true, features = ["ssr"] } leptos_integration_utils = { workspace = true } +server_fn = { workspace = true, features = ["actix"] } serde_json = "1" parking_lot = "0.12.1" regex = "1.7.0" diff --git a/server_fn/Cargo.toml b/server_fn/Cargo.toml index 498443cfc4..22f7d59515 100644 --- a/server_fn/Cargo.toml +++ b/server_fn/Cargo.toml @@ -64,8 +64,10 @@ reqwest = { version = "0.11", default-features = false, optional = true, feature ] } [features] -actix = ["dep:actix-web", "dep:send_wrapper"] +default = ["url", "json"] +actix = ["ssr", "dep:actix-web", "dep:send_wrapper"] axum = [ + "ssr", "dep:axum", "dep:hyper", "dep:http-body-util", From e1a9856ca96f304b8fc19752da05e025323409b6 Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Fri, 5 Jan 2024 09:04:46 -0500 Subject: [PATCH 018/100] more Actix work --- integrations/actix/src/lib.rs | 178 ++++---------------------------- integrations/axum/src/lib.rs | 1 - server_fn/src/lib.rs | 27 ++++- server_fn/src/middleware/mod.rs | 23 ++++- server_fn/src/request/actix.rs | 6 ++ server_fn/src/response/actix.rs | 8 +- server_fn_macro/src/lib.rs | 1 - 7 files changed, 78 insertions(+), 166 deletions(-) diff --git a/integrations/actix/src/lib.rs b/integrations/actix/src/lib.rs index 7b41d156c6..130457a92f 100644 --- a/integrations/actix/src/lib.rs +++ b/integrations/actix/src/lib.rs @@ -17,8 +17,6 @@ use actix_web::{ use futures::{Stream, StreamExt}; use http::StatusCode; use leptos::{ - leptos_server::{server_fn_by_path, Payload}, - server_fn::Encoding, ssr::render_to_stream_with_prefix_undisposed_with_context_and_block_replacement, *, }; @@ -27,6 +25,7 @@ use leptos_meta::*; use leptos_router::*; use parking_lot::RwLock; use regex::Regex; +use server_fn::request::actix::ActixRequest; use std::{ fmt::{Debug, Display}, future::Future, @@ -185,113 +184,34 @@ pub fn handle_server_fns_with_context( async move { let additional_context = additional_context.clone(); - let path = params.into_inner(); - let accept_header = req - .headers() - .get("Accept") - .and_then(|value| value.to_str().ok()); - - if let Some(server_fn) = server_fn_by_path(path.as_str()) { - let body_ref: &[u8] = &body; - + let path = req.path(); + if let Some(mut service) = + server_fn::actix::get_server_fn_service(path) + { let runtime = create_runtime(); // Add additional info to the context of the server function additional_context(); - let res_options = ResponseOptions::default(); - - // provide HttpRequest as context in server scope provide_context(req.clone()); - provide_context(res_options.clone()); - - // we consume the body here (using the web::Bytes extractor), but it is required for things - // like MultipartForm - if req - .headers() - .get("Content-Type") - .and_then(|value| value.to_str().ok()) - .map(|value| { - value.starts_with("multipart/form-data; boundary=") - }) - == Some(true) + let res_parts = ResponseOptions::default(); + provide_context(res_parts.clone()); + + let mut res = + service.0.run(ActixRequest::from(req)).await.take(); + + // Override StatusCode if it was set in a Resource or Element + if let Some(status) = res_parts.0.read().status { + *res.status_mut() = status; + } + + // Use provided ResponseParts headers if they exist + let headers = res.headers_mut(); + for (k, v) in + std::mem::take(&mut res_parts.0.write().headers) { - provide_context(body.clone()); + headers.append(k.clone(), v.clone()); } - let query = req.query_string().as_bytes(); - - let data = match &server_fn.encoding() { - Encoding::Url | Encoding::Cbor => body_ref, - Encoding::GetJSON | Encoding::GetCBOR => query, - }; - - let res = match server_fn.call((), data).await { - Ok(serialized) => { - let res_options = - use_context::<ResponseOptions>().unwrap(); - - let mut res: HttpResponseBuilder = - HttpResponse::Ok(); - let res_parts = res_options.0.write(); - - // if accept_header isn't set to one of these, it's a form submit - // redirect back to the referrer if not redirect has been set - if accept_header != Some("application/json") - && accept_header - != Some("application/x-www-form-urlencoded") - && accept_header != Some("application/cbor") - { - // Location will already be set if redirect() has been used - let has_location_set = - res_parts.headers.get("Location").is_some(); - if !has_location_set { - let referer = req - .headers() - .get("Referer") - .and_then(|value| value.to_str().ok()) - .unwrap_or("/"); - res = HttpResponse::SeeOther(); - res.insert_header(("Location", referer)) - .content_type("application/json"); - } - }; - // Override StatusCode if it was set in a Resource or Element - if let Some(status) = res_parts.status { - res.status(status); - } - - // Use provided ResponseParts headers if they exist - let _count = res_parts - .headers - .clone() - .into_iter() - .map(|(k, v)| { - res.append_header((k, v)); - }) - .count(); - - match serialized { - Payload::Binary(data) => { - res.content_type("application/cbor"); - res.body(Bytes::from(data)) - } - Payload::Url(data) => { - res.content_type( - "application/x-www-form-urlencoded", - ); - res.body(data) - } - Payload::Json(data) => { - res.content_type("application/json"); - res.body(data) - } - } - } - Err(e) => HttpResponse::InternalServerError().body( - serde_json::to_string(&e) - .unwrap_or_else(|_| e.to_string()), - ), - }; // clean up the scope runtime.dispose(); res @@ -1388,62 +1308,6 @@ impl LeptosRoutes for &mut ServiceConfig { } } -/// A helper to make it easier to use Actix extractors in server functions. This takes -/// a handler function as its argument. The handler follows similar rules to an Actix -/// [Handler]: it is an async function that receives arguments that -/// will be extracted from the request and returns some value. -/// -/// ```rust,ignore -/// use leptos::*; -/// use serde::Deserialize; -/// #[derive(Deserialize)] -/// struct Search { -/// q: String, -/// } -/// -/// #[server(ExtractoServerFn, "/api")] -/// pub async fn extractor_server_fn() -> Result<String, ServerFnError> { -/// use actix_web::dev::ConnectionInfo; -/// use actix_web::web::{Data, Query}; -/// -/// extract( -/// |data: Data<String>, search: Query<Search>, connection: ConnectionInfo| async move { -/// format!( -/// "data = {}\nsearch = {}\nconnection = {:?}", -/// data.into_inner(), -/// search.q, -/// connection -/// ) -/// }, -/// ) -/// .await -/// } -/// ``` -pub async fn extract<F, E>( - f: F, -) -> Result<<<F as Extractor<E>>::Future as Future>::Output, ServerFnError> -where - F: Extractor<E>, - E: actix_web::FromRequest, - <E as actix_web::FromRequest>::Error: Display, - <F as Extractor<E>>::Future: Future, -{ - let req = use_context::<actix_web::HttpRequest>() - .expect("HttpRequest should have been provided via context"); - - let input = if let Some(body) = use_context::<Bytes>() { - let (_, mut payload) = actix_http::h1::Payload::create(false); - payload.unread_data(body); - E::from_request(&req, &mut dev::Payload::from(payload)) - } else { - E::extract(&req) - } - .await - .map_err(|e| ServerFnError::ServerError(e.to_string()))?; - - Ok(f.call(input).await) -} - /// A helper to make it easier to use Axum extractors in server functions, with a /// simpler API than [`extract()`]. /// diff --git a/integrations/axum/src/lib.rs b/integrations/axum/src/lib.rs index d0fb8a7186..99f42a1a0d 100644 --- a/integrations/axum/src/lib.rs +++ b/integrations/axum/src/lib.rs @@ -242,7 +242,6 @@ async fn handle_server_fns_inner( additional_context(); provide_context(parts); - // Add this so that we can set headers and status of the response provide_context(ResponseOptions::default()); let mut res = service.run(req).await; diff --git a/server_fn/src/lib.rs b/server_fn/src/lib.rs index 2a6179a78b..ffb45ed6c1 100644 --- a/server_fn/src/lib.rs +++ b/server_fn/src/lib.rs @@ -307,6 +307,9 @@ pub mod actix { ServerFnTraitObj, }; use actix_web::{HttpRequest, HttpResponse}; + use http::Method; + #[doc(hidden)] + pub use send_wrapper::SendWrapper; inventory::collect!(ServerFnTraitObj<ActixRequest, ActixResponse>); @@ -333,6 +336,12 @@ pub mod actix { ); } + pub fn server_fn_paths() -> impl Iterator<Item = (&'static str, Method)> { + REGISTERED_SERVER_FUNCTIONS + .iter() + .map(|item| (item.path(), item.method())) + } + pub async fn handle_server_fn(req: HttpRequest) -> HttpResponse { let path = req.uri().path(); if let Some(server_fn) = REGISTERED_SERVER_FUNCTIONS.get(path) { @@ -343,11 +352,6 @@ pub mod actix { service = middleware.layer(service); } service.0.run(ActixRequest::from(req)).await.0.take() - /*server_fn - .run(ActixRequest(SendWrapper::new(req))) - .await - .0 - .take()*/ } else { HttpResponse::BadRequest().body(format!( "Could not find a server function at the route {path}. \ @@ -361,4 +365,17 @@ pub mod actix { )) } } + + pub fn get_server_fn_service( + path: &str, + ) -> Option<BoxedService<ActixRequest, ActixResponse>> { + REGISTERED_SERVER_FUNCTIONS.get(path).map(|server_fn| { + let middleware = (server_fn.middleware)(); + let mut service = BoxedService::new(server_fn.clone()); + for middleware in middleware { + service = middleware.layer(service); + } + service + }) + } } diff --git a/server_fn/src/middleware/mod.rs b/server_fn/src/middleware/mod.rs index 7c28720e7d..71d5dbd8e5 100644 --- a/server_fn/src/middleware/mod.rs +++ b/server_fn/src/middleware/mod.rs @@ -97,6 +97,7 @@ mod axum { #[cfg(feature = "actix")] mod actix { use crate::{ + request::actix::ActixRequest, response::{actix::ActixResponse, Res}, ServerFnError, }; @@ -121,9 +122,29 @@ mod actix { Box::pin(async move { inner.await.unwrap_or_else(|e| { let err = ServerFnError::from(e); - ActixResponse::error_response(err).into_inner() + ActixResponse::error_response(err).take() }) }) } } + + impl<S> super::Service<ActixRequest, ActixResponse> for S + where + S: actix_web::dev::Service<HttpRequest, Response = HttpResponse>, + S::Future: Send + 'static, + S::Error: Into<ServerFnError> + Debug + Display + 'static, + { + fn run( + &mut self, + req: ActixRequest, + ) -> Pin<Box<dyn Future<Output = ActixResponse> + Send>> { + let inner = self.call(req.0.take()); + Box::pin(async move { + ActixResponse::from(inner.await.unwrap_or_else(|e| { + let err = ServerFnError::from(e); + ActixResponse::error_response(err).take() + })) + }) + } + } } diff --git a/server_fn/src/request/actix.rs b/server_fn/src/request/actix.rs index c8f2213d21..fa526a6dfb 100644 --- a/server_fn/src/request/actix.rs +++ b/server_fn/src/request/actix.rs @@ -7,6 +7,12 @@ use std::future::Future; pub struct ActixRequest(pub(crate) SendWrapper<HttpRequest>); +impl ActixRequest { + pub fn take(self) -> HttpRequest { + self.0.take() + } +} + impl From<HttpRequest> for ActixRequest { fn from(value: HttpRequest) -> Self { ActixRequest(SendWrapper::new(value)) diff --git a/server_fn/src/response/actix.rs b/server_fn/src/response/actix.rs index f4014cd533..2f7f5a234c 100644 --- a/server_fn/src/response/actix.rs +++ b/server_fn/src/response/actix.rs @@ -12,11 +12,17 @@ use std::fmt::{Debug, Display}; pub struct ActixResponse(pub(crate) SendWrapper<HttpResponse>); impl ActixResponse { - pub fn into_inner(self) -> HttpResponse { + pub fn take(self) -> HttpResponse { self.0.take() } } +impl From<HttpResponse> for ActixResponse { + fn from(value: HttpResponse) -> Self { + Self(SendWrapper::new(value)) + } +} + impl<CustErr> Res<CustErr> for ActixResponse where CustErr: Display + Debug + 'static, diff --git a/server_fn_macro/src/lib.rs b/server_fn_macro/src/lib.rs index 8ce1c8efe0..c554dad8fb 100644 --- a/server_fn_macro/src/lib.rs +++ b/server_fn_macro/src/lib.rs @@ -308,7 +308,6 @@ pub fn server_macro_impl( #server_fn_path::client::browser::BrowserClient }; - // TODO Actix etc let req = if !cfg!(feature = "ssr") { quote! { #server_fn_path::request::BrowserMockReq From c8fbee18c8e98cd819b2d591c2def74fca64c530 Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Fri, 5 Jan 2024 15:34:39 -0500 Subject: [PATCH 019/100] finished Actix support? --- examples/todo_app_sqlite/Todos.db | Bin 16384 -> 16384 bytes examples/todo_app_sqlite/src/main.rs | 2 +- integrations/actix/src/lib.rs | 191 ++++++++++----------------- server_fn/src/lib.rs | 22 ++- server_fn/src/middleware/mod.rs | 2 +- server_fn/src/request/actix.rs | 23 ++-- server_fn_macro/src/lib.rs | 33 ++++- 7 files changed, 128 insertions(+), 145 deletions(-) diff --git a/examples/todo_app_sqlite/Todos.db b/examples/todo_app_sqlite/Todos.db index 96bed43b88e9f79a4da0efc6b7fdee030e1dc87a..0fc78f30813410cfeff0c9d6c1192bf1dcc7c589 100644 GIT binary patch delta 69 zcmZo@U~Fh$oFL6OX`+lX<D`uVOZa(M_@6NF|K$I~|C;~FW&wdK{4%U8EDZb{F09NT S%9)i3L^-iCf+)vD3JL(Uq7TUc delta 55 zcmZo@U~Fh$oFL8EHBrWyv1?<(5`G>A0R{&C5B%5o_wn!8EFf@&e{zEy|Kz)J>}-+w L>7_*_ixd<9?G6$N diff --git a/examples/todo_app_sqlite/src/main.rs b/examples/todo_app_sqlite/src/main.rs index b0b14bd2ce..03b1906edb 100644 --- a/examples/todo_app_sqlite/src/main.rs +++ b/examples/todo_app_sqlite/src/main.rs @@ -46,7 +46,7 @@ cfg_if! { App::new() .service(css) - .route("/api/{tail:.*}", leptos_actix::handle_server_fns()) +// .route("/api/{tail:.*}", leptos_actix::handle_server_fns()) .leptos_routes(leptos_options.to_owned(), routes.to_owned(), TodoApp) .service(Files::new("/", site_root)) //.wrap(middleware::Compress::default()) diff --git a/integrations/actix/src/lib.rs b/integrations/actix/src/lib.rs index 130457a92f..cd4e12ef2f 100644 --- a/integrations/actix/src/lib.rs +++ b/integrations/actix/src/lib.rs @@ -11,7 +11,7 @@ use actix_web::{ body::BoxBody, dev::{ServiceFactory, ServiceRequest}, http::header, - web::{Bytes, ServiceConfig}, + web::{Payload, ServiceConfig}, *, }; use futures::{Stream, StreamExt}; @@ -178,60 +178,59 @@ pub fn handle_server_fns() -> Route { pub fn handle_server_fns_with_context( additional_context: impl Fn() + 'static + Clone + Send, ) -> Route { - web::to( - move |req: HttpRequest, params: web::Path<String>, body: web::Bytes| { + web::to(move |req: HttpRequest, payload: Payload| { + let additional_context = additional_context.clone(); + async move { let additional_context = additional_context.clone(); - async move { - let additional_context = additional_context.clone(); - - let path = req.path(); - if let Some(mut service) = - server_fn::actix::get_server_fn_service(path) - { - let runtime = create_runtime(); - - // Add additional info to the context of the server function - additional_context(); - provide_context(req.clone()); - let res_parts = ResponseOptions::default(); - provide_context(res_parts.clone()); - - let mut res = - service.0.run(ActixRequest::from(req)).await.take(); - - // Override StatusCode if it was set in a Resource or Element - if let Some(status) = res_parts.0.read().status { - *res.status_mut() = status; - } - // Use provided ResponseParts headers if they exist - let headers = res.headers_mut(); - for (k, v) in - std::mem::take(&mut res_parts.0.write().headers) - { - headers.append(k.clone(), v.clone()); - } + let path = req.path(); + if let Some(mut service) = + server_fn::actix::get_server_fn_service(path) + { + let runtime = create_runtime(); + + // Add additional info to the context of the server function + additional_context(); + provide_context(req.clone()); + let res_parts = ResponseOptions::default(); + provide_context(res_parts.clone()); + + let mut res = service + .0 + .run(ActixRequest::from((req, payload))) + .await + .take(); + + // Override StatusCode if it was set in a Resource or Element + if let Some(status) = res_parts.0.read().status { + *res.status_mut() = status; + } - // clean up the scope - runtime.dispose(); - res - } else { - HttpResponse::BadRequest().body(format!( - "Could not find a server function at the route {:?}. \ - \n\nIt's likely that either - 1. The API prefix you specify in the `#[server]` \ - macro doesn't match the prefix at which your server \ - function handler is mounted, or \n2. You are on a \ - platform that doesn't support automatic server \ - function registration and you need to call \ - ServerFn::register_explicit() on the server function \ - type, somewhere in your `main` function.", - req.path() - )) + // Use provided ResponseParts headers if they exist + let headers = res.headers_mut(); + for (k, v) in std::mem::take(&mut res_parts.0.write().headers) { + headers.append(k.clone(), v.clone()); } + + // clean up the scope + runtime.dispose(); + res + } else { + HttpResponse::BadRequest().body(format!( + "Could not find a server function at the route {:?}. \ + \n\nIt's likely that either + 1. The API prefix you specify in the `#[server]` \ + macro doesn't match the prefix at which your server \ + function handler is mounted, or \n2. You are on a \ + platform that doesn't support automatic server function \ + registration and you need to call \ + ServerFn::register_explicit() on the server function \ + type, somewhere in your `main` function.", + req.path() + )) } - }, - ) + } + }) } /// Returns an Actix [struct@Route](actix_web::Route) that listens for a `GET` request and tries @@ -1229,6 +1228,14 @@ where }; } } + + // register server functions + for (path, _) in server_fn::actix::server_fn_paths() { + let additional_context = additional_context.clone(); + let handler = handle_server_fns_with_context(additional_context); + router = router.route(path, handler); + } + router } } @@ -1322,82 +1329,22 @@ impl LeptosRoutes for &mut ServiceConfig { /// pub async fn query_extract() -> Result<MyQuery, ServerFnError> { /// use actix_web::web::Query; /// use leptos_actix::*; -/// let Query(data) = extractor().await?; +/// let Query(data) = extract().await?; /// Ok(data) /// } /// ``` -pub async fn extractor<T>() -> Result<T, ServerFnError> +pub async fn extract<T, CustErr>() -> Result<T, ServerFnError<CustErr>> where T: actix_web::FromRequest, - <T as FromRequest>::Error: Debug, + <T as FromRequest>::Error: Display, { - let req = use_context::<actix_web::HttpRequest>() - .expect("HttpRequest should have been provided via context"); - - if let Some(body) = use_context::<Bytes>() { - let (_, mut payload) = actix_http::h1::Payload::create(false); - payload.unread_data(body); - T::from_request(&req, &mut dev::Payload::from(payload)) - } else { - T::extract(&req) - } - .await - .map_err(|e| ServerFnError::ServerError(format!("{e:?}"))) + let req = use_context::<HttpRequest>().ok_or_else(|| { + ServerFnError::ServerError( + "HttpRequest should have been provided via context".to_string(), + ) + })?; + + T::extract(&req) + .await + .map_err(|e| ServerFnError::ServerError(e.to_string())) } - -/// A macro that makes it easier to use extractors in server functions. The macro -/// takes a type or types, and extracts them from the request, returning from the -/// server function with an `Err(_)` if there is an error during extraction. -/// ```rust,ignore -/// let info = extract!(ConnectionInfo); -/// let Query(data) = extract!(Query<Search>); -/// let (info, Query(data)) = extract!(ConnectionInfo, Query<Search>); -/// ``` -#[macro_export] -macro_rules! extract { - ($($x:ty),+) => { - $crate::extract(|fields: ($($x),+)| async move { fields }).await? - }; -} - -// Drawn from the Actix Handler implementation -// https://github.com/actix/actix-web/blob/19c9d858f25e8262e14546f430d713addb397e96/actix-web/src/handler.rs#L124 -pub trait Extractor<T> { - type Future; - - fn call(self, args: T) -> Self::Future; -} - -macro_rules! factory_tuple ({ $($param:ident)* } => { - impl<Func, Fut, $($param,)*> Extractor<($($param,)*)> for Func - where - Func: FnOnce($($param),*) -> Fut + Clone + 'static, - Fut: Future, - { - type Future = Fut; - - #[inline] - #[allow(non_snake_case)] - fn call(self, ($($param,)*): ($($param,)*)) -> Self::Future { - (self)($($param,)*) - } - } -}); - -factory_tuple! {} -factory_tuple! { A } -factory_tuple! { A B } -factory_tuple! { A B C } -factory_tuple! { A B C D } -factory_tuple! { A B C D E } -factory_tuple! { A B C D E F } -factory_tuple! { A B C D E F G } -factory_tuple! { A B C D E F G H } -factory_tuple! { A B C D E F G H I } -factory_tuple! { A B C D E F G H I J } -factory_tuple! { A B C D E F G H I J K } -factory_tuple! { A B C D E F G H I J K L } -factory_tuple! { A B C D E F G H I J K L M } -factory_tuple! { A B C D E F G H I J K L M N } -factory_tuple! { A B C D E F G H I J K L M N O } -factory_tuple! { A B C D E F G H I J K L M N O P } diff --git a/server_fn/src/lib.rs b/server_fn/src/lib.rs index ffb45ed6c1..a8a0ee85cc 100644 --- a/server_fn/src/lib.rs +++ b/server_fn/src/lib.rs @@ -288,11 +288,11 @@ pub mod axum { path: &str, ) -> Option<BoxedService<Request<Body>, Response<Body>>> { REGISTERED_SERVER_FUNCTIONS.get(path).map(|server_fn| { - let middleware = (server_fn.middleware)(); + //let middleware = (server_fn.middleware)(); let mut service = BoxedService::new(server_fn.clone()); - for middleware in middleware { - service = middleware.layer(service); - } + //for middleware in middleware { + //service = middleware.layer(service); + //} service }) } @@ -306,7 +306,7 @@ pub mod actix { response::actix::ActixResponse, Encoding, LazyServerFnMap, ServerFn, ServerFnTraitObj, }; - use actix_web::{HttpRequest, HttpResponse}; + use actix_web::{web::Payload, HttpRequest, HttpResponse}; use http::Method; #[doc(hidden)] pub use send_wrapper::SendWrapper; @@ -342,7 +342,10 @@ pub mod actix { .map(|item| (item.path(), item.method())) } - pub async fn handle_server_fn(req: HttpRequest) -> HttpResponse { + pub async fn handle_server_fn( + req: HttpRequest, + payload: Payload, + ) -> HttpResponse { let path = req.uri().path(); if let Some(server_fn) = REGISTERED_SERVER_FUNCTIONS.get(path) { let middleware = (server_fn.middleware)(); @@ -351,7 +354,12 @@ pub mod actix { for middleware in middleware { service = middleware.layer(service); } - service.0.run(ActixRequest::from(req)).await.0.take() + service + .0 + .run(ActixRequest::from((req, payload))) + .await + .0 + .take() } else { HttpResponse::BadRequest().body(format!( "Could not find a server function at the route {path}. \ diff --git a/server_fn/src/middleware/mod.rs b/server_fn/src/middleware/mod.rs index 71d5dbd8e5..cbc5b0c55b 100644 --- a/server_fn/src/middleware/mod.rs +++ b/server_fn/src/middleware/mod.rs @@ -138,7 +138,7 @@ mod actix { &mut self, req: ActixRequest, ) -> Pin<Box<dyn Future<Output = ActixResponse> + Send>> { - let inner = self.call(req.0.take()); + let inner = self.call(req.0.take().0); Box::pin(async move { ActixResponse::from(inner.await.unwrap_or_else(|e| { let err = ServerFnError::from(e); diff --git a/server_fn/src/request/actix.rs b/server_fn/src/request/actix.rs index fa526a6dfb..b5de1d6408 100644 --- a/server_fn/src/request/actix.rs +++ b/server_fn/src/request/actix.rs @@ -1,31 +1,32 @@ use crate::{error::ServerFnError, request::Req}; -use actix_web::{FromRequest, HttpRequest}; +use actix_web::{web::Payload, HttpRequest}; use bytes::Bytes; use futures::Stream; use send_wrapper::SendWrapper; use std::future::Future; -pub struct ActixRequest(pub(crate) SendWrapper<HttpRequest>); +pub struct ActixRequest(pub(crate) SendWrapper<(HttpRequest, Payload)>); impl ActixRequest { - pub fn take(self) -> HttpRequest { + pub fn take(self) -> (HttpRequest, Payload) { self.0.take() } } -impl From<HttpRequest> for ActixRequest { - fn from(value: HttpRequest) -> Self { +impl From<(HttpRequest, Payload)> for ActixRequest { + fn from(value: (HttpRequest, Payload)) -> Self { ActixRequest(SendWrapper::new(value)) } } impl<CustErr> Req<CustErr> for ActixRequest { fn as_query(&self) -> Option<&str> { - self.0.uri().query() + self.0 .0.uri().query() } fn to_content_type(&self) -> Option<String> { self.0 + .0 .headers() .get("Content-Type") .map(|h| String::from_utf8_lossy(h.as_bytes()).to_string()) @@ -38,7 +39,9 @@ impl<CustErr> Req<CustErr> for ActixRequest { // 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 { - Bytes::extract(&self.0) + let payload = self.0.take().1; + payload + .to_bytes() .await .map_err(|e| ServerFnError::Deserialization(e.to_string())) }) @@ -51,8 +54,12 @@ impl<CustErr> Req<CustErr> for ActixRequest { // 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 { - String::extract(&self.0) + 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())) }) } diff --git a/server_fn_macro/src/lib.rs b/server_fn_macro/src/lib.rs index c554dad8fb..0131860000 100644 --- a/server_fn_macro/src/lib.rs +++ b/server_fn_macro/src/lib.rs @@ -238,6 +238,7 @@ pub fn server_macro_impl( #struct_name::PATH, <#struct_name as ServerFn>::InputEncoding::METHOD, |req| { + println!("running {:?}", stringify!(#struct_name)); Box::pin(#struct_name::run_on_server(req)) }, #struct_name::middlewares @@ -250,10 +251,30 @@ pub fn server_macro_impl( // run_body in the trait implementation let run_body = if cfg!(feature = "ssr") { + // using the impl Future syntax here is thanks to Actix + // + // if we use Actix types inside the function, here, it becomes !Send + // so we need to add SendWrapper, because Actix won't actually send it anywhere + // but if we used SendWrapper in an async fn, the types don't work out because it + // becomes impl Future<Output = SendWrapper<_>> + // + // however, SendWrapper<Future<Output = T>> impls Future<Output = T> + let body = quote! { + let #struct_name { #(#field_names),* } = self; + #dummy_name(#(#field_names),*).await + }; + let body = if cfg!(feature = "actix") { + quote! { + #server_fn_path::actix::SendWrapper::new(async move { + #body + }) + } + } else { + body + }; quote! { - async fn run_body(self) -> #return_ty { - let #struct_name { #(#field_names),* } = self; - #dummy_name(#(#field_names),*).await + fn run_body(self) -> impl std::future::Future<Output = #return_ty> + Send { + #body } } } else { @@ -271,7 +292,7 @@ pub fn server_macro_impl( #docs #(#attrs)* #vis async fn #fn_name(#(#fn_args),*) #output_arrow #return_ty { - #block + #dummy_name(#(#field_names),*).await } } } else { @@ -318,7 +339,7 @@ pub fn server_macro_impl( } } else if cfg!(feature = "actix") { quote! { - #server_fn_path::actix_export::HttpRequest + #server_fn_path::request::actix::ActixRequest } } else { return Err(syn::Error::new( @@ -337,7 +358,7 @@ pub fn server_macro_impl( } } else if cfg!(feature = "actix") { quote! { - #server_fn_path::actix_export::HttpResponse + #server_fn_path::response::actix::ActixResponse } } else { return Err(syn::Error::new( From fd97e2e027c3a476844cd8f686bf2008b91a9dab Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Fri, 5 Jan 2024 16:28:37 -0500 Subject: [PATCH 020/100] Restore the previous full functionality of Form --- examples/todo_app_sqlite/Todos.db | Bin 16384 -> 16384 bytes router/src/components/form.rs | 47 ++++++++++++------------------ 2 files changed, 19 insertions(+), 28 deletions(-) diff --git a/examples/todo_app_sqlite/Todos.db b/examples/todo_app_sqlite/Todos.db index 0fc78f30813410cfeff0c9d6c1192bf1dcc7c589..9942abb2ddf8f508b6b84b026be4e647088e8ded 100644 GIT binary patch delta 39 vcmZo@U~Fh$oFL6OWulBT<CKjF8~GXUZWa<a&Ckxjz`)MW;kx;Wyo>?>`#=kV delta 39 vcmZo@U~Fh$oFL6OX`+lX<D`uV8~GV$Y!(tY&Cgz1so<1Tn!5Rkyo>?>3TF*$ diff --git a/router/src/components/form.rs b/router/src/components/form.rs index ffa50ab9ce..c34a33d6c1 100644 --- a/router/src/components/form.rs +++ b/router/src/components/form.rs @@ -2,9 +2,14 @@ use crate::{ hooks::has_router, use_navigate, use_resolved_path, NavigateOptions, ToHref, Url, }; -use leptos::{html::form, logging::*, server_fn::ServerFn, *}; +use leptos::{ + html::form, + logging::*, + server_fn::{error::ServerFnErrorSerde, ServerFn}, + *, +}; use serde::{de::DeserializeOwned, Serialize}; -use std::{error::Error, rc::Rc}; +use std::{error::Error, fmt::Debug, rc::Rc}; use wasm_bindgen::{JsCast, UnwrapThrowExt}; use wasm_bindgen_futures::JsFuture; use web_sys::RequestRedirect; @@ -415,7 +420,7 @@ pub fn ActionForm<I, O, Enc>( /// The action from which to build the form. This should include a URL, which can be generated /// by default using [`create_server_action`](l:eptos_server::create_server_action) or added /// manually using [`using_server_fn`](leptos_server::Action::using_server_fn). - action: Action<I, Result<O, ServerFnError>>, + action: Action<I, Result<O, ServerFnError<I::Error>>>, /// Sets the `class` attribute on the underlying `<form>` tag, making it easier to style. #[prop(optional, into)] class: Option<AttributeValue>, @@ -437,7 +442,8 @@ pub fn ActionForm<I, O, Enc>( where I: Clone + DeserializeOwned + ServerFn<InputEncoding = Enc> + 'static, O: Clone + Serialize + DeserializeOwned + 'static, - ServerFnError<I::Error>: Clone, // Enc: FormDataEncoding, + ServerFnError<I::Error>: Debug + Clone, // Enc: FormDataEncoding, + I::Error: Debug + 'static, { let action_url = if let Some(url) = action.url() { url @@ -453,15 +459,14 @@ where let input = action.input(); let on_error = Rc::new(move |e: &gloo_net::Error| { - // TODO - /* batch(move || { + batch(move || { action.set_pending(false); let e = ServerFnError::Request(e.to_string()); value.try_set(Some(Err(e.clone()))); if let Some(error) = error { error.try_set(Some(Box::new(ServerFnErrorErr::from(e)))); } - });*/ + }); }); let on_form_data = Rc::new(move |form_data: &web_sys::FormData| { @@ -474,8 +479,7 @@ where }); } Err(e) => { - // TODO - /* error!("{e}"); + error!("{e}"); let e = ServerFnError::Serialization(e.to_string()); batch(move || { value.try_set(Some(Err(e.clone()))); @@ -483,7 +487,7 @@ where error .try_set(Some(Box::new(ServerFnErrorErr::from(e)))); } - }); */ + }); } } }); @@ -508,24 +512,11 @@ where let json = json .as_string() .expect("couldn't get String from JsString"); - if (500..=599).contains(&status) { - match serde_json::from_str::<ServerFnError>(&json) { - Ok(res) => { - value.try_set(Some(Err(res))); - if let Some(error) = error { - error.try_set(None); - } - } - Err(e) => { - value.try_set(Some(Err( - ServerFnError::Deserialization( - e.to_string(), - ), - ))); - if let Some(error) = error { - error.try_set(Some(Box::new(e))); - } - } + if (400..=599).contains(&status) { + let res = ServerFnError::<I::Error>::de(&json); + value.try_set(Some(Err(res))); + if let Some(error) = error { + error.try_set(None); } } else { match serde_json::from_str::<O>(&json) { From 566df034ff0331a3905cb67682e082fd147bfe6c Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Fri, 5 Jan 2024 17:50:02 -0500 Subject: [PATCH 021/100] actually use server functions in `ActionForm` --- examples/todo_app_sqlite/Todos.db | Bin 16384 -> 16384 bytes leptos/Cargo.toml | 5 ++ router/src/components/form.rs | 145 ++++++++++++++++++++++++------ server_fn/src/lib.rs | 18 ++-- server_fn/src/request/browser.rs | 28 +++++- server_fn/src/request/mod.rs | 7 ++ server_fn/src/request/reqwest.rs | 15 ++++ 7 files changed, 185 insertions(+), 33 deletions(-) diff --git a/examples/todo_app_sqlite/Todos.db b/examples/todo_app_sqlite/Todos.db index 9942abb2ddf8f508b6b84b026be4e647088e8ded..75df92c61b2a092a5699ba63b8abfd2f86ea785f 100644 GIT binary patch delta 90 zcmZo@U~Fh$oFL7(XrhcW<D!iT3;BilpD?iSUt{2Z&3~8wC;v4N+$<t+h+m79n}va& j!;O^#L^<<+VqoXzaA9Qy@f;Z#Ks+Z_E)dUkk%9sMjl2|D delta 87 zcmZo@U~Fh$oFL6OWulBT<CKjF3;Bf^7#LXipD^(M<p0F~n*Ry^UH%!HMFbA<Pd*^W kt_+l9=jU)`Wnp39=Wt<V22sweOd!gMl@UZaE>chc03$~eH~;_u diff --git a/leptos/Cargo.toml b/leptos/Cargo.toml index 78f47ad3b5..bbc0ddc4bc 100644 --- a/leptos/Cargo.toml +++ b/leptos/Cargo.toml @@ -74,6 +74,10 @@ experimental-islands = [ "dep:serde", "dep:serde_json", ] +trace-component-props = [ + "leptos_dom/trace-component-props", + "leptos_macro/trace-component-props" +] [package.metadata.cargo-all-features] denylist = [ @@ -83,6 +87,7 @@ denylist = [ "rustls", "default-tls", "wasm-bindgen", + "trace-component-props" ] skip_feature_sets = [ [ diff --git a/router/src/components/form.rs b/router/src/components/form.rs index c34a33d6c1..27c1723770 100644 --- a/router/src/components/form.rs +++ b/router/src/components/form.rs @@ -5,14 +5,22 @@ use crate::{ use leptos::{ html::form, logging::*, - server_fn::{error::ServerFnErrorSerde, ServerFn}, + server_fn::{ + client::Client, + codec::{Encoding, PostUrl}, + request::ClientReq, + ServerFn, + }, *, }; use serde::{de::DeserializeOwned, Serialize}; use std::{error::Error, fmt::Debug, rc::Rc}; use wasm_bindgen::{JsCast, UnwrapThrowExt}; use wasm_bindgen_futures::JsFuture; -use web_sys::RequestRedirect; +use web_sys::{ + FormData, HtmlButtonElement, HtmlFormElement, HtmlInputElement, + RequestRedirect, SubmitEvent, +}; type OnFormData = Rc<dyn Fn(&web_sys::FormData)>; type OnResponse = Rc<dyn Fn(&web_sys::Response)>; @@ -416,11 +424,14 @@ fn current_window_origin() -> String { tracing::instrument(level = "trace", skip_all,) )] #[component] -pub fn ActionForm<I, O, Enc>( +pub fn ActionForm<ServFn>( /// The action from which to build the form. This should include a URL, which can be generated /// by default using [`create_server_action`](l:eptos_server::create_server_action) or added /// manually using [`using_server_fn`](leptos_server::Action::using_server_fn). - action: Action<I, Result<O, ServerFnError<I::Error>>>, + action: Action< + ServFn, + Result<ServFn::Output, ServerFnError<ServFn::Error>>, + >, /// Sets the `class` attribute on the underlying `<form>` tag, making it easier to style. #[prop(optional, into)] class: Option<AttributeValue>, @@ -440,11 +451,15 @@ pub fn ActionForm<I, O, Enc>( children: Children, ) -> impl IntoView where - I: Clone + DeserializeOwned + ServerFn<InputEncoding = Enc> + 'static, - O: Clone + Serialize + DeserializeOwned + 'static, - ServerFnError<I::Error>: Debug + Clone, // Enc: FormDataEncoding, - I::Error: Debug + 'static, + ServFn: + Clone + DeserializeOwned + ServerFn<InputEncoding = PostUrl> + 'static, + ServerFnError<ServFn::Error>: Debug + Clone, + ServFn::Error: Debug + 'static, + <<ServFn::Client as Client<ServFn::Error>>::Request as ClientReq< + ServFn::Error, + >>::FormData: From<FormData>, { + let has_router = has_router(); let action_url = if let Some(url) = action.url() { url } else { @@ -458,7 +473,81 @@ where let value = action.value(); let input = action.input(); - let on_error = Rc::new(move |e: &gloo_net::Error| { + let class = class.map(|bx| bx.into_attribute_boxed()); + + let on_submit = { + let action_url = action_url.clone(); + move |ev: SubmitEvent| { + if ev.default_prevented() { + return; + } + + ev.prevent_default(); + + let navigate = has_router.then(use_navigate); + let navigate_options = NavigateOptions { + scroll: !noscroll, + ..Default::default() + }; + + let form = + form_from_event(&ev).expect("couldn't find form submitter"); + let form_data = FormData::new_with_form(&form).unwrap(); + let req = <<ServFn::Client as Client<ServFn::Error>>::Request as ClientReq< + ServFn::Error, + >>::try_new_post_form_data( + &action_url, + ServFn::OutputEncoding::CONTENT_TYPE, + ServFn::InputEncoding::CONTENT_TYPE, + form_data.into(), + ); + match req { + Ok(req) => { + spawn_local(async move { + // TODO check order of setting things here, and use batch as needed + // TODO set version? + match <ServFn as ServerFn>::run_on_client_with_req(req) + .await + { + Ok(res) => { + batch(move || { + version.update(|n| *n += 1); + value.try_set(Some(Ok(res))); + }); + } + Err(err) => { + batch(move || { + value.set(Some(Err(err.clone()))); + if let Some(error) = error { + error.set(Some(Box::new( + ServerFnErrorErr::from(err), + ))); + } + }); + } + } + }); + } + Err(_) => todo!(), + } + } + }; + + let mut action_form = form() + .attr("action", action_url) + .attr("method", "post") + .attr("class", class) + .on(ev::submit, on_submit) + .child(children()); + if let Some(node_ref) = node_ref { + action_form = action_form.node_ref(node_ref) + }; + for (attr_name, attr_value) in attributes { + action_form = action_form.attr(attr_name, attr_value); + } + action_form + + /* let on_error = Rc::new(move |e: &gloo_net::Error| { batch(move || { action.set_pending(false); let e = ServerFnError::Request(e.to_string()); @@ -556,24 +645,7 @@ where action.set_pending(false); }); }); - }); - let class = class.map(|bx| bx.into_attribute_boxed()); - - let mut props = FormProps::builder() - .action(action_url) - .version(version) - .on_form_data(on_form_data) - .on_response(on_response) - .on_error(on_error) - .method("post") - .class(class) - .noscroll(noscroll) - .children(children) - .build(); - props.error = error; - props.node_ref = node_ref; - props.attributes = attributes; - Form(props) + });*/ } /// Automatically turns a server [MultiAction](leptos_server::MultiAction) into an HTML @@ -657,6 +729,25 @@ where } form } + +fn form_from_event(ev: &SubmitEvent) -> Option<HtmlFormElement> { + let submitter = ev.unchecked_ref::<SubmitEvent>().submitter(); + match &submitter { + Some(el) => { + if let Some(form) = el.dyn_ref::<HtmlFormElement>() { + Some(form.clone()) + } else if el.is_instance_of::<HtmlInputElement>() + || el.is_instance_of::<HtmlButtonElement>() + { + Some(ev.target().unwrap().unchecked_into()) + } else { + None + } + } + None => ev.target().map(|form| form.unchecked_into()), + } +} + #[cfg_attr( any(debug_assertions, feature = "ssr"), tracing::instrument(level = "trace", skip_all,) diff --git a/server_fn/src/lib.rs b/server_fn/src/lib.rs index a8a0ee85cc..250ea27a62 100644 --- a/server_fn/src/lib.rs +++ b/server_fn/src/lib.rs @@ -104,6 +104,15 @@ where // create and send request on client let req = self.into_req(Self::PATH, Self::OutputEncoding::CONTENT_TYPE)?; + Self::run_on_client_with_req(req).await + } + } + + fn run_on_client_with_req( + req: <Self::Client as Client<Self::Error>>::Request, + ) -> impl Future<Output = Result<Self::Output, ServerFnError<Self::Error>>> + Send + { + async move { let res = Self::Client::send(req).await?; let status = res.status(); @@ -122,7 +131,6 @@ where if (300..=399).contains(&status) { redirect::call_redirect_hook(&location); } - res } } @@ -288,11 +296,11 @@ pub mod axum { path: &str, ) -> Option<BoxedService<Request<Body>, Response<Body>>> { REGISTERED_SERVER_FUNCTIONS.get(path).map(|server_fn| { - //let middleware = (server_fn.middleware)(); + let middleware = (server_fn.middleware)(); let mut service = BoxedService::new(server_fn.clone()); - //for middleware in middleware { - //service = middleware.layer(service); - //} + for middleware in middleware { + service = middleware.layer(service); + } service }) } diff --git a/server_fn/src/request/browser.rs b/server_fn/src/request/browser.rs index 228feee1d8..1bff976688 100644 --- a/server_fn/src/request/browser.rs +++ b/server_fn/src/request/browser.rs @@ -4,7 +4,7 @@ use bytes::Bytes; pub use gloo_net::http::Request; use js_sys::Uint8Array; use send_wrapper::SendWrapper; -use web_sys::FormData; +use web_sys::{FormData, UrlSearchParams}; #[derive(Debug)] pub struct BrowserRequest(pub(crate) SendWrapper<Request>); @@ -89,4 +89,30 @@ impl<CustErr> ClientReq<CustErr> for BrowserRequest { .map_err(|e| ServerFnError::Request(e.to_string()))?, ))) } + + fn try_new_post_form_data( + path: &str, + accepts: &str, + content_type: &str, + body: Self::FormData, + ) -> Result<Self, ServerFnError<CustErr>> { + 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( + || { + "Could not serialize FormData to URLSearchParams" + .to_string() + }, + )) + })?; + Ok(Self(SendWrapper::new( + Request::post(path) + .header("Content-Type", content_type) + .header("Accept", accepts) + .body(url_params) + .map_err(|e| ServerFnError::Request(e.to_string()))?, + ))) + } } diff --git a/server_fn/src/request/mod.rs b/server_fn/src/request/mod.rs index a62349eac7..2b55d9ef22 100644 --- a/server_fn/src/request/mod.rs +++ b/server_fn/src/request/mod.rs @@ -40,6 +40,13 @@ where body: Bytes, ) -> Result<Self, ServerFnError<CustErr>>; + fn try_new_post_form_data( + path: &str, + accepts: &str, + content_type: &str, + body: Self::FormData, + ) -> Result<Self, ServerFnError<CustErr>>; + fn try_new_multipart( path: &str, accepts: &str, diff --git a/server_fn/src/request/reqwest.rs b/server_fn/src/request/reqwest.rs index c1ecb649fe..ced0c05d4c 100644 --- a/server_fn/src/request/reqwest.rs +++ b/server_fn/src/request/reqwest.rs @@ -88,4 +88,19 @@ impl<CustErr> ClientReq<CustErr> for Request { .build() .map_err(|e| ServerFnError::Request(e.to_string())) } + + fn try_new_post_form_data( + path: &str, + accepts: &str, + content_type: &str, + body: Self::FormData, + ) -> Result<Self, ServerFnError<CustErr>> { + /*CLIENT + .post(path) + .header(ACCEPT, accepts) + .multipart(body) + .build() + .map_err(|e| ServerFnError::Request(e.to_string()))*/ + todo!() + } } From 4e3f1c834ce57b9898000db85d0c13a35d3574eb Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Sat, 6 Jan 2024 11:46:47 -0500 Subject: [PATCH 022/100] handle client-side and server-side redirects correctly (in Axum) --- examples/todo_app_sqlite_axum/Todos.db | Bin 16384 -> 16384 bytes examples/todo_app_sqlite_axum/src/todo.rs | 1 + integrations/axum/src/lib.rs | 59 ++++++++++++++++++++-- router/Cargo.toml | 1 + router/src/components/form.rs | 30 +++++++++-- router/src/hooks.rs | 2 +- server_fn/src/lib.rs | 12 +++-- server_fn/src/redirect.rs | 4 +- server_fn/src/request/axum.rs | 1 + server_fn/src/response/browser.rs | 6 ++- server_fn/src/response/mod.rs | 3 ++ server_fn/src/response/reqwest.rs | 4 ++ server_fn_macro/src/lib.rs | 4 +- 13 files changed, 111 insertions(+), 16 deletions(-) diff --git a/examples/todo_app_sqlite_axum/Todos.db b/examples/todo_app_sqlite_axum/Todos.db index 536e991ee4b5fb5365ee0ded31402ceaaf029a99..3c695e04da231da42f617ca414e350f9ce9b1441 100644 GIT binary patch delta 124 zcmZo@U~Fh$oFL6;Fj2;t(O_f3B7T|E3@rRdnD~G4Kj6Q~e*_5k@^6E}^_vw1Ch|M7 zim)*7bNH|dgD7uSArR%oDhQ%H`Cl^d@pE{v@`HHptb8EKjg=Ebxs)g}Ffa)4bGUB) IATOW*0K<YB4FCWD delta 90 zcmZo@U~Fh$oFL7pFj2;tQDI}kB7T9F3@rRNS@?hQKj8nwf0O^_W<i6^{MuY>EDVwy q9;rDw`4*fkEDYj6HY*Pc13!l|D<_C@DN$r#U|<LGH-C^9PyhgBa}_oK diff --git a/examples/todo_app_sqlite_axum/src/todo.rs b/examples/todo_app_sqlite_axum/src/todo.rs index 1ae2092e35..b351aaac4f 100644 --- a/examples/todo_app_sqlite_axum/src/todo.rs +++ b/examples/todo_app_sqlite_axum/src/todo.rs @@ -77,6 +77,7 @@ pub async fn add_todo(title: String) -> Result<(), ServerFnError> { pub async fn delete_todo(id: u16) -> Result<(), ServerFnError> { let mut conn = db().await?; + leptos_axum::redirect("/foo"); Ok(sqlx::query("DELETE FROM todos WHERE id = $1") .bind(id) .execute(&mut conn) diff --git a/integrations/axum/src/lib.rs b/integrations/axum/src/lib.rs index 99f42a1a0d..9dfde6937c 100644 --- a/integrations/axum/src/lib.rs +++ b/integrations/axum/src/lib.rs @@ -37,7 +37,7 @@ use axum::{ body::{Body, Bytes}, extract::{FromRef, FromRequestParts, MatchedPath}, http::{ - header::{self, HeaderName, HeaderValue}, + header::{self, HeaderName, HeaderValue, ACCEPT, LOCATION, REFERER}, request::Parts, HeaderMap, Method, Request, Response, StatusCode, }, @@ -54,6 +54,7 @@ use leptos_meta::{generate_head_metadata_separated, MetaContext}; use leptos_router::*; use once_cell::sync::OnceCell; use parking_lot::RwLock; +use server_fn::redirect::REDIRECT_HEADER; use std::{ error::Error, fmt::Debug, io, pin::Pin, sync::Arc, thread::available_parallelism, @@ -114,13 +115,40 @@ impl ResponseOptions { /// it sets a StatusCode of 302 and a LOCATION header with the provided value. /// If looking to redirect from the client, `leptos_router::use_navigate()` should be used instead pub fn redirect(path: &str) { - if let Some(response_options) = use_context::<ResponseOptions>() { - response_options.set_status(StatusCode::FOUND); - response_options.insert_header( + if let (Some(req), Some(res)) = + (use_context::<Parts>(), use_context::<ResponseOptions>()) + { + // insert the Location header in any case + res.insert_header( header::LOCATION, header::HeaderValue::from_str(path) .expect("Failed to create HeaderValue"), ); + + let accepts_html = req + .headers + .get(ACCEPT) + .and_then(|v| v.to_str().ok()) + .map(|v| v.contains("text/html")) + .unwrap_or(false); + if accepts_html { + // if the request accepts text/html, it's a plain form request and needs + // to have the 302 code set + res.set_status(StatusCode::FOUND); + } else { + // otherwise, we sent it from the server fn client and actually don't want + // to set a real redirect, as this will break the ability to return data + // instead, set the REDIRECT_HEADER to indicate that the client should redirect + res.insert_header( + HeaderName::from_static(REDIRECT_HEADER), + HeaderValue::from_str("").unwrap(), + ); + } + } else { + tracing::warn!( + "Couldn't retrieve either Parts or ResponseOptions while trying \ + to redirect()." + ); } } @@ -244,13 +272,36 @@ async fn handle_server_fns_inner( provide_context(parts); provide_context(ResponseOptions::default()); + // store Accepts and Referer in case we need them for redirect (below) + let accepts_html = req + .headers() + .get(ACCEPT) + .and_then(|v| v.to_str().ok()) + .map(|v| v.contains("text/html")) + .unwrap_or(false); + let referrer = req.headers().get(REFERER).cloned(); + + // actually run the server fn let mut res = service.run(req).await; + // update response as needed let res_options = expect_context::<ResponseOptions>().0; let res_options_inner = res_options.read(); let (status, mut res_headers) = (res_options_inner.status, res_options_inner.headers.clone()); + // it it accepts text/html (i.e., is a plain form post) and doesn't already have a + // Location set, then redirect to to Referer + if accepts_html { + if let Some(referrer) = referrer { + let has_location = res.headers().get(LOCATION).is_some(); + if !has_location { + *res.status_mut() = StatusCode::FOUND; + res.headers_mut().insert(LOCATION, referrer); + } + } + } + // apply status code and headers if used changed them if let Some(status) = status { *res.status_mut() = status; diff --git a/router/Cargo.toml b/router/Cargo.toml index 568a8bd229..fedd13081a 100644 --- a/router/Cargo.toml +++ b/router/Cargo.toml @@ -31,6 +31,7 @@ wasm-bindgen-futures = { version = "0.4" } lru = { version = "0.11", optional = true } serde_json = "1.0.96" itertools = "0.12.0" +send_wrapper = "0.6.0" [dependencies.web-sys] version = "0.3" diff --git a/router/src/components/form.rs b/router/src/components/form.rs index 27c1723770..9781b9bf2c 100644 --- a/router/src/components/form.rs +++ b/router/src/components/form.rs @@ -8,15 +8,16 @@ use leptos::{ server_fn::{ client::Client, codec::{Encoding, PostUrl}, + redirect::RedirectHook, request::ClientReq, ServerFn, }, *, }; +use send_wrapper::SendWrapper; use serde::{de::DeserializeOwned, Serialize}; use std::{error::Error, fmt::Debug, rc::Rc}; use wasm_bindgen::{JsCast, UnwrapThrowExt}; -use wasm_bindgen_futures::JsFuture; use web_sys::{ FormData, HtmlButtonElement, HtmlFormElement, HtmlInputElement, RequestRedirect, SubmitEvent, @@ -454,6 +455,7 @@ where ServFn: Clone + DeserializeOwned + ServerFn<InputEncoding = PostUrl> + 'static, ServerFnError<ServFn::Error>: Debug + Clone, + ServFn::Output: Debug, ServFn::Error: Debug + 'static, <<ServFn::Client as Client<ServFn::Error>>::Request as ClientReq< ServFn::Error, @@ -485,10 +487,24 @@ where ev.prevent_default(); let navigate = has_router.then(use_navigate); - let navigate_options = NavigateOptions { + let navigate_options = SendWrapper::new(NavigateOptions { scroll: !noscroll, ..Default::default() - }; + }); + let redirect_hook = navigate.map(|navigate| { + let navigate = SendWrapper::new(navigate); + Box::new(move |path: &str| { + let path = path.to_string(); + // delay by a tick here, so that the Action updates *before* the redirect + request_animation_frame({ + let navigate = navigate.clone(); + let navigate_options = navigate_options.clone(); + move || { + navigate(&path, navigate_options.take()); + } + }); + }) as RedirectHook + }); let form = form_from_event(&ev).expect("couldn't find form submitter"); @@ -504,10 +520,14 @@ where match req { Ok(req) => { spawn_local(async move { + // TODO set input // TODO check order of setting things here, and use batch as needed // TODO set version? - match <ServFn as ServerFn>::run_on_client_with_req(req) - .await + match <ServFn as ServerFn>::run_on_client_with_req( + req, + redirect_hook.as_ref(), + ) + .await { Ok(res) => { batch(move || { diff --git a/router/src/hooks.rs b/router/src/hooks.rs index 37e9f99fd1..d9a9fc1236 100644 --- a/router/src/hooks.rs +++ b/router/src/hooks.rs @@ -190,7 +190,7 @@ pub fn use_resolved_path( /// # runtime.dispose(); /// ``` #[track_caller] -pub fn use_navigate() -> impl Fn(&str, NavigateOptions) { +pub fn use_navigate() -> impl Fn(&str, NavigateOptions) + Clone { let router = use_router(); move |to, options| { let router = Rc::clone(&router.inner); diff --git a/server_fn/src/lib.rs b/server_fn/src/lib.rs index 250ea27a62..add0f537fc 100644 --- a/server_fn/src/lib.rs +++ b/server_fn/src/lib.rs @@ -23,6 +23,7 @@ use error::ServerFnErrorSerde; use http::Method; use middleware::{Layer, Service}; use once_cell::sync::Lazy; +use redirect::RedirectHook; use request::Req; use response::{ClientRes, Res}; #[doc(hidden)] @@ -104,12 +105,14 @@ where // create and send request on client let req = self.into_req(Self::PATH, Self::OutputEncoding::CONTENT_TYPE)?; - Self::run_on_client_with_req(req).await + Self::run_on_client_with_req(req, redirect::REDIRECT_HOOK.get()) + .await } } fn run_on_client_with_req( req: <Self::Client as Client<Self::Error>>::Request, + redirect_hook: Option<&RedirectHook>, ) -> impl Future<Output = Result<Self::Output, ServerFnError<Self::Error>>> + Send { async move { @@ -117,6 +120,7 @@ where let status = res.status(); let location = res.location(); + let has_redirect_header = res.has_redirect(); // if it returns an error status, deserialize the error using FromStr let res = if (400..=599).contains(&status) { @@ -128,8 +132,10 @@ where }?; // if redirected, call the redirect hook (if that's been set) - if (300..=399).contains(&status) { - redirect::call_redirect_hook(&location); + if let Some(redirect_hook) = redirect_hook { + if (300..=399).contains(&status) || has_redirect_header { + redirect_hook(&location); + } } res } diff --git a/server_fn/src/redirect.rs b/server_fn/src/redirect.rs index 178ef3bec6..4eed28bab8 100644 --- a/server_fn/src/redirect.rs +++ b/server_fn/src/redirect.rs @@ -1,10 +1,12 @@ use std::sync::OnceLock; +pub const REDIRECT_HEADER: &str = "serverfnredirect"; + pub type RedirectHook = Box<dyn Fn(&str) + Send + Sync>; // allowed: not in a public API, and pretty straightforward #[allow(clippy::type_complexity)] -static REDIRECT_HOOK: OnceLock<RedirectHook> = OnceLock::new(); +pub(crate) static REDIRECT_HOOK: OnceLock<RedirectHook> = OnceLock::new(); pub fn set_redirect_hook( hook: impl Fn(&str) + Send + Sync + 'static, diff --git a/server_fn/src/request/axum.rs b/server_fn/src/request/axum.rs index 799c82ecc3..696f01e8fd 100644 --- a/server_fn/src/request/axum.rs +++ b/server_fn/src/request/axum.rs @@ -25,6 +25,7 @@ impl<CustErr> Req<CustErr> for Request<Body> { } async fn try_into_string(self) -> Result<String, ServerFnError<CustErr>> { + println!("accepts = {:?}", self.headers().get(http::header::ACCEPT)); let bytes = self.try_into_bytes().await?; String::from_utf8(bytes.to_vec()) .map_err(|e| ServerFnError::Deserialization(e.to_string())) diff --git a/server_fn/src/response/browser.rs b/server_fn/src/response/browser.rs index d1d5ebfdc0..845b2db54d 100644 --- a/server_fn/src/response/browser.rs +++ b/server_fn/src/response/browser.rs @@ -1,5 +1,5 @@ use super::ClientRes; -use crate::error::ServerFnError; +use crate::{error::ServerFnError, redirect::REDIRECT_HEADER}; use bytes::Bytes; use futures::{Stream, StreamExt}; pub use gloo_net::http::Response; @@ -73,4 +73,8 @@ impl<CustErr> ClientRes<CustErr> for BrowserResponse { .get("Location") .unwrap_or_else(|| self.0.url()) } + + fn has_redirect(&self) -> bool { + self.0.headers().get(REDIRECT_HEADER).is_some() + } } diff --git a/server_fn/src/response/mod.rs b/server_fn/src/response/mod.rs index 6458a05820..7fc101de65 100644 --- a/server_fn/src/response/mod.rs +++ b/server_fn/src/response/mod.rs @@ -68,6 +68,9 @@ pub trait ClientRes<CustErr> { /// The `Location` header or (if none is set), the URL of the response. fn location(&self) -> String; + + /// Whether the response has the [`REDIRECT_HEADER`](crate::redirect::REDIRECT_HEADER) set. + fn has_redirect(&self) -> bool; } /// A mocked response type that can be used in place of the actual server response, diff --git a/server_fn/src/response/reqwest.rs b/server_fn/src/response/reqwest.rs index d3c697cb04..f60338e48d 100644 --- a/server_fn/src/response/reqwest.rs +++ b/server_fn/src/response/reqwest.rs @@ -42,4 +42,8 @@ impl<CustErr> ClientRes<CustErr> for Response { .map(|value| String::from_utf8_lossy(value.as_bytes()).to_string()) .unwrap_or_else(|| self.url().to_string()) } + + fn has_redirect(&self) -> bool { + self.headers().get("Location").is_some() + } } diff --git a/server_fn_macro/src/lib.rs b/server_fn_macro/src/lib.rs index 0131860000..2ab30af579 100644 --- a/server_fn_macro/src/lib.rs +++ b/server_fn_macro/src/lib.rs @@ -270,7 +270,9 @@ pub fn server_macro_impl( }) } } else { - body + quote! { async move { + #body + }} }; quote! { fn run_body(self) -> impl std::future::Future<Output = #return_ty> + Send { From 5e6f4403ca46051a24430c17b6f053fa442c9b33 Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Sat, 6 Jan 2024 18:11:11 -0500 Subject: [PATCH 023/100] set up redirects in Actix --- integrations/actix/src/lib.rs | 56 +++++++++++++++++++++++++++++++---- 1 file changed, 51 insertions(+), 5 deletions(-) diff --git a/integrations/actix/src/lib.rs b/integrations/actix/src/lib.rs index cd4e12ef2f..7aaf0ade72 100644 --- a/integrations/actix/src/lib.rs +++ b/integrations/actix/src/lib.rs @@ -6,7 +6,7 @@ //! [`examples`](https://github.com/leptos-rs/leptos/tree/main/examples) //! directory in the Leptos repository. -use actix_http::header::{HeaderName, HeaderValue}; +use actix_http::header::{HeaderName, HeaderValue, ACCEPT}; use actix_web::{ body::BoxBody, dev::{ServiceFactory, ServiceRequest}, @@ -25,7 +25,7 @@ use leptos_meta::*; use leptos_router::*; use parking_lot::RwLock; use regex::Regex; -use server_fn::request::actix::ActixRequest; +use server_fn::{redirect::REDIRECT_HEADER, request::actix::ActixRequest}; use std::{ fmt::{Debug, Display}, future::Future, @@ -99,11 +99,57 @@ impl ResponseOptions { } } -/// Provides an easy way to redirect the user from within a server function. Mimicking the Remix `redirect()`, -/// it sets a [StatusCode] of 302 and a [LOCATION](header::LOCATION) header with the provided value. -/// If looking to redirect from the client, `leptos_router::use_navigate()` should be used instead. +/// Provides an easy way to redirect the user from within a server function. +/// +/// This sets the `Location` header to the URL given. +/// +/// If the route or server function in which this is called is being accessed +/// by an ordinary `GET` request or an HTML `<form>` without any enhancement, it also sets a +/// status code of `302` for a temporary redirect. (This is determined by whether the `Accept` +/// header contains `text/html` as it does for an ordinary navigation.) +/// +/// Otherwise, it sets a custom header that indicates to the client that it should redirect, +/// without actually setting the status code. This means that the client will not follow the +/// redirect, and can therefore return the value of the server function and then handle +/// the redirect with client-side routing. #[tracing::instrument(level = "trace", fields(error), skip_all)] pub fn redirect(path: &str) { + if let (Some(req), Some(res)) = ( + use_context::<HttpRequest>(), + use_context::<ResponseOptions>(), + ) { + // insert the Location header in any case + res.insert_header( + header::LOCATION, + header::HeaderValue::from_str(path) + .expect("Failed to create HeaderValue"), + ); + + let accepts_html = req + .headers() + .get(ACCEPT) + .and_then(|v| v.to_str().ok()) + .map(|v| v.contains("text/html")) + .unwrap_or(false); + if accepts_html { + // if the request accepts text/html, it's a plain form request and needs + // to have the 302 code set + res.set_status(StatusCode::FOUND); + } else { + // otherwise, we sent it from the server fn client and actually don't want + // to set a real redirect, as this will break the ability to return data + // instead, set the REDIRECT_HEADER to indicate that the client should redirect + res.insert_header( + HeaderName::from_static(REDIRECT_HEADER), + HeaderValue::from_str("").unwrap(), + ); + } + } else { + tracing::warn!( + "Couldn't retrieve either Parts or ResponseOptions while trying \ + to redirect()." + ); + } if let Some(response_options) = use_context::<ResponseOptions>() { response_options.set_status(StatusCode::FOUND); response_options.insert_header( From f53ac1a4ae1a92565f0d1111652bd4af99851127 Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Sat, 6 Jan 2024 18:12:11 -0500 Subject: [PATCH 024/100] remove unused var --- server_fn_macro/src/lib.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/server_fn_macro/src/lib.rs b/server_fn_macro/src/lib.rs index 2ab30af579..1b7066aae4 100644 --- a/server_fn_macro/src/lib.rs +++ b/server_fn_macro/src/lib.rs @@ -94,7 +94,6 @@ pub fn server_macro_impl( let fn_name = &body.ident; let fn_name_as_str = body.ident.to_string(); let vis = body.vis; - let block = body.block; let attrs = body.attrs; let fields = body From db1497b9c24a4e9c3ff7eaa7f5db01010bb2f54d Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Sat, 6 Jan 2024 19:34:03 -0500 Subject: [PATCH 025/100] set version, input, etc. correctly --- router/src/components/form.rs | 37 +++++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/router/src/components/form.rs b/router/src/components/form.rs index 9781b9bf2c..048951aaa6 100644 --- a/router/src/components/form.rs +++ b/router/src/components/form.rs @@ -509,6 +509,16 @@ where let form = form_from_event(&ev).expect("couldn't find form submitter"); let form_data = FormData::new_with_form(&form).unwrap(); + match ServFn::from_form_data(&form_data) { + Ok(new_input) => { + input.try_set(Some(new_input)); + } + Err(err) => { + if let Some(error) = error { + error.set(Some(Box::new(err))); + } + } + } let req = <<ServFn::Client as Client<ServFn::Error>>::Request as ClientReq< ServFn::Error, >>::try_new_post_form_data( @@ -519,33 +529,30 @@ where ); match req { Ok(req) => { + action.set_pending(true); spawn_local(async move { - // TODO set input - // TODO check order of setting things here, and use batch as needed - // TODO set version? - match <ServFn as ServerFn>::run_on_client_with_req( + let res = <ServFn as ServerFn>::run_on_client_with_req( req, redirect_hook.as_ref(), ) - .await - { - Ok(res) => { - batch(move || { - version.update(|n| *n += 1); + .await; + batch(move || { + version.update(|n| *n += 1); + action.set_pending(false); + match res { + Ok(res) => { value.try_set(Some(Ok(res))); - }); - } - Err(err) => { - batch(move || { + } + Err(err) => { value.set(Some(Err(err.clone()))); if let Some(error) = error { error.set(Some(Box::new( ServerFnErrorErr::from(err), ))); } - }); + } } - } + }); }); } Err(_) => todo!(), From f6b95e40f42b2c621838dfbcddb613d5057a2265 Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Sat, 6 Jan 2024 19:34:31 -0500 Subject: [PATCH 026/100] make sure endpoint names begin with a / --- server_fn_macro/src/lib.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/server_fn_macro/src/lib.rs b/server_fn_macro/src/lib.rs index 1b7066aae4..1d26b8a925 100644 --- a/server_fn_macro/src/lib.rs +++ b/server_fn_macro/src/lib.rs @@ -370,6 +370,13 @@ pub fn server_macro_impl( }; // generate path + let fn_path_starts_with_slash = fn_path.to_string().starts_with("\"/"); + let fn_path = if fn_path_starts_with_slash || fn_path.to_string() == "\"\"" + { + quote! { #fn_path } + } else { + quote! { concat!("/", #fn_path) } + }; let path = quote! { if #fn_path.is_empty() { #server_fn_path::const_format::concatcp!( @@ -415,7 +422,6 @@ pub fn server_macro_impl( #from_impl impl #server_fn_path::ServerFn for #struct_name { - // TODO prefix const PATH: &'static str = #path; type Client = #client; From 853c080707d7dc1899d33c40e8c6865483cb856b Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Sat, 6 Jan 2024 19:35:50 -0500 Subject: [PATCH 027/100] add missing server fn registration --- integrations/actix/src/lib.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/integrations/actix/src/lib.rs b/integrations/actix/src/lib.rs index 7aaf0ade72..e49c85c50f 100644 --- a/integrations/actix/src/lib.rs +++ b/integrations/actix/src/lib.rs @@ -1357,6 +1357,14 @@ impl LeptosRoutes for &mut ServiceConfig { ); } } + + // register server functions + for (path, _) in server_fn::actix::server_fn_paths() { + let additional_context = additional_context.clone(); + let handler = handle_server_fns_with_context(additional_context); + router = router.route(path, handler); + } + router } } From f6ce82c9d16ad5236388ef38393449514ccd1fbe Mon Sep 17 00:00:00 2001 From: Rakshith Ravi <rakshith.ravi@gmx.com> Date: Mon, 8 Jan 2024 20:31:56 +0000 Subject: [PATCH 028/100] Fixed tests for `server_fn` (#2167) * Fixed server_fn tests * Changed type_name to TypeId * Fixed handling of leading slashes for server_fn endpoint --- leptos_macro/tests/server.rs | 52 +++++++++++++++-------------- leptos_macro/tests/ui/server.stderr | 4 +-- server_fn/Cargo.toml | 2 +- server_fn_macro/src/lib.rs | 10 ++++++ 4 files changed, 40 insertions(+), 28 deletions(-) diff --git a/leptos_macro/tests/server.rs b/leptos_macro/tests/server.rs index 052d88d67a..cbbb848fcb 100644 --- a/leptos_macro/tests/server.rs +++ b/leptos_macro/tests/server.rs @@ -3,7 +3,8 @@ use cfg_if::cfg_if; cfg_if! { if #[cfg(not(feature = "ssr"))] { - use leptos::{server, server_fn::Encoding, ServerFnError}; + use leptos::{server, server_fn::{codec, ServerFn}, ServerFnError}; + use std::any::TypeId; #[test] fn server_default() { @@ -11,9 +12,11 @@ cfg_if! { pub async fn my_server_action() -> Result<(), ServerFnError> { Ok(()) } - assert_eq!(MyServerAction::PREFIX, "/api"); - assert_eq!(&MyServerAction::URL[0..16], "my_server_action"); - assert_eq!(MyServerAction::ENCODING, Encoding::Url); + assert_eq!( + <MyServerAction as ServerFn>::PATH.trim_end_matches(char::is_numeric), + "/api/my_server_action" + ); + assert_eq!(TypeId::of::<<MyServerAction as ServerFn>::InputEncoding>(), TypeId::of::<codec::PostUrl>()); } #[test] @@ -22,9 +25,8 @@ cfg_if! { pub async fn my_server_action() -> Result<(), ServerFnError> { Ok(()) } - assert_eq!(FooBar::PREFIX, "/foo/bar"); - assert_eq!(FooBar::URL, "my_path"); - assert_eq!(FooBar::ENCODING, Encoding::Cbor); + assert_eq!(<FooBar as ServerFn>::PATH, "/foo/bar/my_path"); + assert_eq!(TypeId::of::<<FooBar as ServerFn>::InputEncoding>(), TypeId::of::<codec::Cbor>()); } #[test] @@ -33,9 +35,8 @@ cfg_if! { pub async fn my_server_action() -> Result<(), ServerFnError> { Ok(()) } - assert_eq!(FooBar::PREFIX, "/foo/bar"); - assert_eq!(FooBar::URL, "my_path"); - assert_eq!(FooBar::ENCODING, Encoding::Cbor); + assert_eq!(<FooBar as ServerFn>::PATH, "/foo/bar/my_path"); + assert_eq!(TypeId::of::<<FooBar as ServerFn>::InputEncoding>(), TypeId::of::<codec::Cbor>()); } #[test] @@ -44,9 +45,8 @@ cfg_if! { pub async fn my_server_action() -> Result<(), ServerFnError> { Ok(()) } - assert_eq!(FooBar::PREFIX, "/api"); - assert_eq!(FooBar::URL, "my_path"); - assert_eq!(FooBar::ENCODING, Encoding::Url); + assert_eq!(<FooBar as ServerFn>::PATH, "/api/my_path"); + assert_eq!(TypeId::of::<<FooBar as ServerFn>::InputEncoding>(), TypeId::of::<codec::PostUrl>()); } #[test] @@ -55,9 +55,11 @@ cfg_if! { pub async fn my_server_action() -> Result<(), ServerFnError> { Ok(()) } - assert_eq!(FooBar::PREFIX, "/api"); - assert_eq!(&FooBar::URL[0..16], "my_server_action"); - assert_eq!(FooBar::ENCODING, Encoding::Url); + assert_eq!( + <FooBar as ServerFn>::PATH.trim_end_matches(char::is_numeric), + "/api/my_server_action" + ); + assert_eq!(TypeId::of::<<FooBar as ServerFn>::InputEncoding>(), TypeId::of::<codec::PostUrl>()); } #[test] @@ -66,9 +68,8 @@ cfg_if! { pub async fn my_server_action() -> Result<(), ServerFnError> { Ok(()) } - assert_eq!(MyServerAction::PREFIX, "/foo/bar"); - assert_eq!(&MyServerAction::URL[0..16], "my_server_action"); - assert_eq!(MyServerAction::ENCODING, Encoding::Url); + assert_eq!(<MyServerAction as ServerFn>::PATH.trim_end_matches(char::is_numeric), "/foo/bar/my_server_action"); + assert_eq!(TypeId::of::<<MyServerAction as ServerFn>::InputEncoding>(), TypeId::of::<codec::PostUrl>()); } #[test] @@ -77,9 +78,11 @@ cfg_if! { pub async fn my_server_action() -> Result<(), ServerFnError> { Ok(()) } - assert_eq!(MyServerAction::PREFIX, "/api"); - assert_eq!(&MyServerAction::URL[0..16], "my_server_action"); - assert_eq!(MyServerAction::ENCODING, Encoding::GetJSON); + assert_eq!( + <MyServerAction as ServerFn>::PATH.trim_end_matches(char::is_numeric), + "/api/my_server_action" + ); + assert_eq!(TypeId::of::<<MyServerAction as ServerFn>::InputEncoding>(), TypeId::of::<codec::GetUrl>()); } #[test] @@ -88,9 +91,8 @@ cfg_if! { pub async fn my_server_action() -> Result<(), ServerFnError> { Ok(()) } - assert_eq!(MyServerAction::PREFIX, "/api"); - assert_eq!(MyServerAction::URL, "/path/to/my/endpoint"); - assert_eq!(MyServerAction::ENCODING, Encoding::Url); + assert_eq!(<MyServerAction as ServerFn>::PATH, "/api/path/to/my/endpoint"); + assert_eq!(TypeId::of::<<MyServerAction as ServerFn>::InputEncoding>(), TypeId::of::<codec::PostUrl>()); } } } diff --git a/leptos_macro/tests/ui/server.stderr b/leptos_macro/tests/ui/server.stderr index 0cfa2665e9..cb3614adf1 100644 --- a/leptos_macro/tests/ui/server.stderr +++ b/leptos_macro/tests/ui/server.stderr @@ -4,7 +4,7 @@ error: positional argument follows keyword argument 3 | #[server(endpoint = "my_path", FooBar)] | ^^^^^^ -error: keyword argument repeated: endpoint +error: keyword argument repeated: `endpoint` --> tests/ui/server.rs:8:30 | 8 | #[server(endpoint = "first", endpoint = "second")] @@ -40,7 +40,7 @@ error: unexpected extra argument 32 | #[server(FooBar, "/foo/bar", "Cbor", "my_path", "extra")] | ^^^^^^^ -error: Encoding Not Found +error: Encoding not found. --> tests/ui/server.rs:37:21 | 37 | #[server(encoding = "wrong")] diff --git a/server_fn/Cargo.toml b/server_fn/Cargo.toml index 22f7d59515..2f8bd20fdf 100644 --- a/server_fn/Cargo.toml +++ b/server_fn/Cargo.toml @@ -64,7 +64,7 @@ reqwest = { version = "0.11", default-features = false, optional = true, feature ] } [features] -default = ["url", "json"] +default = ["url", "json", "cbor"] actix = ["ssr", "dep:actix-web", "dep:send_wrapper"] axum = [ "ssr", diff --git a/server_fn_macro/src/lib.rs b/server_fn_macro/src/lib.rs index 1d26b8a925..eca9523672 100644 --- a/server_fn_macro/src/lib.rs +++ b/server_fn_macro/src/lib.rs @@ -369,6 +369,15 @@ pub fn server_macro_impl( )); }; + // Remove any leading slashes, even if they exist (we'll add them below) + let fn_path = Literal::string( + fn_path + .to_string() + .trim_start_matches('\"') + .trim_start_matches('/') + .trim_end_matches('\"'), + ); + // generate path let fn_path_starts_with_slash = fn_path.to_string().starts_with("\"/"); let fn_path = if fn_path_starts_with_slash || fn_path.to_string() == "\"\"" @@ -391,6 +400,7 @@ pub fn server_macro_impl( } else { #server_fn_path::const_format::concatcp!( #prefix, + "/", #fn_path ) } From a1bd84f3dc3060a0a6555dec932613227033d495 Mon Sep 17 00:00:00 2001 From: Rakshith Ravi <rakshith.ravi@gmx.com> Date: Tue, 9 Jan 2024 01:55:15 +0000 Subject: [PATCH 029/100] feat: add `serde-lite` codec for server functions (#2168) --- leptos/Cargo.toml | 2 +- leptos_reactive/Cargo.toml | 4 +- server_fn/Cargo.toml | 2 + server_fn/src/codec/mod.rs | 14 ++++-- server_fn/src/codec/serde_lite.rs | 82 +++++++++++++++++++++++++++++++ server_fn/src/lib.rs | 3 ++ server_fn_macro/src/lib.rs | 32 ++++++------ 7 files changed, 119 insertions(+), 20 deletions(-) create mode 100644 server_fn/src/codec/serde_lite.rs diff --git a/leptos/Cargo.toml b/leptos/Cargo.toml index bbc0ddc4bc..730e7ff792 100644 --- a/leptos/Cargo.toml +++ b/leptos/Cargo.toml @@ -61,7 +61,7 @@ nightly = [ "leptos_server/nightly", ] serde = ["leptos_reactive/serde"] -serde-lite = ["leptos_reactive/serde-lite"] +serde-lite = ["leptos_reactive/serde-lite", "server_fn/serde-lite"] miniserde = ["leptos_reactive/miniserde"] rkyv = ["leptos_reactive/rkyv"] tracing = ["leptos_macro/tracing"] diff --git a/leptos_reactive/Cargo.toml b/leptos_reactive/Cargo.toml index 6abd639344..35960a3d4e 100644 --- a/leptos_reactive/Cargo.toml +++ b/leptos_reactive/Cargo.toml @@ -10,7 +10,7 @@ description = "Reactive system for the Leptos web framework." [dependencies] slotmap = { version = "1", features = ["serde"] } serde = { version = "1", features = ["derive"] } -serde-lite = { version = "0.4", optional = true } +serde-lite = { version = "0.5", optional = true } futures = { version = "0.3" } js-sys = { version = "0.3", optional = true } miniserde = { version = "0.1", optional = true } @@ -24,7 +24,7 @@ bytecheck = { version = "0.7", features = [ "simdutf8", ], optional = true } rustc-hash = "1" -serde-wasm-bindgen = "0.5" +serde-wasm-bindgen = "0.6" serde_json = "1" spin-sdk = { version = "2", optional = true } base64 = "0.21" diff --git a/server_fn/Cargo.toml b/server_fn/Cargo.toml index 2f8bd20fdf..53dd8495ac 100644 --- a/server_fn/Cargo.toml +++ b/server_fn/Cargo.toml @@ -33,6 +33,7 @@ multer = { version = "3", optional = true } ## output encodings # serde serde_json = "1" +serde-lite = { version = "0.5", features = ["derive"], optional = true } futures = "0.3" http = { version = "1" } ciborium = { version = "0.2", optional = true } @@ -83,6 +84,7 @@ browser = [ "dep:wasm-bindgen-futures", ] json = [] +serde-lite = ["dep:serde-lite"] multipart = ["dep:multer"] url = ["dep:serde_qs"] cbor = ["dep:ciborium"] diff --git a/server_fn/src/codec/mod.rs b/server_fn/src/codec/mod.rs index aacb15953d..f4c6fa60d1 100644 --- a/server_fn/src/codec/mod.rs +++ b/server_fn/src/codec/mod.rs @@ -2,19 +2,24 @@ mod cbor; #[cfg(feature = "cbor")] pub use cbor::*; + #[cfg(feature = "json")] mod json; -use http::Method; #[cfg(feature = "json")] pub use json::*; + +#[cfg(feature = "serde-lite")] +mod serde_lite; +#[cfg(feature = "serde-lite")] +pub use serde_lite::*; + #[cfg(feature = "rkyv")] mod rkyv; #[cfg(feature = "rkyv")] pub use rkyv::*; + #[cfg(feature = "url")] mod url; -use crate::{error::ServerFnError, request::ClientReq}; -use futures::Future; #[cfg(feature = "url")] pub use url::*; @@ -24,6 +29,9 @@ mod multipart; pub use multipart::*; mod stream; +use crate::{error::ServerFnError, request::ClientReq}; +use futures::Future; +use http::Method; pub use stream::*; pub trait FromReq<CustErr, Request, Encoding> diff --git a/server_fn/src/codec/serde_lite.rs b/server_fn/src/codec/serde_lite.rs new file mode 100644 index 0000000000..c6054c9c25 --- /dev/null +++ b/server_fn/src/codec/serde_lite.rs @@ -0,0 +1,82 @@ +use super::{Encoding, FromReq, FromRes}; +use crate::{ + error::ServerFnError, + request::{ClientReq, Req}, + response::{ClientRes, Res}, + IntoReq, IntoRes, +}; +use http::Method; +use serde_lite::{Deserialize, Serialize}; +/// Pass arguments and receive responses as JSON in the body of a `POST` request. +pub struct SerdeLite; + +impl Encoding for SerdeLite { + const CONTENT_TYPE: &'static str = "application/json"; + const METHOD: Method = Method::POST; +} + +impl<CustErr, T, Request> IntoReq<CustErr, Request, SerdeLite> for T +where + Request: ClientReq<CustErr>, + T: Serialize + Send, +{ + fn into_req( + self, + path: &str, + accepts: &str, + ) -> Result<Request, ServerFnError<CustErr>> { + let data = serde_json::to_string( + &self + .serialize() + .map_err(|e| ServerFnError::Serialization(e.to_string()))?, + ) + .map_err(|e| ServerFnError::Serialization(e.to_string()))?; + Request::try_new_post(path, accepts, SerdeLite::CONTENT_TYPE, data) + } +} + +impl<CustErr, T, Request> FromReq<CustErr, Request, SerdeLite> for T +where + Request: Req<CustErr> + Send + 'static, + T: Deserialize, +{ + async fn from_req(req: Request) -> Result<Self, ServerFnError<CustErr>> { + 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())) + } +} + +impl<CustErr, T, Response> IntoRes<CustErr, Response, SerdeLite> for T +where + Response: Res<CustErr>, + T: Serialize + Send, +{ + async fn into_res(self) -> Result<Response, ServerFnError<CustErr>> { + let data = serde_json::to_string( + &self + .serialize() + .map_err(|e| ServerFnError::Serialization(e.to_string()))?, + ) + .map_err(|e| ServerFnError::Serialization(e.to_string()))?; + Response::try_from_string(SerdeLite::CONTENT_TYPE, data) + } +} + +impl<CustErr, T, Response> FromRes<CustErr, Response, SerdeLite> for T +where + Response: ClientRes<CustErr> + Send, + T: Deserialize + Send, +{ + async fn from_res(res: Response) -> Result<Self, ServerFnError<CustErr>> { + 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())) + } +} diff --git a/server_fn/src/lib.rs b/server_fn/src/lib.rs index add0f537fc..9314eba8b3 100644 --- a/server_fn/src/lib.rs +++ b/server_fn/src/lib.rs @@ -28,6 +28,9 @@ use request::Req; use response::{ClientRes, Res}; #[doc(hidden)] 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}; #[doc(hidden)] pub use xxhash_rust; diff --git a/server_fn_macro/src/lib.rs b/server_fn_macro/src/lib.rs index eca9523672..b797fee2d8 100644 --- a/server_fn_macro/src/lib.rs +++ b/server_fn_macro/src/lib.rs @@ -74,10 +74,8 @@ pub fn server_macro_impl( } = args; let prefix = prefix.unwrap_or_else(|| Literal::string(default_path)); let fn_path = fn_path.unwrap_or_else(|| Literal::string("")); - let input = input.unwrap_or_else(|| syn::parse_quote!(PostUrl)); - let input_is_rkyv = input == "Rkyv"; - let input_is_multipart = input == "MultipartFormData"; - let input = codec_ident(server_fn_path.as_ref(), input); + let input_ident = input.unwrap_or_else(|| syn::parse_quote!(PostUrl)); + let input = codec_ident(server_fn_path.as_ref(), input_ident.clone()); let output = output.unwrap_or_else(|| syn::parse_quote!(Json)); let output = codec_ident(server_fn_path.as_ref(), output); // default to PascalCase version of function name if no struct name given @@ -309,17 +307,23 @@ pub fn server_macro_impl( } }; - // TODO rkyv derives - let derives = if input_is_multipart { - quote! {} - } else if input_is_rkyv { - todo!("implement derives for Rkyv") - } else { - quote! { - Clone, #server_fn_path::serde::Serialize, #server_fn_path::serde::Deserialize - } + let (is_serde, derives) = match input_ident.to_string().as_str() { + "Rkyv" => todo!("implement derives for Rkyv"), + "MultipartFormData" => (false, quote! {}), + "SerdeLite" => ( + true, + quote! { + Clone, #server_fn_path::serde_lite::Serialize, #server_fn_path::serde_lite::Deserialize + }, + ), + _ => ( + true, + quote! { + Clone, #server_fn_path::serde::Serialize, #server_fn_path::serde::Deserialize + }, + ), }; - let serde_path = (!input_is_multipart && !input_is_rkyv).then(|| { + let serde_path = is_serde.then(|| { quote! { #[serde(crate = #serde_path)] } From f5c007df7b76bbde4025f0da0ff03ab587460dec Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Sun, 7 Jan 2024 19:05:35 -0500 Subject: [PATCH 030/100] use server fns directly in ActionForm and MultiActionForm --- examples/todo_app_sqlite_axum/Todos.db | Bin 16384 -> 16384 bytes examples/todo_app_sqlite_axum/src/todo.rs | 2 - router/src/components/form.rs | 264 +++++----------------- router/src/components/router.rs | 23 +- server_fn/src/request/axum.rs | 1 - server_fn_macro/src/lib.rs | 1 - 6 files changed, 81 insertions(+), 210 deletions(-) diff --git a/examples/todo_app_sqlite_axum/Todos.db b/examples/todo_app_sqlite_axum/Todos.db index 3c695e04da231da42f617ca414e350f9ce9b1441..ec85d2b07f9ac6b3b931f4599e3e7a35107050f5 100644 GIT binary patch delta 133 zcmZo@U~Fh$oFL8UK2gS*(S2jWB7T`q49xsb82Eqkzvh3!|B3%D|91ZMFt}OJU?RU4 zn<NW^AcvPXs{{)JKZh3s1B1LUhr5R-n;1yk-2*7V&d=e_Dgu&rV-*HbuB?I}%9&LF PL^(l}xNQC@&#nLf^MD%e delta 133 zcmZo@U~Fh$oFL6;Fj2;t(O_f3B7T|E3@rRdnD~G4Kj6Q~e*_5k@^6E}^_v9^Ch~i+ zim)*7bNH|dgD7uSArR%oDhQ%H`Cl^d@pE{v@`HHptb8EKjg=Ebxs)g}Ffa)4bGX{u PvvPw(oHqZIXIB6Ku0$IV diff --git a/examples/todo_app_sqlite_axum/src/todo.rs b/examples/todo_app_sqlite_axum/src/todo.rs index b351aaac4f..a4dfbd5232 100644 --- a/examples/todo_app_sqlite_axum/src/todo.rs +++ b/examples/todo_app_sqlite_axum/src/todo.rs @@ -77,7 +77,6 @@ pub async fn add_todo(title: String) -> Result<(), ServerFnError> { pub async fn delete_todo(id: u16) -> Result<(), ServerFnError> { let mut conn = db().await?; - leptos_axum::redirect("/foo"); Ok(sqlx::query("DELETE FROM todos WHERE id = $1") .bind(id) .execute(&mut conn) @@ -90,7 +89,6 @@ pub fn TodoApp() -> impl IntoView { //let id = use_context::<String>(); provide_meta_context(); view! { - <Link rel="shortcut icon" type_="image/ico" href="/favicon.ico"/> <Stylesheet id="leptos" href="/pkg/todo_app_sqlite_axum.css"/> <Router> diff --git a/router/src/components/form.rs b/router/src/components/form.rs index 048951aaa6..f16d223242 100644 --- a/router/src/components/form.rs +++ b/router/src/components/form.rs @@ -17,9 +17,10 @@ use leptos::{ use send_wrapper::SendWrapper; use serde::{de::DeserializeOwned, Serialize}; use std::{error::Error, fmt::Debug, rc::Rc}; -use wasm_bindgen::{JsCast, UnwrapThrowExt}; +use thiserror::Error; +use wasm_bindgen::{JsCast, JsValue, UnwrapThrowExt}; use web_sys::{ - FormData, HtmlButtonElement, HtmlFormElement, HtmlInputElement, + Event, FormData, HtmlButtonElement, HtmlFormElement, HtmlInputElement, RequestRedirect, SubmitEvent, }; @@ -436,17 +437,11 @@ pub fn ActionForm<ServFn>( /// Sets the `class` attribute on the underlying `<form>` tag, making it easier to style. #[prop(optional, into)] class: Option<AttributeValue>, - /// A signal that will be set if the form submission ends in an error. - #[prop(optional)] - error: Option<RwSignal<Option<Box<dyn Error>>>>, /// A [`NodeRef`] in which the `<form>` element should be stored. #[prop(optional)] node_ref: Option<NodeRef<html::Form>>, - /// Sets whether the page should be scrolled to the top when navigating. - #[prop(optional)] - noscroll: bool, /// Arbitrary attributes to add to the `<form>` - #[prop(optional, into)] + #[prop(attrs, optional)] attributes: Vec<(&'static str, Attribute)>, /// Component children; should include the HTML of the form elements. children: Children, @@ -454,14 +449,16 @@ pub fn ActionForm<ServFn>( where ServFn: Clone + DeserializeOwned + ServerFn<InputEncoding = PostUrl> + 'static, - ServerFnError<ServFn::Error>: Debug + Clone, - ServFn::Output: Debug, - ServFn::Error: Debug + 'static, <<ServFn::Client as Client<ServFn::Error>>::Request as ClientReq< ServFn::Error, >>::FormData: From<FormData>, { let has_router = has_router(); + if !has_router { + _ = server_fn::redirect::set_redirect_hook(|path: &str| { + _ = window().location().set_href(path); + }); + } let action_url = if let Some(url) = action.url() { url } else { @@ -486,76 +483,18 @@ where ev.prevent_default(); - let navigate = has_router.then(use_navigate); - let navigate_options = SendWrapper::new(NavigateOptions { - scroll: !noscroll, - ..Default::default() - }); - let redirect_hook = navigate.map(|navigate| { - let navigate = SendWrapper::new(navigate); - Box::new(move |path: &str| { - let path = path.to_string(); - // delay by a tick here, so that the Action updates *before* the redirect - request_animation_frame({ - let navigate = navigate.clone(); - let navigate_options = navigate_options.clone(); - move || { - navigate(&path, navigate_options.take()); - } - }); - }) as RedirectHook - }); - - let form = - form_from_event(&ev).expect("couldn't find form submitter"); - let form_data = FormData::new_with_form(&form).unwrap(); - match ServFn::from_form_data(&form_data) { + match ServFn::from_event(&ev) { Ok(new_input) => { - input.try_set(Some(new_input)); + action.dispatch(new_input); } Err(err) => { - if let Some(error) = error { - error.set(Some(Box::new(err))); - } - } - } - let req = <<ServFn::Client as Client<ServFn::Error>>::Request as ClientReq< - ServFn::Error, - >>::try_new_post_form_data( - &action_url, - ServFn::OutputEncoding::CONTENT_TYPE, - ServFn::InputEncoding::CONTENT_TYPE, - form_data.into(), - ); - match req { - Ok(req) => { - action.set_pending(true); - spawn_local(async move { - let res = <ServFn as ServerFn>::run_on_client_with_req( - req, - redirect_hook.as_ref(), - ) - .await; - batch(move || { - version.update(|n| *n += 1); - action.set_pending(false); - match res { - Ok(res) => { - value.try_set(Some(Ok(res))); - } - Err(err) => { - value.set(Some(Err(err.clone()))); - if let Some(error) = error { - error.set(Some(Box::new( - ServerFnErrorErr::from(err), - ))); - } - } - } - }); + batch(move || { + value.set(Some(Err(ServerFnError::Serialization( + err.to_string(), + )))); + version.update(|n| *n += 1); }); } - Err(_) => todo!(), } } }; @@ -573,106 +512,6 @@ where action_form = action_form.attr(attr_name, attr_value); } action_form - - /* let on_error = Rc::new(move |e: &gloo_net::Error| { - batch(move || { - action.set_pending(false); - let e = ServerFnError::Request(e.to_string()); - value.try_set(Some(Err(e.clone()))); - if let Some(error) = error { - error.try_set(Some(Box::new(ServerFnErrorErr::from(e)))); - } - }); - }); - - let on_form_data = Rc::new(move |form_data: &web_sys::FormData| { - let data = I::from_form_data(form_data); - match data { - Ok(data) => { - batch(move || { - input.try_set(Some(data)); - action.set_pending(true); - }); - } - Err(e) => { - error!("{e}"); - let e = ServerFnError::Serialization(e.to_string()); - batch(move || { - value.try_set(Some(Err(e.clone()))); - if let Some(error) = error { - error - .try_set(Some(Box::new(ServerFnErrorErr::from(e)))); - } - }); - } - } - }); - - let on_response = Rc::new(move |resp: &web_sys::Response| { - let resp = resp.clone().expect("couldn't get Response"); - - // If the response was redirected then a JSON will not be available in the response, instead - // it will be an actual page, so we don't want to try to parse it. - if resp.redirected() { - return; - } - - spawn_local(async move { - let body = JsFuture::from( - resp.text().expect("couldn't get .text() from Response"), - ) - .await; - let status = resp.status(); - match body { - Ok(json) => { - let json = json - .as_string() - .expect("couldn't get String from JsString"); - if (400..=599).contains(&status) { - let res = ServerFnError::<I::Error>::de(&json); - value.try_set(Some(Err(res))); - if let Some(error) = error { - error.try_set(None); - } - } else { - match serde_json::from_str::<O>(&json) { - Ok(res) => { - value.try_set(Some(Ok(res))); - if let Some(error) = error { - error.try_set(None); - } - } - Err(e) => { - value.try_set(Some(Err( - ServerFnError::Deserialization( - e.to_string(), - ), - ))); - if let Some(error) = error { - error.try_set(Some(Box::new(e))); - } - } - } - } - } - Err(e) => { - error!("{e:?}"); - // TODO - /* if let Some(error) = error { - error.try_set(Some(Box::new( - ServerFnErrorErr::Request( - e.as_string().unwrap_or_default(), - ), - ))); - }*/ - } - }; - batch(move || { - input.try_set(None); - action.set_pending(false); - }); - }); - });*/ } /// Automatically turns a server [MultiAction](leptos_server::MultiAction) into an HTML @@ -683,11 +522,11 @@ where tracing::instrument(level = "trace", skip_all,) )] #[component] -pub fn MultiActionForm<I, O>( +pub fn MultiActionForm<ServFn>( /// The action from which to build the form. This should include a URL, which can be generated /// by default using [create_server_action](leptos_server::create_server_action) or added /// manually using [leptos_server::Action::using_server_fn]. - action: MultiAction<I, Result<O, ServerFnError>>, + action: MultiAction<ServFn, Result<ServFn::Output, ServerFnError>>, /// Sets the `class` attribute on the underlying `<form>` tag, making it easier to style. #[prop(optional, into)] class: Option<AttributeValue>, @@ -698,17 +537,25 @@ pub fn MultiActionForm<I, O>( #[prop(optional)] node_ref: Option<NodeRef<html::Form>>, /// Arbitrary attributes to add to the `<form>` - #[prop(optional, into)] + #[prop(attrs, optional)] attributes: Vec<(&'static str, Attribute)>, /// Component children; should include the HTML of the form elements. children: Children, ) -> impl IntoView where - I: Clone + ServerFn + DeserializeOwned + 'static, - O: Clone + Serializable + 'static, + ServFn: + Clone + DeserializeOwned + ServerFn<InputEncoding = PostUrl> + 'static, + <<ServFn::Client as Client<ServFn::Error>>::Request as ClientReq< + ServFn::Error, + >>::FormData: From<FormData>, { - let multi_action = action; - let action = if let Some(url) = multi_action.url() { + let has_router = has_router(); + if !has_router { + _ = server_fn::redirect::set_redirect_hook(|path: &str| { + _ = window().location().set_href(path); + }); + } + let action_url = if let Some(url) = action.url() { url } else { debug_warn!( @@ -718,22 +565,21 @@ where String::new() }; - let on_submit = move |ev: web_sys::SubmitEvent| { + let on_submit = move |ev: SubmitEvent| { if ev.default_prevented() { return; } - match I::from_event(&ev) { + ev.prevent_default(); + + match ServFn::from_event(&ev) { Err(e) => { - error!("{e}"); if let Some(error) = error { error.try_set(Some(Box::new(e))); } } Ok(input) => { - ev.prevent_default(); - ev.stop_propagation(); - multi_action.dispatch(input); + action.dispatch(input); if let Some(error) = error { error.try_set(None); } @@ -742,19 +588,19 @@ where }; let class = class.map(|bx| bx.into_attribute_boxed()); - let mut form = form() - .attr("method", "POST") - .attr("action", action) - .on(ev::submit, on_submit) + let mut action_form = form() + .attr("action", action_url) + .attr("method", "post") .attr("class", class) + .on(ev::submit, on_submit) .child(children()); if let Some(node_ref) = node_ref { - form = form.node_ref(node_ref) + action_form = action_form.node_ref(node_ref) }; for (attr_name, attr_value) in attributes { - form = form.attr(attr_name, attr_value); + action_form = action_form.attr(attr_name, attr_value); } - form + action_form } fn form_from_event(ev: &SubmitEvent) -> Option<HtmlFormElement> { @@ -892,7 +738,7 @@ where Self: Sized + serde::de::DeserializeOwned, { /// Tries to deserialize the data, given only the `submit` event. - fn from_event(ev: &web_sys::Event) -> Result<Self, serde_qs::Error>; + fn from_event(ev: &web_sys::Event) -> Result<Self, FromFormDataError>; /// Tries to deserialize the data, given the actual form data. fn from_form_data( @@ -900,6 +746,16 @@ where ) -> Result<Self, serde_qs::Error>; } +#[derive(Error, Debug)] +pub enum FromFormDataError { + #[error("Could not find <form> connected to event.")] + MissingForm(Event), + #[error("Could not create FormData from <form>: {0:?}")] + FormData(JsValue), + #[error("Deserialization error: {0:?}")] + Deserialization(serde_qs::Error), +} + impl<T> FromFormData for T where T: serde::de::DeserializeOwned, @@ -908,13 +764,15 @@ where any(debug_assertions, feature = "ssr"), tracing::instrument(level = "trace", skip_all,) )] - fn from_event(ev: &web_sys::Event) -> Result<Self, serde_qs::Error> { - let (form, _, _, _) = extract_form_attributes(ev); - - let form_data = web_sys::FormData::new_with_form(&form).unwrap_throw(); - + fn from_event(ev: &Event) -> Result<Self, FromFormDataError> { + let form = form_from_event(ev.unchecked_ref()) + .ok_or_else(|| FromFormDataError::MissingForm(ev.clone()))?; + let form_data = FormData::new_with_form(&form) + .map_err(FromFormDataError::FormData)?; Self::from_form_data(&form_data) + .map_err(FromFormDataError::Deserialization) } + #[cfg_attr( any(debug_assertions, feature = "ssr"), tracing::instrument(level = "trace", skip_all,) diff --git a/router/src/components/router.rs b/router/src/components/router.rs index 0238821303..dbcf54505e 100644 --- a/router/src/components/router.rs +++ b/router/src/components/router.rs @@ -1,13 +1,15 @@ use crate::{ - create_location, matching::resolve_path, scroll_to_el, Branch, History, - Location, LocationChange, RouteContext, RouterIntegrationContext, State, + create_location, matching::resolve_path, scroll_to_el, use_navigate, + Branch, History, Location, LocationChange, RouteContext, + RouterIntegrationContext, State, }; #[cfg(not(feature = "ssr"))] use crate::{unescape, Url}; use cfg_if::cfg_if; -use leptos::*; +use leptos::{server_fn::redirect::RedirectHook, *}; #[cfg(feature = "transition")] use leptos_reactive::use_transition; +use send_wrapper::SendWrapper; use std::{ cell::RefCell, rc::Rc, @@ -45,6 +47,21 @@ pub fn Router( provide_context(SetIsRouting(set_is_routing)); } + // set server function redirect hook + let navigate = use_navigate(); + let navigate = SendWrapper::new(navigate); + let router_hook = Box::new(move |path: &str| { + let path = path.to_string(); + // delay by a tick here, so that the Action updates *before* the redirect + request_animation_frame({ + let navigate = navigate.clone(); + move || { + navigate(&path, Default::default()); + } + }); + }) as RedirectHook; + server_fn::redirect::set_redirect_hook(router_hook); + children() } diff --git a/server_fn/src/request/axum.rs b/server_fn/src/request/axum.rs index 696f01e8fd..799c82ecc3 100644 --- a/server_fn/src/request/axum.rs +++ b/server_fn/src/request/axum.rs @@ -25,7 +25,6 @@ impl<CustErr> Req<CustErr> for Request<Body> { } async fn try_into_string(self) -> Result<String, ServerFnError<CustErr>> { - println!("accepts = {:?}", self.headers().get(http::header::ACCEPT)); let bytes = self.try_into_bytes().await?; String::from_utf8(bytes.to_vec()) .map_err(|e| ServerFnError::Deserialization(e.to_string())) diff --git a/server_fn_macro/src/lib.rs b/server_fn_macro/src/lib.rs index b797fee2d8..e6e90486c6 100644 --- a/server_fn_macro/src/lib.rs +++ b/server_fn_macro/src/lib.rs @@ -235,7 +235,6 @@ pub fn server_macro_impl( #struct_name::PATH, <#struct_name as ServerFn>::InputEncoding::METHOD, |req| { - println!("running {:?}", stringify!(#struct_name)); Box::pin(#struct_name::run_on_server(req)) }, #struct_name::middlewares From be084a5d1d899321f630bbf513119a7df90da5ae Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Mon, 8 Jan 2024 21:51:01 -0500 Subject: [PATCH 031/100] remove list of magic identifiers, use rust-analyzer to help with imports instead --- server_fn_macro/src/lib.rs | 46 +++++++++++++------------------------- 1 file changed, 15 insertions(+), 31 deletions(-) diff --git a/server_fn_macro/src/lib.rs b/server_fn_macro/src/lib.rs index e6e90486c6..275eb66315 100644 --- a/server_fn_macro/src/lib.rs +++ b/server_fn_macro/src/lib.rs @@ -74,10 +74,20 @@ pub fn server_macro_impl( } = args; let prefix = prefix.unwrap_or_else(|| Literal::string(default_path)); let fn_path = fn_path.unwrap_or_else(|| Literal::string("")); - let input_ident = input.unwrap_or_else(|| syn::parse_quote!(PostUrl)); - let input = codec_ident(server_fn_path.as_ref(), input_ident.clone()); - let output = output.unwrap_or_else(|| syn::parse_quote!(Json)); - let output = codec_ident(server_fn_path.as_ref(), output); + let input_ident = input + .as_ref() + .map(ToString::to_string) + .unwrap_or_else(|| "PostUrl".to_string()); + let input = input.map(|n| n.to_token_stream()).unwrap_or_else(|| { + quote! { + #server_fn_path::codec::PostUrl + } + }); + let output = output.map(|n| n.to_token_stream()).unwrap_or_else(|| { + quote! { + #server_fn_path::codec::Json + } + }); // default to PascalCase version of function name if no struct name given let struct_name = struct_name.unwrap_or_else(|| { let upper_camel_case_name = Converter::new() @@ -306,7 +316,7 @@ pub fn server_macro_impl( } }; - let (is_serde, derives) = match input_ident.to_string().as_str() { + let (is_serde, derives) = match input_ident.as_str() { "Rkyv" => todo!("implement derives for Rkyv"), "MultipartFormData" => (false, quote! {}), "SerdeLite" => ( @@ -835,29 +845,3 @@ impl ServerFnBody { } } } - -/// Returns either the path of the codec (if it's a builtin) or the -/// original ident. -fn codec_ident(server_fn_path: Option<&Path>, ident: Ident) -> TokenStream2 { - if let Some(server_fn_path) = server_fn_path { - let str = ident.to_string(); - if [ - "GetUrl", - "PostUrl", - "Cbor", - "Json", - "Rkyv", - "Streaming", - "StreamingText", - "MultipartFormData", - ] - .contains(&str.as_str()) - { - return quote! { - #server_fn_path::codec::#ident - }; - } - } - - ident.into_token_stream() -} From 738eeefe7386ae5e3c07f569c2d00da3b2ace9d0 Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Mon, 8 Jan 2024 22:00:07 -0500 Subject: [PATCH 032/100] chore: clear warnings --- examples/todo_app_sqlite_axum/Cargo.toml | 2 +- examples/todo_app_sqlite_axum/src/main.rs | 2 +- examples/todo_app_sqlite_axum/src/todo.rs | 6 +++--- integrations/axum/src/lib.rs | 6 +++--- router/src/components/form.rs | 16 +++------------- router/src/components/router.rs | 6 ++++-- 6 files changed, 15 insertions(+), 23 deletions(-) diff --git a/examples/todo_app_sqlite_axum/Cargo.toml b/examples/todo_app_sqlite_axum/Cargo.toml index 5e4d187d56..4d6e7f84ed 100644 --- a/examples/todo_app_sqlite_axum/Cargo.toml +++ b/examples/todo_app_sqlite_axum/Cargo.toml @@ -13,7 +13,7 @@ futures = "0.3" cfg-if = "1.0" http = "1.0" leptos = { path = "../../leptos", features = ["nightly"] } -server_fn = { path = "../../server_fn", features = ["url", "json"] } +server_fn = { path = "../../server_fn", features = ["serde-lite"] } leptos_axum = { path = "../../integrations/axum", optional = true } leptos_meta = { path = "../../meta", features = ["nightly"] } leptos_router = { path = "../../router", features = ["nightly"] } diff --git a/examples/todo_app_sqlite_axum/src/main.rs b/examples/todo_app_sqlite_axum/src/main.rs index 62631b84f2..a001e3f723 100644 --- a/examples/todo_app_sqlite_axum/src/main.rs +++ b/examples/todo_app_sqlite_axum/src/main.rs @@ -4,7 +4,7 @@ cfg_if! { if #[cfg(feature = "ssr")] { use leptos::*; use axum::{ - routing::{post, get}, + routing::get, extract::{State, Path}, http::Request, response::{IntoResponse, Response}, diff --git a/examples/todo_app_sqlite_axum/src/todo.rs b/examples/todo_app_sqlite_axum/src/todo.rs index a4dfbd5232..21a70b7779 100644 --- a/examples/todo_app_sqlite_axum/src/todo.rs +++ b/examples/todo_app_sqlite_axum/src/todo.rs @@ -4,6 +4,7 @@ use leptos::*; use leptos_meta::*; use leptos_router::*; use serde::{Deserialize, Serialize}; +use server_fn::codec::SerdeLite; #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[cfg_attr(feature = "ssr", derive(sqlx::FromRow))] @@ -26,8 +27,7 @@ cfg_if! { #[server(GetTodos, "/api")] pub async fn get_todos() -> Result<Vec<Todo>, ServerFnError> { - use http::{header::SET_COOKIE, request::Parts, HeaderValue, StatusCode}; - use leptos_axum::ResponseOptions; + use http::request::Parts; // this is just an example of how to access server context injected in the handlers let req_parts = use_context::<Parts>(); @@ -73,7 +73,7 @@ pub async fn add_todo(title: String) -> Result<(), ServerFnError> { } // The struct name and path prefix arguments are optional. -#[server] +#[server(output = SerdeLite)] pub async fn delete_todo(id: u16) -> Result<(), ServerFnError> { let mut conn = db().await?; diff --git a/integrations/axum/src/lib.rs b/integrations/axum/src/lib.rs index 9dfde6937c..57c9064029 100644 --- a/integrations/axum/src/lib.rs +++ b/integrations/axum/src/lib.rs @@ -664,7 +664,7 @@ where let path = req.uri().path_and_query().unwrap().as_str(); let full_path = format!("http://leptos.dev{path}"); - let (req, req_parts) = generate_request_and_parts(req); + let (_, req_parts) = generate_request_and_parts(req); move || { provide_contexts(full_path, req_parts, default_res_options); app_fn().into_view() @@ -998,7 +998,7 @@ where spawn_task!(async move { let app = { let full_path = full_path.clone(); - let (req, req_parts) = generate_request_and_parts(req); + let (_, req_parts) = generate_request_and_parts(req); move || { provide_contexts( full_path, @@ -1128,7 +1128,7 @@ where spawn_task!(async move { let app = { let full_path = full_path.clone(); - let (req, req_parts) = generate_request_and_parts(req); + let (_, req_parts) = generate_request_and_parts(req); move || { provide_contexts( full_path, diff --git a/router/src/components/form.rs b/router/src/components/form.rs index f16d223242..d11edde10c 100644 --- a/router/src/components/form.rs +++ b/router/src/components/form.rs @@ -5,17 +5,10 @@ use crate::{ use leptos::{ html::form, logging::*, - server_fn::{ - client::Client, - codec::{Encoding, PostUrl}, - redirect::RedirectHook, - request::ClientReq, - ServerFn, - }, + server_fn::{client::Client, codec::PostUrl, request::ClientReq, ServerFn}, *, }; -use send_wrapper::SendWrapper; -use serde::{de::DeserializeOwned, Serialize}; +use serde::de::DeserializeOwned; use std::{error::Error, fmt::Debug, rc::Rc}; use thiserror::Error; use wasm_bindgen::{JsCast, JsValue, UnwrapThrowExt}; @@ -447,8 +440,7 @@ pub fn ActionForm<ServFn>( children: Children, ) -> impl IntoView where - ServFn: - Clone + DeserializeOwned + ServerFn<InputEncoding = PostUrl> + 'static, + ServFn: DeserializeOwned + ServerFn<InputEncoding = PostUrl> + 'static, <<ServFn::Client as Client<ServFn::Error>>::Request as ClientReq< ServFn::Error, >>::FormData: From<FormData>, @@ -470,12 +462,10 @@ where }; let version = action.version(); let value = action.value(); - let input = action.input(); let class = class.map(|bx| bx.into_attribute_boxed()); let on_submit = { - let action_url = action_url.clone(); move |ev: SubmitEvent| { if ev.default_prevented() { return; diff --git a/router/src/components/router.rs b/router/src/components/router.rs index dbcf54505e..875c75d623 100644 --- a/router/src/components/router.rs +++ b/router/src/components/router.rs @@ -6,7 +6,7 @@ use crate::{ #[cfg(not(feature = "ssr"))] use crate::{unescape, Url}; use cfg_if::cfg_if; -use leptos::{server_fn::redirect::RedirectHook, *}; +use leptos::{logging::debug_warn, server_fn::redirect::RedirectHook, *}; #[cfg(feature = "transition")] use leptos_reactive::use_transition; use send_wrapper::SendWrapper; @@ -60,7 +60,9 @@ pub fn Router( } }); }) as RedirectHook; - server_fn::redirect::set_redirect_hook(router_hook); + if server_fn::redirect::set_redirect_hook(router_hook).is_err() { + debug_warn!("Error setting <Router/> server function redirect hook."); + } children() } From 61148026d15ae6bf130efd32796b58a23c16cc4b Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Wed, 10 Jan 2024 08:03:37 -0500 Subject: [PATCH 033/100] allow type paths for input/output, and properly namespace built-in encodings --- examples/todo_app_sqlite/Cargo.toml | 1 - examples/todo_app_sqlite/src/todo.rs | 10 +-- server_fn_macro/src/lib.rs | 102 +++++++++++++++++++-------- 3 files changed, 77 insertions(+), 36 deletions(-) diff --git a/examples/todo_app_sqlite/Cargo.toml b/examples/todo_app_sqlite/Cargo.toml index c5d2ccdf64..f3977ca2c4 100644 --- a/examples/todo_app_sqlite/Cargo.toml +++ b/examples/todo_app_sqlite/Cargo.toml @@ -20,7 +20,6 @@ leptos = { path = "../../leptos", features = ["nightly"] } leptos_actix = { path = "../../integrations/actix", optional = true } leptos_meta = { path = "../../meta", features = ["nightly"] } leptos_router = { path = "../../router", features = ["nightly"] } -server_fn = { path = "../../server_fn", features = ["cbor"] } log = "0.4.17" simple_logger = "4.0.0" gloo = { git = "https://github.com/rustwasm/gloo" } diff --git a/examples/todo_app_sqlite/src/todo.rs b/examples/todo_app_sqlite/src/todo.rs index 74a5f862dc..6256fb8cad 100644 --- a/examples/todo_app_sqlite/src/todo.rs +++ b/examples/todo_app_sqlite/src/todo.rs @@ -22,8 +22,9 @@ cfg_if! { } } -/// Server functions can be given doc comments. -#[server(GetTodos, "/api")] +/// This is an example of a server function using an alternative CBOR encoding. Both the function arguments being sent +/// to the server and the server response will be encoded with CBOR. Good for binary data that doesn't encode well via the default methods +#[server(encoding = "Cbor")] pub async fn get_todos() -> Result<Vec<Todo>, ServerFnError> { // this is just an example of how to access server context injected in the handlers let req = use_context::<actix_web::HttpRequest>(); @@ -44,9 +45,8 @@ pub async fn get_todos() -> Result<Vec<Todo>, ServerFnError> { Ok(todos) } -// This is an example of leptos's server functions using an alternative CBOR encoding. Both the function arguments being sent -// to the server and the server response will be encoded with CBOR. Good for binary data that doesn't encode well via the default methods -#[server(AddTodo, "/api", "Cbor")] + +#[server] pub async fn add_todo(title: String) -> Result<(), ServerFnError> { let mut conn = db().await?; diff --git a/server_fn_macro/src/lib.rs b/server_fn_macro/src/lib.rs index 275eb66315..cb4142bea4 100644 --- a/server_fn_macro/src/lib.rs +++ b/server_fn_macro/src/lib.rs @@ -13,7 +13,7 @@ use syn::{ parse::{Parse, ParseStream}, punctuated::Punctuated, spanned::Spanned, - *, + Type, *, }; /// The implementation of the `server_fn` macro. @@ -71,23 +71,43 @@ pub fn server_macro_impl( input, output, fn_path, + builtin_encoding, } = args; let prefix = prefix.unwrap_or_else(|| Literal::string(default_path)); let fn_path = fn_path.unwrap_or_else(|| Literal::string("")); - let input_ident = input - .as_ref() - .map(ToString::to_string) - .unwrap_or_else(|| "PostUrl".to_string()); - let input = input.map(|n| n.to_token_stream()).unwrap_or_else(|| { - quote! { - #server_fn_path::codec::PostUrl - } - }); - let output = output.map(|n| n.to_token_stream()).unwrap_or_else(|| { - quote! { - #server_fn_path::codec::Json + let input_ident = match &input { + Some(Type::Path(path)) => { + path.path.segments.last().map(|seg| seg.ident.to_string()) } - }); + None => Some("PostUrl".to_string()), + _ => None, + }; + let input = input + .map(|n| { + if builtin_encoding { + quote! { #server_fn_path::codec::#n } + } else { + n.to_token_stream() + } + }) + .unwrap_or_else(|| { + quote! { + #server_fn_path::codec::PostUrl + } + }); + let output = output + .map(|n| { + if builtin_encoding { + quote! { #server_fn_path::codec::#n } + } else { + n.to_token_stream() + } + }) + .unwrap_or_else(|| { + quote! { + #server_fn_path::codec::Json + } + }); // default to PascalCase version of function name if no struct name given let struct_name = struct_name.unwrap_or_else(|| { let upper_camel_case_name = Converter::new() @@ -316,10 +336,10 @@ pub fn server_macro_impl( } }; - let (is_serde, derives) = match input_ident.as_str() { - "Rkyv" => todo!("implement derives for Rkyv"), - "MultipartFormData" => (false, quote! {}), - "SerdeLite" => ( + let (is_serde, derives) = match input_ident.as_deref() { + Some("Rkyv") => todo!("implement derives for Rkyv"), + Some("MultipartFormData") => (false, quote! {}), + Some("SerdeLite") => ( true, quote! { Clone, #server_fn_path::serde_lite::Serialize, #server_fn_path::serde_lite::Deserialize @@ -470,6 +490,21 @@ pub fn server_macro_impl( }) } +fn type_from_ident(ident: Ident) -> Type { + let mut segments = Punctuated::new(); + segments.push(PathSegment { + ident, + arguments: PathArguments::None, + }); + Type::Path(TypePath { + qself: None, + path: Path { + leading_colon: None, + segments, + }, + }) +} + #[derive(Debug)] struct Middleware { expr: syn::Expr, @@ -555,9 +590,10 @@ fn err_type(return_ty: &Type) -> Result<Option<&GenericArgument>> { struct ServerFnArgs { struct_name: Option<Ident>, prefix: Option<Literal>, - input: Option<Ident>, - output: Option<Ident>, + input: Option<Type>, + output: Option<Type>, fn_path: Option<Literal>, + builtin_encoding: bool, } impl Parse for ServerFnArgs { @@ -569,8 +605,8 @@ impl Parse for ServerFnArgs { let mut fn_path: Option<Literal> = None; // new arguments: can only be keyed by name - let mut input: Option<Ident> = None; - let mut output: Option<Ident> = None; + let mut input: Option<Type> = None; + let mut output: Option<Type> = None; let mut use_key_and_value = false; let mut arg_pos = 0; @@ -698,23 +734,28 @@ impl Parse for ServerFnArgs { } // parse legacy encoding into input/output + let mut builtin_encoding = false; if let Some(encoding) = encoding { match encoding.to_string().to_lowercase().as_str() { "\"url\"" => { - input = syn::parse_quote!(PostUrl); - output = syn::parse_quote!(Json); + input = Some(type_from_ident(syn::parse_quote!(PostUrl))); + output = Some(type_from_ident(syn::parse_quote!(Json))); + builtin_encoding = true; } "\"cbor\"" => { - input = syn::parse_quote!(Cbor); - output = syn::parse_quote!(Cbor); + input = Some(type_from_ident(syn::parse_quote!(Cbor))); + output = Some(type_from_ident(syn::parse_quote!(Cbor))); + builtin_encoding = true; } "\"getcbor\"" => { - input = syn::parse_quote!(GetUrl); - output = syn::parse_quote!(Cbor); + input = Some(type_from_ident(syn::parse_quote!(GetUrl))); + output = Some(type_from_ident(syn::parse_quote!(Cbor))); + builtin_encoding = true; } "\"getjson\"" => { - input = syn::parse_quote!(GetUrl); - output = syn::parse_quote!(Json); + input = Some(type_from_ident(syn::parse_quote!(GetUrl))); + output = Some(type_from_ident(syn::parse_quote!(Json))); + builtin_encoding = true; } _ => { return Err(syn::Error::new( @@ -732,6 +773,7 @@ impl Parse for ServerFnArgs { input, output, fn_path, + builtin_encoding, }) } } From c7941f763987989de4beb89f2fffcdc77913d0d3 Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Wed, 10 Jan 2024 08:57:11 -0500 Subject: [PATCH 034/100] clippy --- server_fn_macro/src/lib.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/server_fn_macro/src/lib.rs b/server_fn_macro/src/lib.rs index cb4142bea4..b804bcb681 100644 --- a/server_fn_macro/src/lib.rs +++ b/server_fn_macro/src/lib.rs @@ -301,6 +301,9 @@ pub fn server_macro_impl( }} }; quote! { + // we need this for Actix, for the SendWrapper to count as impl Future + // but non-Actix will have a clippy warning otherwise + #[allow(clippy::manual_async_fn)] fn run_body(self) -> impl std::future::Future<Output = #return_ty> + Send { #body } From 7f532cda70a5160fd70195ddfb87ad97f4741f37 Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Wed, 10 Jan 2024 12:47:09 -0500 Subject: [PATCH 035/100] update todo_app_sqlite_csrs --- examples/todo_app_sqlite_csr/src/todo.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/examples/todo_app_sqlite_csr/src/todo.rs b/examples/todo_app_sqlite_csr/src/todo.rs index 7e6c15e906..0fb39a1642 100644 --- a/examples/todo_app_sqlite_csr/src/todo.rs +++ b/examples/todo_app_sqlite_csr/src/todo.rs @@ -20,11 +20,10 @@ pub async fn db() -> Result<SqliteConnection, ServerFnError> { Ok(SqliteConnection::connect("sqlite:Todos.db").await?) } -#[server(GetTodos, "/api")] +#[server] pub async fn get_todos() -> Result<Vec<Todo>, ServerFnError> { // this is just an example of how to access server context injected in the handlers - // http::Request doesn't implement Clone, so more work will be needed to do use_context() on this - let req_parts = use_context::<leptos_axum::RequestParts>(); + let req_parts = use_context::<http::request::Parts>(); if let Some(req_parts) = req_parts { println!("Uri = {:?}", req_parts.uri); From 2af0d3d781220b27776c76d5eff884570d7f209f Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Wed, 10 Jan 2024 14:05:24 -0500 Subject: [PATCH 036/100] update session_auth_axum --- examples/session_auth_axum/Todos.db | Bin 40960 -> 40960 bytes examples/session_auth_axum/src/auth.rs | 8 ++------ examples/session_auth_axum/src/main.rs | 15 ++++++++------- 3 files changed, 10 insertions(+), 13 deletions(-) diff --git a/examples/session_auth_axum/Todos.db b/examples/session_auth_axum/Todos.db index df5f7158d3af3828e608dc893e65260e667a775a..3f65a9407056ed189c8690a9d576eab6ca005526 100644 GIT binary patch delta 492 zcmZoTz|?SnX@WGP=tLQ3R#67Mq6HgM7VvX0@dq*R&*2Z+EGVGEpOVDH63SAUoM@b8 zW{_;Gn`~-op=)AjVxeo{0g`b|H8e}Jv@|o&HB2!#*ELB=HqlKqFfh@zNJ=p?H%l~6 zwJ<eLoo~0RS}7&5BvHw#x>iRiCqFN}B(*44$to=|r#Q8C@*MdLptb86_;2v9-z=!G zm|s(aIh2tYqnS3}(AQA_YW8B_U&8MNbh90QR0I<ngK#6Oae7jgicyk^p^-{KMRAJ0 zdA4_6h+bB9s!@q!PD-*_YEnc|RgPCgRiMACr9oJbXL?CRV5qCFaad`ZXO^q8k%5tk zu7RPhp@D*-rImq+m5Jr#CGiPB%ic5azvutHS<vAzzbremFeA_+CJttC&XUyP5<^}F z1_mbnR}B1L`CkE*+~XJHWMUR)EXhyFFJ|Us1TssDQ;Uk3HZ`yaa53|*WZ-|ye~f=6 o(BHH9!=;(o7#R6EjF~WPH#L<8`U@=SgyA$(6KO`Mq$3td01lgcssI20 delta 120 zcmZoTz|?SnX@WE(&qNt#Rvreuv{xHb7Vxt%^3P%5pR-v|VG{r3Ir14m{tX8H8=D0k z&hc-)p|7I=6j;K*zXYgY7XRcW@d-fwcLx6Nn*|%*@^f)8i*uHw7MB?EZf11&3gj~a U1xkxki;6cjun25sY52nr0K>E;G5`Po diff --git a/examples/session_auth_axum/src/auth.rs b/examples/session_auth_axum/src/auth.rs index 6dc78c269e..0a9c8caac2 100644 --- a/examples/session_auth_axum/src/auth.rs +++ b/examples/session_auth_axum/src/auth.rs @@ -162,9 +162,7 @@ pub async fn login( let user: User = User::get_from_username(username, &pool) .await - .ok_or_else(|| { - ServerFnError::ServerError("User does not exist.".into()) - })?; + .ok_or_else(|| ServerFnError::new("User does not exist."))?; match verify(password, &user.password)? { true => { @@ -207,9 +205,7 @@ pub async fn signup( User::get_from_username(username, &pool) .await .ok_or_else(|| { - ServerFnError::ServerError( - "Signup failed: User does not exist.".into(), - ) + ServerFnError::new("Signup failed: User does not exist.") })?; auth.login_user(user.id); diff --git a/examples/session_auth_axum/src/main.rs b/examples/session_auth_axum/src/main.rs index a321729fd6..9797d70d09 100644 --- a/examples/session_auth_axum/src/main.rs +++ b/examples/session_auth_axum/src/main.rs @@ -6,8 +6,8 @@ if #[cfg(feature = "ssr")] { use axum::{ response::{Response, IntoResponse}, routing::get, - extract::{Path, State, RawQuery}, - http::{Request, header::HeaderMap}, + extract::{Path, State}, + http::{Request}, body::Body as AxumBody, Router, }; @@ -21,12 +21,12 @@ if #[cfg(feature = "ssr")] { use axum_session::{SessionConfig, SessionLayer, SessionStore}; use axum_session_auth::{AuthSessionLayer, AuthConfig, SessionSqlitePool}; - async fn server_fn_handler(State(app_state): State<AppState>, auth_session: AuthSession, path: Path<String>, headers: HeaderMap, raw_query: RawQuery, + async fn server_fn_handler(State(app_state): State<AppState>, auth_session: AuthSession, path: Path<String>, request: Request<AxumBody>) -> impl IntoResponse { log!("{:?}", path); - handle_server_fns_with_context(path, headers, raw_query, move || { + handle_server_fns_with_context( move || { provide_context(auth_session.clone()); provide_context(app_state.pool.clone()); }, request).await @@ -58,10 +58,11 @@ if #[cfg(feature = "ssr")] { let auth_config = AuthConfig::<i64>::default(); let session_store = SessionStore::<SessionSqlitePool>::new(Some(pool.clone().into()), session_config).await.unwrap(); - sqlx::migrate!() + if let Err(e) = sqlx::migrate!() .run(&pool) - .await - .expect("could not run SQLx migrations"); + .await { + eprintln!("{e:?}"); + } // Explicit server function registration is no longer required // on the main branch. On 0.3.0 and earlier, uncomment the lines From 81fb5160e54d4d15f40e7b74fa83db44a45dbadb Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Wed, 10 Jan 2024 14:08:51 -0500 Subject: [PATCH 037/100] missing makefiles --- server_fn/Cargo.toml | 2 ++ server_fn/Makefile.toml | 1 + server_fn/server_fn_macro_default/Makfile.toml | 4 ++++ server_fn/src/error.rs | 8 +++++++- server_fn/src/response/browser.rs | 3 ++- server_fn_macro/Makefile.toml | 1 + 6 files changed, 17 insertions(+), 2 deletions(-) create mode 100644 server_fn/Makefile.toml create mode 100644 server_fn/server_fn_macro_default/Makfile.toml create mode 100644 server_fn_macro/Makefile.toml diff --git a/server_fn/Cargo.toml b/server_fn/Cargo.toml index 53dd8495ac..36027b4ecc 100644 --- a/server_fn/Cargo.toml +++ b/server_fn/Cargo.toml @@ -50,6 +50,7 @@ rkyv = { version = "0.7", features = [ # client gloo-net = { version = "0.5", optional = true } js-sys = { version = "0.3", optional = true } +wasm-bindgen = { version = "0.2", optional = true } wasm-bindgen-futures = { version = "0.4", optional = true } wasm-streams = { version = "0.4", optional = true } web-sys = { version = "0.3", optional = true, features = [ @@ -79,6 +80,7 @@ browser = [ "dep:gloo-net", "dep:js-sys", "dep:send_wrapper", + "dep:wasm-bindgen", "dep:web-sys", "dep:wasm-streams", "dep:wasm-bindgen-futures", diff --git a/server_fn/Makefile.toml b/server_fn/Makefile.toml new file mode 100644 index 0000000000..3d822c68da --- /dev/null +++ b/server_fn/Makefile.toml @@ -0,0 +1 @@ +extend = { path = "../cargo-make/main.toml" } diff --git a/server_fn/server_fn_macro_default/Makfile.toml b/server_fn/server_fn_macro_default/Makfile.toml new file mode 100644 index 0000000000..4ed6229141 --- /dev/null +++ b/server_fn/server_fn_macro_default/Makfile.toml @@ -0,0 +1,4 @@ +extend = { path = "../../cargo-make/main.toml" } + +[tasks.check-format] +env = { LEPTOS_PROJECT_DIRECTORY = "../../" } diff --git a/server_fn/src/error.rs b/server_fn/src/error.rs index ec0d4f38c4..df9eeb6f81 100644 --- a/server_fn/src/error.rs +++ b/server_fn/src/error.rs @@ -134,7 +134,7 @@ impl<E: std::error::Error + Clone> ViaError<E> for &&WrapError<E> { // we can still wrap it in String form impl<E: Display + Clone> ViaError<E> for &WrapError<E> { fn to_server_error(&self) -> ServerFnError<E> { - ServerFnError::WrappedServerError(self.0.clone()) + ServerFnError::ServerError(self.0.to_string()) } } @@ -179,6 +179,12 @@ pub enum ServerFnError<E = NoCustomError> { MissingArg(String), } +impl ServerFnError<NoCustomError> { + pub fn new(msg: impl ToString) -> Self { + Self::ServerError(msg.to_string()) + } +} + impl<CustErr> From<CustErr> for ServerFnError<CustErr> { fn from(value: CustErr) -> Self { ServerFnError::WrappedServerError(value) diff --git a/server_fn/src/response/browser.rs b/server_fn/src/response/browser.rs index 845b2db54d..4c8701ba8c 100644 --- a/server_fn/src/response/browser.rs +++ b/server_fn/src/response/browser.rs @@ -3,9 +3,10 @@ use crate::{error::ServerFnError, redirect::REDIRECT_HEADER}; use bytes::Bytes; use futures::{Stream, StreamExt}; pub use gloo_net::http::Response; -use js_sys::{wasm_bindgen::JsCast, Uint8Array}; +use js_sys::Uint8Array; use send_wrapper::SendWrapper; use std::future::Future; +use wasm_bindgen::JsCast; use wasm_streams::ReadableStream; pub struct BrowserResponse(pub(crate) SendWrapper<Response>); diff --git a/server_fn_macro/Makefile.toml b/server_fn_macro/Makefile.toml new file mode 100644 index 0000000000..3d822c68da --- /dev/null +++ b/server_fn_macro/Makefile.toml @@ -0,0 +1 @@ +extend = { path = "../cargo-make/main.toml" } From 6c8e704fb37be45b2a82b91a44de11d520935126 Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Wed, 10 Jan 2024 14:28:14 -0500 Subject: [PATCH 038/100] smh --- server_fn/server_fn_macro_default/{Makfile.toml => Makefile.toml} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename server_fn/server_fn_macro_default/{Makfile.toml => Makefile.toml} (100%) diff --git a/server_fn/server_fn_macro_default/Makfile.toml b/server_fn/server_fn_macro_default/Makefile.toml similarity index 100% rename from server_fn/server_fn_macro_default/Makfile.toml rename to server_fn/server_fn_macro_default/Makefile.toml From a5cbfa0aadd5fd27a6fcce1fadca2eb2a42386eb Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Wed, 10 Jan 2024 20:15:30 -0500 Subject: [PATCH 039/100] remove viz integration (see #2177) --- .github/workflows/ci.yml | 1 - Cargo.toml | 1 - examples/Makefile.toml | 1 - examples/todo_app_sqlite_viz/Cargo.toml | 91 - examples/todo_app_sqlite_viz/LICENSE | 21 - examples/todo_app_sqlite_viz/Makefile.toml | 8 - examples/todo_app_sqlite_viz/README.md | 15 - examples/todo_app_sqlite_viz/Todos.db | Bin 16384 -> 0 bytes .../20221118172000_create_todo_table.sql | 7 - .../todo_app_sqlite_viz/public/favicon.ico | Bin 15406 -> 0 bytes .../todo_app_sqlite_viz/rust-toolchain.toml | 2 - .../todo_app_sqlite_viz/src/error_template.rs | 60 - examples/todo_app_sqlite_viz/src/errors.rs | 21 - examples/todo_app_sqlite_viz/src/fallback.rs | 58 - examples/todo_app_sqlite_viz/src/lib.rs | 24 - examples/todo_app_sqlite_viz/src/main.rs | 85 - examples/todo_app_sqlite_viz/src/todo.rs | 200 --- examples/todo_app_sqlite_viz/style.css | 3 - integrations/viz/Cargo.toml | 25 - integrations/viz/Makefile.toml | 4 - integrations/viz/src/lib.rs | 1549 ----------------- leptos/src/lib.rs | 5 +- leptos_config/src/lib.rs | 2 +- router/src/components/redirect.rs | 3 +- router/src/extract_routes.rs | 6 +- 25 files changed, 6 insertions(+), 2186 deletions(-) delete mode 100644 examples/todo_app_sqlite_viz/Cargo.toml delete mode 100644 examples/todo_app_sqlite_viz/LICENSE delete mode 100644 examples/todo_app_sqlite_viz/Makefile.toml delete mode 100644 examples/todo_app_sqlite_viz/README.md delete mode 100644 examples/todo_app_sqlite_viz/Todos.db delete mode 100644 examples/todo_app_sqlite_viz/migrations/20221118172000_create_todo_table.sql delete mode 100644 examples/todo_app_sqlite_viz/public/favicon.ico delete mode 100644 examples/todo_app_sqlite_viz/rust-toolchain.toml delete mode 100644 examples/todo_app_sqlite_viz/src/error_template.rs delete mode 100644 examples/todo_app_sqlite_viz/src/errors.rs delete mode 100644 examples/todo_app_sqlite_viz/src/fallback.rs delete mode 100644 examples/todo_app_sqlite_viz/src/lib.rs delete mode 100644 examples/todo_app_sqlite_viz/src/main.rs delete mode 100644 examples/todo_app_sqlite_viz/src/todo.rs delete mode 100644 examples/todo_app_sqlite_viz/style.css delete mode 100644 integrations/viz/Cargo.toml delete mode 100644 integrations/viz/Makefile.toml delete mode 100644 integrations/viz/src/lib.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a07332c39e..ffedbf2106 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,7 +22,6 @@ jobs: [ integrations/actix, integrations/axum, - integrations/viz, integrations/utils, leptos, leptos_config, diff --git a/Cargo.toml b/Cargo.toml index 7cccce8d38..bb50dfc470 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,6 @@ members = [ # integrations "integrations/actix", "integrations/axum", - "integrations/viz", "integrations/utils", # libraries diff --git a/examples/Makefile.toml b/examples/Makefile.toml index 4373546f68..ec6dffcb95 100644 --- a/examples/Makefile.toml +++ b/examples/Makefile.toml @@ -32,7 +32,6 @@ CARGO_MAKE_CRATE_WORKSPACE_MEMBERS = [ "timer", "todo_app_sqlite", "todo_app_sqlite_axum", - "todo_app_sqlite_viz", "todomvc", ] diff --git a/examples/todo_app_sqlite_viz/Cargo.toml b/examples/todo_app_sqlite_viz/Cargo.toml deleted file mode 100644 index bd65059a30..0000000000 --- a/examples/todo_app_sqlite_viz/Cargo.toml +++ /dev/null @@ -1,91 +0,0 @@ -[package] -name = "todo_app_sqlite_viz" -version = "0.1.0" -edition = "2021" - -[lib] -crate-type = ["cdylib", "rlib"] - -[dependencies] -console_log = "1.0.0" -console_error_panic_hook = "0.1.7" -futures = "0.3.25" -cfg-if = "1.0.0" -leptos = { path = "../../leptos" } -leptos_viz = { path = "../../integrations/viz", optional = true } -leptos_meta = { path = "../../meta", features = ["nightly"] } -leptos_router = { path = "../../router", features = ["nightly"] } -leptos_reactive = { path = "../../leptos_reactive", features = ["nightly"] } -log = "0.4.17" -simple_logger = "4.0.0" -serde = { version = "1", features = ["derive"] } -viz = { version = "0.4.8", features = ["serve"], optional = true } -tokio = { version = "1.25.0", features = ["full"], optional = true } -http = { version = "0.2.11" } -sqlx = { version = "0.6.2", features = [ - "runtime-tokio-rustls", - "sqlite", -], optional = true } -thiserror = "1.0.38" -wasm-bindgen = "0.2" - -[features] -csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"] -hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"] -ssr = [ - "dep:viz", - "dep:tokio", - "dep:sqlx", - "leptos/ssr", - "leptos_meta/ssr", - "leptos_router/ssr", - "dep:leptos_viz", -] - -[package.metadata.cargo-all-features] -denylist = ["viz", "tokio", "sqlx", "leptos_viz"] -skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]] - -[package.metadata.leptos] -# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name -output-name = "todo_app_sqlite_viz" -# The site root folder is where cargo-leptos generate all output. WARNING: all content of this folder will be erased on a rebuild. Use it in your server setup. -site-root = "target/site" -# The site-root relative folder where all compiled output (JS, WASM and CSS) is written -# Defaults to pkg -site-pkg-dir = "pkg" -# [Optional] The source CSS file. If it ends with .sass or .scss then it will be compiled by dart-sass into CSS. The CSS is optimized by Lightning CSS before being written to <site-root>/<site-pkg>/app.css -style-file = "./style.css" -# [Optional] Files in the asset-dir will be copied to the site-root directory -assets-dir = "public" -# The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup. -site-addr = "127.0.0.1:3000" -# The port to use for automatic reload monitoring -reload-port = 3001 -# [Optional] Command to use when running end2end tests. It will run in the end2end dir. -end2end-cmd = "npx playwright test" -# The browserlist query used for optimizing the CSS. -browserquery = "defaults" -# Set by cargo-leptos watch when building with tha tool. Controls whether autoreload JS will be included in the head -watch = false -# The environment Leptos will run in, usually either "DEV" or "PROD" -env = "DEV" -# The features to use when compiling the bin target -# -# Optional. Can be over-ridden with the command line parameter --bin-features -bin-features = ["ssr"] - -# If the --no-default-features flag should be used when compiling the bin target -# -# Optional. Defaults to false. -bin-default-features = false - -# The features to use when compiling the lib target -# -# Optional. Can be over-ridden with the command line parameter --lib-features -lib-features = ["hydrate"] - -# If the --no-default-features flag should be used when compiling the lib target -# -# Optional. Defaults to false. -lib-default-features = false diff --git a/examples/todo_app_sqlite_viz/LICENSE b/examples/todo_app_sqlite_viz/LICENSE deleted file mode 100644 index 77d5625cb3..0000000000 --- a/examples/todo_app_sqlite_viz/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2022 Greg Johnston - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/examples/todo_app_sqlite_viz/Makefile.toml b/examples/todo_app_sqlite_viz/Makefile.toml deleted file mode 100644 index 8f4c607c86..0000000000 --- a/examples/todo_app_sqlite_viz/Makefile.toml +++ /dev/null @@ -1,8 +0,0 @@ -extend = [ - { path = "../cargo-make/main.toml" }, - { path = "../cargo-make/cargo-leptos.toml" }, -] - -[env] - -CLIENT_PROCESS_NAME = "todo_app_sqlite_viz" diff --git a/examples/todo_app_sqlite_viz/README.md b/examples/todo_app_sqlite_viz/README.md deleted file mode 100644 index d17a34644d..0000000000 --- a/examples/todo_app_sqlite_viz/README.md +++ /dev/null @@ -1,15 +0,0 @@ -# Leptos Todo App Sqlite with Viz - -This example creates a basic todo app with a Viz backend that uses Leptos' server functions to call sqlx from the client and seamlessly run it on the server. - -## Getting Started - -See the [Examples README](../README.md) for setup and run instructions. - -## Rendering - -See the [SSR Notes](../SSR_NOTES.md) for more information about Server Side Rendering. - -## Quick Start - -Run `cargo leptos watch` to run this example. diff --git a/examples/todo_app_sqlite_viz/Todos.db b/examples/todo_app_sqlite_viz/Todos.db deleted file mode 100644 index c9e91811510280b8e0884336ca7221cd3be5151a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16384 zcmeI&y>HV%7zg09oi-o+FoGc!2<c#fN~9$z5>*F+Q%s3)lHxju2niGHD_FJbHg+JO zE=4RbF@Zn=Cin*s6C>RiU}8cH(4{L9h>>&BM3A8yNTA;<pPlo2=e&M0U33fQG$&+q z(QkP5kWP_ZL=ebHN(muDyv2CyhR8qs+84Ts9j-%U;$m(@_K7qq68XlcC{OG#8Yu`s z00Izz00bZa0SG_<0ucCv0=LJ-csi32?hJ(WD=ssGRkvk&&ecXebo}L@zc0<|s$!_r zPzst#`?K_Lit@P58Ude73&pu&*`V`!v83oVdRDC+Y3FSgSPf?-`T}aG7Ytgi@K)8d zPTE-x!n*4++l=zXlDc3hrTN|<dP+U5R5gRnR&`x18)k1vXL``IEEWW`P^oCDQtr*` zWUXt=S_+yTEohZOUyiky)r_h#L&sy?^1H>Q#`Dr(dLbhaXW3>g4#k(wX1y8u?Y`NM zVP^a9M9b$y^joeE3h{J0EvzrK(L>+%gZ4@MjkkR(6`4AA<k|^Ns?Mo;ul9c5&<S0( zJ@&k!&z@2A=+!OXTX9*)q9<+Bq{c^hF_Qd|$Y13xenCJ00uX=z1Rwwb2tWV=5P$## zAn=a~3`sGvFEQWn7oG4xT#S)DiF`gkEX2tEM4@?;dXBpkt^Wo2G2sUS0uX=z1Rwwb z2tWV=5P$##AOL~CE?^7sfyt?)EA6(&?!UckHCUZ*)Q@%q(Dt5z+|gVvlg(zb(=<Ch zlRGw(JzjbK^zMU=gKNXD_AZW{@M}*tzsZL-Hov_2ap~Rr7xx~?pC`8-HrFbjq+2gP wyw;z^%d08=UV{9c@B;w>2tWV=5P$##AOHafKmY;|fWUtxkdy|Joxc@+0{oBIF#rGn diff --git a/examples/todo_app_sqlite_viz/migrations/20221118172000_create_todo_table.sql b/examples/todo_app_sqlite_viz/migrations/20221118172000_create_todo_table.sql deleted file mode 100644 index 3c2908e53c..0000000000 --- a/examples/todo_app_sqlite_viz/migrations/20221118172000_create_todo_table.sql +++ /dev/null @@ -1,7 +0,0 @@ - -CREATE TABLE IF NOT EXISTS todos -( - id INTEGER NOT NULL PRIMARY KEY, - title VARCHAR, - completed BOOLEAN -); \ No newline at end of file diff --git a/examples/todo_app_sqlite_viz/public/favicon.ico b/examples/todo_app_sqlite_viz/public/favicon.ico deleted file mode 100644 index 2ba8527cb12f5f28f331b8d361eef560492d4c77..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15406 zcmeHOd3aPs5`TblWD*3D%tXPJ#q(n!z$P=3gCjvf#a)E}a;Uf>h{pmVih!a-5LVO` zB?JrzEFicD0wRLo0iPfO372xnkvkzFlRHB)lcTnNZ}KK@US{UKN#b8?e_zkLy1RZ= zT~*y(-6IICgf>E_P6A)M3(wvl2qr-gx_5Ux-_uzT*6_Q&ee1v9B?vzS3&K5IhO2N5 z$9ukLN<`G>$$|GLnga~y%>f}*j%+w@(ixVUb^1_Gjoc;(?TrD3m2)RduFblVN)uy; zQAEd^T{5>-YYH%|Kv{V^cxHMBr1Ik<Vknc_c<}b#F7>7Frht$i<ZW=>mC`rqx@5*| z+OqN!xAjqmaU=qR$uGDMa7p!W9oZ+64($4xDk^FyFQ<_9Z`(;DLnB<;LLJD1<&vnZ zo0(>zIkQTse}qNMb6+i`th54(3pKm8;UAJ<_BULR*Z=m5FU7jiW(&#l+}WkHZ|e@1 z`pm;Q^pCuLUQUrnQ(hPM10pSSHQS=Bf8DqG1&!-B!oQQ|FuzLruL1w(+g<8&znyI? zzX-}?SwUvNjEuT?7uUOy{Fb@xKklpj+jdYM^IK9}NxvLRZd{l9FHEQJ4IO~q%4I0O zAN|*8x^nIU4Giw?f*tmNx=7H)2-Zn?J^B6SgpcW3ZXV_57Sn%Mtfr_=w|sYpAhdJT zcKo6Z*oIOU(az~3$LOEWm9Q)dYWMA}T7L23MVGqrcA%4H)+^`+=j+Hh8CTCnnG2Rh zgcXVW%F8$R9)6}f=NQiLPt8qt3xNUQI>Q*)H1lzk<&n?XR-f}tc&9V0H0lhGqHJ^N zN%h(9-Of2_)!Xk{qdIkU>1%mk%I_Id1!MU*yq&&>)Q+!L^t&-2<em6urrIod`3!;R z=Wl<Y!MEJ02?OvzHtZ7@%YC929AWK(Z|8caC7$l@-jj~(|6as!`sd>mW9Xq7g9C@* zl&PKJ&su2L+iku?Te?Pf?k3tUK){Bj_gb&aPo8Ago^XI~mRTd(5{&^tf1)!-lSMha z@$~ae!r(~`=p&|mMxy2EiZQ6FvXb(1avS*`Pj%$)*?vwceGKHmHnl`v&fEQ_Wh+G) zEPQ^3&oV%}%;zF`AM|S%d<Va?d!s3R8`U?Gc5T=!(zn}eR8m+-=T4lW_5WVR<Fo5n zEY(q7nO2p&AJ`YJQkGFZM|ZiEHhi$0s;jF-+BRDqrKeX@24}myPJCBwucrUJ{}Dk~ zmGza+>>pM@1}33PN5*4SewROk_K$n^i8QjaYiRzwG8#OvVIF|{x85wH+?*P*%)woI zR538k@=(E`V;p1UwA|fqSh`$n_t;Sz4T)`_s~pRR4lbmWWSdxa-FqLZ%fLT)B<thH z?r2KyM)!-N8kRZu_C{O60t~siHB@c4H=0*TZAw>h?iye?COx~mO1wkn5)HNMg7`8~ z25VJhz&3Z7`M>6luJrEw$<qW~i<R}a8vT1?EUc$>Jikft+6SxyIh?)PU1?DfrKMGC z=3T;;omE4H`PWqF8?0*dOA3o9y@~WK`S}{?tIHquEw?v`M^D%Lobpdrp%3}1=-&qk zqAtb1p<Vype#@bMYnCkaPT!$_UhNP57CtYBean!+o^4-}#r^jd($%XqWVhLAL@$#X z{RH+uV<cVobcJt6N<MC*p<Xb6_K6gS|5>x-1Fy6}E8IUg4s%8B0~P<<jSlYKD`J3; zT`<3l?j6)13-tFwmO1!F`hMqbic%Q^Sntcigq!`uF(AN@<cW9beRP*@;@ASeh6MZ0 zVjDxoJrZONzSU@>P5C;de%@n~XnDKF@fr$a+^@$^P|>vlw($aSK2lRtLt~8tRb`I0 znfI!G?K|<5ry*gk>y56rZy0NkK6)))6Mg1=K?7yS9p+#1Ij=W*%5Rt-mlc;#MOnE9 zoi`-+6oj@)`gq2Af!B+9%J#K9V=ji2dj2<_qaLSXOCeqQ<t!!6U*w!8vfOi(a#!Mr z;I(g4F=Sl2B4U6(W@gq<Rh2_8&+wXIkfBE|uhE!w^@O<@+vF)Nd`o5Cob-Z7`<F9z z8veIJalFD@;Jf`*c%O1MCB;SG)KJvx!(x`1Cc8NL{Xwd$tD{jPikxF*l&PR<NNnLk zrtfiGu7(3T!9H>&<0zMSb$5mAi;HU=v`v<>NYk}MbD!ewYVB+N-ctzn=l&bTwv)*7 zmY<+Y@SBbtl9PPk$HTR?ln@(T92XjTRj0Mx|Mzl;lW>Su_y^~fh?8(L?oz8h!cCpb zZG-OY=NJ3{>r*`U<(J%#zjFT-a9>u6+23H{=d(utkgqt7@^)C;pkb)fQ|Q=*8*SyT z;otKe+f8fEp)ZacKZDn3TNzs><E(S1|L)$QEc^6q=5?o0r=Mx`3x=rM{GOPlwi$N_ z=T)5Zax*gRvmbw&B5%4y)A;0p7d!Kl&vD^Re-S$0D$!}_t5FCD4A<$WFL`Npar$qU z#I+amK;<Q+v}o!~8mM=Tce=x>_Kx+g*c_mr8LBhr8GnoEmAQk#%sR52<z$1b$A%B2 zt*h3G^AqrZxXIdgm(qRR?rw5FIC<lBlgZz(H>`bdbW8Ms$<Hvc-WFZ-8}a3r$4n6C zTHwJ}W#d>!0u2bdt=T-lK3JbDW`F(Urt%Ob2seiN>7U`YN}aOdIiCC;eeufJC#m3S z9#|l2c?G@t*hH5y^76jkv)rs4H+;oiTuY5FQwRMN_7NUqeiD|b&RyxPXQz|3qC(_> zZJMwjC4F!1m2INXqzisQ4X^w=>&(+Ecdu&~IWEMn7f*YcYI&eWI(6hI#f114%aymM zyhlG6{q>XN7(LyGiMAS&qijR%d2rV|>AUT_sE&EKUSTCM26>aKzNxk0?K|utOcxl# zxIOwM#O!!H+QzbX*&p=QuKe4y;bS>&StQOE5AEGg_ubk8{;1yOVAJfE_Js-lL7rr9 z)CEuFIlkApj~uV^zJK7KocjT=<q6S4TFeeWi+QLv1?XqE!ynFLANNs-5|h?vU?0)j zM4v5B5^VrK7~8JpUZo4C&d`?}TSjajvE=6XShnY)K0=qS3Le_<yvpOAq8bn5%`u|y zPrdLP)$)4VQ-XbGUQVTrOB3m_h}b6g4bPcYGyc{R58*d<?!`8qu7*?j9sYmTxTh#5 zJ;UjHfW4;15ko(G*hYsBRZ;4dYK~<%d=tKdkG!lLik~u#A~o*4xzOgdw6Q~Acs>4B zJP(}0x}|A7C$$5gIp>K<R9aLFNMfwz%H?WWzN~^Ce#o)Tlwx1F;@z?jE9lZi=B0jL zpp5lv-o)p8=7F)=S&!y1{#ICj@%<(Vm)5Hs`}ON}v}vQ2#*A!Oqsp<%??*mTNE_E% zsj||shA(A*HFv@@KI;<uqTN_~g!u*C%(|1M6*tNuo|Jjn5mTtFtfl!J1059I5O<T~ zb$5@lug@tZ@Qsw0l}!`+`{t_{b4~>BPZ|A#2Ew;$#g9Fk)r;Q~?G$>x<+JM)J3u>j zi68K=I;ld`JJ<u}v;BD=QY#K%4r`|$!sGJmTI-<PuscG<SQ6xxl~qk+MyczJgjsoo zF2S~u;Fr|m!b+TMw{Nf>?Nq+^_B?C+Q%+x#m{9JF$tbaDeNIep%=^#>KHGtg=L)>m z_J&vaZTs2{qP!4Gdw5u5Kcf}5R4(q}Lebx%(J$7l*Q`Il#pCTM%!`y5y*-~zIVs}D z9;t+(xmV~R65^ZQXe+<5{$QW0O8MT~a{<o>kdFLR)nfRMA9L(YU>x*DTltN#m-2km zC;T`cfb{c`mcx(z7o_a8bYJn8_^dz4Cq!D<UvIA7NcFR`9r}YaEJ_)BduHF0!#bpT zHbdUV)>Z37{P6uF{@#519UWK1{>(9sZB1I^6MmNc39MJ-_|)!S8vO+O3&$MulU3Gc z_W{N*B(yneyl-oN_MKaJ{CZ6dv-~^8uPbLSh&0jfV@EfA{2Dc!_rOyfx`R0T@LonA z<*%O?-aa_Wm-z$s@K(ex7UhM0-?9C=PkYdk&d2n((E4>&(f4D`fOQY%CURMMyJyU` zVeJBAId&StHjw76tnwSqZs3e0683`L{a3k9JYdg#(ZVw4J`&CkV-2LF<L#N}Z<*NN zte-!B>aDE1Z?CehVy%vZx$tM3tTax8E@2;N^QTrPcI?Ob8uK!DM0_sfE6ks<hCyt_ z*KrJMFT@*)3M?WI&?W3suUlhv*{hQD1-L(1T_NuG!*?Np|Cx_Y@Hu|XSZ(Y(=V1K; z{9$ba?_qvY-O1V8JMi#g+<3O<^G=@xT;K&~eX!?`j58+^W_-uFp|lGZ#q}H7@J7Sk zH^!ff^N7G+pIT%8%UxM5@35Z1UiD=KAHXV4*hkFaF&6vmTKCl5(PykljN7?>2M?iw zPS4{(k-PF*-oY<D(4!YkA2Ck!B_|IZ5wT{cWl;LX%i<XX;QxB_=RThkmD6XtEx=wx zz1&?cYzLElwF7zEp6(yItFPEM=nKqo#)J>>S!d9;L+|xdTtLen9B2LvpL4k;#ScB< z$NP_7j~7)5eXuoYEk*dK_rSz9yT_C4B{r~^#^o}-VQI=Y?01|$aa!a7=UEm$|DsQQ zfLK1qmho2@)nwA?$1%T6jwO2HZ({6&;`s|OQOxI4S8*Hw=Qp!b(<qJi)V|^WJ78Ra z9Yx4u*Nmk&To*J}6}YT`*-t;2d2Z5~5l{FL_gu+y5A2tDN;rpf_?vH?@f5~hVDk5@ z^D@ZF+ctdW<gu3S2gJRs<>gNJR%<PtJYl1A=j_h&y08)K<2=$cyn=novkdG8B{;3m zX6`q(hYaSU*)~0t&l4pdJS1Yb@w{p07nStjkcKq`=CX*F+U*>SAj&wGa>^&2@x)Vj zhd^WfzJ^b0O{E^q82Pw({uT`E`MT2WnZ02{E%t*yRPN>?W>0vU^4@Vyh4;mLj918c z*s*papo?<}cQM{5lcgZScx}?usg{mS!KkH9U%@|^_33?{FI{1ss+8kXyFY&5M-e~f zM$){FF;_+z3sNJ)Er~{Beux$fEl{R4|7WKcpEsGtK57f+H0DJ$hI;U;JtF>+lG@sV zQI_;bQ^7XIJ>Bs?C32b1v;am;P4GUqAJ#zOHv}4SmV|xXX6~O9&e_~YCCpbT>s$`! k<4FtN!5<V};cOZ-C_kkBJe0@%L`?wxDD;N}njwMz0Wv;ivH$=8 diff --git a/examples/todo_app_sqlite_viz/rust-toolchain.toml b/examples/todo_app_sqlite_viz/rust-toolchain.toml deleted file mode 100644 index 5d56faf9ae..0000000000 --- a/examples/todo_app_sqlite_viz/rust-toolchain.toml +++ /dev/null @@ -1,2 +0,0 @@ -[toolchain] -channel = "nightly" diff --git a/examples/todo_app_sqlite_viz/src/error_template.rs b/examples/todo_app_sqlite_viz/src/error_template.rs deleted file mode 100644 index 6a367604ee..0000000000 --- a/examples/todo_app_sqlite_viz/src/error_template.rs +++ /dev/null @@ -1,60 +0,0 @@ -use crate::errors::TodoAppError; -use cfg_if::cfg_if; -use leptos::{Errors, *}; -#[cfg(feature = "ssr")] -use leptos_viz::ResponseOptions; - -// A basic function to display errors served by the error boundaries. Feel free to do more complicated things -// here than just displaying them -#[component] -pub fn ErrorTemplate( - #[prop(optional)] outside_errors: Option<Errors>, - #[prop(optional)] errors: Option<RwSignal<Errors>>, -) -> impl IntoView { - let errors = match outside_errors { - Some(e) => create_rw_signal(e), - None => match errors { - Some(e) => e, - None => panic!("No Errors found and we expected errors!"), - }, - }; - - // Get Errors from Signal - // Downcast lets us take a type that implements `std::error::Error` - let errors: Vec<TodoAppError> = errors - .get() - .into_iter() - .filter_map(|(_k, v)| v.downcast_ref::<TodoAppError>().cloned()) - .collect(); - - // Only the response code for the first error is actually sent from the server - // this may be customized by the specific application - cfg_if! { - if #[cfg(feature="ssr")]{ - let response = use_context::<ResponseOptions>(); - if let Some(response) = response{ - response.set_status(errors[0].status_code()); - } - } - } - - view! { - <h1>"Errors"</h1> - <For - // a function that returns the items we're iterating over; a signal is fine - each= move || {errors.clone().into_iter().enumerate()} - // a unique key for each item as a reference - key=|(index, _error)| *index - // renders each item to a view - children= move |error| { - let error_string = error.1.to_string(); - let error_code= error.1.status_code(); - view! { - - <h2>{error_code.to_string()}</h2> - <p>"Error: " {error_string}</p> - } - } - /> - } -} diff --git a/examples/todo_app_sqlite_viz/src/errors.rs b/examples/todo_app_sqlite_viz/src/errors.rs deleted file mode 100644 index 1f1aea92fa..0000000000 --- a/examples/todo_app_sqlite_viz/src/errors.rs +++ /dev/null @@ -1,21 +0,0 @@ -use http::status::StatusCode; -use thiserror::Error; - -#[derive(Debug, Clone, Error)] -pub enum TodoAppError { - #[error("Not Found")] - NotFound, - #[error("Internal Server Error")] - InternalServerError, -} - -impl TodoAppError { - pub fn status_code(&self) -> StatusCode { - match self { - TodoAppError::NotFound => StatusCode::NOT_FOUND, - TodoAppError::InternalServerError => { - StatusCode::INTERNAL_SERVER_ERROR - } - } - } -} diff --git a/examples/todo_app_sqlite_viz/src/fallback.rs b/examples/todo_app_sqlite_viz/src/fallback.rs deleted file mode 100644 index 97740ead60..0000000000 --- a/examples/todo_app_sqlite_viz/src/fallback.rs +++ /dev/null @@ -1,58 +0,0 @@ -use cfg_if::cfg_if; - -cfg_if! { -if #[cfg(feature = "ssr")] { - use crate::{ - error_template::ErrorTemplate, - errors::TodoAppError, - }; - use http::Uri; - use std::sync::Arc; - use leptos::{view, Errors, LeptosOptions}; - use viz::{ - handlers::serve, header::HeaderMap, types::RouteInfo, Body, Error, Handler, - Request, RequestExt, Response, ResponseExt, Result, - }; - - pub async fn file_and_error_handler(req: Request<Body>) -> Result<Response> { - let uri = req.uri().clone(); - let headers = req.headers().clone(); - let route_info = req.route_info().clone(); - let options = req.state::<LeptosOptions>().ok_or( - Error::Responder(Response::text("missing state type LeptosOptions")), - )?; - let root = &options.site_root; - let resp = get_static_file(uri, root, headers, route_info).await?; - let status = resp.status(); - - if status.is_success() || status.is_redirection() { - Ok(resp) - } else { - let mut errors = Errors::default(); - errors.insert_with_default_key(TodoAppError::NotFound); - let handler = leptos_viz::render_app_to_stream( - options.to_owned(), - move || view! {<ErrorTemplate outside_errors=errors.clone()/>}, - ); - handler(req).await - } - } - - async fn get_static_file( - uri: Uri, - root: &str, - headers: HeaderMap, - route_info: Arc<RouteInfo>, - ) -> Result<Response> { - let mut req = Request::builder() - .uri(uri.clone()) - .extension(route_info) - .body(Body::empty()) - .unwrap(); - *req.headers_mut() = headers; - // This path is relative to the cargo root - serve::Dir::new(root).call(req).await - } - -} -} diff --git a/examples/todo_app_sqlite_viz/src/lib.rs b/examples/todo_app_sqlite_viz/src/lib.rs deleted file mode 100644 index 2dd1a23f2e..0000000000 --- a/examples/todo_app_sqlite_viz/src/lib.rs +++ /dev/null @@ -1,24 +0,0 @@ -use cfg_if::cfg_if; -pub mod error_template; -pub mod errors; -pub mod fallback; -pub mod todo; - -// Needs to be in lib.rs AFAIK because wasm-bindgen needs us to be compiling a lib. I may be wrong. -cfg_if! { - if #[cfg(feature = "hydrate")] { - use leptos::*; - use wasm_bindgen::prelude::wasm_bindgen; - use crate::todo::*; - - #[wasm_bindgen] - pub fn hydrate() { - _ = console_log::init_with_level(log::Level::Debug); - console_error_panic_hook::set_once(); - - leptos::mount_to_body(|| { - view! { <TodoApp/> } - }); - } - } -} diff --git a/examples/todo_app_sqlite_viz/src/main.rs b/examples/todo_app_sqlite_viz/src/main.rs deleted file mode 100644 index f0f1bced19..0000000000 --- a/examples/todo_app_sqlite_viz/src/main.rs +++ /dev/null @@ -1,85 +0,0 @@ -use cfg_if::cfg_if; - -// boilerplate to run in different modes -cfg_if! { - if #[cfg(feature = "ssr")] { - use leptos::*; - use crate::fallback::file_and_error_handler; - use crate::todo::*; - use leptos_viz::{generate_route_list, LeptosRoutes}; - use todo_app_sqlite_viz::*; - use viz::{ - types::{State, StateError}, - Request, RequestExt, Response, Result, Router, ServiceMaker, - }; - - //Define a handler to test extractor with state - async fn custom_handler(req: Request) -> Result<Response> { - let id = req.params::<String>()?; - let options = req - .state::<LeptosOptions>() - .ok_or(StateError::new::<LeptosOptions>())?; - let handler = leptos_viz::render_app_to_stream_with_context( - options.clone(), - move || { - provide_context(id.clone()); - }, - TodoApp, - ); - handler(req).await - } - - #[tokio::main] - async fn main() { - simple_logger::init_with_level(log::Level::Debug) - .expect("couldn't initialize logging"); - - let _conn = db().await.expect("couldn't connect to DB"); - /* sqlx::migrate!() - .run(&mut conn) - .await - .expect("could not run SQLx migrations"); */ - - // Explicit server function registration is no longer required - // on the main branch. On 0.3.0 and earlier, uncomment the lines - // below to register the server functions. - // _ = GetTodos::register(); - // _ = AddTodo::register(); - // _ = DeleteTodo::register(); - - // Setting this to None means we'll be using cargo-leptos and its env vars - let conf = get_configuration(None).await.unwrap(); - let leptos_options = conf.leptos_options; - let addr = leptos_options.site_addr; - let routes = generate_route_list(TodoApp); - - // build our application with a route - let app = Router::new() - .post("/api/:fn_name*", leptos_viz::handle_server_fns) - .get("/special/:id", custom_handler) - .leptos_routes( - leptos_options.clone(), - routes, - TodoApp, - ) - .get("/*", file_and_error_handler) - .with(State(leptos_options)); - - // run our app with hyper - // `viz::Server` is a re-export of `hyper::Server` - logging::log!("listening on http://{}", &addr); - viz::Server::bind(&addr) - .serve(ServiceMaker::from(app)) - .await - .unwrap(); - } -} - - // client-only stuff for Trunk - else { - pub fn main() { - // This example cannot be built as a trunk standalone CSR-only app. - // Only the server may directly connect to the database. - } - } -} diff --git a/examples/todo_app_sqlite_viz/src/todo.rs b/examples/todo_app_sqlite_viz/src/todo.rs deleted file mode 100644 index 59681070c2..0000000000 --- a/examples/todo_app_sqlite_viz/src/todo.rs +++ /dev/null @@ -1,200 +0,0 @@ -use crate::error_template::ErrorTemplate; -use cfg_if::cfg_if; -use leptos::*; -use leptos_meta::*; -use leptos_router::*; -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[cfg_attr(feature = "ssr", derive(sqlx::FromRow))] -pub struct Todo { - id: u16, - title: String, - completed: bool, -} - -cfg_if! { - if #[cfg(feature = "ssr")] { - use sqlx::{Connection, SqliteConnection}; - // use http::{header::SET_COOKIE, HeaderMap, HeaderValue, StatusCode}; - - pub async fn db() -> Result<SqliteConnection, ServerFnError> { - Ok(SqliteConnection::connect("sqlite:Todos.db").await?) - } - } -} - -#[server(GetTodos, "/api")] -pub async fn get_todos() -> Result<Vec<Todo>, ServerFnError> { - // this is just an example of how to access server context injected in the handlers - // http::Request doesn't implement Clone, so more work will be needed to do use_context() on this - let req_parts = use_context::<leptos_viz::RequestParts>(); - - if let Some(req_parts) = req_parts { - println!("Uri = {:?}", req_parts.uri); - } - - use futures::TryStreamExt; - - let mut conn = db().await?; - - let mut todos = Vec::new(); - let mut rows = - sqlx::query_as::<_, Todo>("SELECT * FROM todos").fetch(&mut conn); - while let Some(row) = rows.try_next().await? { - todos.push(row); - } - - // Add a random header(because why not) - // let mut res_headers = HeaderMap::new(); - // res_headers.insert(SET_COOKIE, HeaderValue::from_str("fizz=buzz").unwrap()); - - // let res_parts = leptos_viz::ResponseParts { - // headers: res_headers, - // status: Some(StatusCode::IM_A_TEAPOT), - // }; - - // let res_options_outer = use_context::<leptos_viz::ResponseOptions>(); - // if let Some(res_options) = res_options_outer { - // res_options.overwrite(res_parts).await; - // } - - Ok(todos) -} - -#[server(AddTodo, "/api")] -pub async fn add_todo(title: String) -> Result<(), ServerFnError> { - let mut conn = db().await?; - - // fake API delay - std::thread::sleep(std::time::Duration::from_millis(1250)); - - match sqlx::query("INSERT INTO todos (title, completed) VALUES ($1, false)") - .bind(title) - .execute(&mut conn) - .await - { - Ok(_row) => Ok(()), - Err(e) => Err(ServerFnError::ServerError(e.to_string())), - } -} - -// The struct name and path prefix arguments are optional. -#[server] -pub async fn delete_todo(id: u16) -> Result<(), ServerFnError> { - let mut conn = db().await?; - - Ok(sqlx::query("DELETE FROM todos WHERE id = $1") - .bind(id) - .execute(&mut conn) - .await - .map(|_| ())?) -} - -#[component] -pub fn TodoApp() -> impl IntoView { - //let id = use_context::<String>(); - provide_meta_context(); - view! { - - <Link rel="shortcut icon" type_="image/ico" href="/favicon.ico"/> - <Stylesheet id="leptos" href="/pkg/todo_app_sqlite_viz.css"/> - <Router> - <header> - <h1>"My Tasks"</h1> - </header> - <main> - <Routes> - <Route path="" view=Todos/> //Route - </Routes> - </main> - </Router> - } -} - -#[component] -pub fn Todos() -> impl IntoView { - let add_todo = create_server_multi_action::<AddTodo>(); - let delete_todo = create_server_action::<DeleteTodo>(); - let submissions = add_todo.submissions(); - - // list of todos is loaded from the server in reaction to changes - let todos = create_resource( - move || (add_todo.version().get(), delete_todo.version().get()), - move |_| get_todos(), - ); - - view! { - - <div> - <MultiActionForm action=add_todo> - <label> - "Add a Todo" - <input type="text" name="title"/> - </label> - <input type="submit" value="Add"/> - </MultiActionForm> - <Transition fallback=move || view! {<p>"Loading..."</p> }> - <ErrorBoundary fallback=|errors| view!{<ErrorTemplate errors/>}> - {move || { - let existing_todos = { - move || { - todos.get() - .map(move |todos| match todos { - Err(e) => { - view! { <pre class="error">"Server Error: " {e.to_string()}</pre>}.into_view() - } - Ok(todos) => { - if todos.is_empty() { - view! { <p>"No tasks were found."</p> }.into_view() - } else { - todos - .into_iter() - .map(move |todo| { - view! { - - <li> - {todo.title} - <ActionForm action=delete_todo> - <input type="hidden" name="id" value={todo.id}/> - <input type="submit" value="X"/> - </ActionForm> - </li> - } - }) - .collect_view() - } - } - }) - .unwrap_or_default() - } - }; - - let pending_todos = move || { - submissions - .get() - .into_iter() - .filter(|submission| submission.pending().get()) - .map(|submission| { - view! { - - <li class="pending">{move || submission.input.get().map(|data| data.title) }</li> - } - }) - .collect_view() - }; - - view! { - - <ul> - {existing_todos} - {pending_todos} - </ul> - } - } - } - </ErrorBoundary> - </Transition> - </div> - } -} diff --git a/examples/todo_app_sqlite_viz/style.css b/examples/todo_app_sqlite_viz/style.css deleted file mode 100644 index 152dd11327..0000000000 --- a/examples/todo_app_sqlite_viz/style.css +++ /dev/null @@ -1,3 +0,0 @@ -.pending { - color: purple; -} \ No newline at end of file diff --git a/integrations/viz/Cargo.toml b/integrations/viz/Cargo.toml deleted file mode 100644 index caead7be56..0000000000 --- a/integrations/viz/Cargo.toml +++ /dev/null @@ -1,25 +0,0 @@ -[package] -name = "leptos_viz" -version = { workspace = true } -edition = "2021" -authors = ["Greg Johnston", "Fangdun Tsai"] -license = "MIT" -repository = "https://github.com/leptos-rs/leptos" -description = "Viz integrations for the Leptos web framework." - -[dependencies] -viz = { version = "0.4.8" } -futures = "0.3" -http = "0.2.11" -hyper = "0.14.23" -leptos = { workspace = true, features = ["ssr"] } -leptos_meta = { workspace = true, features = ["ssr"] } -leptos_router = { workspace = true, features = ["ssr"] } -leptos_integration_utils = { workspace = true } -serde_json = "1" -tokio = { version = "1", features = ["full"] } -parking_lot = "0.12.1" - -[features] -nonce = ["leptos/nonce"] -experimental-islands = ["leptos_integration_utils/experimental-islands"] diff --git a/integrations/viz/Makefile.toml b/integrations/viz/Makefile.toml deleted file mode 100644 index 4ed6229141..0000000000 --- a/integrations/viz/Makefile.toml +++ /dev/null @@ -1,4 +0,0 @@ -extend = { path = "../../cargo-make/main.toml" } - -[tasks.check-format] -env = { LEPTOS_PROJECT_DIRECTORY = "../../" } diff --git a/integrations/viz/src/lib.rs b/integrations/viz/src/lib.rs deleted file mode 100644 index f5648217cd..0000000000 --- a/integrations/viz/src/lib.rs +++ /dev/null @@ -1,1549 +0,0 @@ -#![forbid(unsafe_code)] - -//! Provides functions to easily integrate Leptos with Viz. -//! -//! For more details on how to use the integrations, see the -//! [`examples`](https://github.com/leptos-rs/leptos/tree/main/examples) -//! directory in the Leptos repository. - -use futures::{ - channel::mpsc::{Receiver, Sender}, - Future, SinkExt, Stream, StreamExt, -}; -use http::{header, method::Method, uri::Uri, version::Version, StatusCode}; -use hyper::body; -use leptos::{ - leptos_server::{server_fn_by_path, Payload}, - server_fn::Encoding, - ssr::*, - *, -}; -use leptos_integration_utils::{build_async_response, html_parts_separated}; -use leptos_meta::{generate_head_metadata_separated, MetaContext}; -use leptos_router::*; -use parking_lot::RwLock; -use std::{pin::Pin, sync::Arc}; -use tokio::task::spawn_blocking; -use viz::{ - headers::{HeaderMap, HeaderName, HeaderValue}, - Body, Bytes, Error, Handler, IntoResponse, Request, RequestExt, Response, - ResponseExt, Result, Router, -}; - -/// A struct to hold the parts of the incoming Request. Since `http::Request` isn't cloneable, we're forced -/// to construct this for Leptos to use in viz -#[derive(Debug, Clone)] -pub struct RequestParts { - pub version: Version, - pub method: Method, - pub uri: Uri, - pub headers: HeaderMap<HeaderValue>, - pub body: Bytes, -} -/// This struct lets you define headers and override the status of the Response from an Element or a Server Function -/// Typically contained inside of a ResponseOptions. Setting this is useful for cookies and custom responses. -#[derive(Debug, Clone, Default)] -pub struct ResponseParts { - pub status: Option<StatusCode>, - pub headers: HeaderMap, -} - -impl ResponseParts { - /// Insert a header, overwriting any previous value with the same key - pub fn insert_header(&mut self, key: HeaderName, value: HeaderValue) { - self.headers.insert(key, value); - } - /// Append a header, leaving any header with the same key intact - pub fn append_header(&mut self, key: HeaderName, value: HeaderValue) { - self.headers.append(key, value); - } -} - -/// Allows you to override details of the HTTP response like the status code and add Headers/Cookies. -#[derive(Debug, Clone, Default)] -pub struct ResponseOptions(pub Arc<RwLock<ResponseParts>>); - -impl ResponseOptions { - /// A simpler way to overwrite the contents of `ResponseOptions` with a new `ResponseParts`. - pub fn overwrite(&self, parts: ResponseParts) { - let mut writable = self.0.write(); - *writable = parts - } - /// Set the status of the returned Response. - pub fn set_status(&self, status: StatusCode) { - let mut writeable = self.0.write(); - let res_parts = &mut *writeable; - res_parts.status = Some(status); - } - /// Insert a header, overwriting any previous value with the same key. - pub fn insert_header(&self, key: HeaderName, value: HeaderValue) { - let mut writeable = self.0.write(); - let res_parts = &mut *writeable; - res_parts.headers.insert(key, value); - } - /// Append a header, leaving any header with the same key intact. - pub fn append_header(&self, key: HeaderName, value: HeaderValue) { - let mut writeable = self.0.write(); - let res_parts = &mut *writeable; - res_parts.headers.append(key, value); - } -} - -/// Provides an easy way to redirect the user from within a server function. Mimicking the Remix `redirect()`, -/// it sets a StatusCode of 302 and a LOCATION header with the provided value. -/// If looking to redirect from the client, `leptos_router::use_navigate()` should be used instead -pub fn redirect(path: &str) { - if let Some(response_options) = use_context::<ResponseOptions>() { - response_options.set_status(StatusCode::FOUND); - response_options.insert_header( - header::LOCATION, - header::HeaderValue::from_str(path) - .expect("Failed to create HeaderValue"), - ); - } -} - -/// Decomposes an HTTP request into its parts, allowing you to read its headers -/// and other data without consuming the body. -pub async fn generate_request_parts(req: Request) -> RequestParts { - // provide request headers as context in server scope - let (parts, body) = req.into_parts(); - let body = body::to_bytes(body).await.unwrap_or_default(); - RequestParts { - method: parts.method, - uri: parts.uri, - headers: parts.headers, - version: parts.version, - body, - } -} - -/// A Viz handlers to listens for a request with Leptos server function arguments in the body, -/// run the server function if found, and return the resulting [Response]. -/// -/// This can then be set up at an appropriate route in your application: -/// -/// ``` -/// use leptos::*; -/// use std::net::SocketAddr; -/// use viz::{Router, ServiceMaker}; -/// -/// # if false { // don't actually try to run a server in a doctest... -/// #[tokio::main] -/// async fn main() { -/// let addr = SocketAddr::from(([127, 0, 0, 1], 8082)); -/// -/// // build our application with a route -/// let app = -/// Router::new().post("/api/:fn_name*", leptos_viz::handle_server_fns); -/// -/// // run our app with hyper -/// // `viz::Server` is a re-export of `hyper::Server` -/// viz::Server::bind(&addr) -/// .serve(ServiceMaker::from(app)) -/// .await -/// .unwrap(); -/// } -/// # } -/// ``` -/// Leptos provides a generic implementation of `handle_server_fns`. If access to more specific parts of the Request is desired, -/// you can specify your own server fn handler based on this one and give it it's own route in the server macro. -/// -/// ## Provided Context Types -/// This function always provides context values including the following types: -/// - [RequestParts] -/// - [ResponseOptions] -pub async fn handle_server_fns(req: Request) -> Result<Response> { - handle_server_fns_inner(req, || {}).await -} - -/// A Viz handlers to listens for a request with Leptos server function arguments in the body, -/// run the server function if found, and return the resulting [Response]. -/// -/// This can then be set up at an appropriate route in your application: -/// -/// This version allows you to pass in a closure to capture additional data from the layers above leptos -/// and store it in context. To use it, you'll need to define your own route, and a handler function -/// that takes in the data you'd like. See the [render_app_to_stream_with_context] docs for an example -/// of one that should work much like this one. -/// -/// **NOTE**: If your server functions expect a context, make sure to provide it both in -/// [`handle_server_fns_with_context`] **and** in [`leptos_routes_with_context`] (or whatever -/// rendering method you are using). During SSR, server functions are called by the rendering -/// method, while subsequent calls from the client are handled by the server function handler. -/// The same context needs to be provided to both handlers. -/// -/// ## Provided Context Types -/// This function always provides context values including the following types: -/// - [RequestParts] -/// - [ResponseOptions] -pub async fn handle_server_fns_with_context( - req: Request, - additional_context: impl Fn() + Clone + Send + 'static, -) -> Result<Response> { - handle_server_fns_inner(req, additional_context).await -} - -async fn handle_server_fns_inner( - req: Request, - additional_context: impl Fn() + Clone + Send + 'static, -) -> Result<Response> { - let fn_name = req.params::<String>()?; - let headers = req.headers().clone(); - let query = req.query_string().unwrap_or("").to_owned().into(); - let (tx, rx) = futures::channel::oneshot::channel(); - spawn_blocking({ - move || { - tokio::runtime::Runtime::new() - .expect("couldn't spawn runtime") - .block_on({ - async move { - let res = if let Some(server_fn) = - server_fn_by_path(fn_name.as_str()) - { - let runtime = create_runtime(); - - additional_context(); - - let req_parts = generate_request_parts(req).await; - // Add this so we can get details about the Request - provide_context(req_parts.clone()); - // Add this so that we can set headers and status of the response - provide_context(ResponseOptions::default()); - - let data = match &server_fn.encoding() { - Encoding::Url | Encoding::Cbor => { - &req_parts.body - } - Encoding::GetJSON | Encoding::GetCBOR => &query, - }; - - let res = match server_fn.call((), data).await { - Ok(serialized) => { - // If ResponseOptions are set, add the headers and status to the request - let res_options = - use_context::<ResponseOptions>(); - - // if this is Accept: application/json then send a serialized JSON response - let accept_header = headers - .get("Accept") - .and_then(|value| value.to_str().ok()); - let mut res = Response::builder(); - - // Add headers from ResponseParts if they exist. These should be added as long - // as the server function returns an OK response - let res_options_outer = - res_options.unwrap().0; - let res_options_inner = - res_options_outer.read(); - let (status, mut res_headers) = ( - res_options_inner.status, - res_options_inner.headers.clone(), - ); - - if let Some(header_ref) = res.headers_mut() - { - header_ref.extend(res_headers.drain()); - }; - - if accept_header == Some("application/json") - || accept_header - == Some( - "application/\ - x-www-form-urlencoded", - ) - || accept_header - == Some("application/cbor") - { - res = res.status(StatusCode::OK); - } - // otherwise, it's probably a <form> submit or something: redirect back to the referrer - else { - let referer = headers - .get("Referer") - .and_then(|value| { - value.to_str().ok() - }) - .unwrap_or("/"); - - res = res - .status(StatusCode::SEE_OTHER) - .header("Location", referer); - } - // Override StatusCode if it was set in a Resource or Element - res = match status { - Some(status) => res.status(status), - None => res, - }; - match serialized { - Payload::Binary(data) => res - .header( - header::CONTENT_TYPE, - "application/cbor", - ) - .body(Body::from(data)), - Payload::Url(data) => res - .header( - header::CONTENT_TYPE, - "application/\ - x-www-form-urlencoded", - ) - .body(Body::from(data)), - Payload::Json(data) => res - .header( - header::CONTENT_TYPE, - "application/json", - ) - .body(Body::from(data)), - } - } - Err(e) => Response::builder() - .status(StatusCode::INTERNAL_SERVER_ERROR) - .body(Body::from( - serde_json::to_string(&e) - .unwrap_or_else(|_| e.to_string()), - )), - }; - runtime.dispose(); - res - } else { - Response::builder() - .status(StatusCode::BAD_REQUEST) - .body(Body::from(format!( - "Could not find a server function at the \ - route {fn_name}. \n\nIt's likely that \ - either - 1. The API prefix you specify in the \ - `#[server]` macro doesn't match the \ - prefix at which your server function \ - handler is mounted, or \n2. You are on a \ - platform that doesn't support automatic \ - server function registration and you \ - need to call \ - ServerFn::register_explicit() on the \ - server function type, somewhere in your \ - `main` function.", - ))) - } - .expect("could not build Response"); - - _ = tx.send(res); - } - }) - } - }); - - rx.await.map_err(Error::normal) -} -/// Returns a Viz [Handler] that listens for a `GET` request and tries -/// to route it using [leptos_router], serving an HTML stream of your application. -/// -/// The provides a [MetaContext] and a [RouterIntegrationContext] to app’s context before -/// rendering it, and includes any meta tags injected using [leptos_meta]. -/// -/// The HTML stream is rendered using [render_to_stream], and includes everything described in -/// the documentation for that function. -/// -/// This can then be set up at an appropriate route in your application: -/// ``` -/// use leptos::*; -/// use leptos_config::get_configuration; -/// use std::{env, net::SocketAddr}; -/// use viz::{Router, ServiceMaker}; -/// -/// #[component] -/// fn MyApp() -> impl IntoView { -/// view! { <main>"Hello, world!"</main> } -/// } -/// -/// # if false { // don't actually try to run a server in a doctest... -/// #[tokio::main] -/// async fn main() { -/// let conf = get_configuration(Some("Cargo.toml")).await.unwrap(); -/// let leptos_options = conf.leptos_options; -/// let addr = leptos_options.site_addr.clone(); -/// -/// // build our application with a route -/// let app = Router::new().any( -/// "*", -/// leptos_viz::render_app_to_stream( -/// leptos_options, -/// || view! { <MyApp/> }, -/// ), -/// ); -/// -/// // run our app with hyper -/// // `viz::Server` is a re-export of `hyper::Server` -/// viz::Server::bind(&addr) -/// .serve(ServiceMaker::from(app)) -/// .await -/// .unwrap(); -/// } -/// # } -/// ``` -/// -/// ## Provided Context Types -/// This function always provides context values including the following types: -/// - [RequestParts] -/// - [ResponseOptions] -/// - [MetaContext] -/// - [RouterIntegrationContext] -pub fn render_app_to_stream<IV>( - options: LeptosOptions, - app_fn: impl Fn() -> IV + Clone + Send + 'static, -) -> impl Fn( - Request, -) -> Pin<Box<dyn Future<Output = Result<Response>> + Send + 'static>> - + Clone - + Send - + 'static -where - IV: IntoView, -{ - render_app_to_stream_with_context(options, || {}, app_fn) -} - -/// Returns a Viz [Handler] that listens for a `GET` request and tries -/// to route it using [leptos_router], serving an HTML stream of your application. -/// This stream will pause at each `<Suspense/>` node and wait for it to resolve before -/// sending down its HTML. The app will become interactive once it has fully loaded. -/// -/// The provides a [MetaContext] and a [RouterIntegrationContext] to app’s context before -/// rendering it, and includes any meta tags injected using [leptos_meta]. -/// -/// The HTML stream is rendered using [render_to_stream], and includes everything described in -/// the documentation for that function. -/// -/// This can then be set up at an appropriate route in your application: -/// ``` -/// use leptos::*; -/// use leptos_config::get_configuration; -/// use std::{env, net::SocketAddr}; -/// use viz::{Router, ServiceMaker}; -/// -/// #[component] -/// fn MyApp() -> impl IntoView { -/// view! { <main>"Hello, world!"</main> } -/// } -/// -/// # if false { // don't actually try to run a server in a doctest... -/// #[tokio::main] -/// async fn main() { -/// let conf = get_configuration(Some("Cargo.toml")).await.unwrap(); -/// let leptos_options = conf.leptos_options; -/// let addr = leptos_options.site_addr.clone(); -/// -/// // build our application with a route -/// let app = Router::new().any( -/// "*", -/// leptos_viz::render_app_to_stream_in_order( -/// leptos_options, -/// || view! { <MyApp/> }, -/// ), -/// ); -/// -/// // run our app with hyper -/// // `viz::Server` is a re-export of `hyper::Server` -/// viz::Server::bind(&addr) -/// .serve(ServiceMaker::from(app)) -/// .await -/// .unwrap(); -/// } -/// # } -/// ``` -/// -/// ## Provided Context Types -/// This function always provides context values including the following types: -/// - [RequestParts] -/// - [ResponseOptions] -/// - [MetaContext] -/// - [RouterIntegrationContext] -pub fn render_app_to_stream_in_order<IV>( - options: LeptosOptions, - app_fn: impl Fn() -> IV + Clone + Send + 'static, -) -> impl Fn( - Request, -) -> Pin<Box<dyn Future<Output = Result<Response>> + Send + 'static>> - + Clone - + Send - + 'static -where - IV: IntoView, -{ - render_app_to_stream_in_order_with_context(options, || {}, app_fn) -} - -/// Returns a Viz [Handler] that listens for a `GET` request and tries -/// to route it using [leptos_router], serving an HTML stream of your application. -/// -/// This version allows us to pass Viz State/Extractor or other infro from Viz or network -/// layers above Leptos itself. To use it, you'll need to write your own handler function that provides -/// the data to leptos in a closure. An example is below -/// ```ignore -/// async fn custom_handler(req: Request) -> Result<Response> { -/// let id = req.params::<String>()?; -/// let options = &*req.state::<Arc<LeptosOptions>>().ok_or(Error::Responder(Response::text("missing state type LeptosOptions")))?; -/// let handler = leptos_viz::render_app_to_stream_with_context(options.clone(), -/// move || { -/// provide_context(id.clone()); -/// }, -/// || view! { <TodoApp/> } -/// ); -/// handler(req).await -/// } -/// ``` -/// Otherwise, this function is identical to [render_app_to_stream]. -/// -/// ## Provided Context Types -/// This function always provides context values including the following types: -/// - [RequestParts] -/// - [ResponseOptions] -/// - [MetaContext] -/// - [RouterIntegrationContext] -pub fn render_app_to_stream_with_context<IV>( - options: LeptosOptions, - additional_context: impl Fn() + Clone + Send + 'static, - app_fn: impl Fn() -> IV + Clone + Send + 'static, -) -> impl Fn( - Request, -) -> Pin<Box<dyn Future<Output = Result<Response>> + Send + 'static>> - + Clone - + Send - + 'static -where - IV: IntoView, -{ - render_app_to_stream_with_context_and_replace_blocks( - options, - additional_context, - app_fn, - false, - ) -} - -/// Returns a Viz [Handler] that listens for a `GET` request and tries -/// to route it using [leptos_router], serving an HTML stream of your application. -/// -/// This version allows us to pass Viz State/Extractor or other infro from Viz or network -/// layers above Leptos itself. To use it, you'll need to write your own handler function that provides -/// the data to leptos in a closure. -/// -/// `replace_blocks` additionally lets you specify whether `<Suspense/>` fragments that read -/// from blocking resources should be retrojected into the HTML that's initially served, rather -/// than dynamically inserting them with JavaScript on the client. This means you will have -/// better support if JavaScript is not enabled, in exchange for a marginally slower response time. -/// -/// Otherwise, this function is identical to [render_app_to_stream_with_context]. -/// -/// ## Provided Context Types -/// This function always provides context values including the following types: -/// - [RequestParts] -/// - [ResponseOptions] -/// - [MetaContext] -/// - [RouterIntegrationContext] -pub fn render_app_to_stream_with_context_and_replace_blocks<IV>( - options: LeptosOptions, - additional_context: impl Fn() + Clone + Send + 'static, - app_fn: impl Fn() -> IV + Clone + Send + 'static, - replace_blocks: bool, -) -> impl Fn( - Request, -) -> Pin<Box<dyn Future<Output = Result<Response>> + Send + 'static>> - + Clone - + Send - + 'static -where - IV: IntoView, -{ - move |req: Request| { - Box::pin({ - let options = options.clone(); - let app_fn = app_fn.clone(); - let add_context = additional_context.clone(); - let default_res_options = ResponseOptions::default(); - let res_options2 = default_res_options.clone(); - let res_options3 = default_res_options.clone(); - - async move { - // Need to get the path and query string of the Request - // For reasons that escape me, if the incoming URI protocol is https, it provides the absolute URI - // if http, it returns a relative path. Adding .path() seems to make it explicitly return the relative uri - let path = req.uri().path_and_query().unwrap().as_str(); - - let full_path = format!("http://leptos.dev{path}"); - - let (tx, rx) = futures::channel::mpsc::channel(8); - - spawn_blocking({ - let app_fn = app_fn.clone(); - let add_context = add_context.clone(); - move || { - tokio::runtime::Runtime::new() - .expect("couldn't spawn runtime") - .block_on({ - let app_fn = app_fn.clone(); - let add_context = add_context.clone(); - async move { - tokio::task::LocalSet::new() - .run_until(async { - let app = { - let full_path = full_path.clone(); - let req_parts = generate_request_parts(req).await; - move || { - provide_contexts(full_path, req_parts, default_res_options); - app_fn().into_view() - } - }; - - let (bundle, runtime) = - leptos::leptos_dom::ssr::render_to_stream_with_prefix_undisposed_with_context_and_block_replacement( - app, - || generate_head_metadata_separated().1.into(), - add_context, - replace_blocks - ); - - forward_stream(&options, res_options2, bundle, tx).await; - - runtime.dispose(); - }) - .await; - } - }); - } - }); - - generate_response(res_options3, rx).await - } - }) - } -} - -async fn generate_response( - res_options: ResponseOptions, - rx: Receiver<String>, -) -> Result<Response> { - let mut stream = - Box::pin(rx.map(|html| Ok::<_, std::io::Error>(Bytes::from(html)))); - - // Get the first and second chunks in the stream, which renders the app shell, and thus allows Resources to run - let first_chunk = stream.next().await; - let second_chunk = stream.next().await; - - // Extract the resources now that they've been rendered - let res_options = res_options.0.read(); - - let complete_stream = - futures::stream::iter([first_chunk.unwrap(), second_chunk.unwrap()]) - .chain(stream); - - let mut res = Response::stream(complete_stream); - - if let Some(status) = res_options.status { - *res.status_mut() = status - } - let mut res_headers = res_options.headers.clone(); - res.headers_mut().extend(res_headers.drain()); - - Ok(res) -} - -async fn forward_stream( - options: &LeptosOptions, - res_options2: ResponseOptions, - bundle: impl Stream<Item = String> + 'static, - mut tx: Sender<String>, -) { - let mut shell = Box::pin(bundle); - let first_app_chunk = shell.next().await.unwrap_or_default(); - - let (head, tail) = - html_parts_separated(options, use_context::<MetaContext>().as_ref()); - - _ = tx.send(head).await; - - _ = tx.send(first_app_chunk).await; - - while let Some(fragment) = shell.next().await { - _ = tx.send(fragment).await; - } - - _ = tx.send(tail.to_string()).await; - - // Extract the value of ResponseOptions from here - let res_options = use_context::<ResponseOptions>().unwrap(); - - let new_res_parts = res_options.0.read().clone(); - - let mut writable = res_options2.0.write(); - *writable = new_res_parts; - - tx.close_channel(); -} - -/// Returns a Viz [Handler] that listens for a `GET` request and tries -/// to route it using [leptos_router], serving an in-order HTML stream of your application. -/// This stream will pause at each `<Suspense/>` node and wait for it to resolve before -/// sending down its HTML. The app will become interactive once it has fully loaded. -/// -/// This version allows us to pass Viz State/Extractor or other infro from Viz or network -/// layers above Leptos itself. To use it, you'll need to write your own handler function that provides -/// the data to leptos in a closure. An example is below -/// ```ignore -/// async fn custom_handler(req: Request) -> Result<Response> { -/// let id = req.params::<String>()?; -/// let options = &*req.state::<Arc<LeptosOptions>>().ok_or(StateError::new::<Arc<LeptosOptions>>())?; -/// let handler = leptos_viz::render_app_to_stream_in_order_with_context(options.clone(), -/// move || { -/// provide_context(id.clone()); -/// }, -/// || view! { <TodoApp/> } -/// ); -/// handler(req).await -/// } -/// ``` -/// Otherwise, this function is identical to [render_app_to_stream]. -/// -/// ## Provided Context Types -/// This function always provides context values including the following types: -/// - [RequestParts] -/// - [ResponseOptions] -/// - [MetaContext] -/// - [RouterIntegrationContext] -pub fn render_app_to_stream_in_order_with_context<IV>( - options: LeptosOptions, - additional_context: impl Fn() + 'static + Clone + Send, - app_fn: impl Fn() -> IV + Clone + Send + 'static, -) -> impl Fn( - Request, -) -> Pin<Box<dyn Future<Output = Result<Response>> + Send + 'static>> - + Clone - + Send - + 'static -where - IV: IntoView, -{ - move |req: Request| { - Box::pin({ - let options = options.clone(); - let app_fn = app_fn.clone(); - let add_context = additional_context.clone(); - let default_res_options = ResponseOptions::default(); - let res_options2 = default_res_options.clone(); - let res_options3 = default_res_options.clone(); - - async move { - // Need to get the path and query string of the Request - // For reasons that escape me, if the incoming URI protocol is https, it provides the absolute URI - // if http, it returns a relative path. Adding .path() seems to make it explicitly return the relative uri - let path = req.uri().path_and_query().unwrap().as_str(); - - let full_path = format!("http://leptos.dev{path}"); - - let (tx, rx) = futures::channel::mpsc::channel(8); - - spawn_blocking({ - let app_fn = app_fn.clone(); - let add_context = add_context.clone(); - move || { - tokio::runtime::Runtime::new() - .expect("couldn't spawn runtime") - .block_on({ - let app_fn = app_fn.clone(); - let add_context = add_context.clone(); - async move { - tokio::task::LocalSet::new() - .run_until(async { - let app = { - let full_path = full_path.clone(); - let req_parts = generate_request_parts(req).await; - move || { - provide_contexts(full_path, req_parts, default_res_options); - app_fn().into_view() - } - }; - - let (bundle, runtime) = - leptos::ssr::render_to_stream_in_order_with_prefix_undisposed_with_context( - app, - || generate_head_metadata_separated().1.into(), - add_context, - ); - - forward_stream(&options, res_options2, bundle, tx).await; - runtime.dispose(); - }) - .await; - } - }); - } - }); - - generate_response(res_options3, rx).await - } - }) - } -} - -fn provide_contexts( - path: String, - req_parts: RequestParts, - default_res_options: ResponseOptions, -) { - let integration = ServerIntegration { path }; - provide_context(RouterIntegrationContext::new(integration)); - provide_context(MetaContext::new()); - provide_context(req_parts); - provide_context(default_res_options); - provide_server_redirect(redirect); - #[cfg(feature = "nonce")] - leptos::nonce::provide_nonce(); -} - -/// Returns a Viz [Handler] that listens for a `GET` request and tries -/// to route it using [leptos_router], asynchronously rendering an HTML page after all -/// `async` [Resource]s have loaded. -/// -/// The provides a [MetaContext] and a [RouterIntegrationContext] to app’s context before -/// rendering it, and includes any meta tags injected using [leptos_meta]. -/// -/// The HTML stream is rendered using [render_to_string_async], and includes everything described in -/// the documentation for that function. -/// -/// This can then be set up at an appropriate route in your application: -/// ``` -/// use leptos::*; -/// use leptos_config::get_configuration; -/// use std::{env, net::SocketAddr}; -/// use viz::{Router, ServiceMaker}; -/// -/// #[component] -/// fn MyApp() -> impl IntoView { -/// view! { <main>"Hello, world!"</main> } -/// } -/// -/// # if false { // don't actually try to run a server in a doctest... -/// #[tokio::main] -/// async fn main() { -/// let conf = get_configuration(Some("Cargo.toml")).await.unwrap(); -/// let leptos_options = conf.leptos_options; -/// let addr = leptos_options.site_addr.clone(); -/// -/// // build our application with a route -/// let app = Router::new().any( -/// "*", -/// leptos_viz::render_app_async(leptos_options, || view! { <MyApp/> }), -/// ); -/// -/// // run our app with hyper -/// // `viz::Server` is a re-export of `hyper::Server` -/// viz::Server::bind(&addr) -/// .serve(ServiceMaker::from(app)) -/// .await -/// .unwrap(); -/// } -/// # } -/// ``` -/// -/// ## Provided Context Types -/// This function always provides context values including the following types: -/// - [RequestParts] -/// - [ResponseOptions] -/// - [MetaContext] -/// - [RouterIntegrationContext] -pub fn render_app_async<IV>( - options: LeptosOptions, - app_fn: impl Fn() -> IV + Clone + Send + 'static, -) -> impl Fn( - Request, -) -> Pin<Box<dyn Future<Output = Result<Response>> + Send + 'static>> - + Clone - + Send - + 'static -where - IV: IntoView, -{ - render_app_async_with_context(options, || {}, app_fn) -} - -/// Returns a Viz [Handler] that listens for a `GET` request and tries -/// to route it using [leptos_router], asynchronously rendering an HTML page after all -/// `async` [Resource]s have loaded. -/// -/// This version allows us to pass Viz State/Extractor or other infro from Viz or network -/// layers above Leptos itself. To use it, you'll need to write your own handler function that provides -/// the data to leptos in a closure. An example is below -/// ```ignore -/// async fn custom_handler(req: Request) -> Result<Response> { -/// let id = req.params::<String>()?; -/// let options = &*req.state::<Arc<LeptosOptions>>().ok_or(StateError::new::<Arc<LeptosOptions>>())?; -/// let handler = leptos_viz::render_app_async_with_context(options.clone(), -/// move || { -/// provide_context(id.clone()); -/// }, -/// || view! { <TodoApp/> } -/// ); -/// handler(req).await.into_response() -/// } -/// ``` -/// Otherwise, this function is identical to [render_app_to_stream]. -/// -/// ## Provided Context Types -/// This function always provides context values including the following types: -/// - [RequestParts] -/// - [ResponseOptions] -/// - [MetaContext] -/// - [RouterIntegrationContext] -pub fn render_app_async_with_context<IV>( - options: LeptosOptions, - additional_context: impl Fn() + 'static + Clone + Send, - app_fn: impl Fn() -> IV + Clone + Send + 'static, -) -> impl Fn( - Request, -) -> Pin<Box<dyn Future<Output = Result<Response>> + Send + 'static>> - + Clone - + Send - + 'static -where - IV: IntoView, -{ - move |req: Request| { - Box::pin({ - let options = options.clone(); - let app_fn = app_fn.clone(); - let add_context = additional_context.clone(); - let default_res_options = ResponseOptions::default(); - let res_options2 = default_res_options.clone(); - let res_options3 = default_res_options.clone(); - - async move { - // Need to get the path and query string of the Request - // For reasons that escape me, if the incoming URI protocol is https, it provides the absolute URI - // if http, it returns a relative path. Adding .path() seems to make it explicitly return the relative uri - let path = req.uri().path_and_query().unwrap().as_str(); - - let full_path = format!("http://leptos.dev{path}"); - - let (tx, rx) = futures::channel::oneshot::channel(); - - spawn_blocking({ - let app_fn = app_fn.clone(); - let add_context = add_context.clone(); - move || { - tokio::runtime::Runtime::new() - .expect("couldn't spawn runtime") - .block_on({ - let app_fn = app_fn.clone(); - let add_context = add_context.clone(); - async move { - tokio::task::LocalSet::new() - .run_until(async { - let app = { - let full_path = full_path.clone(); - let req_parts = generate_request_parts(req).await; - move || { - provide_contexts(full_path, req_parts, default_res_options); - app_fn().into_view() - } - }; - - let (stream, runtime) = - render_to_stream_with_prefix_undisposed_with_context( - app, - || "".into(), - add_context, - ); - - // Extract the value of ResponseOptions from here - let res_options = - use_context::<ResponseOptions>().unwrap(); - - let html = build_async_response(stream, &options, runtime).await; - - let new_res_parts = res_options.0.read().clone(); - - let mut writable = res_options2.0.write(); - *writable = new_res_parts; - - _ = tx.send(html); - }) - .await; - } - }); - } - }); - - let html = rx.await.expect("to complete HTML rendering"); - - let mut res = Response::html(html); - - let res_options = res_options3.0.read(); - - if let Some(status) = res_options.status { - *res.status_mut() = status - } - let mut res_headers = res_options.headers.clone(); - res.headers_mut().extend(res_headers.drain()); - - Ok(res) - } - }) - } -} - -/// Generates a list of all routes defined in Leptos's Router in your app. We can then use this to automatically -/// create routes in Viz's Router without having to use wildcard matching or fallbacks. Takes in your root app Element -/// as an argument so it can walk you app tree. This version is tailored to generate Viz compatible paths. -pub fn generate_route_list<IV>( - app_fn: impl Fn() -> IV + 'static + Clone, -) -> Vec<RouteListing> -where - IV: IntoView + 'static, -{ - generate_route_list_with_exclusions_and_ssg(app_fn, None).0 -} - -/// Generates a list of all routes defined in Leptos's Router in your app. We can then use this to automatically -/// create routes in Viz's Router without having to use wildcard matching or fallbacks. Takes in your root app Element -/// as an argument so it can walk you app tree. This version is tailored to generate Viz compatible paths. -pub fn generate_route_list_with_ssg<IV>( - app_fn: impl Fn() -> IV + 'static + Clone, -) -> (Vec<RouteListing>, StaticDataMap) -where - IV: IntoView + 'static, -{ - generate_route_list_with_exclusions_and_ssg(app_fn, None) -} - -/// Generates a list of all routes defined in Leptos's Router in your app. We can then use this to automatically -/// create routes in Viz's Router without having to use wildcard matching or fallbacks. Takes in your root app Element -/// as an argument so it can walk you app tree. This version is tailored to generate Viz compatible paths. -pub fn generate_route_list_with_exclusions<IV>( - app_fn: impl Fn() -> IV + 'static + Clone, - excluded_routes: Option<Vec<String>>, -) -> Vec<RouteListing> -where - IV: IntoView + 'static, -{ - generate_route_list_with_exclusions_and_ssg(app_fn, excluded_routes).0 -} - -/// Generates a list of all routes defined in Leptos's Router in your app. We can then use this to automatically -/// create routes in Viz's Router without having to use wildcard matching or fallbacks. Takes in your root app Element -/// as an argument so it can walk you app tree. This version is tailored to generate Viz compatible paths. -pub fn generate_route_list_with_exclusions_and_ssg<IV>( - app_fn: impl Fn() -> IV + 'static + Clone, - excluded_routes: Option<Vec<String>>, -) -> (Vec<RouteListing>, StaticDataMap) -where - IV: IntoView + 'static, -{ - generate_route_list_with_exclusions_and_ssg_and_context( - app_fn, - excluded_routes, - || {}, - ) -} - -/// Generates a list of all routes defined in Leptos's Router in your app. We can then use this to automatically -/// create routes in Viz's Router without having to use wildcard matching or fallbacks. Takes in your root app Element -/// as an argument so it can walk you app tree. This version is tailored to generate Viz compatible paths. -/// Additional context will be provided to the app Element. -pub fn generate_route_list_with_exclusions_and_ssg_and_context<IV>( - app_fn: impl Fn() -> IV + 'static + Clone, - excluded_routes: Option<Vec<String>>, - additional_context: impl Fn() + 'static + Clone, -) -> (Vec<RouteListing>, StaticDataMap) -where - IV: IntoView + 'static, -{ - let (routes, static_data_map) = - leptos_router::generate_route_list_inner_with_context( - app_fn, - additional_context, - ); - // Viz's Router defines Root routes as "/" not "" - let mut routes = routes - .into_iter() - .map(|listing| { - let path = listing.path(); - if path.is_empty() { - RouteListing::new( - "/".to_string(), - listing.path(), - listing.mode(), - listing.methods(), - listing.static_mode(), - ) - } else { - listing - } - }) - .collect::<Vec<_>>(); - - ( - if routes.is_empty() { - vec![RouteListing::new( - "/", - "", - Default::default(), - [leptos_router::Method::Get], - None, - )] - } else { - if let Some(excluded_routes) = excluded_routes { - routes - .retain(|p| !excluded_routes.iter().any(|e| e == p.path())) - } - routes - }, - static_data_map, - ) -} - -fn handle_static_response<IV>( - path: String, - options: LeptosOptions, - app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static, - additional_context: impl Fn() + Clone + Send + Sync + 'static, - res: StaticResponse, -) -> Pin<Box<dyn Future<Output = Result<Response>> + 'static>> -where - IV: IntoView + 'static, -{ - Box::pin(async move { - match res { - StaticResponse::ReturnResponse { - body, - status, - content_type, - } => { - let mut res = Response::html(body); - if let Some(v) = content_type { - res.headers_mut().insert( - HeaderName::from_static("content-type"), - HeaderValue::from_static(v), - ); - } - *res.status_mut() = match status { - StaticStatusCode::Ok => StatusCode::OK, - StaticStatusCode::NotFound => StatusCode::NOT_FOUND, - StaticStatusCode::InternalServerError => { - StatusCode::INTERNAL_SERVER_ERROR - } - }; - Ok(res) - } - StaticResponse::RenderDynamic => { - let res = render_dynamic( - &path, - &options, - app_fn.clone(), - additional_context.clone(), - ) - .await; - handle_static_response( - path, - options, - app_fn, - additional_context, - res, - ) - .await - } - StaticResponse::RenderNotFound => { - let res = not_found_page( - tokio::fs::read_to_string(not_found_path(&options)).await, - ); - handle_static_response( - path, - options, - app_fn, - additional_context, - res, - ) - .await - } - StaticResponse::WriteFile { body, path } => { - if let Some(path) = path.parent() { - if let Err(e) = std::fs::create_dir_all(path) { - tracing::error!( - "encountered error {} writing directories {}", - e, - path.display() - ); - } - } - if let Err(e) = std::fs::write(&path, &body) { - tracing::error!( - "encountered error {} writing file {}", - e, - path.display() - ); - } - handle_static_response( - path.to_str().unwrap().to_string(), - options, - app_fn, - additional_context, - StaticResponse::ReturnResponse { - body, - status: StaticStatusCode::Ok, - content_type: Some("text/html"), - }, - ) - .await - } - } - }) -} - -fn static_route<IV>( - router: Router, - path: &str, - options: LeptosOptions, - app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static, - additional_context: impl Fn() + Clone + Send + Sync + 'static, - method: leptos_router::Method, - mode: StaticMode, -) -> Router -where - IV: IntoView + 'static, -{ - match mode { - StaticMode::Incremental => { - let handler = move |req: Request| { - Box::pin({ - let path = req.path().to_string(); - let options = options.clone(); - let app_fn = app_fn.clone(); - let additional_context = additional_context.clone(); - - async move { - let (tx, rx) = futures::channel::oneshot::channel(); - spawn_blocking(move || { - let path = path.clone(); - let options = options.clone(); - let app_fn = app_fn.clone(); - let additional_context = additional_context.clone(); - tokio::runtime::Runtime::new() - .expect("couldn't spawn runtime") - .block_on({ - let path = path.clone(); - let options = options.clone(); - let app_fn = app_fn.clone(); - let additional_context = - additional_context.clone(); - async move { - tokio::task::LocalSet::new().run_until(async { - let res = incremental_static_route( - tokio::fs::read_to_string( - static_file_path( - &options, - &path, - ), - ) - .await, - ); - let res = handle_static_response( - path.clone(), - options, - app_fn, - additional_context, - res, - ) - .await; - - let _ = tx.send(res); - }).await; - } - }) - }); - - rx.await.expect("to complete HTML rendering") - } - }) - }; - match method { - leptos_router::Method::Get => router.get(path, handler), - leptos_router::Method::Post => router.post(path, handler), - leptos_router::Method::Put => router.put(path, handler), - leptos_router::Method::Delete => router.delete(path, handler), - leptos_router::Method::Patch => router.patch(path, handler), - } - } - StaticMode::Upfront => { - let handler = move |req: Request| { - Box::pin({ - let path = req.path().to_string(); - let options = options.clone(); - let app_fn = app_fn.clone(); - let additional_context = additional_context.clone(); - - async move { - let (tx, rx) = futures::channel::oneshot::channel(); - spawn_blocking(move || { - let path = path.clone(); - let options = options.clone(); - let app_fn = app_fn.clone(); - let additional_context = additional_context.clone(); - tokio::runtime::Runtime::new() - .expect("couldn't spawn runtime") - .block_on({ - let path = path.clone(); - let options = options.clone(); - let app_fn = app_fn.clone(); - let additional_context = - additional_context.clone(); - async move { - tokio::task::LocalSet::new() - .run_until(async { - let res = upfront_static_route( - tokio::fs::read_to_string( - static_file_path( - &options, &path, - ), - ) - .await, - ); - let res = - handle_static_response( - path.clone(), - options, - app_fn, - additional_context, - res, - ) - .await; - - let _ = tx.send(res); - }) - .await; - } - }) - }); - - rx.await.expect("to complete HTML rendering") - } - }) - }; - match method { - leptos_router::Method::Get => router.get(path, handler), - leptos_router::Method::Post => router.post(path, handler), - leptos_router::Method::Put => router.put(path, handler), - leptos_router::Method::Delete => router.delete(path, handler), - leptos_router::Method::Patch => router.patch(path, handler), - } - } - } -} - -/// This trait allows one to pass a list of routes and a render function to Viz's router, letting us avoid -/// having to use wildcards or manually define all routes in multiple places. -pub trait LeptosRoutes { - fn leptos_routes<IV>( - self, - options: LeptosOptions, - paths: Vec<RouteListing>, - app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static, - ) -> Self - where - IV: IntoView + 'static; - - fn leptos_routes_with_context<IV>( - self, - options: LeptosOptions, - paths: Vec<RouteListing>, - additional_context: impl Fn() + Clone + Send + Sync + 'static, - app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static, - ) -> Self - where - IV: IntoView + 'static; - - fn leptos_routes_with_handler<H, O>( - self, - paths: Vec<RouteListing>, - handler: H, - ) -> Self - where - H: Handler<Request, Output = Result<O>> + Clone, - O: IntoResponse + Send + Sync + 'static; -} -/// The default implementation of `LeptosRoutes` which takes in a list of paths, and dispatches GET requests -/// to those paths to Leptos's renderer. -impl LeptosRoutes for Router { - fn leptos_routes<IV>( - self, - options: LeptosOptions, - paths: Vec<RouteListing>, - app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static, - ) -> Self - where - IV: IntoView + 'static, - { - self.leptos_routes_with_context(options, paths, || {}, app_fn) - } - - fn leptos_routes_with_context<IV>( - self, - options: LeptosOptions, - paths: Vec<RouteListing>, - additional_context: impl Fn() + Clone + Send + Sync + 'static, - app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static, - ) -> Self - where - IV: IntoView + 'static, - { - paths.iter().fold(self, |router, listing| { - let path = listing.path(); - let mode = listing.mode(); - - listing.methods().fold(router, |router, method| { - if let Some(static_mode) = listing.static_mode() { - static_route( - router, - path, - options.clone(), - app_fn.clone(), - additional_context.clone(), - method, - static_mode, - ) - } else { - match mode { - SsrMode::OutOfOrder => { - let s = render_app_to_stream_with_context( - options.clone(), - additional_context.clone(), - app_fn.clone(), - ); - match method { - leptos_router::Method::Get => { - router.get(path, s) - } - leptos_router::Method::Post => { - router.post(path, s) - } - leptos_router::Method::Put => { - router.put(path, s) - } - leptos_router::Method::Delete => { - router.delete(path, s) - } - leptos_router::Method::Patch => { - router.patch(path, s) - } - } - } - SsrMode::PartiallyBlocked => { - let s = - render_app_to_stream_with_context_and_replace_blocks( - options.clone(), - additional_context.clone(), - app_fn.clone(), - true, - ); - match method { - leptos_router::Method::Get => { - router.get(path, s) - } - leptos_router::Method::Post => { - router.post(path, s) - } - leptos_router::Method::Put => { - router.put(path, s) - } - leptos_router::Method::Delete => { - router.delete(path, s) - } - leptos_router::Method::Patch => { - router.patch(path, s) - } - } - } - SsrMode::InOrder => { - let s = render_app_to_stream_in_order_with_context( - options.clone(), - additional_context.clone(), - app_fn.clone(), - ); - match method { - leptos_router::Method::Get => { - router.get(path, s) - } - leptos_router::Method::Post => { - router.post(path, s) - } - leptos_router::Method::Put => { - router.put(path, s) - } - leptos_router::Method::Delete => { - router.delete(path, s) - } - leptos_router::Method::Patch => { - router.patch(path, s) - } - } - } - SsrMode::Async => { - let s = render_app_async_with_context( - options.clone(), - additional_context.clone(), - app_fn.clone(), - ); - match method { - leptos_router::Method::Get => { - router.get(path, s) - } - leptos_router::Method::Post => { - router.post(path, s) - } - leptos_router::Method::Put => { - router.put(path, s) - } - leptos_router::Method::Delete => { - router.delete(path, s) - } - leptos_router::Method::Patch => { - router.patch(path, s) - } - } - } - } - } - }) - }) - } - - fn leptos_routes_with_handler<H, O>( - self, - paths: Vec<RouteListing>, - handler: H, - ) -> Self - where - H: Handler<Request, Output = Result<O>> + Clone, - O: IntoResponse + Send + Sync + 'static, - { - paths.iter().fold(self, |router, listing| { - listing - .methods() - .fold(router, |router, method| match method { - leptos_router::Method::Get => { - router.get(listing.path(), handler.clone()) - } - leptos_router::Method::Post => { - router.post(listing.path(), handler.clone()) - } - leptos_router::Method::Put => { - router.put(listing.path(), handler.clone()) - } - leptos_router::Method::Delete => { - router.delete(listing.path(), handler.clone()) - } - leptos_router::Method::Patch => { - router.patch(listing.path(), handler.clone()) - } - }) - }) - } -} diff --git a/leptos/src/lib.rs b/leptos/src/lib.rs index 1d1004d5e7..7ffba8638f 100644 --- a/leptos/src/lib.rs +++ b/leptos/src/lib.rs @@ -53,9 +53,8 @@ //! and [`hackernews_axum`](https://github.com/leptos-rs/leptos/tree/main/examples/hackernews_axum) //! integrate calls to a real external REST API, routing, server-side rendering and hydration to create //! a fully-functional application that works as intended even before WASM has loaded and begun to run. -//! - [`todo_app_sqlite`](https://github.com/leptos-rs/leptos/tree/main/examples/todo_app_sqlite), -//! [`todo_app_sqlite_axum`](https://github.com/leptos-rs/leptos/tree/main/examples/todo_app_sqlite_axum), and -//! [`todo_app_sqlite_viz`](https://github.com/leptos-rs/leptos/tree/main/examples/todo_app_sqlite_viz) +//! - [`todo_app_sqlite`](https://github.com/leptos-rs/leptos/tree/main/examples/todo_app_sqlite) and +//! [`todo_app_sqlite_axum`](https://github.com/leptos-rs/leptos/tree/main/examples/todo_app_sqlite_axum) //! show how to build a full-stack app using server functions and database connections. //! - [`tailwind`](https://github.com/leptos-rs/leptos/tree/main/examples/tailwind_csr) shows how to integrate //! TailwindCSS with `trunk` for CSR. diff --git a/leptos_config/src/lib.rs b/leptos_config/src/lib.rs index 3033088db5..e1f5f35675 100644 --- a/leptos_config/src/lib.rs +++ b/leptos_config/src/lib.rs @@ -20,7 +20,7 @@ pub struct ConfFile { } /// This struct serves as a convenient place to store details used for configuring Leptos. -/// It's used in our actix, axum, and viz integrations to generate the +/// It's used in our actix and axum integrations to generate the /// correct path for WASM, JS, and Websockets, as well as other configuration tasks. /// It shares keys with cargo-leptos, to allow for easy interoperability #[derive(TypedBuilder, Debug, Clone, serde::Deserialize)] diff --git a/router/src/components/redirect.rs b/router/src/components/redirect.rs index 1aabb5841e..d5bccab60f 100644 --- a/router/src/components/redirect.rs +++ b/router/src/components/redirect.rs @@ -11,13 +11,12 @@ use std::rc::Rc; /// an absolute path, prefix it with `/`). /// /// **Note**: Support for server-side redirects is provided by the server framework -/// integrations ([`leptos_actix`], [`leptos_axum`], and [`leptos_viz`]). If you’re not using one of those +/// integrations ([`leptos_actix`] and [`leptos_axum`]. If you’re not using one of those /// integrations, you should manually provide a way of redirecting on the server /// using [`provide_server_redirect`]. /// /// [`leptos_actix`]: <https://docs.rs/leptos_actix/> /// [`leptos_axum`]: <https://docs.rs/leptos_axum/> -/// [`leptos_viz`]: <https://docs.rs/leptos_viz/> #[cfg_attr( any(debug_assertions, feature = "ssr"), tracing::instrument(level = "trace", skip_all,) diff --git a/router/src/extract_routes.rs b/router/src/extract_routes.rs index 031632f13d..8111d60770 100644 --- a/router/src/extract_routes.rs +++ b/router/src/extract_routes.rs @@ -100,12 +100,11 @@ impl RouteListing { } /// Generates a list of all routes this application could possibly serve. This returns the raw routes in the leptos_router -/// format. Odds are you want `generate_route_list()` from either the [`actix`], [`axum`], or [`viz`] integrations if you want +/// format. Odds are you want `generate_route_list()` from either the [`actix`] or [`axum`] integrations if you want /// to work with their router. /// /// [`actix`]: <https://docs.rs/actix/> /// [`axum`]: <https://docs.rs/axum/> -/// [`viz`]: <https://docs.rs/viz/> pub fn generate_route_list_inner<IV>( app_fn: impl Fn() -> IV + 'static + Clone, ) -> (Vec<RouteListing>, StaticDataMap) @@ -115,12 +114,11 @@ where generate_route_list_inner_with_context(app_fn, || {}) } /// Generates a list of all routes this application could possibly serve. This returns the raw routes in the leptos_router -/// format. Odds are you want `generate_route_list()` from either the [`actix`], [`axum`], or [`viz`] integrations if you want +/// format. Odds are you want `generate_route_list()` from either the [`actix`] or [`axum`] integrations if you want /// to work with their router. /// /// [`actix`]: <https://docs.rs/actix/> /// [`axum`]: <https://docs.rs/axum/> -/// [`viz`]: <https://docs.rs/viz/> pub fn generate_route_list_inner_with_context<IV>( app_fn: impl Fn() -> IV + 'static + Clone, additional_context: impl Fn() + 'static + Clone, From 8f078186875f8bca1fab578d03222d4257900a74 Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Wed, 10 Jan 2024 20:31:30 -0500 Subject: [PATCH 040/100] nicer formatting, remove cfg-if --- examples/errors_axum/Cargo.toml | 1 - examples/errors_axum/src/error_template.rs | 6 +- examples/errors_axum/src/fallback.rs | 81 ++++++++++++---------- examples/errors_axum/src/lib.rs | 29 ++++---- examples/errors_axum/src/main.rs | 57 ++++++++------- 5 files changed, 86 insertions(+), 88 deletions(-) diff --git a/examples/errors_axum/Cargo.toml b/examples/errors_axum/Cargo.toml index 30748b1709..5e3e3ea840 100644 --- a/examples/errors_axum/Cargo.toml +++ b/examples/errors_axum/Cargo.toml @@ -9,7 +9,6 @@ crate-type = ["cdylib", "rlib"] [dependencies] console_log = "1.0" console_error_panic_hook = "0.1" -cfg-if = "1.0" leptos = { path = "../../leptos", features = ["nightly"] } leptos_axum = { path = "../../integrations/axum", optional = true } leptos_meta = { path = "../../meta" } diff --git a/examples/errors_axum/src/error_template.rs b/examples/errors_axum/src/error_template.rs index 045ff1c787..ca4239328a 100644 --- a/examples/errors_axum/src/error_template.rs +++ b/examples/errors_axum/src/error_template.rs @@ -1,5 +1,4 @@ use crate::errors::AppError; -use cfg_if::cfg_if; use leptos::{logging::log, Errors, *}; #[cfg(feature = "ssr")] use leptos_axum::ResponseOptions; @@ -30,12 +29,13 @@ pub fn ErrorTemplate( // Only the response code for the first error is actually sent from the server // this may be customized by the specific application - cfg_if! { if #[cfg(feature="ssr")] { + #[cfg(feature = "ssr")] + { let response = use_context::<ResponseOptions>(); if let Some(response) = response { response.set_status(errors[0].status_code()); } - }} + } view! { <h1>{if errors.len() > 1 {"Errors"} else {"Error"}}</h1> diff --git a/examples/errors_axum/src/fallback.rs b/examples/errors_axum/src/fallback.rs index ba7ae7a58f..553888d286 100644 --- a/examples/errors_axum/src/fallback.rs +++ b/examples/errors_axum/src/fallback.rs @@ -1,43 +1,48 @@ -use cfg_if::cfg_if; +use crate::landing::App; +use axum::{ + body::Body, + extract::State, + http::{Request, Response, StatusCode, Uri}, + response::{IntoResponse, Response as AxumResponse}, +}; +use leptos::{view, LeptosOptions}; +use tower::ServiceExt; +use tower_http::services::ServeDir; -cfg_if! { if #[cfg(feature = "ssr")] { - use axum::{ - body::{Body}, - extract::State, - response::IntoResponse, - http::{Request, Response, StatusCode, Uri}, - }; - use axum::response::Response as AxumResponse; - use tower::ServiceExt; - use tower_http::services::ServeDir; - use leptos::{LeptosOptions, view}; - use crate::landing::App; +pub async fn file_and_error_handler( + uri: Uri, + State(options): State<LeptosOptions>, + req: Request<Body>, +) -> AxumResponse { + let root = options.site_root.clone(); + let res = get_static_file(uri.clone(), &root).await.unwrap(); - pub async fn file_and_error_handler(uri: Uri, State(options): State<LeptosOptions>, req: Request<Body>) -> AxumResponse { - let root = options.site_root.clone(); - let res = get_static_file(uri.clone(), &root).await.unwrap(); - - if res.status() == StatusCode::OK { - res.into_response() - } else{ - let handler = leptos_axum::render_app_to_stream( - options.to_owned(), - move || view!{ <App/> } - ); - handler(req).await.into_response() - } + if res.status() == StatusCode::OK { + res.into_response() + } else { + let handler = leptos_axum::render_app_to_stream( + options.to_owned(), + move || view! { <App/> }, + ); + handler(req).await.into_response() } +} - async fn get_static_file(uri: Uri, root: &str) -> Result<Response<Body>, (StatusCode, String)> { - let req = Request::builder().uri(uri.clone()).body(Body::empty()).unwrap(); - // `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot` - // This path is relative to the cargo root - match ServeDir::new(root).oneshot(req).await { - Ok(res) => Ok(res.into_response()), - Err(err) => Err(( - StatusCode::INTERNAL_SERVER_ERROR, - format!("Something went wrong: {err}"), - )), - } +async fn get_static_file( + uri: Uri, + root: &str, +) -> Result<Response<Body>, (StatusCode, String)> { + let req = Request::builder() + .uri(uri.clone()) + .body(Body::empty()) + .unwrap(); + // `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot` + // This path is relative to the cargo root + match ServeDir::new(root).oneshot(req).await { + Ok(res) => Ok(res.into_response()), + Err(err) => Err(( + StatusCode::INTERNAL_SERVER_ERROR, + format!("Something went wrong: {err}"), + )), } -}} +} diff --git a/examples/errors_axum/src/lib.rs b/examples/errors_axum/src/lib.rs index 7cfdeb063e..e657d3d1b4 100644 --- a/examples/errors_axum/src/lib.rs +++ b/examples/errors_axum/src/lib.rs @@ -1,24 +1,21 @@ -use cfg_if::cfg_if; pub mod error_template; pub mod errors; +#[cfg(feature = "ssr")] pub mod fallback; pub mod landing; -// Needs to be in lib.rs AFAIK because wasm-bindgen needs us to be compiling a lib. I may be wrong. -cfg_if! { - if #[cfg(feature = "hydrate")] { - use leptos::*; - use wasm_bindgen::prelude::wasm_bindgen; - use crate::landing::*; +use wasm_bindgen::prelude::wasm_bindgen; - #[wasm_bindgen] - pub fn hydrate() { - _ = console_log::init_with_level(log::Level::Debug); - console_error_panic_hook::set_once(); +#[cfg(feature = "hydrate")] +#[wasm_bindgen] +pub fn hydrate() { + use crate::landing::*; + use leptos::*; - leptos::mount_to_body(|| { - view! { <App/> } - }); - } - } + _ = console_log::init_with_level(log::Level::Debug); + console_error_panic_hook::set_once(); + + leptos::mount_to_body(|| { + view! { <App/> } + }); } diff --git a/examples/errors_axum/src/main.rs b/examples/errors_axum/src/main.rs index 8179e5fe36..84215fc2c6 100644 --- a/examples/errors_axum/src/main.rs +++ b/examples/errors_axum/src/main.rs @@ -1,41 +1,39 @@ -use cfg_if::cfg_if; - -cfg_if! { if #[cfg(feature = "ssr")] { - use crate::fallback::file_and_error_handler; - use crate::landing::*; - use axum::body::Body as AxumBody; - use axum::{ - extract::{State, Path}, +#[cfg(feature = "ssr")] +mod ssr_imports { + pub use axum::{ + body::Body as AxumBody, + extract::{Path, State}, http::Request, response::{IntoResponse, Response}, - routing::{get, post}, + routing::get, Router, }; - use errors_axum::*; - use leptos::{logging::log, *}; - use leptos_axum::{generate_route_list, LeptosRoutes}; -}} + pub use errors_axum::{fallback::*, landing::App}; + pub use leptos::{logging::log, *}; + pub use leptos_axum::{generate_route_list, LeptosRoutes}; -//Define a handler to test extractor with state -#[cfg(feature = "ssr")] -async fn custom_handler( - Path(id): Path<String>, - State(options): State<LeptosOptions>, - req: Request<AxumBody>, -) -> Response { - let handler = leptos_axum::render_app_to_stream_with_context( - options.clone(), - move || { - provide_context(id.clone()); - }, - App, - ); - handler(req).await.into_response() + // This custom handler lets us provide Axum State via context + pub async fn custom_handler( + Path(id): Path<String>, + State(options): State<LeptosOptions>, + req: Request<AxumBody>, + ) -> Response { + let handler = leptos_axum::render_app_to_stream_with_context( + options.clone(), + move || { + provide_context(id.clone()); + }, + App, + ); + handler(req).await.into_response() + } } #[cfg(feature = "ssr")] #[tokio::main] async fn main() { + use ssr_imports::*; + simple_logger::init_with_level(log::Level::Debug) .expect("couldn't initialize logging"); @@ -52,7 +50,6 @@ async fn main() { // build our application with a route let app = Router::new() - .route("/api/*fn_name", post(leptos_axum::handle_server_fns)) .route("/special/:id", get(custom_handler)) .leptos_routes(&leptos_options, routes, App) .fallback(file_and_error_handler) @@ -71,5 +68,5 @@ async fn main() { #[cfg(not(feature = "ssr"))] pub fn main() { // This example cannot be built as a trunk standalone CSR-only app. - // The server is needed to demonstrate the error statuses. + // The server is needed to demonstrate the error statuses. } From 31b2b9e94c4acc896451b03486751dba00128551 Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Wed, 10 Jan 2024 20:35:45 -0500 Subject: [PATCH 041/100] remove explicit `handle_server_fns` in most cases because it's now included in `.leptos_routes()` --- examples/counter_isomorphic/src/main.rs | 5 ++--- examples/hackernews/src/main.rs | 1 - examples/hackernews_js_fetch/src/lib.rs | 2 -- examples/ssr_modes/src/main.rs | 3 +-- examples/ssr_modes_axum/src/main.rs | 1 - examples/suspense_tests/src/main.rs | 5 ++--- examples/tailwind_axum/src/main.rs | 3 +-- examples/todo_app_sqlite/src/main.rs | 1 - examples/todo_app_sqlite_csr/src/main.rs | 4 ++++ 9 files changed, 10 insertions(+), 15 deletions(-) diff --git a/examples/counter_isomorphic/src/main.rs b/examples/counter_isomorphic/src/main.rs index be38369841..7129de2c10 100644 --- a/examples/counter_isomorphic/src/main.rs +++ b/examples/counter_isomorphic/src/main.rs @@ -43,7 +43,7 @@ cfg_if! { let conf = get_configuration(None).await.unwrap(); let addr = conf.leptos_options.site_addr; - let routes = generate_route_list(|| view! { <Counters/> }); + let routes = generate_route_list(Counters); HttpServer::new(move || { let leptos_options = &conf.leptos_options; @@ -51,8 +51,7 @@ cfg_if! { App::new() .service(counter_events) - .route("/api/{tail:.*}", leptos_actix::handle_server_fns()) - .leptos_routes(leptos_options.to_owned(), routes.to_owned(), || view! { <Counters/> }) + .leptos_routes(leptos_options.to_owned(), routes.to_owned(), Counters) .service(Files::new("/", site_root)) //.wrap(middleware::Compress::default()) }) diff --git a/examples/hackernews/src/main.rs b/examples/hackernews/src/main.rs index 94112f90e4..24a16044ba 100644 --- a/examples/hackernews/src/main.rs +++ b/examples/hackernews/src/main.rs @@ -35,7 +35,6 @@ cfg_if! { App::new() .service(css) .service(favicon) - .route("/api/{tail:.*}", leptos_actix::handle_server_fns()) .leptos_routes(leptos_options.to_owned(), routes.to_owned(), App) .service(Files::new("/", site_root)) //.wrap(middleware::Compress::default()) diff --git a/examples/hackernews_js_fetch/src/lib.rs b/examples/hackernews_js_fetch/src/lib.rs index f1bbeeb5e9..cb54285b05 100644 --- a/examples/hackernews_js_fetch/src/lib.rs +++ b/examples/hackernews_js_fetch/src/lib.rs @@ -43,7 +43,6 @@ cfg_if! { use axum::{ Router, - routing::post }; use leptos_axum::{generate_route_list, LeptosRoutes}; use leptos::*; @@ -64,7 +63,6 @@ cfg_if! { // build our application with a route let app: axum::Router = Router::new() .leptos_routes(&leptos_options, routes, || view! { <App/> } ) - .route("/api/*fn_name", post(leptos_axum::handle_server_fns)) .with_state(leptos_options); info!("creating handler instance"); diff --git a/examples/ssr_modes/src/main.rs b/examples/ssr_modes/src/main.rs index 51f55e3a2a..1b8f3f4485 100644 --- a/examples/ssr_modes/src/main.rs +++ b/examples/ssr_modes/src/main.rs @@ -23,11 +23,10 @@ async fn main() -> std::io::Result<()> { let site_root = &leptos_options.site_root; App::new() - .route("/api/{tail:.*}", leptos_actix::handle_server_fns()) .leptos_routes( leptos_options.to_owned(), routes.to_owned(), - || view! { <App/> }, + App, ) .service(Files::new("/", site_root)) //.wrap(middleware::Compress::default()) diff --git a/examples/ssr_modes_axum/src/main.rs b/examples/ssr_modes_axum/src/main.rs index 0c2b04aef5..96273919c0 100644 --- a/examples/ssr_modes_axum/src/main.rs +++ b/examples/ssr_modes_axum/src/main.rs @@ -19,7 +19,6 @@ async fn main() { // _ = ListPostMetadata::register(); let app = Router::new() - .route("/api/*fn_name", post(leptos_axum::handle_server_fns)) .leptos_routes(&leptos_options, routes, || view! { <App/> }) .fallback(file_and_error_handler) .with_state(leptos_options); diff --git a/examples/suspense_tests/src/main.rs b/examples/suspense_tests/src/main.rs index ffd5d49dc9..3ea89403bf 100644 --- a/examples/suspense_tests/src/main.rs +++ b/examples/suspense_tests/src/main.rs @@ -10,18 +10,17 @@ async fn main() -> std::io::Result<()> { let conf = get_configuration(None).await.unwrap(); let addr = conf.leptos_options.site_addr; // Generate the list of routes in your Leptos App - let routes = generate_route_list(|| view! { <App/> }); + let routes = generate_route_list(App); HttpServer::new(move || { let leptos_options = &conf.leptos_options; let site_root = &leptos_options.site_root; App::new() - .route("/api/{tail:.*}", leptos_actix::handle_server_fns()) .leptos_routes( leptos_options.to_owned(), routes.to_owned(), - || view! { <App/> }, + App, ) .service(Files::new("/", site_root)) //.wrap(middleware::Compress::default()) diff --git a/examples/tailwind_axum/src/main.rs b/examples/tailwind_axum/src/main.rs index d179ac888a..8bc5a0d937 100644 --- a/examples/tailwind_axum/src/main.rs +++ b/examples/tailwind_axum/src/main.rs @@ -23,8 +23,7 @@ async fn main() { // build our application with a route let app = Router::new() - .route("/api/*fn_name", post(leptos_axum::handle_server_fns)) - .leptos_routes(&leptos_options, routes, || view! { <App/> }) + .leptos_routes(&leptos_options, routes, App) .fallback(file_and_error_handler) .with_state(leptos_options); diff --git a/examples/todo_app_sqlite/src/main.rs b/examples/todo_app_sqlite/src/main.rs index 03b1906edb..09e344f3ed 100644 --- a/examples/todo_app_sqlite/src/main.rs +++ b/examples/todo_app_sqlite/src/main.rs @@ -46,7 +46,6 @@ cfg_if! { App::new() .service(css) -// .route("/api/{tail:.*}", leptos_actix::handle_server_fns()) .leptos_routes(leptos_options.to_owned(), routes.to_owned(), TodoApp) .service(Files::new("/", site_root)) //.wrap(middleware::Compress::default()) diff --git a/examples/todo_app_sqlite_csr/src/main.rs b/examples/todo_app_sqlite_csr/src/main.rs index 0d83232c38..3ea3e9c67a 100644 --- a/examples/todo_app_sqlite_csr/src/main.rs +++ b/examples/todo_app_sqlite_csr/src/main.rs @@ -32,6 +32,10 @@ async fn main() { // build our application with a route let app = Router::new() + // server function handlers are normally set up by .leptos_routes() + // 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)) .fallback(file_or_index_handler) .with_state(leptos_options); From 1d1de4ac38e7ecf94650d84e547712fee2acecad Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Wed, 10 Jan 2024 21:43:19 -0500 Subject: [PATCH 042/100] remove `cfg-if` from all examples --- examples/counter_isomorphic/Cargo.toml | 1 - examples/counter_isomorphic/src/counters.rs | 39 ++-- examples/counter_isomorphic/src/lib.rs | 24 +- examples/counter_isomorphic/src/main.rs | 14 -- examples/hackernews/Cargo.toml | 1 - examples/hackernews/src/lib.rs | 19 +- examples/hackernews/src/main.rs | 107 ++++----- examples/hackernews_axum/Cargo.toml | 1 - examples/hackernews_axum/src/fallback.rs | 80 +++---- examples/hackernews_axum/src/handlers.rs | 106 ++++----- examples/hackernews_axum/src/lib.rs | 53 ++--- examples/hackernews_axum/src/main.rs | 71 +++--- examples/hackernews_islands_axum/Cargo.toml | 1 - .../hackernews_islands_axum/src/fallback.rs | 80 +++---- .../hackernews_islands_axum/src/handlers.rs | 63 ------ examples/hackernews_islands_axum/src/lib.rs | 20 +- examples/hackernews_islands_axum/src/main.rs | 17 +- examples/hackernews_js_fetch/Cargo.toml | 1 - examples/hackernews_js_fetch/src/fallback.rs | 72 +++--- examples/hackernews_js_fetch/src/lib.rs | 40 ++-- examples/session_auth_axum/Cargo.toml | 1 - examples/session_auth_axum/src/auth.rs | 82 ++++--- .../session_auth_axum/src/error_template.rs | 10 +- examples/session_auth_axum/src/fallback.rs | 85 +++---- examples/session_auth_axum/src/lib.rs | 27 +-- examples/session_auth_axum/src/main.rs | 213 ++++++++++-------- examples/session_auth_axum/src/state.rs | 13 +- examples/session_auth_axum/src/todo.rs | 69 +++--- examples/ssr_modes/Cargo.toml | 1 - examples/ssr_modes/src/lib.rs | 26 +-- examples/ssr_modes/src/main.rs | 6 +- examples/ssr_modes_axum/Cargo.toml | 1 - examples/ssr_modes_axum/src/fallback.rs | 81 +++---- examples/ssr_modes_axum/src/lib.rs | 28 +-- examples/ssr_modes_axum/src/main.rs | 2 +- examples/suspense_tests/Cargo.toml | 1 - examples/suspense_tests/src/lib.rs | 25 +- examples/suspense_tests/src/main.rs | 6 +- examples/tailwind_actix/Cargo.toml | 1 - examples/tailwind_actix/src/lib.rs | 50 ++-- examples/tailwind_actix/src/main.rs | 61 +++-- examples/tailwind_axum/Cargo.toml | 1 - examples/tailwind_axum/src/fallback.rs | 81 +++---- examples/tailwind_axum/src/lib.rs | 26 +-- examples/tailwind_axum/src/main.rs | 2 +- examples/todo_app_sqlite/Cargo.toml | 1 - examples/todo_app_sqlite/src/lib.rs | 21 +- examples/todo_app_sqlite/src/main.rs | 115 +++++----- examples/todo_app_sqlite/src/todo.rs | 24 +- examples/todo_app_sqlite_axum/Cargo.toml | 1 - .../src/error_template.rs | 11 +- examples/todo_app_sqlite_axum/src/fallback.rs | 85 +++---- examples/todo_app_sqlite_axum/src/lib.rs | 23 +- examples/todo_app_sqlite_axum/src/main.rs | 119 +++++----- examples/todo_app_sqlite_axum/src/todo.rs | 23 +- 55 files changed, 986 insertions(+), 1146 deletions(-) delete mode 100644 examples/hackernews_islands_axum/src/handlers.rs diff --git a/examples/counter_isomorphic/Cargo.toml b/examples/counter_isomorphic/Cargo.toml index 6c6db15fac..503c3bb824 100644 --- a/examples/counter_isomorphic/Cargo.toml +++ b/examples/counter_isomorphic/Cargo.toml @@ -17,7 +17,6 @@ broadcaster = "1" console_log = "1" console_error_panic_hook = "0.1" futures = "0.3" -cfg-if = "1" lazy_static = "1" leptos = { path = "../../leptos" } leptos_actix = { path = "../../integrations/actix", optional = true } diff --git a/examples/counter_isomorphic/src/counters.rs b/examples/counter_isomorphic/src/counters.rs index 7a0fde001c..755c83fb5d 100644 --- a/examples/counter_isomorphic/src/counters.rs +++ b/examples/counter_isomorphic/src/counters.rs @@ -1,34 +1,35 @@ -use cfg_if::cfg_if; use leptos::*; use leptos_meta::*; use leptos_router::*; #[cfg(feature = "ssr")] use tracing::instrument; -cfg_if! { - if #[cfg(feature = "ssr")] { - use std::sync::atomic::{AtomicI32, Ordering}; - use broadcaster::BroadcastChannel; - use once_cell::sync::OnceCell; +#[cfg(feature = "ssr")] +mod ssr_imports { + pub use broadcaster::BroadcastChannel; + pub use once_cell::sync::OnceCell; + pub use std::sync::atomic::{AtomicI32, Ordering}; + + pub static COUNT: AtomicI32 = AtomicI32::new(0); - static COUNT: AtomicI32 = AtomicI32::new(0); + lazy_static::lazy_static! { + pub static ref COUNT_CHANNEL: BroadcastChannel<i32> = BroadcastChannel::new(); + } - lazy_static::lazy_static! { - pub static ref COUNT_CHANNEL: BroadcastChannel<i32> = BroadcastChannel::new(); - } + static LOG_INIT: OnceCell<()> = OnceCell::new(); - static LOG_INIT: OnceCell<()> = OnceCell::new(); - fn init_logging() { - LOG_INIT.get_or_init(|| { - simple_logger::SimpleLogger::new().env().init().unwrap(); - }); - } + pub fn init_logging() { + LOG_INIT.get_or_init(|| { + simple_logger::SimpleLogger::new().env().init().unwrap(); + }); } } #[server] #[cfg_attr(feature = "ssr", instrument)] pub async fn get_server_count() -> Result<i32, ServerFnError> { + use ssr_imports::*; + Ok(COUNT.load(Ordering::Relaxed)) } @@ -38,6 +39,8 @@ pub async fn adjust_server_count( delta: i32, msg: String, ) -> Result<i32, ServerFnError> { + use ssr_imports::*; + let new = COUNT.load(Ordering::Relaxed) + delta; COUNT.store(new, Ordering::Relaxed); _ = COUNT_CHANNEL.send(&new).await; @@ -48,6 +51,8 @@ pub async fn adjust_server_count( #[server] #[cfg_attr(feature = "ssr", instrument)] pub async fn clear_server_count() -> Result<i32, ServerFnError> { + use ssr_imports::*; + COUNT.store(0, Ordering::Relaxed); _ = COUNT_CHANNEL.send(&0).await; Ok(0) @@ -55,7 +60,7 @@ pub async fn clear_server_count() -> Result<i32, ServerFnError> { #[component] pub fn Counters() -> impl IntoView { #[cfg(feature = "ssr")] - init_logging(); + ssr_imports::init_logging(); provide_meta_context(); view! { diff --git a/examples/counter_isomorphic/src/lib.rs b/examples/counter_isomorphic/src/lib.rs index 5970dfc3b7..bdf43e9893 100644 --- a/examples/counter_isomorphic/src/lib.rs +++ b/examples/counter_isomorphic/src/lib.rs @@ -1,21 +1,13 @@ -use cfg_if::cfg_if; pub mod counters; -// Needs to be in lib.rs AFAIK because wasm-bindgen needs us to be compiling a lib. I may be wrong. -cfg_if! { - if #[cfg(feature = "hydrate")] { - use leptos::*; - use wasm_bindgen::prelude::wasm_bindgen; - use crate::counters::*; +#[cfg(feature = "hydrate")] +#[wasm_bindgen::prelude::wasm_bindgen] +pub fn hydrate() { + use crate::counters::*; + use leptos::*; - #[wasm_bindgen] - pub fn hydrate() { - _ = console_log::init_with_level(log::Level::Debug); - console_error_panic_hook::set_once(); + _ = console_log::init_with_level(log::Level::Debug); + console_error_panic_hook::set_once(); - mount_to_body(|| { - view! { <Counters/> } - }); - } - } + mount_to_body(Counters); } diff --git a/examples/counter_isomorphic/src/main.rs b/examples/counter_isomorphic/src/main.rs index 7129de2c10..1dd7f6828c 100644 --- a/examples/counter_isomorphic/src/main.rs +++ b/examples/counter_isomorphic/src/main.rs @@ -1,10 +1,5 @@ -use cfg_if::cfg_if; mod counters; -// boilerplate to run in different modes -cfg_if! { - // server-only stuff - if #[cfg(feature = "ssr")] { use leptos::*; use actix_files::{Files}; use actix_web::*; @@ -59,13 +54,4 @@ cfg_if! { .run() .await } - } - - // client-only main for Trunk - else { - pub fn main() { - // isomorphic counters cannot work in a Client-Side-Rendered only - // app as a server is required to maintain state - } - } } diff --git a/examples/hackernews/Cargo.toml b/examples/hackernews/Cargo.toml index af07988f4c..07190636a4 100644 --- a/examples/hackernews/Cargo.toml +++ b/examples/hackernews/Cargo.toml @@ -15,7 +15,6 @@ actix-files = { version = "0.6", optional = true } actix-web = { version = "4", optional = true, features = ["macros"] } console_log = "1" console_error_panic_hook = "0.1" -cfg-if = "1" leptos = { path = "../../leptos", features = ["nightly"] } leptos_meta = { path = "../../meta", features = ["nightly"] } leptos_actix = { path = "../../integrations/actix", optional = true } diff --git a/examples/hackernews/src/lib.rs b/examples/hackernews/src/lib.rs index 2493ce4461..dd46495ce1 100644 --- a/examples/hackernews/src/lib.rs +++ b/examples/hackernews/src/lib.rs @@ -1,4 +1,3 @@ -use cfg_if::cfg_if; use leptos::*; use leptos_meta::*; use leptos_router::*; @@ -33,16 +32,10 @@ pub fn App() -> impl IntoView { } } -// Needs to be in lib.rs AFAIK because wasm-bindgen needs us to be compiling a lib. I may be wrong. -cfg_if! { - if #[cfg(feature = "hydrate")] { - use wasm_bindgen::prelude::wasm_bindgen; - - #[wasm_bindgen] - pub fn hydrate() { - _ = console_log::init_with_level(log::Level::Debug); - console_error_panic_hook::set_once(); - leptos::mount_to_body(App); - } - } +#[cfg(feature = "hydrate")] +#[wasm_bindgen::prelude::wasm_bindgen] +pub fn hydrate() { + _ = console_log::init_with_level(log::Level::Debug); + console_error_panic_hook::set_once(); + leptos::mount_to_body(App); } diff --git a/examples/hackernews/src/main.rs b/examples/hackernews/src/main.rs index 24a16044ba..5ee11f48c0 100644 --- a/examples/hackernews/src/main.rs +++ b/examples/hackernews/src/main.rs @@ -1,55 +1,56 @@ -use cfg_if::cfg_if; -use leptos::*; - -// boilerplate to run in different modes -cfg_if! { - // server-only stuff - if #[cfg(feature = "ssr")] { - use actix_files::{Files}; - use actix_web::*; - use hackernews::{App}; - use leptos_actix::{LeptosRoutes, generate_route_list}; - - #[get("/style.css")] - async fn css() -> impl Responder { - actix_files::NamedFile::open_async("./style.css").await - } - #[get("/favicon.ico")] - async fn favicon() -> impl Responder { - actix_files::NamedFile::open_async("./target/site//favicon.ico").await - } - - #[actix_web::main] - async fn main() -> std::io::Result<()> { - // Setting this to None means we'll be using cargo-leptos and its env vars. - let conf = get_configuration(None).await.unwrap(); - - let addr = conf.leptos_options.site_addr; - // Generate the list of routes in your Leptos App - let routes = generate_route_list(App); - - HttpServer::new(move || { - let leptos_options = &conf.leptos_options; - let site_root = &leptos_options.site_root; - - App::new() - .service(css) - .service(favicon) - .leptos_routes(leptos_options.to_owned(), routes.to_owned(), App) - .service(Files::new("/", site_root)) - //.wrap(middleware::Compress::default()) - }) - .bind(&addr)? - .run() - .await - } - } else { - fn main() { - use hackernews::{App}; - - _ = console_log::init_with_level(log::Level::Debug); - console_error_panic_hook::set_once(); - mount_to_body(App) - } +// server-only stuff +#[cfg(feature = "ssr")] +mod ssr_imports { + pub use actix_files::Files; + pub use actix_web::*; + pub use hackernews::App; + pub use leptos_actix::{generate_route_list, LeptosRoutes}; + + #[get("/style.css")] + pub async fn css() -> impl Responder { + actix_files::NamedFile::open_async("./style.css").await + } + #[get("/favicon.ico")] + pub async fn favicon() -> impl Responder { + actix_files::NamedFile::open_async("./target/site//favicon.ico").await } } + +#[cfg(feature = "ssr")] +#[actix_web::main] +async fn main() -> std::io::Result<()> { + use leptos::get_configuration; + use ssr_imports::*; + + // Setting this to None means we'll be using cargo-leptos and its env vars. + let conf = get_configuration(None).await.unwrap(); + + let addr = conf.leptos_options.site_addr; + // Generate the list of routes in your Leptos App + let routes = generate_route_list(App); + + HttpServer::new(move || { + let leptos_options = &conf.leptos_options; + let site_root = &leptos_options.site_root; + + App::new() + .service(css) + .service(favicon) + .leptos_routes(leptos_options.to_owned(), routes.to_owned(), App) + .service(Files::new("/", site_root)) + //.wrap(middleware::Compress::default()) + }) + .bind(&addr)? + .run() + .await +} + +// CSR-only setup +#[cfg(not(feature = "ssr"))] +fn main() { + use hackernews::App; + + _ = console_log::init_with_level(log::Level::Debug); + console_error_panic_hook::set_once(); + mount_to_body(App) +} diff --git a/examples/hackernews_axum/Cargo.toml b/examples/hackernews_axum/Cargo.toml index 41b5a01bcb..151358cfcc 100644 --- a/examples/hackernews_axum/Cargo.toml +++ b/examples/hackernews_axum/Cargo.toml @@ -13,7 +13,6 @@ lto = true [dependencies] console_log = "1.0" console_error_panic_hook = "0.1" -cfg-if = "1.0" leptos = { path = "../../leptos", features = ["nightly"] } leptos_axum = { path = "../../integrations/axum", optional = true } leptos_meta = { path = "../../meta", features = ["nightly"] } diff --git a/examples/hackernews_axum/src/fallback.rs b/examples/hackernews_axum/src/fallback.rs index dbdcd85ed9..7967e0062f 100644 --- a/examples/hackernews_axum/src/fallback.rs +++ b/examples/hackernews_axum/src/fallback.rs @@ -1,44 +1,48 @@ -use cfg_if::cfg_if; +use crate::error_template::error_template; +use axum::{ + body::Body, + extract::State, + http::{Request, Response, StatusCode, Uri}, + response::{IntoResponse, Response as AxumResponse}, +}; +use leptos::LeptosOptions; +use tower::ServiceExt; +use tower_http::services::ServeDir; -cfg_if! { -if #[cfg(feature = "ssr")] { - use axum::{ - body::Body, - extract::State, - response::IntoResponse, - http::{Request, Response, StatusCode, Uri}, - }; - use axum::response::Response as AxumResponse; - use tower::ServiceExt; - use tower_http::services::ServeDir; - use leptos::{LeptosOptions}; - use crate::error_template::error_template; +pub async fn file_and_error_handler( + uri: Uri, + State(options): State<LeptosOptions>, + req: Request<Body>, +) -> AxumResponse { + let root = options.site_root.clone(); + let res = get_static_file(uri.clone(), &root).await.unwrap(); - pub async fn file_and_error_handler(uri: Uri, State(options): State<LeptosOptions>, req: Request<Body>) -> AxumResponse { - let root = options.site_root.clone(); - let res = get_static_file(uri.clone(), &root).await.unwrap(); - - if res.status() == StatusCode::OK { - res.into_response() - } else{ - let handler = leptos_axum::render_app_to_stream(options.to_owned(), || error_template( None)); - handler(req).await.into_response() - } + if res.status() == StatusCode::OK { + res.into_response() + } else { + let handler = + leptos_axum::render_app_to_stream(options.to_owned(), || { + error_template(None) + }); + handler(req).await.into_response() } +} - async fn get_static_file(uri: Uri, root: &str) -> Result<Response<Body>, (StatusCode, String)> { - let req = Request::builder().uri(uri.clone()).body(Body::empty()).unwrap(); - // `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot` - // This path is relative to the cargo root - match ServeDir::new(root).oneshot(req).await { - Ok(res) => Ok(res.into_response()), - Err(err) => Err(( - StatusCode::INTERNAL_SERVER_ERROR, - format!("Something went wrong: {}", err), - )), - } +async fn get_static_file( + uri: Uri, + root: &str, +) -> Result<Response<Body>, (StatusCode, String)> { + let req = Request::builder() + .uri(uri.clone()) + .body(Body::empty()) + .unwrap(); + // `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot` + // This path is relative to the cargo root + match ServeDir::new(root).oneshot(req).await { + Ok(res) => Ok(res.into_response()), + Err(err) => Err(( + StatusCode::INTERNAL_SERVER_ERROR, + format!("Something went wrong: {}", err), + )), } - - -} } diff --git a/examples/hackernews_axum/src/handlers.rs b/examples/hackernews_axum/src/handlers.rs index 4554b1f191..5b22633bb2 100644 --- a/examples/hackernews_axum/src/handlers.rs +++ b/examples/hackernews_axum/src/handlers.rs @@ -1,64 +1,68 @@ -use cfg_if::cfg_if; +use axum::{ + body::Body, + http::{Request, Response, StatusCode, Uri}, + response::IntoResponse, +}; +use tower::ServiceExt; +use tower_http::services::ServeDir; -cfg_if! { -if #[cfg(feature = "ssr")] { - use axum::{ - body::Body, - http::{Request, Response, StatusCode, Uri}, - response::IntoResponse, - }; - use tower::ServiceExt; - use tower_http::services::ServeDir; +pub async fn file_handler( + uri: Uri, +) -> Result<Response<Body>, (StatusCode, String)> { + let res = get_static_file(uri.clone(), "/pkg").await?; - pub async fn file_handler(uri: Uri) -> Result<Response<Body>, (StatusCode, String)> { - let res = get_static_file(uri.clone(), "/pkg").await?; - - if res.status() == StatusCode::NOT_FOUND { - // try with `.html` - // TODO: handle if the Uri has query parameters - match format!("{}.html", uri).parse() { - Ok(uri_html) => get_static_file(uri_html, "/pkg").await, - Err(_) => Err((StatusCode::INTERNAL_SERVER_ERROR, "Invalid URI".to_string())), - } - } else { - Ok(res) + if res.status() == StatusCode::NOT_FOUND { + // try with `.html` + // TODO: handle if the Uri has query parameters + match format!("{}.html", uri).parse() { + Ok(uri_html) => get_static_file(uri_html, "/pkg").await, + Err(_) => Err(( + StatusCode::INTERNAL_SERVER_ERROR, + "Invalid URI".to_string(), + )), } + } else { + Ok(res) } +} - pub async fn get_static_file_handler(uri: Uri) -> Result<Response<Body>, (StatusCode, String)> { - let res = get_static_file(uri.clone(), "/static").await?; +pub async fn get_static_file_handler( + uri: Uri, +) -> Result<Response<Body>, (StatusCode, String)> { + let res = get_static_file(uri.clone(), "/static").await?; - if res.status() == StatusCode::NOT_FOUND { - Err((StatusCode::INTERNAL_SERVER_ERROR, "Invalid URI".to_string())) - } else { - Ok(res) - } + if res.status() == StatusCode::NOT_FOUND { + Err((StatusCode::INTERNAL_SERVER_ERROR, "Invalid URI".to_string())) + } else { + Ok(res) } +} - async fn get_static_file(uri: Uri, base: &str) -> Result<Response<Body>, (StatusCode, String)> { - let req = Request::builder().uri(&uri).body(Body::empty()).unwrap(); +async fn get_static_file( + uri: Uri, + base: &str, +) -> Result<Response<Body>, (StatusCode, String)> { + let req = Request::builder().uri(&uri).body(Body::empty()).unwrap(); - // `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot` - // When run normally, the root should be the crate root - if base == "/static" { - match ServeDir::new("./static").oneshot(req).await { - Ok(res) => Ok(res.into_response()), - Err(err) => Err(( - StatusCode::INTERNAL_SERVER_ERROR, - format!("Something went wrong: {}", err), - )) - } - } else if base == "/pkg" { - match ServeDir::new("./pkg").oneshot(req).await { - Ok(res) => Ok(res.into_response()), - Err(err) => Err(( - StatusCode::INTERNAL_SERVER_ERROR, - format!("Something went wrong: {}", err), - )), + // `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot` + // When run normally, the root should be the crate root + if base == "/static" { + match ServeDir::new("./static").oneshot(req).await { + Ok(res) => Ok(res.into_response()), + Err(err) => Err(( + StatusCode::INTERNAL_SERVER_ERROR, + format!("Something went wrong: {}", err), + )), } - } else{ - Err((StatusCode::NOT_FOUND, "Not Found".to_string())) + } else if base == "/pkg" { + match ServeDir::new("./pkg").oneshot(req).await { + Ok(res) => Ok(res.into_response()), + Err(err) => Err(( + StatusCode::INTERNAL_SERVER_ERROR, + format!("Something went wrong: {}", err), + )), } - } + } else { + Err((StatusCode::NOT_FOUND, "Not Found".to_string())) } } diff --git a/examples/hackernews_axum/src/lib.rs b/examples/hackernews_axum/src/lib.rs index 0749486ba2..82814e9f35 100644 --- a/examples/hackernews_axum/src/lib.rs +++ b/examples/hackernews_axum/src/lib.rs @@ -1,10 +1,11 @@ -use cfg_if::cfg_if; use leptos::{component, view, IntoView}; use leptos_meta::*; use leptos_router::*; mod api; pub mod error_template; +#[cfg(feature = "ssr")] pub mod fallback; +#[cfg(feature = "ssr")] pub mod handlers; mod routes; use routes::{nav::*, stories::*, story::*, users::*}; @@ -12,38 +13,28 @@ use routes::{nav::*, stories::*, story::*, users::*}; #[component] pub fn App() -> impl IntoView { provide_meta_context(); - view! { - <> - <Link rel="shortcut icon" type_="image/ico" href="/favicon.ico"/> - <Stylesheet id="leptos" href="/pkg/hackernews_axum.css"/> - <Meta name="description" content="Leptos implementation of a HackerNews demo."/> - <Router> - <Nav /> - <main> - <Routes> - <Route path="users/:id" view=User/> - <Route path="stories/:id" view=Story/> - <Route path=":stories?" view=Stories/> - </Routes> - </main> - </Router> - </> + view! { + <Link rel="shortcut icon" type_="image/ico" href="/favicon.ico"/> + <Stylesheet id="leptos" href="/pkg/hackernews_axum.css"/> + <Meta name="description" content="Leptos implementation of a HackerNews demo."/> + <Router> + <Nav /> + <main> + <Routes> + <Route path="users/:id" view=User/> + <Route path="stories/:id" view=Story/> + <Route path=":stories?" view=Stories/> + </Routes> + </main> + </Router> } } -// Needs to be in lib.rs AFAIK because wasm-bindgen needs us to be compiling a lib. I may be wrong. -cfg_if! { - if #[cfg(feature = "hydrate")] { - use wasm_bindgen::prelude::wasm_bindgen; - - #[wasm_bindgen] - pub fn hydrate() { - _ = console_log::init_with_level(log::Level::Debug); - console_error_panic_hook::set_once(); - leptos::mount_to_body(move || { - view! { <App/> } - }); - } - } +#[cfg(feature = "hydrate")] +#[wasm_bindgen::prelude::wasm_bindgen] +pub fn hydrate() { + _ = console_log::init_with_level(log::Level::Debug); + console_error_panic_hook::set_once(); + leptos::mount_to_body(App); } diff --git a/examples/hackernews_axum/src/main.rs b/examples/hackernews_axum/src/main.rs index da4db51648..a8df2367c3 100644 --- a/examples/hackernews_axum/src/main.rs +++ b/examples/hackernews_axum/src/main.rs @@ -1,54 +1,41 @@ -use cfg_if::cfg_if; -use leptos::{logging::log, *}; - -// boilerplate to run in different modes -cfg_if! { -if #[cfg(feature = "ssr")] { - use axum::{ - Router, - routing::get, - }; +#[cfg(feature = "ssr")] +#[tokio::main] +async fn main() { + use axum::{routing::get, Router}; + use hackernews_axum::{fallback::file_and_error_handler, *}; + use leptos::get_configuration; use leptos_axum::{generate_route_list, LeptosRoutes}; - use hackernews_axum::fallback::file_and_error_handler; - - #[tokio::main] - async fn main() { - use hackernews_axum::*; - let conf = get_configuration(Some("Cargo.toml")).await.unwrap(); - let leptos_options = conf.leptos_options; - let addr = leptos_options.site_addr; - let routes = generate_route_list(App); + let conf = get_configuration(Some("Cargo.toml")).await.unwrap(); + let leptos_options = conf.leptos_options; + let addr = leptos_options.site_addr; + let routes = generate_route_list(App); - simple_logger::init_with_level(log::Level::Debug).expect("couldn't initialize logging"); + simple_logger::init_with_level(log::Level::Debug) + .expect("couldn't initialize logging"); - // build our application with a route - let app = Router::new() + // build our application with a route + let app = Router::new() .route("/favicon.ico", get(file_and_error_handler)) - .leptos_routes(&leptos_options, routes, || view! { <App/> } ) + .leptos_routes(&leptos_options, routes, App) .fallback(file_and_error_handler) .with_state(leptos_options); - // run our app with hyper - // `axum::Server` is a re-export of `hyper::Server` - log!("listening on {}", addr); - let listener = tokio::net::TcpListener::bind(&addr).await.unwrap(); - axum::serve(listener, app.into_make_service()) - .await - .unwrap(); - } + // run our app with hyper + // `axum::Server` is a re-export of `hyper::Server` + println!("listening on {}", addr); + let listener = tokio::net::TcpListener::bind(&addr).await.unwrap(); + axum::serve(listener, app.into_make_service()) + .await + .unwrap(); } - // client-only stuff for Trunk - else { - use hackernews_axum::*; +// client-only stuff for Trunk +#[cfg(not(feature = "ssr"))] +pub fn main() { + use hackernews_axum::*; - pub fn main() { - _ = console_log::init_with_level(log::Level::Debug); - console_error_panic_hook::set_once(); - mount_to_body(|| { - view! { <App/> } - }); - } - } + _ = console_log::init_with_level(log::Level::Debug); + console_error_panic_hook::set_once(); + mount_to_body(App); } diff --git a/examples/hackernews_islands_axum/Cargo.toml b/examples/hackernews_islands_axum/Cargo.toml index aea1fae068..aac78d6a5a 100644 --- a/examples/hackernews_islands_axum/Cargo.toml +++ b/examples/hackernews_islands_axum/Cargo.toml @@ -13,7 +13,6 @@ lto = true [dependencies] console_log = "1.0" console_error_panic_hook = "0.1" -cfg-if = "1.0" leptos = { path = "../../leptos", features = [ "nightly", "experimental-islands", diff --git a/examples/hackernews_islands_axum/src/fallback.rs b/examples/hackernews_islands_axum/src/fallback.rs index dbdcd85ed9..7967e0062f 100644 --- a/examples/hackernews_islands_axum/src/fallback.rs +++ b/examples/hackernews_islands_axum/src/fallback.rs @@ -1,44 +1,48 @@ -use cfg_if::cfg_if; +use crate::error_template::error_template; +use axum::{ + body::Body, + extract::State, + http::{Request, Response, StatusCode, Uri}, + response::{IntoResponse, Response as AxumResponse}, +}; +use leptos::LeptosOptions; +use tower::ServiceExt; +use tower_http::services::ServeDir; -cfg_if! { -if #[cfg(feature = "ssr")] { - use axum::{ - body::Body, - extract::State, - response::IntoResponse, - http::{Request, Response, StatusCode, Uri}, - }; - use axum::response::Response as AxumResponse; - use tower::ServiceExt; - use tower_http::services::ServeDir; - use leptos::{LeptosOptions}; - use crate::error_template::error_template; +pub async fn file_and_error_handler( + uri: Uri, + State(options): State<LeptosOptions>, + req: Request<Body>, +) -> AxumResponse { + let root = options.site_root.clone(); + let res = get_static_file(uri.clone(), &root).await.unwrap(); - pub async fn file_and_error_handler(uri: Uri, State(options): State<LeptosOptions>, req: Request<Body>) -> AxumResponse { - let root = options.site_root.clone(); - let res = get_static_file(uri.clone(), &root).await.unwrap(); - - if res.status() == StatusCode::OK { - res.into_response() - } else{ - let handler = leptos_axum::render_app_to_stream(options.to_owned(), || error_template( None)); - handler(req).await.into_response() - } + if res.status() == StatusCode::OK { + res.into_response() + } else { + let handler = + leptos_axum::render_app_to_stream(options.to_owned(), || { + error_template(None) + }); + handler(req).await.into_response() } +} - async fn get_static_file(uri: Uri, root: &str) -> Result<Response<Body>, (StatusCode, String)> { - let req = Request::builder().uri(uri.clone()).body(Body::empty()).unwrap(); - // `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot` - // This path is relative to the cargo root - match ServeDir::new(root).oneshot(req).await { - Ok(res) => Ok(res.into_response()), - Err(err) => Err(( - StatusCode::INTERNAL_SERVER_ERROR, - format!("Something went wrong: {}", err), - )), - } +async fn get_static_file( + uri: Uri, + root: &str, +) -> Result<Response<Body>, (StatusCode, String)> { + let req = Request::builder() + .uri(uri.clone()) + .body(Body::empty()) + .unwrap(); + // `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot` + // This path is relative to the cargo root + match ServeDir::new(root).oneshot(req).await { + Ok(res) => Ok(res.into_response()), + Err(err) => Err(( + StatusCode::INTERNAL_SERVER_ERROR, + format!("Something went wrong: {}", err), + )), } - - -} } diff --git a/examples/hackernews_islands_axum/src/handlers.rs b/examples/hackernews_islands_axum/src/handlers.rs deleted file mode 100644 index 1526447f30..0000000000 --- a/examples/hackernews_islands_axum/src/handlers.rs +++ /dev/null @@ -1,63 +0,0 @@ -use cfg_if::cfg_if; - -cfg_if! { -if #[cfg(feature = "ssr")] { - use axum::{ - body::Body, - http::{Request, Response, StatusCode, Uri}, - }; - use tower::ServiceExt; - use tower_http::services::ServeDir; - - pub async fn file_handler(uri: Uri) -> Result<Response<Body>, (StatusCode, String)> { - let res = get_static_file(uri.clone(), "/pkg").await?; - - if res.status() == StatusCode::NOT_FOUND { - // try with `.html` - // TODO: handle if the Uri has query parameters - match format!("{}.html", uri).parse() { - Ok(uri_html) => get_static_file(uri_html, "/pkg").await, - Err(_) => Err((StatusCode::INTERNAL_SERVER_ERROR, "Invalid URI".to_string())), - } - } else { - Ok(res) - } - } - - pub async fn get_static_file_handler(uri: Uri) -> Result<Response<Body>, (StatusCode, String)> { - let res = get_static_file(uri.clone(), "/static").await?; - - if res.status() == StatusCode::NOT_FOUND { - Err((StatusCode::INTERNAL_SERVER_ERROR, "Invalid URI".to_string())) - } else { - Ok(res) - } - } - - async fn get_static_file(uri: Uri, base: &str) -> Result<Response<Body>, (StatusCode, String)> { - let req = Request::builder().uri(&uri).body(Body::empty()).unwrap(); - - // `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot` - // When run normally, the root should be the crate root - if base == "/static" { - match ServeDir::new("./static").oneshot(req).await { - Ok(res) => Ok(res.into_response()), - Err(err) => Err(( - StatusCode::INTERNAL_SERVER_ERROR, - format!("Something went wrong: {}", err), - )) - } - } else if base == "/pkg" { - match ServeDir::new("./pkg").oneshot(req).await { - Ok(res) => Ok(res.into_response()), - Err(err) => Err(( - StatusCode::INTERNAL_SERVER_ERROR, - format!("Something went wrong: {}", err), - )), - } - } else{ - Err((StatusCode::NOT_FOUND, "Not Found".to_string())) - } - } - } -} diff --git a/examples/hackernews_islands_axum/src/lib.rs b/examples/hackernews_islands_axum/src/lib.rs index 8024b736f6..50aa6366e2 100644 --- a/examples/hackernews_islands_axum/src/lib.rs +++ b/examples/hackernews_islands_axum/src/lib.rs @@ -1,11 +1,11 @@ #![feature(lazy_cell)] -use cfg_if::cfg_if; use leptos::*; use leptos_meta::*; use leptos_router::*; mod api; pub mod error_template; +#[cfg(feature = "ssr")] pub mod fallback; mod routes; use routes::{nav::*, stories::*, story::*, users::*}; @@ -31,16 +31,10 @@ pub fn App() -> impl IntoView { } } -// Needs to be in lib.rs AFAIK because wasm-bindgen needs us to be compiling a lib. I may be wrong. -cfg_if! { - if #[cfg(feature = "hydrate")] { - use wasm_bindgen::prelude::wasm_bindgen; - - #[wasm_bindgen] - pub fn hydrate() { - #[cfg(debug_assertions)] - console_error_panic_hook::set_once(); - leptos::leptos_dom::HydrationCtx::stop_hydrating(); - } - } +#[cfg(feature = "hydrate")] +#[wasm_bindgen::prelude::wasm_bindgen] +pub fn hydrate() { + #[cfg(debug_assertions)] + console_error_panic_hook::set_once(); + leptos::leptos_dom::HydrationCtx::stop_hydrating(); } diff --git a/examples/hackernews_islands_axum/src/main.rs b/examples/hackernews_islands_axum/src/main.rs index 0191d06781..bcc1d42718 100644 --- a/examples/hackernews_islands_axum/src/main.rs +++ b/examples/hackernews_islands_axum/src/main.rs @@ -1,16 +1,11 @@ -#[cfg(feature = "ssr")] -mod ssr_imports { - pub use axum::{routing::get, Router}; - pub use hackernews_islands::fallback::file_and_error_handler; - pub use leptos::*; - pub use leptos_axum::{generate_route_list, LeptosRoutes}; -} - #[cfg(feature = "ssr")] #[tokio::main] async fn main() { + pub use axum::{routing::get, Router}; + pub use hackernews_islands::fallback::file_and_error_handler; use hackernews_islands::*; - use ssr_imports::*; + pub use leptos::get_configuration; + pub use leptos_axum::{generate_route_list, LeptosRoutes}; let conf = get_configuration(Some("Cargo.toml")).await.unwrap(); let leptos_options = conf.leptos_options; @@ -40,7 +35,5 @@ pub fn main() { use leptos::*; _ = console_log::init_with_level(log::Level::Debug); console_error_panic_hook::set_once(); - mount_to_body(|| { - view! { <App/> } - }); + mount_to_body(App); } diff --git a/examples/hackernews_js_fetch/Cargo.toml b/examples/hackernews_js_fetch/Cargo.toml index e69cff15d2..698a01a560 100644 --- a/examples/hackernews_js_fetch/Cargo.toml +++ b/examples/hackernews_js_fetch/Cargo.toml @@ -13,7 +13,6 @@ lto = true [dependencies] console_log = "1.0" console_error_panic_hook = "0.1" -cfg-if = "1.0" leptos = { path = "../../leptos", features = ["nightly"] } leptos_axum = { path = "../../integrations/axum", default-features = false, optional = true } leptos_meta = { path = "../../meta", features = ["nightly"] } diff --git a/examples/hackernews_js_fetch/src/fallback.rs b/examples/hackernews_js_fetch/src/fallback.rs index 099d5ee254..dddebd1cae 100644 --- a/examples/hackernews_js_fetch/src/fallback.rs +++ b/examples/hackernews_js_fetch/src/fallback.rs @@ -1,39 +1,43 @@ -use cfg_if::cfg_if; +use crate::error_template::error_template; +use axum::{ + body::Body, + extract::State, + http::{Request, Response, StatusCode, Uri}, + response::{IntoResponse, Response as AxumResponse}, +}; +//use tower::ServiceExt; +use leptos::LeptosOptions; -cfg_if! { -if #[cfg(feature = "ssr")] { - use axum::{ - body::Body, - extract::State, - response::IntoResponse, - http::{Request, Response, StatusCode, Uri}, - }; - use axum::response::Response as AxumResponse; - //use tower::ServiceExt; - use leptos::{LeptosOptions}; - use crate::error_template::error_template; +pub async fn file_and_error_handler( + uri: Uri, + State(options): State<LeptosOptions>, + req: Request<Body>, +) -> AxumResponse { + let root = options.site_root.clone(); + let res = get_static_file(uri.clone(), &root).await.unwrap(); - pub async fn file_and_error_handler(uri: Uri, State(options): State<LeptosOptions>, req: Request<Body>) -> AxumResponse { - let root = options.site_root.clone(); - let res = get_static_file(uri.clone(), &root).await.unwrap(); - - if res.status() == StatusCode::OK { - res.into_response() - } else{ - let handler = leptos_axum::render_app_to_stream(options.to_owned(), || error_template(None)); - handler(req).await.into_response() - } - } - - async fn get_static_file(uri: Uri, root: &str) -> Result<Response<Body>, (StatusCode, String)> { - let req = Request::builder().uri(uri.clone()).body(Body::empty()).unwrap(); - // `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot` - // This path is relative to the cargo root - _ = req; - _ = root; - todo!() + if res.status() == StatusCode::OK { + res.into_response() + } else { + let handler = + leptos_axum::render_app_to_stream(options.to_owned(), || { + error_template(None) + }); + handler(req).await.into_response() } - - } + +async fn get_static_file( + uri: Uri, + root: &str, +) -> Result<Response<Body>, (StatusCode, String)> { + let req = Request::builder() + .uri(uri.clone()) + .body(Body::empty()) + .unwrap(); + // `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot` + // This path is relative to the cargo root + _ = req; + _ = root; + todo!() } diff --git a/examples/hackernews_js_fetch/src/lib.rs b/examples/hackernews_js_fetch/src/lib.rs index cb54285b05..3a262b8995 100644 --- a/examples/hackernews_js_fetch/src/lib.rs +++ b/examples/hackernews_js_fetch/src/lib.rs @@ -1,9 +1,9 @@ -use cfg_if::cfg_if; use leptos::{component, view, IntoView}; use leptos_meta::*; use leptos_router::*; mod api; pub mod error_template; +#[cfg(feature = "ssr")] pub mod fallback; mod routes; use routes::{nav::*, stories::*, story::*, users::*}; @@ -29,24 +29,22 @@ pub fn App() -> impl IntoView { } } -cfg_if! { - if #[cfg(feature = "hydrate")] { - #[wasm_bindgen] - pub fn hydrate() { - _ = console_log::init_with_level(log::Level::Debug); - console_error_panic_hook::set_once(); - leptos::mount_to_body(move || { - view! { <App/> } - }); - } - } else if #[cfg(feature = "ssr")] { +#[cfg(feature = "hydrate")] +#[wasm_bindgen] +pub fn hydrate() { + _ = console_log::init_with_level(log::Level::Debug); + console_error_panic_hook::set_once(); + leptos::mount_to_body(App); +} - use axum::{ - Router, - }; - use leptos_axum::{generate_route_list, LeptosRoutes}; +#[cfg(feature = "ssr")] +mod ssr_imports { + use crate::App; + use axum::Router; use leptos::*; + use leptos_axum::{generate_route_list, LeptosRoutes}; use log::{info, Level}; + use wasm_bindgen::prelude::wasm_bindgen; #[wasm_bindgen] pub struct Handler(axum_js_fetch::App); @@ -57,13 +55,16 @@ cfg_if! { console_log::init_with_level(Level::Debug); console_error_panic_hook::set_once(); - let leptos_options = LeptosOptions::builder().output_name("client").site_pkg_dir("pkg").build(); + let leptos_options = LeptosOptions::builder() + .output_name("client") + .site_pkg_dir("pkg") + .build(); let routes = generate_route_list(App); // build our application with a route let app: axum::Router = Router::new() - .leptos_routes(&leptos_options, routes, || view! { <App/> } ) - .with_state(leptos_options); + .leptos_routes(&leptos_options, routes, App) + .with_state(leptos_options); info!("creating handler instance"); @@ -75,4 +76,3 @@ cfg_if! { } } } -} diff --git a/examples/session_auth_axum/Cargo.toml b/examples/session_auth_axum/Cargo.toml index 31ae3bdd07..7792f8e7ee 100644 --- a/examples/session_auth_axum/Cargo.toml +++ b/examples/session_auth_axum/Cargo.toml @@ -12,7 +12,6 @@ console_log = "1.0" rand = { version = "0.8", features = ["min_const_gen"], optional = true } console_error_panic_hook = "0.1" futures = "0.3" -cfg-if = "1.0" leptos = { path = "../../leptos", features = ["nightly"] } leptos_meta = { path = "../../meta", features = ["nightly"] } leptos_axum = { path = "../../integrations/axum", optional = true } diff --git a/examples/session_auth_axum/src/auth.rs b/examples/session_auth_axum/src/auth.rs index 0a9c8caac2..0324a6604b 100644 --- a/examples/session_auth_axum/src/auth.rs +++ b/examples/session_auth_axum/src/auth.rs @@ -1,17 +1,7 @@ -use cfg_if::cfg_if; use leptos::*; use serde::{Deserialize, Serialize}; use std::collections::HashSet; -cfg_if! { -if #[cfg(feature = "ssr")] { - use sqlx::SqlitePool; - use axum_session_auth::{SessionSqlitePool, Authentication, HasPermission}; - use bcrypt::{hash, verify, DEFAULT_COST}; - use crate::todo::{pool, auth}; - pub type AuthSession = axum_session_auth::AuthSession<User, i64, SessionSqlitePool, SqlitePool>; -}} - #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct User { pub id: i64, @@ -33,17 +23,33 @@ impl Default for User { } } -cfg_if! { -if #[cfg(feature = "ssr")] { - use async_trait::async_trait; +#[cfg(feature = "ssr")] +pub mod ssr { + pub use super::User; + pub use axum_session_auth::{ + Authentication, HasPermission, SessionSqlitePool, + }; + pub use sqlx::SqlitePool; + pub use std::collections::HashSet; + pub type AuthSession = axum_session_auth::AuthSession< + User, + i64, + SessionSqlitePool, + SqlitePool, + >; + pub use crate::todo::ssr::{auth, pool}; + pub use async_trait::async_trait; + pub use bcrypt::{hash, verify, DEFAULT_COST}; impl User { pub async fn get(id: i64, pool: &SqlitePool) -> Option<Self> { - let sqluser = sqlx::query_as::<_, SqlUser>("SELECT * FROM users WHERE id = ?") - .bind(id) - .fetch_one(pool) - .await - .ok()?; + let sqluser = sqlx::query_as::<_, SqlUser>( + "SELECT * FROM users WHERE id = ?", + ) + .bind(id) + .fetch_one(pool) + .await + .ok()?; //lets just get all the tokens the user can use, we will only use the full permissions if modifing them. let sql_user_perms = sqlx::query_as::<_, SqlPermissionTokens>( @@ -57,12 +63,17 @@ if #[cfg(feature = "ssr")] { Some(sqluser.into_user(Some(sql_user_perms))) } - pub async fn get_from_username(name: String, pool: &SqlitePool) -> Option<Self> { - let sqluser = sqlx::query_as::<_, SqlUser>("SELECT * FROM users WHERE username = ?") - .bind(name) - .fetch_one(pool) - .await - .ok()?; + pub async fn get_from_username( + name: String, + pool: &SqlitePool, + ) -> Option<Self> { + let sqluser = sqlx::query_as::<_, SqlUser>( + "SELECT * FROM users WHERE username = ?", + ) + .bind(name) + .fetch_one(pool) + .await + .ok()?; //lets just get all the tokens the user can use, we will only use the full permissions if modifing them. let sql_user_perms = sqlx::query_as::<_, SqlPermissionTokens>( @@ -84,7 +95,10 @@ if #[cfg(feature = "ssr")] { #[async_trait] impl Authentication<User, i64, SqlitePool> for User { - async fn load_user(userid: i64, pool: Option<&SqlitePool>) -> Result<User, anyhow::Error> { + async fn load_user( + userid: i64, + pool: Option<&SqlitePool>, + ) -> Result<User, anyhow::Error> { let pool = pool.unwrap(); User::get(userid, pool) @@ -120,7 +134,10 @@ if #[cfg(feature = "ssr")] { } impl SqlUser { - pub fn into_user(self, sql_user_perms: Option<Vec<SqlPermissionTokens>>) -> User { + pub fn into_user( + self, + sql_user_perms: Option<Vec<SqlPermissionTokens>>, + ) -> User { User { id: self.id, username: self.username, @@ -137,15 +154,16 @@ if #[cfg(feature = "ssr")] { } } } -} -#[server(Foo, "/api")] +#[server] pub async fn foo() -> Result<String, ServerFnError> { Ok(String::from("Bar!")) } -#[server(GetUser, "/api")] +#[server] pub async fn get_user() -> Result<Option<User>, ServerFnError> { + use crate::todo::ssr::auth; + let auth = auth()?; Ok(auth.current_user) @@ -157,6 +175,8 @@ pub async fn login( password: String, remember: Option<String>, ) -> Result<(), ServerFnError> { + use self::ssr::*; + let pool = pool()?; let auth = auth()?; @@ -184,6 +204,8 @@ pub async fn signup( password_confirmation: String, remember: Option<String>, ) -> Result<(), ServerFnError> { + use self::ssr::*; + let pool = pool()?; let auth = auth()?; @@ -218,6 +240,8 @@ pub async fn signup( #[server(Logout, "/api")] pub async fn logout() -> Result<(), ServerFnError> { + use self::ssr::*; + let auth = auth()?; auth.logout_user(); diff --git a/examples/session_auth_axum/src/error_template.rs b/examples/session_auth_axum/src/error_template.rs index 4f760c6b69..16e81df545 100644 --- a/examples/session_auth_axum/src/error_template.rs +++ b/examples/session_auth_axum/src/error_template.rs @@ -1,5 +1,4 @@ use crate::errors::TodoAppError; -use cfg_if::cfg_if; use leptos::{Errors, *}; #[cfg(feature = "ssr")] use leptos_axum::ResponseOptions; @@ -29,13 +28,12 @@ pub fn ErrorTemplate( // Only the response code for the first error is actually sent from the server // this may be customized by the specific application - cfg_if! { - if #[cfg(feature="ssr")]{ + #[cfg(feature = "ssr")] + { let response = use_context::<ResponseOptions>(); - if let Some(response) = response{ - response.set_status(errors[0].status_code()); + if let Some(response) = response { + response.set_status(errors[0].status_code()); } - } } view! { diff --git a/examples/session_auth_axum/src/fallback.rs b/examples/session_auth_axum/src/fallback.rs index 9eb77ac5fd..66b2a5ffe0 100644 --- a/examples/session_auth_axum/src/fallback.rs +++ b/examples/session_auth_axum/src/fallback.rs @@ -1,47 +1,50 @@ -use cfg_if::cfg_if; +use crate::{error_template::ErrorTemplate, errors::TodoAppError}; +use axum::{ + body::Body, + extract::State, + http::{Request, Response, StatusCode, Uri}, + response::{IntoResponse, Response as AxumResponse}, +}; +use leptos::{view, Errors, LeptosOptions}; +use tower::ServiceExt; +use tower_http::services::ServeDir; -cfg_if! { -if #[cfg(feature = "ssr")] { - use axum::{ - body::Body, - extract::State, - response::IntoResponse, - http::{Request, Response, StatusCode, Uri}, - }; - use axum::response::Response as AxumResponse; - use tower::ServiceExt; - use tower_http::services::ServeDir; - use leptos::{LeptosOptions, Errors, view}; - use crate::error_template::ErrorTemplate; - use crate::errors::TodoAppError; +pub async fn file_and_error_handler( + uri: Uri, + State(options): State<LeptosOptions>, + req: Request<Body>, +) -> AxumResponse { + let root = options.site_root.clone(); + let res = get_static_file(uri.clone(), &root).await.unwrap(); - pub async fn file_and_error_handler(uri: Uri, State(options): State<LeptosOptions>, req: Request<Body>) -> AxumResponse { - let root = options.site_root.clone(); - let res = get_static_file(uri.clone(), &root).await.unwrap(); - - if res.status() == StatusCode::OK { - res.into_response() - } else{ - let mut errors = Errors::default(); - errors.insert_with_default_key(TodoAppError::NotFound); - let handler = leptos_axum::render_app_to_stream(options.to_owned(), move || view!{<ErrorTemplate outside_errors=errors.clone()/>}); - handler(req).await.into_response() - } + if res.status() == StatusCode::OK { + res.into_response() + } else { + let mut errors = Errors::default(); + errors.insert_with_default_key(TodoAppError::NotFound); + let handler = leptos_axum::render_app_to_stream( + options.to_owned(), + move || view! {<ErrorTemplate outside_errors=errors.clone()/>}, + ); + handler(req).await.into_response() } +} - async fn get_static_file(uri: Uri, root: &str) -> Result<Response<Body>, (StatusCode, String)> { - let req = Request::builder().uri(uri.clone()).body(Body::empty()).unwrap(); - // `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot` - // This path is relative to the cargo root - match ServeDir::new(root).oneshot(req).await { - Ok(res) => Ok(res.into_response()), - Err(err) => Err(( - StatusCode::INTERNAL_SERVER_ERROR, - format!("Something went wrong: {err}"), - )), - } +async fn get_static_file( + uri: Uri, + root: &str, +) -> Result<Response<Body>, (StatusCode, String)> { + let req = Request::builder() + .uri(uri.clone()) + .body(Body::empty()) + .unwrap(); + // `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot` + // This path is relative to the cargo root + match ServeDir::new(root).oneshot(req).await { + Ok(res) => Ok(res.into_response()), + Err(err) => Err(( + StatusCode::INTERNAL_SERVER_ERROR, + format!("Something went wrong: {err}"), + )), } - - -} } diff --git a/examples/session_auth_axum/src/lib.rs b/examples/session_auth_axum/src/lib.rs index 3e09c652e6..09cb6a6a33 100644 --- a/examples/session_auth_axum/src/lib.rs +++ b/examples/session_auth_axum/src/lib.rs @@ -1,27 +1,18 @@ -use cfg_if::cfg_if; - pub mod auth; pub mod error_template; pub mod errors; +#[cfg(feature = "ssr")] pub mod fallback; +#[cfg(feature = "ssr")] pub mod state; pub mod todo; -// Needs to be in lib.rs AFAIK because wasm-bindgen needs us to be compiling a lib. I may be wrong. -cfg_if! { - if #[cfg(feature = "hydrate")] { - use wasm_bindgen::prelude::wasm_bindgen; - use crate::todo::*; - use leptos::view; - - #[wasm_bindgen] - pub fn hydrate() { - _ = console_log::init_with_level(log::Level::Debug); - console_error_panic_hook::set_once(); +#[cfg(feature = "hydrate")] +#[wasm_bindgen::prelude::wasm_bindgen] +pub fn hydrate() { + use crate::todo::*; + _ = console_log::init_with_level(log::Level::Debug); + console_error_panic_hook::set_once(); - leptos::mount_to_body(|| { - view! { <TodoApp/> } - }); - } - } + leptos::mount_to_body(TodoApp); } diff --git a/examples/session_auth_axum/src/main.rs b/examples/session_auth_axum/src/main.rs index 9797d70d09..95ae525af6 100644 --- a/examples/session_auth_axum/src/main.rs +++ b/examples/session_auth_axum/src/main.rs @@ -1,118 +1,131 @@ -use cfg_if::cfg_if; +use axum::{ + body::Body as AxumBody, + extract::{Path, State}, + http::Request, + response::{IntoResponse, Response}, + routing::get, + Router, +}; +use axum_session::{SessionConfig, SessionLayer, SessionStore}; +use axum_session_auth::{AuthConfig, AuthSessionLayer, SessionSqlitePool}; +use leptos::{get_configuration, logging::log, provide_context}; +use leptos_axum::{ + generate_route_list, handle_server_fns_with_context, LeptosRoutes, +}; +use session_auth_axum::{ + auth::{ssr::AuthSession, User}, + fallback::file_and_error_handler, + state::AppState, + todo::*, +}; +use sqlx::{sqlite::SqlitePoolOptions, SqlitePool}; -// boilerplate to run in different modes -cfg_if! { -if #[cfg(feature = "ssr")] { - use axum::{ - response::{Response, IntoResponse}, - routing::get, - extract::{Path, State}, - http::{Request}, - body::Body as AxumBody, - Router, - }; - use session_auth_axum::todo::*; - use session_auth_axum::auth::*; - use session_auth_axum::state::AppState; - use session_auth_axum::fallback::file_and_error_handler; - use leptos_axum::{generate_route_list, LeptosRoutes, handle_server_fns_with_context}; - use leptos::{logging::log, provide_context, get_configuration}; - use sqlx::{SqlitePool, sqlite::SqlitePoolOptions}; - use axum_session::{SessionConfig, SessionLayer, SessionStore}; - use axum_session_auth::{AuthSessionLayer, AuthConfig, SessionSqlitePool}; - - async fn server_fn_handler(State(app_state): State<AppState>, auth_session: AuthSession, path: Path<String>, - request: Request<AxumBody>) -> impl IntoResponse { +async fn server_fn_handler( + State(app_state): State<AppState>, + auth_session: AuthSession, + path: Path<String>, + request: Request<AxumBody>, +) -> impl IntoResponse { + log!("{:?}", path); - log!("{:?}", path); - - handle_server_fns_with_context( move || { + handle_server_fns_with_context( + move || { provide_context(auth_session.clone()); provide_context(app_state.pool.clone()); - }, request).await - } + }, + request, + ) + .await +} - async fn leptos_routes_handler(auth_session: AuthSession, State(app_state): State<AppState>, req: Request<AxumBody>) -> Response{ - let handler = leptos_axum::render_route_with_context(app_state.leptos_options.clone(), - app_state.routes.clone(), - move || { - provide_context(auth_session.clone()); - provide_context(app_state.pool.clone()); - }, - TodoApp - ); - handler(req).await.into_response() - } +async fn leptos_routes_handler( + auth_session: AuthSession, + State(app_state): State<AppState>, + req: Request<AxumBody>, +) -> Response { + let handler = leptos_axum::render_route_with_context( + app_state.leptos_options.clone(), + app_state.routes.clone(), + move || { + provide_context(auth_session.clone()); + provide_context(app_state.pool.clone()); + }, + TodoApp, + ); + handler(req).await.into_response() +} - #[tokio::main] - async fn main() { - simple_logger::init_with_level(log::Level::Info).expect("couldn't initialize logging"); +#[tokio::main] +async fn main() { + simple_logger::init_with_level(log::Level::Info) + .expect("couldn't initialize logging"); - let pool = SqlitePoolOptions::new() - .connect("sqlite:Todos.db") - .await - .expect("Could not make pool."); + let pool = SqlitePoolOptions::new() + .connect("sqlite:Todos.db") + .await + .expect("Could not make pool."); - // Auth section - let session_config = SessionConfig::default().with_table_name("axum_sessions"); - let auth_config = AuthConfig::<i64>::default(); - let session_store = SessionStore::<SessionSqlitePool>::new(Some(pool.clone().into()), session_config).await.unwrap(); + // Auth section + let session_config = + SessionConfig::default().with_table_name("axum_sessions"); + let auth_config = AuthConfig::<i64>::default(); + let session_store = SessionStore::<SessionSqlitePool>::new( + Some(pool.clone().into()), + session_config, + ) + .await + .unwrap(); - if let Err(e) = sqlx::migrate!() - .run(&pool) - .await { - eprintln!("{e:?}"); - } + if let Err(e) = sqlx::migrate!().run(&pool).await { + eprintln!("{e:?}"); + } - // Explicit server function registration is no longer required - // on the main branch. On 0.3.0 and earlier, uncomment the lines - // below to register the server functions. - // _ = GetTodos::register(); - // _ = AddTodo::register(); - // _ = DeleteTodo::register(); - // _ = Login::register(); - // _ = Logout::register(); - // _ = Signup::register(); - // _ = GetUser::register(); - // _ = Foo::register(); + // Explicit server function registration is no longer required + // on the main branch. On 0.3.0 and earlier, uncomment the lines + // below to register the server functions. + // _ = GetTodos::register(); + // _ = AddTodo::register(); + // _ = DeleteTodo::register(); + // _ = Login::register(); + // _ = Logout::register(); + // _ = Signup::register(); + // _ = GetUser::register(); + // _ = Foo::register(); - // Setting this to None means we'll be using cargo-leptos and its env vars - let conf = get_configuration(None).await.unwrap(); - let leptos_options = conf.leptos_options; - let addr = leptos_options.site_addr; - let routes = generate_route_list(TodoApp); + // Setting this to None means we'll be using cargo-leptos and its env vars + let conf = get_configuration(None).await.unwrap(); + let leptos_options = conf.leptos_options; + let addr = leptos_options.site_addr; + let routes = generate_route_list(TodoApp); - let app_state = AppState{ - leptos_options, - pool: pool.clone(), - routes: routes.clone(), - }; + let app_state = AppState { + leptos_options, + pool: pool.clone(), + routes: routes.clone(), + }; - // build our application with a route - let app = Router::new() - .route("/api/*fn_name", get(server_fn_handler).post(server_fn_handler)) - .leptos_routes_with_handler(routes, get(leptos_routes_handler) ) + // build our application with a route + let app = Router::new() + .route( + "/api/*fn_name", + get(server_fn_handler).post(server_fn_handler), + ) + .leptos_routes_with_handler(routes, get(leptos_routes_handler)) .fallback(file_and_error_handler) - .layer(AuthSessionLayer::<User, i64, SessionSqlitePool, SqlitePool>::new(Some(pool.clone())) - .with_config(auth_config)) + .layer( + AuthSessionLayer::<User, i64, SessionSqlitePool, SqlitePool>::new( + Some(pool.clone()), + ) + .with_config(auth_config), + ) .layer(SessionLayer::new(session_store)) .with_state(app_state); - // run our app with hyper - // `axum::Server` is a re-export of `hyper::Server` - log!("listening on http://{}", &addr); - let listener = tokio::net::TcpListener::bind(&addr).await.unwrap(); - axum::serve(listener, app.into_make_service()) - .await - .unwrap(); - } -} - - // client-only stuff for Trunk - else { - pub fn main() { - // This example cannot be built as a trunk standalone CSR-only app. - // Only the server may directly connect to the database. - } - } + // run our app with hyper + // `axum::Server` is a re-export of `hyper::Server` + log!("listening on http://{}", &addr); + let listener = tokio::net::TcpListener::bind(&addr).await.unwrap(); + axum::serve(listener, app.into_make_service()) + .await + .unwrap(); } diff --git a/examples/session_auth_axum/src/state.rs b/examples/session_auth_axum/src/state.rs index e857915851..89ddaf7777 100644 --- a/examples/session_auth_axum/src/state.rs +++ b/examples/session_auth_axum/src/state.rs @@ -1,18 +1,13 @@ -use cfg_if::cfg_if; - -cfg_if! { - if #[cfg(feature = "ssr")] { -use leptos::LeptosOptions; -use sqlx::SqlitePool; use axum::extract::FromRef; +use leptos::LeptosOptions; use leptos_router::RouteListing; +use sqlx::SqlitePool; + /// This takes advantage of Axum's SubStates feature by deriving FromRef. This is the only way to have more than one /// item in Axum's State. Leptos requires you to have leptosOptions in your State struct for the leptos route handlers #[derive(FromRef, Debug, Clone)] -pub struct AppState{ +pub struct AppState { pub leptos_options: LeptosOptions, pub pool: SqlitePool, pub routes: Vec<RouteListing>, -} - } } diff --git a/examples/session_auth_axum/src/todo.rs b/examples/session_auth_axum/src/todo.rs index 97ad08ad6f..6193e09991 100644 --- a/examples/session_auth_axum/src/todo.rs +++ b/examples/session_auth_axum/src/todo.rs @@ -1,5 +1,4 @@ use crate::{auth::*, error_template::ErrorTemplate}; -use cfg_if::cfg_if; use leptos::*; use leptos_meta::*; use leptos_router::*; @@ -14,40 +13,41 @@ pub struct Todo { completed: bool, } -cfg_if! { - if #[cfg(feature = "ssr")] { +#[cfg(feature = "ssr")] +pub mod ssr { + use super::Todo; + use crate::auth::{ssr::AuthSession, User}; + use leptos::*; + use sqlx::SqlitePool; - use sqlx::SqlitePool; - use futures::future::join_all; - - pub fn pool() -> Result<SqlitePool, ServerFnError> { - use_context::<SqlitePool>() - .ok_or_else(|| ServerFnError::ServerError("Pool missing.".into())) - } + pub fn pool() -> Result<SqlitePool, ServerFnError> { + use_context::<SqlitePool>() + .ok_or_else(|| ServerFnError::ServerError("Pool missing.".into())) + } - pub fn auth() -> Result<AuthSession, ServerFnError> { - use_context::<AuthSession>() - .ok_or_else(|| ServerFnError::ServerError("Auth session missing.".into())) - } + pub fn auth() -> Result<AuthSession, ServerFnError> { + use_context::<AuthSession>().ok_or_else(|| { + ServerFnError::ServerError("Auth session missing.".into()) + }) + } - #[derive(sqlx::FromRow, Clone)] - pub struct SqlTodo { - id: u32, - user_id: i64, - title: String, - created_at: String, - completed: bool, - } + #[derive(sqlx::FromRow, Clone)] + pub struct SqlTodo { + id: u32, + user_id: i64, + title: String, + created_at: String, + completed: bool, + } - impl SqlTodo { - pub async fn into_todo(self, pool: &SqlitePool) -> Todo { - Todo { - id: self.id, - user: User::get(self.user_id, pool).await, - title: self.title, - created_at: self.created_at, - completed: self.completed, - } + impl SqlTodo { + pub async fn into_todo(self, pool: &SqlitePool) -> Todo { + Todo { + id: self.id, + user: User::get(self.user_id, pool).await, + title: self.title, + created_at: self.created_at, + completed: self.completed, } } } @@ -55,6 +55,9 @@ cfg_if! { #[server(GetTodos, "/api")] pub async fn get_todos() -> Result<Vec<Todo>, ServerFnError> { + use self::ssr::{pool, SqlTodo}; + use futures::future::join_all; + let pool = pool()?; Ok(join_all( @@ -69,6 +72,8 @@ pub async fn get_todos() -> Result<Vec<Todo>, ServerFnError> { #[server(AddTodo, "/api")] pub async fn add_todo(title: String) -> Result<(), ServerFnError> { + use self::ssr::*; + let user = get_user().await?; let pool = pool()?; @@ -93,6 +98,8 @@ pub async fn add_todo(title: String) -> Result<(), ServerFnError> { // The struct name and path prefix arguments are optional. #[server] pub async fn delete_todo(id: u16) -> Result<(), ServerFnError> { + use self::ssr::*; + let pool = pool()?; Ok(sqlx::query("DELETE FROM todos WHERE id = $1") diff --git a/examples/ssr_modes/Cargo.toml b/examples/ssr_modes/Cargo.toml index 732ce97d2f..da2d7cecf5 100644 --- a/examples/ssr_modes/Cargo.toml +++ b/examples/ssr_modes/Cargo.toml @@ -11,7 +11,6 @@ actix-files = { version = "0.6", optional = true } actix-web = { version = "4", optional = true, features = ["macros"] } console_error_panic_hook = "0.1" console_log = "1" -cfg-if = "1" lazy_static = "1" leptos = { path = "../../leptos", features = ["nightly"] } leptos_meta = { path = "../../meta", features = ["nightly"] } diff --git a/examples/ssr_modes/src/lib.rs b/examples/ssr_modes/src/lib.rs index bc654c4897..b6f5d5ebbb 100644 --- a/examples/ssr_modes/src/lib.rs +++ b/examples/ssr_modes/src/lib.rs @@ -1,25 +1,15 @@ #![feature(result_flattening)] pub mod app; -use cfg_if::cfg_if; -cfg_if! { -if #[cfg(feature = "hydrate")] { +#[cfg(feature = "hydrate")] +#[wasm_bindgen::prelude::wasm_bindgen] +pub fn hydrate() { + use app::*; - use wasm_bindgen::prelude::wasm_bindgen; + // initializes logging using the `log` crate + _ = console_log::init_with_level(log::Level::Debug); + console_error_panic_hook::set_once(); - #[wasm_bindgen] - pub fn hydrate() { - use app::*; - use leptos::*; - - // initializes logging using the `log` crate - _ = console_log::init_with_level(log::Level::Debug); - console_error_panic_hook::set_once(); - - leptos::mount_to_body(move || { - view! { <App/> } - }); - } -} + leptos::mount_to_body(App); } diff --git a/examples/ssr_modes/src/main.rs b/examples/ssr_modes/src/main.rs index 1b8f3f4485..e998e4907c 100644 --- a/examples/ssr_modes/src/main.rs +++ b/examples/ssr_modes/src/main.rs @@ -23,11 +23,7 @@ async fn main() -> std::io::Result<()> { let site_root = &leptos_options.site_root; App::new() - .leptos_routes( - leptos_options.to_owned(), - routes.to_owned(), - App, - ) + .leptos_routes(leptos_options.to_owned(), routes.to_owned(), App) .service(Files::new("/", site_root)) //.wrap(middleware::Compress::default()) }) diff --git a/examples/ssr_modes_axum/Cargo.toml b/examples/ssr_modes_axum/Cargo.toml index e57730e9c6..dcc6568146 100644 --- a/examples/ssr_modes_axum/Cargo.toml +++ b/examples/ssr_modes_axum/Cargo.toml @@ -9,7 +9,6 @@ crate-type = ["cdylib", "rlib"] [dependencies] console_error_panic_hook = "0.1" console_log = "1" -cfg-if = "1" lazy_static = "1" leptos = { path = "../../leptos", features = ["nightly"] } leptos_meta = { path = "../../meta", features = ["nightly"] } diff --git a/examples/ssr_modes_axum/src/fallback.rs b/examples/ssr_modes_axum/src/fallback.rs index 1d6f95333c..205c3abd6b 100644 --- a/examples/ssr_modes_axum/src/fallback.rs +++ b/examples/ssr_modes_axum/src/fallback.rs @@ -1,43 +1,48 @@ -use cfg_if::cfg_if; +use crate::app::App; +use axum::{ + body::Body, + extract::State, + http::{Request, Response, StatusCode, Uri}, + response::{IntoResponse, Response as AxumResponse}, +}; +use leptos::{view, LeptosOptions}; +use tower::ServiceExt; +use tower_http::services::ServeDir; -cfg_if! { if #[cfg(feature = "ssr")] { - use axum::{ - body::Body, - extract::State, - response::IntoResponse, - http::{Request, Response, StatusCode, Uri}, - }; - use axum::response::Response as AxumResponse; - use tower::ServiceExt; - use tower_http::services::ServeDir; - use leptos::{LeptosOptions, view}; - use crate::app::App; +pub async fn file_and_error_handler( + uri: Uri, + State(options): State<LeptosOptions>, + req: Request<Body>, +) -> AxumResponse { + let root = options.site_root.clone(); + let res = get_static_file(uri.clone(), &root).await.unwrap(); - pub async fn file_and_error_handler(uri: Uri, State(options): State<LeptosOptions>, req: Request<Body>) -> AxumResponse { - let root = options.site_root.clone(); - let res = get_static_file(uri.clone(), &root).await.unwrap(); - - if res.status() == StatusCode::OK { - res.into_response() - } else{ - let handler = leptos_axum::render_app_to_stream( - options.to_owned(), - move || view!{ <App/> } - ); - handler(req).await.into_response() - } + if res.status() == StatusCode::OK { + res.into_response() + } else { + let handler = leptos_axum::render_app_to_stream( + options.to_owned(), + move || view! { <App/> }, + ); + handler(req).await.into_response() } +} - async fn get_static_file(uri: Uri, root: &str) -> Result<Response<Body>, (StatusCode, String)> { - let req = Request::builder().uri(uri.clone()).body(Body::empty()).unwrap(); - // `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot` - // This path is relative to the cargo root - match ServeDir::new(root).oneshot(req).await { - Ok(res) => Ok(res.into_response()), - Err(err) => Err(( - StatusCode::INTERNAL_SERVER_ERROR, - format!("Something went wrong: {err}"), - )), - } +async fn get_static_file( + uri: Uri, + root: &str, +) -> Result<Response<Body>, (StatusCode, String)> { + let req = Request::builder() + .uri(uri.clone()) + .body(Body::empty()) + .unwrap(); + // `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot` + // This path is relative to the cargo root + match ServeDir::new(root).oneshot(req).await { + Ok(res) => Ok(res.into_response()), + Err(err) => Err(( + StatusCode::INTERNAL_SERVER_ERROR, + format!("Something went wrong: {err}"), + )), } -}} +} diff --git a/examples/ssr_modes_axum/src/lib.rs b/examples/ssr_modes_axum/src/lib.rs index 9f0cce6c4f..344da184e1 100644 --- a/examples/ssr_modes_axum/src/lib.rs +++ b/examples/ssr_modes_axum/src/lib.rs @@ -1,26 +1,18 @@ #![feature(result_flattening)] pub mod app; -pub mod fallback; -use cfg_if::cfg_if; - -cfg_if! { -if #[cfg(feature = "hydrate")] { - use wasm_bindgen::prelude::wasm_bindgen; +#[cfg(feature = "ssr")] +pub mod fallback; - #[wasm_bindgen] - pub fn hydrate() { - use app::*; - use leptos::*; +#[cfg(feature = "hydrate")] +#[wasm_bindgen::prelude::wasm_bindgen] +pub fn hydrate() { + use app::*; - // initializes logging using the `log` crate - _ = console_log::init_with_level(log::Level::Debug); - console_error_panic_hook::set_once(); + // initializes logging using the `log` crate + _ = console_log::init_with_level(log::Level::Debug); + console_error_panic_hook::set_once(); - leptos::mount_to_body(move || { - view! { <App/> } - }); - } -} + leptos::mount_to_body(App); } diff --git a/examples/ssr_modes_axum/src/main.rs b/examples/ssr_modes_axum/src/main.rs index 96273919c0..a34742646e 100644 --- a/examples/ssr_modes_axum/src/main.rs +++ b/examples/ssr_modes_axum/src/main.rs @@ -1,7 +1,7 @@ #[cfg(feature = "ssr")] #[tokio::main] async fn main() { - use axum::{routing::post, Router}; + use axum::Router; use leptos::{logging::log, *}; use leptos_axum::{generate_route_list, LeptosRoutes}; use ssr_modes_axum::{app::*, fallback::file_and_error_handler}; diff --git a/examples/suspense_tests/Cargo.toml b/examples/suspense_tests/Cargo.toml index 2e1f7e909f..65a8031c02 100644 --- a/examples/suspense_tests/Cargo.toml +++ b/examples/suspense_tests/Cargo.toml @@ -11,7 +11,6 @@ actix-files = { version = "0.6", optional = true } actix-web = { version = "4", optional = true, features = ["macros"] } console_error_panic_hook = "0.1" console_log = "1" -cfg-if = "1" leptos = { path = "../../leptos", features = ["serde"] } leptos_actix = { path = "../../integrations/actix", optional = true } leptos_router = { path = "../../router" } diff --git a/examples/suspense_tests/src/lib.rs b/examples/suspense_tests/src/lib.rs index 98d449d804..a296f173ab 100644 --- a/examples/suspense_tests/src/lib.rs +++ b/examples/suspense_tests/src/lib.rs @@ -1,23 +1,12 @@ pub mod app; -use cfg_if::cfg_if; -cfg_if! { -if #[cfg(feature = "hydrate")] { +#[wasm_bindgen::prelude::wasm_bindgen] +pub fn hydrate() { + use app::*; - use wasm_bindgen::prelude::wasm_bindgen; + // initializes logging using the `log` crate + _ = console_log::init_with_level(log::Level::Debug); + console_error_panic_hook::set_once(); - #[wasm_bindgen] - pub fn hydrate() { - use app::*; - use leptos::*; - - // initializes logging using the `log` crate - _ = console_log::init_with_level(log::Level::Debug); - console_error_panic_hook::set_once(); - - leptos::mount_to_body(|| { - view! { <App/> } - }); - } -} + leptos::mount_to_body(App); } diff --git a/examples/suspense_tests/src/main.rs b/examples/suspense_tests/src/main.rs index 3ea89403bf..41b451ba74 100644 --- a/examples/suspense_tests/src/main.rs +++ b/examples/suspense_tests/src/main.rs @@ -17,11 +17,7 @@ async fn main() -> std::io::Result<()> { let site_root = &leptos_options.site_root; App::new() - .leptos_routes( - leptos_options.to_owned(), - routes.to_owned(), - App, - ) + .leptos_routes(leptos_options.to_owned(), routes.to_owned(), App) .service(Files::new("/", site_root)) //.wrap(middleware::Compress::default()) }) diff --git a/examples/tailwind_actix/Cargo.toml b/examples/tailwind_actix/Cargo.toml index 65547f576d..d2d566a531 100644 --- a/examples/tailwind_actix/Cargo.toml +++ b/examples/tailwind_actix/Cargo.toml @@ -16,7 +16,6 @@ leptos_meta = { path = "../../meta", features = ["nightly"] } leptos_router = { path = "../../router", features = ["nightly"] } gloo-net = { version = "0.2", features = ["http"] } log = "0.4" -cfg-if = "1.0" # dependecies for client (enable when csr or hydrate set) wasm-bindgen = { version = "0.2", optional = true } diff --git a/examples/tailwind_actix/src/lib.rs b/examples/tailwind_actix/src/lib.rs index 303998183a..591a50c890 100644 --- a/examples/tailwind_actix/src/lib.rs +++ b/examples/tailwind_actix/src/lib.rs @@ -1,39 +1,27 @@ mod app; -use cfg_if::cfg_if; -cfg_if! { - if #[cfg(feature = "hydrate")] { - use wasm_bindgen::prelude::wasm_bindgen; - use crate::app::*; - use leptos::*; +#[cfg(feature = "hydrate")] +#[wasm_bindgen::prelude::wasm_bindgen] +pub fn hydrate() { + use crate::app::App; + use leptos::{logging, mount_to_body}; + _ = console_log::init_with_level(log::Level::Debug); + console_error_panic_hook::set_once(); - #[wasm_bindgen] - pub fn hydrate() { - _ = console_log::init_with_level(log::Level::Debug); - console_error_panic_hook::set_once(); + logging::log!("hydrate mode - hydrating"); - logging::log!("hydrate mode - hydrating"); - - leptos::mount_to_body(|| { - view! { <App/> } - }); - } - } - else if #[cfg(feature = "csr")] { - use wasm_bindgen::prelude::wasm_bindgen; + mount_to_body(App); +} - #[wasm_bindgen(start)] - pub fn main() { - use app::*; - use leptos::*; - _ = console_log::init_with_level(log::Level::Debug); - console_error_panic_hook::set_once(); +#[cfg(feature = "csr")] +#[wasm_bindgen::prelude::wasm_bindgen(start)] +pub fn main() { + use crate::app::App; + use leptos::{logging, mount_to_body}; + _ = console_log::init_with_level(log::Level::Debug); + console_error_panic_hook::set_once(); - logging::log!("csr mode - mounting to body"); + logging::log!("csr mode - mounting to body"); - mount_to_body(|| { - view! { <App /> } - }); - } - } + mount_to_body(App); } diff --git a/examples/tailwind_actix/src/main.rs b/examples/tailwind_actix/src/main.rs index b94b351d01..38b798a727 100644 --- a/examples/tailwind_actix/src/main.rs +++ b/examples/tailwind_actix/src/main.rs @@ -1,40 +1,35 @@ mod app; -use cfg_if::cfg_if; -cfg_if! { - if #[cfg(feature = "ssr")] { - use actix_files::Files; - use actix_web::*; - use leptos::*; - use crate::app::*; - use leptos_actix::{generate_route_list, LeptosRoutes}; +use crate::app::*; +use actix_files::Files; +use actix_web::*; +use leptos::*; +use leptos_actix::{generate_route_list, LeptosRoutes}; - #[actix_web::main] - async fn main() -> std::io::Result<()> { +#[actix_web::main] +async fn main() -> std::io::Result<()> { + // Setting this to None means we'll be using cargo-leptos and its env vars. + let conf = get_configuration(None).await.unwrap(); - // Setting this to None means we'll be using cargo-leptos and its env vars. - let conf = get_configuration(None).await.unwrap(); + let addr = conf.leptos_options.site_addr; - let addr = conf.leptos_options.site_addr; + // Generate the list of routes in your Leptos App + let routes = generate_route_list(|| view! { <App/> }); - // Generate the list of routes in your Leptos App - let routes = generate_route_list(|| view! { <App/> }); - - HttpServer::new(move || { - let leptos_options = &conf.leptos_options; - let site_root = &leptos_options.site_root; - let routes = &routes; - App::new() - .leptos_routes(leptos_options.to_owned(), routes.to_owned(), || view! { <App/> }) - .service(Files::new("/", site_root)) - .wrap(middleware::Compress::default()) - }) - .bind(&addr)? - .run() - .await - } - } - else { - pub fn main() {} - } + HttpServer::new(move || { + let leptos_options = &conf.leptos_options; + let site_root = &leptos_options.site_root; + let routes = &routes; + App::new() + .leptos_routes( + leptos_options.to_owned(), + routes.to_owned(), + || view! { <App/> }, + ) + .service(Files::new("/", site_root)) + .wrap(middleware::Compress::default()) + }) + .bind(&addr)? + .run() + .await } diff --git a/examples/tailwind_axum/Cargo.toml b/examples/tailwind_axum/Cargo.toml index e4bd1aaba7..b454c37c0a 100644 --- a/examples/tailwind_axum/Cargo.toml +++ b/examples/tailwind_axum/Cargo.toml @@ -10,7 +10,6 @@ crate-type = ["cdylib", "rlib"] axum = { version = "0.7", optional = true } console_error_panic_hook = "0.1" console_log = "1" -cfg-if = "1" leptos = { path = "../../leptos", features = ["nightly"] } leptos_meta = { path = "../../meta", features = ["nightly"] } leptos_axum = { path = "../../integrations/axum", optional = true } diff --git a/examples/tailwind_axum/src/fallback.rs b/examples/tailwind_axum/src/fallback.rs index 1d6f95333c..205c3abd6b 100644 --- a/examples/tailwind_axum/src/fallback.rs +++ b/examples/tailwind_axum/src/fallback.rs @@ -1,43 +1,48 @@ -use cfg_if::cfg_if; +use crate::app::App; +use axum::{ + body::Body, + extract::State, + http::{Request, Response, StatusCode, Uri}, + response::{IntoResponse, Response as AxumResponse}, +}; +use leptos::{view, LeptosOptions}; +use tower::ServiceExt; +use tower_http::services::ServeDir; -cfg_if! { if #[cfg(feature = "ssr")] { - use axum::{ - body::Body, - extract::State, - response::IntoResponse, - http::{Request, Response, StatusCode, Uri}, - }; - use axum::response::Response as AxumResponse; - use tower::ServiceExt; - use tower_http::services::ServeDir; - use leptos::{LeptosOptions, view}; - use crate::app::App; +pub async fn file_and_error_handler( + uri: Uri, + State(options): State<LeptosOptions>, + req: Request<Body>, +) -> AxumResponse { + let root = options.site_root.clone(); + let res = get_static_file(uri.clone(), &root).await.unwrap(); - pub async fn file_and_error_handler(uri: Uri, State(options): State<LeptosOptions>, req: Request<Body>) -> AxumResponse { - let root = options.site_root.clone(); - let res = get_static_file(uri.clone(), &root).await.unwrap(); - - if res.status() == StatusCode::OK { - res.into_response() - } else{ - let handler = leptos_axum::render_app_to_stream( - options.to_owned(), - move || view!{ <App/> } - ); - handler(req).await.into_response() - } + if res.status() == StatusCode::OK { + res.into_response() + } else { + let handler = leptos_axum::render_app_to_stream( + options.to_owned(), + move || view! { <App/> }, + ); + handler(req).await.into_response() } +} - async fn get_static_file(uri: Uri, root: &str) -> Result<Response<Body>, (StatusCode, String)> { - let req = Request::builder().uri(uri.clone()).body(Body::empty()).unwrap(); - // `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot` - // This path is relative to the cargo root - match ServeDir::new(root).oneshot(req).await { - Ok(res) => Ok(res.into_response()), - Err(err) => Err(( - StatusCode::INTERNAL_SERVER_ERROR, - format!("Something went wrong: {err}"), - )), - } +async fn get_static_file( + uri: Uri, + root: &str, +) -> Result<Response<Body>, (StatusCode, String)> { + let req = Request::builder() + .uri(uri.clone()) + .body(Body::empty()) + .unwrap(); + // `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot` + // This path is relative to the cargo root + match ServeDir::new(root).oneshot(req).await { + Ok(res) => Ok(res.into_response()), + Err(err) => Err(( + StatusCode::INTERNAL_SERVER_ERROR, + format!("Something went wrong: {err}"), + )), } -}} +} diff --git a/examples/tailwind_axum/src/lib.rs b/examples/tailwind_axum/src/lib.rs index 88e96beb01..dd1561d334 100644 --- a/examples/tailwind_axum/src/lib.rs +++ b/examples/tailwind_axum/src/lib.rs @@ -1,20 +1,16 @@ -use cfg_if::cfg_if; pub mod app; + +#[cfg(feature = "ssr")] pub mod fallback; -cfg_if! { if #[cfg(feature = "hydrate")] { - use leptos::*; - use wasm_bindgen::prelude::wasm_bindgen; - use crate::app::*; +#[cfg(feature = "hydrate")] +#[wasm_bindgen::prelude::wasm_bindgen] +pub fn hydrate() { + use crate::app::App; - #[wasm_bindgen] - pub fn hydrate() { - // initializes logging using the `log` crate - _ = console_log::init_with_level(log::Level::Debug); - console_error_panic_hook::set_once(); + // initializes logging using the `log` crate + _ = console_log::init_with_level(log::Level::Debug); + console_error_panic_hook::set_once(); - leptos::mount_to_body(move || { - view! { <App/> } - }); - } -}} + leptos::mount_to_body(App); +} diff --git a/examples/tailwind_axum/src/main.rs b/examples/tailwind_axum/src/main.rs index 8bc5a0d937..e1a7085be1 100644 --- a/examples/tailwind_axum/src/main.rs +++ b/examples/tailwind_axum/src/main.rs @@ -1,7 +1,7 @@ #[cfg(feature = "ssr")] #[tokio::main] async fn main() { - use axum::{routing::post, Router}; + use axum::Router; use leptos::*; use leptos_axum::{generate_route_list, LeptosRoutes}; use leptos_tailwind::{app::*, fallback::file_and_error_handler}; diff --git a/examples/todo_app_sqlite/Cargo.toml b/examples/todo_app_sqlite/Cargo.toml index f3977ca2c4..c0f97e4f64 100644 --- a/examples/todo_app_sqlite/Cargo.toml +++ b/examples/todo_app_sqlite/Cargo.toml @@ -15,7 +15,6 @@ console_log = "1.0.0" console_error_panic_hook = "0.1.7" serde = { version = "1.0.152", features = ["derive"] } futures = "0.3.25" -cfg-if = "1.0.0" leptos = { path = "../../leptos", features = ["nightly"] } leptos_actix = { path = "../../integrations/actix", optional = true } leptos_meta = { path = "../../meta", features = ["nightly"] } diff --git a/examples/todo_app_sqlite/src/lib.rs b/examples/todo_app_sqlite/src/lib.rs index eabb764e7f..74f9012fcd 100644 --- a/examples/todo_app_sqlite/src/lib.rs +++ b/examples/todo_app_sqlite/src/lib.rs @@ -1,18 +1,11 @@ -use cfg_if::cfg_if; pub mod todo; -// Needs to be in lib.rs AFAIK because wasm-bindgen needs us to be compiling a lib. I may be wrong. -cfg_if! { - if #[cfg(feature = "hydrate")] { - use wasm_bindgen::prelude::wasm_bindgen; - use crate::todo::*; +#[cfg(feature = "hydrate")] +#[wasm_bindgen::prelude::wasm_bindgen] +pub fn hydrate() { + use crate::todo::*; + console_error_panic_hook::set_once(); + _ = console_log::init_with_level(log::Level::Debug); - #[wasm_bindgen] - pub fn hydrate() { - console_error_panic_hook::set_once(); - _ = console_log::init_with_level(log::Level::Debug); - - leptos::mount_to_body(TodoApp); - } - } + leptos::mount_to_body(TodoApp); } diff --git a/examples/todo_app_sqlite/src/main.rs b/examples/todo_app_sqlite/src/main.rs index 09e344f3ed..f893c85e72 100644 --- a/examples/todo_app_sqlite/src/main.rs +++ b/examples/todo_app_sqlite/src/main.rs @@ -1,62 +1,61 @@ -use cfg_if::cfg_if; mod todo; -// boilerplate to run in different modes -cfg_if! { - // server-only stuff - if #[cfg(feature = "ssr")] { - use actix_files::{Files}; - use actix_web::*; - use crate::todo::*; - use leptos::*; - use leptos_actix::{generate_route_list, LeptosRoutes}; - - #[get("/style.css")] - async fn css() -> impl Responder { - actix_files::NamedFile::open_async("./style.css").await - } - - #[actix_web::main] - async fn main() -> std::io::Result<()> { - let mut conn = db().await.expect("couldn't connect to DB"); - sqlx::migrate!() - .run(&mut conn) - .await - .expect("could not run SQLx migrations"); - - // Explicit server function registration is no longer required - // on the main branch. On 0.3.0 and earlier, uncomment the lines - // below to register the server functions. - // _ = GetTodos::register(); - // _ = AddTodo::register(); - // _ = DeleteTodo::register(); - - // Setting this to None means we'll be using cargo-leptos and its env vars. - let conf = get_configuration(None).await.unwrap(); - - let addr = conf.leptos_options.site_addr; - - // Generate the list of routes in your Leptos App - let routes = generate_route_list(TodoApp); - - HttpServer::new(move || { - let leptos_options = &conf.leptos_options; - let site_root = &leptos_options.site_root; - let routes = &routes; - - App::new() - .service(css) - .leptos_routes(leptos_options.to_owned(), routes.to_owned(), TodoApp) - .service(Files::new("/", site_root)) - //.wrap(middleware::Compress::default()) - }) - .bind(addr)? - .run() - .await - } - } else { - fn main() { - // no client-side main function - } +#[cfg(feature = "ssr")] +mod ssr { + pub use crate::todo::*; + pub use actix_files::Files; + pub use actix_web::*; + pub use leptos::*; + pub use leptos_actix::{generate_route_list, LeptosRoutes}; + + #[get("/style.css")] + pub async fn css() -> impl Responder { + actix_files::NamedFile::open_async("./style.css").await } } + +#[cfg(feature = "ssr")] +#[actix_web::main] +async fn main() -> std::io::Result<()> { + use self::{ssr::*, todo::ssr::*}; + + let mut conn = db().await.expect("couldn't connect to DB"); + sqlx::migrate!() + .run(&mut conn) + .await + .expect("could not run SQLx migrations"); + + // Explicit server function registration is no longer required + // on the main branch. On 0.3.0 and earlier, uncomment the lines + // below to register the server functions. + // _ = GetTodos::register(); + // _ = AddTodo::register(); + // _ = DeleteTodo::register(); + + // Setting this to None means we'll be using cargo-leptos and its env vars. + let conf = get_configuration(None).await.unwrap(); + + let addr = conf.leptos_options.site_addr; + + // Generate the list of routes in your Leptos App + let routes = generate_route_list(TodoApp); + + HttpServer::new(move || { + let leptos_options = &conf.leptos_options; + let site_root = &leptos_options.site_root; + let routes = &routes; + + App::new() + .service(css) + .leptos_routes( + leptos_options.to_owned(), + routes.to_owned(), + TodoApp, + ) + .service(Files::new("/", site_root)) + //.wrap(middleware::Compress::default()) + }) + .bind(addr)? + .run() + .await +} diff --git a/examples/todo_app_sqlite/src/todo.rs b/examples/todo_app_sqlite/src/todo.rs index 6256fb8cad..cb0095782a 100644 --- a/examples/todo_app_sqlite/src/todo.rs +++ b/examples/todo_app_sqlite/src/todo.rs @@ -1,4 +1,3 @@ -use cfg_if::cfg_if; use leptos::*; use leptos_meta::*; use leptos_router::*; @@ -12,13 +11,14 @@ pub struct Todo { completed: bool, } -cfg_if! { - if #[cfg(feature = "ssr")] { - use sqlx::{Connection, SqliteConnection}; +#[cfg(feature = "ssr")] +pub mod ssr { + pub use actix_web::HttpRequest; + pub use leptos::ServerFnError; + pub use sqlx::{Connection, SqliteConnection}; - pub async fn db() -> Result<SqliteConnection, ServerFnError> { - Ok(SqliteConnection::connect("sqlite:Todos.db").await?) - } + pub async fn db() -> Result<SqliteConnection, ServerFnError> { + Ok(SqliteConnection::connect("sqlite:Todos.db").await?) } } @@ -26,8 +26,10 @@ cfg_if! { /// to the server and the server response will be encoded with CBOR. Good for binary data that doesn't encode well via the default methods #[server(encoding = "Cbor")] pub async fn get_todos() -> Result<Vec<Todo>, ServerFnError> { + use self::ssr::*; + // this is just an example of how to access server context injected in the handlers - let req = use_context::<actix_web::HttpRequest>(); + let req = use_context::<HttpRequest>(); if let Some(req) = req { println!("req.path = {:#?}", req.path()); @@ -48,6 +50,8 @@ pub async fn get_todos() -> Result<Vec<Todo>, ServerFnError> { #[server] pub async fn add_todo(title: String) -> Result<(), ServerFnError> { + use self::ssr::*; + let mut conn = db().await?; // fake API delay @@ -66,6 +70,8 @@ pub async fn add_todo(title: String) -> Result<(), ServerFnError> { // The struct name and path prefix arguments are optional. #[server] pub async fn delete_todo(id: u16) -> Result<(), ServerFnError> { + use self::ssr::*; + let mut conn = db().await?; Ok(sqlx::query("DELETE FROM todos WHERE id = $1") @@ -78,8 +84,8 @@ pub async fn delete_todo(id: u16) -> Result<(), ServerFnError> { #[component] pub fn TodoApp() -> impl IntoView { provide_meta_context(); - view! { + view! { <Link rel="shortcut icon" type_="image/ico" href="/favicon.ico"/> <Stylesheet id="leptos" href="/pkg/todo_app_sqlite.css"/> <Router> diff --git a/examples/todo_app_sqlite_axum/Cargo.toml b/examples/todo_app_sqlite_axum/Cargo.toml index 4d6e7f84ed..08d73d2b27 100644 --- a/examples/todo_app_sqlite_axum/Cargo.toml +++ b/examples/todo_app_sqlite_axum/Cargo.toml @@ -10,7 +10,6 @@ crate-type = ["cdylib", "rlib"] console_log = "1.0" console_error_panic_hook = "0.1" futures = "0.3" -cfg-if = "1.0" http = "1.0" leptos = { path = "../../leptos", features = ["nightly"] } server_fn = { path = "../../server_fn", features = ["serde-lite"] } diff --git a/examples/todo_app_sqlite_axum/src/error_template.rs b/examples/todo_app_sqlite_axum/src/error_template.rs index 329dfd7f4d..0a1731abe1 100644 --- a/examples/todo_app_sqlite_axum/src/error_template.rs +++ b/examples/todo_app_sqlite_axum/src/error_template.rs @@ -1,5 +1,4 @@ use crate::errors::TodoAppError; -use cfg_if::cfg_if; use leptos::{Errors, *}; #[cfg(feature = "ssr")] use leptos_axum::ResponseOptions; @@ -29,13 +28,12 @@ pub fn ErrorTemplate( // Only the response code for the first error is actually sent from the server // this may be customized by the specific application - cfg_if! { - if #[cfg(feature="ssr")]{ + #[cfg(feature = "ssr")] + { let response = use_context::<ResponseOptions>(); - if let Some(response) = response{ - response.set_status(errors[0].status_code()); + if let Some(response) = response { + response.set_status(errors[0].status_code()); } - } } view! { @@ -50,7 +48,6 @@ pub fn ErrorTemplate( let error_string = error.1.to_string(); let error_code= error.1.status_code(); view! { - <h2>{error_code.to_string()}</h2> <p>"Error: " {error_string}</p> } diff --git a/examples/todo_app_sqlite_axum/src/fallback.rs b/examples/todo_app_sqlite_axum/src/fallback.rs index 9eb77ac5fd..66b2a5ffe0 100644 --- a/examples/todo_app_sqlite_axum/src/fallback.rs +++ b/examples/todo_app_sqlite_axum/src/fallback.rs @@ -1,47 +1,50 @@ -use cfg_if::cfg_if; +use crate::{error_template::ErrorTemplate, errors::TodoAppError}; +use axum::{ + body::Body, + extract::State, + http::{Request, Response, StatusCode, Uri}, + response::{IntoResponse, Response as AxumResponse}, +}; +use leptos::{view, Errors, LeptosOptions}; +use tower::ServiceExt; +use tower_http::services::ServeDir; -cfg_if! { -if #[cfg(feature = "ssr")] { - use axum::{ - body::Body, - extract::State, - response::IntoResponse, - http::{Request, Response, StatusCode, Uri}, - }; - use axum::response::Response as AxumResponse; - use tower::ServiceExt; - use tower_http::services::ServeDir; - use leptos::{LeptosOptions, Errors, view}; - use crate::error_template::ErrorTemplate; - use crate::errors::TodoAppError; +pub async fn file_and_error_handler( + uri: Uri, + State(options): State<LeptosOptions>, + req: Request<Body>, +) -> AxumResponse { + let root = options.site_root.clone(); + let res = get_static_file(uri.clone(), &root).await.unwrap(); - pub async fn file_and_error_handler(uri: Uri, State(options): State<LeptosOptions>, req: Request<Body>) -> AxumResponse { - let root = options.site_root.clone(); - let res = get_static_file(uri.clone(), &root).await.unwrap(); - - if res.status() == StatusCode::OK { - res.into_response() - } else{ - let mut errors = Errors::default(); - errors.insert_with_default_key(TodoAppError::NotFound); - let handler = leptos_axum::render_app_to_stream(options.to_owned(), move || view!{<ErrorTemplate outside_errors=errors.clone()/>}); - handler(req).await.into_response() - } + if res.status() == StatusCode::OK { + res.into_response() + } else { + let mut errors = Errors::default(); + errors.insert_with_default_key(TodoAppError::NotFound); + let handler = leptos_axum::render_app_to_stream( + options.to_owned(), + move || view! {<ErrorTemplate outside_errors=errors.clone()/>}, + ); + handler(req).await.into_response() } +} - async fn get_static_file(uri: Uri, root: &str) -> Result<Response<Body>, (StatusCode, String)> { - let req = Request::builder().uri(uri.clone()).body(Body::empty()).unwrap(); - // `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot` - // This path is relative to the cargo root - match ServeDir::new(root).oneshot(req).await { - Ok(res) => Ok(res.into_response()), - Err(err) => Err(( - StatusCode::INTERNAL_SERVER_ERROR, - format!("Something went wrong: {err}"), - )), - } +async fn get_static_file( + uri: Uri, + root: &str, +) -> Result<Response<Body>, (StatusCode, String)> { + let req = Request::builder() + .uri(uri.clone()) + .body(Body::empty()) + .unwrap(); + // `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot` + // This path is relative to the cargo root + match ServeDir::new(root).oneshot(req).await { + Ok(res) => Ok(res.into_response()), + Err(err) => Err(( + StatusCode::INTERNAL_SERVER_ERROR, + format!("Something went wrong: {err}"), + )), } - - -} } diff --git a/examples/todo_app_sqlite_axum/src/lib.rs b/examples/todo_app_sqlite_axum/src/lib.rs index bc31201be2..69c52ba4ea 100644 --- a/examples/todo_app_sqlite_axum/src/lib.rs +++ b/examples/todo_app_sqlite_axum/src/lib.rs @@ -1,24 +1,15 @@ -use cfg_if::cfg_if; pub mod error_template; pub mod errors; +#[cfg(feature = "ssr")] pub mod fallback; pub mod todo; -// Needs to be in lib.rs AFAIK because wasm-bindgen needs us to be compiling a lib. I may be wrong. -cfg_if! { - if #[cfg(feature = "hydrate")] { - use leptos::*; - use wasm_bindgen::prelude::wasm_bindgen; - use crate::todo::*; +#[wasm_bindgen::prelude::wasm_bindgen] +pub fn hydrate() { + use crate::todo::TodoApp; - #[wasm_bindgen] - pub fn hydrate() { - _ = console_log::init_with_level(log::Level::Error); - console_error_panic_hook::set_once(); + _ = console_log::init_with_level(log::Level::Error); + console_error_panic_hook::set_once(); - leptos::mount_to_body(|| { - view! { <TodoApp/> } - }); - } - } + leptos::mount_to_body(TodoApp); } diff --git a/examples/todo_app_sqlite_axum/src/main.rs b/examples/todo_app_sqlite_axum/src/main.rs index a001e3f723..2b4a1cd8a6 100644 --- a/examples/todo_app_sqlite_axum/src/main.rs +++ b/examples/todo_app_sqlite_axum/src/main.rs @@ -1,77 +1,62 @@ -use cfg_if::cfg_if; -// boilerplate to run in different modes -cfg_if! { - if #[cfg(feature = "ssr")] { - use leptos::*; - use axum::{ - routing::get, - extract::{State, Path}, - http::Request, - response::{IntoResponse, Response}, - Router, - }; - use axum::body::Body; - use crate::todo::*; - use todo_app_sqlite_axum::*; - use crate::fallback::file_and_error_handler; - use leptos_axum::{generate_route_list, LeptosRoutes}; - - //Define a handler to test extractor with state - async fn custom_handler(Path(id): Path<String>, State(options): State<LeptosOptions>, req: Request<Body>) -> Response{ - let handler = leptos_axum::render_app_to_stream_with_context(options, - move || { - provide_context(id.clone()); - }, - || view! { <TodoApp/> } - ); - handler(req).await.into_response() - } +use crate::{fallback::file_and_error_handler, todo::*}; +use axum::{ + body::Body, + extract::{Path, State}, + http::Request, + response::{IntoResponse, Response}, + routing::get, + Router, +}; +use leptos::*; +use leptos_axum::{generate_route_list, LeptosRoutes}; +use todo_app_sqlite_axum::*; + +//Define a handler to test extractor with state +async fn custom_handler( + Path(id): Path<String>, + State(options): State<LeptosOptions>, + req: Request<Body>, +) -> Response { + let handler = leptos_axum::render_app_to_stream_with_context( + options, + move || { + provide_context(id.clone()); + }, + TodoApp, + ); + handler(req).await.into_response() +} - #[tokio::main] - async fn main() { - simple_logger::init_with_level(log::Level::Error).expect("couldn't initialize logging"); +#[tokio::main] +async fn main() { + use crate::todo::ssr::db; - let _conn = db().await.expect("couldn't connect to DB"); - /* sqlx::migrate!() - .run(&mut conn) - .await - .expect("could not run SQLx migrations"); */ + simple_logger::init_with_level(log::Level::Error) + .expect("couldn't initialize logging"); - // Explicit server function registration is no longer required - // on the main branch. On 0.3.0 and earlier, uncomment the lines - // below to register the server functions. - // _ = GetTodos::register(); - // _ = AddTodo::register(); - // _ = DeleteTodo::register(); + let mut conn = db().await.expect("couldn't connect to DB"); + if let Err(e) = sqlx::migrate!().run(&mut conn).await { + eprintln!("{e:?}"); + } - // Setting this to None means we'll be using cargo-leptos and its env vars - let conf = get_configuration(None).await.unwrap(); - let leptos_options = conf.leptos_options; - let addr = leptos_options.site_addr; - let routes = generate_route_list(TodoApp); + // Setting this to None means we'll be using cargo-leptos and its env vars + let conf = get_configuration(None).await.unwrap(); + let leptos_options = conf.leptos_options; + let addr = leptos_options.site_addr; + let routes = generate_route_list(TodoApp); - // build our application with a route - let app = Router::new() + // build our application with a route + let app = Router::new() .route("/special/:id", get(custom_handler)) - .leptos_routes(&leptos_options, routes, || view! { <TodoApp/> } ) + .leptos_routes(&leptos_options, routes, || view! { <TodoApp/> }) .fallback(file_and_error_handler) .with_state(leptos_options); - // run our app with hyper - // `axum::Server` is a re-export of `hyper::Server` - let listener = tokio::net::TcpListener::bind(&addr).await.unwrap(); - logging::log!("listening on http://{}", &addr); - axum::serve(listener, app.into_make_service()) - .await - .unwrap(); - } -} - - // client-only stuff for Trunk - else { - pub fn main() { - // This example cannot be built as a trunk standalone CSR-only app. - // Only the server may directly connect to the database. - } - } + // run our app with hyper + // `axum::Server` is a re-export of `hyper::Server` + let listener = tokio::net::TcpListener::bind(&addr).await.unwrap(); + logging::log!("listening on http://{}", &addr); + axum::serve(listener, app.into_make_service()) + .await + .unwrap(); } diff --git a/examples/todo_app_sqlite_axum/src/todo.rs b/examples/todo_app_sqlite_axum/src/todo.rs index 21a70b7779..8fc910ce56 100644 --- a/examples/todo_app_sqlite_axum/src/todo.rs +++ b/examples/todo_app_sqlite_axum/src/todo.rs @@ -1,5 +1,4 @@ use crate::error_template::ErrorTemplate; -use cfg_if::cfg_if; use leptos::*; use leptos_meta::*; use leptos_router::*; @@ -14,19 +13,20 @@ pub struct Todo { completed: bool, } -cfg_if! { - if #[cfg(feature = "ssr")] { - use sqlx::{Connection, SqliteConnection}; - // use http::{header::SET_COOKIE, HeaderMap, HeaderValue, StatusCode}; +#[cfg(feature = "ssr")] +pub mod ssr { + // use http::{header::SET_COOKIE, HeaderMap, HeaderValue, StatusCode}; + use leptos::ServerFnError; + use sqlx::{Connection, SqliteConnection}; - pub async fn db() -> Result<SqliteConnection, ServerFnError> { - Ok(SqliteConnection::connect("sqlite:Todos.db").await?) - } + pub async fn db() -> Result<SqliteConnection, ServerFnError> { + Ok(SqliteConnection::connect("sqlite:Todos.db").await?) } } -#[server(GetTodos, "/api")] +#[server] pub async fn get_todos() -> Result<Vec<Todo>, ServerFnError> { + use self::ssr::*; use http::request::Parts; // this is just an example of how to access server context injected in the handlers @@ -55,8 +55,9 @@ pub async fn get_todos() -> Result<Vec<Todo>, ServerFnError> { Ok(todos) } -#[server(AddTodo, "/api")] +#[server] pub async fn add_todo(title: String) -> Result<(), ServerFnError> { + use self::ssr::*; let mut conn = db().await?; // fake API delay @@ -72,9 +73,9 @@ pub async fn add_todo(title: String) -> Result<(), ServerFnError> { } } -// The struct name and path prefix arguments are optional. #[server(output = SerdeLite)] pub async fn delete_todo(id: u16) -> Result<(), ServerFnError> { + use self::ssr::*; let mut conn = db().await?; Ok(sqlx::query("DELETE FROM todos WHERE id = $1") From 0a9cdba22e0c461eb4ae1a5813afc074c5806635 Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Thu, 11 Jan 2024 17:26:08 -0500 Subject: [PATCH 043/100] getting started on docs --- server_fn/src/client.rs | 15 +++- server_fn/src/codec/mod.rs | 55 ++++++++++-- server_fn/src/lib.rs | 175 +++++++++++++++++++++++++++++++++++-- server_fn/src/redirect.rs | 12 +++ 4 files changed, 242 insertions(+), 15 deletions(-) diff --git a/server_fn/src/client.rs b/server_fn/src/client.rs index cd3599e63f..b4bbdb95fc 100644 --- a/server_fn/src/client.rs +++ b/server_fn/src/client.rs @@ -1,16 +1,26 @@ use crate::{error::ServerFnError, request::ClientReq, response::ClientRes}; use std::future::Future; +/// A client defines a pair of request/response types and the logic to send +/// and receive them. +/// +/// 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<CustErr> { + /// The type of a request sent by this client. type Request: ClientReq<CustErr> + Send; + /// The type of a response received by this client. type Response: ClientRes<CustErr> + Send; + /// Sends the request and receives a response. fn send( req: Self::Request, ) -> impl Future<Output = Result<Self::Response, ServerFnError<CustErr>>> + Send; } -#[cfg(feature = "browser")] +#[cfg(any(feature = "browser", doc))] +/// Implements [`Client`] for a `fetch` request in the browser. pub mod browser { use super::Client; use crate::{ @@ -20,6 +30,7 @@ pub mod browser { use send_wrapper::SendWrapper; use std::future::Future; + /// Implements [`Client`] for a `fetch` request in the browser. pub struct BrowserClient; impl<CustErr> Client<CustErr> for BrowserClient { @@ -42,7 +53,7 @@ pub mod browser { } } -#[cfg(feature = "reqwest")] +#[cfg(any(feature = "reqwest", doc))] pub mod reqwest { use super::Client; use crate::{error::ServerFnError, request::reqwest::CLIENT}; diff --git a/server_fn/src/codec/mod.rs b/server_fn/src/codec/mod.rs index f4c6fa60d1..af3f06313e 100644 --- a/server_fn/src/codec/mod.rs +++ b/server_fn/src/codec/mod.rs @@ -1,31 +1,47 @@ +//! The serialization/deserialization process for server functions consists of a series of steps, +//! each of which is represented by a different trait: +//! 1. [`IntoReq`]: The client serializes the [`ServerFn`] argument type into an HTTP request. +//! 2. The [`Client`] sends the request to the server. +//! 3. [`FromReq`]: The server deserializes the HTTP request back into the [`ServerFn`] type. +//! 4. The server calls calls [`ServerFn::run_body`] on the data. +//! 5. [`IntoRes`]: The server serializes the [`ServerFn::Output`] type into an HTTP response. +//! 6. The server integration applies any middleware from [`ServerFn::middlewares`] and responds to the request. +//! 7. [`FromRes`]: The client deserializes the response back into the [`ServerFn::Output`] type. +//! +//! Rather than a limited number of encodings, this crate allows you to define server functions that +//! mix and match the input encoding and output encoding. To define a new encoding, you simply implement +//! an input combination ([`IntoReq`] and [`FromReq`]) and/or an output encoding ([`IntoRes`] and [`FromRes`]). +//! This genuinely is an and/or: while some encodings can be used for both input and output ([`Json`], [`Cbor`], [`Rkyv`]), +//! others can only be used for input ([`GetUrl`], [`MultipartData`]) or only output ([`ByteStream`], [`StreamingText`]). + #[cfg(feature = "cbor")] mod cbor; -#[cfg(feature = "cbor")] +#[cfg(any(feature = "cbor", doc))] pub use cbor::*; #[cfg(feature = "json")] mod json; -#[cfg(feature = "json")] +#[cfg(any(feature = "json", doc))] pub use json::*; #[cfg(feature = "serde-lite")] mod serde_lite; -#[cfg(feature = "serde-lite")] +#[cfg(any(feature = "serde-lite", doc))] pub use serde_lite::*; #[cfg(feature = "rkyv")] mod rkyv; -#[cfg(feature = "rkyv")] +#[cfg(any(feature = "rkyv", doc))] pub use rkyv::*; #[cfg(feature = "url")] mod url; -#[cfg(feature = "url")] +#[cfg(any(feature = "url", doc))] pub use url::*; #[cfg(feature = "multipart")] mod multipart; -#[cfg(feature = "multipart")] +#[cfg(any(feature = "multipart", doc))] pub use multipart::*; mod stream; @@ -34,10 +50,37 @@ use futures::Future; use http::Method; pub use stream::*; +/// Deserializes an HTTP request into the data type. +/// +/// Implementations use the methods of the [`Req`](crate::Req) trait to access whatever is +/// needed from the request. +/// +/// For example, here’s the implementation for [`Json`]. +/// +/// ```rust +/// impl<CustErr, T, Request> FromReq<CustErr, Request, Json> for T +/// where +/// // require the Request implement `Req` +/// Request: Req<CustErr> + Send + 'static, +/// // require that the type can be deserialized with `serde` +/// T: DeserializeOwned, +/// { +/// async fn from_req( +/// req: Request, +/// ) -> Result<Self, ServerFnError<CustErr>> { +/// // 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::<Self>(&string_data) +/// .map_err(|e| ServerFnError::Args(e.to_string())) +/// } +/// } +/// ``` pub trait FromReq<CustErr, Request, Encoding> where Self: Sized, { + /// Attempts to deserialize the request. fn from_req( req: Request, ) -> impl Future<Output = Result<Self, ServerFnError<CustErr>>> + Send; diff --git a/server_fn/src/lib.rs b/server_fn/src/lib.rs index 9314eba8b3..7d777badf9 100644 --- a/server_fn/src/lib.rs +++ b/server_fn/src/lib.rs @@ -1,10 +1,117 @@ +#![forbid(unsafe_code)] +// uncomment this if you want to feel pain +//#![deny(missing_docs)] + +//! # Server Functions +//! +//! This package is based on a simple idea: sometimes it’s useful to write functions +//! that will only run on the server, and call them from the client. +//! +//! If you’re creating anything beyond a toy app, you’ll need to do this all the time: +//! reading from or writing to a database that only runs on the server, running expensive +//! computations using libraries you don’t want to ship down to the client, accessing +//! APIs that need to be called from the server rather than the client for CORS reasons +//! or because you need a secret API key that’s stored on the server and definitely +//! shouldn’t be shipped down to a user’s browser. +//! +//! Traditionally, this is done by separating your server and client code, and by setting +//! up something like a REST API or GraphQL API to allow your client to fetch and mutate +//! data on the server. This is fine, but it requires you to write and maintain your code +//! in multiple separate places (client-side code for fetching, server-side functions to run), +//! as well as creating a third thing to manage, which is the API contract between the two. +//! +//! This package provides two simple primitives that allow you instead to write co-located, +//! isomorphic server functions. (*Co-located* means you can write them in your app code so +//! that they are “located alongside” the client code that calls them, rather than separating +//! the client and server sides. *Isomorphic* means you can call them from the client as if +//! you were simply calling a function; the function call has the “same shape” on the client +//! as it does on the server.) +//! +//! ### `#[server]` +//! +//! The [`#[server]`][server] macro allows you to annotate a function to +//! indicate that it should only run on the server (i.e., when you have an `ssr` feature in your +//! crate that is enabled). +//! +//! **Important**: Before calling a server function on a non-web platform, you must set the server URL by calling [`set_server_url`]. +//! +//! ```rust,ignore +//! #[server] +//! async fn read_posts(how_many: usize, query: String) -> Result<Vec<Posts>, ServerFnError> { +//! // do some server-only work here to access the database +//! let posts = ...; +//! Ok(posts) +//! } +//! +//! // call the function +//! # #[tokio::main] +//! # async fn main() { +//! async { +//! let posts = read_posts(3, "my search".to_string()).await; +//! log::debug!("posts = {posts:#?}"); +//! } +//! # } +//! ``` +//! +//! If you call this function from the client, it will serialize the function arguments and `POST` +//! them to the server as if they were the URL-encoded inputs in `<form method="post">`. +//! +//! Here’s what you need to remember: +//! - **Server functions must be `async`.** Even if the work being done inside the function body +//! can run synchronously on the server, from the client’s perspective it involves an asynchronous +//! function call. +//! - **Server functions must return `Result<T, ServerFnError>`.** Even if the work being done +//! inside the function body can’t fail, the processes of serialization/deserialization and the +//! network call are fallible. [`ServerFnError`] can receive generic errors. +//! - **Server functions are part of the public API of your application.** A server function is an +//! ad hoc HTTP API endpoint, not a magic formula. Any server function can be accessed by any HTTP +//! client. You should take care to sanitize any data being returned from the function to ensure it +//! does not leak data that should exist only on the server. +//! - **Server functions can’t be generic.** Because each server function creates a separate API endpoint, +//! it is difficult to monomorphize. As a result, server functions cannot be generic (for now?) If you need to use +//! a generic function, you can define a generic inner function called by multiple concrete server functions. +//! - **Arguments and return types must be serializable.** We support a variety of different encodings, +//! but one way or another arguments need to be serialized to be sent to the server and deserialized +//! on the server, and the return type must be serialized on the server and deserialized on the client. +//! This means that the set of valid server function argument and return types is a subset of all +//! possible Rust argument and return types. (i.e., server functions are strictly more limited than +//! ordinary functions.) +//! +//! ## Server Function Encodings +//! +//! Server functions are designed to allow a flexible combination of input and output encodings, the set +//! of which can be found in the [`codec`] module. +//! +//! The serialization/deserialization process for server functions consists of a series of steps, +//! each of which is represented by a different trait: +//! 1. [`IntoReq`]: The client serializes the [`ServerFn`] argument type into an HTTP request. +//! 2. The [`Client`] sends the request to the server. +//! 3. [`FromReq`]: The server deserializes the HTTP request back into the [`ServerFn`] type. +//! 4. The server calls calls [`ServerFn::run_body`] on the data. +//! 5. [`IntoRes`]: The server serializes the [`ServerFn::Output`] type into an HTTP response. +//! 6. The server integration applies any middleware from [`ServerFn::middlewares`] and responds to the request. +//! 7. [`FromRes`]: The client deserializes the response back into the [`ServerFn::Output`] type. +//! +//! [server]: <https://docs.rs/server_fn/latest/server_fn/attr.server.html> +//! [`serde_qs`]: <https://docs.rs/serde_qs/latest/serde_qs/> +//! [`cbor`]: <https://docs.rs/cbor/latest/cbor/> + +/// Implementations of the client side of the server function call. pub mod client; + +/// Encodings for arguments and results. pub mod codec; + #[macro_use] +/// Error types and utilities. pub mod error; +/// Types to add server middleware to a server function. pub mod middleware; +/// Utilities to allow client-side redirects. pub mod redirect; +/// Types and traits for for HTTP requests. pub mod request; +/// Types and traits for HTTP responses. pub mod response; #[cfg(feature = "actix")] @@ -35,6 +142,35 @@ use std::{fmt::Display, future::Future, pin::Pin, str::FromStr, sync::Arc}; #[doc(hidden)] pub use xxhash_rust; +/// Defines a function that runs only on the server, but can be called from the server or the client. +/// +/// The type for which `ServerFn` is implemented is actually the type of the arguments to the function, +/// while the function body itself is implemented in [`run_body`]. +/// +/// This means that `Self` here is usually a struct, in which each field is an argument to the function. +/// In other words, +/// ```rust,ignore +/// #[server] +/// pub async fn my_function(foo: String, bar: usize) -> Result<usize, ServerFnError> { +/// Ok(foo.len() + bar) +/// } +/// ``` +/// should expand to +/// ```rust,ignore +/// #[derive(Serialize, Deserialize)] +/// pub struct MyFunction { +/// foo: String, +/// bar: usize +/// } +/// +/// impl ServerFn for MyFunction { +/// async fn run_body() -> Result<usize, ServerFnError> { +/// Ok(foo.len() + bar) +/// } +/// +/// // etc. +/// } +/// ``` pub trait ServerFn where Self: Send @@ -45,6 +181,7 @@ where Self::InputEncoding, >, { + /// A unique path for the server function’s API endpoint, relative to the host, including its prefix. const PATH: &'static str; /// The type of the HTTP client that will send the request from the client side. @@ -79,17 +216,23 @@ where /// custom error type, this can be `NoCustomError` by default.) type Error: FromStr + Display; + /// Returns [`Self::PATH`]. + fn url() -> &'static str { + Self::PATH + } + /// Middleware that should be applied to this server function. fn middlewares( ) -> Vec<Arc<dyn Layer<Self::ServerRequest, Self::ServerResponse>>> { Vec::new() } - // The body of the server function. This will only run on the server. + /// The body of the server function. This will only run on the server. fn run_body( self, ) -> impl Future<Output = Result<Self::Output, ServerFnError<Self::Error>>> + Send; + #[doc(hidden)] fn run_on_server( req: Self::ServerRequest, ) -> impl Future<Output = Self::ServerResponse> + Send { @@ -100,6 +243,7 @@ where } } + #[doc(hidden)] fn run_on_client( self, ) -> impl Future<Output = Result<Self::Output, ServerFnError<Self::Error>>> + Send @@ -113,6 +257,7 @@ where } } + #[doc(hidden)] fn run_on_client_with_req( req: <Self::Client as Client<Self::Error>>::Request, redirect_hook: Option<&RedirectHook>, @@ -157,16 +302,13 @@ where Ok(res) } } - - fn url() -> &'static str { - Self::PATH - } } #[cfg(feature = "ssr")] #[doc(hidden)] pub use inventory; +/// Uses the `inventory` crate to initialize a map between paths and server functions. #[macro_export] macro_rules! initialize_server_fn_map { ($req:ty, $res:ty) => { @@ -179,8 +321,12 @@ macro_rules! initialize_server_fn_map { }; } +/// A list of middlewares that can be applied to a server function. pub type MiddlewareSet<Req, Res> = Vec<Arc<dyn Layer<Req, Res>>>; +/// A trait object that allows multiple server functions that take the same +/// request type and return the same response type to be gathered into a single +/// collection. pub struct ServerFnTraitObj<Req, Res> { path: &'static str, method: Method, @@ -189,6 +335,7 @@ pub struct ServerFnTraitObj<Req, Res> { } impl<Req, Res> ServerFnTraitObj<Req, Res> { + /// Converts the relevant parts of a server function into a trait object. pub const fn new( path: &'static str, method: Method, @@ -203,10 +350,12 @@ impl<Req, Res> ServerFnTraitObj<Req, Res> { } } + /// The path of the server function. pub fn path(&self) -> &'static str { self.path } + /// The HTTP method the server function expects. pub fn method(&self) -> Method { self.method.clone() } @@ -238,7 +387,7 @@ impl<Req, Res> Clone for ServerFnTraitObj<Req, Res> { type LazyServerFnMap<Req, Res> = Lazy<DashMap<&'static str, ServerFnTraitObj<Req, Res>>>; -// Axum integration +/// Axum integration. #[cfg(feature = "axum")] pub mod axum { use crate::{ @@ -255,6 +404,9 @@ pub mod axum { Response<Body>, > = initialize_server_fn_map!(Request<Body>, Response<Body>); + /// Explicitly register a server function. This is only necessary if you are + /// running the server in a WASM environment (or a rare environment that the + /// `inventory`). pub fn register_explicit<T>() where T: ServerFn< @@ -273,12 +425,14 @@ pub mod axum { ); } + /// The set of all registered server function paths. pub fn server_fn_paths() -> impl Iterator<Item = (&'static str, Method)> { REGISTERED_SERVER_FUNCTIONS .iter() .map(|item| (item.path(), item.method())) } + /// An Axum handler that responds to a server function request. pub async fn handle_server_fn(req: Request<Body>) -> Response<Body> { let path = req.uri().path(); @@ -301,6 +455,7 @@ pub mod axum { } } + /// Returns the server function at the given path as a service that can be modified. pub fn get_server_fn_service( path: &str, ) -> Option<BoxedService<Request<Body>, Response<Body>>> { @@ -315,7 +470,7 @@ pub mod axum { } } -// Actix integration +/// Actix integration. #[cfg(feature = "actix")] pub mod actix { use crate::{ @@ -335,6 +490,9 @@ pub mod actix { ActixResponse, > = initialize_server_fn_map!(ActixRequest, ActixResponse); + /// Explicitly register a server function. This is only necessary if you are + /// running the server in a WASM environment (or a rare environment that the + /// `inventory`). pub fn register_explicit<T>() where T: ServerFn< @@ -353,12 +511,14 @@ pub mod actix { ); } + /// The set of all registered server function paths. pub fn server_fn_paths() -> impl Iterator<Item = (&'static str, Method)> { REGISTERED_SERVER_FUNCTIONS .iter() .map(|item| (item.path(), item.method())) } + /// An Actix handler that responds to a server function request. pub async fn handle_server_fn( req: HttpRequest, payload: Payload, @@ -391,6 +551,7 @@ pub mod actix { } } + /// Returns the server function at the given path as a service that can be modified. pub fn get_server_fn_service( path: &str, ) -> Option<BoxedService<ActixRequest, ActixResponse>> { diff --git a/server_fn/src/redirect.rs b/server_fn/src/redirect.rs index 4eed28bab8..aa279d3936 100644 --- a/server_fn/src/redirect.rs +++ b/server_fn/src/redirect.rs @@ -1,19 +1,31 @@ use std::sync::OnceLock; +/// A custom header that can be set with any value to indicate +/// that the server function client should redirect to a new route. +/// +/// This is useful because it allows returning a value from the request, +/// while also indicating that a redirect should follow. This cannot be +/// done with an HTTP `3xx` status code, because the browser will follow +/// that redirect rather than returning the desired data. pub const REDIRECT_HEADER: &str = "serverfnredirect"; +/// A function that will be called if a server function returns a `3xx` status +/// or the [`REDIRECT_HEADER`]. pub type RedirectHook = Box<dyn Fn(&str) + Send + Sync>; // allowed: not in a public API, and pretty straightforward #[allow(clippy::type_complexity)] pub(crate) static REDIRECT_HOOK: OnceLock<RedirectHook> = OnceLock::new(); +/// Sets a function that will be called if a server function returns a `3xx` status +/// or the [`REDIRECT_HEADER`]. Returns `Err(_)` if the hook has already been set. pub fn set_redirect_hook( hook: impl Fn(&str) + Send + Sync + 'static, ) -> Result<(), RedirectHook> { REDIRECT_HOOK.set(Box::new(hook)) } +/// Calls the hook that has been set by [`set_redirect_hook`] to redirect to `path`. pub fn call_redirect_hook(path: &str) { if let Some(hook) = REDIRECT_HOOK.get() { hook(path) From 15b04a8a85dda54060b7b614988f51836ddb2973 Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Thu, 11 Jan 2024 22:09:40 -0500 Subject: [PATCH 044/100] more docs --- server_fn/src/client.rs | 2 + server_fn/src/codec/mod.rs | 134 ++++++++++++++++++++++++------ server_fn/src/codec/multipart.rs | 24 ++++-- server_fn/src/codec/serde_lite.rs | 2 +- server_fn/src/codec/stream.rs | 14 ++++ server_fn/src/error.rs | 17 ++++ server_fn/src/lib.rs | 2 +- server_fn/src/middleware/mod.rs | 7 ++ server_fn/src/request/actix.rs | 6 ++ server_fn/src/request/browser.rs | 2 + server_fn/src/request/mod.rs | 18 +++- server_fn/src/request/reqwest.rs | 14 ++-- server_fn/src/response/actix.rs | 6 ++ server_fn/src/response/browser.rs | 1 + server_fn/src/response/mod.rs | 13 ++- 15 files changed, 214 insertions(+), 48 deletions(-) diff --git a/server_fn/src/client.rs b/server_fn/src/client.rs index b4bbdb95fc..fdfc85358f 100644 --- a/server_fn/src/client.rs +++ b/server_fn/src/client.rs @@ -54,6 +54,7 @@ pub mod browser { } #[cfg(any(feature = "reqwest", doc))] +/// Implements [`Client`] for a request made by [`reqwest`]. pub mod reqwest { use super::Client; use crate::{error::ServerFnError, request::reqwest::CLIENT}; @@ -61,6 +62,7 @@ pub mod reqwest { use reqwest::{Request, Response}; use std::future::Future; + /// Implements [`Client`] for a request made by [`reqwest`]. pub struct ReqwestClient; impl<CustErr> Client<CustErr> for ReqwestClient { diff --git a/server_fn/src/codec/mod.rs b/server_fn/src/codec/mod.rs index af3f06313e..32ada42e5c 100644 --- a/server_fn/src/codec/mod.rs +++ b/server_fn/src/codec/mod.rs @@ -45,15 +45,56 @@ mod multipart; pub use multipart::*; mod stream; -use crate::{error::ServerFnError, request::ClientReq}; +use crate::error::ServerFnError; use futures::Future; use http::Method; pub use stream::*; -/// Deserializes an HTTP request into the data type. +/// Serializes a data type into an HTTP request, on the client. +/// +/// Implementations use the methods of the [`ClientReq`](crate::ClientReq) trait to +/// convert data into a request body. They are often quite short, usually consisting +/// of just two steps: +/// 1. Serializing the data into some [`String`], [`Bytes`](bytes::Bytes), or [`Stream`](futures::Stream). +/// 2. Creating a request with a body of that type. +/// +/// For example, here’s the implementation for [`Json`]. +/// +/// ```rust +/// impl<CustErr, T, Request> IntoReq<CustErr, Request, Json> for T +/// where +/// Request: ClientReq<CustErr>, +/// T: Serialize + Send, +/// { +/// fn into_req( +/// self, +/// path: &str, +/// accepts: &str, +/// ) -> Result<Request, ServerFnError<CustErr>> { +/// // try to serialize the data +/// let data = serde_json::to_string(&self) +/// .map_err(|e| ServerFnError::Serialization(e.to_string()))?; +/// // and use it as the body of a POST request +/// Request::try_new_post(path, accepts, Json::CONTENT_TYPE, data) +/// } +/// } +/// ``` +pub trait IntoReq<CustErr, Request, Encoding> { + /// Attempts to serialize the arguments into an HTTP request. + fn into_req( + self, + path: &str, + accepts: &str, + ) -> Result<Request, ServerFnError<CustErr>>; +} + +/// Deserializes an HTTP request into the data type, on the server. /// /// Implementations use the methods of the [`Req`](crate::Req) trait to access whatever is -/// needed from the request. +/// needed from the request. They are often quite short, usually consisting +/// of just two steps: +/// 1. Extracting the request body into some [`String`], [`Bytes`](bytes::Bytes), or [`Stream`](futures::Stream). +/// 2. Deserializing that data into the data type. /// /// For example, here’s the implementation for [`Json`]. /// @@ -80,46 +121,89 @@ pub trait FromReq<CustErr, Request, Encoding> where Self: Sized, { - /// Attempts to deserialize the request. + /// Attempts to deserialize the arguments from a request. fn from_req( req: Request, ) -> impl Future<Output = Result<Self, ServerFnError<CustErr>>> + Send; } -pub trait IntoReq<CustErr, Request, Encoding> { - fn into_req( +/// Serializes the data type into an HTTP response. +/// +/// Implementations use the methods of the [`Res`](crate::Res) trait to create a +/// response. They are often quite short, usually consisting +/// of just two steps: +/// 1. Serializing the data type to a [`String`], [`Bytes`](bytes::Bytes), or a [`Stream`](futures::Stream). +/// 2. Creating a response with that serialized value as its body. +/// +/// For example, here’s the implementation for [`Json`]. +/// +/// ```rust +/// impl<CustErr, T, Response> IntoRes<CustErr, Response, Json> for T +/// where +/// Response: Res<CustErr>, +/// T: Serialize + Send, +/// { +/// async fn into_res(self) -> Result<Response, ServerFnError<CustErr>> { +/// // try to serialize the data +/// let data = serde_json::to_string(&self) +/// .map_err(|e| ServerFnError::Serialization(e.to_string()))?; +/// // and use it as the body of a response +/// Response::try_from_string(Json::CONTENT_TYPE, data) +/// } +/// } +/// ``` +pub trait IntoRes<CustErr, Response, Encoding> { + /// Attempts to serialize the output into an HTTP response. + fn into_res( self, - path: &str, - accepts: &str, - ) -> Result<Request, ServerFnError<CustErr>>; + ) -> impl Future<Output = Result<Response, ServerFnError<CustErr>>> + Send; } +/// Deserializes the data type from an HTTP response. +/// +/// Implementations use the methods of the [`ClientRes`](crate::ClientRes) trait to extract +/// data from a response. They are often quite short, usually consisting +/// of just two steps: +/// 1. Extracting a [`String`], [`Bytes`](bytes::Bytes), or a [`Stream`](futures::Stream) +/// from the response body. +/// 2. Deserializing the data type from that value. +/// +/// For example, here’s the implementation for [`Json`]. +/// +/// ```rust +/// impl<CustErr, T, Response> FromRes<CustErr, Response, Json> for T +/// where +/// Response: ClientRes<CustErr> + Send, +/// T: DeserializeOwned + Send, +/// { +/// async fn from_res( +/// res: Response, +/// ) -> Result<Self, ServerFnError<CustErr>> { +/// // 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())) +/// } +/// } +/// ``` pub trait FromRes<CustErr, Response, Encoding> where Self: Sized, { + /// Attempts to deserialize the outputs from a response. fn from_res( res: Response, ) -> impl Future<Output = Result<Self, ServerFnError<CustErr>>> + Send; } -pub trait IntoRes<CustErr, Response, Encoding> { - fn into_res( - self, - ) -> impl Future<Output = Result<Response, ServerFnError<CustErr>>> + Send; -} - +/// Defines a particular encoding format, which can be used for serializing or deserializing data. pub trait Encoding { + /// The MIME type of the data. const CONTENT_TYPE: &'static str; - const METHOD: Method; -} -pub trait FormDataEncoding<Client, CustErr, Request> -where - Self: Sized, - Client: ClientReq<CustErr>, -{ - fn form_data_into_req( - form_data: Client::FormData, - ) -> Result<Self, ServerFnError<CustErr>>; + /// The HTTP method used for requests. + /// + /// This should be `POST` in most cases. + const METHOD: Method; } diff --git a/server_fn/src/codec/multipart.rs b/server_fn/src/codec/multipart.rs index a3c254e6ed..8dee495567 100644 --- a/server_fn/src/codec/multipart.rs +++ b/server_fn/src/codec/multipart.rs @@ -9,6 +9,9 @@ use http::Method; use multer::Multipart; use web_sys::FormData; +/// Encodes multipart form data. +/// +/// You should primarily use this if you are trying to handle file uploads. pub struct MultipartFormData; impl Encoding for MultipartFormData { @@ -16,24 +19,33 @@ impl Encoding for MultipartFormData { const METHOD: Method = Method::POST; } +/// Describes whether the multipart data is on the client side or the server side. #[derive(Debug)] pub enum MultipartData { + /// `FormData` from the browser. Client(BrowserFormData), + /// Generic multipart form using [`multer`]. This implements [`Stream`](futures::Stream). Server(multer::Multipart<'static>), } impl MultipartData { - pub fn into_client_data(self) -> Option<BrowserFormData> { + /// Extracts the inner data to handle as a stream. + /// + /// On the server side, this always returns `Some(_)`. On the client side, always returns `None`. + pub fn into_inner(self) -> Option<Multipart<'static>> { match self { - MultipartData::Client(data) => Some(data), - MultipartData::Server(_) => None, + MultipartData::Client(_) => None, + MultipartData::Server(data) => Some(data), } } - pub fn into_data(self) -> Option<Multipart<'static>> { + /// Extracts the inner form data on the client side. + /// + /// On the server side, this always returns `None`. On the client side, always returns `Some(_)`. + pub fn into_client_data(self) -> Option<BrowserFormData> { match self { - MultipartData::Client(_) => None, - MultipartData::Server(data) => Some(data), + MultipartData::Client(data) => Some(data), + MultipartData::Server(_) => None, } } } diff --git a/server_fn/src/codec/serde_lite.rs b/server_fn/src/codec/serde_lite.rs index c6054c9c25..415dc32bd0 100644 --- a/server_fn/src/codec/serde_lite.rs +++ b/server_fn/src/codec/serde_lite.rs @@ -74,7 +74,7 @@ where async fn from_res(res: Response) -> Result<Self, ServerFnError<CustErr>> { let data = res.try_into_string().await?; Self::deserialize( - &&serde_json::from_str(&data) + &serde_json::from_str(&data) .map_err(|e| ServerFnError::Args(e.to_string()))?, ) .map_err(|e| ServerFnError::Deserialization(e.to_string())) diff --git a/server_fn/src/codec/stream.rs b/server_fn/src/codec/stream.rs index 020bfdb546..57646f0da5 100644 --- a/server_fn/src/codec/stream.rs +++ b/server_fn/src/codec/stream.rs @@ -9,6 +9,9 @@ use futures::{Stream, StreamExt}; use http::Method; use std::pin::Pin; +/// An encoding that represents a stream of bytes. +/// +/// A server function that uses this as its output encoding should return [`ByteStream`]. pub struct Streaming; impl Encoding for Streaming { @@ -36,11 +39,15 @@ where } } */ +/// A stream of bytes. +/// +/// A server function can return this type if its output encoding is [`Streaming`]. pub struct ByteStream<CustErr = NoCustomError>( Pin<Box<dyn Stream<Item = Result<Bytes, ServerFnError<CustErr>>> + Send>>, ); impl<CustErr> ByteStream<CustErr> { + /// Consumes the wrapper, returning a stream of bytes. pub fn into_inner( self, ) -> impl Stream<Item = Result<Bytes, ServerFnError<CustErr>>> + Send { @@ -79,6 +86,9 @@ where } } +/// An encoding that represents a stream of text. +/// +/// A server function that uses this as its output encoding should return [`TextStream`]. pub struct StreamingText; impl Encoding for StreamingText { @@ -86,11 +96,15 @@ impl Encoding for StreamingText { const METHOD: Method = Method::POST; } +/// A stream of bytes. +/// +/// A server function can return this type if its output encoding is [`StreamingText`]. pub struct TextStream<CustErr = NoCustomError>( Pin<Box<dyn Stream<Item = Result<String, ServerFnError<CustErr>>> + Send>>, ); impl<CustErr> TextStream<CustErr> { + /// Consumes the wrapper, returning a stream of text. pub fn into_inner( self, ) -> impl Stream<Item = Result<String, ServerFnError<CustErr>>> + Send { diff --git a/server_fn/src/error.rs b/server_fn/src/error.rs index df9eeb6f81..4771bd053d 100644 --- a/server_fn/src/error.rs +++ b/server_fn/src/error.rs @@ -75,6 +75,8 @@ impl FromStr for NoCustomError { } } +/// Wraps some error type, which may implement any of [`Error`], [`Clone`], or +/// [`Display`]. #[derive(Debug)] pub struct WrapError<T>(pub T); @@ -98,6 +100,7 @@ macro_rules! server_fn_error { /// This trait serves as the conversion method between a variety of types /// and [`ServerFnError`]. pub trait ViaError<E> { + /// Converts something into an error. fn to_server_error(&self) -> ServerFnError<E>; } @@ -160,6 +163,7 @@ impl<E> ViaError<E> for WrapError<E> { #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "type")] pub enum ServerFnError<E = NoCustomError> { + /// 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). Registration(String), @@ -180,6 +184,7 @@ pub enum ServerFnError<E = NoCustomError> { } impl ServerFnError<NoCustomError> { + /// Constructs a new [`ServerFnError::ServerError`] from some other type. pub fn new(msg: impl ToString) -> Self { Self::ServerError(msg.to_string()) } @@ -230,9 +235,20 @@ 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<String, std::fmt::Error>; + /// Deserializes the custom error type from a [`String`]. fn de(data: &str) -> Self; } @@ -327,6 +343,7 @@ where /// it is easy to convert between the two types. #[derive(Error, Debug, Clone)] pub enum ServerFnErrorErr<E = NoCustomError> { + /// A user-defined custom error type, which defaults to [`NoCustomError`]. #[error("internal error: {0}")] WrappedServerError(E), /// Error while trying to register the server function (only occurs in case of poisoned RwLock). diff --git a/server_fn/src/lib.rs b/server_fn/src/lib.rs index 7d777badf9..23221ae972 100644 --- a/server_fn/src/lib.rs +++ b/server_fn/src/lib.rs @@ -1,6 +1,6 @@ #![forbid(unsafe_code)] // uncomment this if you want to feel pain -//#![deny(missing_docs)] +#![deny(missing_docs)] //! # Server Functions //! diff --git a/server_fn/src/middleware/mod.rs b/server_fn/src/middleware/mod.rs index cbc5b0c55b..4d093b53e5 100644 --- a/server_fn/src/middleware/mod.rs +++ b/server_fn/src/middleware/mod.rs @@ -1,18 +1,25 @@ use std::{future::Future, pin::Pin}; +/// An abstraction over a middleware layer, which can be used to add additional +/// middleware layer to a [`Service`]. pub trait Layer<Req, Res>: Send + Sync + 'static { + /// Adds this layer to the inner service. fn layer(&self, inner: BoxedService<Req, Res>) -> BoxedService<Req, Res>; } +/// A type-erased service, which takes an HTTP request and returns a response. pub struct BoxedService<Req, Res>(pub Box<dyn Service<Req, Res> + Send>); impl<Req, Res> BoxedService<Req, Res> { + /// Constructs a type-erased service from this service. pub fn new(service: impl Service<Req, Res> + Send + 'static) -> Self { Self(Box::new(service)) } } +/// A service converts an HTTP request into a response. pub trait Service<Request, Response> { + /// Converts a request into a response. fn run( &mut self, req: Request, diff --git a/server_fn/src/request/actix.rs b/server_fn/src/request/actix.rs index b5de1d6408..b0a727aac3 100644 --- a/server_fn/src/request/actix.rs +++ b/server_fn/src/request/actix.rs @@ -5,9 +5,15 @@ use futures::Stream; use send_wrapper::SendWrapper; use std::future::Future; +/// A wrapped Actix request. +/// +/// This uses a [`SendWrapper`] that allows the Actix `HttpRequest` type to be `Send`, but panics +/// if it it is ever sent to another thread. Actix pins request handling to a single thread, so this +/// is necessary to be compatible with traits that require `Send` but should never panic in actual use. pub struct ActixRequest(pub(crate) SendWrapper<(HttpRequest, Payload)>); impl ActixRequest { + /// Returns the raw Actix request, and its body. pub fn take(self) -> (HttpRequest, Payload) { self.0.take() } diff --git a/server_fn/src/request/browser.rs b/server_fn/src/request/browser.rs index 1bff976688..c1ce0f89f9 100644 --- a/server_fn/src/request/browser.rs +++ b/server_fn/src/request/browser.rs @@ -6,6 +6,7 @@ use js_sys::Uint8Array; use send_wrapper::SendWrapper; use web_sys::{FormData, UrlSearchParams}; +/// A `fetch` request made in the browser. #[derive(Debug)] pub struct BrowserRequest(pub(crate) SendWrapper<Request>); @@ -15,6 +16,7 @@ impl From<Request> for BrowserRequest { } } +/// The `FormData` type available in the browser. #[derive(Debug)] pub struct BrowserFormData(pub(crate) SendWrapper<FormData>); diff --git a/server_fn/src/request/mod.rs b/server_fn/src/request/mod.rs index 2b55d9ef22..871a156585 100644 --- a/server_fn/src/request/mod.rs +++ b/server_fn/src/request/mod.rs @@ -3,13 +3,17 @@ use bytes::Bytes; use futures::Stream; use std::future::Future; -#[cfg(feature = "actix")] +/// Request types for Actix. +#[cfg(any(feature = "actix", doc))] pub mod actix; -#[cfg(feature = "axum")] +/// Request types for Axum. +#[cfg(any(feature = "axum", doc))] pub mod axum; -#[cfg(feature = "browser")] +/// Request types for the browser. +#[cfg(any(feature = "browser", doc))] pub mod browser; -#[cfg(feature = "reqwest")] +/// Request types for [`reqwest`]. +#[cfg(any(feature = "reqwest", doc))] pub mod reqwest; /// Represents a request as made by the client. @@ -17,8 +21,10 @@ pub trait ClientReq<CustErr> where Self: Sized, { + /// The type used for URL-encoded form data in this client. type FormData; + /// Attempts to construct a new `GET` request. fn try_new_get( path: &str, content_type: &str, @@ -26,6 +32,7 @@ where query: &str, ) -> Result<Self, ServerFnError<CustErr>>; + /// Attempts to construct a new `POST` request with a text body. fn try_new_post( path: &str, content_type: &str, @@ -33,6 +40,7 @@ where body: String, ) -> Result<Self, ServerFnError<CustErr>>; + /// Attempts to construct a new `POST` request with a binary body. fn try_new_post_bytes( path: &str, content_type: &str, @@ -40,6 +48,7 @@ where body: Bytes, ) -> Result<Self, ServerFnError<CustErr>>; + /// Attempts to construct a new `POST` request with form data as the body. fn try_new_post_form_data( path: &str, accepts: &str, @@ -47,6 +56,7 @@ where body: Self::FormData, ) -> Result<Self, ServerFnError<CustErr>>; + /// Attempts to construct a new `POST` request with a multipart body. fn try_new_multipart( path: &str, accepts: &str, diff --git a/server_fn/src/request/reqwest.rs b/server_fn/src/request/reqwest.rs index ced0c05d4c..5e63956554 100644 --- a/server_fn/src/request/reqwest.rs +++ b/server_fn/src/request/reqwest.rs @@ -95,12 +95,12 @@ impl<CustErr> ClientReq<CustErr> for Request { content_type: &str, body: Self::FormData, ) -> Result<Self, ServerFnError<CustErr>> { - /*CLIENT - .post(path) - .header(ACCEPT, accepts) - .multipart(body) - .build() - .map_err(|e| ServerFnError::Request(e.to_string()))*/ - todo!() + CLIENT + .post(path) + .header(CONTENT_TYPE, content_type) + .header(ACCEPT, accepts) + .multipart(body) + .build() + .map_err(|e| ServerFnError::Request(e.to_string())) } } diff --git a/server_fn/src/response/actix.rs b/server_fn/src/response/actix.rs index 2f7f5a234c..a0c6efa6bf 100644 --- a/server_fn/src/response/actix.rs +++ b/server_fn/src/response/actix.rs @@ -9,9 +9,15 @@ use futures::{Stream, StreamExt}; use send_wrapper::SendWrapper; use std::fmt::{Debug, Display}; +/// A wrapped Actix response. +/// +/// This uses a [`SendWrapper`] that allows the Actix `HttpResponse` type to be `Send`, but panics +/// if it it is ever sent to another thread. Actix pins request handling to a single thread, so this +/// is necessary to be compatible with traits that require `Send` but should never panic in actual use. pub struct ActixResponse(pub(crate) SendWrapper<HttpResponse>); impl ActixResponse { + /// Returns the raw Actix response. pub fn take(self) -> HttpResponse { self.0.take() } diff --git a/server_fn/src/response/browser.rs b/server_fn/src/response/browser.rs index 4c8701ba8c..dfbf41af56 100644 --- a/server_fn/src/response/browser.rs +++ b/server_fn/src/response/browser.rs @@ -9,6 +9,7 @@ use std::future::Future; use wasm_bindgen::JsCast; use wasm_streams::ReadableStream; +/// The response to a `fetch` request made in the browser. pub struct BrowserResponse(pub(crate) SendWrapper<Response>); impl<CustErr> ClientRes<CustErr> for BrowserResponse { diff --git a/server_fn/src/response/mod.rs b/server_fn/src/response/mod.rs index 7fc101de65..c6bf5a4a3a 100644 --- a/server_fn/src/response/mod.rs +++ b/server_fn/src/response/mod.rs @@ -1,10 +1,14 @@ -#[cfg(feature = "actix")] +/// Response types for Actix. +#[cfg(any(feature = "actix", doc))] pub mod actix; -#[cfg(feature = "browser")] +/// Response types for the browser. +#[cfg(any(feature = "browser", doc))] pub mod browser; -#[cfg(feature = "axum")] +/// Response types for Axum. +#[cfg(any(feature = "axum", doc))] pub mod http; -#[cfg(feature = "reqwest")] +/// Response types for [`reqwest`]. +#[cfg(any(feature = "reqwest", doc))] pub mod reqwest; use crate::error::ServerFnError; @@ -37,6 +41,7 @@ where + 'static, ) -> Result<Self, ServerFnError<CustErr>>; + /// Converts an error into a response, with a `500` status code and the error text as its body. fn error_response(err: ServerFnError<CustErr>) -> Self; } From def4be80b27d79fa4f4ee9867746e8c5b2b7867c Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Fri, 12 Jan 2024 12:40:08 -0500 Subject: [PATCH 045/100] docs --- server_fn/server_fn_macro_default/src/lib.rs | 60 ++++++++++++++++++++ server_fn_macro/src/lib.rs | 9 +-- 2 files changed, 61 insertions(+), 8 deletions(-) diff --git a/server_fn/server_fn_macro_default/src/lib.rs b/server_fn/server_fn_macro_default/src/lib.rs index e5230df260..9d18372839 100644 --- a/server_fn/server_fn_macro_default/src/lib.rs +++ b/server_fn/server_fn_macro_default/src/lib.rs @@ -1,9 +1,69 @@ #![forbid(unsafe_code)] +#![deny(missing_docs)] + +//! This crate contains the default implementation of the #[macro@crate::server] macro without additional context from the server. +//! See the [server_fn_macro] crate for more information. use proc_macro::TokenStream; use server_fn_macro::server_macro_impl; use syn::__private::ToTokens; +/// Declares that a function is a [server function](https://docs.rs/server_fn/). +/// This means that its body will only run on the server, i.e., when the `ssr` +/// feature is enabled on this crate. +/// +/// ## Usage +/// ```rust,ignore +/// #[server] +/// pub async fn blog_posts( +/// category: String, +/// ) -> Result<Vec<BlogPost>, ServerFnError> { +/// let posts = load_posts(&category).await?; +/// // maybe do some other work +/// Ok(posts) +/// } +/// ``` +/// +/// ## Named Arguments +/// +/// You can any combination of the following named arguments: +/// - `name`: sets the identifier for the server function’s type, which is a struct created +/// to hold the arguments (defaults to the function identifier in PascalCase) +/// - `prefix`: a prefix at which the server function handler will be mounted (defaults to `/api`) +/// - `endpoint`: specifies the exact path at which the server function handler will be mounted, +/// relative to the prefix (defaults to the function name followed by unique hash) +/// - `input`: the encoding for the arguments (defaults to `PostUrl`) +/// - `output`: the encoding for the response (defaults to `Json`) +/// - `encoding`: (legacy, may be deprecated in future) specifies the encoding, which may be one +/// of the following (not case sensitive) +/// - `"Url"`: `POST` request with URL-encoded arguments and JSON response +/// - `"GetUrl"`: `GET` request with URL-encoded arguments and JSON response +/// - `"Cbor"`: `POST` request with CBOR-encoded arguments and response +/// - `"GetCbor"`: `GET` request with URL-encoded arguments and CBOR response +/// ```rust,ignore +/// #[server( +/// name = SomeStructName, +/// prefix = "/my_api", +/// endpoint = "my_fn", +/// input = Cbor, +/// output = Json +/// )] +/// pub async fn my_wacky_server_fn(input: Vec<String>) -> Result<usize, ServerFnError> { +/// todo!() +/// } +/// +/// // expands to +/// #[derive(Deserialize, Serialize)] +/// struct SomeStructName { +/// input: Vec<String> +/// } +/// +/// impl ServerFn for SomeStructName { +/// const PATH: &'static str = "/my_api/my_fn"; +/// +/// // etc. +/// } +/// ``` #[proc_macro_attribute] pub fn server(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream { match server_macro_impl( diff --git a/server_fn_macro/src/lib.rs b/server_fn_macro/src/lib.rs index b804bcb681..9ed4ffe3a0 100644 --- a/server_fn_macro/src/lib.rs +++ b/server_fn_macro/src/lib.rs @@ -16,14 +16,7 @@ use syn::{ Type, *, }; -/// The implementation of the `server_fn` macro. -/// To allow the macro to accept a custom context from the server, pass a custom server context to this function. -/// **The Context comes from the server.** Optionally, the first argument of a server function -/// can be a custom context. This context can be used to inject dependencies like the HTTP request -/// or response or other server-only dependencies, but it does *not* have access to state that exists in the client. -/// -/// The paths passed into this function are used in the generated code, so they must be in scope when the macro is called. -/// +/// The implementation of the `server` macro. /// ```ignore /// #[proc_macro_attribute] /// pub fn server(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream { From b0cdeab906803c2fd3b82221b1e73c15a7110017 Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Fri, 12 Jan 2024 12:40:19 -0500 Subject: [PATCH 046/100] remove old code --- leptos_server/src/lib.rs | 248 --------------------------------------- 1 file changed, 248 deletions(-) diff --git a/leptos_server/src/lib.rs b/leptos_server/src/lib.rs index 6055e13936..eece8a32b9 100644 --- a/leptos_server/src/lib.rs +++ b/leptos_server/src/lib.rs @@ -120,251 +120,3 @@ mod multi_action; pub use action::*; pub use multi_action::*; extern crate tracing; - -//#[cfg(any(feature = "ssr", doc))] -//use std::{ -// collections::HashMap, -// sync::{Arc, RwLock}, -//}; -// -//#[cfg(any(feature = "ssr", doc))] -///// A concrete type for a server function. -//#[derive(Clone)] -//pub struct ServerFnTraitObj(pub server_fn::ServerFnTraitObj<()>); -// -//#[cfg(any(feature = "ssr", doc))] -//impl std::ops::Deref for ServerFnTraitObj { -// type Target = server_fn::ServerFnTraitObj<()>; -// -// fn deref(&self) -> &Self::Target { -// &self.0 -// } -//} -// -//#[cfg(any(feature = "ssr", doc))] -//impl std::ops::DerefMut for ServerFnTraitObj { -// fn deref_mut(&mut self) -> &mut Self::Target { -// &mut self.0 -// } -//} -// -//#[cfg(any(feature = "ssr", doc))] -//impl ServerFnTraitObj { -// /// Create a new `ServerFnTraitObj` from a `server_fn::ServerFnTraitObj`. -// pub const fn from_generic_server_fn( -// server_fn: server_fn::ServerFnTraitObj<()>, -// ) -> Self { -// Self(server_fn) -// } -//} -// -//#[cfg(feature = "ssr")] -//inventory::collect!(ServerFnTraitObj); -// -//#[allow(unused)] -//type ServerFunction = server_fn::ServerFnTraitObj<()>; -// -//#[cfg(any(feature = "ssr", doc))] -//lazy_static::lazy_static! { -// static ref REGISTERED_SERVER_FUNCTIONS: Arc<RwLock<HashMap<&'static str, ServerFnTraitObj>>> = { -// let mut map = HashMap::new(); -// for server_fn in inventory::iter::<ServerFnTraitObj> { -// map.insert(server_fn.0.url(), server_fn.clone()); -// } -// Arc::new(RwLock::new(map)) -// }; -//} -// -//#[cfg(any(feature = "ssr", doc))] -///// The registry of all Leptos server functions. -//pub struct LeptosServerFnRegistry; -// -//#[cfg(any(feature = "ssr", doc))] -//impl server_fn::ServerFunctionRegistry<()> for LeptosServerFnRegistry { -// type Error = ServerRegistrationFnError; -// -// /// Server functions are automatically registered on most platforms, (including Linux, macOS, -// /// iOS, FreeBSD, Android, and Windows). If you are on another platform, like a WASM server runtime, -// /// you should register server functions by calling this `T::register_explicit()`. -// fn register_explicit( -// prefix: &'static str, -// url: &'static str, -// server_function: server_fn::SerializedFnTraitObj<()>, -// encoding: Encoding, -// ) -> Result<(), Self::Error> { -// // store it in the hashmap -// let mut func_write = REGISTERED_SERVER_FUNCTIONS -// .write() -// .map_err(|e| ServerRegistrationFnError::Poisoned(e.to_string()))?; -// let prev = func_write.insert( -// url, -// ServerFnTraitObj(server_fn::ServerFnTraitObj::new( -// prefix, -// url, -// encoding, -// server_function, -// )), -// ); -// -// // if there was already a server function with this key, -// // return Err -// match prev { -// Some(_) => { -// Err(ServerRegistrationFnError::AlreadyRegistered(format!( -// "There was already a server function registered at {:?}. \ -// This can happen if you use the same server function name \ -// in two different modules -// on `stable` or in `release` mode.", -// url -// ))) -// } -// None => Ok(()), -// } -// } -// -// /// Returns the server function registered at the given URL, or `None` if no function is registered at that URL. -// fn get(url: &str) -> Option<server_fn::ServerFnTraitObj<()>> { -// REGISTERED_SERVER_FUNCTIONS -// .read() -// .ok() -// .and_then(|fns| fns.get(url).map(|sf| sf.0.clone())) -// } -// -// /// Returns the server function trait obj registered at the given URL, or `None` if no function is registered at that URL. -// fn get_trait_obj(url: &str) -> Option<server_fn::ServerFnTraitObj<()>> { -// REGISTERED_SERVER_FUNCTIONS -// .read() -// .ok() -// .and_then(|fns| fns.get(url).map(|sf| sf.0.clone())) -// } -// /// Return the -// fn get_encoding(url: &str) -> Option<Encoding> { -// REGISTERED_SERVER_FUNCTIONS -// .read() -// .ok() -// .and_then(|fns| fns.get(url).map(|sf| sf.encoding())) -// } -// -// /// Returns a list of all registered server functions. -// fn paths_registered() -> Vec<&'static str> { -// REGISTERED_SERVER_FUNCTIONS -// .read() -// .ok() -// .map(|fns| fns.keys().cloned().collect()) -// .unwrap_or_default() -// } -//} -// -//#[cfg(any(feature = "ssr", doc))] -///// Errors that can occur when registering a server function. -//#[derive( -// thiserror::Error, Debug, Clone, serde::Serialize, serde::Deserialize, -//)] -//pub enum ServerRegistrationFnError { -// /// The server function is already registered. -// #[error("The server function {0} is already registered")] -// AlreadyRegistered(String), -// /// The server function registry is poisoned. -// #[error("The server function registry is poisoned: {0}")] -// Poisoned(String), -//} -// -///// Get a ServerFunction struct containing info about the server fn -//#[cfg(any(feature = "ssr", doc))] -//pub fn server_fn_by_path(path: &str) -> Option<ServerFnTraitObj> { -// REGISTERED_SERVER_FUNCTIONS -// .read() -// .expect("Server function registry is poisoned") -// .get(path) -// .cloned() -//} -// -///// Attempts to find a server function registered at the given path. -///// -///// This can be used by a server to handle the requests, as in the following example (using `actix-web`) -///// -///// ```rust, ignore -///// #[post("{tail:.*}")] -///// async fn handle_server_fns( -///// req: HttpRequest, -///// params: web::Path<String>, -///// body: web::Bytes, -///// ) -> impl Responder { -///// let path = params.into_inner(); -///// let accept_header = req -///// .headers() -///// .get("Accept") -///// .and_then(|value| value.to_str().ok()); -///// if let Some(server_fn) = server_fn_by_path(path.as_str()) { -///// let query = req.query_string().as_bytes(); -///// let data = match &server_fn.encoding { -///// Encoding::Url | Encoding::Cbor => &body, -///// Encoding::GetJSON | Encoding::GetCBOR => query, -///// }; -///// match (server_fn.trait_obj)(data).await { -///// Ok(serialized) => { -///// // if this is Accept: application/json then send a serialized JSON response -///// if let Some("application/json") = accept_header { -///// HttpResponse::Ok().body(serialized) -///// } -///// // otherwise, it's probably a <form> submit or something: redirect back to the referrer -///// else { -///// HttpResponse::SeeOther() -///// .insert_header(("Location", "/")) -///// .content_type("application/json") -///// .body(serialized) -///// } -///// } -///// Err(e) => { -///// eprintln!("server function error: {e:#?}"); -///// HttpResponse::InternalServerError().body(e.to_string()) -///// } -///// } -///// } else { -///// HttpResponse::BadRequest().body(format!("Could not find a server function at that route.")) -///// } -///// } -///// ``` -//#[cfg(any(feature = "ssr", doc))] -//pub fn server_fn_trait_obj_by_path(path: &str) -> Option<ServerFnTraitObj> { -// server_fn::server_fn_trait_obj_by_path::<(), LeptosServerFnRegistry>(path) -// .map(ServerFnTraitObj::from_generic_server_fn) -//} -// -///// Get the Encoding of a server fn if one is registered at that path. Otherwise, return None -//#[cfg(any(feature = "ssr", doc))] -//pub fn server_fn_encoding_by_path(path: &str) -> Option<Encoding> { -// server_fn::server_fn_encoding_by_path::<(), LeptosServerFnRegistry>(path) -//} -// -///// Returns the set of currently-registered server function paths, for debugging purposes. -//#[cfg(any(feature = "ssr", doc))] -//pub fn server_fns_by_path() -> Vec<&'static str> { -// server_fn::server_fns_by_path::<(), LeptosServerFnRegistry>() -//} -// -///// Defines a "server function." A server function can be called from the server or the client, -///// but the body of its code will only be run on the server, i.e., if a crate feature `ssr` is enabled. -///// -///// (This follows the same convention as the Leptos framework's distinction between `ssr` for server-side rendering, -///// and `csr` and `hydrate` for client-side rendering and hydration, respectively.) -///// -///// Server functions are created using the `server` macro. -///// -///// The function should be registered by calling `ServerFn::register()`. The set of server functions -///// can be queried on the server for routing purposes by calling [server_fn_by_path]. -///// -///// Technically, the trait is implemented on a type that describes the server function's arguments. -//pub trait ServerFn: server_fn::ServerFn<()> { -// #[cfg(any(feature = "ssr", doc))] -// /// Explicitly registers the server function on platforms that require it, -// /// allowing the server to query it by URL. -// /// -// /// Explicit server function registration is no longer required on most platforms -// /// (including Linux, macOS, iOS, FreeBSD, Android, and Windows) -// fn register_explicit() -> Result<(), ServerFnError> { -// Self::register_in_explicit::<LeptosServerFnRegistry>() -// } -//} -// -//impl<T> ServerFn for T where T: server_fn::ServerFn<()> {} From 94881148010bd1c44c3e27e831a6bf419d90b219 Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Fri, 12 Jan 2024 12:50:32 -0500 Subject: [PATCH 047/100] docs --- leptos_macro/src/lib.rs | 146 +++++++++++++++++----------------------- 1 file changed, 60 insertions(+), 86 deletions(-) diff --git a/leptos_macro/src/lib.rs b/leptos_macro/src/lib.rs index 04c8a41d2f..1ce7dd5336 100644 --- a/leptos_macro/src/lib.rs +++ b/leptos_macro/src/lib.rs @@ -856,64 +856,82 @@ pub fn slot(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream { } /// Declares that a function is a [server function](https://docs.rs/server_fn/latest/server_fn/index.html). -/// This means that its body will only run on the server, i.e., when the `ssr` feature is enabled. +/// This means that its body will only run on the server, i.e., when the `ssr` feature on this crate is enabled. /// /// If you call a server function from the client (i.e., when the `csr` or `hydrate` features /// are enabled), it will instead make a network request to the server. /// -/// You can specify one, two, three, or four arguments to the server function. All of these arguments are optional. -/// 1. **`name`**: A type name that will be used to identify and register the server function -/// (e.g., `MyServerFn`). Defaults to a PascalCased version of the function name. -/// 2. **`prefix`**: A URL prefix at which the function will be mounted when it’s registered -/// (e.g., `"/api"`). Defaults to `"/api"`. -/// 3. **`encoding`**: The encoding for the server function (`"Url"`, `"Cbor"`, `"GetJson"`, or `"GetCbor`". See **Server Function Encodings** below.) -/// 4. **`endpoint`**: A specific endpoint path to be used in the URL. (By default, a unique path will be generated.) +/// ## Named Arguments +/// +/// You can provide any combination of the following named arguments: +/// - `name`: sets the identifier for the server function’s type, which is a struct created +/// to hold the arguments (defaults to the function identifier in PascalCase) +/// - `prefix`: a prefix at which the server function handler will be mounted (defaults to `/api`) +/// - `endpoint`: specifies the exact path at which the server function handler will be mounted, +/// relative to the prefix (defaults to the function name followed by unique hash) +/// - `input`: the encoding for the arguments (defaults to `PostUrl`) +/// - `output`: the encoding for the response (defaults to `Json`) +/// - `encoding`: (legacy, may be deprecated in future) specifies the encoding, which may be one +/// of the following (not case sensitive) +/// - `"Url"`: `POST` request with URL-encoded arguments and JSON response +/// - `"GetUrl"`: `GET` request with URL-encoded arguments and JSON response +/// - `"Cbor"`: `POST` request with CBOR-encoded arguments and response +/// - `"GetCbor"`: `GET` request with URL-encoded arguments and CBOR response /// /// ```rust,ignore -/// // will generate a server function at `/api-prefix/hello` -/// #[server(MyServerFnType, "/api-prefix", "Url", "hello")] -/// pub async fn my_server_fn_type() /* ... */ -/// -/// // will generate a server function with struct `HelloWorld` and path -/// // `/api/hello2349232342342` (hash based on location in source) -/// #[server] -/// pub async fn hello_world() /* ... */ -/// -/// // The server function accepts keyword parameters -/// #[server(endpoint = "my_endpoint")] -/// pub async fn hello_leptos() /* ... */ -/// ``` -/// -/// The server function itself can take any number of arguments, each of which should be serializable -/// and deserializable with `serde`. -/// -/// ```ignore -/// # use leptos::*; use serde::{Serialize, Deserialize}; -/// # #[derive(Serialize, Deserialize)] -/// # pub struct Post { } -/// #[server(ReadPosts, "/api")] -/// pub async fn read_posts(how_many: u8, query: String) -> Result<Vec<Post>, ServerFnError> { -/// // do some work on the server to access the database +/// #[server( +/// name = SomeStructName, +/// prefix = "/my_api", +/// endpoint = "my_fn", +/// input = Cbor, +/// output = Json +/// )] +/// pub async fn my_wacky_server_fn(input: Vec<String>) -> Result<usize, ServerFnError> { /// todo!() /// } /// ``` /// -/// Note the following: +/// ## Server Function Encodings +/// +/// Server functions are designed to allow a flexible combination of `input` and `output` encodings, the set +/// of which can be found in the [`server_fn::codec`] module. +/// +/// The serialization/deserialization process for server functions consists of a series of steps, +/// each of which is represented by a different trait: +/// 1. [`IntoReq`]: The client serializes the [`ServerFn`] argument type into an HTTP request. +/// 2. The [`Client`] sends the request to the server. +/// 3. [`FromReq`]: The server deserializes the HTTP request back into the [`ServerFn`] type. +/// 4. The server calls calls [`ServerFn::run_body`] on the data. +/// 5. [`IntoRes`]: The server serializes the [`ServerFn::Output`] type into an HTTP response. +/// 6. The server integration applies any middleware from [`ServerFn::middlewares`] and responds to the request. +/// 7. [`FromRes`]: The client deserializes the response back into the [`ServerFn::Output`] type. +/// +/// Whatever encoding is provided to `input` should implement `IntoReq` and `FromReq`. Whatever encoding is provided +/// to `output` should implement `IntoRes` and `FromRes`. +/// +/// ## Important Notes /// - **Server functions must be `async`.** Even if the work being done inside the function body /// can run synchronously on the server, from the client’s perspective it involves an asynchronous /// function call. /// - **Server functions must return `Result<T, ServerFnError>`.** Even if the work being done /// inside the function body can’t fail, the processes of serialization/deserialization and the /// network call are fallible. -/// - **Return types must implement [`Serialize`](https://docs.rs/serde/latest/serde/trait.Serialize.html).** -/// This should be fairly obvious: we have to serialize arguments to send them to the server, and we -/// need to deserialize the result to return it to the client. -/// - **Arguments must implement [`Serialize`](https://docs.rs/serde/latest/serde/trait.Serialize.html) -/// and [`DeserializeOwned`](https://docs.rs/serde/latest/serde/de/trait.DeserializeOwned.html).** -/// They are serialized as an `application/x-www-form-urlencoded` -/// form data using [`serde_qs`](https://docs.rs/serde_qs/latest/serde_qs/) or as `application/cbor` -/// using [`cbor`](https://docs.rs/cbor/latest/cbor/). **Note**: You should explicitly include `serde` with the -/// `derive` feature enabled in your `Cargo.toml`. You can do this by running `cargo add serde --features=derive`. +/// - [`ServerFnError`] can be generic over some custom error type. If so, that type should implement +/// [`FromStr`] and [`Display`], but does not need to implement [`Error`]. This is so the value +/// can be easily serialized and deserialized along with the result. +/// - **Server functions are part of the public API of your application.** A server function is an +/// ad hoc HTTP API endpoint, not a magic formula. Any server function can be accessed by any HTTP +/// client. You should take care to sanitize any data being returned from the function to ensure it +/// does not leak data that should exist only on the server. +/// - **Server functions can’t be generic.** Because each server function creates a separate API endpoint, +/// it is difficult to monomorphize. As a result, server functions cannot be generic (for now?) If you need to use +/// a generic function, you can define a generic inner function called by multiple concrete server functions. +/// - **Arguments and return types must be serializable.** We support a variety of different encodings, +/// but one way or another arguments need to be serialized to be sent to the server and deserialized +/// on the server, and the return type must be serialized on the server and deserialized on the client. +/// This means that the set of valid server function argument and return types is a subset of all +/// possible Rust argument and return types. (i.e., server functions are strictly more limited than +/// ordinary functions.) /// - **Context comes from the server.** Server functions are provided access to the HTTP request and other relevant /// server data via the server integrations, but they do *not* have access to reactive state that exists in the client. /// - Your server must be ready to handle the server functions at the API prefix you list. The easiest way to do this @@ -923,50 +941,6 @@ pub fn slot(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream { /// server function. If you choose to specify a path in the fourth argument, you must ensure that these /// are unique. You cannot define two server functions with the same URL prefix and endpoint path, /// even if they have different URL encodings, e.g. a POST method at `/api/foo` and a GET method at `/api/foo`. -/// -/// ## Server Function Encodings -/// -/// By default, the server function call is a `POST` request that serializes the arguments as URL-encoded form data in the body -/// of the request. But there are a few other methods supported. Optionally, we can provide another argument to the `#[server]` -/// macro to specify an alternate encoding: -/// -/// ```rust,ignore -/// #[server(AddTodo, "/api", "Url")] -/// #[server(AddTodo, "/api", "GetJson")] -/// #[server(AddTodo, "/api", "Cbor")] -/// #[server(AddTodo, "/api", "GetCbor")] -/// ``` -/// -/// The four options use different combinations of HTTP verbs and encoding methods: -/// -/// | Name | Method | Request | Response | -/// | ----------------- | ------ | ----------- | -------- | -/// | **Url** (default) | POST | URL encoded | JSON | -/// | **GetJson** | GET | URL encoded | JSON | -/// | **Cbor** | POST | CBOR | CBOR | -/// | **GetCbor** | GET | URL encoded | CBOR | -/// -/// In other words, you have two choices: -/// -/// - `GET` or `POST`? This has implications for things like browser or CDN caching; while `POST` requests should not be cached, -/// `GET` requests can be. -/// - Plain text (arguments sent with URL/form encoding, results sent as JSON) or a binary format (CBOR, encoded as a base64 -/// string)? -/// -/// ## Why not `PUT` or `DELETE`? Why URL/form encoding, and not JSON?** -/// -/// These are reasonable questions. Much of the web is built on REST API patterns that encourage the use of semantic HTTP -/// methods like `DELETE` to delete an item from a database, and many devs are accustomed to sending data to APIs in the -/// JSON format. -/// -/// The reason we use `POST` or `GET` with URL-encoded data by default is the `<form>` support. For better or for worse, -/// HTML forms don’t support `PUT` or `DELETE`, and they don’t support sending JSON. This means that if you use anything -/// but a `GET` or `POST` request with URL-encoded data, it can only work once WASM has loaded. -/// -/// The CBOR encoding is suported for historical reasons; an earlier version of server functions used a URL encoding that -/// didn’t support nested objects like structs or vectors as server function arguments, which CBOR did. But note that the -/// CBOR forms encounter the same issue as `PUT`, `DELETE`, or JSON: they do not degrade gracefully if the WASM version of -/// your app is not available. #[proc_macro_attribute] #[proc_macro_error] pub fn server(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream { From cc6f65cd830bded6143b329ca9cdbbe850d23dd2 Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Fri, 12 Jan 2024 20:23:26 -0500 Subject: [PATCH 048/100] initial version of server action error handling without JS --- .../action-form-error-handling/Cargo.toml | 90 +++++++++++ .../action-form-error-handling/Makefile.toml | 8 + examples/action-form-error-handling/README.md | 68 ++++++++ .../action-form-error-handling/src/app.rs | 90 +++++++++++ .../action-form-error-handling/src/lib.rs | 18 +++ .../action-form-error-handling/src/main.rs | 53 +++++++ .../style/main.scss | 15 ++ integrations/actix/Cargo.toml | 1 + integrations/actix/src/lib.rs | 81 +++++++++- leptos_server/src/action.rs | 149 ++++++++++-------- router/src/components/router.rs | 23 ++- server_fn/src/error.rs | 70 ++++++++ server_fn/src/lib.rs | 7 +- server_fn/src/middleware/mod.rs | 13 +- server_fn/src/response/actix.rs | 23 +-- server_fn/src/response/http.rs | 2 +- server_fn/src/response/mod.rs | 4 +- 17 files changed, 627 insertions(+), 88 deletions(-) create mode 100644 examples/action-form-error-handling/Cargo.toml create mode 100644 examples/action-form-error-handling/Makefile.toml create mode 100644 examples/action-form-error-handling/README.md create mode 100644 examples/action-form-error-handling/src/app.rs create mode 100644 examples/action-form-error-handling/src/lib.rs create mode 100644 examples/action-form-error-handling/src/main.rs create mode 100644 examples/action-form-error-handling/style/main.scss diff --git a/examples/action-form-error-handling/Cargo.toml b/examples/action-form-error-handling/Cargo.toml new file mode 100644 index 0000000000..80500d4821 --- /dev/null +++ b/examples/action-form-error-handling/Cargo.toml @@ -0,0 +1,90 @@ +[package] +name = "action-form-error-handling" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +actix-files = { version = "0.6", optional = true } +actix-web = { version = "4", optional = true, features = ["macros"] } +console_error_panic_hook = "0.1" +cfg-if = "1" +http = { version = "0.2", optional = true } +leptos = { path = "../../leptos" } +leptos_meta = { path = "../../meta" } +leptos_actix = { path = "../../integrations/actix", optional = true } +leptos_router = { path = "../../router" } +wasm-bindgen = "0.2" +serde = { version = "1", features = ["derive"] } + +[features] +csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"] +hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"] +ssr = [ + "dep:actix-files", + "dep:actix-web", + "dep:leptos_actix", + "leptos/ssr", + "leptos_meta/ssr", + "leptos_router/ssr", +] + +# Defines a size-optimized profile for the WASM bundle in release mode +[profile.wasm-release] +inherits = "release" +opt-level = 'z' +lto = true +codegen-units = 1 +panic = "abort" + +[package.metadata.leptos] +# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name +output-name = "leptos_start" +# The site root folder is where cargo-leptos generate all output. WARNING: all content of this folder will be erased on a rebuild. Use it in your server setup. +site-root = "target/site" +# The site-root relative folder where all compiled output (JS, WASM and CSS) is written +# Defaults to pkg +site-pkg-dir = "pkg" +# [Optional] The source CSS file. If it ends with .sass or .scss then it will be compiled by dart-sass into CSS. The CSS is optimized by Lightning CSS before being written to <site-root>/<site-pkg>/app.css +style-file = "style/main.scss" +# The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup. +site-addr = "127.0.0.1:3000" +# The port to use for automatic reload monitoring +reload-port = 3001 +# [Optional] Command to use when running end2end tests. It will run in the end2end dir. +# [Windows] for non-WSL use "npx.cmd playwright test" +# This binary name can be checked in Powershell with Get-Command npx +end2end-cmd = "npx playwright test" +end2end-dir = "end2end" +# The browserlist query used for optimizing the CSS. +browserquery = "defaults" +# Set by cargo-leptos watch when building with that tool. Controls whether autoreload JS will be included in the head +watch = false +# The environment Leptos will run in, usually either "DEV" or "PROD" +env = "DEV" +# The features to use when compiling the bin target +# +# Optional. Can be over-ridden with the command line parameter --bin-features +bin-features = ["ssr"] + +# If the --no-default-features flag should be used when compiling the bin target +# +# Optional. Defaults to false. +bin-default-features = false + +# The features to use when compiling the lib target +# +# Optional. Can be over-ridden with the command line parameter --lib-features +lib-features = ["hydrate"] + +# If the --no-default-features flag should be used when compiling the lib target +# +# Optional. Defaults to false. +lib-default-features = false + +# The profile to use for the lib target when compiling for release +# +# Optional. Defaults to "release". +lib-profile-release = "wasm-release" diff --git a/examples/action-form-error-handling/Makefile.toml b/examples/action-form-error-handling/Makefile.toml new file mode 100644 index 0000000000..a1f669ca28 --- /dev/null +++ b/examples/action-form-error-handling/Makefile.toml @@ -0,0 +1,8 @@ +extend = [ + { path = "../cargo-make/main.toml" }, + { path = "../cargo-make/cargo-leptos.toml" }, +] + +[env] + +CLIENT_PROCESS_NAME = "action_form_error_handling" diff --git a/examples/action-form-error-handling/README.md b/examples/action-form-error-handling/README.md new file mode 100644 index 0000000000..e85b41b2ce --- /dev/null +++ b/examples/action-form-error-handling/README.md @@ -0,0 +1,68 @@ +<picture> + <source srcset="https://raw.githubusercontent.com/leptos-rs/leptos/main/docs/logos/Leptos_logo_Solid_White.svg" media="(prefers-color-scheme: dark)"> + <img src="https://raw.githubusercontent.com/leptos-rs/leptos/main/docs/logos/Leptos_logo_RGB.svg" alt="Leptos Logo"> +</picture> + +# Leptos Starter Template + +This is a template for use with the [Leptos](https://github.com/leptos-rs/leptos) web framework and the [cargo-leptos](https://github.com/akesson/cargo-leptos) tool. + +## Creating your template repo + +If you don't have `cargo-leptos` installed you can install it with + +`cargo install cargo-leptos` + +Then run + +`cargo leptos new --git leptos-rs/start` + +to generate a new project template (you will be prompted to enter a project name). + +`cd {projectname}` + +to go to your newly created project. + +Of course, you should explore around the project structure, but the best place to start with your application code is in `src/app.rs`. + +## Running your project + +`cargo leptos watch` +By default, you can access your local project at `http://localhost:3000` + +## Installing Additional Tools + +By default, `cargo-leptos` uses `nightly` Rust, `cargo-generate`, and `sass`. If you run into any trouble, you may need to install one or more of these tools. + +1. `rustup toolchain install nightly --allow-downgrade` - make sure you have Rust nightly +2. `rustup target add wasm32-unknown-unknown` - add the ability to compile Rust to WebAssembly +3. `cargo install cargo-generate` - install `cargo-generate` binary (should be installed automatically in future) +4. `npm install -g sass` - install `dart-sass` (should be optional in future) + +## Executing a Server on a Remote Machine Without the Toolchain +After running a `cargo leptos build --release` the minimum files needed are: + +1. The server binary located in `target/server/release` +2. The `site` directory and all files within located in `target/site` + +Copy these files to your remote server. The directory structure should be: +```text +leptos_start +site/ +``` +Set the following environment variables (updating for your project as needed): +```sh +export LEPTOS_OUTPUT_NAME="leptos_start" +export LEPTOS_SITE_ROOT="site" +export LEPTOS_SITE_PKG_DIR="pkg" +export LEPTOS_SITE_ADDR="127.0.0.1:3000" +export LEPTOS_RELOAD_PORT="3001" +``` +Finally, run the server binary. + +## Notes about CSR and Trunk: +Although it is not recommended, you can also run your project without server integration using the feature `csr` and `trunk serve`: + +`trunk serve --open --features csr` + +This may be useful for integrating external tools which require a static site, e.g. `tauri`. diff --git a/examples/action-form-error-handling/src/app.rs b/examples/action-form-error-handling/src/app.rs new file mode 100644 index 0000000000..6989756fcc --- /dev/null +++ b/examples/action-form-error-handling/src/app.rs @@ -0,0 +1,90 @@ +use leptos::*; +use leptos_meta::*; +use leptos_router::*; + +#[component] +pub fn App() -> impl IntoView { + // Provides context that manages stylesheets, titles, meta tags, etc. + provide_meta_context(); + + view! { + // injects a stylesheet into the document <head> + // id=leptos means cargo-leptos will hot-reload this stylesheet + <Stylesheet id="leptos" href="/pkg/leptos_start.css"/> + + // sets the document title + <Title text="Welcome to Leptos"/> + + // content for this welcome page + <Router> + <main id="app"> + <Routes> + <Route path="" view=HomePage/> + <Route path="/*any" view=NotFound/> + </Routes> + </main> + </Router> + } +} + +#[server] +async fn do_something(should_error: Option<String>) -> Result<String, ServerFnError> { + if should_error.is_none() { + Ok(String::from("Successful submit")) + } else { + Err(ServerFnError::ServerError(String::from( + "You got an error!", + ))) + } +} + +/// Renders the home page of your application. +#[component] +fn HomePage() -> impl IntoView { + let do_something_action = Action::<DoSomething, _>::server(); + let value = Signal::derive(move || do_something_action.value().get().unwrap_or_else(|| Ok(String::new()))); + + Effect::new_isomorphic(move |_| { + logging::log!("Got value = {:?}", value.get()); + }); + + view! { + <h1>"Test the action form!"</h1> + <ErrorBoundary fallback=move |error| format!("{:#?}", error + .get() + .into_iter() + .next() + .unwrap() + .1.into_inner() + .to_string()) + > + {value} + <ActionForm action=do_something_action class="form"> + <label>Should error: <input type="checkbox" name="should_error"/></label> + <button type="submit">Submit</button> + </ActionForm> + </ErrorBoundary> + } +} + +/// 404 - Not Found +#[component] +fn NotFound() -> impl IntoView { + // set an HTTP status code 404 + // this is feature gated because it can only be done during + // initial server-side rendering + // if you navigate to the 404 page subsequently, the status + // code will not be set because there is not a new HTTP request + // to the server + #[cfg(feature = "ssr")] + { + // this can be done inline because it's synchronous + // if it were async, we'd use a server function + let resp = expect_context::<leptos_actix::ResponseOptions>(); + resp.set_status(actix_web::http::StatusCode::NOT_FOUND); + } + + view! { + <h1>"Not Found"</h1> + } +} diff --git a/examples/action-form-error-handling/src/lib.rs b/examples/action-form-error-handling/src/lib.rs new file mode 100644 index 0000000000..f1b7478302 --- /dev/null +++ b/examples/action-form-error-handling/src/lib.rs @@ -0,0 +1,18 @@ +pub mod app; +use cfg_if::cfg_if; + +cfg_if! { +if #[cfg(feature = "hydrate")] { + + use wasm_bindgen::prelude::wasm_bindgen; + + #[wasm_bindgen] + pub fn hydrate() { + use app::*; + + console_error_panic_hook::set_once(); + + leptos::mount_to_body(App); + } +} +} diff --git a/examples/action-form-error-handling/src/main.rs b/examples/action-form-error-handling/src/main.rs new file mode 100644 index 0000000000..d9cbb052e8 --- /dev/null +++ b/examples/action-form-error-handling/src/main.rs @@ -0,0 +1,53 @@ +#[cfg(feature = "ssr")] +#[actix_web::main] +async fn main() -> std::io::Result<()> { + use actix_files::Files; + use actix_web::*; + use leptos::*; + use leptos_actix::{generate_route_list, LeptosRoutes}; + use action_form_error_handling::app::*; + + let conf = get_configuration(None).await.unwrap(); + let addr = conf.leptos_options.site_addr; + // Generate the list of routes in your Leptos App + let routes = generate_route_list(App); + println!("listening on http://{}", &addr); + + HttpServer::new(move || { + let leptos_options = &conf.leptos_options; + let site_root = &leptos_options.site_root; + + App::new() + .route("/api/{tail:.*}", leptos_actix::handle_server_fns()) + // serve JS/WASM/CSS from `pkg` + .service(Files::new("/pkg", format!("{site_root}/pkg"))) + .leptos_routes(leptos_options.to_owned(), routes.to_owned(), App) + .app_data(web::Data::new(leptos_options.to_owned())) + //.wrap(middleware::Compress::default()) + }) + .bind(&addr)? + .run() + .await +} + +#[cfg(not(any(feature = "ssr", feature = "csr")))] +pub fn main() { + // no client-side main function + // unless we want this to work with e.g., Trunk for pure client-side testing + // see lib.rs for hydration function instead + // see optional feature `csr` instead +} + +#[cfg(all(not(feature = "ssr"), feature = "csr"))] +pub fn main() { + // a client-side main function is required for using `trunk serve` + // prefer using `cargo leptos serve` instead + // to run: `trunk serve --open --features csr` + use leptos::*; + use action_form_error_handling::app::*; + use wasm_bindgen::prelude::wasm_bindgen; + + console_error_panic_hook::set_once(); + + leptos::mount_to_body(App); +} diff --git a/examples/action-form-error-handling/style/main.scss b/examples/action-form-error-handling/style/main.scss new file mode 100644 index 0000000000..908b7515df --- /dev/null +++ b/examples/action-form-error-handling/style/main.scss @@ -0,0 +1,15 @@ +body { + font-family: sans-serif; + text-align: center; +} + +#app { + text-align: center; +} + +.form { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.5rem; +} diff --git a/integrations/actix/Cargo.toml b/integrations/actix/Cargo.toml index aafd392755..7f09700fea 100644 --- a/integrations/actix/Cargo.toml +++ b/integrations/actix/Cargo.toml @@ -22,6 +22,7 @@ parking_lot = "0.12.1" regex = "1.7.0" tracing = "0.1.37" tokio = { version = "1", features = ["rt", "fs"] } +url = "2.5.0" [features] nonce = ["leptos/nonce"] diff --git a/integrations/actix/src/lib.rs b/integrations/actix/src/lib.rs index e49c85c50f..224d0e2a1d 100644 --- a/integrations/actix/src/lib.rs +++ b/integrations/actix/src/lib.rs @@ -6,7 +6,10 @@ //! [`examples`](https://github.com/leptos-rs/leptos/tree/main/examples) //! directory in the Leptos repository. -use actix_http::header::{HeaderName, HeaderValue, ACCEPT}; +use actix_http::{ + body::MessageBody, + header::{HeaderName, HeaderValue, ACCEPT, LOCATION, REFERER}, +}; use actix_web::{ body::BoxBody, dev::{ServiceFactory, ServiceRequest}, @@ -25,15 +28,21 @@ use leptos_meta::*; use leptos_router::*; use parking_lot::RwLock; use regex::Regex; -use server_fn::{redirect::REDIRECT_HEADER, request::actix::ActixRequest}; +use server_fn::{ + error::{NoCustomError, ServerFnErrorSerde, ServerFnUrlError}, + redirect::REDIRECT_HEADER, + request::actix::ActixRequest, +}; use std::{ fmt::{Debug, Display}, future::Future, pin::Pin, + str::FromStr, sync::Arc, }; #[cfg(debug_assertions)] use tracing::instrument; +use url::Url; /// This struct lets you define headers and override the status of the Response from an Element or a Server Function /// Typically contained inside of a ResponseOptions. Setting this is useful for cookies and custom responses. #[derive(Debug, Clone, Default)] @@ -241,12 +250,80 @@ pub fn handle_server_fns_with_context( let res_parts = ResponseOptions::default(); provide_context(res_parts.clone()); + let accepts_html = req + .headers() + .get(ACCEPT) + .and_then(|v| v.to_str().ok()) + .map(|v| v.contains("text/html")) + .unwrap_or(false); + let referrer = req.headers().get(REFERER).cloned(); + let mut res = service .0 .run(ActixRequest::from((req, payload))) .await .take(); + // it it accepts text/html (i.e., is a plain form post) and doesn't already have a + // Location set, then redirect to to Referer + if accepts_html { + if let Some(mut referrer) = referrer { + let location = res.headers().get(LOCATION); + if location.is_none() { + let is_error = res.status() + == StatusCode::INTERNAL_SERVER_ERROR; + if is_error { + let (headers, body) = res.into_parts(); + if let Ok(body) = body.try_into_bytes() { + if let Ok(body) = + String::from_utf8(body.to_vec()) + { + // TODO allow other kinds? + if let Ok(err) = ServerFnUrlError::< + NoCustomError, + >::from_str( + &body + ) { + if let Ok(referrer_str) = + referrer.to_str() + { + let mut modified = + Url::parse(referrer_str) + .expect( + "couldn't parse \ + URL from Referer \ + header.", + ); + modified + .query_pairs_mut() + .append_pair( + "__path", + err.path(), + ) + .append_pair( + "__err", + &ServerFnErrorSerde::ser(err.error()) + .unwrap_or_else(|_| err.error().to_string()) + ); + let modified = + HeaderValue::from_str( + modified.as_ref(), + ); + if let Ok(header) = modified { + referrer = header; + } + } + } + } + } + res = headers.set_body(BoxBody::new("")) + }; + *res.status_mut() = StatusCode::FOUND; + res.headers_mut().insert(LOCATION, referrer); + } + } + }; + // Override StatusCode if it was set in a Resource or Element if let Some(status) = res_parts.0.read().status { *res.status_mut() = status; diff --git a/leptos_server/src/action.rs b/leptos_server/src/action.rs index ef2eeae89c..15dc1e65e8 100644 --- a/leptos_server/src/action.rs +++ b/leptos_server/src/action.rs @@ -1,9 +1,12 @@ //use crate::{ServerFn, ServerFnError}; use leptos_reactive::{ batch, create_rw_signal, is_suppressing_resource_load, signal_prelude::*, - spawn_local, store_value, ReadSignal, RwSignal, StoredValue, + spawn_local, store_value, use_context, ReadSignal, RwSignal, StoredValue, +}; +use server_fn::{ + error::{NoCustomError, ServerFnUrlError}, + ServerFn, ServerFnError, }; -use server_fn::{ServerFn, ServerFnError}; use std::{cell::Cell, future::Future, pin::Pin, rc::Rc}; /// An action synchronizes an imperative `async` call to the synchronous reactive system. @@ -168,6 +171,67 @@ where self.0.with_value(|a| a.pending.read_only()) } + /// Updates whether the action is currently pending. If the action has been dispatched + /// multiple times, and some of them are still pending, it will *not* update the `pending` + /// signal. + #[cfg_attr( + any(debug_assertions, feature = "ssr"), + tracing::instrument(level = "trace", skip_all,) + )] + pub fn set_pending(&self, pending: bool) { + self.0.try_with_value(|a| { + let pending_dispatches = &a.pending_dispatches; + let still_pending = { + pending_dispatches.set(if pending { + pending_dispatches.get().wrapping_add(1) + } else { + pending_dispatches.get().saturating_sub(1) + }); + pending_dispatches.get() + }; + if still_pending == 0 { + a.pending.set(false); + } else { + a.pending.set(true); + } + }); + } + + /// The URL associated with the action (typically as part of a server function.) + /// This enables integration with the `ActionForm` component in `leptos_router`. + pub fn url(&self) -> Option<String> { + self.0.with_value(|a| a.url.as_ref().cloned()) + } + + /// How many times the action has successfully resolved. + pub fn version(&self) -> RwSignal<usize> { + self.0.with_value(|a| a.version) + } + + /// The current argument that was dispatched to the `async` function. + /// `Some` while we are waiting for it to resolve, `None` if it has resolved. + #[cfg_attr( + any(debug_assertions, feature = "ssr"), + tracing::instrument(level = "trace", skip_all,) + )] + pub fn input(&self) -> RwSignal<Option<I>> { + self.0.with_value(|a| a.input) + } + + /// The most recent return value of the `async` function. + #[cfg_attr( + any(debug_assertions, feature = "ssr"), + tracing::instrument(level = "trace", skip_all,) + )] + pub fn value(&self) -> RwSignal<Option<O>> { + self.0.with_value(|a| a.value) + } +} + +impl<I> Action<I, Result<I::Output, ServerFnError<I::Error>>> +where + I: ServerFn + 'static, +{ /// Create an [Action] to imperatively call a [server_fn::server] function. /// /// The struct representing your server function's arguments should be @@ -214,7 +278,8 @@ where )] pub fn server() -> Action<I, Result<I::Output, ServerFnError<I::Error>>> where - I: ServerFn<Output = O> + Clone, + I: ServerFn + Clone, + I::Error: Clone + 'static, { // The server is able to call the function directly #[cfg(feature = "ssr")] @@ -225,39 +290,7 @@ where let action_function = |args: &I| I::run_on_client(args.clone()); // create the action - Action::new(action_function).using_server_fn::<I>() - } - - /// Updates whether the action is currently pending. If the action has been dispatched - /// multiple times, and some of them are still pending, it will *not* update the `pending` - /// signal. - #[cfg_attr( - any(debug_assertions, feature = "ssr"), - tracing::instrument(level = "trace", skip_all,) - )] - pub fn set_pending(&self, pending: bool) { - self.0.try_with_value(|a| { - let pending_dispatches = &a.pending_dispatches; - let still_pending = { - pending_dispatches.set(if pending { - pending_dispatches.get().wrapping_add(1) - } else { - pending_dispatches.get().saturating_sub(1) - }); - pending_dispatches.get() - }; - if still_pending == 0 { - a.pending.set(false); - } else { - a.pending.set(true); - } - }); - } - - /// The URL associated with the action (typically as part of a server function.) - /// This enables integration with the `ActionForm` component in `leptos_router`. - pub fn url(&self) -> Option<String> { - self.0.with_value(|a| a.url.as_ref().cloned()) + Action::new(action_function).using_server_fn() } /// Associates the URL of the given server function with this action. @@ -266,36 +299,27 @@ where any(debug_assertions, feature = "ssr"), tracing::instrument(level = "trace", skip_all,) )] - pub fn using_server_fn<T: ServerFn>(self) -> Self { + pub fn using_server_fn(self) -> Self + where + I::Error: Clone + 'static, + { + let url = I::url(); + let action_error = use_context::<Rc<ServerFnUrlError<I::Error>>>() + .and_then(|err| { + if err.path() == url { + Some(err.error().clone()) + } else { + None + } + }); self.0.update_value(|state| { - state.url = Some(T::url().to_string()); + if let Some(err) = action_error { + state.value.set_untracked(Some(Err(err))); + } + state.url = Some(url.to_string()); }); self } - - /// How many times the action has successfully resolved. - pub fn version(&self) -> RwSignal<usize> { - self.0.with_value(|a| a.version) - } - - /// The current argument that was dispatched to the `async` function. - /// `Some` while we are waiting for it to resolve, `None` if it has resolved. - #[cfg_attr( - any(debug_assertions, feature = "ssr"), - tracing::instrument(level = "trace", skip_all,) - )] - pub fn input(&self) -> RwSignal<Option<I>> { - self.0.with_value(|a| a.input) - } - - /// The most recent return value of the `async` function. - #[cfg_attr( - any(debug_assertions, feature = "ssr"), - tracing::instrument(level = "trace", skip_all,) - )] - pub fn value(&self) -> RwSignal<Option<O>> { - self.0.with_value(|a| a.value) - } } impl<I, O> Clone for Action<I, O> @@ -482,6 +506,7 @@ pub fn create_server_action<S>( ) -> Action<S, Result<S::Output, ServerFnError<S::Error>>> where S: Clone + ServerFn, + S::Error: Clone + 'static, { Action::<S, _>::server() } diff --git a/router/src/components/router.rs b/router/src/components/router.rs index 875c75d623..2c63fee8f4 100644 --- a/router/src/components/router.rs +++ b/router/src/components/router.rs @@ -1,12 +1,19 @@ use crate::{ - create_location, matching::resolve_path, scroll_to_el, use_navigate, - Branch, History, Location, LocationChange, RouteContext, + create_location, matching::resolve_path, scroll_to_el, use_location, + use_navigate, Branch, History, Location, LocationChange, RouteContext, RouterIntegrationContext, State, }; #[cfg(not(feature = "ssr"))] use crate::{unescape, Url}; use cfg_if::cfg_if; -use leptos::{logging::debug_warn, server_fn::redirect::RedirectHook, *}; +use leptos::{ + logging::debug_warn, + server_fn::{ + error::{ServerFnErrorSerde, ServerFnUrlError}, + redirect::RedirectHook, + }, + *, +}; #[cfg(feature = "transition")] use leptos_reactive::use_transition; use send_wrapper::SendWrapper; @@ -64,6 +71,16 @@ pub fn Router( debug_warn!("Error setting <Router/> server function redirect hook."); } + // provide ServerFnUrlError if it exists + let location = use_location(); + if let (Some(path), Some(err)) = location + .query + .with_untracked(|q| (q.get("__path").cloned(), q.get("__err").cloned())) + { + let err: ServerFnError = ServerFnErrorSerde::de(&err); + provide_context(Rc::new(ServerFnUrlError::new(path, err))) + } + children() } diff --git a/server_fn/src/error.rs b/server_fn/src/error.rs index 4771bd053d..e6c1f84dbe 100644 --- a/server_fn/src/error.rs +++ b/server_fn/src/error.rs @@ -399,3 +399,73 @@ impl<CustErr> From<ServerFnError<CustErr>> for ServerFnErrorErr<CustErr> { } } } + +/// TODO: Write Documentation +#[derive(Debug)] +pub struct ServerFnUrlError<CustErr> { + path: String, + error: ServerFnError<CustErr>, +} + +impl<CustErr> FromStr for ServerFnUrlError<CustErr> +where + CustErr: FromStr + Display, +{ + type Err = (); + + fn from_str(s: &str) -> Result<Self, Self::Err> { + match s.split_once('|') { + None => Err(()), + Some((path, error)) => { + let error = ServerFnError::<CustErr>::de(error); + Ok(ServerFnUrlError { + path: path.to_string(), + error, + }) + } + } + } +} + +impl<CustErr> Display for ServerFnUrlError<CustErr> +where + CustErr: FromStr + Display, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}|", self.path)?; + write!(f, "{}", &self.error.ser()?)?; + Ok(()) + } +} + +impl<CustErr> ServerFnUrlError<CustErr> { + /// TODO: Write Documentation + pub fn new(path: impl Display, error: ServerFnError<CustErr>) -> Self { + Self { + path: path.to_string(), + error, + } + } + + /// TODO: Write documentation + pub fn error(&self) -> &ServerFnError<CustErr> { + &self.error + } + + /// TODO: Add docs + pub fn path(&self) -> &str { + &self.path + } +} + +impl<CustErr> From<ServerFnUrlError<CustErr>> for ServerFnError<CustErr> { + fn from(error: ServerFnUrlError<CustErr>) -> Self { + error.error + } +} + +impl<CustErr> From<ServerFnUrlError<CustErr>> for ServerFnErrorErr<CustErr> { + fn from(error: ServerFnUrlError<CustErr>) -> Self { + error.error.into() + } +} diff --git a/server_fn/src/lib.rs b/server_fn/src/lib.rs index 23221ae972..279a722d31 100644 --- a/server_fn/src/lib.rs +++ b/server_fn/src/lib.rs @@ -237,9 +237,9 @@ where req: Self::ServerRequest, ) -> impl Future<Output = Self::ServerResponse> + Send { async { - Self::execute_on_server(req) - .await - .unwrap_or_else(Self::ServerResponse::error_response) + Self::execute_on_server(req).await.unwrap_or_else(|e| { + Self::ServerResponse::error_response(Self::PATH, e) + }) } } @@ -289,6 +289,7 @@ where } } + /// Runs the server function (on the server), bubbling up an `Err(_)` after any stage. #[doc(hidden)] fn execute_on_server( req: Self::ServerRequest, diff --git a/server_fn/src/middleware/mod.rs b/server_fn/src/middleware/mod.rs index 4d093b53e5..ed537238cb 100644 --- a/server_fn/src/middleware/mod.rs +++ b/server_fn/src/middleware/mod.rs @@ -48,11 +48,12 @@ mod axum { &mut self, req: Request<Body>, ) -> Pin<Box<dyn Future<Output = Response<Body>> + 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::from(e); - Response::<Body>::error_response(err) + Response::<Body>::error_response(&path, err) }) }) } @@ -125,11 +126,12 @@ mod actix { &mut self, req: HttpRequest, ) -> Pin<Box<dyn Future<Output = HttpResponse> + 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::from(e); - ActixResponse::error_response(err).take() + let err = ServerFnError::new(e); + ActixResponse::error_response(&path, err).take() }) }) } @@ -145,11 +147,12 @@ mod actix { &mut self, req: ActixRequest, ) -> Pin<Box<dyn Future<Output = ActixResponse> + 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::from(e); - ActixResponse::error_response(err).take() + let err = ServerFnError::new(e); + ActixResponse::error_response(&path, err).take() })) }) } diff --git a/server_fn/src/response/actix.rs b/server_fn/src/response/actix.rs index a0c6efa6bf..14b630ea70 100644 --- a/server_fn/src/response/actix.rs +++ b/server_fn/src/response/actix.rs @@ -1,5 +1,5 @@ use super::Res; -use crate::error::{ServerFnError, ServerFnErrorErr}; +use crate::error::{ServerFnError, ServerFnErrorErr, ServerFnUrlError}; use actix_web::{ http::{header, StatusCode}, HttpResponse, @@ -7,7 +7,10 @@ use actix_web::{ use bytes::Bytes; use futures::{Stream, StreamExt}; use send_wrapper::SendWrapper; -use std::fmt::{Debug, Display}; +use std::{ + fmt::{Debug, Display}, + str::FromStr, +}; /// A wrapped Actix response. /// @@ -31,7 +34,7 @@ impl From<HttpResponse> for ActixResponse { impl<CustErr> Res<CustErr> for ActixResponse where - CustErr: Display + Debug + 'static, + CustErr: FromStr + Display + Debug + 'static, { fn try_from_string( content_type: &str, @@ -57,13 +60,6 @@ where ))) } - fn error_response(err: ServerFnError<CustErr>) -> Self { - ActixResponse(SendWrapper::new( - HttpResponse::build(StatusCode::INTERNAL_SERVER_ERROR) - .body(err.to_string()), - )) - } - fn try_from_stream( content_type: &str, data: impl Stream<Item = Result<Bytes, ServerFnError<CustErr>>> + 'static, @@ -77,4 +73,11 @@ where ), ))) } + + fn error_response(path: &str, err: ServerFnError<CustErr>) -> Self { + ActixResponse(SendWrapper::new( + HttpResponse::build(StatusCode::INTERNAL_SERVER_ERROR) + .body(ServerFnUrlError::new(path, err).to_string()), + )) + } } diff --git a/server_fn/src/response/http.rs b/server_fn/src/response/http.rs index 09844e72b4..ed6176bcd8 100644 --- a/server_fn/src/response/http.rs +++ b/server_fn/src/response/http.rs @@ -50,7 +50,7 @@ where .map_err(|e| ServerFnError::Response(e.to_string())) } - fn error_response(err: ServerFnError<CustErr>) -> Self { + fn error_response(path: &str, err: ServerFnError<CustErr>) -> Self { Response::builder() .status(http::StatusCode::INTERNAL_SERVER_ERROR) .body(Body::from(err.to_string())) diff --git a/server_fn/src/response/mod.rs b/server_fn/src/response/mod.rs index c6bf5a4a3a..620b5395fa 100644 --- a/server_fn/src/response/mod.rs +++ b/server_fn/src/response/mod.rs @@ -42,7 +42,7 @@ where ) -> Result<Self, ServerFnError<CustErr>>; /// Converts an error into a response, with a `500` status code and the error text as its body. - fn error_response(err: ServerFnError<CustErr>) -> Self; + fn error_response(path: &str, err: ServerFnError<CustErr>) -> Self; } /// Represents the response as received by the client. @@ -101,7 +101,7 @@ impl<CustErr> Res<CustErr> for BrowserMockRes { unreachable!() } - fn error_response(_err: ServerFnError<CustErr>) -> Self { + fn error_response(_path: &str, _err: ServerFnError<CustErr>) -> Self { unreachable!() } From 5e082535211c1540cd8d6096536a86e1d56b84f5 Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Fri, 12 Jan 2024 20:40:03 -0500 Subject: [PATCH 049/100] get both client and server side working --- integrations/actix/src/lib.rs | 37 +++++++++++++++++++-------------- server_fn/src/error.rs | 3 +++ server_fn/src/response/actix.rs | 8 +++++-- 3 files changed, 30 insertions(+), 18 deletions(-) diff --git a/integrations/actix/src/lib.rs b/integrations/actix/src/lib.rs index 224d0e2a1d..3cfce41265 100644 --- a/integrations/actix/src/lib.rs +++ b/integrations/actix/src/lib.rs @@ -29,7 +29,10 @@ use leptos_router::*; use parking_lot::RwLock; use regex::Regex; use server_fn::{ - error::{NoCustomError, ServerFnErrorSerde, ServerFnUrlError}, + error::{ + NoCustomError, ServerFnErrorSerde, ServerFnUrlError, + SERVER_FN_ERROR_HEADER, + }, redirect::REDIRECT_HEADER, request::actix::ActixRequest, }; @@ -273,17 +276,20 @@ pub fn handle_server_fns_with_context( let is_error = res.status() == StatusCode::INTERNAL_SERVER_ERROR; if is_error { - let (headers, body) = res.into_parts(); - if let Ok(body) = body.try_into_bytes() { - if let Ok(body) = - String::from_utf8(body.to_vec()) - { - // TODO allow other kinds? - if let Ok(err) = ServerFnUrlError::< - NoCustomError, - >::from_str( - &body - ) { + if let Some(Ok(path)) = res + .headers() + .get(SERVER_FN_ERROR_HEADER) + .map(|n| n.to_str().map(|n| n.to_owned())) + { + let (headers, body) = res.into_parts(); + if let Ok(body) = body.try_into_bytes() { + if let Ok(body) = + String::from_utf8(body.to_vec()) + { + // TODO allow other kinds? + let err: ServerFnError< + NoCustomError, + > = ServerFnErrorSerde::de(&body); if let Ok(referrer_str) = referrer.to_str() { @@ -298,12 +304,11 @@ pub fn handle_server_fns_with_context( .query_pairs_mut() .append_pair( "__path", - err.path(), + &path ) .append_pair( "__err", - &ServerFnErrorSerde::ser(err.error()) - .unwrap_or_else(|_| err.error().to_string()) + &ServerFnErrorSerde::ser(&err).unwrap_or_default() ); let modified = HeaderValue::from_str( @@ -315,8 +320,8 @@ pub fn handle_server_fns_with_context( } } } + res = headers.set_body(BoxBody::new("")) } - res = headers.set_body(BoxBody::new("")) }; *res.status_mut() = StatusCode::FOUND; res.headers_mut().insert(LOCATION, referrer); diff --git a/server_fn/src/error.rs b/server_fn/src/error.rs index e6c1f84dbe..e47ff6e036 100644 --- a/server_fn/src/error.rs +++ b/server_fn/src/error.rs @@ -8,6 +8,9 @@ use std::{ }; use thiserror::Error; +/// A custom header that can be used to indicate a server function returned an error. +pub const SERVER_FN_ERROR_HEADER: &'static str = "serverfnerror"; + /// This is a result type into which any error can be converted, /// and which can be used directly in your `view`. /// diff --git a/server_fn/src/response/actix.rs b/server_fn/src/response/actix.rs index 14b630ea70..4f0ecb6cb5 100644 --- a/server_fn/src/response/actix.rs +++ b/server_fn/src/response/actix.rs @@ -1,5 +1,8 @@ use super::Res; -use crate::error::{ServerFnError, ServerFnErrorErr, ServerFnUrlError}; +use crate::error::{ + ServerFnError, ServerFnErrorErr, ServerFnErrorSerde, ServerFnUrlError, + SERVER_FN_ERROR_HEADER, +}; use actix_web::{ http::{header, StatusCode}, HttpResponse, @@ -77,7 +80,8 @@ where fn error_response(path: &str, err: ServerFnError<CustErr>) -> Self { ActixResponse(SendWrapper::new( HttpResponse::build(StatusCode::INTERNAL_SERVER_ERROR) - .body(ServerFnUrlError::new(path, err).to_string()), + .append_header((SERVER_FN_ERROR_HEADER, path)) + .body(err.ser().unwrap_or_else(|_| err.to_string())), )) } } From 88fee243a88e217dde90db4d847d397e941abb55 Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Sat, 13 Jan 2024 15:17:23 -0500 Subject: [PATCH 050/100] support setting server URL on either platform --- server_fn/src/client.rs | 16 +++++++++++++++- server_fn/src/request/browser.rs | 27 ++++++++++++++++++++++----- server_fn/src/request/reqwest.rs | 17 +---------------- 3 files changed, 38 insertions(+), 22 deletions(-) diff --git a/server_fn/src/client.rs b/server_fn/src/client.rs index fdfc85358f..84538bb333 100644 --- a/server_fn/src/client.rs +++ b/server_fn/src/client.rs @@ -1,5 +1,19 @@ use crate::{error::ServerFnError, request::ClientReq, response::ClientRes}; -use std::future::Future; +use std::{future::Future, sync::OnceLock}; + +static ROOT_URL: OnceLock<&'static str> = OnceLock::new(); + +/// Set the root server URL that all server function paths are relative to for the client. +/// +/// If this is not set, it defaults to the origin. +pub fn set_server_url(url: &'static str) { + ROOT_URL.set(url).unwrap(); +} + +/// Returns the root server URL for all server functions. +pub fn get_server_url() -> &'static str { + ROOT_URL.get().copied().unwrap_or("") +} /// A client defines a pair of request/response types and the logic to send /// and receive them. diff --git a/server_fn/src/request/browser.rs b/server_fn/src/request/browser.rs index c1ce0f89f9..b2af59d7fc 100644 --- a/server_fn/src/request/browser.rs +++ b/server_fn/src/request/browser.rs @@ -1,5 +1,5 @@ use super::ClientReq; -use crate::error::ServerFnError; +use crate::{client::get_server_url, error::ServerFnError}; use bytes::Bytes; pub use gloo_net::http::Request; use js_sys::Uint8Array; @@ -35,7 +35,12 @@ impl<CustErr> ClientReq<CustErr> for BrowserRequest { content_type: &str, query: &str, ) -> Result<Self, ServerFnError<CustErr>> { - let mut url = path.to_owned(); + let server_url = get_server_url(); + let mut url = String::with_capacity( + server_url.len() + path.len() + 1 + query.len(), + ); + url.push_str(server_url); + url.push_str(path); url.push('?'); url.push_str(query); Ok(Self(SendWrapper::new( @@ -53,8 +58,12 @@ impl<CustErr> ClientReq<CustErr> for BrowserRequest { content_type: &str, body: String, ) -> Result<Self, ServerFnError<CustErr>> { + let server_url = get_server_url(); + let mut url = String::with_capacity(server_url.len() + path.len()); + url.push_str(server_url); + url.push_str(path); Ok(Self(SendWrapper::new( - Request::post(path) + Request::post(&url) .header("Content-Type", content_type) .header("Accept", accepts) .body(body) @@ -68,10 +77,14 @@ impl<CustErr> ClientReq<CustErr> for BrowserRequest { content_type: &str, body: Bytes, ) -> Result<Self, ServerFnError<CustErr>> { + let server_url = get_server_url(); + let mut url = String::with_capacity(server_url.len() + path.len()); + url.push_str(server_url); + url.push_str(path); let body: &[u8] = &body; let body = Uint8Array::from(body).buffer(); Ok(Self(SendWrapper::new( - Request::post(path) + Request::post(&url) .header("Content-Type", content_type) .header("Accept", accepts) .body(body) @@ -84,8 +97,12 @@ impl<CustErr> ClientReq<CustErr> for BrowserRequest { accepts: &str, body: Self::FormData, ) -> Result<Self, ServerFnError<CustErr>> { + let server_url = get_server_url(); + let mut url = String::with_capacity(server_url.len() + path.len()); + url.push_str(server_url); + url.push_str(path); Ok(Self(SendWrapper::new( - Request::post(path) + Request::post(&url) .header("Accept", accepts) .body(body.0.take()) .map_err(|e| ServerFnError::Request(e.to_string()))?, diff --git a/server_fn/src/request/reqwest.rs b/server_fn/src/request/reqwest.rs index 5e63956554..bfff453f60 100644 --- a/server_fn/src/request/reqwest.rs +++ b/server_fn/src/request/reqwest.rs @@ -1,26 +1,11 @@ use super::ClientReq; -use crate::error::ServerFnError; +use crate::{client::get_server_url, error::ServerFnError}; use bytes::Bytes; use once_cell::sync::Lazy; use reqwest::header::{ACCEPT, CONTENT_TYPE}; pub use reqwest::{multipart::Form, Client, Method, Request, Url}; -use std::sync::OnceLock; pub(crate) static CLIENT: Lazy<Client> = Lazy::new(Client::new); -static ROOT_URL: OnceLock<&'static str> = OnceLock::new(); - -/// Set the root server url that all server function paths are relative to for the client. -/// -/// If this is not set, it defaults to the origin. -pub fn set_server_url(url: &'static str) { - ROOT_URL.set(url).unwrap(); -} - -fn get_server_url() -> &'static str { - ROOT_URL - .get() - .expect("Call `set_root_url` before calling a server function.") -} impl<CustErr> ClientReq<CustErr> for Request { type FormData = Form; From 1ad7ee8a03f0b8b5f83bb0eefd21a43cd84bad93 Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Sat, 13 Jan 2024 21:58:46 -0500 Subject: [PATCH 051/100] generalize error redirect behavior across integrations --- integrations/actix/Cargo.toml | 1 - integrations/actix/src/lib.rs | 86 +-------------------------------- leptos/Cargo.toml | 2 +- leptos_server/src/action.rs | 5 +- server_fn/Cargo.toml | 3 +- server_fn/src/error.rs | 61 ++++++++++------------- server_fn/src/lib.rs | 43 ++++++++++++++--- server_fn/src/middleware/mod.rs | 6 +-- server_fn/src/request/actix.rs | 26 +++++++--- server_fn/src/request/axum.rs | 22 +++++++-- server_fn/src/request/mod.rs | 19 ++++++-- server_fn/src/response/actix.rs | 18 +++++-- server_fn/src/response/http.rs | 11 ++++- server_fn/src/response/mod.rs | 13 +++-- 14 files changed, 159 insertions(+), 157 deletions(-) diff --git a/integrations/actix/Cargo.toml b/integrations/actix/Cargo.toml index 7f09700fea..aafd392755 100644 --- a/integrations/actix/Cargo.toml +++ b/integrations/actix/Cargo.toml @@ -22,7 +22,6 @@ parking_lot = "0.12.1" regex = "1.7.0" tracing = "0.1.37" tokio = { version = "1", features = ["rt", "fs"] } -url = "2.5.0" [features] nonce = ["leptos/nonce"] diff --git a/integrations/actix/src/lib.rs b/integrations/actix/src/lib.rs index 3cfce41265..e49c85c50f 100644 --- a/integrations/actix/src/lib.rs +++ b/integrations/actix/src/lib.rs @@ -6,10 +6,7 @@ //! [`examples`](https://github.com/leptos-rs/leptos/tree/main/examples) //! directory in the Leptos repository. -use actix_http::{ - body::MessageBody, - header::{HeaderName, HeaderValue, ACCEPT, LOCATION, REFERER}, -}; +use actix_http::header::{HeaderName, HeaderValue, ACCEPT}; use actix_web::{ body::BoxBody, dev::{ServiceFactory, ServiceRequest}, @@ -28,24 +25,15 @@ use leptos_meta::*; use leptos_router::*; use parking_lot::RwLock; use regex::Regex; -use server_fn::{ - error::{ - NoCustomError, ServerFnErrorSerde, ServerFnUrlError, - SERVER_FN_ERROR_HEADER, - }, - redirect::REDIRECT_HEADER, - request::actix::ActixRequest, -}; +use server_fn::{redirect::REDIRECT_HEADER, request::actix::ActixRequest}; use std::{ fmt::{Debug, Display}, future::Future, pin::Pin, - str::FromStr, sync::Arc, }; #[cfg(debug_assertions)] use tracing::instrument; -use url::Url; /// This struct lets you define headers and override the status of the Response from an Element or a Server Function /// Typically contained inside of a ResponseOptions. Setting this is useful for cookies and custom responses. #[derive(Debug, Clone, Default)] @@ -253,82 +241,12 @@ pub fn handle_server_fns_with_context( let res_parts = ResponseOptions::default(); provide_context(res_parts.clone()); - let accepts_html = req - .headers() - .get(ACCEPT) - .and_then(|v| v.to_str().ok()) - .map(|v| v.contains("text/html")) - .unwrap_or(false); - let referrer = req.headers().get(REFERER).cloned(); - let mut res = service .0 .run(ActixRequest::from((req, payload))) .await .take(); - // it it accepts text/html (i.e., is a plain form post) and doesn't already have a - // Location set, then redirect to to Referer - if accepts_html { - if let Some(mut referrer) = referrer { - let location = res.headers().get(LOCATION); - if location.is_none() { - let is_error = res.status() - == StatusCode::INTERNAL_SERVER_ERROR; - if is_error { - if let Some(Ok(path)) = res - .headers() - .get(SERVER_FN_ERROR_HEADER) - .map(|n| n.to_str().map(|n| n.to_owned())) - { - let (headers, body) = res.into_parts(); - if let Ok(body) = body.try_into_bytes() { - if let Ok(body) = - String::from_utf8(body.to_vec()) - { - // TODO allow other kinds? - let err: ServerFnError< - NoCustomError, - > = ServerFnErrorSerde::de(&body); - if let Ok(referrer_str) = - referrer.to_str() - { - let mut modified = - Url::parse(referrer_str) - .expect( - "couldn't parse \ - URL from Referer \ - header.", - ); - modified - .query_pairs_mut() - .append_pair( - "__path", - &path - ) - .append_pair( - "__err", - &ServerFnErrorSerde::ser(&err).unwrap_or_default() - ); - let modified = - HeaderValue::from_str( - modified.as_ref(), - ); - if let Ok(header) = modified { - referrer = header; - } - } - } - } - res = headers.set_body(BoxBody::new("")) - } - }; - *res.status_mut() = StatusCode::FOUND; - res.headers_mut().insert(LOCATION, referrer); - } - } - }; - // Override StatusCode if it was set in a Resource or Element if let Some(status) = res_parts.0.read().status { *res.status_mut() = status; diff --git a/leptos/Cargo.toml b/leptos/Cargo.toml index 730e7ff792..17f287f2a2 100644 --- a/leptos/Cargo.toml +++ b/leptos/Cargo.toml @@ -20,7 +20,7 @@ typed-builder = "0.18" typed-builder-macro = "0.18" serde = { version = "1", optional = true } serde_json = { version = "1", optional = true } -server_fn = { workspace = true, features = ["browser"] } +server_fn = { workspace = true, features = ["browser", "url", "cbor"] } web-sys = { version = "0.3.63", features = [ "ShadowRoot", "ShadowRootInit", diff --git a/leptos_server/src/action.rs b/leptos_server/src/action.rs index 15dc1e65e8..b5ca4e5d12 100644 --- a/leptos_server/src/action.rs +++ b/leptos_server/src/action.rs @@ -3,10 +3,7 @@ use leptos_reactive::{ batch, create_rw_signal, is_suppressing_resource_load, signal_prelude::*, spawn_local, store_value, use_context, ReadSignal, RwSignal, StoredValue, }; -use server_fn::{ - error::{NoCustomError, ServerFnUrlError}, - ServerFn, ServerFnError, -}; +use server_fn::{error::ServerFnUrlError, ServerFn, ServerFnError}; use std::{cell::Cell, future::Future, pin::Pin, rc::Rc}; /// An action synchronizes an imperative `async` call to the synchronous reactive system. diff --git a/server_fn/Cargo.toml b/server_fn/Cargo.toml index 36027b4ecc..aa1394ceb2 100644 --- a/server_fn/Cargo.toml +++ b/server_fn/Cargo.toml @@ -64,9 +64,10 @@ reqwest = { version = "0.11", default-features = false, optional = true, feature "multipart", "stream", ] } +url = "2" [features] -default = ["url", "json", "cbor"] +default = [ "json", "cbor"] actix = ["ssr", "dep:actix-web", "dep:send_wrapper"] axum = [ "ssr", diff --git a/server_fn/src/error.rs b/server_fn/src/error.rs index e47ff6e036..ab5b57fedf 100644 --- a/server_fn/src/error.rs +++ b/server_fn/src/error.rs @@ -7,6 +7,7 @@ use std::{ sync::Arc, }; use thiserror::Error; +use url::Url; /// A custom header that can be used to indicate a server function returned an error. pub const SERVER_FN_ERROR_HEADER: &'static str = "serverfnerror"; @@ -403,46 +404,21 @@ impl<CustErr> From<ServerFnError<CustErr>> for ServerFnErrorErr<CustErr> { } } -/// TODO: Write Documentation +/// Associates a particular server function error with the server function +/// found at a particular path. +/// +/// This can be used to pass an error from the server back to the client +/// without JavaScript/WASM supported, by encoding it in the URL as a qurey string. +/// This is useful for progressive enhancement. #[derive(Debug)] pub struct ServerFnUrlError<CustErr> { path: String, error: ServerFnError<CustErr>, } -impl<CustErr> FromStr for ServerFnUrlError<CustErr> -where - CustErr: FromStr + Display, -{ - type Err = (); - - fn from_str(s: &str) -> Result<Self, Self::Err> { - match s.split_once('|') { - None => Err(()), - Some((path, error)) => { - let error = ServerFnError::<CustErr>::de(error); - Ok(ServerFnUrlError { - path: path.to_string(), - error, - }) - } - } - } -} - -impl<CustErr> Display for ServerFnUrlError<CustErr> -where - CustErr: FromStr + Display, -{ - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}|", self.path)?; - write!(f, "{}", &self.error.ser()?)?; - Ok(()) - } -} - impl<CustErr> ServerFnUrlError<CustErr> { - /// TODO: Write Documentation + /// Creates a new structure associating the server function at some path + /// with a particular error. pub fn new(path: impl Display, error: ServerFnError<CustErr>) -> Self { Self { path: path.to_string(), @@ -450,15 +426,30 @@ impl<CustErr> ServerFnUrlError<CustErr> { } } - /// TODO: Write documentation + /// The error itself. pub fn error(&self) -> &ServerFnError<CustErr> { &self.error } - /// TODO: Add docs + /// The path of the server function that generated this error. pub fn path(&self) -> &str { &self.path } + + /// Adds an encoded form of this server function error to the given base URL. + pub fn to_url(&self, base: &str) -> Result<Url, url::ParseError> + where + CustErr: FromStr + Display, + { + 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(), + ); + Ok(url) + } } impl<CustErr> From<ServerFnUrlError<CustErr>> for ServerFnError<CustErr> { diff --git a/server_fn/src/lib.rs b/server_fn/src/lib.rs index 279a722d31..b6986020ec 100644 --- a/server_fn/src/lib.rs +++ b/server_fn/src/lib.rs @@ -1,5 +1,4 @@ #![forbid(unsafe_code)] -// uncomment this if you want to feel pain #![deny(missing_docs)] //! # Server Functions @@ -126,7 +125,7 @@ use codec::{Encoding, FromReq, FromRes, IntoReq, IntoRes}; pub use const_format; use dashmap::DashMap; pub use error::ServerFnError; -use error::ServerFnErrorSerde; +use error::{ServerFnErrorSerde, ServerFnUrlError}; use http::Method; use middleware::{Layer, Service}; use once_cell::sync::Lazy; @@ -236,10 +235,42 @@ where fn run_on_server( req: Self::ServerRequest, ) -> impl Future<Output = Self::ServerResponse> + Send { - async { - Self::execute_on_server(req).await.unwrap_or_else(|e| { - Self::ServerResponse::error_response(Self::PATH, e) - }) + // Server functions can either be called by a real Client, + // or directly by an HTML <form>. If they're accessed by a <form>, default to + // redirecting back to the Referer. + let accepts_html = req + .accepts() + .map(|n| n.contains("text/html")) + .unwrap_or(false); + let mut referer = req.referer().as_deref().map(ToOwned::to_owned); + + async move { + let (mut res, err) = Self::execute_on_server(req) + .await + .map(|res| (res, None)) + .unwrap_or_else(|e| { + ( + Self::ServerResponse::error_response(Self::PATH, &e), + Some(e), + ) + }); + + // if it accepts HTML, we'll redirect to the Referer + if accepts_html { + // if it had an error, encode that error in the URL + if let Some(err) = err { + if let Ok(url) = ServerFnUrlError::new(Self::PATH, err) + .to_url(referer.as_deref().unwrap_or("/")) + { + referer = Some(url.to_string()); + } + } + + // set the status code and Location header + res.redirect(referer.as_deref().unwrap_or("/")); + } + + res } } diff --git a/server_fn/src/middleware/mod.rs b/server_fn/src/middleware/mod.rs index ed537238cb..9c23e8582e 100644 --- a/server_fn/src/middleware/mod.rs +++ b/server_fn/src/middleware/mod.rs @@ -53,7 +53,7 @@ mod axum { Box::pin(async move { inner.await.unwrap_or_else(|e| { let err = ServerFnError::from(e); - Response::<Body>::error_response(&path, err) + Response::<Body>::error_response(&path, &err) }) }) } @@ -131,7 +131,7 @@ mod actix { Box::pin(async move { inner.await.unwrap_or_else(|e| { let err = ServerFnError::new(e); - ActixResponse::error_response(&path, err).take() + ActixResponse::error_response(&path, &err).take() }) }) } @@ -152,7 +152,7 @@ mod actix { Box::pin(async move { ActixResponse::from(inner.await.unwrap_or_else(|e| { let err = ServerFnError::new(e); - ActixResponse::error_response(&path, err).take() + ActixResponse::error_response(&path, &err).take() })) }) } diff --git a/server_fn/src/request/actix.rs b/server_fn/src/request/actix.rs index b0a727aac3..05c801a19b 100644 --- a/server_fn/src/request/actix.rs +++ b/server_fn/src/request/actix.rs @@ -3,7 +3,7 @@ use actix_web::{web::Payload, HttpRequest}; use bytes::Bytes; use futures::Stream; use send_wrapper::SendWrapper; -use std::future::Future; +use std::{borrow::Cow, future::Future}; /// A wrapped Actix request. /// @@ -17,6 +17,14 @@ impl ActixRequest { pub fn take(self) -> (HttpRequest, Payload) { self.0.take() } + + fn header(&self, name: &str) -> Option<Cow<'_, str>> { + self.0 + .0 + .headers() + .get(name) + .map(|h| String::from_utf8_lossy(h.as_bytes())) + } } impl From<(HttpRequest, Payload)> for ActixRequest { @@ -30,12 +38,16 @@ impl<CustErr> Req<CustErr> for ActixRequest { self.0 .0.uri().query() } - fn to_content_type(&self) -> Option<String> { - self.0 - .0 - .headers() - .get("Content-Type") - .map(|h| String::from_utf8_lossy(h.as_bytes()).to_string()) + fn to_content_type(&self) -> Option<Cow<'_, str>> { + self.header("Content-Type") + } + + fn accepts(&self) -> Option<Cow<'_, str>> { + self.header("Accept") + } + + fn referer(&self) -> Option<Cow<'_, str>> { + self.header("Referer") } fn try_into_bytes( diff --git a/server_fn/src/request/axum.rs b/server_fn/src/request/axum.rs index 799c82ecc3..690eb9e5d6 100644 --- a/server_fn/src/request/axum.rs +++ b/server_fn/src/request/axum.rs @@ -1,18 +1,34 @@ use crate::{error::ServerFnError, request::Req}; use axum::body::{Body, Bytes}; use futures::{Stream, StreamExt}; -use http::{header::CONTENT_TYPE, Request}; +use http::{ + header::{ACCEPT, CONTENT_TYPE, REFERER}, + Request, +}; use http_body_util::BodyExt; +use std::borrow::Cow; impl<CustErr> Req<CustErr> for Request<Body> { fn as_query(&self) -> Option<&str> { self.uri().query() } - fn to_content_type(&self) -> Option<String> { + fn to_content_type(&self) -> Option<Cow<'_, str>> { self.headers() .get(CONTENT_TYPE) - .map(|h| String::from_utf8_lossy(h.as_bytes()).to_string()) + .map(|h| String::from_utf8_lossy(h.as_bytes())) + } + + fn accepts(&self) -> Option<Cow<'_, str>> { + self.headers() + .get(ACCEPT) + .map(|h| String::from_utf8_lossy(h.as_bytes())) + } + + fn referer(&self) -> Option<Cow<'_, str>> { + self.headers() + .get(REFERER) + .map(|h| String::from_utf8_lossy(h.as_bytes())) } async fn try_into_bytes(self) -> Result<Bytes, ServerFnError<CustErr>> { diff --git a/server_fn/src/request/mod.rs b/server_fn/src/request/mod.rs index 871a156585..eee926a3ba 100644 --- a/server_fn/src/request/mod.rs +++ b/server_fn/src/request/mod.rs @@ -1,7 +1,7 @@ use crate::error::ServerFnError; use bytes::Bytes; use futures::Stream; -use std::future::Future; +use std::{borrow::Cow, future::Future}; /// Request types for Actix. #[cfg(any(feature = "actix", doc))] @@ -73,7 +73,13 @@ where fn as_query(&self) -> Option<&str>; /// Returns the `Content-Type` header, if any. - fn to_content_type(&self) -> Option<String>; + fn to_content_type(&self) -> Option<Cow<'_, str>>; + + /// Returns the `Accepts` header, if any. + fn accepts(&self) -> Option<Cow<'_, str>>; + + /// Returns the `Referer` header, if any. + fn referer(&self) -> Option<Cow<'_, str>>; /// Attempts to extract the body of the request into [`Bytes`]. fn try_into_bytes( @@ -103,10 +109,17 @@ impl<CustErr> Req<CustErr> for BrowserMockReq { unreachable!() } - fn to_content_type(&self) -> Option<String> { + fn to_content_type(&self) -> Option<Cow<'_, str>> { unreachable!() } + fn accepts(&self) -> Option<Cow<'_, str>> { + unreachable!() + } + + fn referer(&self) -> Option<Cow<'_, str>> { + unreachable!() + } async fn try_into_bytes(self) -> Result<Bytes, ServerFnError<CustErr>> { unreachable!() } diff --git a/server_fn/src/response/actix.rs b/server_fn/src/response/actix.rs index 4f0ecb6cb5..711268e717 100644 --- a/server_fn/src/response/actix.rs +++ b/server_fn/src/response/actix.rs @@ -1,10 +1,13 @@ use super::Res; use crate::error::{ - ServerFnError, ServerFnErrorErr, ServerFnErrorSerde, ServerFnUrlError, - SERVER_FN_ERROR_HEADER, + ServerFnError, ServerFnErrorErr, ServerFnErrorSerde, SERVER_FN_ERROR_HEADER, }; use actix_web::{ - http::{header, StatusCode}, + http::{ + header, + header::{HeaderValue, LOCATION}, + StatusCode, + }, HttpResponse, }; use bytes::Bytes; @@ -77,11 +80,18 @@ where ))) } - fn error_response(path: &str, err: ServerFnError<CustErr>) -> Self { + fn error_response(path: &str, err: &ServerFnError<CustErr>) -> 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())), )) } + + fn redirect(&mut self, path: &str) { + if let Ok(path) = HeaderValue::from_str(path) { + *self.0.status_mut() = StatusCode::FOUND; + self.0.headers_mut().insert(LOCATION, path); + } + } } diff --git a/server_fn/src/response/http.rs b/server_fn/src/response/http.rs index ed6176bcd8..fa28515c5a 100644 --- a/server_fn/src/response/http.rs +++ b/server_fn/src/response/http.rs @@ -3,7 +3,7 @@ use crate::error::{ServerFnError, ServerFnErrorErr}; use axum::body::Body; use bytes::Bytes; use futures::{Stream, StreamExt}; -use http::Response; +use http::{header, HeaderValue, Response, StatusCode}; use std::fmt::{Debug, Display}; impl<CustErr> Res<CustErr> for Response<Body> @@ -50,10 +50,17 @@ where .map_err(|e| ServerFnError::Response(e.to_string())) } - fn error_response(path: &str, err: ServerFnError<CustErr>) -> Self { + fn error_response(path: &str, err: &ServerFnError<CustErr>) -> Self { Response::builder() .status(http::StatusCode::INTERNAL_SERVER_ERROR) .body(Body::from(err.to_string())) .unwrap() } + + fn redirect(&mut self, path: &str) { + if let Ok(path) = HeaderValue::from_str(path) { + self.headers_mut().insert(header::LOCATION, path); + *self.status_mut() = StatusCode::FOUND; + } + } } diff --git a/server_fn/src/response/mod.rs b/server_fn/src/response/mod.rs index 620b5395fa..6de74b1cc6 100644 --- a/server_fn/src/response/mod.rs +++ b/server_fn/src/response/mod.rs @@ -42,7 +42,10 @@ where ) -> Result<Self, ServerFnError<CustErr>>; /// 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<CustErr>) -> Self; + fn error_response(path: &str, err: &ServerFnError<CustErr>) -> 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. @@ -101,7 +104,7 @@ impl<CustErr> Res<CustErr> for BrowserMockRes { unreachable!() } - fn error_response(_path: &str, _err: ServerFnError<CustErr>) -> Self { + fn error_response(_path: &str, _err: &ServerFnError<CustErr>) -> Self { unreachable!() } @@ -109,6 +112,10 @@ impl<CustErr> Res<CustErr> for BrowserMockRes { _content_type: &str, _data: impl Stream<Item = Result<Bytes, ServerFnError<CustErr>>>, ) -> Result<Self, ServerFnError<CustErr>> { - todo!() + unreachable!() + } + + fn redirect(&mut self, _path: &str) { + unreachable!() } } From 13a2691806e358c47c09e2df6643db78b4b856ed Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Sun, 14 Jan 2024 16:17:06 -0500 Subject: [PATCH 052/100] working on server fn example --- examples/server_fns_axum/Cargo.toml | 89 ++++++++ examples/server_fns_axum/LICENSE | 21 ++ examples/server_fns_axum/Makefile.toml | 12 ++ examples/server_fns_axum/README.md | 19 ++ examples/server_fns_axum/Todos.db | Bin 0 -> 16384 bytes examples/server_fns_axum/e2e/Cargo.toml | 18 ++ examples/server_fns_axum/e2e/Makefile.toml | 20 ++ examples/server_fns_axum/e2e/README.md | 34 +++ .../e2e/features/add_todo.feature | 16 ++ .../e2e/features/delete_todo.feature | 18 ++ .../e2e/features/open_app.feature | 12 ++ .../server_fns_axum/e2e/tests/app_suite.rs | 14 ++ .../e2e/tests/fixtures/action.rs | 60 ++++++ .../e2e/tests/fixtures/check.rs | 57 +++++ .../e2e/tests/fixtures/find.rs | 63 ++++++ .../server_fns_axum/e2e/tests/fixtures/mod.rs | 4 + .../e2e/tests/fixtures/world/action_steps.rs | 57 +++++ .../e2e/tests/fixtures/world/check_steps.rs | 67 ++++++ .../e2e/tests/fixtures/world/mod.rs | 39 ++++ .../20221118172000_create_todo_table.sql | 7 + examples/server_fns_axum/public/favicon.ico | Bin 0 -> 15406 bytes examples/server_fns_axum/rust-toolchain.toml | 2 + examples/server_fns_axum/src/app.rs | 199 ++++++++++++++++++ .../server_fns_axum/src/error_template.rs | 57 +++++ examples/server_fns_axum/src/errors.rs | 21 ++ examples/server_fns_axum/src/fallback.rs | 50 +++++ examples/server_fns_axum/src/lib.rs | 15 ++ examples/server_fns_axum/src/main.rs | 38 ++++ examples/server_fns_axum/style.css | 3 + 29 files changed, 1012 insertions(+) create mode 100644 examples/server_fns_axum/Cargo.toml create mode 100644 examples/server_fns_axum/LICENSE create mode 100644 examples/server_fns_axum/Makefile.toml create mode 100644 examples/server_fns_axum/README.md create mode 100644 examples/server_fns_axum/Todos.db create mode 100644 examples/server_fns_axum/e2e/Cargo.toml create mode 100644 examples/server_fns_axum/e2e/Makefile.toml create mode 100644 examples/server_fns_axum/e2e/README.md create mode 100644 examples/server_fns_axum/e2e/features/add_todo.feature create mode 100644 examples/server_fns_axum/e2e/features/delete_todo.feature create mode 100644 examples/server_fns_axum/e2e/features/open_app.feature create mode 100644 examples/server_fns_axum/e2e/tests/app_suite.rs create mode 100644 examples/server_fns_axum/e2e/tests/fixtures/action.rs create mode 100644 examples/server_fns_axum/e2e/tests/fixtures/check.rs create mode 100644 examples/server_fns_axum/e2e/tests/fixtures/find.rs create mode 100644 examples/server_fns_axum/e2e/tests/fixtures/mod.rs create mode 100644 examples/server_fns_axum/e2e/tests/fixtures/world/action_steps.rs create mode 100644 examples/server_fns_axum/e2e/tests/fixtures/world/check_steps.rs create mode 100644 examples/server_fns_axum/e2e/tests/fixtures/world/mod.rs create mode 100644 examples/server_fns_axum/migrations/20221118172000_create_todo_table.sql create mode 100644 examples/server_fns_axum/public/favicon.ico create mode 100644 examples/server_fns_axum/rust-toolchain.toml create mode 100644 examples/server_fns_axum/src/app.rs create mode 100644 examples/server_fns_axum/src/error_template.rs create mode 100644 examples/server_fns_axum/src/errors.rs create mode 100644 examples/server_fns_axum/src/fallback.rs create mode 100644 examples/server_fns_axum/src/lib.rs create mode 100644 examples/server_fns_axum/src/main.rs create mode 100644 examples/server_fns_axum/style.css diff --git a/examples/server_fns_axum/Cargo.toml b/examples/server_fns_axum/Cargo.toml new file mode 100644 index 0000000000..4d518bb275 --- /dev/null +++ b/examples/server_fns_axum/Cargo.toml @@ -0,0 +1,89 @@ +[package] +name = "server_fns_axum" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +console_log = "1.0" +console_error_panic_hook = "0.1" +futures = "0.3" +http = "1.0" +leptos = { path = "../../leptos", features = ["nightly"] } +leptos_axum = { path = "../../integrations/axum", optional = true } +leptos_meta = { path = "../../meta", features = ["nightly"] } +leptos_router = { path = "../../router", features = ["nightly"] } +server_fn = { path = "../../server_fn", features = ["serde-lite", "rkyv", "multipart"] } +log = "0.4" +simple_logger = "4.0" +serde = { version = "1", features = ["derive"] } +axum = { version = "0.7", optional = true } +tower = { version = "0.4", optional = true } +tower-http = { version = "0.5", features = ["fs"], optional = true } +tokio = { version = "1", features = ["full"], optional = true } +thiserror = "1.0" +wasm-bindgen = "0.2" + +[features] +hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"] +ssr = [ + "dep:axum", + "dep:tower", + "dep:tower-http", + "dep:tokio", + "leptos/ssr", + "leptos_meta/ssr", + "leptos_router/ssr", + "dep:leptos_axum", +] + +[package.metadata.cargo-all-features] +denylist = ["axum", "tower", "tower-http", "tokio", "leptos_axum"] +skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]] + +[package.metadata.leptos] +# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name +output-name = "server_fns_axum" +# The site root folder is where cargo-leptos generate all output. WARNING: all content of this folder will be erased on a rebuild. Use it in your server setup. +site-root = "target/site" +# The site-root relative folder where all compiled output (JS, WASM and CSS) is written +# Defaults to pkg +site-pkg-dir = "pkg" +# [Optional] The source CSS file. If it ends with .sass or .scss then it will be compiled by dart-sass into CSS. The CSS is optimized by Lightning CSS before being written to <site-root>/<site-pkg>/app.css +style-file = "./style.css" +# [Optional] Files in the asset-dir will be copied to the site-root directory +assets-dir = "public" +# The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup. +site-addr = "127.0.0.1:3000" +# The port to use for automatic reload monitoring +reload-port = 3001 +# [Optional] Command to use when running end2end tests. It will run in the end2end dir. +end2end-cmd = "cargo make test-ui" +end2end-dir = "e2e" +# The browserlist query used for optimizing the CSS. +browserquery = "defaults" +# Set by cargo-leptos watch when building with tha tool. Controls whether autoreload JS will be included in the head +watch = false +# The environment Leptos will run in, usually either "DEV" or "PROD" +env = "DEV" +# The features to use when compiling the bin target +# +# Optional. Can be over-ridden with the command line parameter --bin-features +bin-features = ["ssr"] + +# If the --no-default-features flag should be used when compiling the bin target +# +# Optional. Defaults to false. +bin-default-features = false + +# The features to use when compiling the lib target +# +# Optional. Can be over-ridden with the command line parameter --lib-features +lib-features = ["hydrate"] + +# If the --no-default-features flag should be used when compiling the lib target +# +# Optional. Defaults to false. +lib-default-features = false diff --git a/examples/server_fns_axum/LICENSE b/examples/server_fns_axum/LICENSE new file mode 100644 index 0000000000..77d5625cb3 --- /dev/null +++ b/examples/server_fns_axum/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Greg Johnston + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/examples/server_fns_axum/Makefile.toml b/examples/server_fns_axum/Makefile.toml new file mode 100644 index 0000000000..d5fd86f317 --- /dev/null +++ b/examples/server_fns_axum/Makefile.toml @@ -0,0 +1,12 @@ +extend = [ + { path = "../cargo-make/main.toml" }, + { path = "../cargo-make/cargo-leptos-webdriver-test.toml" }, +] + +[env] +CLIENT_PROCESS_NAME = "todo_app_sqlite_axum" + +[tasks.test-ui] +cwd = "./e2e" +command = "cargo" +args = ["make", "test-ui", "${@}"] diff --git a/examples/server_fns_axum/README.md b/examples/server_fns_axum/README.md new file mode 100644 index 0000000000..b4f1639c47 --- /dev/null +++ b/examples/server_fns_axum/README.md @@ -0,0 +1,19 @@ +# Leptos Todo App Sqlite with Axum + +This example creates a basic todo app with an Axum backend that uses Leptos' server functions to call sqlx from the client and seamlessly run it on the server. + +## Getting Started + +See the [Examples README](../README.md) for setup and run instructions. + +## E2E Testing + +See the [E2E README](./e2e/README.md) for more information about the testing strategy. + +## Rendering + +See the [SSR Notes](../SSR_NOTES.md) for more information about Server Side Rendering. + +## Quick Start + +Run `cargo leptos watch` to run this example. diff --git a/examples/server_fns_axum/Todos.db b/examples/server_fns_axum/Todos.db new file mode 100644 index 0000000000000000000000000000000000000000..ec85d2b07f9ac6b3b931f4599e3e7a35107050f5 GIT binary patch literal 16384 zcmeI&KTH!*90%}sE!UPtcp%22i!UxpV!;*@34?ltqZCT3$6;jgtk;KLXgTOzl^7O` zi%w1&P5iUy>R^n6qiha3x#^%?j2nqYH{WY%qfKPP!F*ru_4U2q?_J-0uFExV_Dt6C zIa_cm<$}+KNe@vJa*8oRNLthmQJW<oKK$NSnt^TFG#RWs?xHRUb_WQ(+8v;S+hijI z0SG_<0uX=z1Rwwb2tWV=J1B5{AP|nl<H`-yFI+5f%Uda}T4m=_rQkd6lGol3jv2aU z>de$qS)H{<+2JS?<qEHOVlqo*(wUseP8*pS&6sD?`uvf4yvRMf;w;NAV5WY~WVyVk zbJ=Vo>@0bHp;Y2UOU5%Z`mCwVoNgttaeYFY%bIL#&M@?xX|+-s)4i%~bI)U`d_Jpd zxz@Z!)V|E^MXy?Bscb&g7UQejuF6$ezEkGS<u?}>9XJ^b#b)CQah8gFRX~YHXRA>4 z-Fn|@$FLIZBl7V%BJbsTND0ScF=c(Rj_$if*Q>t;{@|?-MWv}zl&*~;NcFUCwASA4 z>pOmlZ{?oVjIl}0ke6<|<>eChxjfSrO?04(ZjvCa5&DyUr8T-qKhT%-S@X4nKRcWn z0uX=z1Rwwb2tWV=5P$##AOL~C3551_klx7DwA$Z6Xk<#H9q5asCo`eFay&gL2EsIw zR`<y8gxV(?UG0_4nA#(oR3l|v4auI?CAtc|PsD)&0SG_<0uX=z1Rwwb2tWV=5P-lx z7bq%Wb!fP=6x?0xzy0QdUEu|>QD5!|VD&u%$)m|+JdubeMp$BGG<j?^aXkOz;mtc6 z2iJDJ*uT(!!kvFm`$i9K)IPudasKVQr?>9WPeWhsR@d?$gV&zFe`P!h=T@TPx)l1I khyw)z5P$##AOHafKmY;|fB*y_0D=EXpfjj;HvU%l1<a}70ssI2 literal 0 HcmV?d00001 diff --git a/examples/server_fns_axum/e2e/Cargo.toml b/examples/server_fns_axum/e2e/Cargo.toml new file mode 100644 index 0000000000..7224286755 --- /dev/null +++ b/examples/server_fns_axum/e2e/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "todo_app_sqlite_axum_e2e" +version = "0.1.0" +edition = "2021" + +[dev-dependencies] +anyhow = "1.0.72" +async-trait = "0.1.72" +cucumber = "0.19.1" +fantoccini = "0.19.3" +pretty_assertions = "1.4.0" +serde_json = "1.0.104" +tokio = { version = "1.29.1", features = ["macros", "rt-multi-thread", "time"] } +url = "2.4.0" + +[[test]] +name = "app_suite" +harness = false # Allow Cucumber to print output instead of libtest diff --git a/examples/server_fns_axum/e2e/Makefile.toml b/examples/server_fns_axum/e2e/Makefile.toml new file mode 100644 index 0000000000..cd76be24d5 --- /dev/null +++ b/examples/server_fns_axum/e2e/Makefile.toml @@ -0,0 +1,20 @@ +extend = { path = "../../cargo-make/main.toml" } + +[tasks.test] +env = { RUN_AUTOMATICALLY = false } +condition = { env_true = ["RUN_AUTOMATICALLY"] } + +[tasks.ci] + +[tasks.test-ui] +command = "cargo" +args = [ + "test", + "--test", + "app_suite", + "--", + "--retry", + "2", + "--fail-fast", + "${@}", +] diff --git a/examples/server_fns_axum/e2e/README.md b/examples/server_fns_axum/e2e/README.md new file mode 100644 index 0000000000..026f2befd3 --- /dev/null +++ b/examples/server_fns_axum/e2e/README.md @@ -0,0 +1,34 @@ +# E2E Testing + +This example demonstrates e2e testing with Rust using executable requirements. + +## Testing Stack + +| | Role | Description | +|---|---|---| +| [Cucumber](https://github.com/cucumber-rs/cucumber/tree/main) | Test Runner | Run [Gherkin](https://cucumber.io/docs/gherkin/reference/) specifications as Rust tests | +| [Fantoccini](https://github.com/jonhoo/fantoccini/tree/main) | Browser Client | Interact with web pages through WebDriver | +| [Cargo Leptos ](https://github.com/leptos-rs/cargo-leptos) | Build Tool | Compile example and start the server and end-2-end tests | +| [chromedriver](https://chromedriver.chromium.org/downloads) | WebDriver | Provide WebDriver for Chrome + +## Testing Organization + +Testing is organized around what a user can do and see/not see. Test scenarios are grouped by the **user action** and the **object** of that action. This makes it easier to locate and reason about requirements. + +Here is a brief overview of how things fit together. + +```bash +features +└── {action}_{object}.feature # Specify test scenarios +tests +├── fixtures +│ ├── action.rs # Perform a user action (click, type, etc.) +│ ├── check.rs # Assert what a user can see/not see +│ ├── find.rs # Query page elements +│ ├── mod.rs +│ └── world +│ ├── action_steps.rs # Map Gherkin steps to user actions +│ ├── check_steps.rs # Map Gherkin steps to user expectations +│ └── mod.rs +└── app_suite.rs # Test main +``` diff --git a/examples/server_fns_axum/e2e/features/add_todo.feature b/examples/server_fns_axum/e2e/features/add_todo.feature new file mode 100644 index 0000000000..b2a1331ca9 --- /dev/null +++ b/examples/server_fns_axum/e2e/features/add_todo.feature @@ -0,0 +1,16 @@ +@add_todo +Feature: Add Todo + + Background: + Given I see the app + + @add_todo-see + Scenario: Should see the todo + Given I set the todo as Buy Bread + When I click the Add button + Then I see the todo named Buy Bread + + @add_todo-style + Scenario: Should see the pending todo + When I add a todo as Buy Oranges + Then I see the pending todo diff --git a/examples/server_fns_axum/e2e/features/delete_todo.feature b/examples/server_fns_axum/e2e/features/delete_todo.feature new file mode 100644 index 0000000000..3c1e743d26 --- /dev/null +++ b/examples/server_fns_axum/e2e/features/delete_todo.feature @@ -0,0 +1,18 @@ +@delete_todo +Feature: Delete Todo + + Background: + Given I see the app + + @serial + @delete_todo-remove + Scenario: Should not see the deleted todo + Given I add a todo as Buy Yogurt + When I delete the todo named Buy Yogurt + Then I do not see the todo named Buy Yogurt + + @serial + @delete_todo-message + Scenario: Should see the empty list message + When I empty the todo list + Then I see the empty list message is No tasks were found. \ No newline at end of file diff --git a/examples/server_fns_axum/e2e/features/open_app.feature b/examples/server_fns_axum/e2e/features/open_app.feature new file mode 100644 index 0000000000..f4b4e39529 --- /dev/null +++ b/examples/server_fns_axum/e2e/features/open_app.feature @@ -0,0 +1,12 @@ +@open_app +Feature: Open App + + @open_app-title + Scenario: Should see the home page title + When I open the app + Then I see the page title is My Tasks + + @open_app-label + Scenario: Should see the input label + When I open the app + Then I see the label of the input is Add a Todo \ No newline at end of file diff --git a/examples/server_fns_axum/e2e/tests/app_suite.rs b/examples/server_fns_axum/e2e/tests/app_suite.rs new file mode 100644 index 0000000000..5c56b6aca8 --- /dev/null +++ b/examples/server_fns_axum/e2e/tests/app_suite.rs @@ -0,0 +1,14 @@ +mod fixtures; + +use anyhow::Result; +use cucumber::World; +use fixtures::world::AppWorld; + +#[tokio::main] +async fn main() -> Result<()> { + AppWorld::cucumber() + .fail_on_skipped() + .run_and_exit("./features") + .await; + Ok(()) +} diff --git a/examples/server_fns_axum/e2e/tests/fixtures/action.rs b/examples/server_fns_axum/e2e/tests/fixtures/action.rs new file mode 100644 index 0000000000..79b5c685ee --- /dev/null +++ b/examples/server_fns_axum/e2e/tests/fixtures/action.rs @@ -0,0 +1,60 @@ +use super::{find, world::HOST}; +use anyhow::Result; +use fantoccini::Client; +use std::result::Result::Ok; +use tokio::{self, time}; + +pub async fn goto_path(client: &Client, path: &str) -> Result<()> { + let url = format!("{}{}", HOST, path); + client.goto(&url).await?; + + Ok(()) +} + +pub async fn add_todo(client: &Client, text: &str) -> Result<()> { + fill_todo(client, text).await?; + click_add_button(client).await?; + Ok(()) +} + +pub async fn fill_todo(client: &Client, text: &str) -> Result<()> { + let textbox = find::todo_input(client).await; + textbox.send_keys(text).await?; + + Ok(()) +} + +pub async fn click_add_button(client: &Client) -> Result<()> { + let add_button = find::add_button(client).await; + add_button.click().await?; + + Ok(()) +} + +pub async fn empty_todo_list(client: &Client) -> Result<()> { + let todos = find::todos(client).await; + + for _todo in todos { + let _ = delete_first_todo(client).await?; + } + + Ok(()) +} + +pub async fn delete_first_todo(client: &Client) -> Result<()> { + if let Some(element) = find::first_delete_button(client).await { + element.click().await.expect("Failed to delete todo"); + time::sleep(time::Duration::from_millis(250)).await; + } + + Ok(()) +} + +pub async fn delete_todo(client: &Client, text: &str) -> Result<()> { + if let Some(element) = find::delete_button(client, text).await { + element.click().await?; + time::sleep(time::Duration::from_millis(250)).await; + } + + Ok(()) +} diff --git a/examples/server_fns_axum/e2e/tests/fixtures/check.rs b/examples/server_fns_axum/e2e/tests/fixtures/check.rs new file mode 100644 index 0000000000..f43629b95c --- /dev/null +++ b/examples/server_fns_axum/e2e/tests/fixtures/check.rs @@ -0,0 +1,57 @@ +use super::find; +use anyhow::{Ok, Result}; +use fantoccini::{Client, Locator}; +use pretty_assertions::assert_eq; + +pub async fn text_on_element( + client: &Client, + selector: &str, + expected_text: &str, +) -> Result<()> { + let element = client + .wait() + .for_element(Locator::Css(selector)) + .await + .expect( + format!("Element not found by Css selector `{}`", selector) + .as_str(), + ); + + let actual = element.text().await?; + assert_eq!(&actual, expected_text); + + Ok(()) +} + +pub async fn todo_present( + client: &Client, + text: &str, + expected: bool, +) -> Result<()> { + let todo_present = is_todo_present(client, text).await; + + assert_eq!(todo_present, expected); + + Ok(()) +} + +async fn is_todo_present(client: &Client, text: &str) -> bool { + let todos = find::todos(client).await; + + for todo in todos { + let todo_title = todo.text().await.expect("Todo title not found"); + if todo_title == text { + return true; + } + } + + false +} + +pub async fn todo_is_pending(client: &Client) -> Result<()> { + if let None = find::pending_todo(client).await { + assert!(false, "Pending todo not found"); + } + + Ok(()) +} diff --git a/examples/server_fns_axum/e2e/tests/fixtures/find.rs b/examples/server_fns_axum/e2e/tests/fixtures/find.rs new file mode 100644 index 0000000000..228fce6a28 --- /dev/null +++ b/examples/server_fns_axum/e2e/tests/fixtures/find.rs @@ -0,0 +1,63 @@ +use fantoccini::{elements::Element, Client, Locator}; + +pub async fn todo_input(client: &Client) -> Element { + let textbox = client + .wait() + .for_element(Locator::Css("input[name='title")) + .await + .expect("Todo textbox not found"); + + textbox +} + +pub async fn add_button(client: &Client) -> Element { + let button = client + .wait() + .for_element(Locator::Css("input[value='Add']")) + .await + .expect(""); + + button +} + +pub async fn first_delete_button(client: &Client) -> Option<Element> { + if let Ok(element) = client + .wait() + .for_element(Locator::Css("li:first-child input[value='X']")) + .await + { + return Some(element); + } + + None +} + +pub async fn delete_button(client: &Client, text: &str) -> Option<Element> { + let selector = format!("//*[text()='{text}']//input[@value='X']"); + if let Ok(element) = + client.wait().for_element(Locator::XPath(&selector)).await + { + return Some(element); + } + + None +} + +pub async fn pending_todo(client: &Client) -> Option<Element> { + if let Ok(element) = + client.wait().for_element(Locator::Css(".pending")).await + { + return Some(element); + } + + None +} + +pub async fn todos(client: &Client) -> Vec<Element> { + let todos = client + .find_all(Locator::Css("li")) + .await + .expect("Todo List not found"); + + todos +} diff --git a/examples/server_fns_axum/e2e/tests/fixtures/mod.rs b/examples/server_fns_axum/e2e/tests/fixtures/mod.rs new file mode 100644 index 0000000000..72b1bd65e4 --- /dev/null +++ b/examples/server_fns_axum/e2e/tests/fixtures/mod.rs @@ -0,0 +1,4 @@ +pub mod action; +pub mod check; +pub mod find; +pub mod world; diff --git a/examples/server_fns_axum/e2e/tests/fixtures/world/action_steps.rs b/examples/server_fns_axum/e2e/tests/fixtures/world/action_steps.rs new file mode 100644 index 0000000000..5c4e062dba --- /dev/null +++ b/examples/server_fns_axum/e2e/tests/fixtures/world/action_steps.rs @@ -0,0 +1,57 @@ +use crate::fixtures::{action, world::AppWorld}; +use anyhow::{Ok, Result}; +use cucumber::{given, when}; + +#[given("I see the app")] +#[when("I open the app")] +async fn i_open_the_app(world: &mut AppWorld) -> Result<()> { + let client = &world.client; + action::goto_path(client, "").await?; + + Ok(()) +} + +#[given(regex = "^I add a todo as (.*)$")] +#[when(regex = "^I add a todo as (.*)$")] +async fn i_add_a_todo_titled(world: &mut AppWorld, text: String) -> Result<()> { + let client = &world.client; + action::add_todo(client, text.as_str()).await?; + + Ok(()) +} + +#[given(regex = "^I set the todo as (.*)$")] +async fn i_set_the_todo_as(world: &mut AppWorld, text: String) -> Result<()> { + let client = &world.client; + action::fill_todo(client, &text).await?; + + Ok(()) +} + +#[when(regex = "I click the Add button$")] +async fn i_click_the_button(world: &mut AppWorld) -> Result<()> { + let client = &world.client; + action::click_add_button(client).await?; + + Ok(()) +} + +#[when(regex = "^I delete the todo named (.*)$")] +async fn i_delete_the_todo_named( + world: &mut AppWorld, + text: String, +) -> Result<()> { + let client = &world.client; + action::delete_todo(client, text.as_str()).await?; + + Ok(()) +} + +#[given("the todo list is empty")] +#[when("I empty the todo list")] +async fn i_empty_the_todo_list(world: &mut AppWorld) -> Result<()> { + let client = &world.client; + action::empty_todo_list(client).await?; + + Ok(()) +} diff --git a/examples/server_fns_axum/e2e/tests/fixtures/world/check_steps.rs b/examples/server_fns_axum/e2e/tests/fixtures/world/check_steps.rs new file mode 100644 index 0000000000..3e51215dba --- /dev/null +++ b/examples/server_fns_axum/e2e/tests/fixtures/world/check_steps.rs @@ -0,0 +1,67 @@ +use crate::fixtures::{check, world::AppWorld}; +use anyhow::{Ok, Result}; +use cucumber::then; + +#[then(regex = "^I see the page title is (.*)$")] +async fn i_see_the_page_title_is( + world: &mut AppWorld, + text: String, +) -> Result<()> { + let client = &world.client; + check::text_on_element(client, "h1", &text).await?; + + Ok(()) +} + +#[then(regex = "^I see the label of the input is (.*)$")] +async fn i_see_the_label_of_the_input_is( + world: &mut AppWorld, + text: String, +) -> Result<()> { + let client = &world.client; + check::text_on_element(client, "label", &text).await?; + + Ok(()) +} + +#[then(regex = "^I see the todo named (.*)$")] +async fn i_see_the_todo_is_present( + world: &mut AppWorld, + text: String, +) -> Result<()> { + let client = &world.client; + check::todo_present(client, text.as_str(), true).await?; + + Ok(()) +} + +#[then("I see the pending todo")] +async fn i_see_the_pending_todo(world: &mut AppWorld) -> Result<()> { + let client = &world.client; + + check::todo_is_pending(client).await?; + + Ok(()) +} + +#[then(regex = "^I see the empty list message is (.*)$")] +async fn i_see_the_empty_list_message_is( + world: &mut AppWorld, + text: String, +) -> Result<()> { + let client = &world.client; + check::text_on_element(client, "ul p", &text).await?; + + Ok(()) +} + +#[then(regex = "^I do not see the todo named (.*)$")] +async fn i_do_not_see_the_todo_is_present( + world: &mut AppWorld, + text: String, +) -> Result<()> { + let client = &world.client; + check::todo_present(client, text.as_str(), false).await?; + + Ok(()) +} diff --git a/examples/server_fns_axum/e2e/tests/fixtures/world/mod.rs b/examples/server_fns_axum/e2e/tests/fixtures/world/mod.rs new file mode 100644 index 0000000000..c25a925709 --- /dev/null +++ b/examples/server_fns_axum/e2e/tests/fixtures/world/mod.rs @@ -0,0 +1,39 @@ +pub mod action_steps; +pub mod check_steps; + +use anyhow::Result; +use cucumber::World; +use fantoccini::{ + error::NewSessionError, wd::Capabilities, Client, ClientBuilder, +}; + +pub const HOST: &str = "http://127.0.0.1:3000"; + +#[derive(Debug, World)] +#[world(init = Self::new)] +pub struct AppWorld { + pub client: Client, +} + +impl AppWorld { + async fn new() -> Result<Self, anyhow::Error> { + let webdriver_client = build_client().await?; + + Ok(Self { + client: webdriver_client, + }) + } +} + +async fn build_client() -> Result<Client, NewSessionError> { + let mut cap = Capabilities::new(); + let arg = serde_json::from_str("{\"args\": [\"-headless\"]}").unwrap(); + cap.insert("goog:chromeOptions".to_string(), arg); + + let client = ClientBuilder::native() + .capabilities(cap) + .connect("http://localhost:4444") + .await?; + + Ok(client) +} diff --git a/examples/server_fns_axum/migrations/20221118172000_create_todo_table.sql b/examples/server_fns_axum/migrations/20221118172000_create_todo_table.sql new file mode 100644 index 0000000000..3c2908e53c --- /dev/null +++ b/examples/server_fns_axum/migrations/20221118172000_create_todo_table.sql @@ -0,0 +1,7 @@ + +CREATE TABLE IF NOT EXISTS todos +( + id INTEGER NOT NULL PRIMARY KEY, + title VARCHAR, + completed BOOLEAN +); \ No newline at end of file diff --git a/examples/server_fns_axum/public/favicon.ico b/examples/server_fns_axum/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..2ba8527cb12f5f28f331b8d361eef560492d4c77 GIT binary patch literal 15406 zcmeHOd3aPs5`TblWD*3D%tXPJ#q(n!z$P=3gCjvf#a)E}a;Uf>h{pmVih!a-5LVO` zB?JrzEFicD0wRLo0iPfO372xnkvkzFlRHB)lcTnNZ}KK@US{UKN#b8?e_zkLy1RZ= zT~*y(-6IICgf>E_P6A)M3(wvl2qr-gx_5Ux-_uzT*6_Q&ee1v9B?vzS3&K5IhO2N5 z$9ukLN<`G>$$|GLnga~y%>f}*j%+w@(ixVUb^1_Gjoc;(?TrD3m2)RduFblVN)uy; zQAEd^T{5>-YYH%|Kv{V^cxHMBr1Ik<Vknc_c<}b#F7>7Frht$i<ZW=>mC`rqx@5*| z+OqN!xAjqmaU=qR$uGDMa7p!W9oZ+64($4xDk^FyFQ<_9Z`(;DLnB<;LLJD1<&vnZ zo0(>zIkQTse}qNMb6+i`th54(3pKm8;UAJ<_BULR*Z=m5FU7jiW(&#l+}WkHZ|e@1 z`pm;Q^pCuLUQUrnQ(hPM10pSSHQS=Bf8DqG1&!-B!oQQ|FuzLruL1w(+g<8&znyI? zzX-}?SwUvNjEuT?7uUOy{Fb@xKklpj+jdYM^IK9}NxvLRZd{l9FHEQJ4IO~q%4I0O zAN|*8x^nIU4Giw?f*tmNx=7H)2-Zn?J^B6SgpcW3ZXV_57Sn%Mtfr_=w|sYpAhdJT zcKo6Z*oIOU(az~3$LOEWm9Q)dYWMA}T7L23MVGqrcA%4H)+^`+=j+Hh8CTCnnG2Rh zgcXVW%F8$R9)6}f=NQiLPt8qt3xNUQI>Q*)H1lzk<&n?XR-f}tc&9V0H0lhGqHJ^N zN%h(9-Of2_)!Xk{qdIkU>1%mk%I_Id1!MU*yq&&>)Q+!L^t&-2<em6urrIod`3!;R z=Wl<Y!MEJ02?OvzHtZ7@%YC929AWK(Z|8caC7$l@-jj~(|6as!`sd>mW9Xq7g9C@* zl&PKJ&su2L+iku?Te?Pf?k3tUK){Bj_gb&aPo8Ago^XI~mRTd(5{&^tf1)!-lSMha z@$~ae!r(~`=p&|mMxy2EiZQ6FvXb(1avS*`Pj%$)*?vwceGKHmHnl`v&fEQ_Wh+G) zEPQ^3&oV%}%;zF`AM|S%d<Va?d!s3R8`U?Gc5T=!(zn}eR8m+-=T4lW_5WVR<Fo5n zEY(q7nO2p&AJ`YJQkGFZM|ZiEHhi$0s;jF-+BRDqrKeX@24}myPJCBwucrUJ{}Dk~ zmGza+>>pM@1}33PN5*4SewROk_K$n^i8QjaYiRzwG8#OvVIF|{x85wH+?*P*%)woI zR538k@=(E`V;p1UwA|fqSh`$n_t;Sz4T)`_s~pRR4lbmWWSdxa-FqLZ%fLT)B<thH z?r2KyM)!-N8kRZu_C{O60t~siHB@c4H=0*TZAw>h?iye?COx~mO1wkn5)HNMg7`8~ z25VJhz&3Z7`M>6luJrEw$<qW~i<R}a8vT1?EUc$>Jikft+6SxyIh?)PU1?DfrKMGC z=3T;;omE4H`PWqF8?0*dOA3o9y@~WK`S}{?tIHquEw?v`M^D%Lobpdrp%3}1=-&qk zqAtb1p<Vype#@bMYnCkaPT!$_UhNP57CtYBean!+o^4-}#r^jd($%XqWVhLAL@$#X z{RH+uV<cVobcJt6N<MC*p<Xb6_K6gS|5>x-1Fy6}E8IUg4s%8B0~P<<jSlYKD`J3; zT`<3l?j6)13-tFwmO1!F`hMqbic%Q^Sntcigq!`uF(AN@<cW9beRP*@;@ASeh6MZ0 zVjDxoJrZONzSU@>P5C;de%@n~XnDKF@fr$a+^@$^P|>vlw($aSK2lRtLt~8tRb`I0 znfI!G?K|<5ry*gk>y56rZy0NkK6)))6Mg1=K?7yS9p+#1Ij=W*%5Rt-mlc;#MOnE9 zoi`-+6oj@)`gq2Af!B+9%J#K9V=ji2dj2<_qaLSXOCeqQ<t!!6U*w!8vfOi(a#!Mr z;I(g4F=Sl2B4U6(W@gq<Rh2_8&+wXIkfBE|uhE!w^@O<@+vF)Nd`o5Cob-Z7`<F9z z8veIJalFD@;Jf`*c%O1MCB;SG)KJvx!(x`1Cc8NL{Xwd$tD{jPikxF*l&PR<NNnLk zrtfiGu7(3T!9H>&<0zMSb$5mAi;HU=v`v<>NYk}MbD!ewYVB+N-ctzn=l&bTwv)*7 zmY<+Y@SBbtl9PPk$HTR?ln@(T92XjTRj0Mx|Mzl;lW>Su_y^~fh?8(L?oz8h!cCpb zZG-OY=NJ3{>r*`U<(J%#zjFT-a9>u6+23H{=d(utkgqt7@^)C;pkb)fQ|Q=*8*SyT z;otKe+f8fEp)ZacKZDn3TNzs><E(S1|L)$QEc^6q=5?o0r=Mx`3x=rM{GOPlwi$N_ z=T)5Zax*gRvmbw&B5%4y)A;0p7d!Kl&vD^Re-S$0D$!}_t5FCD4A<$WFL`Npar$qU z#I+amK;<Q+v}o!~8mM=Tce=x>_Kx+g*c_mr8LBhr8GnoEmAQk#%sR52<z$1b$A%B2 zt*h3G^AqrZxXIdgm(qRR?rw5FIC<lBlgZz(H>`bdbW8Ms$<Hvc-WFZ-8}a3r$4n6C zTHwJ}W#d>!0u2bdt=T-lK3JbDW`F(Urt%Ob2seiN>7U`YN}aOdIiCC;eeufJC#m3S z9#|l2c?G@t*hH5y^76jkv)rs4H+;oiTuY5FQwRMN_7NUqeiD|b&RyxPXQz|3qC(_> zZJMwjC4F!1m2INXqzisQ4X^w=>&(+Ecdu&~IWEMn7f*YcYI&eWI(6hI#f114%aymM zyhlG6{q>XN7(LyGiMAS&qijR%d2rV|>AUT_sE&EKUSTCM26>aKzNxk0?K|utOcxl# zxIOwM#O!!H+QzbX*&p=QuKe4y;bS>&StQOE5AEGg_ubk8{;1yOVAJfE_Js-lL7rr9 z)CEuFIlkApj~uV^zJK7KocjT=<q6S4TFeeWi+QLv1?XqE!ynFLANNs-5|h?vU?0)j zM4v5B5^VrK7~8JpUZo4C&d`?}TSjajvE=6XShnY)K0=qS3Le_<yvpOAq8bn5%`u|y zPrdLP)$)4VQ-XbGUQVTrOB3m_h}b6g4bPcYGyc{R58*d<?!`8qu7*?j9sYmTxTh#5 zJ;UjHfW4;15ko(G*hYsBRZ;4dYK~<%d=tKdkG!lLik~u#A~o*4xzOgdw6Q~Acs>4B zJP(}0x}|A7C$$5gIp>K<R9aLFNMfwz%H?WWzN~^Ce#o)Tlwx1F;@z?jE9lZi=B0jL zpp5lv-o)p8=7F)=S&!y1{#ICj@%<(Vm)5Hs`}ON}v}vQ2#*A!Oqsp<%??*mTNE_E% zsj||shA(A*HFv@@KI;<uqTN_~g!u*C%(|1M6*tNuo|Jjn5mTtFtfl!J1059I5O<T~ zb$5@lug@tZ@Qsw0l}!`+`{t_{b4~>BPZ|A#2Ew;$#g9Fk)r;Q~?G$>x<+JM)J3u>j zi68K=I;ld`JJ<u}v;BD=QY#K%4r`|$!sGJmTI-<PuscG<SQ6xxl~qk+MyczJgjsoo zF2S~u;Fr|m!b+TMw{Nf>?Nq+^_B?C+Q%+x#m{9JF$tbaDeNIep%=^#>KHGtg=L)>m z_J&vaZTs2{qP!4Gdw5u5Kcf}5R4(q}Lebx%(J$7l*Q`Il#pCTM%!`y5y*-~zIVs}D z9;t+(xmV~R65^ZQXe+<5{$QW0O8MT~a{<o>kdFLR)nfRMA9L(YU>x*DTltN#m-2km zC;T`cfb{c`mcx(z7o_a8bYJn8_^dz4Cq!D<UvIA7NcFR`9r}YaEJ_)BduHF0!#bpT zHbdUV)>Z37{P6uF{@#519UWK1{>(9sZB1I^6MmNc39MJ-_|)!S8vO+O3&$MulU3Gc z_W{N*B(yneyl-oN_MKaJ{CZ6dv-~^8uPbLSh&0jfV@EfA{2Dc!_rOyfx`R0T@LonA z<*%O?-aa_Wm-z$s@K(ex7UhM0-?9C=PkYdk&d2n((E4>&(f4D`fOQY%CURMMyJyU` zVeJBAId&StHjw76tnwSqZs3e0683`L{a3k9JYdg#(ZVw4J`&CkV-2LF<L#N}Z<*NN zte-!B>aDE1Z?CehVy%vZx$tM3tTax8E@2;N^QTrPcI?Ob8uK!DM0_sfE6ks<hCyt_ z*KrJMFT@*)3M?WI&?W3suUlhv*{hQD1-L(1T_NuG!*?Np|Cx_Y@Hu|XSZ(Y(=V1K; z{9$ba?_qvY-O1V8JMi#g+<3O<^G=@xT;K&~eX!?`j58+^W_-uFp|lGZ#q}H7@J7Sk zH^!ff^N7G+pIT%8%UxM5@35Z1UiD=KAHXV4*hkFaF&6vmTKCl5(PykljN7?>2M?iw zPS4{(k-PF*-oY<D(4!YkA2Ck!B_|IZ5wT{cWl;LX%i<XX;QxB_=RThkmD6XtEx=wx zz1&?cYzLElwF7zEp6(yItFPEM=nKqo#)J>>S!d9;L+|xdTtLen9B2LvpL4k;#ScB< z$NP_7j~7)5eXuoYEk*dK_rSz9yT_C4B{r~^#^o}-VQI=Y?01|$aa!a7=UEm$|DsQQ zfLK1qmho2@)nwA?$1%T6jwO2HZ({6&;`s|OQOxI4S8*Hw=Qp!b(<qJi)V|^WJ78Ra z9Yx4u*Nmk&To*J}6}YT`*-t;2d2Z5~5l{FL_gu+y5A2tDN;rpf_?vH?@f5~hVDk5@ z^D@ZF+ctdW<gu3S2gJRs<>gNJR%<PtJYl1A=j_h&y08)K<2=$cyn=novkdG8B{;3m zX6`q(hYaSU*)~0t&l4pdJS1Yb@w{p07nStjkcKq`=CX*F+U*>SAj&wGa>^&2@x)Vj zhd^WfzJ^b0O{E^q82Pw({uT`E`MT2WnZ02{E%t*yRPN>?W>0vU^4@Vyh4;mLj918c z*s*papo?<}cQM{5lcgZScx}?usg{mS!KkH9U%@|^_33?{FI{1ss+8kXyFY&5M-e~f zM$){FF;_+z3sNJ)Er~{Beux$fEl{R4|7WKcpEsGtK57f+H0DJ$hI;U;JtF>+lG@sV zQI_;bQ^7XIJ>Bs?C32b1v;am;P4GUqAJ#zOHv}4SmV|xXX6~O9&e_~YCCpbT>s$`! k<4FtN!5<V};cOZ-C_kkBJe0@%L`?wxDD;N}njwMz0Wv;ivH$=8 literal 0 HcmV?d00001 diff --git a/examples/server_fns_axum/rust-toolchain.toml b/examples/server_fns_axum/rust-toolchain.toml new file mode 100644 index 0000000000..5d56faf9ae --- /dev/null +++ b/examples/server_fns_axum/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +channel = "nightly" diff --git a/examples/server_fns_axum/src/app.rs b/examples/server_fns_axum/src/app.rs new file mode 100644 index 0000000000..ddfc25510f --- /dev/null +++ b/examples/server_fns_axum/src/app.rs @@ -0,0 +1,199 @@ +use crate::error_template::ErrorTemplate; +use leptos::{html::Input, *}; +use leptos_meta::*; +use leptos_router::*; +use serde::{Deserialize, Serialize}; +use server_fn::codec::SerdeLite; +#[cfg(feature = "ssr")] +use std::sync::{ + atomic::{AtomicU8, Ordering}, + Mutex, +}; + +#[component] +pub fn TodoApp() -> impl IntoView { + provide_meta_context(); + + view! { + <Link rel="shortcut icon" type_="image/ico" href="/favicon.ico"/> + <Stylesheet id="leptos" href="/pkg/server_fns_axum.css"/> + <Router> + <header> + <h1>"Server Function Demo"</h1> + </header> + <main> + <Routes> + <Route path="" view=HomePage/> + </Routes> + </main> + </Router> + } +} + +#[component] +pub fn HomePage() -> impl IntoView { + view! { + <h2>"Some Simple Server Functions"</h2> + <SpawnLocal/> + <WithAnAction/> + <WithActionForm/> + } +} + +/// A server function is really just an API call to your server. But it provides a plain async +/// function as a wrapper around that. This means you can call it like any other async code, just +/// by spawning a task with `spawn_local`. +/// +/// In reality, you usually want to use a resource to load data from the server or an action to +/// mutate data on the server. But a simple `spawn_local` can make it more obvious what's going on. +#[component] +pub fn SpawnLocal() -> impl IntoView { + /// A basic server function can be called like any other async function. + /// + /// You can define a server function at any scope. This one, for example, is only available + /// inside the SpawnLocal component. **However**, note that all server functions are publicly + /// available API endpoints: This scoping means you can only call this server function + /// from inside this component, but it is still available at its URL to any caller, from within + /// your app or elsewhere. + #[server] + pub async fn shouting_text(input: String) -> Result<String, ServerFnError> { + // insert a simulated wait + tokio::time::sleep(std::time::Duration::from_millis(250)).await; + Ok(input.to_ascii_uppercase()) + } + + let input_ref = NodeRef::<Input>::new(); + let (shout_result, set_shout_result) = + create_signal("Click me".to_string()); + + view! { + <h3>Using <code>spawn_local</code></h3> + <p> + "You can call a server function by using "<code>"spawn_local"</code> " in an event listener. " + "Clicking this button should alert with the uppercase version of the input." + </p> + <input node_ref=input_ref placeholder="Type something here."/> + <button + on:click=move |_| { + let value = input_ref.get().unwrap().value(); + spawn_local(async move { + let uppercase_text = shouting_text(value).await.unwrap_or_else(|e| e.to_string()); + set_shout_result(uppercase_text); + }); + } + > + {shout_result} + </button> + } +} + +/// Pretend this is a database and we're storing some rows in memory! +/// This exists only on the server. +#[cfg(feature = "ssr")] +static ROWS: Mutex<Vec<String>> = Mutex::new(Vec::new()); + +/// Imagine this server function mutates some state on the server, like a database row. +/// Every third time, it will return an error. +/// +/// This kind of mutation is often best handled by an Action. +/// Remember, if you're loading data, use a resource; if you're running an occasional action, +/// use an action. +#[server] +pub async fn add_row(text: String) -> Result<usize, ServerFnError> { + static N: AtomicU8 = AtomicU8::new(0); + + // insert a simulated wait + tokio::time::sleep(std::time::Duration::from_millis(250)).await; + + let nth_run = N.fetch_add(1, Ordering::Relaxed); + // this will print on the server, like any server function + println!("Adding {text:?} to the database!"); + if nth_run % 3 == 2 { + Err(ServerFnError::new("Oh no! Couldn't add to database!")) + } else { + let mut rows = ROWS.lock().unwrap(); + rows.push(text); + Ok(rows.len()) + } +} + +/// Simply returns the number of rows. +#[server] +pub async fn get_rows() -> Result<usize, ServerFnError> { + // insert a simulated wait + tokio::time::sleep(std::time::Duration::from_millis(250)).await; + + Ok(ROWS.lock().unwrap().len()) +} + +/// An action abstracts over the process of spawning a future and setting a signal when it +/// resolves. Its .input() signal holds the most recent argument while it's still pending, +/// and its .value() signal holds the most recent result. Its .version() signal can be fed +/// into a resource, telling it to refetch whenever the action has successfully resolved. +/// +/// This makes actions useful for mutations, i.e., some server function that invalidates +/// loaded previously loaded from another server function. +#[component] +pub fn WithAnAction() -> impl IntoView { + let input_ref = NodeRef::<Input>::new(); + + // a server action can be created by using the server function's type name as a generic + // the type name defaults to the PascalCased function name + let action = create_server_action::<AddRow>(); + + // this resource will hold the total number of rows + // passing it action.version() means it will refetch whenever the action resolves successfully + let row_count = create_resource(action.version(), |_| get_rows()); + + view! { + <h3>Using <code>create_action</code></h3> + <p> + "Some server functions are conceptually \"mutations,\", which change something on the server. " + "These often work well as actions." + </p> + <input node_ref=input_ref placeholder="Type something here."/> + <button + on:click=move |_| { + let text = input_ref.get().unwrap().value(); + action.dispatch(AddRow { text }); + } + > + Submit + </button> + <p>You submitted: {move || format!("{:?}", action.input().get())}</p> + <p>The result was: {move || format!("{:?}", action.value().get())}</p> + <Transition> + <p>Total rows: {row_count}</p> + </Transition> + } +} + +/// An <ActionForm/> lets you do the same thing as dispatching an action, but automates the +/// creation of the dispatched argument struct using a <form>. This means it also gracefully +/// degrades well when JS/WASM are not available. +#[component] +pub fn WithActionForm() -> impl IntoView { + let action = create_server_action::<AddRow>(); + let row_count = create_resource(action.version(), |_| get_rows()); + + view! { + <h3>Using <code>create_action</code></h3> + <p> + <code>"<ActionForm/>"</code> "lets you use an HTML " <code>"<form>"</code> + "to call a server function in a way that gracefully degrades." + </p> + <ActionForm action> + <input + // the `name` of the input corresponds to the argument name + name="text" + placeholder="Type something here." + /> + <button> Submit </button> + </ActionForm> + <p>You submitted: {move || format!("{:?}", action.input().get())}</p> + <p>The result was: {move || format!("{:?}", action.value().get())}</p> + <Transition> + <p>Total rows: {row_count}</p> + </Transition> + } +} diff --git a/examples/server_fns_axum/src/error_template.rs b/examples/server_fns_axum/src/error_template.rs new file mode 100644 index 0000000000..0a1731abe1 --- /dev/null +++ b/examples/server_fns_axum/src/error_template.rs @@ -0,0 +1,57 @@ +use crate::errors::TodoAppError; +use leptos::{Errors, *}; +#[cfg(feature = "ssr")] +use leptos_axum::ResponseOptions; + +// A basic function to display errors served by the error boundaries. Feel free to do more complicated things +// here than just displaying them +#[component] +pub fn ErrorTemplate( + #[prop(optional)] outside_errors: Option<Errors>, + #[prop(optional)] errors: Option<RwSignal<Errors>>, +) -> impl IntoView { + let errors = match outside_errors { + Some(e) => create_rw_signal(e), + None => match errors { + Some(e) => e, + None => panic!("No Errors found and we expected errors!"), + }, + }; + + // Get Errors from Signal + // Downcast lets us take a type that implements `std::error::Error` + let errors: Vec<TodoAppError> = errors + .get() + .into_iter() + .filter_map(|(_, v)| v.downcast_ref::<TodoAppError>().cloned()) + .collect(); + + // Only the response code for the first error is actually sent from the server + // this may be customized by the specific application + #[cfg(feature = "ssr")] + { + let response = use_context::<ResponseOptions>(); + if let Some(response) = response { + response.set_status(errors[0].status_code()); + } + } + + view! { + <h1>"Errors"</h1> + <For + // a function that returns the items we're iterating over; a signal is fine + each= move || {errors.clone().into_iter().enumerate()} + // a unique key for each item as a reference + key=|(index, _error)| *index + // renders each item to a view + children=move |error| { + let error_string = error.1.to_string(); + let error_code= error.1.status_code(); + view! { + <h2>{error_code.to_string()}</h2> + <p>"Error: " {error_string}</p> + } + } + /> + } +} diff --git a/examples/server_fns_axum/src/errors.rs b/examples/server_fns_axum/src/errors.rs new file mode 100644 index 0000000000..1f1aea92fa --- /dev/null +++ b/examples/server_fns_axum/src/errors.rs @@ -0,0 +1,21 @@ +use http::status::StatusCode; +use thiserror::Error; + +#[derive(Debug, Clone, Error)] +pub enum TodoAppError { + #[error("Not Found")] + NotFound, + #[error("Internal Server Error")] + InternalServerError, +} + +impl TodoAppError { + pub fn status_code(&self) -> StatusCode { + match self { + TodoAppError::NotFound => StatusCode::NOT_FOUND, + TodoAppError::InternalServerError => { + StatusCode::INTERNAL_SERVER_ERROR + } + } + } +} diff --git a/examples/server_fns_axum/src/fallback.rs b/examples/server_fns_axum/src/fallback.rs new file mode 100644 index 0000000000..66b2a5ffe0 --- /dev/null +++ b/examples/server_fns_axum/src/fallback.rs @@ -0,0 +1,50 @@ +use crate::{error_template::ErrorTemplate, errors::TodoAppError}; +use axum::{ + body::Body, + extract::State, + http::{Request, Response, StatusCode, Uri}, + response::{IntoResponse, Response as AxumResponse}, +}; +use leptos::{view, Errors, LeptosOptions}; +use tower::ServiceExt; +use tower_http::services::ServeDir; + +pub async fn file_and_error_handler( + uri: Uri, + State(options): State<LeptosOptions>, + req: Request<Body>, +) -> AxumResponse { + let root = options.site_root.clone(); + let res = get_static_file(uri.clone(), &root).await.unwrap(); + + if res.status() == StatusCode::OK { + res.into_response() + } else { + let mut errors = Errors::default(); + errors.insert_with_default_key(TodoAppError::NotFound); + let handler = leptos_axum::render_app_to_stream( + options.to_owned(), + move || view! {<ErrorTemplate outside_errors=errors.clone()/>}, + ); + handler(req).await.into_response() + } +} + +async fn get_static_file( + uri: Uri, + root: &str, +) -> Result<Response<Body>, (StatusCode, String)> { + let req = Request::builder() + .uri(uri.clone()) + .body(Body::empty()) + .unwrap(); + // `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot` + // This path is relative to the cargo root + match ServeDir::new(root).oneshot(req).await { + Ok(res) => Ok(res.into_response()), + Err(err) => Err(( + StatusCode::INTERNAL_SERVER_ERROR, + format!("Something went wrong: {err}"), + )), + } +} diff --git a/examples/server_fns_axum/src/lib.rs b/examples/server_fns_axum/src/lib.rs new file mode 100644 index 0000000000..36750be4e4 --- /dev/null +++ b/examples/server_fns_axum/src/lib.rs @@ -0,0 +1,15 @@ +pub mod app; +pub mod error_template; +pub mod errors; +#[cfg(feature = "ssr")] +pub mod fallback; + +#[wasm_bindgen::prelude::wasm_bindgen] +pub fn hydrate() { + use crate::app::TodoApp; + + _ = console_log::init_with_level(log::Level::Error); + console_error_panic_hook::set_once(); + + leptos::mount_to_body(TodoApp); +} diff --git a/examples/server_fns_axum/src/main.rs b/examples/server_fns_axum/src/main.rs new file mode 100644 index 0000000000..ba03453b4a --- /dev/null +++ b/examples/server_fns_axum/src/main.rs @@ -0,0 +1,38 @@ +use crate::{fallback::file_and_error_handler, app::*}; +use axum::{ + body::Body, + extract::{Path, State}, + http::Request, + response::{IntoResponse, Response}, + routing::get, + Router, +}; +use leptos::*; +use leptos_axum::{generate_route_list, LeptosRoutes}; +use server_fns_axum::*; + +#[tokio::main] +async fn main() { + simple_logger::init_with_level(log::Level::Error) + .expect("couldn't initialize logging"); + + // Setting this to None means we'll be using cargo-leptos and its env vars + let conf = get_configuration(None).await.unwrap(); + let leptos_options = conf.leptos_options; + let addr = leptos_options.site_addr; + let routes = generate_route_list(TodoApp); + + // build our application with a route + let app = Router::new() + .leptos_routes(&leptos_options, routes, || view! { <TodoApp/> }) + .fallback(file_and_error_handler) + .with_state(leptos_options); + + // run our app with hyper + // `axum::Server` is a re-export of `hyper::Server` + let listener = tokio::net::TcpListener::bind(&addr).await.unwrap(); + logging::log!("listening on http://{}", &addr); + axum::serve(listener, app.into_make_service()) + .await + .unwrap(); +} diff --git a/examples/server_fns_axum/style.css b/examples/server_fns_axum/style.css new file mode 100644 index 0000000000..9ba7149f99 --- /dev/null +++ b/examples/server_fns_axum/style.css @@ -0,0 +1,3 @@ +.pending { + color: purple; +} From 90ba3529e992bf2502e2705f2a0c2cf379f3d418 Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Sun, 14 Jan 2024 16:17:24 -0500 Subject: [PATCH 053/100] working on Axum version --- server_fn/src/middleware/mod.rs | 2 +- server_fn/src/response/http.rs | 14 ++++++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/server_fn/src/middleware/mod.rs b/server_fn/src/middleware/mod.rs index 9c23e8582e..dfbb897866 100644 --- a/server_fn/src/middleware/mod.rs +++ b/server_fn/src/middleware/mod.rs @@ -52,7 +52,7 @@ mod axum { let inner = self.call(req); Box::pin(async move { inner.await.unwrap_or_else(|e| { - let err = ServerFnError::from(e); + let err = ServerFnError::new(e); Response::<Body>::error_response(&path, &err) }) }) diff --git a/server_fn/src/response/http.rs b/server_fn/src/response/http.rs index fa28515c5a..e8117f75cc 100644 --- a/server_fn/src/response/http.rs +++ b/server_fn/src/response/http.rs @@ -1,14 +1,19 @@ use super::Res; -use crate::error::{ServerFnError, ServerFnErrorErr}; +use crate::error::{ + ServerFnError, ServerFnErrorErr, ServerFnErrorSerde, SERVER_FN_ERROR_HEADER, +}; use axum::body::Body; use bytes::Bytes; use futures::{Stream, StreamExt}; use http::{header, HeaderValue, Response, StatusCode}; -use std::fmt::{Debug, Display}; +use std::{ + fmt::{Debug, Display}, + str::FromStr, +}; impl<CustErr> Res<CustErr> for Response<Body> where - CustErr: Send + Sync + Debug + Display + 'static, + CustErr: Send + Sync + Debug + FromStr + Display + 'static, { fn try_from_string( content_type: &str, @@ -53,7 +58,8 @@ where fn error_response(path: &str, err: &ServerFnError<CustErr>) -> Self { Response::builder() .status(http::StatusCode::INTERNAL_SERVER_ERROR) - .body(Body::from(err.to_string())) + .header(SERVER_FN_ERROR_HEADER, path) + .body(err.ser().unwrap_or_else(|_| err.to_string()).into()) .unwrap() } From 06c478b7cb506c7bb4db4a3c5559b93c6bea9540 Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Sun, 14 Jan 2024 20:19:47 -0500 Subject: [PATCH 054/100] feature-gate the form redirect stuff, and clear old errors from query --- leptos/Cargo.toml | 2 +- server_fn/Cargo.toml | 1 + server_fn/src/error.rs | 25 ++++++++++++++++++++++++- server_fn/src/lib.rs | 17 +++++++++++++---- 4 files changed, 39 insertions(+), 6 deletions(-) diff --git a/leptos/Cargo.toml b/leptos/Cargo.toml index 17f287f2a2..f27a3f4502 100644 --- a/leptos/Cargo.toml +++ b/leptos/Cargo.toml @@ -20,7 +20,7 @@ typed-builder = "0.18" typed-builder-macro = "0.18" serde = { version = "1", optional = true } serde_json = { version = "1", optional = true } -server_fn = { workspace = true, features = ["browser", "url", "cbor"] } +server_fn = { workspace = true, features = ["form-redirects", "browser", "url", "cbor"] } web-sys = { version = "0.3.63", features = [ "ShadowRoot", "ShadowRootInit", diff --git a/server_fn/Cargo.toml b/server_fn/Cargo.toml index aa1394ceb2..e143c62c8b 100644 --- a/server_fn/Cargo.toml +++ b/server_fn/Cargo.toml @@ -68,6 +68,7 @@ url = "2" [features] default = [ "json", "cbor"] +form-redirects = [] actix = ["ssr", "dep:actix-web", "dep:send_wrapper"] axum = [ "ssr", diff --git a/server_fn/src/error.rs b/server_fn/src/error.rs index ab5b57fedf..5352a8d598 100644 --- a/server_fn/src/error.rs +++ b/server_fn/src/error.rs @@ -408,7 +408,7 @@ impl<CustErr> From<ServerFnError<CustErr>> for ServerFnErrorErr<CustErr> { /// found at a particular path. /// /// This can be used to pass an error from the server back to the client -/// without JavaScript/WASM supported, by encoding it in the URL as a qurey string. +/// 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<CustErr> { @@ -450,6 +450,29 @@ impl<CustErr> ServerFnUrlError<CustErr> { ); Ok(url) } + + /// Replaces any ServerFnUrlError info from the URL in the given string + /// with the serialized success value given. + pub fn strip_error_info(path: &mut String) { + if let Ok(mut url) = Url::parse(&*path) { + // NOTE: This is gross, but the Serializer you get from + // .query_pairs_mut() isn't an Iterator so you can't just .retain(). + let pairs_previously = url + .query_pairs() + .map(|(k, v)| (k.to_string(), v.to_string())) + .collect::<Vec<_>>(); + let mut pairs = url.query_pairs_mut(); + pairs.clear(); + for (key, value) in pairs_previously + .into_iter() + .filter(|(key, _)| key != "__path" && key != "__err") + { + pairs.append_pair(&key, &value); + } + drop(pairs); + *path = url.to_string(); + } + } } impl<CustErr> From<ServerFnUrlError<CustErr>> for ServerFnError<CustErr> { diff --git a/server_fn/src/lib.rs b/server_fn/src/lib.rs index b6986020ec..780f5cc5c1 100644 --- a/server_fn/src/lib.rs +++ b/server_fn/src/lib.rs @@ -238,10 +238,13 @@ where // Server functions can either be called by a real Client, // or directly by an HTML <form>. If they're accessed by a <form>, default to // redirecting back to the Referer. - let accepts_html = req - .accepts() - .map(|n| n.contains("text/html")) - .unwrap_or(false); + let accepts_html = if cfg!(feature = "form-redirects") { + req.accepts() + .map(|n| n.contains("text/html")) + .unwrap_or(false) + } else { + false + }; let mut referer = req.referer().as_deref().map(ToOwned::to_owned); async move { @@ -256,6 +259,7 @@ where }); // if it accepts HTML, we'll redirect to the Referer + #[cfg(feature = "form-redirects")] if accepts_html { // if it had an error, encode that error in the URL if let Some(err) = err { @@ -265,6 +269,11 @@ where referer = Some(url.to_string()); } } + // otherwise, strip error info from referer URL, as that means it's from a previous + // call + else if let Some(referer) = referer.as_mut() { + ServerFnUrlError::<Self::Error>::strip_error_info(referer) + } // set the status code and Location header res.redirect(referer.as_deref().unwrap_or("/")); From 0571ebbc363479fbdc699339ce10e6275cbac1c2 Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Sun, 14 Jan 2024 20:19:56 -0500 Subject: [PATCH 055/100] working on example --- examples/server_fns_axum/src/app.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/examples/server_fns_axum/src/app.rs b/examples/server_fns_axum/src/app.rs index ddfc25510f..27a6b43a79 100644 --- a/examples/server_fns_axum/src/app.rs +++ b/examples/server_fns_axum/src/app.rs @@ -171,13 +171,16 @@ pub fn WithAnAction() -> impl IntoView { /// An <ActionForm/> lets you do the same thing as dispatching an action, but automates the /// creation of the dispatched argument struct using a <form>. This means it also gracefully /// degrades well when JS/WASM are not available. +/// +/// Try turning off WASM in your browser. The form still works, and successfully displays the error +/// message if the server function returns an error. Otherwise, it loads the new resource data. #[component] pub fn WithActionForm() -> impl IntoView { let action = create_server_action::<AddRow>(); let row_count = create_resource(action.version(), |_| get_rows()); view! { - <h3>Using <code>create_action</code></h3> + <h3>Using <code>"<ActionForm/>"</code></h3> <p> <code>"<ActionForm/>"</code> "lets you use an HTML " <code>"<form>"</code> "to call a server function in a way that gracefully degrades." From 1777a4057a353bc321c9e1fdb59d23427a2d9066 Mon Sep 17 00:00:00 2001 From: Ari Seyhun <ariseyhun@live.com.au> Date: Tue, 16 Jan 2024 05:22:58 +1030 Subject: [PATCH 056/100] fix!: remove clone in `Cow<'static, str>` `IntoView` impl (#1946) --- leptos_dom/src/lib.rs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/leptos_dom/src/lib.rs b/leptos_dom/src/lib.rs index 740710dcfc..aa2a4fc670 100644 --- a/leptos_dom/src/lib.rs +++ b/leptos_dom/src/lib.rs @@ -1154,6 +1154,17 @@ impl IntoView for &'static str { } } +impl IntoView for Cow<'static, str> { + #[cfg_attr( + any(debug_assertions, feature = "ssr"), + instrument(level = "info", name = "#text", skip_all) + )] + #[inline(always)] + fn into_view(self) -> View { + View::Text(Text::new(self.into())) + } +} + impl IntoView for Oco<'static, str> { #[cfg_attr( any(debug_assertions, feature = "ssr"), @@ -1225,7 +1236,6 @@ viewable_primitive![ f64, char, bool, - Cow<'_, str>, std::net::IpAddr, std::net::SocketAddr, std::net::SocketAddrV4, From 4366d786acae41f0886fb3d490a7f23806cab067 Mon Sep 17 00:00:00 2001 From: Markus Kohlhase <markus.kohlhase@slowtec.de> Date: Mon, 15 Jan 2024 21:42:04 +0100 Subject: [PATCH 057/100] Update login example (CSR only) (#2155) --- .../client/Cargo.toml | 4 +- .../client/src/api.rs | 4 +- .../server/Cargo.toml | 14 +++--- .../server/src/application.rs | 24 +++++------ .../server/src/main.rs | 43 +++++++++++-------- 5 files changed, 47 insertions(+), 42 deletions(-) diff --git a/examples/login_with_token_csr_only/client/Cargo.toml b/examples/login_with_token_csr_only/client/Cargo.toml index 466df84e77..25cf73cb71 100644 --- a/examples/login_with_token_csr_only/client/Cargo.toml +++ b/examples/login_with_token_csr_only/client/Cargo.toml @@ -14,7 +14,7 @@ leptos_router = { path = "../../../router", features = ["csr"] } log = "0.4" console_error_panic_hook = "0.1" console_log = "1" -gloo-net = "0.2" -gloo-storage = "0.2" +gloo-net = "0.5" +gloo-storage = "0.3" serde = "1.0" thiserror = "1.0" diff --git a/examples/login_with_token_csr_only/client/src/api.rs b/examples/login_with_token_csr_only/client/src/api.rs index d4bcda1bef..a2c401bcf2 100644 --- a/examples/login_with_token_csr_only/client/src/api.rs +++ b/examples/login_with_token_csr_only/client/src/api.rs @@ -1,5 +1,5 @@ use api_boundary::*; -use gloo_net::http::{Request, Response}; +use gloo_net::http::{Request, RequestBuilder, Response}; use serde::de::DeserializeOwned; use thiserror::Error; @@ -41,7 +41,7 @@ impl AuthorizedApi { fn auth_header_value(&self) -> String { format!("Bearer {}", self.token.token) } - async fn send<T>(&self, req: Request) -> Result<T> + async fn send<T>(&self, req: RequestBuilder) -> Result<T> where T: DeserializeOwned, { diff --git a/examples/login_with_token_csr_only/server/Cargo.toml b/examples/login_with_token_csr_only/server/Cargo.toml index 5ce72418f0..b9917926ac 100644 --- a/examples/login_with_token_csr_only/server/Cargo.toml +++ b/examples/login_with_token_csr_only/server/Cargo.toml @@ -5,14 +5,18 @@ edition = "2021" publish = false [dependencies] +api-boundary = "=0.0.0" + anyhow = "1.0" -api-boundary = "*" -axum = { version = "0.6", features = ["headers"] } +axum = "0.7" +axum-extra = { version = "0.9.2", features = ["typed-header"] } env_logger = "0.10" log = "0.4" mailparse = "0.14" pwhash = "1.0" thiserror = "1.0" -tokio = { version = "1.25", features = ["macros", "rt-multi-thread"] } -tower-http = { version = "0.4", features = ["cors"] } -uuid = { version = "1.3", features = ["v4"] } +tokio = { version = "1.35", features = ["macros", "rt-multi-thread"] } +tower-http = { version = "0.5", features = ["cors"] } +uuid = { version = "1.6", features = ["v4"] } +parking_lot = "0.12.1" +headers = "0.4.0" diff --git a/examples/login_with_token_csr_only/server/src/application.rs b/examples/login_with_token_csr_only/server/src/application.rs index 91da01b2f8..a410be951f 100644 --- a/examples/login_with_token_csr_only/server/src/application.rs +++ b/examples/login_with_token_csr_only/server/src/application.rs @@ -1,38 +1,36 @@ use mailparse::addrparse; use pwhash::bcrypt; -use std::{collections::HashMap, str::FromStr, sync::RwLock}; +use std::{collections::HashMap, str::FromStr}; use thiserror::Error; use uuid::Uuid; #[derive(Default)] pub struct AppState { - users: RwLock<HashMap<EmailAddress, Password>>, - tokens: RwLock<HashMap<Uuid, EmailAddress>>, + users: HashMap<EmailAddress, Password>, + tokens: HashMap<Uuid, EmailAddress>, } impl AppState { pub fn create_user( - &self, + &mut self, credentials: Credentials, ) -> Result<(), CreateUserError> { let Credentials { email, password } = credentials; - let user_exists = self.users.read().unwrap().get(&email).is_some(); + let user_exists = self.users.get(&email).is_some(); if user_exists { return Err(CreateUserError::UserExists); } - self.users.write().unwrap().insert(email, password); + self.users.insert(email, password); Ok(()) } pub fn login( - &self, + &mut self, email: EmailAddress, password: &str, ) -> Result<Uuid, LoginError> { let valid_credentials = self .users - .read() - .unwrap() .get(&email) .map(|hashed_password| hashed_password.verify(password)) .unwrap_or(false); @@ -40,16 +38,16 @@ impl AppState { Err(LoginError::InvalidEmailOrPassword) } else { let token = Uuid::new_v4(); - self.tokens.write().unwrap().insert(token, email); + self.tokens.insert(token, email); Ok(token) } } - pub fn logout(&self, token: &str) -> Result<(), LogoutError> { + pub fn logout(&mut self, token: &str) -> Result<(), LogoutError> { let token = token .parse::<Uuid>() .map_err(|_| LogoutError::NotLoggedIn)?; - self.tokens.write().unwrap().remove(&token); + self.tokens.remove(&token); Ok(()) } @@ -62,8 +60,6 @@ impl AppState { .map_err(|_| AuthError::NotAuthorized) .and_then(|token| { self.tokens - .read() - .unwrap() .get(&token) .cloned() .map(|email| CurrentUser { email, token }) diff --git a/examples/login_with_token_csr_only/server/src/main.rs b/examples/login_with_token_csr_only/server/src/main.rs index 8d7566add3..fe67efa6ab 100644 --- a/examples/login_with_token_csr_only/server/src/main.rs +++ b/examples/login_with_token_csr_only/server/src/main.rs @@ -1,13 +1,16 @@ use api_boundary as json; use axum::{ - extract::{State, TypedHeader}, - headers::{authorization::Bearer, Authorization}, + extract::State, http::Method, response::Json, routing::{get, post}, Router, }; -use std::{env, sync::Arc}; +use axum_extra::TypedHeader; +use headers::{authorization::Bearer, Authorization}; +use parking_lot::RwLock; +use std::{env, net::SocketAddr, sync::Arc}; +use tokio::net::TcpListener; use tower_http::cors::{Any, CorsLayer}; mod adapters; @@ -32,7 +35,7 @@ async fn main() -> anyhow::Result<()> { } env_logger::init(); - let shared_state = Arc::new(AppState::default()); + let shared_state = Arc::new(RwLock::new(AppState::default())); let cors_layer = CorsLayer::new() .allow_methods([Method::GET, Method::POST]) @@ -46,11 +49,10 @@ async fn main() -> anyhow::Result<()> { .route_layer(cors_layer) .with_state(shared_state); - let addr = "0.0.0.0:3000".parse().unwrap(); - log::info!("Listen on {addr}"); - axum::Server::bind(&addr) - .serve(app.into_make_service()) - .await?; + let addr = "0.0.0.0:3000".parse::<SocketAddr>()?; + log::info!("Start listening on http://{addr}"); + let listener = TcpListener::bind(addr).await?; + axum::serve(listener, app.into_make_service()).await?; Ok(()) } @@ -73,40 +75,43 @@ enum Error { } async fn create_user( - State(state): State<Arc<AppState>>, + State(state): State<Arc<RwLock<AppState>>>, Json(credentials): Json<json::Credentials>, ) -> Result<()> { let credentials = Credentials::try_from(credentials)?; - state.create_user(credentials)?; + state.write().create_user(credentials)?; Ok(Json(())) } async fn login( - State(state): State<Arc<AppState>>, + State(state): State<Arc<RwLock<AppState>>>, Json(credentials): Json<json::Credentials>, ) -> Result<json::ApiToken> { let json::Credentials { email, password } = credentials; log::debug!("{email} tries to login"); let email = email.parse().map_err(|_| - // Here we don't want to leak detailed info. - LoginError::InvalidEmailOrPassword)?; - let token = state.login(email, &password).map(|s| s.to_string())?; + // Here we don't want to leak detailed info. + LoginError::InvalidEmailOrPassword)?; + let token = state + .write() + .login(email, &password) + .map(|s| s.to_string())?; Ok(Json(json::ApiToken { token })) } async fn logout( - State(state): State<Arc<AppState>>, + State(state): State<Arc<RwLock<AppState>>>, TypedHeader(auth): TypedHeader<Authorization<Bearer>>, ) -> Result<()> { - state.logout(auth.token())?; + state.write().logout(auth.token())?; Ok(Json(())) } async fn get_user_info( - State(state): State<Arc<AppState>>, + State(state): State<Arc<RwLock<AppState>>>, TypedHeader(auth): TypedHeader<Authorization<Bearer>>, ) -> Result<json::UserInfo> { - let user = state.authorize_user(auth.token())?; + let user = state.read().authorize_user(auth.token())?; let CurrentUser { email, .. } = user; Ok(Json(json::UserInfo { email: email.into_string(), From 35e8e74dcf225490223b0f1248e063cf678ce95d Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Mon, 15 Jan 2024 16:14:35 -0500 Subject: [PATCH 058/100] get rkyv working and work on custom encoding example --- examples/server_fns_axum/Cargo.toml | 2 + examples/server_fns_axum/src/app.rs | 225 +++++++++++++++++++++++++++- leptos_server/src/action.rs | 4 +- server_fn/src/codec/cbor.rs | 8 +- server_fn/src/codec/json.rs | 8 +- server_fn/src/codec/mod.rs | 16 +- server_fn/src/codec/multipart.rs | 4 +- server_fn/src/codec/rkyv.rs | 10 +- server_fn/src/codec/serde_lite.rs | 8 +- server_fn/src/codec/stream.rs | 12 +- server_fn/src/codec/url.rs | 8 +- server_fn/src/lib.rs | 14 +- server_fn_macro/src/lib.rs | 43 ++++-- 13 files changed, 302 insertions(+), 60 deletions(-) diff --git a/examples/server_fns_axum/Cargo.toml b/examples/server_fns_axum/Cargo.toml index 4d518bb275..c23ec1ae34 100644 --- a/examples/server_fns_axum/Cargo.toml +++ b/examples/server_fns_axum/Cargo.toml @@ -25,6 +25,8 @@ tower-http = { version = "0.5", features = ["fs"], optional = true } tokio = { version = "1", features = ["full"], optional = true } thiserror = "1.0" wasm-bindgen = "0.2" +serde_toml = "0.0.1" +toml = "0.8.8" [features] hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"] diff --git a/examples/server_fns_axum/src/app.rs b/examples/server_fns_axum/src/app.rs index 27a6b43a79..fd09978aaf 100644 --- a/examples/server_fns_axum/src/app.rs +++ b/examples/server_fns_axum/src/app.rs @@ -1,9 +1,18 @@ use crate::error_template::ErrorTemplate; +use http::{Request, Response}; use leptos::{html::Input, *}; -use leptos_meta::*; +use leptos_meta::{Link, Stylesheet}; use leptos_router::*; -use serde::{Deserialize, Serialize}; -use server_fn::codec::SerdeLite; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use server_fn::{ + codec::{ + Encoding, FromReq, FromRes, GetUrl, IntoReq, IntoRes, Rkyv, SerdeLite, + }, + error::NoCustomError, + request::{browser::BrowserRequest, BrowserMockReq, ClientReq, Req}, + response::{browser::BrowserResponse, ClientRes, Res}, + rkyv::AlignedVec, +}; #[cfg(feature = "ssr")] use std::sync::{ atomic::{AtomicU8, Ordering}, @@ -37,6 +46,10 @@ pub fn HomePage() -> impl IntoView { <SpawnLocal/> <WithAnAction/> <WithActionForm/> + <h2>"Alternative Encodings"</h2> + <ServerFnArgumentExample/> + <RkyvExample/> + <CustomEncoding/> } } @@ -155,7 +168,15 @@ pub fn WithAnAction() -> impl IntoView { <button on:click=move |_| { let text = input_ref.get().unwrap().value(); - action.dispatch(AddRow { text }); + action.dispatch(text); + // note: technically, this `action` takes `AddRow` (the server fn type) as its + // argument + // + // however, `.dispatch()` takes `impl Into<I>`, and for any one-argument server + // functions, `From<_>` is implemented between the server function type and the + // type of this single argument + // + // so `action.dispatch(text)` means `action.dispatch(AddRow { text })` } > Submit @@ -195,8 +216,202 @@ pub fn WithActionForm() -> impl IntoView { </ActionForm> <p>You submitted: {move || format!("{:?}", action.input().get())}</p> <p>The result was: {move || format!("{:?}", action.value().get())}</p> - <Transition> + <Transition>archive underaligned: need alignment 4 but have alignment 1 <p>Total rows: {row_count}</p> </Transition> } } + +/// The plain `#[server]` macro gives sensible defaults for the settings needed to create a server +/// function, but those settings can also be customized. For example, you can set a specific unique +/// path rather than the hashed path, or you can choose a different combination of input and output +/// encodings. +/// +/// Arguments to the server macro can be specified as named key-value pairs, like `name = value`. +#[server( + // this server function will be exposed at /api2/custom_path + prefix = "/api2", + endpoint = "custom_path", + // it will take its arguments as a URL-encoded GET request (useful for caching) + input = GetUrl, + // it will return its output using SerdeLite + // (this needs to be enabled with the `serde-lite` feature on the `server_fn` crate + output = SerdeLite +)] +pub async fn length_of_input(input: String) -> Result<usize, ServerFnError> { + // insert a simulated wait + tokio::time::sleep(std::time::Duration::from_millis(250)).await; + Ok(input.len()) +} + +#[component] +pub fn ServerFnArgumentExample() -> impl IntoView { + let input_ref = NodeRef::<Input>::new(); + let (result, set_result) = create_signal(0); + + view! { + <h3>Custom arguments to the <code>#[server]</code> " macro"</h3> + <p> + </p> + <input node_ref=input_ref placeholder="Type something here."/> + <button + on:click=move |_| { + let value = input_ref.get().unwrap().value(); + spawn_local(async move { + let length = length_of_input(value).await.unwrap_or(0); + set_result(length); + }); + } + > + Click to see length + </button> + <p>Length is {result}</p> + } +} + +/// `server_fn` supports a wide variety of input and output encodings, each of which can be +/// referred to as a PascalCased struct name +/// - Toml +/// - Cbor +/// - Rkyv +/// - etc. +#[server( + input = Rkyv, + output = Rkyv +)] +pub async fn rkyv_example(input: String) -> Result<String, ServerFnError> { + // insert a simulated wait + tokio::time::sleep(std::time::Duration::from_millis(250)).await; + Ok(input.to_ascii_uppercase()) +} + +#[component] +pub fn RkyvExample() -> impl IntoView { + let input_ref = NodeRef::<Input>::new(); + let (input, set_input) = create_signal(String::new()); + let rkyv_result = create_resource(input, rkyv_example); + + view! { + <h3>Using <code>rkyv</code> encoding</h3> + <p> + </p> + <input node_ref=input_ref placeholder="Type something here."/> + <button + on:click=move |_| { + let value = input_ref.get().unwrap().value(); + set_input(value); + } + > + Click to see length + </button> + <p>{input}</p> + <Transition> + {rkyv_result} + </Transition> + } +} + +/// Server function encodings are just types that implement a few traits. +/// This means that you can implement your own encodings, by implementing those traits! +/// +/// Here, we'll create a custom encoding that serializes and deserializes the server fn +/// using TOML. Why would you ever want to do this? I don't know, but you can! +struct Toml; + +impl Encoding for Toml { + const CONTENT_TYPE: &'static str = "application/toml"; + const METHOD: Method = Method::POST; +} + +#[cfg(not(feature = "ssr"))] +type Request = BrowserMockReq; +#[cfg(feature = "ssr")] +type Request = http::Request<axum::body::Body>; +#[cfg(not(feature = "ssr"))] +type Response = BrowserMockRes; +#[cfg(feature = "ssr")] +type Response = http::Response<axum::body::Body>; + +impl<T> IntoReq<Toml, BrowserRequest, NoCustomError> for T { + fn into_req( + self, + path: &str, + accepts: &str, + ) -> Result<BrowserRequest, ServerFnError> { + let data = toml::to_string(&self) + .map_err(|e| ServerFnError::Serialization(e.to_string()))?; + Request::try_new_post(path, Toml::CONTENT_TYPE, accepts, data) + } +} + +impl<T> FromReq<Toml, Request, NoCustomError> for T +where + T: DeserializeOwned, +{ + async fn from_req(req: Request) -> Result<Self, ServerFnError> { + let string_data = req.try_into_string().await?; + toml::from_str::<Self>(&string_data) + .map_err(|e| ServerFnError::Args(e.to_string())) + } +} + +impl<T> IntoRes<Toml, Response, NoCustomError> for T +where + T: Serialize + Send, +{ + async fn into_res(self) -> Result<Response, ServerFnError> { + let data = toml::to_string(&self) + .map_err(|e| ServerFnError::Serialization(e.to_string()))?; + Response::try_from_string(Toml::CONTENT_TYPE, data) + } +} + +impl<e> FromRes<Toml, BrowserResponse, NoCustomError> for T +where + T: DeserializeOwned + Send, +{ + async fn from_res(res: BrowserResponse) -> Result<Self, ServerFnError> { + let data = res.try_into_string().await?; + toml::from_str(&data) + .map_err(|e| ServerFnError::Deserialization(e.to_string())) + } +} + +#[server( + input = Toml, + output = Toml +)] +pub async fn why_not( + foo: String, + bar: String, +) -> Result<String, ServerFnError> { + // insert a simulated wait + tokio::time::sleep(std::time::Duration::from_millis(250)).await; + Ok(foo + &bar) +} + +#[component] +pub fn CustomEncoding() -> impl IntoView { + let input_ref = NodeRef::<Input>::new(); + let (result, set_result) = create_signal(0); + + view! { + <h3>Custom encodings</h3> + <p> + "This example creates a custom encoding that sends server fn data using TOML. Why? Well... why not?" + </p> + <input node_ref=input_ref placeholder="Type something here."/> + <button + on:click=move |_| { + let value = input_ref.get().unwrap().value(); + spawn_local(async move { + let new_value = why_not(value, ", but in TOML!!!".to_string()); + set_result(new_value); + }); + } + > + Submit + </button> + <p>{result}</p> + } +} diff --git a/leptos_server/src/action.rs b/leptos_server/src/action.rs index b5ca4e5d12..f50de6c628 100644 --- a/leptos_server/src/action.rs +++ b/leptos_server/src/action.rs @@ -93,8 +93,8 @@ where any(debug_assertions, feature = "ssr"), tracing::instrument(level = "trace", skip_all,) )] - pub fn dispatch(&self, input: I) { - self.0.with_value(|a| a.dispatch(input)) + pub fn dispatch(&self, input: impl Into<I>) { + self.0.with_value(|a| a.dispatch(input.into())) } /// Create an [Action]. diff --git a/server_fn/src/codec/cbor.rs b/server_fn/src/codec/cbor.rs index 9f0e8d6ab7..a5a91c8117 100644 --- a/server_fn/src/codec/cbor.rs +++ b/server_fn/src/codec/cbor.rs @@ -16,7 +16,7 @@ impl Encoding for Cbor { const METHOD: Method = Method::POST; } -impl<CustErr, T, Request> IntoReq<CustErr, Request, Cbor> for T +impl<CustErr, T, Request> IntoReq<Cbor, Request, CustErr> for T where Request: ClientReq<CustErr>, T: Serialize + Send, @@ -38,7 +38,7 @@ where } } -impl<CustErr, T, Request> FromReq<CustErr, Request, Cbor> for T +impl<CustErr, T, Request> FromReq<Cbor, Request, CustErr> for T where Request: Req<CustErr> + Send + 'static, T: DeserializeOwned, @@ -50,7 +50,7 @@ where } } -impl<CustErr, T, Response> IntoRes<CustErr, Response, Cbor> for T +impl<CustErr, T, Response> IntoRes<Cbor, Response, CustErr> for T where Response: Res<CustErr>, T: Serialize + Send, @@ -63,7 +63,7 @@ where } } -impl<CustErr, T, Response> FromRes<CustErr, Response, Cbor> for T +impl<CustErr, T, Response> FromRes<Cbor, Response, CustErr> for T where Response: ClientRes<CustErr> + Send, T: DeserializeOwned + Send, diff --git a/server_fn/src/codec/json.rs b/server_fn/src/codec/json.rs index 0dcb062d21..db9292cbe9 100644 --- a/server_fn/src/codec/json.rs +++ b/server_fn/src/codec/json.rs @@ -15,7 +15,7 @@ impl Encoding for Json { const METHOD: Method = Method::POST; } -impl<CustErr, T, Request> IntoReq<CustErr, Request, Json> for T +impl<CustErr, T, Request> IntoReq<Json, Request, CustErr> for T where Request: ClientReq<CustErr>, T: Serialize + Send, @@ -31,7 +31,7 @@ where } } -impl<CustErr, T, Request> FromReq<CustErr, Request, Json> for T +impl<CustErr, T, Request> FromReq<Json, Request, CustErr> for T where Request: Req<CustErr> + Send + 'static, T: DeserializeOwned, @@ -43,7 +43,7 @@ where } } -impl<CustErr, T, Response> IntoRes<CustErr, Response, Json> for T +impl<CustErr, T, Response> IntoRes<Json, Response, CustErr> for T where Response: Res<CustErr>, T: Serialize + Send, @@ -55,7 +55,7 @@ where } } -impl<CustErr, T, Response> FromRes<CustErr, Response, Json> for T +impl<CustErr, T, Response> FromRes<Json, Response, CustErr> for T where Response: ClientRes<CustErr> + Send, T: DeserializeOwned + Send, diff --git a/server_fn/src/codec/mod.rs b/server_fn/src/codec/mod.rs index 32ada42e5c..63167294b3 100644 --- a/server_fn/src/codec/mod.rs +++ b/server_fn/src/codec/mod.rs @@ -61,7 +61,7 @@ pub use stream::*; /// For example, here’s the implementation for [`Json`]. /// /// ```rust -/// impl<CustErr, T, Request> IntoReq<CustErr, Request, Json> for T +/// impl<CustErr, T, Request> IntoReq<Json, Request, CustErr> for T /// where /// Request: ClientReq<CustErr>, /// T: Serialize + Send, @@ -79,7 +79,7 @@ pub use stream::*; /// } /// } /// ``` -pub trait IntoReq<CustErr, Request, Encoding> { +pub trait IntoReq<Encoding, Request, CustErr> { /// Attempts to serialize the arguments into an HTTP request. fn into_req( self, @@ -99,7 +99,7 @@ pub trait IntoReq<CustErr, Request, Encoding> { /// For example, here’s the implementation for [`Json`]. /// /// ```rust -/// impl<CustErr, T, Request> FromReq<CustErr, Request, Json> for T +/// impl<CustErr, T, Request> FromReq<Json, Request, CustErr> for T /// where /// // require the Request implement `Req` /// Request: Req<CustErr> + Send + 'static, @@ -117,7 +117,7 @@ pub trait IntoReq<CustErr, Request, Encoding> { /// } /// } /// ``` -pub trait FromReq<CustErr, Request, Encoding> +pub trait FromReq<Encoding, Request, CustErr> where Self: Sized, { @@ -138,7 +138,7 @@ where /// For example, here’s the implementation for [`Json`]. /// /// ```rust -/// impl<CustErr, T, Response> IntoRes<CustErr, Response, Json> for T +/// impl<CustErr, T, Response> IntoRes<Json, Response, CustErr> for T /// where /// Response: Res<CustErr>, /// T: Serialize + Send, @@ -152,7 +152,7 @@ where /// } /// } /// ``` -pub trait IntoRes<CustErr, Response, Encoding> { +pub trait IntoRes<Encoding, Response, CustErr> { /// Attempts to serialize the output into an HTTP response. fn into_res( self, @@ -171,7 +171,7 @@ pub trait IntoRes<CustErr, Response, Encoding> { /// For example, here’s the implementation for [`Json`]. /// /// ```rust -/// impl<CustErr, T, Response> FromRes<CustErr, Response, Json> for T +/// impl<CustErr, T, Response> FromRes<Json, Response, CustErr> for T /// where /// Response: ClientRes<CustErr> + Send, /// T: DeserializeOwned + Send, @@ -187,7 +187,7 @@ pub trait IntoRes<CustErr, Response, Encoding> { /// } /// } /// ``` -pub trait FromRes<CustErr, Response, Encoding> +pub trait FromRes<Encoding, Response, CustErr> where Self: Sized, { diff --git a/server_fn/src/codec/multipart.rs b/server_fn/src/codec/multipart.rs index 8dee495567..998e822260 100644 --- a/server_fn/src/codec/multipart.rs +++ b/server_fn/src/codec/multipart.rs @@ -56,7 +56,7 @@ impl From<FormData> for MultipartData { } } -impl<CustErr, T, Request> IntoReq<CustErr, Request, MultipartFormData> for T +impl<CustErr, T, Request> IntoReq<MultipartFormData, Request, CustErr> for T where Request: ClientReq<CustErr, FormData = BrowserFormData>, T: Into<MultipartData>, @@ -75,7 +75,7 @@ where } } -impl<CustErr, T, Request> FromReq<CustErr, Request, MultipartFormData> for T +impl<CustErr, T, Request> FromReq<MultipartFormData, Request, CustErr> for T where Request: Req<CustErr> + Send + 'static, T: From<MultipartData>, diff --git a/server_fn/src/codec/rkyv.rs b/server_fn/src/codec/rkyv.rs index 17435d342b..2d9753ad12 100644 --- a/server_fn/src/codec/rkyv.rs +++ b/server_fn/src/codec/rkyv.rs @@ -20,7 +20,7 @@ impl Encoding for Rkyv { const METHOD: Method = Method::POST; } -impl<CustErr, T, Request> IntoReq<CustErr, Request, Rkyv> for T +impl<CustErr, T, Request> IntoReq<Rkyv, Request, CustErr> for T where Request: ClientReq<CustErr>, T: Serialize<AllocSerializer<1024>> + Send, @@ -40,7 +40,7 @@ where } } -impl<CustErr, T, Request> FromReq<CustErr, Request, Rkyv> for T +impl<CustErr, T, Request> FromReq<Rkyv, Request, CustErr> for T where Request: Req<CustErr> + Send + 'static, T: Serialize<AllocSerializer<1024>> + Send, @@ -50,12 +50,12 @@ where { async fn from_req(req: Request) -> Result<Self, ServerFnError<CustErr>> { let body_bytes = req.try_into_bytes().await?; - rkyv::from_bytes::<T>(&body_bytes) + rkyv::from_bytes::<T>(body_bytes.as_ref()) .map_err(|e| ServerFnError::Args(e.to_string())) } } -impl<CustErr, T, Response> IntoRes<CustErr, Response, Rkyv> for T +impl<CustErr, T, Response> IntoRes<Rkyv, Response, CustErr> for T where Response: Res<CustErr>, T: Serialize<AllocSerializer<1024>> + Send, @@ -71,7 +71,7 @@ where } } -impl<CustErr, T, Response> FromRes<CustErr, Response, Rkyv> for T +impl<CustErr, T, Response> FromRes<Rkyv, Response, CustErr> for T where Response: ClientRes<CustErr> + Send, T: Serialize<AllocSerializer<1024>> + Send, diff --git a/server_fn/src/codec/serde_lite.rs b/server_fn/src/codec/serde_lite.rs index 415dc32bd0..b71b9390fe 100644 --- a/server_fn/src/codec/serde_lite.rs +++ b/server_fn/src/codec/serde_lite.rs @@ -15,7 +15,7 @@ impl Encoding for SerdeLite { const METHOD: Method = Method::POST; } -impl<CustErr, T, Request> IntoReq<CustErr, Request, SerdeLite> for T +impl<CustErr, T, Request> IntoReq<SerdeLite, Request, CustErr> for T where Request: ClientReq<CustErr>, T: Serialize + Send, @@ -35,7 +35,7 @@ where } } -impl<CustErr, T, Request> FromReq<CustErr, Request, SerdeLite> for T +impl<CustErr, T, Request> FromReq<SerdeLite, Request, CustErr> for T where Request: Req<CustErr> + Send + 'static, T: Deserialize, @@ -50,7 +50,7 @@ where } } -impl<CustErr, T, Response> IntoRes<CustErr, Response, SerdeLite> for T +impl<CustErr, T, Response> IntoRes<SerdeLite, Response, CustErr> for T where Response: Res<CustErr>, T: Serialize + Send, @@ -66,7 +66,7 @@ where } } -impl<CustErr, T, Response> FromRes<CustErr, Response, SerdeLite> for T +impl<CustErr, T, Response> FromRes<SerdeLite, Response, CustErr> for T where Response: ClientRes<CustErr> + Send, T: Deserialize + Send, diff --git a/server_fn/src/codec/stream.rs b/server_fn/src/codec/stream.rs index 57646f0da5..faafba94eb 100644 --- a/server_fn/src/codec/stream.rs +++ b/server_fn/src/codec/stream.rs @@ -19,7 +19,7 @@ impl Encoding for Streaming { const METHOD: Method = Method::POST; } -/* impl<CustErr, T, Request> IntoReq<CustErr, Request, ByteStream> for T +/* impl<CustErr, T, Request> IntoReq<ByteStream, Request, CustErr> for T where Request: ClientReq<CustErr>, T: Stream<Item = Bytes> + Send, @@ -29,7 +29,7 @@ where } } */ -/* impl<CustErr, T, Request> FromReq<CustErr, Request, ByteStream> for T +/* impl<CustErr, T, Request> FromReq<ByteStream, Request, CustErr> for T where Request: Req<CustErr> + Send + 'static, T: Stream<Item = Bytes> + Send, @@ -65,7 +65,7 @@ where } } -impl<CustErr, Response> IntoRes<CustErr, Response, Streaming> +impl<CustErr, Response> IntoRes<Streaming, Response, CustErr> for ByteStream<CustErr> where Response: Res<CustErr>, @@ -76,7 +76,7 @@ where } } -impl<CustErr, Response> FromRes<CustErr, Response, Streaming> for ByteStream +impl<CustErr, Response> FromRes<Streaming, Response, CustErr> for ByteStream where Response: ClientRes<CustErr> + Send, { @@ -122,7 +122,7 @@ where } } -impl<CustErr, Response> IntoRes<CustErr, Response, StreamingText> +impl<CustErr, Response> IntoRes<StreamingText, Response, CustErr> for TextStream<CustErr> where Response: Res<CustErr>, @@ -136,7 +136,7 @@ where } } -impl<CustErr, Response> FromRes<CustErr, Response, StreamingText> for TextStream +impl<CustErr, Response> FromRes<StreamingText, Response, CustErr> for TextStream where Response: ClientRes<CustErr> + Send, { diff --git a/server_fn/src/codec/url.rs b/server_fn/src/codec/url.rs index e75953c6ad..72cdec2b86 100644 --- a/server_fn/src/codec/url.rs +++ b/server_fn/src/codec/url.rs @@ -17,7 +17,7 @@ impl Encoding for GetUrl { const METHOD: Method = Method::GET; } -impl<CustErr, T, Request> IntoReq<CustErr, Request, GetUrl> for T +impl<CustErr, T, Request> IntoReq<GetUrl, Request, CustErr> for T where Request: ClientReq<CustErr>, T: Serialize + Send, @@ -33,7 +33,7 @@ where } } -impl<CustErr, T, Request> FromReq<CustErr, Request, GetUrl> for T +impl<CustErr, T, Request> FromReq<GetUrl, Request, CustErr> for T where Request: Req<CustErr> + Send + 'static, T: DeserializeOwned, @@ -51,7 +51,7 @@ impl Encoding for PostUrl { const METHOD: Method = Method::POST; } -impl<CustErr, T, Request> IntoReq<CustErr, Request, PostUrl> for T +impl<CustErr, T, Request> IntoReq<PostUrl, Request, CustErr> for T where Request: ClientReq<CustErr>, T: Serialize + Send, @@ -67,7 +67,7 @@ where } } -impl<CustErr, T, Request> FromReq<CustErr, Request, PostUrl> for T +impl<CustErr, T, Request> FromReq<PostUrl, Request, CustErr> for T where Request: Req<CustErr> + Send + 'static, T: DeserializeOwned, diff --git a/server_fn/src/lib.rs b/server_fn/src/lib.rs index 780f5cc5c1..d8cb5a4919 100644 --- a/server_fn/src/lib.rs +++ b/server_fn/src/lib.rs @@ -132,6 +132,8 @@ use once_cell::sync::Lazy; use redirect::RedirectHook; use request::Req; use response::{ClientRes, Res}; +#[cfg(feature = "rkyv")] +pub use rkyv; #[doc(hidden)] pub use serde; #[doc(hidden)] @@ -173,11 +175,11 @@ pub use xxhash_rust; pub trait ServerFn where Self: Send - + FromReq<Self::Error, Self::ServerRequest, Self::InputEncoding> + + FromReq<Self::InputEncoding, Self::ServerRequest, Self::Error> + IntoReq< - Self::Error, - <Self::Client as Client<Self::Error>>::Request, Self::InputEncoding, + <Self::Client as Client<Self::Error>>::Request, + Self::Error, >, { /// A unique path for the server function’s API endpoint, relative to the host, including its prefix. @@ -198,11 +200,11 @@ where /// /// This needs to be converted into `ServerResponse` on the server side, and converted /// *from* `ClientResponse` when received by the client. - type Output: IntoRes<Self::Error, Self::ServerResponse, Self::OutputEncoding> + type Output: IntoRes<Self::OutputEncoding, Self::ServerResponse, Self::Error> + FromRes< - Self::Error, - <Self::Client as Client<Self::Error>>::Response, Self::OutputEncoding, + <Self::Client as Client<Self::Error>>::Response, + Self::Error, > + Send; /// The [`Encoding`] used in the request for arguments into the server function. diff --git a/server_fn_macro/src/lib.rs b/server_fn_macro/src/lib.rs index 9ed4ffe3a0..0b5ec358b9 100644 --- a/server_fn_macro/src/lib.rs +++ b/server_fn_macro/src/lib.rs @@ -130,6 +130,12 @@ pub fn server_macro_impl( } FnArg::Typed(t) => t, }; + + // strip `mut`, which is allowed in fn args but not in struct fields + if let Pat::Ident(ident) = &mut *typed_arg.pat { + ident.mutability = None; + } + // allow #[server(default)] on fields — TODO is this documented? let mut default = false; let mut other_attrs = Vec::new(); @@ -332,27 +338,45 @@ pub fn server_macro_impl( } }; - let (is_serde, derives) = match input_ident.as_deref() { - Some("Rkyv") => todo!("implement derives for Rkyv"), - Some("MultipartFormData") => (false, quote! {}), + enum PathInfo { + Serde, + Rkyv, + None, + } + + let (path, derives) = match input_ident.as_deref() { + Some("Rkyv") => ( + PathInfo::Rkyv, + quote! { + Clone, #server_fn_path::rkyv::Archive, #server_fn_path::rkyv::Serialize, #server_fn_path::rkyv::Deserialize + }, + ), + Some("MultipartFormData") => (PathInfo::None, quote! {}), Some("SerdeLite") => ( - true, + PathInfo::Serde, quote! { Clone, #server_fn_path::serde_lite::Serialize, #server_fn_path::serde_lite::Deserialize }, ), _ => ( - true, + PathInfo::Serde, quote! { Clone, #server_fn_path::serde::Serialize, #server_fn_path::serde::Deserialize }, ), }; - let serde_path = is_serde.then(|| { - quote! { + let addl_path = match path { + PathInfo::Serde => quote! { #[serde(crate = #serde_path)] + }, + PathInfo::Rkyv => { + let rkyv_path = format!("{server_fn_path}::rkyv"); + quote! { + #[archive(crate = #rkyv_path, check_bytes)] + } } - }); + PathInfo::None => quote! {}, + }; // TODO reqwest let client = quote! { @@ -429,7 +453,6 @@ pub fn server_macro_impl( } else { #server_fn_path::const_format::concatcp!( #prefix, - "/", #fn_path ) } @@ -453,7 +476,7 @@ pub fn server_macro_impl( #args_docs #docs #[derive(Debug, #derives)] - #serde_path + #addl_path pub struct #struct_name { #(#fields),* } From 1f017a2ade08710fbdcb27710fa3b83c0c50708f Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Mon, 15 Jan 2024 16:14:58 -0500 Subject: [PATCH 059/100] hm custom encodings have orphan rule issues --- examples/server_fns_axum/src/app.rs | 105 ---------------------------- 1 file changed, 105 deletions(-) diff --git a/examples/server_fns_axum/src/app.rs b/examples/server_fns_axum/src/app.rs index fd09978aaf..af9d4b5d0d 100644 --- a/examples/server_fns_axum/src/app.rs +++ b/examples/server_fns_axum/src/app.rs @@ -310,108 +310,3 @@ pub fn RkyvExample() -> impl IntoView { </Transition> } } - -/// Server function encodings are just types that implement a few traits. -/// This means that you can implement your own encodings, by implementing those traits! -/// -/// Here, we'll create a custom encoding that serializes and deserializes the server fn -/// using TOML. Why would you ever want to do this? I don't know, but you can! -struct Toml; - -impl Encoding for Toml { - const CONTENT_TYPE: &'static str = "application/toml"; - const METHOD: Method = Method::POST; -} - -#[cfg(not(feature = "ssr"))] -type Request = BrowserMockReq; -#[cfg(feature = "ssr")] -type Request = http::Request<axum::body::Body>; -#[cfg(not(feature = "ssr"))] -type Response = BrowserMockRes; -#[cfg(feature = "ssr")] -type Response = http::Response<axum::body::Body>; - -impl<T> IntoReq<Toml, BrowserRequest, NoCustomError> for T { - fn into_req( - self, - path: &str, - accepts: &str, - ) -> Result<BrowserRequest, ServerFnError> { - let data = toml::to_string(&self) - .map_err(|e| ServerFnError::Serialization(e.to_string()))?; - Request::try_new_post(path, Toml::CONTENT_TYPE, accepts, data) - } -} - -impl<T> FromReq<Toml, Request, NoCustomError> for T -where - T: DeserializeOwned, -{ - async fn from_req(req: Request) -> Result<Self, ServerFnError> { - let string_data = req.try_into_string().await?; - toml::from_str::<Self>(&string_data) - .map_err(|e| ServerFnError::Args(e.to_string())) - } -} - -impl<T> IntoRes<Toml, Response, NoCustomError> for T -where - T: Serialize + Send, -{ - async fn into_res(self) -> Result<Response, ServerFnError> { - let data = toml::to_string(&self) - .map_err(|e| ServerFnError::Serialization(e.to_string()))?; - Response::try_from_string(Toml::CONTENT_TYPE, data) - } -} - -impl<e> FromRes<Toml, BrowserResponse, NoCustomError> for T -where - T: DeserializeOwned + Send, -{ - async fn from_res(res: BrowserResponse) -> Result<Self, ServerFnError> { - let data = res.try_into_string().await?; - toml::from_str(&data) - .map_err(|e| ServerFnError::Deserialization(e.to_string())) - } -} - -#[server( - input = Toml, - output = Toml -)] -pub async fn why_not( - foo: String, - bar: String, -) -> Result<String, ServerFnError> { - // insert a simulated wait - tokio::time::sleep(std::time::Duration::from_millis(250)).await; - Ok(foo + &bar) -} - -#[component] -pub fn CustomEncoding() -> impl IntoView { - let input_ref = NodeRef::<Input>::new(); - let (result, set_result) = create_signal(0); - - view! { - <h3>Custom encodings</h3> - <p> - "This example creates a custom encoding that sends server fn data using TOML. Why? Well... why not?" - </p> - <input node_ref=input_ref placeholder="Type something here."/> - <button - on:click=move |_| { - let value = input_ref.get().unwrap().value(); - spawn_local(async move { - let new_value = why_not(value, ", but in TOML!!!".to_string()); - set_result(new_value); - }); - } - > - Submit - </button> - <p>{result}</p> - } -} From 7d114c7414c6fbb06d0772c3dbf936f757282086 Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Mon, 15 Jan 2024 17:07:14 -0500 Subject: [PATCH 060/100] file upload example --- examples/server_fns_axum/Cargo.toml | 1 + examples/server_fns_axum/src/app.rs | 86 ++++++++++++++++++++++++----- 2 files changed, 73 insertions(+), 14 deletions(-) diff --git a/examples/server_fns_axum/Cargo.toml b/examples/server_fns_axum/Cargo.toml index c23ec1ae34..fb1d7b7628 100644 --- a/examples/server_fns_axum/Cargo.toml +++ b/examples/server_fns_axum/Cargo.toml @@ -27,6 +27,7 @@ thiserror = "1.0" wasm-bindgen = "0.2" serde_toml = "0.0.1" toml = "0.8.8" +web-sys = { version = "0.3.67", features = ["FileList", "File"] } [features] hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"] diff --git a/examples/server_fns_axum/src/app.rs b/examples/server_fns_axum/src/app.rs index af9d4b5d0d..dd5edb0fb3 100644 --- a/examples/server_fns_axum/src/app.rs +++ b/examples/server_fns_axum/src/app.rs @@ -1,23 +1,16 @@ -use crate::error_template::ErrorTemplate; -use http::{Request, Response}; use leptos::{html::Input, *}; -use leptos_meta::{Link, Stylesheet}; -use leptos_router::*; -use serde::{de::DeserializeOwned, Deserialize, Serialize}; -use server_fn::{ - codec::{ - Encoding, FromReq, FromRes, GetUrl, IntoReq, IntoRes, Rkyv, SerdeLite, - }, - error::NoCustomError, - request::{browser::BrowserRequest, BrowserMockReq, ClientReq, Req}, - response::{browser::BrowserResponse, ClientRes, Res}, - rkyv::AlignedVec, +use leptos_meta::{provide_meta_context, Link, Stylesheet}; +use leptos_router::{ActionForm, Route, Router, Routes}; +use server_fn::codec::{ + GetUrl, MultipartData, MultipartFormData, Rkyv, SerdeLite, }; #[cfg(feature = "ssr")] use std::sync::{ atomic::{AtomicU8, Ordering}, Mutex, }; +use wasm_bindgen::JsCast; +use web_sys::{FormData, HtmlFormElement, SubmitEvent}; #[component] pub fn TodoApp() -> impl IntoView { @@ -49,7 +42,7 @@ pub fn HomePage() -> impl IntoView { <h2>"Alternative Encodings"</h2> <ServerFnArgumentExample/> <RkyvExample/> - <CustomEncoding/> + <FileUpload/> } } @@ -310,3 +303,68 @@ pub fn RkyvExample() -> impl IntoView { </Transition> } } + +#[component] +pub fn FileUpload() -> impl IntoView { + /// A simple file upload function, which does just returns the length of the file. + /// + /// On the server, this uses the `multer` crate, which provides a streaming API. + #[server( + input = MultipartFormData, +)] + pub async fn file_length( + data: MultipartData, + ) -> Result<usize, ServerFnError> { + // `.into_inner()` returns the inner `multer` stream + // it is `None` if we call this on the client, but always `Some(_)` on the server, so is safe to + // unwrap + let mut data = data.into_inner().unwrap(); + + // this will just measure the total number of bytes uploaded + let mut count = 0; + while let Ok(Some(mut field)) = data.next_field().await { + println!("\n[NEXT FIELD]\n"); + let name = field.name().unwrap_or_default().to_string(); + println!(" [NAME] {name}"); + while let Ok(Some(chunk)) = field.chunk().await { + let len = chunk.len(); + count += len; + println!(" [CHUNK] {len}"); + // in a real server function, you'd do something like saving the file here + } + } + + Ok(count) + } + + let upload_action = create_action(|data: &FormData| { + let data = data.clone(); + // `MultipartData` implements `From<FormData>` + file_length(data.into()) + }); + + view! { + <h3>File Upload</h3> + <p>Uploading files is fairly easy using multipart form data.</p> + <form on:submit=move |ev: SubmitEvent| { + ev.prevent_default(); + let target = ev.target().unwrap().unchecked_into::<HtmlFormElement>(); + let form_data = FormData::new_with_form(&target).unwrap(); + upload_action.dispatch(form_data); + }> + <input type="file" name="file_to_upload"/> + <input type="submit"/> + </form> + <p> + {move || if upload_action.input().get().is_none() && upload_action.value().get().is_none() { + "Upload a file.".to_string() + } else if upload_action.pending().get() { + "Uploading...".to_string() + } else if let Some(Ok(value)) = upload_action.value().get() { + value.to_string() + } else { + format!("{:?}", upload_action.value().get()) + }} + </p> + } +} From 4d602c21f8757cd3685424c798abf8dabfab11f1 Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Mon, 15 Jan 2024 20:38:21 -0500 Subject: [PATCH 061/100] example with custom errors --- examples/server_fns_axum/Cargo.toml | 1 + examples/server_fns_axum/src/app.rs | 66 +++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+) diff --git a/examples/server_fns_axum/Cargo.toml b/examples/server_fns_axum/Cargo.toml index fb1d7b7628..dfd06f3825 100644 --- a/examples/server_fns_axum/Cargo.toml +++ b/examples/server_fns_axum/Cargo.toml @@ -28,6 +28,7 @@ wasm-bindgen = "0.2" serde_toml = "0.0.1" toml = "0.8.8" web-sys = { version = "0.3.67", features = ["FileList", "File"] } +strum = { version = "0.25.0", features = ["strum_macros", "derive"] } [features] hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"] diff --git a/examples/server_fns_axum/src/app.rs b/examples/server_fns_axum/src/app.rs index dd5edb0fb3..b37526b805 100644 --- a/examples/server_fns_axum/src/app.rs +++ b/examples/server_fns_axum/src/app.rs @@ -9,6 +9,7 @@ use std::sync::{ atomic::{AtomicU8, Ordering}, Mutex, }; +use strum::{Display, EnumString}; use wasm_bindgen::JsCast; use web_sys::{FormData, HtmlFormElement, SubmitEvent}; @@ -39,6 +40,8 @@ pub fn HomePage() -> impl IntoView { <SpawnLocal/> <WithAnAction/> <WithActionForm/> + <h2>"Custom Error Types"</h2> + <CustomErrorTypes/> <h2>"Alternative Encodings"</h2> <ServerFnArgumentExample/> <RkyvExample/> @@ -368,3 +371,66 @@ pub fn FileUpload() -> impl IntoView { </p> } } + +/// The `ServerFnError` type is generic over a custom error type, which defaults to `NoCustomError` +/// for backwards compatibility and to support the most common use case. +/// +/// A custom error type should implement `FromStr` and `Display`, which allows it to be converted +/// into and from a string easily to be sent over the network. It does *not* need to implement +/// `Serialize` and `Deserialize`, although these can be used to generate the `FromStr`/`Display` +/// 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<String, ServerFnError<InvalidArgument>> { + if text.len() < 5 { + Err(InvalidArgument::TooShort.into()) + } else if text.len() > 15 { + Err(InvalidArgument::TooLong.into()) + } else if text.is_ascii() { + Ok(text.to_ascii_uppercase()) + } else { + Err(InvalidArgument::NotAscii.into()) + } +} + +// The EnumString and Display derive macros are provided by strum +#[derive(Debug, Clone, EnumString, Display)] +pub enum InvalidArgument { + TooShort, + TooLong, + NotAscii, +} + +#[component] +pub fn CustomErrorTypes() -> impl IntoView { + let input_ref = NodeRef::<Input>::new(); + let (result, set_result) = create_signal(None); + + view! { + <h3>Using custom error types</h3> + <p> + "Server functions can use a custom error type that is preserved across the network boundary." + </p> + <p> + "Try typing a message that is between 5 and 15 characters of ASCII text below. Then try breaking \ + the rules!" + </p> + <input node_ref=input_ref placeholder="Type something here."/> + <button + on:click=move |_| { + let value = input_ref.get().unwrap().value(); + spawn_local(async move { + let data = ascii_uppercase(value).await; + set_result(Some(data)); + }); + } + > + "Submit" + </button> + <p> + {move || format!("{:?}", result.get())} + </p> + } +} From 9a5a102ce321c9a9c7ef3c41c0fa555722117744 Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Mon, 15 Jan 2024 21:02:52 -0500 Subject: [PATCH 062/100] add middleware to kitchen-sink example --- examples/Makefile.toml | 1 + examples/server_fns_axum/Cargo.toml | 2 +- examples/server_fns_axum/Todos.db | Bin 16384 -> 0 bytes examples/server_fns_axum/src/app.rs | 11 +++++- examples/server_fns_axum/src/lib.rs | 2 ++ examples/server_fns_axum/src/main.rs | 15 +++----- examples/server_fns_axum/src/middleware.rs | 40 +++++++++++++++++++++ 7 files changed, 58 insertions(+), 13 deletions(-) delete mode 100644 examples/server_fns_axum/Todos.db create mode 100644 examples/server_fns_axum/src/middleware.rs diff --git a/examples/Makefile.toml b/examples/Makefile.toml index ec6dffcb95..c41d30ed5d 100644 --- a/examples/Makefile.toml +++ b/examples/Makefile.toml @@ -21,6 +21,7 @@ CARGO_MAKE_CRATE_WORKSPACE_MEMBERS = [ "login_with_token_csr_only", "parent_child", "router", + "server_fns_axum", "session_auth_axum", "slots", "ssr_modes", diff --git a/examples/server_fns_axum/Cargo.toml b/examples/server_fns_axum/Cargo.toml index dfd06f3825..58c9e42186 100644 --- a/examples/server_fns_axum/Cargo.toml +++ b/examples/server_fns_axum/Cargo.toml @@ -21,7 +21,7 @@ simple_logger = "4.0" serde = { version = "1", features = ["derive"] } axum = { version = "0.7", optional = true } tower = { version = "0.4", optional = true } -tower-http = { version = "0.5", features = ["fs"], optional = true } +tower-http = { version = "0.5", features = ["fs", "tracing", "trace"], optional = true } tokio = { version = "1", features = ["full"], optional = true } thiserror = "1.0" wasm-bindgen = "0.2" diff --git a/examples/server_fns_axum/Todos.db b/examples/server_fns_axum/Todos.db deleted file mode 100644 index ec85d2b07f9ac6b3b931f4599e3e7a35107050f5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16384 zcmeI&KTH!*90%}sE!UPtcp%22i!UxpV!;*@34?ltqZCT3$6;jgtk;KLXgTOzl^7O` zi%w1&P5iUy>R^n6qiha3x#^%?j2nqYH{WY%qfKPP!F*ru_4U2q?_J-0uFExV_Dt6C zIa_cm<$}+KNe@vJa*8oRNLthmQJW<oKK$NSnt^TFG#RWs?xHRUb_WQ(+8v;S+hijI z0SG_<0uX=z1Rwwb2tWV=J1B5{AP|nl<H`-yFI+5f%Uda}T4m=_rQkd6lGol3jv2aU z>de$qS)H{<+2JS?<qEHOVlqo*(wUseP8*pS&6sD?`uvf4yvRMf;w;NAV5WY~WVyVk zbJ=Vo>@0bHp;Y2UOU5%Z`mCwVoNgttaeYFY%bIL#&M@?xX|+-s)4i%~bI)U`d_Jpd zxz@Z!)V|E^MXy?Bscb&g7UQejuF6$ezEkGS<u?}>9XJ^b#b)CQah8gFRX~YHXRA>4 z-Fn|@$FLIZBl7V%BJbsTND0ScF=c(Rj_$if*Q>t;{@|?-MWv}zl&*~;NcFUCwASA4 z>pOmlZ{?oVjIl}0ke6<|<>eChxjfSrO?04(ZjvCa5&DyUr8T-qKhT%-S@X4nKRcWn z0uX=z1Rwwb2tWV=5P$##AOL~C3551_klx7DwA$Z6Xk<#H9q5asCo`eFay&gL2EsIw zR`<y8gxV(?UG0_4nA#(oR3l|v4auI?CAtc|PsD)&0SG_<0uX=z1Rwwb2tWV=5P-lx z7bq%Wb!fP=6x?0xzy0QdUEu|>QD5!|VD&u%$)m|+JdubeMp$BGG<j?^aXkOz;mtc6 z2iJDJ*uT(!!kvFm`$i9K)IPudasKVQr?>9WPeWhsR@d?$gV&zFe`P!h=T@TPx)l1I khyw)z5P$##AOHafKmY;|fB*y_0D=EXpfjj;HvU%l1<a}70ssI2 diff --git a/examples/server_fns_axum/src/app.rs b/examples/server_fns_axum/src/app.rs index b37526b805..82445a2eb9 100644 --- a/examples/server_fns_axum/src/app.rs +++ b/examples/server_fns_axum/src/app.rs @@ -232,8 +232,11 @@ pub fn WithActionForm() -> impl IntoView { input = GetUrl, // it will return its output using SerdeLite // (this needs to be enabled with the `serde-lite` feature on the `server_fn` crate - output = SerdeLite + output = SerdeLite, )] +// You can use the `#[middleware]` macro to add appropriate middleware +// In this case, any `tower::Layer` that takes services of `Request<Body>` will work +#[middleware(crate::middleware::LoggingLayer)] pub async fn length_of_input(input: String) -> Result<usize, ServerFnError> { // insert a simulated wait tokio::time::sleep(std::time::Duration::from_millis(250)).await; @@ -248,6 +251,12 @@ pub fn ServerFnArgumentExample() -> impl IntoView { view! { <h3>Custom arguments to the <code>#[server]</code> " macro"</h3> <p> + This example shows how to specify additional behavior including + <ul> + <li>Specific server function <strong>paths</strong></li> + <li>Mixing and matching input and output <strong>encodings</strong></li> + <li>Adding custom <strong>middleware</strong> on a per-server-fn basis</li> + </ul> </p> <input node_ref=input_ref placeholder="Type something here."/> <button diff --git a/examples/server_fns_axum/src/lib.rs b/examples/server_fns_axum/src/lib.rs index 36750be4e4..b3ca16a250 100644 --- a/examples/server_fns_axum/src/lib.rs +++ b/examples/server_fns_axum/src/lib.rs @@ -3,6 +3,8 @@ pub mod error_template; pub mod errors; #[cfg(feature = "ssr")] pub mod fallback; +#[cfg(feature = "ssr")] +pub mod middleware; #[wasm_bindgen::prelude::wasm_bindgen] pub fn hydrate() { diff --git a/examples/server_fns_axum/src/main.rs b/examples/server_fns_axum/src/main.rs index ba03453b4a..5a4d3b933d 100644 --- a/examples/server_fns_axum/src/main.rs +++ b/examples/server_fns_axum/src/main.rs @@ -1,13 +1,6 @@ -use crate::{fallback::file_and_error_handler, app::*}; -use axum::{ - body::Body, - extract::{Path, State}, - http::Request, - response::{IntoResponse, Response}, - routing::get, - Router, -}; -use leptos::*; +use crate::{app::*, fallback::file_and_error_handler}; +use axum::Router; +use leptos::{get_configuration, logging}; use leptos_axum::{generate_route_list, LeptosRoutes}; use server_fns_axum::*; @@ -24,7 +17,7 @@ async fn main() { // build our application with a route let app = Router::new() - .leptos_routes(&leptos_options, routes, || view! { <TodoApp/> }) + .leptos_routes(&leptos_options, routes, TodoApp) .fallback(file_and_error_handler) .with_state(leptos_options); diff --git a/examples/server_fns_axum/src/middleware.rs b/examples/server_fns_axum/src/middleware.rs new file mode 100644 index 0000000000..cab68babeb --- /dev/null +++ b/examples/server_fns_axum/src/middleware.rs @@ -0,0 +1,40 @@ +use axum::body::Body; +use http::Request; +use std::task::{Context, Poll}; +use tower::{Layer, Service}; + +pub struct LoggingLayer; + +impl<S> Layer<S> for LoggingLayer { + type Service = LoggingService<S>; + + fn layer(&self, inner: S) -> Self::Service { + LoggingService { inner } + } +} + +pub struct LoggingService<T> { + inner: T, +} + +impl<T> Service<Request<Body>> for LoggingService<T> +where + T: Service<Request<Body>>, +{ + type Response = T::Response; + type Error = T::Error; + type Future = T::Future; + + fn poll_ready( + &mut self, + cx: &mut Context<'_>, + ) -> Poll<Result<(), Self::Error>> { + self.inner.poll_ready(cx) + } + + fn call(&mut self, req: Request<Body>) -> Self::Future { + println!("Running my middleware!"); + + self.inner.call(req) + } +} From 21f8085851697c7bd400b2c4ea190d7373ca4a02 Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Tue, 16 Jan 2024 13:08:46 -0500 Subject: [PATCH 063/100] add streaming/file watcher example --- examples/server_fns_axum/Cargo.toml | 3 ++ examples/server_fns_axum/src/app.rs | 62 ++++++++++++++++++++++++++++- 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/examples/server_fns_axum/Cargo.toml b/examples/server_fns_axum/Cargo.toml index 58c9e42186..c8e4d6f78d 100644 --- a/examples/server_fns_axum/Cargo.toml +++ b/examples/server_fns_axum/Cargo.toml @@ -29,6 +29,7 @@ serde_toml = "0.0.1" toml = "0.8.8" web-sys = { version = "0.3.67", features = ["FileList", "File"] } strum = { version = "0.25.0", features = ["strum_macros", "derive"] } +notify = { version = "6.1.1", optional = true } [features] hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"] @@ -41,7 +42,9 @@ ssr = [ "leptos_meta/ssr", "leptos_router/ssr", "dep:leptos_axum", + "dep:notify" ] +notify = ["dep:notify"] [package.metadata.cargo-all-features] denylist = ["axum", "tower", "tower-http", "tokio", "leptos_axum"] diff --git a/examples/server_fns_axum/src/app.rs b/examples/server_fns_axum/src/app.rs index 82445a2eb9..59944a9cf9 100644 --- a/examples/server_fns_axum/src/app.rs +++ b/examples/server_fns_axum/src/app.rs @@ -1,8 +1,10 @@ +use futures::StreamExt; use leptos::{html::Input, *}; use leptos_meta::{provide_meta_context, Link, Stylesheet}; use leptos_router::{ActionForm, Route, Router, Routes}; use server_fn::codec::{ - GetUrl, MultipartData, MultipartFormData, Rkyv, SerdeLite, + GetUrl, MultipartData, MultipartFormData, Rkyv, SerdeLite, StreamingText, + TextStream, }; #[cfg(feature = "ssr")] use std::sync::{ @@ -46,6 +48,7 @@ pub fn HomePage() -> impl IntoView { <ServerFnArgumentExample/> <RkyvExample/> <FileUpload/> + <FileWatcher/> } } @@ -381,6 +384,63 @@ pub fn FileUpload() -> impl IntoView { } } +#[component] +pub fn FileWatcher() -> impl IntoView { + #[server(input = GetUrl, output = StreamingText)] + pub async fn watched_files() -> Result<TextStream, ServerFnError> { + use notify::{ + Config, Error, Event, RecommendedWatcher, RecursiveMode, Watcher, + }; + use std::path::Path; + + let (tx, rx) = futures::channel::mpsc::unbounded(); + + let mut watcher = RecommendedWatcher::new( + move |res: Result<Event, Error>| { + if let Ok(ev) = res { + if let Some(path) = ev.paths.last() { + let filename = path + .file_name() + .unwrap() + .to_str() + .unwrap() + .to_string(); + _ = tx.unbounded_send(filename); //res); + } + } + }, + Config::default(), + )?; + watcher.watch(Path::new("."), RecursiveMode::Recursive)?; + std::mem::forget(watcher); + + Ok(TextStream::from(rx)) + } + + let (files, set_files) = create_signal(Vec::new()); + + create_effect(move |_| { + spawn_local(async move { + while let Some(res) = + watched_files().await.unwrap().into_inner().next().await + { + if let Ok(filename) = res { + set_files.update(|n| n.push(filename)); + } + } + }); + }); + + view! { + <h3>Watching files and returning a streaming response</h3> + <p>Add or remove some text files in the root directory of the project and see the list of changes here.</p> + <p>Files changed since you loaded the page:</p> + <ul> + {move || files.get().into_iter().map(|file| view! { <li><code>{file}</code></li> }).collect::<Vec<_>>()} + </ul> + } +} + /// The `ServerFnError` type is generic over a custom error type, which defaults to `NoCustomError` /// for backwards compatibility and to support the most common use case. /// From 6a8c26a8208d82a23d308ccef7edb69a68a2aefc Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Tue, 16 Jan 2024 20:01:26 -0500 Subject: [PATCH 064/100] streaming example with filesystem watcher --- examples/server_fns_axum/src/app.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/examples/server_fns_axum/src/app.rs b/examples/server_fns_axum/src/app.rs index 59944a9cf9..93d6780cca 100644 --- a/examples/server_fns_axum/src/app.rs +++ b/examples/server_fns_axum/src/app.rs @@ -1,6 +1,6 @@ use futures::StreamExt; use leptos::{html::Input, *}; -use leptos_meta::{provide_meta_context, Link, Stylesheet}; +use leptos_meta::{provide_meta_context, Link, Meta, Stylesheet}; use leptos_router::{ActionForm, Route, Router, Routes}; use server_fn::codec::{ GetUrl, MultipartData, MultipartFormData, Rkyv, SerdeLite, StreamingText, @@ -20,6 +20,7 @@ pub fn TodoApp() -> impl IntoView { provide_meta_context(); view! { + <Meta name="color-scheme" content="dark-light"/> <Link rel="shortcut icon" type_="image/ico" href="/favicon.ico"/> <Stylesheet id="leptos" href="/pkg/server_fns_axum.css"/> <Router> @@ -411,7 +412,8 @@ pub fn FileWatcher() -> impl IntoView { }, Config::default(), )?; - watcher.watch(Path::new("."), RecursiveMode::Recursive)?; + watcher + .watch(Path::new("./watched_files"), RecursiveMode::Recursive)?; std::mem::forget(watcher); Ok(TextStream::from(rx)) @@ -433,11 +435,11 @@ pub fn FileWatcher() -> impl IntoView { view! { <h3>Watching files and returning a streaming response</h3> - <p>Add or remove some text files in the root directory of the project and see the list of changes here.</p> <p>Files changed since you loaded the page:</p> <ul> {move || files.get().into_iter().map(|file| view! { <li><code>{file}</code></li> }).collect::<Vec<_>>()} </ul> + <p><em>Add or remove some text files in the <code>watched_files</code> directory and see the list of changes here.</em></p> } } From bb923b3f9b30f6f892b4decd4268efec34e43df4 Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Tue, 16 Jan 2024 20:02:47 -0500 Subject: [PATCH 065/100] erroneous hyphen --- examples/server_fns_axum/src/app.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/server_fns_axum/src/app.rs b/examples/server_fns_axum/src/app.rs index 93d6780cca..0e19fa075c 100644 --- a/examples/server_fns_axum/src/app.rs +++ b/examples/server_fns_axum/src/app.rs @@ -20,7 +20,7 @@ pub fn TodoApp() -> impl IntoView { provide_meta_context(); view! { - <Meta name="color-scheme" content="dark-light"/> + <Meta name="color-scheme" content="dark light"/> <Link rel="shortcut icon" type_="image/ico" href="/favicon.ico"/> <Stylesheet id="leptos" href="/pkg/server_fns_axum.css"/> <Router> From 7a086ad1591be6e31fe05601d16fb22de90d3857 Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Tue, 16 Jan 2024 20:03:11 -0500 Subject: [PATCH 066/100] update version number --- Cargo.toml | 28 ++++++++++++++-------------- integrations/axum/src/lib.rs | 2 +- meta/Cargo.toml | 2 +- router/Cargo.toml | 2 +- 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index bb50dfc470..ece842107b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,22 +25,22 @@ members = [ exclude = ["benchmarks", "examples"] [workspace.package] -version = "0.5.7" +version = "0.6.0-beta" [workspace.dependencies] -leptos = { path = "./leptos", version = "0.5.7" } -leptos_dom = { path = "./leptos_dom", version = "0.5.7" } -leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.5.7" } -leptos_macro = { path = "./leptos_macro", version = "0.5.7" } -leptos_reactive = { path = "./leptos_reactive", version = "0.5.7" } -leptos_server = { path = "./leptos_server", version = "0.5.7" } -server_fn = { path = "./server_fn", version = "0.5.7" } -server_fn_macro = { path = "./server_fn_macro", version = "0.5.7" } -server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.5.7" } -leptos_config = { path = "./leptos_config", version = "0.5.7" } -leptos_router = { path = "./router", version = "0.5.7" } -leptos_meta = { path = "./meta", version = "0.5.7" } -leptos_integration_utils = { path = "./integrations/utils", version = "0.5.7" } +leptos = { path = "./leptos", version = "0.6.0-beta" } +leptos_dom = { path = "./leptos_dom", version = "0.6.0-beta" } +leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.6.0-beta" } +leptos_macro = { path = "./leptos_macro", version = "0.6.0-beta" } +leptos_reactive = { path = "./leptos_reactive", version = "0.6.0-beta" } +leptos_server = { path = "./leptos_server", version = "0.6.0-beta" } +server_fn = { path = "./server_fn", version = "0.6" } +server_fn_macro = { path = "./server_fn_macro", version = "0.6" } +server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.6" } +leptos_config = { path = "./leptos_config", version = "0.6.0-beta" } +leptos_router = { path = "./router", version = "0.6.0-beta" } +leptos_meta = { path = "./meta", version = "0.6.0-beta" } +leptos_integration_utils = { path = "./integrations/utils", version = "0.6.0-beta" } [profile.release] codegen-units = 1 diff --git a/integrations/axum/src/lib.rs b/integrations/axum/src/lib.rs index 57c9064029..2efd11fcd6 100644 --- a/integrations/axum/src/lib.rs +++ b/integrations/axum/src/lib.rs @@ -7,7 +7,7 @@ //! To run in this environment, you need to disable the default feature set and enable //! the `wasm` feature on `leptos_axum` in your `Cargo.toml`. //! ```toml -//! leptos_axum = { version = "", default-features = false, features = ["wasm"] } +//! leptos_axum = { version = "0.6.0-beta", default-features = false, features = ["wasm"] } //! ``` //! //! ## Features diff --git a/meta/Cargo.toml b/meta/Cargo.toml index 5feea8f32d..8060a2ab5e 100644 --- a/meta/Cargo.toml +++ b/meta/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "leptos_meta" -version = "0.5.7" +version = "0.6.0-beta" edition = "2021" authors = ["Greg Johnston"] license = "MIT" diff --git a/router/Cargo.toml b/router/Cargo.toml index fedd13081a..8c7c53db0f 100644 --- a/router/Cargo.toml +++ b/router/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "leptos_router" -version = "0.5.7" +version = "0.6.0-beta" edition = "2021" authors = ["Greg Johnston"] license = "MIT" From 047235e7c187df8aed1e04158bb185ca4f130d50 Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Wed, 17 Jan 2024 07:48:30 -0500 Subject: [PATCH 067/100] clippy --- server_fn/src/error.rs | 2 +- server_fn/src/lib.rs | 19 +++++++++++-------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/server_fn/src/error.rs b/server_fn/src/error.rs index 5352a8d598..fa84d9128c 100644 --- a/server_fn/src/error.rs +++ b/server_fn/src/error.rs @@ -10,7 +10,7 @@ use thiserror::Error; use url::Url; /// A custom header that can be used to indicate a server function returned an error. -pub const SERVER_FN_ERROR_HEADER: &'static str = "serverfnerror"; +pub const SERVER_FN_ERROR_HEADER: &str = "serverfnerror"; /// This is a result type into which any error can be converted, /// and which can be used directly in your `view`. diff --git a/server_fn/src/lib.rs b/server_fn/src/lib.rs index d8cb5a4919..b51506b3af 100644 --- a/server_fn/src/lib.rs +++ b/server_fn/src/lib.rs @@ -125,7 +125,9 @@ use codec::{Encoding, FromReq, FromRes, IntoReq, IntoRes}; pub use const_format; use dashmap::DashMap; pub use error::ServerFnError; -use error::{ServerFnErrorSerde, ServerFnUrlError}; +use error::ServerFnErrorSerde; +#[cfg(feature = "form-redirects")] +use error::ServerFnUrlError; use http::Method; use middleware::{Layer, Service}; use once_cell::sync::Lazy; @@ -240,16 +242,17 @@ where // Server functions can either be called by a real Client, // or directly by an HTML <form>. If they're accessed by a <form>, default to // redirecting back to the Referer. - let accepts_html = if cfg!(feature = "form-redirects") { - req.accepts() - .map(|n| n.contains("text/html")) - .unwrap_or(false) - } else { - false - }; + #[cfg(feature = "form-redirects")] + let accepts_html = req + .accepts() + .map(|n| n.contains("text/html")) + .unwrap_or(false); + #[cfg(feature = "form-redirects")] let mut referer = req.referer().as_deref().map(ToOwned::to_owned); async move { + #[allow(unused_variables, unused_mut)] + // used in form redirects feature let (mut res, err) = Self::execute_on_server(req) .await .map(|res| (res, None)) From c7fac640548e8fea0b734078a8705d46ae880993 Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Wed, 17 Jan 2024 07:49:21 -0500 Subject: [PATCH 068/100] fix merge error --- server_fn_macro/src/lib.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/server_fn_macro/src/lib.rs b/server_fn_macro/src/lib.rs index 0b5ec358b9..72c71d9450 100644 --- a/server_fn_macro/src/lib.rs +++ b/server_fn_macro/src/lib.rs @@ -786,7 +786,6 @@ impl Parse for ServerFnArgs { } Ok(Self { - _attrs, struct_name, prefix, input, From 8d23d5136aa450bd1c4b1372150721773a01d330 Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Wed, 17 Jan 2024 07:55:23 -0500 Subject: [PATCH 069/100] add package metadata --- server_fn/Cargo.toml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/server_fn/Cargo.toml b/server_fn/Cargo.toml index e143c62c8b..a00daba1a3 100644 --- a/server_fn/Cargo.toml +++ b/server_fn/Cargo.toml @@ -1,7 +1,12 @@ [package] name = "server_fn" -version = "0.6.0" +version = { workspace = true } edition = "2021" +authors = ["Greg Johnston"] +license = "MIT" +repository = "https://github.com/leptos-rs/leptos" +description = "RPC for any web framework." +readme = "../README.md" [dependencies] server_fn_macro_default = { workspace = true} From 22b4537f2789c9c07cdd6bb128da932d6deb260d Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Wed, 17 Jan 2024 07:57:04 -0500 Subject: [PATCH 070/100] fix version numbers --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index ece842107b..59e344f198 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,8 +34,8 @@ leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.6.0-beta" } leptos_macro = { path = "./leptos_macro", version = "0.6.0-beta" } leptos_reactive = { path = "./leptos_reactive", version = "0.6.0-beta" } leptos_server = { path = "./leptos_server", version = "0.6.0-beta" } -server_fn = { path = "./server_fn", version = "0.6" } -server_fn_macro = { path = "./server_fn_macro", version = "0.6" } +server_fn = { path = "./server_fn", version = "0.6.0-beta" } +server_fn_macro = { path = "./server_fn_macro", version = "0.6.0-beta" } server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.6" } leptos_config = { path = "./leptos_config", version = "0.6.0-beta" } leptos_router = { path = "./router", version = "0.6.0-beta" } From 5065bed594f86318fd6851ce258f0684aa34d590 Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Wed, 17 Jan 2024 10:29:45 -0500 Subject: [PATCH 071/100] example of middleware that can run before and/or after server fn --- examples/server_fns_axum/Cargo.toml | 1 + examples/server_fns_axum/src/app.rs | 1 + examples/server_fns_axum/src/middleware.rs | 40 +++++++++++++++++++--- 3 files changed, 38 insertions(+), 4 deletions(-) diff --git a/examples/server_fns_axum/Cargo.toml b/examples/server_fns_axum/Cargo.toml index c8e4d6f78d..d2996e5e9c 100644 --- a/examples/server_fns_axum/Cargo.toml +++ b/examples/server_fns_axum/Cargo.toml @@ -30,6 +30,7 @@ toml = "0.8.8" web-sys = { version = "0.3.67", features = ["FileList", "File"] } strum = { version = "0.25.0", features = ["strum_macros", "derive"] } notify = { version = "6.1.1", optional = true } +pin-project-lite = "0.2.13" [features] hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"] diff --git a/examples/server_fns_axum/src/app.rs b/examples/server_fns_axum/src/app.rs index 0e19fa075c..e6a5b5e5ef 100644 --- a/examples/server_fns_axum/src/app.rs +++ b/examples/server_fns_axum/src/app.rs @@ -242,6 +242,7 @@ pub fn WithActionForm() -> impl IntoView { // In this case, any `tower::Layer` that takes services of `Request<Body>` will work #[middleware(crate::middleware::LoggingLayer)] pub async fn length_of_input(input: String) -> Result<usize, ServerFnError> { + println!("2. Running server function."); // insert a simulated wait tokio::time::sleep(std::time::Duration::from_millis(250)).await; Ok(input.len()) diff --git a/examples/server_fns_axum/src/middleware.rs b/examples/server_fns_axum/src/middleware.rs index cab68babeb..7af6dbddef 100644 --- a/examples/server_fns_axum/src/middleware.rs +++ b/examples/server_fns_axum/src/middleware.rs @@ -1,6 +1,11 @@ use axum::body::Body; use http::Request; -use std::task::{Context, Poll}; +use pin_project_lite::pin_project; +use std::{ + future::Future, + pin::Pin, + task::{Context, Poll}, +}; use tower::{Layer, Service}; pub struct LoggingLayer; @@ -23,7 +28,7 @@ where { type Response = T::Response; type Error = T::Error; - type Future = T::Future; + type Future = LoggingServiceFuture<T::Future>; fn poll_ready( &mut self, @@ -33,8 +38,35 @@ where } fn call(&mut self, req: Request<Body>) -> Self::Future { - println!("Running my middleware!"); + println!("1. Running my middleware!"); - self.inner.call(req) + LoggingServiceFuture { + inner: self.inner.call(req), + } + } +} + +pin_project! { + pub struct LoggingServiceFuture<T> { + #[pin] + inner: T, + } +} + +impl<T> Future for LoggingServiceFuture<T> +where + T: Future, +{ + type Output = T::Output; + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> { + let this = self.project(); + match this.inner.poll(cx) { + Poll::Pending => Poll::Pending, + Poll::Ready(output) => { + println!("3. Running my middleware!"); + Poll::Ready(output) + } + } } } From 320179bc040d19cc7954340ab1603bbaf99cf0df Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Wed, 17 Jan 2024 19:30:48 -0500 Subject: [PATCH 072/100] remove misleading warning --- router/src/components/router.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/router/src/components/router.rs b/router/src/components/router.rs index 2c63fee8f4..c35c499f0c 100644 --- a/router/src/components/router.rs +++ b/router/src/components/router.rs @@ -67,9 +67,7 @@ pub fn Router( } }); }) as RedirectHook; - if server_fn::redirect::set_redirect_hook(router_hook).is_err() { - debug_warn!("Error setting <Router/> server function redirect hook."); - } + _ = server_fn::redirect::set_redirect_hook(router_hook); // provide ServerFnUrlError if it exists let location = use_location(); From c5bab094239a349c91cd5f2f098f0204696cb0bc Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Wed, 17 Jan 2024 21:26:36 -0500 Subject: [PATCH 073/100] partial support for streaming requests (doesn't actually work in the browser) --- examples/server_fns_axum/Cargo.toml | 2 +- examples/server_fns_axum/src/app.rs | 45 +++++++++++++ server_fn/src/codec/stream.rs | 99 +++++++++++++++++++++++++---- server_fn/src/request/actix.rs | 5 +- server_fn/src/request/axum.rs | 7 +- server_fn/src/request/browser.rs | 23 +++++++ server_fn/src/request/mod.rs | 15 ++++- server_fn/src/request/reqwest.rs | 32 +++++++++- server_fn/src/response/mod.rs | 2 +- server_fn_macro/src/lib.rs | 4 +- 10 files changed, 213 insertions(+), 21 deletions(-) diff --git a/examples/server_fns_axum/Cargo.toml b/examples/server_fns_axum/Cargo.toml index d2996e5e9c..77bbbea58b 100644 --- a/examples/server_fns_axum/Cargo.toml +++ b/examples/server_fns_axum/Cargo.toml @@ -15,7 +15,7 @@ leptos = { path = "../../leptos", features = ["nightly"] } leptos_axum = { path = "../../integrations/axum", optional = true } leptos_meta = { path = "../../meta", features = ["nightly"] } leptos_router = { path = "../../router", features = ["nightly"] } -server_fn = { path = "../../server_fn", features = ["serde-lite", "rkyv", "multipart"] } +server_fn = { path = "../../server_fn", features = ["serde-lite", "rkyv", "multipart" ]} log = "0.4" simple_logger = "4.0" serde = { version = "1", features = ["derive"] } diff --git a/examples/server_fns_axum/src/app.rs b/examples/server_fns_axum/src/app.rs index e6a5b5e5ef..acfdcdfb14 100644 --- a/examples/server_fns_axum/src/app.rs +++ b/examples/server_fns_axum/src/app.rs @@ -50,6 +50,7 @@ pub fn HomePage() -> impl IntoView { <RkyvExample/> <FileUpload/> <FileWatcher/> + <StreamingValues/> } } @@ -506,3 +507,47 @@ pub fn CustomErrorTypes() -> impl IntoView { </p> } } + +#[component] +pub fn StreamingValues() -> impl IntoView { + use futures::StreamExt; + + /// You can create server functions that accept streaming values by using the encoding + /// `Streaming` (with type `ByteStream`) or encoding `StreamingText` (with type `TextStream`) + #[server(input = StreamingText, output = StreamingText)] + pub async fn streaming(input: TextStream) -> Result<TextStream, ServerFnError> { + println!("inside streaming() fn"); + Ok(TextStream::from(input.into_inner().map(|text| format!("{}!!!", text.unwrap_or_else(|e| e.to_string()))))) + } + + let mut count = 0; + let (tx, rx) = futures::channel::mpsc::unbounded(); + let (result, set_result) = create_signal("Click me...".to_string()); + + + if cfg!(feature = "hydrate") { + spawn_local(async move { + logging::log!("calling streaming server fn"); + match streaming(TextStream::new(rx)).await { + Ok(res) => { + logging::log!("after calling streaming()"); + let mut stream = res.into_inner(); + while let Some(chunk) = stream.next().await { + set_result(chunk.unwrap_or_else(|e| e.to_string())); + } + }, Err(e) => logging::log!("{e}") } + }) + } + + view! { + <h3>Streaming arguments and responses</h3> + <button + on:click=move |_| { + count += 1; + tx.unbounded_send(Ok(count.to_string())).expect("couldn't send into channel"); + } + > + {result} + </button> + } +} diff --git a/server_fn/src/codec/stream.rs b/server_fn/src/codec/stream.rs index faafba94eb..14d67c46bf 100644 --- a/server_fn/src/codec/stream.rs +++ b/server_fn/src/codec/stream.rs @@ -1,13 +1,14 @@ -use super::{Encoding, FromRes}; +use super::{Encoding, FromReq, FromRes, IntoReq}; use crate::{ error::{NoCustomError, ServerFnError}, + request::{ClientReq, Req}, response::{ClientRes, Res}, IntoRes, }; use bytes::Bytes; use futures::{Stream, StreamExt}; use http::Method; -use std::pin::Pin; +use std::{fmt::Debug, pin::Pin}; /// An encoding that represents a stream of bytes. /// @@ -19,25 +20,31 @@ impl Encoding for Streaming { const METHOD: Method = Method::POST; } -/* impl<CustErr, T, Request> IntoReq<ByteStream, Request, CustErr> for T +impl<CustErr, T, Request> IntoReq<Streaming, Request, CustErr> for T where Request: ClientReq<CustErr>, - T: Stream<Item = Bytes> + Send, + T: Stream<Item = Bytes> + Send + Sync + 'static, { - fn into_req(self, path: &str, accepts: &str) -> Result<Request, ServerFnError<CustErr>> { - Request::try_new_stream(path, ByteStream::CONTENT_TYPE, self) + fn into_req( + self, + path: &str, + accepts: &str, + ) -> Result<Request, ServerFnError<CustErr>> { + Request::try_new_streaming(path, accepts, Streaming::CONTENT_TYPE, self) } -} */ +} -/* impl<CustErr, T, Request> FromReq<ByteStream, Request, CustErr> for T +impl<CustErr, T, Request> FromReq<Streaming, Request, CustErr> for T where Request: Req<CustErr> + Send + 'static, - T: Stream<Item = Bytes> + Send, + T: From<ByteStream> + 'static, { async fn from_req(req: Request) -> Result<Self, ServerFnError<CustErr>> { - req.try_into_stream().await + let data = req.try_into_stream()?; + let s = ByteStream::new(data); + Ok(s.into()) } -} */ +} /// A stream of bytes. /// @@ -55,6 +62,24 @@ impl<CustErr> ByteStream<CustErr> { } } +impl<CustErr> Debug for ByteStream<CustErr> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_tuple("ByteStream").finish() + } +} + +impl ByteStream { + /// Creates a new `ByteStream` from the given stream. + pub fn new<T>( + value: impl Stream<Item = Result<T, ServerFnError>> + Send + 'static, + ) -> Self + where + T: Into<Bytes>, + { + Self(Box::pin(value.map(|value| value.map(Into::into)))) + } +} + impl<S, T> From<S> for ByteStream where S: Stream<Item = T> + Send + 'static, @@ -103,6 +128,21 @@ pub struct TextStream<CustErr = NoCustomError>( Pin<Box<dyn Stream<Item = Result<String, ServerFnError<CustErr>>> + Send>>, ); +impl<CustErr> Debug for TextStream<CustErr> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_tuple("TextStream").finish() + } +} + +impl TextStream { + /// Creates a new `ByteStream` from the given stream. + pub fn new( + value: impl Stream<Item = Result<String, ServerFnError>> + Send + 'static, + ) -> Self { + Self(Box::pin(value.map(|value| value.map(Into::into)))) + } +} + impl<CustErr> TextStream<CustErr> { /// Consumes the wrapper, returning a stream of text. pub fn into_inner( @@ -122,6 +162,43 @@ where } } +impl<CustErr, T, Request> IntoReq<StreamingText, Request, CustErr> for T +where + Request: ClientReq<CustErr>, + T: Into<TextStream>, +{ + fn into_req( + self, + path: &str, + accepts: &str, + ) -> Result<Request, ServerFnError<CustErr>> { + let data = self.into(); + Request::try_new_streaming( + path, + accepts, + Streaming::CONTENT_TYPE, + data.0.map(|chunk| chunk.unwrap_or_default().into()), + ) + } +} + +impl<CustErr, T, Request> FromReq<StreamingText, Request, CustErr> for T +where + Request: Req<CustErr> + Send + 'static, + T: From<TextStream> + 'static, +{ + async fn from_req(req: Request) -> Result<Self, ServerFnError<CustErr>> { + 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())) + }) + })); + Ok(s.into()) + } +} + impl<CustErr, Response> IntoRes<StreamingText, Response, CustErr> for TextStream<CustErr> where diff --git a/server_fn/src/request/actix.rs b/server_fn/src/request/actix.rs index 05c801a19b..90e8e1f26a 100644 --- a/server_fn/src/request/actix.rs +++ b/server_fn/src/request/actix.rs @@ -33,7 +33,10 @@ impl From<(HttpRequest, Payload)> for ActixRequest { } } -impl<CustErr> Req<CustErr> for ActixRequest { +impl<CustErr> Req<CustErr> for ActixRequest +where + CustErr: 'static, +{ fn as_query(&self) -> Option<&str> { self.0 .0.uri().query() } diff --git a/server_fn/src/request/axum.rs b/server_fn/src/request/axum.rs index 690eb9e5d6..e26f7c7676 100644 --- a/server_fn/src/request/axum.rs +++ b/server_fn/src/request/axum.rs @@ -8,7 +8,10 @@ use http::{ use http_body_util::BodyExt; use std::borrow::Cow; -impl<CustErr> Req<CustErr> for Request<Body> { +impl<CustErr> Req<CustErr> for Request<Body> +where + CustErr: 'static, +{ fn as_query(&self) -> Option<&str> { self.uri().query() } @@ -49,7 +52,7 @@ impl<CustErr> Req<CustErr> for Request<Body> { fn try_into_stream( self, ) -> Result< - impl Stream<Item = Result<Bytes, ServerFnError>> + Send, + impl Stream<Item = Result<Bytes, ServerFnError>> + Send + 'static, ServerFnError<CustErr>, > { Ok(self.into_body().into_data_stream().map(|chunk| { diff --git a/server_fn/src/request/browser.rs b/server_fn/src/request/browser.rs index b2af59d7fc..b2fda95ff0 100644 --- a/server_fn/src/request/browser.rs +++ b/server_fn/src/request/browser.rs @@ -1,9 +1,12 @@ use super::ClientReq; use crate::{client::get_server_url, error::ServerFnError}; use bytes::Bytes; +use futures::{Stream, StreamExt}; pub use gloo_net::http::Request; use js_sys::Uint8Array; use send_wrapper::SendWrapper; +use wasm_bindgen::JsValue; +use wasm_streams::ReadableStream; use web_sys::{FormData, UrlSearchParams}; /// A `fetch` request made in the browser. @@ -134,4 +137,24 @@ impl<CustErr> ClientReq<CustErr> for BrowserRequest { .map_err(|e| ServerFnError::Request(e.to_string()))?, ))) } + + fn try_new_streaming( + path: &str, + accepts: &str, + content_type: &str, + body: impl Stream<Item = Bytes> + 'static, + ) -> Result<Self, ServerFnError<CustErr>> { + let stream = ReadableStream::from_stream(body.map(|bytes| { + let data = Uint8Array::from(bytes.as_ref()); + let data = JsValue::from(data); + Ok(data) as Result<JsValue, JsValue> + })); + Ok(Self(SendWrapper::new( + Request::post(path) + .header("Content-Type", content_type) + .header("Accept", accepts) + .body(stream.into_raw()) + .map_err(|e| ServerFnError::Request(e.to_string()))?, + ))) + } } diff --git a/server_fn/src/request/mod.rs b/server_fn/src/request/mod.rs index eee926a3ba..e1a0085207 100644 --- a/server_fn/src/request/mod.rs +++ b/server_fn/src/request/mod.rs @@ -62,6 +62,14 @@ where accepts: &str, body: Self::FormData, ) -> Result<Self, ServerFnError<CustErr>>; + + /// Attempts to construct a new `POST` request with a streaming body. + fn try_new_streaming( + path: &str, + accepts: &str, + content_type: &str, + body: impl Stream<Item = Bytes> + Send + 'static, + ) -> Result<Self, ServerFnError<CustErr>>; } /// Represents the request as received by the server. @@ -95,7 +103,7 @@ where fn try_into_stream( self, ) -> Result< - impl Stream<Item = Result<Bytes, ServerFnError>> + Send, + impl Stream<Item = Result<Bytes, ServerFnError>> + Send + 'static, ServerFnError<CustErr>, >; } @@ -104,7 +112,10 @@ where /// when compiling for the browser. pub struct BrowserMockReq; -impl<CustErr> Req<CustErr> for BrowserMockReq { +impl<CustErr> Req<CustErr> for BrowserMockReq +where + CustErr: 'static, +{ fn as_query(&self) -> Option<&str> { unreachable!() } diff --git a/server_fn/src/request/reqwest.rs b/server_fn/src/request/reqwest.rs index bfff453f60..012985470a 100644 --- a/server_fn/src/request/reqwest.rs +++ b/server_fn/src/request/reqwest.rs @@ -1,8 +1,15 @@ use super::ClientReq; -use crate::{client::get_server_url, error::ServerFnError}; +use crate::{ + client::get_server_url, + error::{ServerFnError, ServerFnErrorErr}, +}; use bytes::Bytes; +use futures::{Stream, StreamExt}; use once_cell::sync::Lazy; -use reqwest::header::{ACCEPT, CONTENT_TYPE}; +use reqwest::{ + header::{ACCEPT, CONTENT_TYPE}, + Body, +}; pub use reqwest::{multipart::Form, Client, Method, Request, Url}; pub(crate) static CLIENT: Lazy<Client> = Lazy::new(Client::new); @@ -88,4 +95,25 @@ impl<CustErr> ClientReq<CustErr> for Request { .build() .map_err(|e| ServerFnError::Request(e.to_string())) } + + fn try_new_streaming( + path: &str, + accepts: &str, + content_type: &str, + body: impl Stream<Item = Bytes> + 'static, + ) -> Result<Self, ServerFnError<CustErr>> { + todo!("Streaming requests are not yet implemented for reqwest.") + /* let url = format!("{}{}", get_server_url(), path); + let body = Body::wrap_stream( + body.map(|chunk| Ok(chunk) as Result<Bytes, ServerFnErrorErr>), + ); + CLIENT + .post(url) + .header(CONTENT_TYPE, content_type) + .header(ACCEPT, accepts) + .body(body) + .build() + .map_err(|e| ServerFnError::Request(e.to_string())) + }*/ + } } diff --git a/server_fn/src/response/mod.rs b/server_fn/src/response/mod.rs index 6de74b1cc6..2862eae984 100644 --- a/server_fn/src/response/mod.rs +++ b/server_fn/src/response/mod.rs @@ -64,7 +64,7 @@ pub trait ClientRes<CustErr> { fn try_into_stream( self, ) -> Result< - impl Stream<Item = Result<Bytes, ServerFnError>> + Send + 'static, + impl Stream<Item = Result<Bytes, ServerFnError>> + Send + Sync + 'static, ServerFnError<CustErr>, >; diff --git a/server_fn_macro/src/lib.rs b/server_fn_macro/src/lib.rs index 72c71d9450..03db265357 100644 --- a/server_fn_macro/src/lib.rs +++ b/server_fn_macro/src/lib.rs @@ -351,7 +351,9 @@ pub fn server_macro_impl( Clone, #server_fn_path::rkyv::Archive, #server_fn_path::rkyv::Serialize, #server_fn_path::rkyv::Deserialize }, ), - Some("MultipartFormData") => (PathInfo::None, quote! {}), + Some("MultipartFormData") + | Some("Streaming") + | Some("StreamingText") => (PathInfo::None, quote! {}), Some("SerdeLite") => ( PathInfo::Serde, quote! { From 33ad30515d118d76ef9f8d14bce01879c7aa2ddb Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Wed, 17 Jan 2024 21:27:44 -0500 Subject: [PATCH 074/100] serde-lite support should be enabled directly on server_fn --- leptos/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/leptos/Cargo.toml b/leptos/Cargo.toml index f27a3f4502..194a472c61 100644 --- a/leptos/Cargo.toml +++ b/leptos/Cargo.toml @@ -61,7 +61,7 @@ nightly = [ "leptos_server/nightly", ] serde = ["leptos_reactive/serde"] -serde-lite = ["leptos_reactive/serde-lite", "server_fn/serde-lite"] +serde-lite = ["leptos_reactive/serde-lite"] miniserde = ["leptos_reactive/miniserde"] rkyv = ["leptos_reactive/rkyv"] tracing = ["leptos_macro/tracing"] From 3f22906053ffdbe696e20801a7f35888ff8889bb Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Fri, 19 Jan 2024 11:07:08 -0500 Subject: [PATCH 075/100] fix warning --- router/src/components/router.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/router/src/components/router.rs b/router/src/components/router.rs index c35c499f0c..55056a7b2a 100644 --- a/router/src/components/router.rs +++ b/router/src/components/router.rs @@ -7,7 +7,6 @@ use crate::{ use crate::{unescape, Url}; use cfg_if::cfg_if; use leptos::{ - logging::debug_warn, server_fn::{ error::{ServerFnErrorSerde, ServerFnUrlError}, redirect::RedirectHook, From 0df5dfeaf8cca011b1e0ef03936ffa9e2f4b3649 Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Fri, 19 Jan 2024 11:07:17 -0500 Subject: [PATCH 076/100] weak dependency on Cargo.toml --- server_fn/Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server_fn/Cargo.toml b/server_fn/Cargo.toml index a00daba1a3..f68d0f698e 100644 --- a/server_fn/Cargo.toml +++ b/server_fn/Cargo.toml @@ -98,7 +98,7 @@ multipart = ["dep:multer"] url = ["dep:serde_qs"] cbor = ["dep:ciborium"] rkyv = ["dep:rkyv"] -default-tls = ["reqwest/default-tls"] -rustls = ["reqwest/rustls-tls"] +default-tls = ["reqwest?/default-tls"] +rustls = ["reqwest?/rustls-tls"] reqwest = ["dep:reqwest"] ssr = ["inventory"] From a3a15f244dfec3529bd09805825daff4600f2c6e Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Fri, 19 Jan 2024 11:30:28 -0500 Subject: [PATCH 077/100] expose all fields of ServerFnTraitObj via methods --- server_fn/src/lib.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/server_fn/src/lib.rs b/server_fn/src/lib.rs index b51506b3af..1a2ff628af 100644 --- a/server_fn/src/lib.rs +++ b/server_fn/src/lib.rs @@ -405,6 +405,16 @@ impl<Req, Res> ServerFnTraitObj<Req, Res> { pub fn method(&self) -> Method { self.method.clone() } + + /// The handler for this server function. + pub fn handler(&self, req: Req) -> impl Future<Output = Res> + Send { + (self.handler)(req) + } + + /// The set of middleware that should be applied to this function. + pub fn middleware(&self) -> MiddlewareSet<Req, Res> { + (self.middleware)() + } } impl<Req, Res> Service<Req, Res> for ServerFnTraitObj<Req, Res> From 2fa60103b480e137bca7474e4fe024273608580b Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Fri, 19 Jan 2024 11:39:51 -0500 Subject: [PATCH 078/100] share inventory collect across types --- server_fn/src/lib.rs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/server_fn/src/lib.rs b/server_fn/src/lib.rs index 1a2ff628af..ba67bdb9df 100644 --- a/server_fn/src/lib.rs +++ b/server_fn/src/lib.rs @@ -443,6 +443,17 @@ impl<Req, Res> Clone for ServerFnTraitObj<Req, Res> { type LazyServerFnMap<Req, Res> = Lazy<DashMap<&'static str, ServerFnTraitObj<Req, Res>>>; +#[cfg(feature = "ssr")] +impl<Req: 'static, Res: 'static> inventory::Collect + for ServerFnTraitObj<Req, Res> +{ + #[inline] + fn registry() -> &'static inventory::Registry { + static REGISTRY: inventory::Registry = inventory::Registry::new(); + ®ISTRY + } +} + /// Axum integration. #[cfg(feature = "axum")] pub mod axum { @@ -453,8 +464,6 @@ pub mod axum { use axum::body::Body; use http::{Method, Request, Response, StatusCode}; - inventory::collect!(ServerFnTraitObj<Request<Body>, Response<Body>>); - static REGISTERED_SERVER_FUNCTIONS: LazyServerFnMap< Request<Body>, Response<Body>, @@ -539,8 +548,6 @@ pub mod actix { #[doc(hidden)] pub use send_wrapper::SendWrapper; - inventory::collect!(ServerFnTraitObj<ActixRequest, ActixResponse>); - static REGISTERED_SERVER_FUNCTIONS: LazyServerFnMap< ActixRequest, ActixResponse, From e179db1d4245cb9aa12fdd55b97f2ca0a8feefbf Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Fri, 19 Jan 2024 12:08:27 -0500 Subject: [PATCH 079/100] use `&[u8]` instead of `Bytes` for requests --- server_fn/src/codec/cbor.rs | 7 +------ server_fn/src/codec/rkyv.rs | 10 +++++++--- server_fn/src/request/browser.rs | 3 +-- server_fn/src/request/mod.rs | 2 +- server_fn/src/request/reqwest.rs | 4 ++-- 5 files changed, 12 insertions(+), 14 deletions(-) diff --git a/server_fn/src/codec/cbor.rs b/server_fn/src/codec/cbor.rs index a5a91c8117..051bc266c6 100644 --- a/server_fn/src/codec/cbor.rs +++ b/server_fn/src/codec/cbor.rs @@ -29,12 +29,7 @@ where let mut buffer: Vec<u8> = Vec::new(); ciborium::ser::into_writer(&self, &mut buffer) .map_err(|e| ServerFnError::Serialization(e.to_string()))?; - Request::try_new_post_bytes( - path, - accepts, - Cbor::CONTENT_TYPE, - Bytes::from(buffer), - ) + Request::try_new_post_bytes(path, accepts, Cbor::CONTENT_TYPE, &buffer) } } diff --git a/server_fn/src/codec/rkyv.rs b/server_fn/src/codec/rkyv.rs index 2d9753ad12..ab5ed08ae2 100644 --- a/server_fn/src/codec/rkyv.rs +++ b/server_fn/src/codec/rkyv.rs @@ -35,8 +35,12 @@ where ) -> Result<Request, ServerFnError<CustErr>> { let encoded = rkyv::to_bytes::<T, 1024>(&self) .map_err(|e| ServerFnError::Serialization(e.to_string()))?; - let bytes = Bytes::copy_from_slice(encoded.as_ref()); - Request::try_new_post_bytes(path, accepts, Rkyv::CONTENT_TYPE, bytes) + Request::try_new_post_bytes( + path, + accepts, + Rkyv::CONTENT_TYPE, + encoded.as_ref(), + ) } } @@ -50,7 +54,7 @@ where { async fn from_req(req: Request) -> Result<Self, ServerFnError<CustErr>> { let body_bytes = req.try_into_bytes().await?; - rkyv::from_bytes::<T>(body_bytes.as_ref()) + rkyv::from_bytes::<T>(&body_bytes) .map_err(|e| ServerFnError::Args(e.to_string())) } } diff --git a/server_fn/src/request/browser.rs b/server_fn/src/request/browser.rs index b2fda95ff0..4e5db475a2 100644 --- a/server_fn/src/request/browser.rs +++ b/server_fn/src/request/browser.rs @@ -78,13 +78,12 @@ impl<CustErr> ClientReq<CustErr> for BrowserRequest { path: &str, accepts: &str, content_type: &str, - body: Bytes, + body: &[u8], ) -> Result<Self, ServerFnError<CustErr>> { let server_url = get_server_url(); let mut url = String::with_capacity(server_url.len() + path.len()); url.push_str(server_url); url.push_str(path); - let body: &[u8] = &body; let body = Uint8Array::from(body).buffer(); Ok(Self(SendWrapper::new( Request::post(&url) diff --git a/server_fn/src/request/mod.rs b/server_fn/src/request/mod.rs index e1a0085207..75d8ad17fc 100644 --- a/server_fn/src/request/mod.rs +++ b/server_fn/src/request/mod.rs @@ -45,7 +45,7 @@ where path: &str, content_type: &str, accepts: &str, - body: Bytes, + body: &[u8], ) -> Result<Self, ServerFnError<CustErr>>; /// Attempts to construct a new `POST` request with form data as the body. diff --git a/server_fn/src/request/reqwest.rs b/server_fn/src/request/reqwest.rs index 012985470a..db20f28734 100644 --- a/server_fn/src/request/reqwest.rs +++ b/server_fn/src/request/reqwest.rs @@ -56,14 +56,14 @@ impl<CustErr> ClientReq<CustErr> for Request { path: &str, accepts: &str, content_type: &str, - body: Bytes, + body: &[u8], ) -> Result<Self, ServerFnError<CustErr>> { let url = format!("{}{}", get_server_url(), path); CLIENT .post(url) .header(CONTENT_TYPE, content_type) .header(ACCEPT, accepts) - .body(body) + .body(body.to_owned()) .build() .map_err(|e| ServerFnError::Request(e.to_string())) } From 14072457d03ce854d9d15d61b1b89aef31492faa Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Fri, 19 Jan 2024 12:43:45 -0500 Subject: [PATCH 080/100] clean up docs (closes #2197) --- server_fn/Cargo.toml | 3 +++ server_fn/src/client.rs | 4 ++-- server_fn/src/codec/mod.rs | 18 +++++++++--------- server_fn/src/codec/stream.rs | 12 +++++++++++- server_fn/src/error.rs | 9 ++++----- server_fn/src/lib.rs | 5 +++-- server_fn/src/request/mod.rs | 8 ++++---- server_fn/src/response/mod.rs | 8 ++++---- 8 files changed, 40 insertions(+), 27 deletions(-) diff --git a/server_fn/Cargo.toml b/server_fn/Cargo.toml index f68d0f698e..cbb4d069d9 100644 --- a/server_fn/Cargo.toml +++ b/server_fn/Cargo.toml @@ -102,3 +102,6 @@ default-tls = ["reqwest?/default-tls"] rustls = ["reqwest?/rustls-tls"] reqwest = ["dep:reqwest"] ssr = ["inventory"] + +[package.metadata.docs.rs] +all-features = true diff --git a/server_fn/src/client.rs b/server_fn/src/client.rs index 84538bb333..c7b73cbb19 100644 --- a/server_fn/src/client.rs +++ b/server_fn/src/client.rs @@ -33,7 +33,7 @@ pub trait Client<CustErr> { ) -> impl Future<Output = Result<Self::Response, ServerFnError<CustErr>>> + Send; } -#[cfg(any(feature = "browser", doc))] +#[cfg(feature = "browser")] /// Implements [`Client`] for a `fetch` request in the browser. pub mod browser { use super::Client; @@ -67,7 +67,7 @@ pub mod browser { } } -#[cfg(any(feature = "reqwest", doc))] +#[cfg(feature = "reqwest")] /// Implements [`Client`] for a request made by [`reqwest`]. pub mod reqwest { use super::Client; diff --git a/server_fn/src/codec/mod.rs b/server_fn/src/codec/mod.rs index 63167294b3..94faa8b34b 100644 --- a/server_fn/src/codec/mod.rs +++ b/server_fn/src/codec/mod.rs @@ -11,37 +11,37 @@ //! Rather than a limited number of encodings, this crate allows you to define server functions that //! mix and match the input encoding and output encoding. To define a new encoding, you simply implement //! an input combination ([`IntoReq`] and [`FromReq`]) and/or an output encoding ([`IntoRes`] and [`FromRes`]). -//! This genuinely is an and/or: while some encodings can be used for both input and output ([`Json`], [`Cbor`], [`Rkyv`]), -//! others can only be used for input ([`GetUrl`], [`MultipartData`]) or only output ([`ByteStream`], [`StreamingText`]). +//! This genuinely is an and/or: while some encodings can be used for both input and output (`Json`, `Cbor`, `Rkyv`), +//! others can only be used for input (`GetUrl`, `MultipartData`). #[cfg(feature = "cbor")] mod cbor; -#[cfg(any(feature = "cbor", doc))] +#[cfg(feature = "cbor")] pub use cbor::*; #[cfg(feature = "json")] mod json; -#[cfg(any(feature = "json", doc))] +#[cfg(feature = "json")] pub use json::*; #[cfg(feature = "serde-lite")] mod serde_lite; -#[cfg(any(feature = "serde-lite", doc))] +#[cfg(feature = "serde-lite")] pub use serde_lite::*; #[cfg(feature = "rkyv")] mod rkyv; -#[cfg(any(feature = "rkyv", doc))] +#[cfg(feature = "rkyv")] pub use rkyv::*; #[cfg(feature = "url")] mod url; -#[cfg(any(feature = "url", doc))] +#[cfg(feature = "url")] pub use url::*; #[cfg(feature = "multipart")] mod multipart; -#[cfg(any(feature = "multipart", doc))] +#[cfg(feature = "multipart")] pub use multipart::*; mod stream; @@ -52,7 +52,7 @@ pub use stream::*; /// Serializes a data type into an HTTP request, on the client. /// -/// Implementations use the methods of the [`ClientReq`](crate::ClientReq) trait to +/// Implementations use the methods of the [`ClientReq`](crate::request::ClientReq) trait to /// convert data into a request body. They are often quite short, usually consisting /// of just two steps: /// 1. Serializing the data into some [`String`], [`Bytes`](bytes::Bytes), or [`Stream`](futures::Stream). diff --git a/server_fn/src/codec/stream.rs b/server_fn/src/codec/stream.rs index 14d67c46bf..38173f6074 100644 --- a/server_fn/src/codec/stream.rs +++ b/server_fn/src/codec/stream.rs @@ -114,6 +114,11 @@ where /// An encoding that represents a stream of text. /// /// A server function that uses this as its output encoding should return [`TextStream`]. +/// +/// **Note**: Browser fetch requests do not currently support full request duplexing, which +/// means that that they do begin handling responses until the full request has been sent. +/// This means that if you use streaming text as an input encoding, the input stream needs to +/// end before the output will begin. pub struct StreamingText; impl Encoding for StreamingText { @@ -121,9 +126,14 @@ impl Encoding for StreamingText { const METHOD: Method = Method::POST; } -/// A stream of bytes. +/// A stream of text. /// /// A server function can return this type if its output encoding is [`StreamingText`]. +/// +/// **Note**: Browser fetch requests do not currently support full request duplexing, which +/// means that that they do begin handling responses until the full request has been sent. +/// This means that if you use streaming text as an input encoding, the input stream needs to +/// end before the output will begin. pub struct TextStream<CustErr = NoCustomError>( Pin<Box<dyn Stream<Item = Result<String, ServerFnError<CustErr>>> + Send>>, ); diff --git a/server_fn/src/error.rs b/server_fn/src/error.rs index fa84d9128c..543d8eab2f 100644 --- a/server_fn/src/error.rs +++ b/server_fn/src/error.rs @@ -79,14 +79,13 @@ impl FromStr for NoCustomError { } } -/// Wraps some error type, which may implement any of [`Error`], [`Clone`], or +/// Wraps some error type, which may implement any of [`Error`](trait@std::error::Error), [`Clone`], or /// [`Display`]. #[derive(Debug)] pub struct WrapError<T>(pub T); -/// This helper macro lets you call the gnarly autoref-specialization call -/// without having to worry about things like how many & you need. -/// Mostly used when you impl From<ServerFnError> for YourError +/// A helper macro to convert a variety of different types into `ServerFnError`. +/// This should mostly be used if you are implementing `From<ServerFnError>` for `YourError`. #[macro_export] macro_rules! server_fn_error { () => {{ @@ -161,7 +160,7 @@ impl<E> ViaError<E> for WrapError<E> { /// Type for errors that can occur when using server functions. /// -/// Unlike [`ServerFnErrorErr`], this does not implement [`Error`](std::error::Error). +/// 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 /// `?` operator. #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/server_fn/src/lib.rs b/server_fn/src/lib.rs index ba67bdb9df..a762e3ee55 100644 --- a/server_fn/src/lib.rs +++ b/server_fn/src/lib.rs @@ -32,7 +32,8 @@ //! indicate that it should only run on the server (i.e., when you have an `ssr` feature in your //! crate that is enabled). //! -//! **Important**: Before calling a server function on a non-web platform, you must set the server URL by calling [`set_server_url`]. +//! **Important**: Before calling a server function on a non-web platform, you must set the server URL by calling +//! [`set_server_url`](crate::client::set_server_url). //! //! ```rust,ignore //! #[server] @@ -148,7 +149,7 @@ pub use xxhash_rust; /// Defines a function that runs only on the server, but can be called from the server or the client. /// /// The type for which `ServerFn` is implemented is actually the type of the arguments to the function, -/// while the function body itself is implemented in [`run_body`]. +/// while the function body itself is implemented in [`run_body`](ServerFn::run_body). /// /// This means that `Self` here is usually a struct, in which each field is an argument to the function. /// In other words, diff --git a/server_fn/src/request/mod.rs b/server_fn/src/request/mod.rs index 75d8ad17fc..b9ff889d4e 100644 --- a/server_fn/src/request/mod.rs +++ b/server_fn/src/request/mod.rs @@ -4,16 +4,16 @@ use futures::Stream; use std::{borrow::Cow, future::Future}; /// Request types for Actix. -#[cfg(any(feature = "actix", doc))] +#[cfg(feature = "actix")] pub mod actix; /// Request types for Axum. -#[cfg(any(feature = "axum", doc))] +#[cfg(feature = "axum")] pub mod axum; /// Request types for the browser. -#[cfg(any(feature = "browser", doc))] +#[cfg(feature = "browser")] pub mod browser; /// Request types for [`reqwest`]. -#[cfg(any(feature = "reqwest", doc))] +#[cfg(feature = "reqwest")] pub mod reqwest; /// Represents a request as made by the client. diff --git a/server_fn/src/response/mod.rs b/server_fn/src/response/mod.rs index 2862eae984..cf374636a5 100644 --- a/server_fn/src/response/mod.rs +++ b/server_fn/src/response/mod.rs @@ -1,14 +1,14 @@ /// Response types for Actix. -#[cfg(any(feature = "actix", doc))] +#[cfg(feature = "actix")] pub mod actix; /// Response types for the browser. -#[cfg(any(feature = "browser", doc))] +#[cfg(feature = "browser")] pub mod browser; /// Response types for Axum. -#[cfg(any(feature = "axum", doc))] +#[cfg(feature = "axum")] pub mod http; /// Response types for [`reqwest`]. -#[cfg(any(feature = "reqwest", doc))] +#[cfg(feature = "reqwest")] pub mod reqwest; use crate::error::ServerFnError; From f9cd8539e4aae8b440a72991a99bbab64ba98a7c Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Fri, 19 Jan 2024 12:46:08 -0500 Subject: [PATCH 081/100] add missing PartialEq/Eq implementations on ServerFnError (closes #2198) --- server_fn/src/error.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server_fn/src/error.rs b/server_fn/src/error.rs index 543d8eab2f..63071ce950 100644 --- a/server_fn/src/error.rs +++ b/server_fn/src/error.rs @@ -163,7 +163,7 @@ impl<E> ViaError<E> for WrapError<E> { /// 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 /// `?` operator. -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(tag = "type")] pub enum ServerFnError<E = NoCustomError> { /// A user-defined custom error type, which defaults to [`NoCustomError`]. @@ -344,7 +344,7 @@ where /// /// [`ServerFnError`] and [`ServerFnErrorErr`] mutually implement [`From`], so /// it is easy to convert between the two types. -#[derive(Error, Debug, Clone)] +#[derive(Error, Debug, Clone, PartialEq, Eq)] pub enum ServerFnErrorErr<E = NoCustomError> { /// A user-defined custom error type, which defaults to [`NoCustomError`]. #[error("internal error: {0}")] From 94cb4c0ec3440790afacb7811c68bf7254e95475 Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Fri, 19 Jan 2024 13:41:30 -0500 Subject: [PATCH 082/100] remove pavex work (now in `pavex` branch) --- examples/pavex_demo/.gitignore | 3 - examples/pavex_demo/Cargo.toml | 92 - examples/pavex_demo/README.md | 71 - examples/pavex_demo/flake.lock | 119 -- examples/pavex_demo/flake.nix | 129 -- examples/pavex_demo/leptos_app/Cargo.toml | 21 - .../leptos_app/src/error_template.rs | 73 - examples/pavex_demo/leptos_app/src/lib.rs | 45 - examples/pavex_demo/leptos_front/Cargo.toml | 8 - examples/pavex_demo/leptos_front/src/lib.rs | 13 - examples/pavex_demo/style/main.scss | 4 - .../todo_app_sqlite_pavex/Cargo.toml | 22 - .../todo_app_sqlite_pavex/src/bin/bp.rs | 17 - .../todo_app_sqlite_pavex/src/blueprint.rs | 98 -- .../src/configuration.rs | 32 - .../todo_app_sqlite_pavex/src/file_handler.rs | 45 - .../todo_app_sqlite_pavex/src/leptos.rs | 19 - .../todo_app_sqlite_pavex/src/lib.rs | 7 - .../todo_app_sqlite_pavex/src/routes/greet.rs | 21 - .../todo_app_sqlite_pavex/src/routes/mod.rs | 3 - .../src/routes/status.rs | 7 - .../todo_app_sqlite_pavex/src/telemetry.rs | 84 - .../todo_app_sqlite_pavex/src/user_agent.rs | 27 - .../todo_app_sqlite_pavex_server/Cargo.toml | 29 - .../configuration/base.yml | 3 - .../configuration/dev.yml | 6 - .../configuration/prod.yml | 3 - .../configuration/test.yml | 8 - .../src/bin/api.rs | 49 - .../src/configuration.rs | 140 -- .../todo_app_sqlite_pavex_server/src/lib.rs | 2 - .../src/telemetry.rs | 40 - .../tests/integration/greet.rs | 37 - .../tests/integration/helpers.rs | 52 - .../tests/integration/main.rs | 4 - .../tests/integration/ping.rs | 11 - .../Cargo.toml | 21 - .../blueprint.ron | 233 --- .../src/lib.rs | 254 --- integrations/pavex/Cargo.toml | 24 - integrations/pavex/Makefile.toml | 4 - integrations/pavex/src/lib.rs | 1554 ----------------- 42 files changed, 3434 deletions(-) delete mode 100644 examples/pavex_demo/.gitignore delete mode 100644 examples/pavex_demo/Cargo.toml delete mode 100644 examples/pavex_demo/README.md delete mode 100644 examples/pavex_demo/flake.lock delete mode 100644 examples/pavex_demo/flake.nix delete mode 100644 examples/pavex_demo/leptos_app/Cargo.toml delete mode 100644 examples/pavex_demo/leptos_app/src/error_template.rs delete mode 100644 examples/pavex_demo/leptos_app/src/lib.rs delete mode 100644 examples/pavex_demo/leptos_front/Cargo.toml delete mode 100644 examples/pavex_demo/leptos_front/src/lib.rs delete mode 100644 examples/pavex_demo/style/main.scss delete mode 100644 examples/pavex_demo/todo_app_sqlite_pavex/Cargo.toml delete mode 100644 examples/pavex_demo/todo_app_sqlite_pavex/src/bin/bp.rs delete mode 100644 examples/pavex_demo/todo_app_sqlite_pavex/src/blueprint.rs delete mode 100644 examples/pavex_demo/todo_app_sqlite_pavex/src/configuration.rs delete mode 100644 examples/pavex_demo/todo_app_sqlite_pavex/src/file_handler.rs delete mode 100644 examples/pavex_demo/todo_app_sqlite_pavex/src/leptos.rs delete mode 100644 examples/pavex_demo/todo_app_sqlite_pavex/src/lib.rs delete mode 100644 examples/pavex_demo/todo_app_sqlite_pavex/src/routes/greet.rs delete mode 100644 examples/pavex_demo/todo_app_sqlite_pavex/src/routes/mod.rs delete mode 100644 examples/pavex_demo/todo_app_sqlite_pavex/src/routes/status.rs delete mode 100644 examples/pavex_demo/todo_app_sqlite_pavex/src/telemetry.rs delete mode 100644 examples/pavex_demo/todo_app_sqlite_pavex/src/user_agent.rs delete mode 100644 examples/pavex_demo/todo_app_sqlite_pavex_server/Cargo.toml delete mode 100644 examples/pavex_demo/todo_app_sqlite_pavex_server/configuration/base.yml delete mode 100644 examples/pavex_demo/todo_app_sqlite_pavex_server/configuration/dev.yml delete mode 100644 examples/pavex_demo/todo_app_sqlite_pavex_server/configuration/prod.yml delete mode 100644 examples/pavex_demo/todo_app_sqlite_pavex_server/configuration/test.yml delete mode 100644 examples/pavex_demo/todo_app_sqlite_pavex_server/src/bin/api.rs delete mode 100644 examples/pavex_demo/todo_app_sqlite_pavex_server/src/configuration.rs delete mode 100644 examples/pavex_demo/todo_app_sqlite_pavex_server/src/lib.rs delete mode 100644 examples/pavex_demo/todo_app_sqlite_pavex_server/src/telemetry.rs delete mode 100644 examples/pavex_demo/todo_app_sqlite_pavex_server/tests/integration/greet.rs delete mode 100644 examples/pavex_demo/todo_app_sqlite_pavex_server/tests/integration/helpers.rs delete mode 100644 examples/pavex_demo/todo_app_sqlite_pavex_server/tests/integration/main.rs delete mode 100644 examples/pavex_demo/todo_app_sqlite_pavex_server/tests/integration/ping.rs delete mode 100644 examples/pavex_demo/todo_app_sqlite_pavex_server_sdk/Cargo.toml delete mode 100644 examples/pavex_demo/todo_app_sqlite_pavex_server_sdk/blueprint.ron delete mode 100644 examples/pavex_demo/todo_app_sqlite_pavex_server_sdk/src/lib.rs delete mode 100644 integrations/pavex/Cargo.toml delete mode 100644 integrations/pavex/Makefile.toml delete mode 100644 integrations/pavex/src/lib.rs diff --git a/examples/pavex_demo/.gitignore b/examples/pavex_demo/.gitignore deleted file mode 100644 index b6be732f92..0000000000 --- a/examples/pavex_demo/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -/target -.env -.direnv diff --git a/examples/pavex_demo/Cargo.toml b/examples/pavex_demo/Cargo.toml deleted file mode 100644 index f46abb11a5..0000000000 --- a/examples/pavex_demo/Cargo.toml +++ /dev/null @@ -1,92 +0,0 @@ -[workspace] -members = ["todo_app_sqlite_pavex", "todo_app_sqlite_pavex_server_sdk", "todo_app_sqlite_pavex_server", "leptos_app"] -# By setting `todo_app_sqlite_pavex_server` as the default member, `cargo run` will default to running the server binary -# when executed from the root of the workspace. -# Otherwise, you would have to use `cargo run --bin api` to run the server binary. -default-members = ["todo_app_sqlite_pavex_server"] -resolver = "2" - -# need to be applied only to wasm build -[profile.wasm_release] -codegen-units = 1 -lto = true -opt-level = 'z' - -[workspace.dependencies] -leptos = { version = "0.5", features = ["nightly"] } -leptos_meta = { version = "0.5", features = ["nightly"] } -leptos_router = { version = "0.5", features = ["nightly"] } -leptos_pavex = { version = "0.5" } -cfg_if = "1" -thiserror = "1" - -# See https://github.com/akesson/cargo-leptos for documentation of all the parameters. - -# A leptos project defines which workspace members -# that are used together frontend (lib) & server (bin) -[[workspace.metadata.leptos]] -# this name is used for the wasm, js and css file names -name = "start-pavex-workspace" - -# the package in the workspace that contains the server binary (binary crate) -bin-package = "server" - -# the package in the workspace that contains the frontend wasm binary (library crate) -lib-package = "leptos_frontend" - -# The site root folder is where cargo-leptos generate all output. WARNING: all content of this folder will be erased on a rebuild. Use it in your server setup. -site-root = "target/site" - -# The site-root relative folder where all compiled output (JS, WASM and CSS) is written -# Defaults to pkg -site-pkg-dir = "pkg" - -# [Optional] The source CSS file. If it ends with .sass or .scss then it will be compiled by dart-sass into CSS. The CSS is optimized by Lightning CSS before being written to <site-root>/<site-pkg>/app.css -style-file = "style/main.scss" - -# Assets source dir. All files found here will be copied and synchronized to site-root. -# The assets-dir cannot have a sub directory with the same name/path as site-pkg-dir. -# -# Optional. Env: LEPTOS_ASSETS_DIR. -assets-dir = "public" - -# The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup. -site-addr = "127.0.0.1:3000" - -# The port to use for automatic reload monitoring -reload-port = 3001 - -# [Optional] Command to use when running end2end tests. It will run in the end2end dir. -# [Windows] for non-WSL use "npx.cmd playwright test" -# This binary name can be checked in Powershell with Get-Command npx -end2end-cmd = "npx playwright test" -end2end-dir = "end2end" - -# The browserlist query used for optimizing the CSS. -browserquery = "defaults" - -# Set by cargo-leptos watch when building with that tool. Controls whether autoreload JS will be included in the head -watch = false - -# The environment Leptos will run in, usually either "DEV" or "PROD" -env = "DEV" - -# The features to use when compiling the bin target -# -# Optional. Can be over-ridden with the command line parameter --bin-features -bin-features = [] - -# If the --no-default-features flag should be used when compiling the bin target -# -# Optional. Defaults to false. -bin-default-features = false - -# The features to use when compiling the lib target -# -# Optional. Can be over-ridden with the command line parameter --lib-features -lib-features = [] - -# If the --no-default-features flag should be used when compiling the lib target -# -# Optional. Defaults to false. -lib-default-features = false diff --git a/examples/pavex_demo/README.md b/examples/pavex_demo/README.md deleted file mode 100644 index 72141d8ffb..0000000000 --- a/examples/pavex_demo/README.md +++ /dev/null @@ -1,71 +0,0 @@ -# todo_app_sqlite_pavex - -# Getting started - -## Prerequisites - -- Rust (see [here](https://www.rust-lang.org/tools/install) for instructions) -- `cargo-px`: - ```bash - cargo install --locked cargo-px --version="~0.1" - ``` -- [Pavex](https://pavex.dev) - -## Useful commands - -`todo_app_sqlite_pavex` is built using the [Pavex](https://pavex.dev) web framework, which relies on code generation. -You need to use the `cargo px` command instead of `cargo`: it ensures that the -`todo_app_sqlite_pavex_server_sdk` crate is correctly regenerated when the -application blueprint changes. - -`cargo px` is a wrapper around `cargo` that will automatically regenerate the -server SDK when needed. Check out its [documentation](https://github.com/LukeMathWalker/cargo-px) -for more details. - -### Build - -```bash -cargo px build -``` - -### Run - -```bash -cargo px run -``` - -### Test - -```bash -cargo px test -``` - -## Configuration - -All configurable parameters are listed in `todo_app_sqlite_pavex/src/configuration.rs`. - -Configuration values are loaded from two sources: - -- Configuration files -- Environment variables - -Environment variables take precedence over configuration files. - -All configuration files are in the `todo_app_sqlite_pavex_server/configuration` folder. -The application can be run in three different profiles: `dev`, `test` and `prod`. -The settings that you want to share across all profiles should be placed in `todo_app_sqlite_pavex_server/configuration/base.yml`. -Profile-specific configuration files can be then used -to override or supply additional values on top of the default settings (e.g. `todo_app_sqlite_pavex_server/configuration/dev.yml`). - -You can specify the app profile that you want to use by setting the `APP_PROFILE` environment variable; e.g.: - -```bash -APP_PROFILE=prod cargo px run -``` - -for running the application with the `prod` profile. - -By default, the `dev` profile is used since `APP_PROFILE` is set to `dev` in the `.env` file at the root of the project. -The `.env` file should not be committed to version control: it is meant to be used for local development only, -so that each developer can specify their own environment variables for secret values (e.g. database credentials) -that shouldn't be stored in configuration files (given their sensitive nature). diff --git a/examples/pavex_demo/flake.lock b/examples/pavex_demo/flake.lock deleted file mode 100644 index c219f46f50..0000000000 --- a/examples/pavex_demo/flake.lock +++ /dev/null @@ -1,119 +0,0 @@ -{ - "nodes": { - "cargo-pavex-git": { - "flake": false, - "locked": { - "lastModified": 1703610192, - "narHash": "sha256-+oM6VGRRt/DQdhEFWJFIpKfY29w72V0vRpud8NsOI7c=", - "owner": "LukeMathWalker", - "repo": "pavex", - "rev": "e302f99e3641a55fe5624ba6c8154ce64e732a89", - "type": "github" - }, - "original": { - "owner": "LukeMathWalker", - "repo": "pavex", - "type": "github" - } - }, - "cargo-px-git": { - "flake": false, - "locked": { - "lastModified": 1702137928, - "narHash": "sha256-FbwHEOQnIYKhxp4Ne9XBIUJXu1o+ak6y9MhzRenIW40=", - "owner": "LukeMathWalker", - "repo": "cargo-px", - "rev": "d1bb9075c4993130f31f31c95642567a2255bd8e", - "type": "github" - }, - "original": { - "owner": "LukeMathWalker", - "repo": "cargo-px", - "type": "github" - } - }, - "flake-utils": { - "inputs": { - "systems": "systems" - }, - "locked": { - "lastModified": 1701680307, - "narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "4022d587cbbfd70fe950c1e2083a02621806a725", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, - "nixpkgs": { - "locked": { - "lastModified": 1703499205, - "narHash": "sha256-lF9rK5mSUfIZJgZxC3ge40tp1gmyyOXZ+lRY3P8bfbg=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "e1fa12d4f6c6fe19ccb59cac54b5b3f25e160870", - "type": "github" - }, - "original": { - "owner": "NixOS", - "ref": "nixpkgs-unstable", - "repo": "nixpkgs", - "type": "github" - } - }, - "root": { - "inputs": { - "cargo-pavex-git": "cargo-pavex-git", - "cargo-px-git": "cargo-px-git", - "flake-utils": "flake-utils", - "nixpkgs": "nixpkgs", - "rust-overlay": "rust-overlay" - } - }, - "rust-overlay": { - "inputs": { - "flake-utils": [ - "flake-utils" - ], - "nixpkgs": [ - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1703643208, - "narHash": "sha256-UL4KO8JxnD5rOycwHqBAf84lExF1/VnYMDC7b/wpPDU=", - "owner": "oxalica", - "repo": "rust-overlay", - "rev": "ce117f3e0de8262be8cd324ee6357775228687cf", - "type": "github" - }, - "original": { - "owner": "oxalica", - "repo": "rust-overlay", - "type": "github" - } - }, - "systems": { - "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default", - "type": "github" - } - } - }, - "root": "root", - "version": 7 -} diff --git a/examples/pavex_demo/flake.nix b/examples/pavex_demo/flake.nix deleted file mode 100644 index 4452c5f02a..0000000000 --- a/examples/pavex_demo/flake.nix +++ /dev/null @@ -1,129 +0,0 @@ -{ - description = "Build Pavex tools"; - - inputs = { - nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; - - flake-utils.url = "github:numtide/flake-utils"; - - cargo-px-git = { - url = "github:/LukeMathWalker/cargo-px"; - flake = false; - }; - cargo-pavex-git = { - url = "github:LukeMathWalker/pavex"; - flake = false; - }; - - rust-overlay = { - url = "github:oxalica/rust-overlay"; - inputs = { - nixpkgs.follows = "nixpkgs"; - flake-utils.follows = "flake-utils"; - }; - }; - }; - - outputs = { self, nixpkgs, flake-utils, rust-overlay, ... } @inputs: - flake-utils.lib.eachDefaultSystem - (system: - let - pkgs = import nixpkgs { - inherit system; - overlays = [ (import rust-overlay) ]; - }; - inherit (pkgs) lib; - rustTarget = pkgs.rust-bin.selectLatestNightlyWith( toolchain: toolchain.default.override { - extensions = [ "rust-src" "rust-analyzer" "rustc-codegen-cranelift-preview" "rust-docs-json"]; - targets = [ "wasm32-unknown-unknown" ]; - }); - - - cargo-pavex_cli-git = pkgs.rustPlatform.buildRustPackage rec { - pname = "cargo-pavex-cli"; - version = "0.2.22"; - #buildFeatures = ["no_downloads"]; # cargo-leptos will try to download Ruby and other things without this feature - - src = inputs.cargo-pavex-git; - sourceRoot = "source/libs"; - cargoLock = { - lockFile = inputs.cargo-pavex-git + "/libs/Cargo.lock"; - outputHashes = { - "matchit-0.7.3" = "sha256-1bhbWvLlDb6/UJ4j2FqoG7j3DD1dTOLl6RaiY9kasmQ="; - #"pavex-0.1.0" = "sha256-NC7T1pcXJiWPtAWeiMUNzf2MUsYaRYxjLIL9fCqhExo="; - }; - }; - #buildAndTestSubdir = "libs"; - cargoSha256 = ""; - nativeBuildInputs = [pkgs.pkg-config pkgs.openssl pkgs.git]; - - buildInputs = with pkgs; - [openssl pkg-config git] - ++ lib.optionals stdenv.isDarwin [ - Security - ]; - - doCheck = false; # integration tests depend on changing cargo config - - meta = with lib; { - description = "An easy-to-use Rust framework for building robust and performant APIs"; - homepage = "https://github.com/LukeMatthewWalker/pavex"; - changelog = "https://github.com/LukeMatthewWalker/pavex/blob/v${version}/CHANGELOG.md"; - license = with licenses; [mit]; - maintainers = with maintainers; [benwis]; - }; - }; - cargo-px-git = pkgs.rustPlatform.buildRustPackage rec { - pname = "cargo-px"; - version = "0.2.22"; - #buildFeatures = ["no_downloads"]; # cargo-leptos will try to download Ruby and other things without this feature - - src = inputs.cargo-px-git; - - cargoSha256 ="sha256-+pyeqh0IoZ1JMgbhWxhEJw1MPgG7XeocVrqJoSNjgDA="; - - nativeBuildInputs = [pkgs.pkg-config pkgs.openssl pkgs.git]; - - buildInputs = with pkgs; - [openssl pkg-config git] - ++ lib.optionals stdenv.isDarwin [ - Security - ]; - - doCheck = false; # integration tests depend on changing cargo config - - meta = with lib; { - description = "A cargo subcommand that extends cargo's capabilities when it comes to code generation."; - homepage = "https://github.com/LukeMatthewWalker/cargo-px"; - changelog = "https://github.com/LukeMatthewWalker/cargo-px/blob/v${version}/CHANGELOG.md"; - license = with licenses; [mit]; - maintainers = with maintainers; [benwis]; - }; - }; - in - { - - devShells.default = pkgs.mkShell { - - # Extra inputs can be added here - nativeBuildInputs = with pkgs; [ - #rustTarget - rustup - openssl - pkg-config - clang - tailwindcss - mold-wrapped - cargo-px-git - cargo-pavex_cli-git - ]; - #RUST_SRC_PATH = "${rustTarget}/lib/rustlib/src/rust/library"; - MOLD_PATH = "${pkgs.mold-wrapped}/bin/mold"; - - shellHook = '' - sed -i -e '/rustflags = \["-C", "link-arg=-fuse-ld=/ s|ld=.*|ld=${pkgs.mold-wrapped}/bin/mold"]|' .cargo/config.toml - ''; - LIBCLANG_PATH = "${pkgs.llvmPackages.libclang.lib}/lib"; - }; - }); -} diff --git a/examples/pavex_demo/leptos_app/Cargo.toml b/examples/pavex_demo/leptos_app/Cargo.toml deleted file mode 100644 index 2a455b47dc..0000000000 --- a/examples/pavex_demo/leptos_app/Cargo.toml +++ /dev/null @@ -1,21 +0,0 @@ -[package] -name = "leptos_app" -version = "0.1.0" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -leptos.workspace = true -leptos_meta.workspace = true -leptos_router.workspace = true -leptos_pavex = { workspace = true, optional = true } - -#http.workspace = true -cfg_if.workspace = true -thiserror.workspace = true - -[features] -default = [] -hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"] -ssr = ["leptos/ssr", "leptos_meta/ssr", "leptos_router/ssr", "dep:leptos_pavex"] diff --git a/examples/pavex_demo/leptos_app/src/error_template.rs b/examples/pavex_demo/leptos_app/src/error_template.rs deleted file mode 100644 index f22e4687b9..0000000000 --- a/examples/pavex_demo/leptos_app/src/error_template.rs +++ /dev/null @@ -1,73 +0,0 @@ -use cfg_if::cfg_if; -use http::status::StatusCode; -use leptos::*; -#[cfg(feature = "ssr")] -use leptos_axum::ResponseOptions; -use thiserror::Error; - -#[derive(Clone, Debug, Error)] -pub enum AppError { - #[error("Not Found")] - NotFound, -} - -impl AppError { - pub fn status_code(&self) -> StatusCode { - match self { - AppError::NotFound => StatusCode::NOT_FOUND, - } - } -} - -// A basic function to display errors served by the error boundaries. -// Feel free to do more complicated things here than just displaying the error. -#[component] -pub fn ErrorTemplate( - #[prop(optional)] outside_errors: Option<Errors>, - #[prop(optional)] errors: Option<RwSignal<Errors>>, -) -> impl IntoView { - let errors = match outside_errors { - Some(e) => create_rw_signal(e), - None => match errors { - Some(e) => e, - None => panic!("No Errors found and we expected errors!"), - }, - }; - // Get Errors from Signal - let errors = errors.get_untracked(); - - // Downcast lets us take a type that implements `std::error::Error` - let errors: Vec<AppError> = errors - .into_iter() - .filter_map(|(_k, v)| v.downcast_ref::<AppError>().cloned()) - .collect(); - println!("Errors: {errors:#?}"); - - // Only the response code for the first error is actually sent from the server - // this may be customized by the specific application - cfg_if! { if #[cfg(feature="ssr")] { - let response = use_context::<ResponseOptions>(); - if let Some(response) = response { - response.set_status(errors[0].status_code()); - } - }} - - view! { - <h1>{if errors.len() > 1 { "Errors" } else { "Error" }}</h1> - <For - // a function that returns the items we're iterating over; a signal is fine - each=move || { errors.clone().into_iter().enumerate() } - // a unique key for each item as a reference - key=|(index, _error)| *index - // renders each item to a view - children=move |error| { - let error_string = error.1.to_string(); - let error_code = error.1.status_code(); - view! { - <h2>{error_code.to_string()}</h2> - <p>"Error: " {error_string}</p> - } - } - /> - } -} diff --git a/examples/pavex_demo/leptos_app/src/lib.rs b/examples/pavex_demo/leptos_app/src/lib.rs deleted file mode 100644 index f68dcf688f..0000000000 --- a/examples/pavex_demo/leptos_app/src/lib.rs +++ /dev/null @@ -1,45 +0,0 @@ -use crate::error_template::{AppError, ErrorTemplate}; -use leptos::*; -use leptos_meta::*; -use leptos_router::*; - -pub mod error_template; - -#[component] -pub fn App() -> impl IntoView { - // Provides context that manages stylesheets, titles, meta tags, etc. - provide_meta_context(); - - view! { - <Stylesheet id="leptos" href="/pkg/start-axum-workspace.css"/> - - // sets the document title - <Title text="Welcome to Leptos"/> - - // content for this welcome page - <Router fallback=|| { - let mut outside_errors = Errors::default(); - outside_errors.insert_with_default_key(AppError::NotFound); - view! { <ErrorTemplate outside_errors/> }.into_view() - }> - <main> - <Routes> - <Route path="" view=HomePage/> - </Routes> - </main> - </Router> - } -} - -/// Renders the home page of your application. -#[component] -fn HomePage() -> impl IntoView { - // Creates a reactive value to update the button - let (count, set_count) = create_signal(0); - let on_click = move |_| set_count.update(|count| *count += 1); - - view! { - <h1>"Welcome to Leptos on Pavex!"</h1> - <button on:click=on_click>"Click Me: " {count}</button> - } -} diff --git a/examples/pavex_demo/leptos_front/Cargo.toml b/examples/pavex_demo/leptos_front/Cargo.toml deleted file mode 100644 index da9f88d9f0..0000000000 --- a/examples/pavex_demo/leptos_front/Cargo.toml +++ /dev/null @@ -1,8 +0,0 @@ -[package] -name = "leptos_front" -version = "0.1.0" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] diff --git a/examples/pavex_demo/leptos_front/src/lib.rs b/examples/pavex_demo/leptos_front/src/lib.rs deleted file mode 100644 index 8e25fd20e5..0000000000 --- a/examples/pavex_demo/leptos_front/src/lib.rs +++ /dev/null @@ -1,13 +0,0 @@ -use leptos::*; -use leptos_app::*; -use wasm_bindgen::prelude::wasm_bindgen; - -#[wasm_bindgen] -pub fn hydrate() { - // initializes logging using the `log` crate - _ = console_log::init_with_level(log::Level::Debug); - console_error_panic_hook::set_once(); - - leptos::mount_to_body(App); -} - diff --git a/examples/pavex_demo/style/main.scss b/examples/pavex_demo/style/main.scss deleted file mode 100644 index 2ef54d3348..0000000000 --- a/examples/pavex_demo/style/main.scss +++ /dev/null @@ -1,4 +0,0 @@ -body { - font-family: sans-serif; - text-align: center; -} diff --git a/examples/pavex_demo/todo_app_sqlite_pavex/Cargo.toml b/examples/pavex_demo/todo_app_sqlite_pavex/Cargo.toml deleted file mode 100644 index fc53c56663..0000000000 --- a/examples/pavex_demo/todo_app_sqlite_pavex/Cargo.toml +++ /dev/null @@ -1,22 +0,0 @@ -[package] -name = "todo_app_sqlite_pavex" -version = "0.1.0" -edition = "2021" - -[[bin]] -path = "src/bin/bp.rs" -name = "bp" - -[dependencies] -cargo_px_env = "0.1" -pavex = { git = "https://github.com/LukeMathWalker/pavex", branch = "main" } -pavex_cli_client = { git = "https://github.com/LukeMathWalker/pavex", branch = "main" } -tracing = "0.1" - -# Configuration -serde = { version = "1", features = ["derive"] } -serde-aux = "4" - -# Leptos -leptos_pavex.workspace = true - diff --git a/examples/pavex_demo/todo_app_sqlite_pavex/src/bin/bp.rs b/examples/pavex_demo/todo_app_sqlite_pavex/src/bin/bp.rs deleted file mode 100644 index 7a75bb9a6b..0000000000 --- a/examples/pavex_demo/todo_app_sqlite_pavex/src/bin/bp.rs +++ /dev/null @@ -1,17 +0,0 @@ -use cargo_px_env::generated_pkg_manifest_path; -use todo_app_sqlite_pavex::blueprint; -use pavex_cli_client::Client; -use std::error::Error; - -/// Generate the `todo_app_sqlite_pavex_server_sdk` crate using Pavex's CLI. -/// -/// Pavex will automatically wire all our routes, constructors and error handlers -/// into the a "server SDK" that can be used by the final API server binary to launch -/// the application. -fn main() -> Result<(), Box<dyn Error>> { - let generated_dir = generated_pkg_manifest_path()?.parent().unwrap().into(); - Client::new() - .generate(blueprint(), generated_dir) - .execute()?; - Ok(()) -} diff --git a/examples/pavex_demo/todo_app_sqlite_pavex/src/blueprint.rs b/examples/pavex_demo/todo_app_sqlite_pavex/src/blueprint.rs deleted file mode 100644 index 7ea7275a95..0000000000 --- a/examples/pavex_demo/todo_app_sqlite_pavex/src/blueprint.rs +++ /dev/null @@ -1,98 +0,0 @@ -use leptos_pavex::{LeptosOptions, RouteListing}; -use pavex::{ - blueprint::{ - constructor::{CloningStrategy, Lifecycle}, - router::{ANY, GET}, - Blueprint, - }, - f, -}; -/// The main blueprint, containing all the routes, constructors and error handlers -/// required by our API. -pub fn blueprint() -> Blueprint { - let mut bp = Blueprint::new(); - register_common_constructors(&mut bp); - - bp.constructor( - f!(crate::user_agent::UserAgent::extract), - Lifecycle::RequestScoped, - ) - .error_handler(f!(crate::user_agent::invalid_user_agent)); - - add_telemetry_middleware(&mut bp); - - bp.route(GET, "/test/ping", f!(crate::routes::status::ping)); - bp.route(GET, "/test/greet/:name", f!(crate::routes::greet::greet)); - // Handle all /api requests as those are Leptos server fns - bp.route(ANY, "/api/*fn_name", f!(leptos_pavex::handle_server_fns)); - bp.route(ANY, "/"); - bp.fallback(f!(file_handler)); - bp -} - -/// Common constructors used by all routes. -fn register_common_constructors(bp: &mut Blueprint) { - // Configuration Options - bp.constructor( - f!(crate::leptos::get_cargo_leptos_conf(), Lifecycle::Singleton), - Lifecycle::Singleton, - ); - // List of Routes - bp.constructor( - f!(crate::leptos::get_app_route_listing(), Lifecycle::Singleton), - Lifecycle::Singleton, - ); - - bp.constructor( - f!(leptos_pavex::PavexRequest::extract), - LifeCycle::RequestScoped, - ); - // Query parameters - bp.constructor( - f!(pavex::request::query::QueryParams::extract), - Lifecycle::RequestScoped, - ) - .error_handler(f!( - pavex::request::query::errors::ExtractQueryParamsError::into_response - )); - - // Route parameters - bp.constructor( - f!(pavex::request::route::RouteParams::extract), - Lifecycle::RequestScoped, - ) - .error_handler(f!( - pavex::request::route::errors::ExtractRouteParamsError::into_response - )); - - // Json body - bp.constructor( - f!(pavex::request::body::JsonBody::extract), - Lifecycle::RequestScoped, - ) - .error_handler(f!( - pavex::request::body::errors::ExtractJsonBodyError::into_response - )); - bp.constructor( - f!(pavex::request::body::BufferedBody::extract), - Lifecycle::RequestScoped, - ) - .error_handler(f!( - pavex::request::body::errors::ExtractBufferedBodyError::into_response - )); - bp.constructor( - f!(<pavex::request::body::BodySizeLimit as std::default::Default>::default), - Lifecycle::RequestScoped, - ); -} - -/// Add the telemetry middleware, as well as the constructors of its dependencies. -fn add_telemetry_middleware(bp: &mut Blueprint) { - bp.constructor( - f!(crate::telemetry::RootSpan::new), - Lifecycle::RequestScoped, - ) - .cloning(CloningStrategy::CloneIfNecessary); - - bp.wrap(f!(crate::telemetry::logger)); -} diff --git a/examples/pavex_demo/todo_app_sqlite_pavex/src/configuration.rs b/examples/pavex_demo/todo_app_sqlite_pavex/src/configuration.rs deleted file mode 100644 index ad8bc7b385..0000000000 --- a/examples/pavex_demo/todo_app_sqlite_pavex/src/configuration.rs +++ /dev/null @@ -1,32 +0,0 @@ -use pavex::server::IncomingStream; -use serde_aux::field_attributes::deserialize_number_from_string; -use std::net::SocketAddr; - -#[derive(serde::Deserialize)] -/// The top-level configuration, holding all the values required -/// to configure the entire application. -pub struct Config { - pub server: ServerConfig, -} - -#[derive(serde::Deserialize, Clone)] -/// Configuration for the HTTP server used to expose our API -/// to users. -pub struct ServerConfig { - /// The port that the server must listen on. - #[serde(deserialize_with = "deserialize_number_from_string")] - pub port: u16, - /// The network interface that the server must be bound to. - /// - /// E.g. `0.0.0.0` for listening to incoming requests from - /// all sources. - pub ip: std::net::IpAddr, -} - -impl ServerConfig { - /// Bind a TCP listener according to the specified parameters. - pub async fn listener(&self) -> Result<IncomingStream, std::io::Error> { - let addr = SocketAddr::new(self.ip, self.port); - IncomingStream::bind(addr).await - } -} \ No newline at end of file diff --git a/examples/pavex_demo/todo_app_sqlite_pavex/src/file_handler.rs b/examples/pavex_demo/todo_app_sqlite_pavex/src/file_handler.rs deleted file mode 100644 index 98b7d8f8a9..0000000000 --- a/examples/pavex_demo/todo_app_sqlite_pavex/src/file_handler.rs +++ /dev/null @@ -1,45 +0,0 @@ -use app::error_template::AppError; -use app::error_template::ErrorTemplate; -use app::App; -use axum::response::Response as AxumResponse; -use axum::{ - body::{boxed, Body, BoxBody}, - extract::State, - http::{Request, Response, StatusCode, Uri}, - response::IntoResponse, -}; -use leptos::*; -use tower::ServiceExt; -use tower_http::services::ServeDir; - -pub async fn file_and_error_handler( - uri: Uri, - State(options): State<LeptosOptions>, - req: Request<Body>, -) -> AxumResponse { - let root = options.site_root.clone(); - let res = get_static_file(uri.clone(), &root).await.unwrap(); - - if res.status() == StatusCode::OK { - res.into_response() - } else { - let handler = leptos_axum::render_app_to_stream(options.to_owned(), move || view! { <App/> }); - handler(req).await.into_response() - } -} - -async fn get_static_file(uri: Uri, root: &str) -> Result<Response<BoxBody>, (StatusCode, String)> { - let req = Request::builder() - .uri(uri.clone()) - .body(Body::empty()) - .unwrap(); - // `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot` - // This path is relative to the cargo root - match ServeDir::new(root).oneshot(req).await { - Ok(res) => Ok(res.map(boxed)), - Err(err) => Err(( - StatusCode::INTERNAL_SERVER_ERROR, - format!("Something went wrong: {err}"), - )), - } -} diff --git a/examples/pavex_demo/todo_app_sqlite_pavex/src/leptos.rs b/examples/pavex_demo/todo_app_sqlite_pavex/src/leptos.rs deleted file mode 100644 index 1c774ae86c..0000000000 --- a/examples/pavex_demo/todo_app_sqlite_pavex/src/leptos.rs +++ /dev/null @@ -1,19 +0,0 @@ -use leptos::{get_configuration, leptos_config::ConfFile}; -use leptos_pavex::generate_route_list; -use leptos_router::RouteListing; -use pavex::{ - http::header::{ToStrError, USER_AGENT}, - request::RequestHead, - response::Response, -}; - -/// Easier to do this to avoid having to register things with Blueprints -/// Provide LeptosOptions via env vars provided by cargo-leptos or the user -pub fn get_cargo_leptos_conf() -> ConfFile { - get_configuration(None) -} - -/// Generate all possible non server fn routes for our app -pub fn get_app_route_listing() -> Vec<RouteListing> { - generate_route_list(TodoApp) -} diff --git a/examples/pavex_demo/todo_app_sqlite_pavex/src/lib.rs b/examples/pavex_demo/todo_app_sqlite_pavex/src/lib.rs deleted file mode 100644 index bea4f0fb94..0000000000 --- a/examples/pavex_demo/todo_app_sqlite_pavex/src/lib.rs +++ /dev/null @@ -1,7 +0,0 @@ -mod blueprint; -pub mod configuration; -pub mod leptos; -pub mod routes; -pub mod telemetry; -pub mod user_agent; -pub use blueprint::blueprint; diff --git a/examples/pavex_demo/todo_app_sqlite_pavex/src/routes/greet.rs b/examples/pavex_demo/todo_app_sqlite_pavex/src/routes/greet.rs deleted file mode 100644 index 1ac11bc56d..0000000000 --- a/examples/pavex_demo/todo_app_sqlite_pavex/src/routes/greet.rs +++ /dev/null @@ -1,21 +0,0 @@ -use crate::user_agent::UserAgent; -use pavex::{request::route::RouteParams, response::Response}; - -#[RouteParams] -pub struct GreetParams { - pub name: String, -} -pub fn greet( - params: RouteParams<GreetParams>, - user_agent: UserAgent, -) -> Response { - if let UserAgent::Unknown = user_agent { - return Response::unauthorized() - .set_typed_body("You must provide a `User-Agent` header") - .box_body(); - } - let GreetParams { name } = params.0; - Response::ok() - .set_typed_body(format!("Hello, {name}!")) - .box_body() -} diff --git a/examples/pavex_demo/todo_app_sqlite_pavex/src/routes/mod.rs b/examples/pavex_demo/todo_app_sqlite_pavex/src/routes/mod.rs deleted file mode 100644 index 47f146cc9d..0000000000 --- a/examples/pavex_demo/todo_app_sqlite_pavex/src/routes/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod greet; -pub mod status; - diff --git a/examples/pavex_demo/todo_app_sqlite_pavex/src/routes/status.rs b/examples/pavex_demo/todo_app_sqlite_pavex/src/routes/status.rs deleted file mode 100644 index acec3eefe4..0000000000 --- a/examples/pavex_demo/todo_app_sqlite_pavex/src/routes/status.rs +++ /dev/null @@ -1,7 +0,0 @@ -use pavex::http::StatusCode; - -/// Respond with a `200 OK` status code to indicate that the server is alive -/// and ready to accept new requests. -pub fn ping() -> StatusCode { - StatusCode::OK -} diff --git a/examples/pavex_demo/todo_app_sqlite_pavex/src/telemetry.rs b/examples/pavex_demo/todo_app_sqlite_pavex/src/telemetry.rs deleted file mode 100644 index 6867b98767..0000000000 --- a/examples/pavex_demo/todo_app_sqlite_pavex/src/telemetry.rs +++ /dev/null @@ -1,84 +0,0 @@ -use pavex::request::route::MatchedRouteTemplate; -use pavex::http::Version; -use pavex::middleware::Next; -use pavex::request::RequestHead; -use pavex::response::Response; -use std::borrow::Cow; -use std::future::IntoFuture; -use tracing::Instrument; - -/// A logging middleware that wraps the request pipeline in the root span. -/// It takes care to record key information about the request and the response. -pub async fn logger<T>(next: Next<T>, root_span: RootSpan) -> Response - where - T: IntoFuture<Output = Response>, -{ - let response = next - .into_future() - .instrument(root_span.clone().into_inner()) - .await; - root_span.record_response_data(&response); - response -} - -/// A root span is the top-level *logical* span for an incoming request. -/// -/// It is not necessarily the top-level *physical* span, as it may be a child of -/// another span (e.g. a span representing the underlying HTTP connection). -/// -/// We use the root span to attach as much information as possible about the -/// incoming request, and to record the final outcome of the request (success or -/// failure). -#[derive(Debug, Clone)] -pub struct RootSpan(tracing::Span); - -impl RootSpan { - /// Create a new root span for the given request. - /// - /// We follow OpenTelemetry's HTTP semantic conventions as closely as - /// possible for field naming. - pub fn new(request_head: &RequestHead, matched_route: MatchedRouteTemplate) -> Self { - let user_agent = request_head - .headers - .get("User-Agent") - .map(|h| h.to_str().unwrap_or_default()) - .unwrap_or_default(); - - let span = tracing::info_span!( - "HTTP request", - http.method = %request_head.method, - http.flavor = %http_flavor(request_head.version), - user_agent.original = %user_agent, - http.response.status_code = tracing::field::Empty, - http.route = %matched_route, - http.target = %request_head.uri.path_and_query().map(|p| p.as_str()).unwrap_or(""), - ); - Self(span) - } - - pub fn record_response_data(&self, response: &Response) { - self.0 - .record("http.response.status_code", &response.status().as_u16()); - } - - /// Get a reference to the underlying [`tracing::Span`]. - pub fn inner(&self) -> &tracing::Span { - &self.0 - } - - /// Deconstruct the root span into its underlying [`tracing::Span`]. - pub fn into_inner(self) -> tracing::Span { - self.0 - } -} - -fn http_flavor(version: Version) -> Cow<'static, str> { - match version { - Version::HTTP_09 => "0.9".into(), - Version::HTTP_10 => "1.0".into(), - Version::HTTP_11 => "1.1".into(), - Version::HTTP_2 => "2.0".into(), - Version::HTTP_3 => "3.0".into(), - other => format!("{other:?}").into(), - } -} \ No newline at end of file diff --git a/examples/pavex_demo/todo_app_sqlite_pavex/src/user_agent.rs b/examples/pavex_demo/todo_app_sqlite_pavex/src/user_agent.rs deleted file mode 100644 index 1e08618e1e..0000000000 --- a/examples/pavex_demo/todo_app_sqlite_pavex/src/user_agent.rs +++ /dev/null @@ -1,27 +0,0 @@ -use pavex::{ - http::header::{ToStrError, USER_AGENT}, - request::RequestHead, - response::Response, -}; - -pub enum UserAgent { - /// No User-Agent header was provided - Unknown, - /// The value of the 'User-Agent' header for the incoming request - Known(String), -} -impl UserAgent { - pub fn extract(request_head: &RequestHead) -> Result<Self, ToStrError> { - let Some(user_agent) = request_head.headers.get(USER_AGENT) else { - return Ok(UserAgent::Unknown); - }; - - user_agent.to_str().map(|s| UserAgent::Known(s.into())) - } -} - -pub fn invalid_user_agent(_e: &ToStrError) -> Response { - Response::bad_request() - .set_typed_body("The `User-Agent` header must be a valid UTF-8 string") - .box_body() -} diff --git a/examples/pavex_demo/todo_app_sqlite_pavex_server/Cargo.toml b/examples/pavex_demo/todo_app_sqlite_pavex_server/Cargo.toml deleted file mode 100644 index 52d1462184..0000000000 --- a/examples/pavex_demo/todo_app_sqlite_pavex_server/Cargo.toml +++ /dev/null @@ -1,29 +0,0 @@ -[package] -name = "todo_app_sqlite_pavex_server" -version = "0.1.0" -edition = "2021" - -[[bin]] -path = "src/bin/api.rs" -name = "api" - -[dependencies] -anyhow = "1" -pavex = { git = "https://github.com/LukeMathWalker/pavex", branch = "main" } -tokio = { version = "1", features = ["full"] } -todo_app_sqlite_pavex_server_sdk = { path = "../todo_app_sqlite_pavex_server_sdk" } -todo_app_sqlite_pavex = { path = "../todo_app_sqlite_pavex" } - -# Configuration -dotenvy = "0.15" -figment = { version = "0.10", features = ["env", "yaml"] } -serde = { version = "1", features = ["derive"]} - -# Telemetry -tracing = "0.1" -tracing-bunyan-formatter = "0.3" -tracing-panic = "0.1" -tracing-subscriber = { version = "0.3", default-features = false, features = ["env-filter", "registry", "smallvec", "std", "tracing-log"] } - -[dev-dependencies] -reqwest = "0.11" \ No newline at end of file diff --git a/examples/pavex_demo/todo_app_sqlite_pavex_server/configuration/base.yml b/examples/pavex_demo/todo_app_sqlite_pavex_server/configuration/base.yml deleted file mode 100644 index 2e6c1938e4..0000000000 --- a/examples/pavex_demo/todo_app_sqlite_pavex_server/configuration/base.yml +++ /dev/null @@ -1,3 +0,0 @@ -server: - ip: "0.0.0.0" - port: 8000 \ No newline at end of file diff --git a/examples/pavex_demo/todo_app_sqlite_pavex_server/configuration/dev.yml b/examples/pavex_demo/todo_app_sqlite_pavex_server/configuration/dev.yml deleted file mode 100644 index fe1171b8cd..0000000000 --- a/examples/pavex_demo/todo_app_sqlite_pavex_server/configuration/dev.yml +++ /dev/null @@ -1,6 +0,0 @@ -# This file contains the configuration for the dev environment. -# None of the values here are actually secret, so it's fine -# to commit this file to the repository. -server: - ip: "127.0.0.1" - port: 8000 diff --git a/examples/pavex_demo/todo_app_sqlite_pavex_server/configuration/prod.yml b/examples/pavex_demo/todo_app_sqlite_pavex_server/configuration/prod.yml deleted file mode 100644 index 2a9375957f..0000000000 --- a/examples/pavex_demo/todo_app_sqlite_pavex_server/configuration/prod.yml +++ /dev/null @@ -1,3 +0,0 @@ -server: - ip: "0.0.0.0" - port: 8000 diff --git a/examples/pavex_demo/todo_app_sqlite_pavex_server/configuration/test.yml b/examples/pavex_demo/todo_app_sqlite_pavex_server/configuration/test.yml deleted file mode 100644 index 4e7a868f5e..0000000000 --- a/examples/pavex_demo/todo_app_sqlite_pavex_server/configuration/test.yml +++ /dev/null @@ -1,8 +0,0 @@ -# This file contains the configuration for the API when spawned -# in black-box tests. -# None of the values here are actually secret, so it's fine -# to commit this file to the repository. -server: - ip: "127.0.0.1" - # The OS will assign a random port to the test server. - port: 0 \ No newline at end of file diff --git a/examples/pavex_demo/todo_app_sqlite_pavex_server/src/bin/api.rs b/examples/pavex_demo/todo_app_sqlite_pavex_server/src/bin/api.rs deleted file mode 100644 index d5fd21d3d6..0000000000 --- a/examples/pavex_demo/todo_app_sqlite_pavex_server/src/bin/api.rs +++ /dev/null @@ -1,49 +0,0 @@ -use anyhow::Context; -use todo_app_sqlite_pavex_server::{ - configuration::load_configuration, - telemetry::{get_subscriber, init_telemetry}, -}; -use todo_app_sqlite_pavex_server_sdk::{build_application_state, run}; -use pavex::server::Server; - -#[tokio::main] -async fn main() -> anyhow::Result<()> { - let subscriber = get_subscriber("todo_app_sqlite_pavex".into(), "info".into(), std::io::stdout); - init_telemetry(subscriber)?; - - // We isolate all the server setup and launch logic in a separate function - // in order to have a single choke point where we make sure to log fatal errors - // that will cause the application to exit. - if let Err(e) = _main().await { - tracing::error!( - error.msg = %e, - error.error_chain = ?e, - "The application is exiting due to an error" - ) - } - - Ok(()) -} - -async fn _main() -> anyhow::Result<()> { - // Load environment variables from a .env file, if it exists. - let _ = dotenvy::dotenv(); - - let config = load_configuration(None)?; - let application_state = build_application_state() - .await; - - let tcp_listener = config - .server - .listener() - .await - .context("Failed to bind the server TCP listener")?; - let address = tcp_listener - .local_addr() - .context("The server TCP listener doesn't have a local socket address")?; - let server_builder = Server::new().listen(tcp_listener); - - tracing::info!("Starting to listen for incoming requests at {}", address); - run(server_builder, application_state).await; - Ok(()) -} diff --git a/examples/pavex_demo/todo_app_sqlite_pavex_server/src/configuration.rs b/examples/pavex_demo/todo_app_sqlite_pavex_server/src/configuration.rs deleted file mode 100644 index 093377b596..0000000000 --- a/examples/pavex_demo/todo_app_sqlite_pavex_server/src/configuration.rs +++ /dev/null @@ -1,140 +0,0 @@ -use std::env::VarError; - -use anyhow::Context; -use todo_app_sqlite_pavex::configuration::Config; -use figment::{ - providers::{Env, Format, Yaml}, - Figment, -}; - -/// Retrieve the application configuration by merging together multiple configuration sources. -/// -/// # Application profiles -/// -/// We use the concept of application profiles to allow for -/// different configuration values depending on the type of environment -/// the application is running in. -/// -/// We don't rely on `figment`'s built-in support for profiles because -/// we want to make sure that values for different profiles are not co-located in -/// the same configuration file. -/// This makes it easier to avoid leaking sensitive information by mistake (e.g. -/// by committing configuration values for the `dev` profile to the repository). -/// -/// You primary mechanism to specify the desired application profile is the `APP_PROFILE` -/// environment variable. -/// You can pass a `default_profile` value that will be used if the environment variable -/// is not set. -/// -/// # Hierarchy -/// -/// The configuration sources are: -/// -/// 1. `base.yml` - Contains the default configuration values, common to all profiles. -/// 2. `<profile>.yml` - Contains the configuration values specific to the desired profile. -/// 3. Environment variables - Contains the configuration values specific to the current environment. -/// -/// The configuration sources are listed in priority order, i.e. -/// the last source in the list will override any previous source. -/// -/// For example, if the same configuration key is defined in both -/// the YAML file and the environment, the value from the environment -/// will be used. -pub fn load_configuration( - default_profile: Option<ApplicationProfile>, -) -> Result<Config, anyhow::Error> { - let application_profile = load_app_profile(default_profile) - .context("Failed to load the desired application profile")?; - - let configuration_dir = { - let manifest_dir = env!( - "CARGO_MANIFEST_DIR", - "`CARGO_MANIFEST_DIR` was not set. Are you using a custom build system?" - ); - std::path::Path::new(manifest_dir).join("configuration") - }; - - let base_filepath = configuration_dir.join("base.yml"); - - let profile_filename = format!("{}.yml", application_profile.as_str()); - let profile_filepath = configuration_dir.join(profile_filename); - - let figment = Figment::new() - .merge(Yaml::file(base_filepath)) - .merge(Yaml::file(profile_filepath)) - .merge(Env::prefixed("APP_")); - - let configuration: Config = figment - .extract() - .context("Failed to load hierarchical configuration")?; - Ok(configuration) -} - -/// Load the application profile from the `APP_PROFILE` environment variable. -fn load_app_profile( - default_profile: Option<ApplicationProfile>, -) -> Result<ApplicationProfile, anyhow::Error> { - static PROFILE_ENV_VAR: &str = "APP_PROFILE"; - - match std::env::var(PROFILE_ENV_VAR) { - Ok(raw_value) => raw_value.parse().with_context(|| { - format!("Failed to parse the `{PROFILE_ENV_VAR}` environment variable") - }), - Err(VarError::NotPresent) if default_profile.is_some() => Ok(default_profile.unwrap()), - Err(e) => Err(anyhow::anyhow!(e).context(format!( - "Failed to read the `{PROFILE_ENV_VAR}` environment variable" - ))), - } -} - -/// The application profile, i.e. the type of environment the application is running in. -/// See [`load_configuration`] for more details. -pub enum ApplicationProfile { - /// Test profile. - /// - /// This is the profile used by the integration test suite. - Test, - /// Local development profile. - /// - /// This is the profile you should use when running the application locally - /// for exploratory testing. - /// - /// The corresponding configuration file is `dev.yml` and it's *never* committed to the repository. - Dev, - /// Production profile. - /// - /// This is the profile you should use when running the application in production—e.g. - /// when deploying it to a staging or production environment, exposed to live traffic. - /// - /// The corresponding configuration file is `prod.yml`. - /// It's committed to the repository, but it's meant to contain exclusively - /// non-sensitive configuration values. - Prod, -} - -impl ApplicationProfile { - /// Return the environment as a string. - pub fn as_str(&self) -> &'static str { - match self { - ApplicationProfile::Test => "test", - ApplicationProfile::Dev => "dev", - ApplicationProfile::Prod => "prod", - } - } -} - -impl std::str::FromStr for ApplicationProfile { - type Err = anyhow::Error; - - fn from_str(s: &str) -> Result<Self, Self::Err> { - match s.to_lowercase().as_str() { - "test" => Ok(ApplicationProfile::Test), - "dev" | "development" => Ok(ApplicationProfile::Dev), - "prod" | "production" => Ok(ApplicationProfile::Prod), - s => Err(anyhow::anyhow!( - "`{}` is not a valid application profile.\nValid options are: `test`, `dev`, `prod`.", - s - )), - } - } -} diff --git a/examples/pavex_demo/todo_app_sqlite_pavex_server/src/lib.rs b/examples/pavex_demo/todo_app_sqlite_pavex_server/src/lib.rs deleted file mode 100644 index 39bc2ff72c..0000000000 --- a/examples/pavex_demo/todo_app_sqlite_pavex_server/src/lib.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod configuration; -pub mod telemetry; \ No newline at end of file diff --git a/examples/pavex_demo/todo_app_sqlite_pavex_server/src/telemetry.rs b/examples/pavex_demo/todo_app_sqlite_pavex_server/src/telemetry.rs deleted file mode 100644 index 848bc1884c..0000000000 --- a/examples/pavex_demo/todo_app_sqlite_pavex_server/src/telemetry.rs +++ /dev/null @@ -1,40 +0,0 @@ -use anyhow::Context; -use tracing::subscriber::set_global_default; -use tracing::Subscriber; -use tracing_bunyan_formatter::{BunyanFormattingLayer, JsonStorageLayer}; -use tracing_subscriber::fmt::MakeWriter; -use tracing_subscriber::{layer::SubscriberExt, EnvFilter, Registry}; - -/// Perform all the required setup steps for our telemetry: -/// -/// - Register a subscriber as global default to process span data -/// - Register a panic hook to capture any panic and record its details -/// -/// It should only be called once! -pub fn init_telemetry(subscriber: impl Subscriber + Sync + Send) -> Result<(), anyhow::Error> { - std::panic::set_hook(Box::new(tracing_panic::panic_hook)); - set_global_default(subscriber).context("Failed to set a `tracing` global subscriber") -} - -/// Compose multiple layers into a `tracing`'s subscriber. -/// -/// # Implementation Notes -/// -/// We are using `impl Subscriber` as return type to avoid having to spell out the actual -/// type of the returned subscriber, which is indeed quite complex. -pub fn get_subscriber<Sink>( - application_name: String, - default_env_filter: String, - sink: Sink, -) -> impl Subscriber + Sync + Send - where - Sink: for<'a> MakeWriter<'a> + Send + Sync + 'static, -{ - let env_filter = - EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(default_env_filter)); - let formatting_layer = BunyanFormattingLayer::new(application_name, sink); - Registry::default() - .with(env_filter) - .with(JsonStorageLayer) - .with(formatting_layer) -} \ No newline at end of file diff --git a/examples/pavex_demo/todo_app_sqlite_pavex_server/tests/integration/greet.rs b/examples/pavex_demo/todo_app_sqlite_pavex_server/tests/integration/greet.rs deleted file mode 100644 index 9ba0dd35cc..0000000000 --- a/examples/pavex_demo/todo_app_sqlite_pavex_server/tests/integration/greet.rs +++ /dev/null @@ -1,37 +0,0 @@ -use crate::helpers::TestApi; -use pavex::http::StatusCode; - -#[tokio::test] -async fn greet_happy_path() { - let api = TestApi::spawn().await; - let name = "Ursula"; - - let response = api - .api_client - .get(&format!("{}/api/greet/{name}", &api.api_address)) - .header("User-Agent", "Test runner") - .send() - .await - .expect("Failed to execute request."); - assert_eq!(response.status().as_u16(), StatusCode::OK.as_u16()); - assert_eq!(response.text().await.unwrap(), "Hello, Ursula!"); -} - -#[tokio::test] -async fn non_utf8_agent_is_rejected() { - let api = TestApi::spawn().await; - let name = "Ursula"; - - let response = api - .api_client - .get(&format!("{}/api/greet/{name}", &api.api_address)) - .header("User-Agent", b"hello\xfa".as_slice()) - .send() - .await - .expect("Failed to execute request."); - assert_eq!(response.status().as_u16(), StatusCode::BAD_REQUEST.as_u16()); - assert_eq!( - response.text().await.unwrap(), - "The `User-Agent` header must be a valid UTF-8 string" - ); -} diff --git a/examples/pavex_demo/todo_app_sqlite_pavex_server/tests/integration/helpers.rs b/examples/pavex_demo/todo_app_sqlite_pavex_server/tests/integration/helpers.rs deleted file mode 100644 index 3eaff3713d..0000000000 --- a/examples/pavex_demo/todo_app_sqlite_pavex_server/tests/integration/helpers.rs +++ /dev/null @@ -1,52 +0,0 @@ -use todo_app_sqlite_pavex_server::configuration::{load_configuration, ApplicationProfile}; -use todo_app_sqlite_pavex_server_sdk::{build_application_state, run}; -use todo_app_sqlite_pavex::configuration::Config; -use pavex::server::Server; - -pub struct TestApi { - pub api_address: String, - pub api_client: reqwest::Client, -} - -impl TestApi { - pub async fn spawn() -> Self { - let config = Self::get_config(); - - let application_state = build_application_state().await; - - let tcp_listener = config - .server - .listener() - .await - .expect("Failed to bind the server TCP listener"); - let address = tcp_listener - .local_addr() - .expect("The server TCP listener doesn't have a local socket address"); - let server_builder = Server::new().listen(tcp_listener); - - tokio::spawn(async move { - run(server_builder, application_state).await - }); - - TestApi { - api_address: format!("http://{}:{}", config.server.ip, address.port()), - api_client: reqwest::Client::new(), - } - } - - fn get_config() -> Config { - load_configuration(Some(ApplicationProfile::Test)).expect("Failed to load test configuration") - } -} - -/// Convenient methods for calling the API under test. -impl TestApi { - pub async fn get_ping(&self) -> reqwest::Response - { - self.api_client - .get(&format!("{}/api/ping", &self.api_address)) - .send() - .await - .expect("Failed to execute request.") - } -} diff --git a/examples/pavex_demo/todo_app_sqlite_pavex_server/tests/integration/main.rs b/examples/pavex_demo/todo_app_sqlite_pavex_server/tests/integration/main.rs deleted file mode 100644 index 67767e05fc..0000000000 --- a/examples/pavex_demo/todo_app_sqlite_pavex_server/tests/integration/main.rs +++ /dev/null @@ -1,4 +0,0 @@ -mod greet; -mod helpers; -mod ping; - diff --git a/examples/pavex_demo/todo_app_sqlite_pavex_server/tests/integration/ping.rs b/examples/pavex_demo/todo_app_sqlite_pavex_server/tests/integration/ping.rs deleted file mode 100644 index c79eb0eb76..0000000000 --- a/examples/pavex_demo/todo_app_sqlite_pavex_server/tests/integration/ping.rs +++ /dev/null @@ -1,11 +0,0 @@ -use crate::helpers::TestApi; -use pavex::http::StatusCode; - -#[tokio::test] -async fn ping_works() { - let api = TestApi::spawn().await; - - let response = api.get_ping().await; - - assert_eq!(response.status().as_u16(), StatusCode::OK.as_u16()); -} diff --git a/examples/pavex_demo/todo_app_sqlite_pavex_server_sdk/Cargo.toml b/examples/pavex_demo/todo_app_sqlite_pavex_server_sdk/Cargo.toml deleted file mode 100644 index 336485067e..0000000000 --- a/examples/pavex_demo/todo_app_sqlite_pavex_server_sdk/Cargo.toml +++ /dev/null @@ -1,21 +0,0 @@ -[package] -name = "todo_app_sqlite_pavex_server_sdk" -version = "0.1.0" -edition = "2021" - -[package.metadata.px.generate] -generator_type = "cargo_workspace_binary" -generator_name = "bp" - -[lints] -clippy = { all = "allow" } - -[dependencies] -bytes = { version = "1.5.0", package = "bytes" } -http = { version = "1.0.0", package = "http" } -http_body_util = { version = "0.1.0", package = "http-body-util" } -hyper = { version = "1.1.0", package = "hyper" } -matchit = { version = "0.7.3", git = "https://github.com/ibraheemdev/matchit", branch = "master", package = "matchit" } -pavex = { version = "0.1.0", git = "https://github.com/LukeMathWalker/pavex", branch = "main", package = "pavex" } -thiserror = { version = "1.0.52", package = "thiserror" } -todo_app_sqlite_pavex = { version = "0.1.0", path = "../todo_app_sqlite_pavex", package = "todo_app_sqlite_pavex" } diff --git a/examples/pavex_demo/todo_app_sqlite_pavex_server_sdk/blueprint.ron b/examples/pavex_demo/todo_app_sqlite_pavex_server_sdk/blueprint.ron deleted file mode 100644 index 39906399ea..0000000000 --- a/examples/pavex_demo/todo_app_sqlite_pavex_server_sdk/blueprint.ron +++ /dev/null @@ -1,233 +0,0 @@ -( - creation_location: ( - line: 13, - column: 18, - file: "todo_app_sqlite_pavex/src/blueprint.rs", - ), - constructors: [ - ( - constructor: ( - callable: ( - registered_at: "todo_app_sqlite_pavex", - import_path: "pavex::request::query::QueryParams::extract", - ), - location: ( - line: 32, - column: 8, - file: "todo_app_sqlite_pavex/src/blueprint.rs", - ), - ), - lifecycle: RequestScoped, - cloning_strategy: None, - error_handler: Some(( - callable: ( - registered_at: "todo_app_sqlite_pavex", - import_path: "pavex::request::query::errors::ExtractQueryParamsError::into_response", - ), - location: ( - line: 36, - column: 6, - file: "todo_app_sqlite_pavex/src/blueprint.rs", - ), - )), - ), - ( - constructor: ( - callable: ( - registered_at: "todo_app_sqlite_pavex", - import_path: "pavex::request::route::RouteParams::extract", - ), - location: ( - line: 41, - column: 8, - file: "todo_app_sqlite_pavex/src/blueprint.rs", - ), - ), - lifecycle: RequestScoped, - cloning_strategy: None, - error_handler: Some(( - callable: ( - registered_at: "todo_app_sqlite_pavex", - import_path: "pavex::request::route::errors::ExtractRouteParamsError::into_response", - ), - location: ( - line: 45, - column: 6, - file: "todo_app_sqlite_pavex/src/blueprint.rs", - ), - )), - ), - ( - constructor: ( - callable: ( - registered_at: "todo_app_sqlite_pavex", - import_path: "pavex::request::body::JsonBody::extract", - ), - location: ( - line: 50, - column: 8, - file: "todo_app_sqlite_pavex/src/blueprint.rs", - ), - ), - lifecycle: RequestScoped, - cloning_strategy: None, - error_handler: Some(( - callable: ( - registered_at: "todo_app_sqlite_pavex", - import_path: "pavex::request::body::errors::ExtractJsonBodyError::into_response", - ), - location: ( - line: 54, - column: 6, - file: "todo_app_sqlite_pavex/src/blueprint.rs", - ), - )), - ), - ( - constructor: ( - callable: ( - registered_at: "todo_app_sqlite_pavex", - import_path: "pavex::request::body::BufferedBody::extract", - ), - location: ( - line: 57, - column: 8, - file: "todo_app_sqlite_pavex/src/blueprint.rs", - ), - ), - lifecycle: RequestScoped, - cloning_strategy: None, - error_handler: Some(( - callable: ( - registered_at: "todo_app_sqlite_pavex", - import_path: "pavex::request::body::errors::ExtractBufferedBodyError::into_response", - ), - location: ( - line: 61, - column: 6, - file: "todo_app_sqlite_pavex/src/blueprint.rs", - ), - )), - ), - ( - constructor: ( - callable: ( - registered_at: "todo_app_sqlite_pavex", - import_path: "<pavex::request::body::BodySizeLimit as std::default::Default>::default", - ), - location: ( - line: 64, - column: 8, - file: "todo_app_sqlite_pavex/src/blueprint.rs", - ), - ), - lifecycle: RequestScoped, - cloning_strategy: None, - error_handler: None, - ), - ( - constructor: ( - callable: ( - registered_at: "todo_app_sqlite_pavex", - import_path: "crate::user_agent::UserAgent::extract", - ), - location: ( - line: 16, - column: 8, - file: "todo_app_sqlite_pavex/src/blueprint.rs", - ), - ), - lifecycle: RequestScoped, - cloning_strategy: None, - error_handler: Some(( - callable: ( - registered_at: "todo_app_sqlite_pavex", - import_path: "crate::user_agent::invalid_user_agent", - ), - location: ( - line: 20, - column: 6, - file: "todo_app_sqlite_pavex/src/blueprint.rs", - ), - )), - ), - ( - constructor: ( - callable: ( - registered_at: "todo_app_sqlite_pavex", - import_path: "crate::telemetry::RootSpan::new", - ), - location: ( - line: 72, - column: 8, - file: "todo_app_sqlite_pavex/src/blueprint.rs", - ), - ), - lifecycle: RequestScoped, - cloning_strategy: Some(CloneIfNecessary), - error_handler: None, - ), - ], - middlewares: [ - ( - middleware: ( - callable: ( - registered_at: "todo_app_sqlite_pavex", - import_path: "crate::telemetry::logger", - ), - location: ( - line: 78, - column: 8, - file: "todo_app_sqlite_pavex/src/blueprint.rs", - ), - ), - error_handler: None, - ), - ], - routes: [ - ( - path: "/api/ping", - method_guard: ( - inner: Some(( - bitset: 256, - extensions: [], - )), - ), - request_handler: ( - callable: ( - registered_at: "todo_app_sqlite_pavex", - import_path: "crate::routes::status::ping", - ), - location: ( - line: 24, - column: 8, - file: "todo_app_sqlite_pavex/src/blueprint.rs", - ), - ), - error_handler: None, - ), - ( - path: "/api/greet/:name", - method_guard: ( - inner: Some(( - bitset: 256, - extensions: [], - )), - ), - request_handler: ( - callable: ( - registered_at: "todo_app_sqlite_pavex", - import_path: "crate::routes::greet::greet", - ), - location: ( - line: 25, - column: 8, - file: "todo_app_sqlite_pavex/src/blueprint.rs", - ), - ), - error_handler: None, - ), - ], - fallback_request_handler: None, - nested_blueprints: [], -) \ No newline at end of file diff --git a/examples/pavex_demo/todo_app_sqlite_pavex_server_sdk/src/lib.rs b/examples/pavex_demo/todo_app_sqlite_pavex_server_sdk/src/lib.rs deleted file mode 100644 index a76065bfb3..0000000000 --- a/examples/pavex_demo/todo_app_sqlite_pavex_server_sdk/src/lib.rs +++ /dev/null @@ -1,254 +0,0 @@ -//! Do NOT edit this code. -//! It was automatically generated by Pavex. -//! All manual edits will be lost next time the code is generated. -extern crate alloc; -struct ServerState { - router: matchit::Router<u32>, - #[allow(dead_code)] - application_state: ApplicationState, -} -pub struct ApplicationState {} -pub async fn build_application_state() -> crate::ApplicationState { - crate::ApplicationState {} -} -pub fn run( - server_builder: pavex::server::Server, - application_state: ApplicationState, -) -> pavex::server::ServerHandle { - let server_state = std::sync::Arc::new(ServerState { - router: build_router(), - application_state, - }); - server_builder.serve(route_request, server_state) -} -fn build_router() -> matchit::Router<u32> { - let mut router = matchit::Router::new(); - router.insert("/api/greet/:name", 0u32).unwrap(); - router.insert("/api/ping", 1u32).unwrap(); - router -} -async fn route_request( - request: http::Request<hyper::body::Incoming>, - server_state: std::sync::Arc<ServerState>, -) -> pavex::response::Response { - let (request_head, request_body) = request.into_parts(); - #[allow(unused)] - let request_body = pavex::request::body::RawIncomingBody::from(request_body); - let request_head: pavex::request::RequestHead = request_head.into(); - let matched_route = match server_state.router.at(&request_head.uri.path()) { - Ok(m) => m, - Err(_) => { - let allowed_methods: pavex::router::AllowedMethods = pavex::router::MethodAllowList::from_iter( - vec![], - ) - .into(); - let matched_route_template = pavex::request::route::MatchedRouteTemplate::new( - "*", - ); - return route_2::middleware_0( - matched_route_template, - &allowed_methods, - &request_head, - ) - .await; - } - }; - let route_id = matched_route.value; - #[allow(unused)] - let url_params: pavex::request::route::RawRouteParams<'_, '_> = matched_route - .params - .into(); - match route_id { - 0u32 => { - let matched_route_template = pavex::request::route::MatchedRouteTemplate::new( - "/api/greet/:name", - ); - match &request_head.method { - &pavex::http::Method::GET => { - route_1::middleware_0( - matched_route_template, - url_params, - &request_head, - ) - .await - } - _ => { - let allowed_methods: pavex::router::AllowedMethods = pavex::router::MethodAllowList::from_iter([ - pavex::http::Method::GET, - ]) - .into(); - route_2::middleware_0( - matched_route_template, - &allowed_methods, - &request_head, - ) - .await - } - } - } - 1u32 => { - let matched_route_template = pavex::request::route::MatchedRouteTemplate::new( - "/api/ping", - ); - match &request_head.method { - &pavex::http::Method::GET => { - route_0::middleware_0(matched_route_template, &request_head).await - } - _ => { - let allowed_methods: pavex::router::AllowedMethods = pavex::router::MethodAllowList::from_iter([ - pavex::http::Method::GET, - ]) - .into(); - route_2::middleware_0( - matched_route_template, - &allowed_methods, - &request_head, - ) - .await - } - } - } - i => unreachable!("Unknown route id: {}", i), - } -} -pub mod route_0 { - pub async fn middleware_0( - v0: pavex::request::route::MatchedRouteTemplate, - v1: &pavex::request::RequestHead, - ) -> pavex::response::Response { - let v2 = todo_app_sqlite_pavex::telemetry::RootSpan::new(v1, v0); - let v3 = crate::route_0::Next0 { - next: handler, - }; - let v4 = pavex::middleware::Next::new(v3); - todo_app_sqlite_pavex::telemetry::logger(v4, v2).await - } - pub async fn handler() -> pavex::response::Response { - let v0 = todo_app_sqlite_pavex::routes::status::ping(); - <http::StatusCode as pavex::response::IntoResponse>::into_response(v0) - } - pub struct Next0<T> - where - T: std::future::Future<Output = pavex::response::Response>, - { - next: fn() -> T, - } - impl<T> std::future::IntoFuture for Next0<T> - where - T: std::future::Future<Output = pavex::response::Response>, - { - type Output = pavex::response::Response; - type IntoFuture = T; - fn into_future(self) -> Self::IntoFuture { - (self.next)() - } - } -} -pub mod route_1 { - pub async fn middleware_0( - v0: pavex::request::route::MatchedRouteTemplate, - v1: pavex::request::route::RawRouteParams<'_, '_>, - v2: &pavex::request::RequestHead, - ) -> pavex::response::Response { - let v3 = todo_app_sqlite_pavex::telemetry::RootSpan::new(v2, v0); - let v4 = crate::route_1::Next0 { - s_0: v1, - s_1: v2, - next: handler, - }; - let v5 = pavex::middleware::Next::new(v4); - todo_app_sqlite_pavex::telemetry::logger(v5, v3).await - } - pub async fn handler( - v0: pavex::request::route::RawRouteParams<'_, '_>, - v1: &pavex::request::RequestHead, - ) -> pavex::response::Response { - let v2 = todo_app_sqlite_pavex::user_agent::UserAgent::extract(v1); - let v3 = match v2 { - Ok(ok) => ok, - Err(v3) => { - return { - let v4 = todo_app_sqlite_pavex::user_agent::invalid_user_agent(&v3); - <pavex::response::Response as pavex::response::IntoResponse>::into_response( - v4, - ) - }; - } - }; - let v4 = pavex::request::route::RouteParams::extract(v0); - let v5 = match v4 { - Ok(ok) => ok, - Err(v5) => { - return { - let v6 = pavex::request::route::errors::ExtractRouteParamsError::into_response( - &v5, - ); - <pavex::response::Response< - http_body_util::Full<bytes::Bytes>, - > as pavex::response::IntoResponse>::into_response(v6) - }; - } - }; - let v6 = todo_app_sqlite_pavex::routes::greet::greet(v5, v3); - <pavex::response::Response as pavex::response::IntoResponse>::into_response(v6) - } - pub struct Next0<'a, 'b, 'c, T> - where - T: std::future::Future<Output = pavex::response::Response>, - { - s_0: pavex::request::route::RawRouteParams<'a, 'b>, - s_1: &'c pavex::request::RequestHead, - next: fn( - pavex::request::route::RawRouteParams<'a, 'b>, - &'c pavex::request::RequestHead, - ) -> T, - } - impl<'a, 'b, 'c, T> std::future::IntoFuture for Next0<'a, 'b, 'c, T> - where - T: std::future::Future<Output = pavex::response::Response>, - { - type Output = pavex::response::Response; - type IntoFuture = T; - fn into_future(self) -> Self::IntoFuture { - (self.next)(self.s_0, self.s_1) - } - } -} -pub mod route_2 { - pub async fn middleware_0( - v0: pavex::request::route::MatchedRouteTemplate, - v1: &pavex::router::AllowedMethods, - v2: &pavex::request::RequestHead, - ) -> pavex::response::Response { - let v3 = todo_app_sqlite_pavex::telemetry::RootSpan::new(v2, v0); - let v4 = crate::route_2::Next0 { - s_0: v1, - next: handler, - }; - let v5 = pavex::middleware::Next::new(v4); - todo_app_sqlite_pavex::telemetry::logger(v5, v3).await - } - pub async fn handler( - v0: &pavex::router::AllowedMethods, - ) -> pavex::response::Response { - let v1 = pavex::router::default_fallback(v0).await; - <pavex::response::Response as pavex::response::IntoResponse>::into_response(v1) - } - pub struct Next0<'a, T> - where - T: std::future::Future<Output = pavex::response::Response>, - { - s_0: &'a pavex::router::AllowedMethods, - next: fn(&'a pavex::router::AllowedMethods) -> T, - } - impl<'a, T> std::future::IntoFuture for Next0<'a, T> - where - T: std::future::Future<Output = pavex::response::Response>, - { - type Output = pavex::response::Response; - type IntoFuture = T; - fn into_future(self) -> Self::IntoFuture { - (self.next)(self.s_0) - } - } -} diff --git a/integrations/pavex/Cargo.toml b/integrations/pavex/Cargo.toml deleted file mode 100644 index 5ac8c3d8d4..0000000000 --- a/integrations/pavex/Cargo.toml +++ /dev/null @@ -1,24 +0,0 @@ -[package] -name = "leptos_pavex" -version = { workspace = true } -edition = "2021" -authors = ["Ben Wishovich"] -license = "MIT" -repository = "https://github.com/leptos-rs/leptos" -description = "Pavex integrations for the Leptos web framework." - -[dependencies] -pavex = { git = "https://github.com/LukeMathWalker/pavex", branch = "main" } -futures = "0.3" -leptos = { workspace = true, features = ["ssr"] } -leptos_meta = { workspace = true, features = ["ssr"] } -leptos_router = { workspace = true, features = ["ssr"] } -leptos_integration_utils = { workspace = true } -parking_lot = "0.12.1" -regex = "1.7.0" -tracing = "0.1.37" -tokio = { version = "1", features = ["full"] } - -[features] -nonce = ["leptos/nonce"] -experimental-islands = ["leptos_integration_utils/experimental-islands"] diff --git a/integrations/pavex/Makefile.toml b/integrations/pavex/Makefile.toml deleted file mode 100644 index 4ed6229141..0000000000 --- a/integrations/pavex/Makefile.toml +++ /dev/null @@ -1,4 +0,0 @@ -extend = { path = "../../cargo-make/main.toml" } - -[tasks.check-format] -env = { LEPTOS_PROJECT_DIRECTORY = "../../" } diff --git a/integrations/pavex/src/lib.rs b/integrations/pavex/src/lib.rs deleted file mode 100644 index a21f110f9a..0000000000 --- a/integrations/pavex/src/lib.rs +++ /dev/null @@ -1,1554 +0,0 @@ -#![forbid(unsafe_code)] - -//! Provides functions to easily integrate Leptos with Actix. -//! -//! For more details on how to use the integrations, see the -//! [`examples`](https://github.com/leptos-rs/leptos/tree/main/examples) -//! directory in the Leptos repository. - -//use actix_web::{ -// body::BoxBody, -// dev::{ServiceFactory, ServiceRequest}, -// http::header, -// web::{Bytes, ServiceConfig}, -// *, -//}; -use futures::{Stream, StreamExt}; -use leptos::{ - leptos_server::{server_fn_by_path, Payload}, - server_fn::Encoding, - ssr::render_to_stream_with_prefix_undisposed_with_context_and_block_replacement, - *, -}; -use leptos_integration_utils::{build_async_response, html_parts_separated}; -use leptos_meta::*; -use leptos_router::*; -use parking_lot::RwLock; -use pavex::{http::header::{self, HeaderMap, HeaderName, HeaderValue}, request::{RequestHead, body::RawIncomingBody}}; -use pavex::http::StatusCode; -use pavex::request::Request; -use pavex::response::body::raw::BoxBody; -use pavex::response::Response; -use regex::Regex; -use std::{ - fmt::{Debug, Display}, - future::Future, - pin::Pin, - sync::Arc, -}; -#[cfg(debug_assertions)] -use tracing::instrument; -/// This struct lets you define headers and override the status of the Response from an Element or a Server Function -/// Typically contained inside of a ResponseOptions. Setting this is useful for cookies and custom responses. -#[derive(Debug, Clone, Default)] -pub struct ResponseParts { - pub headers: HeaderMap, - pub status: Option<StatusCode>, -} - -impl ResponseParts { - /// Insert a header, overwriting any previous value with the same key - pub fn insert_header( - &mut self, - key: header::HeaderName, - value: header::HeaderValue, - ) { - self.headers.insert(key, value); - } - /// Append a header, leaving any header with the same key intact - pub fn append_header( - &mut self, - key: header::HeaderName, - value: header::HeaderValue, - ) { - self.headers.append(key, value); - } -} - -/// Allows you to override details of the HTTP response like the status code and add Headers/Cookies. -#[derive(Debug, Clone, Default)] -pub struct ResponseOptions(pub Arc<RwLock<ResponseParts>>); - -impl ResponseOptions { - /// A simpler way to overwrite the contents of `ResponseOptions` with a new `ResponseParts`. - pub fn overwrite(&self, parts: ResponseParts) { - let mut writable = self.0.write(); - *writable = parts - } - /// Set the status of the returned Response. - pub fn set_status(&self, status: StatusCode) { - let mut writeable = self.0.write(); - let res_parts = &mut *writeable; - res_parts.status = Some(status); - } - /// Insert a header, overwriting any previous value with the same key. - pub fn insert_header( - &self, - key: header::HeaderName, - value: header::HeaderValue, - ) { - let mut writeable = self.0.write(); - let res_parts = &mut *writeable; - res_parts.headers.insert(key, value); - } - /// Append a header, leaving any header with the same key intact. - pub fn append_header( - &self, - key: header::HeaderName, - value: header::HeaderValue, - ) { - let mut writeable = self.0.write(); - let res_parts = &mut *writeable; - res_parts.headers.append(key, value); - } -} -/// We're creating this type because Pavex doesn't provide one, and this seems -/// easier than trying to convert into the other ones -#[derive(Debug, Clone)] -pub struct PavexRequest{ -head: RequestHead, -body: RawIncomingBody, -} - -impl PavexRequest{ - // Tell Pavex how to create this type - pub fn extract(head: &RequestHead, body: RawIncomingBody) -> PavexRequest{ - PavexRequest { head, body } - } -} - -/// Provides an easy way to redirect the user from within a server function. Mimicking the Remix `redirect()`, -/// it sets a [StatusCode] of 302 and a [LOCATION](header::LOCATION) header with the provided value. -/// If looking to redirect from the client, `leptos_router::use_navigate()` should be used instead. -#[tracing::instrument(level = "trace", fields(error), skip_all)] -pub fn redirect(path: &str) { - if let Some(response_options) = use_context::<ResponseOptions>() { - response_options.set_status(StatusCode::FOUND); - response_options.insert_header( - header::LOCATION, - header::HeaderValue::from_str(path) - .expect("Failed to create HeaderValue"), - ); - } -} - -/// An Actix [struct@Route](actix_web::Route) that listens for a `POST` request with -/// Leptos server function arguments in the body, runs the server function if found, -/// and returns the resulting [HttpResponse]. -/// -/// This can then be set up at an appropriate route in your application: -/// -/// ``` -/// use actix_web::*; -/// -/// fn register_server_functions() { -/// // call ServerFn::register() for each of the server functions you've defined -/// } -/// -/// # if false { // don't actually try to run a server in a doctest... -/// #[actix_web::main] -/// async fn main() -> std::io::Result<()> { -/// // make sure you actually register your server functions -/// register_server_functions(); -/// -/// HttpServer::new(|| { -/// App::new() -/// // "/api" should match the prefix, if any, declared when defining server functions -/// // {tail:.*} passes the remainder of the URL as the server function name -/// .route("/api/{tail:.*}", leptos_actix::handle_server_fns()) -/// }) -/// .bind(("127.0.0.1", 8080))? -/// .run() -/// .await -/// } -/// # } -/// ``` -/// -/// ## Provided Context Types -/// This function always provides context values including the following types: -/// - [ResponseOptions] -/// - [HttpRequest](actix_web::HttpRequest) -#[tracing::instrument(level = "trace", fields(error), skip_all)] -pub fn handle_server_fns() -> Route { - handle_server_fns_with_context(|| {}) -} - -/// An Actix [struct@Route](actix_web::Route) that listens for `GET` or `POST` requests with -/// Leptos server function arguments in the URL (`GET`) or body (`POST`), -/// runs the server function if found, and returns the resulting [HttpResponse]. -/// -/// This can then be set up at an appropriate route in your application: -/// -/// This version allows you to pass in a closure that adds additional route data to the -/// context, allowing you to pass in info about the route or user from Actix, or other info. -/// -/// **NOTE**: If your server functions expect a context, make sure to provide it both in -/// [`handle_server_fns_with_context`] **and** in [`leptos_routes_with_context`] (or whatever -/// rendering method you are using). During SSR, server functions are called by the rendering -/// method, while subsequent calls from the client are handled by the server function handler. -/// The same context needs to be provided to both handlers. -/// -/// ## Provided Context Types -/// This function always provides context values including the following types: -/// - [ResponseOptions] -/// - [HttpRequest](actix_web::HttpRequest) -#[tracing::instrument(level = "trace", fields(error), skip_all)] -pub fn handle_server_fns_with_context( - additional_context: impl Fn() + 'static + Clone + Send, -) -> Route { - web::to( - move |req: HttpRequest, params: web::Path<String>, body: web::Bytes| { - let additional_context = additional_context.clone(); - async move { - let additional_context = additional_context.clone(); - - let path = params.into_inner(); - let accept_header = req - .headers() - .get("Accept") - .and_then(|value| value.to_str().ok()); - - if let Some(server_fn) = server_fn_by_path(path.as_str()) { - let body_ref: &[u8] = &body; - - let runtime = create_runtime(); - - // Add additional info to the context of the server function - additional_context(); - let res_options = ResponseOptions::default(); - - // provide HttpRequest as context in server scope - provide_context(req.clone()); - provide_context(res_options.clone()); - - // we consume the body here (using the web::Bytes extractor), but it is required for things - // like MultipartForm - if req - .headers() - .get("Content-Type") - .and_then(|value| value.to_str().ok()) - .map(|value| { - value.starts_with("multipart/form-data; boundary=") - }) - == Some(true) - { - provide_context(body.clone()); - } - - let query = req.query_string().as_bytes(); - - let data = match &server_fn.encoding() { - Encoding::Url | Encoding::Cbor => body_ref, - Encoding::GetJSON | Encoding::GetCBOR => query, - }; - - let res = match server_fn.call((), data).await { - Ok(serialized) => { - let res_options = - use_context::<ResponseOptions>().unwrap(); - - let mut res: HttpResponseBuilder = - HttpResponse::Ok(); - let res_parts = res_options.0.write(); - - // if accept_header isn't set to one of these, it's a form submit - // redirect back to the referrer if not redirect has been set - if accept_header != Some("application/json") - && accept_header - != Some("application/x-www-form-urlencoded") - && accept_header != Some("application/cbor") - { - // Location will already be set if redirect() has been used - let has_location_set = - res_parts.headers.get("Location").is_some(); - if !has_location_set { - let referer = req - .headers() - .get("Referer") - .and_then(|value| value.to_str().ok()) - .unwrap_or("/"); - res = HttpResponse::SeeOther(); - res.insert_header(("Location", referer)) - .content_type("application/json"); - } - }; - // Override StatusCode if it was set in a Resource or Element - if let Some(status) = res_parts.status { - res.status(status); - } - - // Use provided ResponseParts headers if they exist - let _count = res_parts - .headers - .clone() - .into_iter() - .map(|(k, v)| { - res.append_header((k, v)); - }) - .count(); - - match serialized { - Payload::Binary(data) => { - res.content_type("application/cbor"); - res.body(Bytes::from(data)) - } - Payload::Url(data) => { - res.content_type( - "application/x-www-form-urlencoded", - ); - res.body(data) - } - Payload::Json(data) => { - res.content_type("application/json"); - res.body(data) - } - } - } - Err(e) => HttpResponse::InternalServerError().body( - serde_json::to_string(&e) - .unwrap_or_else(|_| e.to_string()), - ), - }; - // clean up the scope - runtime.dispose(); - res - } else { - HttpResponse::BadRequest().body(format!( - "Could not find a server function at the route {:?}. \ - \n\nIt's likely that either - 1. The API prefix you specify in the `#[server]` \ - macro doesn't match the prefix at which your server \ - function handler is mounted, or \n2. You are on a \ - platform that doesn't support automatic server \ - function registration and you need to call \ - ServerFn::register_explicit() on the server function \ - type, somewhere in your `main` function.", - req.path() - )) - } - } - }, - ) -} - -/// Returns an Actix [struct@Route](actix_web::Route) that listens for a `GET` request and tries -/// to route it using [leptos_router], serving an HTML stream of your application. The stream -/// will include fallback content for any `<Suspense/>` nodes, and be immediately interactive, -/// but requires some client-side JavaScript. -/// -/// The provides a [MetaContext] and a [RouterIntegrationContext] to app’s context before -/// rendering it, and includes any meta tags injected using [leptos_meta]. -/// -/// The HTML stream is rendered using [render_to_stream](leptos::ssr::render_to_stream), and -/// includes everything described in the documentation for that function. -/// -/// This can then be set up at an appropriate route in your application: -/// ``` -/// use actix_web::{App, HttpServer}; -/// use leptos::*; -/// use leptos_router::Method; -/// use std::{env, net::SocketAddr}; -/// -/// #[component] -/// fn MyApp() -> impl IntoView { -/// view! { <main>"Hello, world!"</main> } -/// } -/// -/// # if false { // don't actually try to run a server in a doctest... -/// #[actix_web::main] -/// async fn main() -> std::io::Result<()> { -/// let conf = get_configuration(Some("Cargo.toml")).await.unwrap(); -/// let addr = conf.leptos_options.site_addr.clone(); -/// HttpServer::new(move || { -/// let leptos_options = &conf.leptos_options; -/// -/// App::new() -/// // {tail:.*} passes the remainder of the URL as the route -/// // the actual routing will be handled by `leptos_router` -/// .route( -/// "/{tail:.*}", -/// leptos_actix::render_app_to_stream( -/// leptos_options.to_owned(), -/// || view! { <MyApp/> }, -/// Method::Get, -/// ), -/// ) -/// }) -/// .bind(&addr)? -/// .run() -/// .await -/// } -/// # } -/// ``` -/// -/// ## Provided Context Types -/// This function always provides context values including the following types: -/// - [ResponseOptions] -/// - [HttpRequest](actix_web::HttpRequest) -/// - [MetaContext](leptos_meta::MetaContext) -/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext) -#[tracing::instrument(level = "trace", fields(error), skip_all)] -pub fn render_app_to_stream<IV>( - options: LeptosOptions, - app_fn: impl Fn() -> IV + Clone + 'static, - method: Method, -) -> Route -where - IV: IntoView, -{ - render_app_to_stream_with_context(options, || {}, app_fn, method) -} - -/// Returns an Actix [struct@Route](actix_web::Route) that listens for a `GET` request and tries -/// to route it using [leptos_router], serving an in-order HTML stream of your application. -/// This stream will pause at each `<Suspense/>` node and wait for it to resolve before -/// sending down its HTML. The app will become interactive once it has fully loaded. -/// -/// The provides a [MetaContext] and a [RouterIntegrationContext] to app’s context before -/// rendering it, and includes any meta tags injected using [leptos_meta]. -/// -/// The HTML stream is rendered using -/// [render_to_stream_in_order](leptos::ssr::render_to_stream_in_order), -/// and includes everything described in the documentation for that function. -/// -/// This can then be set up at an appropriate route in your application: -/// ``` -/// use actix_web::{App, HttpServer}; -/// use leptos::*; -/// use leptos_router::Method; -/// use std::{env, net::SocketAddr}; -/// -/// #[component] -/// fn MyApp() -> impl IntoView { -/// view! { <main>"Hello, world!"</main> } -/// } -/// -/// # if false { // don't actually try to run a server in a doctest... -/// #[actix_web::main] -/// async fn main() -> std::io::Result<()> { -/// let conf = get_configuration(Some("Cargo.toml")).await.unwrap(); -/// let addr = conf.leptos_options.site_addr.clone(); -/// HttpServer::new(move || { -/// let leptos_options = &conf.leptos_options; -/// -/// App::new() -/// // {tail:.*} passes the remainder of the URL as the route -/// // the actual routing will be handled by `leptos_router` -/// .route( -/// "/{tail:.*}", -/// leptos_actix::render_app_to_stream_in_order( -/// leptos_options.to_owned(), -/// || view! { <MyApp/> }, -/// Method::Get, -/// ), -/// ) -/// }) -/// .bind(&addr)? -/// .run() -/// .await -/// } -/// # } -/// ``` -/// -/// ## Provided Context Types -/// This function always provides context values including the following types: -/// - [ResponseOptions] -/// - [HttpRequest](actix_web::HttpRequest) -/// - [MetaContext](leptos_meta::MetaContext) -/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext) -#[tracing::instrument(level = "trace", fields(error), skip_all)] -pub fn render_app_to_stream_in_order<IV>( - options: LeptosOptions, - app_fn: impl Fn() -> IV + Clone + 'static, - method: Method, -) -> Route -where - IV: IntoView, -{ - render_app_to_stream_in_order_with_context(options, || {}, app_fn, method) -} - -/// Returns an Actix [struct@Route](actix_web::Route) that listens for a `GET` request and tries -/// to route it using [leptos_router], asynchronously rendering an HTML page after all -/// `async` [Resource](leptos::Resource)s have loaded. -/// -/// The provides a [MetaContext] and a [RouterIntegrationContext] to the app’s context before -/// rendering it, and includes any meta tags injected using [leptos_meta]. -/// -/// The HTML stream is rendered using [render_to_string_async](leptos::ssr::render_to_string_async), and -/// includes everything described in the documentation for that function. -/// -/// This can then be set up at an appropriate route in your application: -/// ``` -/// use actix_web::{App, HttpServer}; -/// use leptos::*; -/// use leptos_router::Method; -/// use std::{env, net::SocketAddr}; -/// -/// #[component] -/// fn MyApp() -> impl IntoView { -/// view! { <main>"Hello, world!"</main> } -/// } -/// -/// # if false { // don't actually try to run a server in a doctest... -/// #[actix_web::main] -/// async fn main() -> std::io::Result<()> { -/// let conf = get_configuration(Some("Cargo.toml")).await.unwrap(); -/// let addr = conf.leptos_options.site_addr.clone(); -/// HttpServer::new(move || { -/// let leptos_options = &conf.leptos_options; -/// -/// App::new() -/// // {tail:.*} passes the remainder of the URL as the route -/// // the actual routing will be handled by `leptos_router` -/// .route( -/// "/{tail:.*}", -/// leptos_actix::render_app_async( -/// leptos_options.to_owned(), -/// || view! { <MyApp/> }, -/// Method::Get, -/// ), -/// ) -/// }) -/// .bind(&addr)? -/// .run() -/// .await -/// } -/// # } -/// ``` -/// -/// ## Provided Context Types -/// This function always provides context values including the following types: -/// - [ResponseOptions] -/// - [HttpRequest](actix_web::HttpRequest) -/// - [MetaContext](leptos_meta::MetaContext) -/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext) -#[tracing::instrument(level = "trace", fields(error), skip_all)] -pub fn render_app_async<IV>( - options: LeptosOptions, - app_fn: impl Fn() -> IV + Clone + 'static, - method: Method, -) -> Route -where - IV: IntoView, -{ - render_app_async_with_context(options, || {}, app_fn, method) -} - -/// Returns an Actix [struct@Route] that listens for a `GET` request and tries -/// to route it using [leptos_router], serving an HTML stream of your application. -/// -/// This function allows you to provide additional information to Leptos for your route. -/// It could be used to pass in Path Info, Connection Info, or anything your heart desires. -/// -/// ## Provided Context Types -/// This function always provides context values including the following types: -/// - [ResponseOptions] -/// - [HttpRequest](actix_web::HttpRequest) -/// - [MetaContext](leptos_meta::MetaContext) -/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext) -#[tracing::instrument(level = "trace", fields(error), skip_all)] -pub fn render_app_to_stream_with_context<IV>( - options: LeptosOptions, - additional_context: impl Fn() + 'static + Clone + Send, - app_fn: impl Fn() -> IV + Clone + 'static, - method: Method, -) -> Route -where - IV: IntoView, -{ - render_app_to_stream_with_context_and_replace_blocks( - options, - additional_context, - app_fn, - method, - false, - ) -} - -/// Returns an Actix [struct@Route](actix_web::Route) that listens for a `GET` request and tries -/// to route it using [leptos_router], serving an HTML stream of your application. -/// -/// This function allows you to provide additional information to Leptos for your route. -/// It could be used to pass in Path Info, Connection Info, or anything your heart desires. -/// -/// `replace_blocks` additionally lets you specify whether `<Suspense/>` fragments that read -/// from blocking resources should be retrojected into the HTML that's initially served, rather -/// than dynamically inserting them with JavaScript on the client. This means you will have -/// better support if JavaScript is not enabled, in exchange for a marginally slower response time. -/// -/// ## Provided Context Types -/// This function always provides context values including the following types: -/// - [ResponseOptions] -/// - [HttpRequest](actix_web::HttpRequest) -/// - [MetaContext](leptos_meta::MetaContext) -/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext) -#[tracing::instrument(level = "trace", fields(error), skip_all)] -pub fn render_app_to_stream_with_context_and_replace_blocks<IV>( - options: LeptosOptions, - additional_context: impl Fn() + 'static + Clone + Send, - app_fn: impl Fn() -> IV + Clone + 'static, - method: Method, - replace_blocks: bool, -) -> Route -where - IV: IntoView, -{ - let handler = move |req: HttpRequest| { - let options = options.clone(); - let app_fn = app_fn.clone(); - let additional_context = additional_context.clone(); - let res_options = ResponseOptions::default(); - - async move { - let app = { - let app_fn = app_fn.clone(); - let res_options = res_options.clone(); - move || { - provide_contexts(&req, res_options); - (app_fn)().into_view() - } - }; - - stream_app( - &options, - app, - res_options, - additional_context, - replace_blocks, - ) - .await - } - }; - match method { - Method::Get => web::get().to(handler), - Method::Post => web::post().to(handler), - Method::Put => web::put().to(handler), - Method::Delete => web::delete().to(handler), - Method::Patch => web::patch().to(handler), - } -} - -/// Returns an Actix [struct@Route](actix_web::Route) that listens for a `GET` request and tries -/// to route it using [leptos_router], serving an in-order HTML stream of your application. -/// -/// This function allows you to provide additional information to Leptos for your route. -/// It could be used to pass in Path Info, Connection Info, or anything your heart desires. -/// -/// ## Provided Context Types -/// This function always provides context values including the following types: -/// - [ResponseOptions] -/// - [HttpRequest](actix_web::HttpRequest) -/// - [MetaContext](leptos_meta::MetaContext) -/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext) -#[tracing::instrument(level = "trace", fields(error), skip_all)] -pub fn render_app_to_stream_in_order_with_context<IV>( - options: LeptosOptions, - additional_context: impl Fn() + 'static + Clone + Send, - app_fn: impl Fn() -> IV + Clone + 'static, - method: Method, -) -> Route -where - IV: IntoView, -{ - let handler = move |req: HttpRequest| { - let options = options.clone(); - let app_fn = app_fn.clone(); - let additional_context = additional_context.clone(); - let res_options = ResponseOptions::default(); - - async move { - let app = { - let app_fn = app_fn.clone(); - let res_options = res_options.clone(); - move || { - provide_contexts(&req, res_options); - (app_fn)().into_view() - } - }; - - stream_app_in_order(&options, app, res_options, additional_context) - .await - } - }; - match method { - Method::Get => web::get().to(handler), - Method::Post => web::post().to(handler), - Method::Put => web::put().to(handler), - Method::Delete => web::delete().to(handler), - Method::Patch => web::patch().to(handler), - } -} - -/// Returns an Actix [struct@Route](actix_web::Route) that listens for a `GET` request and tries -/// to route it using [leptos_router], asynchronously serving the page once all `async` -/// [Resource](leptos::Resource)s have loaded. -/// -/// This function allows you to provide additional information to Leptos for your route. -/// It could be used to pass in Path Info, Connection Info, or anything your heart desires. -/// -/// ## Provided Context Types -/// This function always provides context values including the following types: -/// - [ResponseOptions] -/// - [HttpRequest](actix_web::HttpRequest) -/// - [MetaContext](leptos_meta::MetaContext) -/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext) -#[tracing::instrument(level = "trace", fields(error), skip_all)] -pub fn render_app_async_with_context<IV>( - options: LeptosOptions, - additional_context: impl Fn() + 'static + Clone + Send, - app_fn: impl Fn() -> IV + Clone + 'static, - method: Method, -) -> Route -where - IV: IntoView, -{ - let handler = move |req: HttpRequest| { - let options = options.clone(); - let app_fn = app_fn.clone(); - let additional_context = additional_context.clone(); - let res_options = ResponseOptions::default(); - - async move { - let app = { - let app_fn = app_fn.clone(); - let res_options = res_options.clone(); - move || { - provide_contexts(&req, res_options); - (app_fn)().into_view() - } - }; - - render_app_async_helper( - &options, - app, - res_options, - additional_context, - ) - .await - } - }; - match method { - Method::Get => web::get().to(handler), - Method::Post => web::post().to(handler), - Method::Put => web::put().to(handler), - Method::Delete => web::delete().to(handler), - Method::Patch => web::patch().to(handler), - } -} - -#[tracing::instrument(level = "trace", fields(error), skip_all)] -fn provide_contexts(req: &HttpRequest, res_options: ResponseOptions) { - let path = leptos_corrected_path(req); - - let integration = ServerIntegration { path }; - provide_context(RouterIntegrationContext::new(integration)); - provide_context(MetaContext::new()); - provide_context(res_options); - provide_context(req.clone()); - provide_server_redirect(redirect); - #[cfg(feature = "nonce")] - leptos::nonce::provide_nonce(); -} - -fn leptos_corrected_path(req: &HttpRequest) -> String { - let path = req.path(); - let query = req.query_string(); - if query.is_empty() { - "http://leptos".to_string() + path - } else { - "http://leptos".to_string() + path + "?" + query - } -} -#[tracing::instrument(level = "trace", fields(error), skip_all)] -async fn stream_app( - options: &LeptosOptions, - app: impl FnOnce() -> View + 'static, - res_options: ResponseOptions, - additional_context: impl Fn() + 'static + Clone + Send, - replace_blocks: bool, -) -> Response { - let (stream, runtime) = - render_to_stream_with_prefix_undisposed_with_context_and_block_replacement( - app, - move || generate_head_metadata_separated().1.into(), - additional_context, - replace_blocks - ); - - build_stream_response(options, res_options, stream, runtime).await -} -#[cfg_attr( - any(debug_assertions, feature = "ssr"), - instrument(level = "trace", skip_all,) -)] -async fn stream_app_in_order( - options: &LeptosOptions, - app: impl FnOnce() -> View + 'static, - res_options: ResponseOptions, - additional_context: impl Fn() + 'static + Clone + Send, -) -> Response { - let (stream, runtime) = - leptos::ssr::render_to_stream_in_order_with_prefix_undisposed_with_context( - app, - move || { - generate_head_metadata_separated().1.into() - }, - additional_context, - ); - - build_stream_response(options, res_options, stream, runtime).await -} -#[tracing::instrument(level = "trace", fields(error), skip_all)] -async fn build_stream_response( - options: &LeptosOptions, - res_options: ResponseOptions, - stream: impl Stream<Item = String> + 'static, - runtime: RuntimeId, -) -> Response { - let mut stream = Box::pin(stream); - - // wait for any blocking resources to load before pulling metadata - let first_app_chunk = stream.next().await.unwrap_or_default(); - - let (head, tail) = - html_parts_separated(options, use_context::<MetaContext>().as_ref()); - - let mut stream = Box::pin( - futures::stream::once(async move { head.clone() }) - .chain( - futures::stream::once(async move { first_app_chunk }) - .chain(stream), - ) - .map(|html| Ok(web::Bytes::from(html)) as Result<web::Bytes>), - ); - - // Get the first and second in the stream, which renders the app shell, and thus allows Resources to run - - let first_chunk = stream.next().await; - - let second_chunk = stream.next().await; - - let res_options = res_options.0.read(); - - let (status, headers) = (res_options.status, res_options.headers.clone()); - let status = status.unwrap_or_default(); - - let complete_stream = - futures::stream::iter([first_chunk.unwrap(), second_chunk.unwrap()]) - .chain(stream) - .chain( - futures::stream::once(async move { - runtime.dispose(); - tail.to_string() - }) - .map(|html| Ok(web::Bytes::from(html)) as Result<web::Bytes>), - ); - let mut res = Response::ok() - .content_type("text/html") - .streaming(complete_stream); - - // Add headers manipulated in the response - for (key, value) in headers.into_iter() { - res.append_header(key, value); - } - - // Set status to what is returned in the function - res.set_status(status); - // Return the response - res -} -#[tracing::instrument(level = "trace", fields(error), skip_all)] -async fn render_app_async_helper( - options: &LeptosOptions, - app: impl FnOnce() -> View + 'static, - res_options: ResponseOptions, - additional_context: impl Fn() + 'static + Clone + Send, -) -> Response { - let (stream, runtime) = - leptos::ssr::render_to_stream_in_order_with_prefix_undisposed_with_context( - app, - move || "".into(), - additional_context, - ); - - let html = build_async_response(stream, options, runtime).await; - - let res_options = res_options.0.read(); - - let (status, headers) = (res_options.status, res_options.headers.clone()); - let status = status.unwrap_or_default(); - - let mut res = Response::ok().set_typed_body(html); - - // Add headers manipulated in the response - for (key, value) in headers.into_iter() { - res.headers_mut().append(key, value); - } - - // Set status to what is returned in the function - res.set_status(status); - // Return the response - res -} - -/// Generates a list of all routes defined in Leptos's Router in your app. We can then use this to automatically -/// create routes in Actix's App without having to use wildcard matching or fallbacks. Takes in your root app Element -/// as an argument so it can walk you app tree. This version is tailored to generated Actix compatible paths. -pub fn generate_route_list<IV>( - app_fn: impl Fn() -> IV + 'static + Clone, -) -> Vec<RouteListing> -where - IV: IntoView + 'static, -{ - generate_route_list_with_exclusions_and_ssg(app_fn, None).0 -} - -/// Generates a list of all routes defined in Leptos's Router in your app. We can then use this to automatically -/// create routes in Actix's App without having to use wildcard matching or fallbacks. Takes in your root app Element -/// as an argument so it can walk you app tree. This version is tailored to generated Actix compatible paths. -pub fn generate_route_list_with_ssg<IV>( - app_fn: impl Fn() -> IV + 'static + Clone, -) -> (Vec<RouteListing>, StaticDataMap) -where - IV: IntoView + 'static, -{ - generate_route_list_with_exclusions_and_ssg(app_fn, None) -} - -/// Generates a list of all routes defined in Leptos's Router in your app. We can then use this to automatically -/// create routes in Actix's App without having to use wildcard matching or fallbacks. Takes in your root app Element -/// as an argument so it can walk you app tree. This version is tailored to generated Actix compatible paths. Adding excluded_routes -/// to this function will stop `.leptos_routes()` from generating a route for it, allowing a custom handler. These need to be in Actix path format -pub fn generate_route_list_with_exclusions<IV>( - app_fn: impl Fn() -> IV + 'static + Clone, - excluded_routes: Option<Vec<String>>, -) -> Vec<RouteListing> -where - IV: IntoView + 'static, -{ - generate_route_list_with_exclusions_and_ssg(app_fn, excluded_routes).0 -} - -/// Generates a list of all routes defined in Leptos's Router in your app. We can then use this to automatically -/// create routes in Actix's App without having to use wildcard matching or fallbacks. Takes in your root app Element -/// as an argument so it can walk you app tree. This version is tailored to generated Actix compatible paths. Adding excluded_routes -/// to this function will stop `.leptos_routes()` from generating a route for it, allowing a custom handler. These need to be in Actix path format -pub fn generate_route_list_with_exclusions_and_ssg<IV>( - app_fn: impl Fn() -> IV + 'static + Clone, - excluded_routes: Option<Vec<String>>, -) -> (Vec<RouteListing>, StaticDataMap) -where - IV: IntoView + 'static, -{ - generate_route_list_with_exclusions_and_ssg_and_context( - app_fn, - excluded_routes, - || {}, - ) -} - -/// Generates a list of all routes defined in Leptos's Router in your app. We can then use this to automatically -/// create routes in Actix's App without having to use wildcard matching or fallbacks. Takes in your root app Element -/// as an argument so it can walk you app tree. This version is tailored to generated Actix compatible paths. Adding excluded_routes -/// to this function will stop `.leptos_routes()` from generating a route for it, allowing a custom handler. These need to be in Actix path format. -/// Additional context will be provided to the app Element. -pub fn generate_route_list_with_exclusions_and_ssg_and_context<IV>( - app_fn: impl Fn() -> IV + 'static + Clone, - excluded_routes: Option<Vec<String>>, - additional_context: impl Fn() + 'static + Clone, -) -> (Vec<RouteListing>, StaticDataMap) -where - IV: IntoView + 'static, -{ - let (mut routes, static_data_map) = - leptos_router::generate_route_list_inner_with_context( - app_fn, - additional_context, - ); - - // Actix's Router doesn't follow Leptos's - // Match `*` or `*someword` to replace with replace it with "/{tail.*} - let wildcard_re = Regex::new(r"\*.*").unwrap(); - // Match `:some_word` but only capture `some_word` in the groups to replace with `{some_word}` - let capture_re = Regex::new(r":((?:[^.,/]+)+)[^/]?").unwrap(); - - // Empty strings screw with Actix pathing, they need to be "/" - routes = routes - .into_iter() - .map(|listing| { - let path = listing.path(); - if path.is_empty() { - return RouteListing::new( - "/".to_string(), - listing.path(), - listing.mode(), - listing.methods(), - listing.static_mode(), - ); - } - RouteListing::new( - listing.path(), - listing.path(), - listing.mode(), - listing.methods(), - listing.static_mode(), - ) - }) - .map(|listing| { - let path = wildcard_re - .replace_all(listing.path(), "{tail:.*}") - .to_string(); - let path = capture_re.replace_all(&path, "{$1}").to_string(); - RouteListing::new( - path, - listing.path(), - listing.mode(), - listing.methods(), - listing.static_mode(), - ) - }) - .collect::<Vec<_>>(); - - ( - if routes.is_empty() { - vec![RouteListing::new( - "/", - "", - Default::default(), - [Method::Get], - None, - )] - } else { - // Routes to exclude from auto generation - if let Some(excluded_routes) = excluded_routes { - routes - .retain(|p| !excluded_routes.iter().any(|e| e == p.path())) - } - routes - }, - static_data_map, - ) -} - -//pub enum DataResponse<T> { -// Data(T), -// Response(Response), -//} - -//fn handle_static_response<'a, IV>( -// path: &'a str, -// options: &'a LeptosOptions, -// app_fn: &'a (impl Fn() -> IV + Clone + Send + 'static), -// additional_context: &'a (impl Fn() + 'static + Clone + Send), -// res: StaticResponse, -//) -> Pin<Box<dyn Future<Output = Response> + 'a>> -//where -// IV: IntoView + 'static, -//{ -// Box::pin(async move { -// match res { -// StaticResponse::ReturnResponse { -// body, -// status, -// content_type, -// } => { -// let mut res = Response::new(match status { -// StaticStatusCode::Ok => StatusCode::OK, -// StaticStatusCode::NotFound => StatusCode::NOT_FOUND, -// StaticStatusCode::InternalServerError => { -// StatusCode::INTERNAL_SERVER_ERROR -// } -// }); -// if let Some(v) = content_type { -// res.headers_mut().insert( -// HeaderName::from_static("content-type"), -// HeaderValue::from_static(v), -// ); -// } -// res.set_body(body) -// } -// StaticResponse::RenderDynamic => { -// handle_static_response( -// path, -// options, -// app_fn, -// additional_context, -// render_dynamic( -// path, -// options, -// app_fn.clone(), -// additional_context.clone(), -// ) -// .await, -// ) -// .await -// } -// StaticResponse::RenderNotFound => { -// handle_static_response( -// path, -// options, -// app_fn, -// additional_context, -// not_found_page( -// tokio::fs::read_to_string(not_found_path(options)) -// .await, -// ), -// ) -// .await -// } -// StaticResponse::WriteFile { body, path } => { -// if let Some(path) = path.parent() { -// if let Err(e) = std::fs::create_dir_all(path) { -// tracing::error!( -// "encountered error {} writing directories {}", -// e, -// path.display() -// ); -// } -// } -// if let Err(e) = std::fs::write(&path, &body) { -// tracing::error!( -// "encountered error {} writing file {}", -// e, -// path.display() -// ); -// } -// handle_static_response( -// path.to_str().unwrap(), -// options, -// app_fn, -// additional_context, -// StaticResponse::ReturnResponse { -// body, -// status: StaticStatusCode::Ok, -// content_type: Some("text/html"), -// }, -// ) -// .await -// } -// } -// }) -//} -// -//fn static_route<IV>( -// options: LeptosOptions, -// app_fn: impl Fn() -> IV + Clone + Send + 'static, -// additional_context: impl Fn() + 'static + Clone + Send, -// method: Method, -// mode: StaticMode, -//) -> Route -//where -// IV: IntoView + 'static, -//{ -// match mode { -// StaticMode::Incremental => { -// let handler = move |req: HttpRequest| { -// Box::pin({ -// let options = options.clone(); -// let app_fn = app_fn.clone(); -// let additional_context = additional_context.clone(); -// async move { -// handle_static_response( -// req.path(), -// &options, -// &app_fn, -// &additional_context, -// incremental_static_route( -// tokio::fs::read_to_string(static_file_path( -// &options, -// req.path(), -// )) -// .await, -// ), -// ) -// .await -// } -// }) -// }; -// match method { -// Method::Get => web::get().to(handler), -// Method::Post => web::post().to(handler), -// Method::Put => web::put().to(handler), -// Method::Delete => web::delete().to(handler), -// Method::Patch => web::patch().to(handler), -// } -// } -// StaticMode::Upfront => { -// let handler = move |req: HttpRequest| { -// Box::pin({ -// let options = options.clone(); -// let app_fn = app_fn.clone(); -// let additional_context = additional_context.clone(); -// async move { -// handle_static_response( -// req.path(), -// &options, -// &app_fn, -// &additional_context, -// upfront_static_route( -// tokio::fs::read_to_string(static_file_path( -// &options, -// req.path(), -// )) -// .await, -// ), -// ) -// .await -// } -// }) -// }; -// match method { -// Method::Get => web::get().to(handler), -// Method::Post => web::post().to(handler), -// Method::Put => web::put().to(handler), -// Method::Delete => web::delete().to(handler), -// Method::Patch => web::patch().to(handler), -// } -// } -// } -//} - -/// This trait allows one to pass a list of routes and a render function to Actix's router, letting us avoid -/// having to use wildcards or manually define all routes in multiple places. -pub trait LeptosRoutes { - fn leptos_routes<IV>( - self, - options: LeptosOptions, - paths: Vec<RouteListing>, - app_fn: impl Fn() -> IV + Clone + Send + 'static, - ) -> Self - where - IV: IntoView + 'static; - - fn leptos_routes_with_context<IV>( - self, - options: LeptosOptions, - paths: Vec<RouteListing>, - additional_context: impl Fn() + 'static + Clone + Send, - app_fn: impl Fn() -> IV + Clone + Send + 'static, - ) -> Self - where - IV: IntoView + 'static; -} - -/// The default implementation of `LeptosRoutes` which takes in a list of paths, and dispatches GET requests -/// to those paths to Leptos's renderer. -impl<T> LeptosRoutes for actix_web::App<T> -where - T: ServiceFactory< - ServiceRequest, - Config = (), - Error = Error, - InitError = (), - >, -{ - #[tracing::instrument(level = "trace", fields(error), skip_all)] - fn leptos_routes<IV>( - self, - options: LeptosOptions, - paths: Vec<RouteListing>, - app_fn: impl Fn() -> IV + Clone + Send + 'static, - ) -> Self - where - IV: IntoView + 'static, - { - self.leptos_routes_with_context(options, paths, || {}, app_fn) - } - - #[tracing::instrument(level = "trace", fields(error), skip_all)] - fn leptos_routes_with_context<IV>( - self, - options: LeptosOptions, - paths: Vec<RouteListing>, - additional_context: impl Fn() + 'static + Clone + Send, - app_fn: impl Fn() -> IV + Clone + Send + 'static, - ) -> Self - where - IV: IntoView + 'static, - { - let mut router = self; - for listing in paths.iter() { - let path = listing.path(); - let mode = listing.mode(); - - for method in listing.methods() { - router = if let Some(static_mode) = listing.static_mode() { - router.route( - path, - static_route( - options.clone(), - app_fn.clone(), - additional_context.clone(), - method, - static_mode, - ), - ) - } else { - router.route( - path, - match mode { - SsrMode::OutOfOrder => { - render_app_to_stream_with_context( - options.clone(), - additional_context.clone(), - app_fn.clone(), - method, - ) - } - SsrMode::PartiallyBlocked => { - render_app_to_stream_with_context_and_replace_blocks( - options.clone(), - additional_context.clone(), - app_fn.clone(), - method, - true, - ) - } - SsrMode::InOrder => { - render_app_to_stream_in_order_with_context( - options.clone(), - additional_context.clone(), - app_fn.clone(), - method, - ) - } - SsrMode::Async => render_app_async_with_context( - options.clone(), - additional_context.clone(), - app_fn.clone(), - method, - ), - }, - ) - }; - } - } - router - } -} - -/// The default implementation of `LeptosRoutes` which takes in a list of paths, and dispatches GET requests -/// to those paths to Leptos's renderer. -impl LeptosRoutes for &mut ServiceConfig { - #[tracing::instrument(level = "trace", fields(error), skip_all)] - fn leptos_routes<IV>( - self, - options: LeptosOptions, - paths: Vec<RouteListing>, - app_fn: impl Fn() -> IV + Clone + Send + 'static, - ) -> Self - where - IV: IntoView + 'static, - { - self.leptos_routes_with_context(options, paths, || {}, app_fn) - } - - #[tracing::instrument(level = "trace", fields(error), skip_all)] - fn leptos_routes_with_context<IV>( - self, - options: LeptosOptions, - paths: Vec<RouteListing>, - additional_context: impl Fn() + 'static + Clone + Send, - app_fn: impl Fn() -> IV + Clone + Send + 'static, - ) -> Self - where - IV: IntoView + 'static, - { - let mut router = self; - for listing in paths.iter() { - let path = listing.path(); - let mode = listing.mode(); - - for method in listing.methods() { - router = router.route( - path, - match mode { - SsrMode::OutOfOrder => { - render_app_to_stream_with_context( - options.clone(), - additional_context.clone(), - app_fn.clone(), - method, - ) - } - SsrMode::PartiallyBlocked => { - render_app_to_stream_with_context_and_replace_blocks( - options.clone(), - additional_context.clone(), - app_fn.clone(), - method, - true, - ) - } - SsrMode::InOrder => { - render_app_to_stream_in_order_with_context( - options.clone(), - additional_context.clone(), - app_fn.clone(), - method, - ) - } - SsrMode::Async => render_app_async_with_context( - options.clone(), - additional_context.clone(), - app_fn.clone(), - method, - ), - }, - ); - } - } - router - } -} - -/// A helper to make it easier to use Actix extractors in server functions. This takes -/// a handler function as its argument. The handler follows similar rules to an Actix -/// [Handler]: it is an async function that receives arguments that -/// will be extracted from the request and returns some value. -/// -/// ```rust,ignore -/// use leptos::*; -/// use serde::Deserialize; -/// #[derive(Deserialize)] -/// struct Search { -/// q: String, -/// } -/// -/// #[server(ExtractoServerFn, "/api")] -/// pub async fn extractor_server_fn() -> Result<String, ServerFnError> { -/// use actix_web::dev::ConnectionInfo; -/// use actix_web::web::{Data, Query}; -/// -/// extract( -/// |data: Data<String>, search: Query<Search>, connection: ConnectionInfo| async move { -/// format!( -/// "data = {}\nsearch = {}\nconnection = {:?}", -/// data.into_inner(), -/// search.q, -/// connection -/// ) -/// }, -/// ) -/// .await -/// } -/// ``` -//pub async fn extract<F, E>( -// f: F, -//) -> Result<<<F as Extractor<E>>::Future as Future>::Output, ServerFnError> -//where -// F: Extractor<E>, -// E: actix_web::FromRequest, -// <E as actix_web::FromRequest>::Error: Display, -// <F as Extractor<E>>::Future: Future, -//{ -// let req = use_context::<actix_web::HttpRequest>() -// .expect("HttpRequest should have been provided via context"); -// -// let input = if let Some(body) = use_context::<Bytes>() { -// let (_, mut payload) = actix_http::h1::Payload::create(false); -// payload.unread_data(body); -// E::from_request(&req, &mut dev::Payload::from(payload)) -// } else { -// E::extract(&req) -// } -// .await -// .map_err(|e| ServerFnError::ServerError(e.to_string()))?; -// -// Ok(f.call(input).await) -//} -// -///// A helper to make it easier to use Axum extractors in server functions, with a -///// simpler API than [`extract()`]. -///// -///// It is generic over some type `T` that implements [`FromRequest`] and can -///// therefore be used in an extractor. The compiler can often infer this type. -///// -///// Any error that occurs during extraction is converted to a [`ServerFnError`]. -///// -///// ```rust,ignore -///// // MyQuery is some type that implements `Deserialize + Serialize` -///// #[server] -///// pub async fn query_extract() -> Result<MyQuery, ServerFnError> { -///// use actix_web::web::Query; -///// use leptos_actix::*; -///// let Query(data) = extractor().await?; -///// Ok(data) -///// } -///// ``` -//pub async fn extractor<T>() -> Result<T, ServerFnError> -//where -// T: actix_web::FromRequest, -// <T as FromRequest>::Error: Debug, -//{ -// let req = use_context::<actix_web::HttpRequest>() -// .expect("HttpRequest should have been provided via context"); -// -// if let Some(body) = use_context::<Bytes>() { -// let (_, mut payload) = actix_http::h1::Payload::create(false); -// payload.unread_data(body); -// T::from_request(&req, &mut dev::Payload::from(payload)) -// } else { -// T::extract(&req) -// } -// .await -// .map_err(|e| ServerFnError::ServerError(format!("{e:?}"))) -//} -// -///// A macro that makes it easier to use extractors in server functions. The macro -///// takes a type or types, and extracts them from the request, returning from the -///// server function with an `Err(_)` if there is an error during extraction. -///// ```rust,ignore -///// let info = extract!(ConnectionInfo); -///// let Query(data) = extract!(Query<Search>); -///// let (info, Query(data)) = extract!(ConnectionInfo, Query<Search>); -///// ``` -//#[macro_export] -//macro_rules! extract { -// ($($x:ty),+) => { -// $crate::extract(|fields: ($($x),+)| async move { fields }).await? -// }; -//} -// -//// Drawn from the Actix Handler implementation -//// https://github.com/actix/actix-web/blob/19c9d858f25e8262e14546f430d713addb397e96/actix-web/src/handler.rs#L124 -//pub trait Extractor<T> { -// type Future; -// -// fn call(self, args: T) -> Self::Future; -//} -// -//macro_rules! factory_tuple ({ $($param:ident)* } => { -// impl<Func, Fut, $($param,)*> Extractor<($($param,)*)> for Func -// where -// Func: FnOnce($($param),*) -> Fut + Clone + 'static, -// Fut: Future, -// { -// type Future = Fut; -// -// #[inline] -// #[allow(non_snake_case)] -// fn call(self, ($($param,)*): ($($param,)*)) -> Self::Future { -// (self)($($param,)*) -// } -// } -//}); -// -//factory_tuple! {} -//factory_tuple! { A } -//factory_tuple! { A B } -//factory_tuple! { A B C } -//factory_tuple! { A B C D } -//factory_tuple! { A B C D E } -//factory_tuple! { A B C D E F } -//factory_tuple! { A B C D E F G } -//factory_tuple! { A B C D E F G H } -//factory_tuple! { A B C D E F G H I } -//factory_tuple! { A B C D E F G H I J } -//factory_tuple! { A B C D E F G H I J K } -//factory_tuple! { A B C D E F G H I J K L } -//factory_tuple! { A B C D E F G H I J K L M } -//factory_tuple! { A B C D E F G H I J K L M N } -//factory_tuple! { A B C D E F G H I J K L M N O } -//factory_tuple! { A B C D E F G H I J K L M N O P } From 25120c0e9fe527357b255c257149fc3ed61955ed Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Fri, 19 Jan 2024 14:13:47 -0500 Subject: [PATCH 083/100] fix streaming requests and clarify in docs --- examples/server_fns_axum/src/app.rs | 45 --------------------------- server_fn/src/codec/stream.rs | 34 ++++++++++++++++++--- server_fn/src/request/browser.rs | 47 ++++++++++++++++++++--------- 3 files changed, 63 insertions(+), 63 deletions(-) diff --git a/examples/server_fns_axum/src/app.rs b/examples/server_fns_axum/src/app.rs index acfdcdfb14..e6a5b5e5ef 100644 --- a/examples/server_fns_axum/src/app.rs +++ b/examples/server_fns_axum/src/app.rs @@ -50,7 +50,6 @@ pub fn HomePage() -> impl IntoView { <RkyvExample/> <FileUpload/> <FileWatcher/> - <StreamingValues/> } } @@ -507,47 +506,3 @@ pub fn CustomErrorTypes() -> impl IntoView { </p> } } - -#[component] -pub fn StreamingValues() -> impl IntoView { - use futures::StreamExt; - - /// You can create server functions that accept streaming values by using the encoding - /// `Streaming` (with type `ByteStream`) or encoding `StreamingText` (with type `TextStream`) - #[server(input = StreamingText, output = StreamingText)] - pub async fn streaming(input: TextStream) -> Result<TextStream, ServerFnError> { - println!("inside streaming() fn"); - Ok(TextStream::from(input.into_inner().map(|text| format!("{}!!!", text.unwrap_or_else(|e| e.to_string()))))) - } - - let mut count = 0; - let (tx, rx) = futures::channel::mpsc::unbounded(); - let (result, set_result) = create_signal("Click me...".to_string()); - - - if cfg!(feature = "hydrate") { - spawn_local(async move { - logging::log!("calling streaming server fn"); - match streaming(TextStream::new(rx)).await { - Ok(res) => { - logging::log!("after calling streaming()"); - let mut stream = res.into_inner(); - while let Some(chunk) = stream.next().await { - set_result(chunk.unwrap_or_else(|e| e.to_string())); - } - }, Err(e) => logging::log!("{e}") } - }) - } - - view! { - <h3>Streaming arguments and responses</h3> - <button - on:click=move |_| { - count += 1; - tx.unbounded_send(Ok(count.to_string())).expect("couldn't send into channel"); - } - > - {result} - </button> - } -} diff --git a/server_fn/src/codec/stream.rs b/server_fn/src/codec/stream.rs index 38173f6074..15e2765363 100644 --- a/server_fn/src/codec/stream.rs +++ b/server_fn/src/codec/stream.rs @@ -13,6 +13,15 @@ use std::{fmt::Debug, pin::Pin}; /// An encoding that represents a stream of bytes. /// /// A server function that uses this as its output encoding should return [`ByteStream`]. +/// +/// ## Browser Support for Streaming Input +/// +/// Browser fetch requests do not currently support full request duplexing, which +/// means that that they do begin handling responses until the full request has been sent. +/// This means that if you use a streaming input encoding, the input stream needs to +/// end before the output will begin. +/// +/// Streaming requests are only allowed over HTTP2 or HTTP3. pub struct Streaming; impl Encoding for Streaming { @@ -49,6 +58,15 @@ where /// A stream of bytes. /// /// A server function can return this type if its output encoding is [`Streaming`]. +/// +/// ## Browser Support for Streaming Input +/// +/// Browser fetch requests do not currently support full request duplexing, which +/// means that that they do begin handling responses until the full request has been sent. +/// This means that if you use a streaming input encoding, the input stream needs to +/// end before the output will begin. +/// +/// Streaming requests are only allowed over HTTP2 or HTTP3. pub struct ByteStream<CustErr = NoCustomError>( Pin<Box<dyn Stream<Item = Result<Bytes, ServerFnError<CustErr>>> + Send>>, ); @@ -115,10 +133,14 @@ where /// /// A server function that uses this as its output encoding should return [`TextStream`]. /// -/// **Note**: Browser fetch requests do not currently support full request duplexing, which +/// ## Browser Support for Streaming Input +/// +/// Browser fetch requests do not currently support full request duplexing, which /// means that that they do begin handling responses until the full request has been sent. -/// This means that if you use streaming text as an input encoding, the input stream needs to +/// This means that if you use a streaming input encoding, the input stream needs to /// end before the output will begin. +/// +/// Streaming requests are only allowed over HTTP2 or HTTP3. pub struct StreamingText; impl Encoding for StreamingText { @@ -130,10 +152,14 @@ impl Encoding for StreamingText { /// /// A server function can return this type if its output encoding is [`StreamingText`]. /// -/// **Note**: Browser fetch requests do not currently support full request duplexing, which +/// ## Browser Support for Streaming Input +/// +/// Browser fetch requests do not currently support full request duplexing, which /// means that that they do begin handling responses until the full request has been sent. -/// This means that if you use streaming text as an input encoding, the input stream needs to +/// This means that if you use a streaming input encoding, the input stream needs to /// end before the output will begin. +/// +/// Streaming requests are only allowed over HTTP2 or HTTP3. pub struct TextStream<CustErr = NoCustomError>( Pin<Box<dyn Stream<Item = Result<String, ServerFnError<CustErr>>> + Send>>, ); diff --git a/server_fn/src/request/browser.rs b/server_fn/src/request/browser.rs index 4e5db475a2..5708ed3d25 100644 --- a/server_fn/src/request/browser.rs +++ b/server_fn/src/request/browser.rs @@ -3,11 +3,11 @@ use crate::{client::get_server_url, error::ServerFnError}; use bytes::Bytes; use futures::{Stream, StreamExt}; pub use gloo_net::http::Request; -use js_sys::Uint8Array; +use js_sys::{Reflect, Uint8Array}; use send_wrapper::SendWrapper; use wasm_bindgen::JsValue; use wasm_streams::ReadableStream; -use web_sys::{FormData, UrlSearchParams}; +use web_sys::{FormData, Headers, RequestInit, UrlSearchParams}; /// A `fetch` request made in the browser. #[derive(Debug)] @@ -143,17 +143,36 @@ impl<CustErr> ClientReq<CustErr> for BrowserRequest { content_type: &str, body: impl Stream<Item = Bytes> + 'static, ) -> Result<Self, ServerFnError<CustErr>> { - let stream = ReadableStream::from_stream(body.map(|bytes| { - let data = Uint8Array::from(bytes.as_ref()); - let data = JsValue::from(data); - Ok(data) as Result<JsValue, JsValue> - })); - Ok(Self(SendWrapper::new( - Request::post(path) - .header("Content-Type", content_type) - .header("Accept", accepts) - .body(stream.into_raw()) - .map_err(|e| ServerFnError::Request(e.to_string()))?, - ))) + let req = streaming_request(path, accepts, content_type, body) + .map_err(|e| ServerFnError::Request(format!("{e:?}")))?; + Ok(Self(SendWrapper::new(req))) } } + +fn streaming_request( + path: &str, + accepts: &str, + content_type: &str, + body: impl Stream<Item = Bytes> + 'static, +) -> Result<Request, JsValue> { + let stream = ReadableStream::from_stream(body.map(|bytes| { + let data = Uint8Array::from(bytes.as_ref()); + let data = JsValue::from(data); + Ok(data) as Result<JsValue, JsValue> + })) + .into_raw(); + let headers = Headers::new()?; + headers.append("Content-Type", content_type)?; + headers.append("Accept", accepts)?; + let mut init = RequestInit::new(); + init.headers(&headers).method("POST").body(Some(&stream)); + + // Chrome requires setting `duplex: "half"` on streaming requests + Reflect::set( + &init, + &JsValue::from_str("duplex"), + &JsValue::from_str("half"), + )?; + let req = web_sys::Request::new_with_str_and_init(path, &init)?; + Ok(Request::from(req)) +} From a519859a66fde764c46b172e46e250a2e747492e Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Fri, 19 Jan 2024 14:37:03 -0500 Subject: [PATCH 084/100] Revert "use `&[u8]` instead of `Bytes` for requests" This reverts commit e179db1d4245cb9aa12fdd55b97f2ca0a8feefbf. --- server_fn/src/codec/cbor.rs | 7 ++++++- server_fn/src/codec/rkyv.rs | 10 +++------- server_fn/src/request/browser.rs | 3 ++- server_fn/src/request/mod.rs | 2 +- server_fn/src/request/reqwest.rs | 4 ++-- 5 files changed, 14 insertions(+), 12 deletions(-) diff --git a/server_fn/src/codec/cbor.rs b/server_fn/src/codec/cbor.rs index 051bc266c6..a5a91c8117 100644 --- a/server_fn/src/codec/cbor.rs +++ b/server_fn/src/codec/cbor.rs @@ -29,7 +29,12 @@ where let mut buffer: Vec<u8> = Vec::new(); ciborium::ser::into_writer(&self, &mut buffer) .map_err(|e| ServerFnError::Serialization(e.to_string()))?; - Request::try_new_post_bytes(path, accepts, Cbor::CONTENT_TYPE, &buffer) + Request::try_new_post_bytes( + path, + accepts, + Cbor::CONTENT_TYPE, + Bytes::from(buffer), + ) } } diff --git a/server_fn/src/codec/rkyv.rs b/server_fn/src/codec/rkyv.rs index ab5ed08ae2..2d9753ad12 100644 --- a/server_fn/src/codec/rkyv.rs +++ b/server_fn/src/codec/rkyv.rs @@ -35,12 +35,8 @@ where ) -> Result<Request, ServerFnError<CustErr>> { let encoded = rkyv::to_bytes::<T, 1024>(&self) .map_err(|e| ServerFnError::Serialization(e.to_string()))?; - Request::try_new_post_bytes( - path, - accepts, - Rkyv::CONTENT_TYPE, - encoded.as_ref(), - ) + let bytes = Bytes::copy_from_slice(encoded.as_ref()); + Request::try_new_post_bytes(path, accepts, Rkyv::CONTENT_TYPE, bytes) } } @@ -54,7 +50,7 @@ where { async fn from_req(req: Request) -> Result<Self, ServerFnError<CustErr>> { let body_bytes = req.try_into_bytes().await?; - rkyv::from_bytes::<T>(&body_bytes) + rkyv::from_bytes::<T>(body_bytes.as_ref()) .map_err(|e| ServerFnError::Args(e.to_string())) } } diff --git a/server_fn/src/request/browser.rs b/server_fn/src/request/browser.rs index 5708ed3d25..820ea77ffb 100644 --- a/server_fn/src/request/browser.rs +++ b/server_fn/src/request/browser.rs @@ -78,12 +78,13 @@ impl<CustErr> ClientReq<CustErr> for BrowserRequest { path: &str, accepts: &str, content_type: &str, - body: &[u8], + body: Bytes, ) -> Result<Self, ServerFnError<CustErr>> { let server_url = get_server_url(); let mut url = String::with_capacity(server_url.len() + path.len()); url.push_str(server_url); url.push_str(path); + let body: &[u8] = &body; let body = Uint8Array::from(body).buffer(); Ok(Self(SendWrapper::new( Request::post(&url) diff --git a/server_fn/src/request/mod.rs b/server_fn/src/request/mod.rs index b9ff889d4e..785df34a53 100644 --- a/server_fn/src/request/mod.rs +++ b/server_fn/src/request/mod.rs @@ -45,7 +45,7 @@ where path: &str, content_type: &str, accepts: &str, - body: &[u8], + body: Bytes, ) -> Result<Self, ServerFnError<CustErr>>; /// Attempts to construct a new `POST` request with form data as the body. diff --git a/server_fn/src/request/reqwest.rs b/server_fn/src/request/reqwest.rs index db20f28734..012985470a 100644 --- a/server_fn/src/request/reqwest.rs +++ b/server_fn/src/request/reqwest.rs @@ -56,14 +56,14 @@ impl<CustErr> ClientReq<CustErr> for Request { path: &str, accepts: &str, content_type: &str, - body: &[u8], + body: Bytes, ) -> Result<Self, ServerFnError<CustErr>> { let url = format!("{}{}", get_server_url(), path); CLIENT .post(url) .header(CONTENT_TYPE, content_type) .header(ACCEPT, accepts) - .body(body.to_owned()) + .body(body) .build() .map_err(|e| ServerFnError::Request(e.to_string())) } From 2a9e50289335b792641d67a7e3ae03932a939a0e Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Fri, 19 Jan 2024 15:03:21 -0500 Subject: [PATCH 085/100] fix rkyv deserialization --- server_fn/src/codec/rkyv.rs | 22 ++++++++++++++++++---- server_fn/src/request/mod.rs | 2 +- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/server_fn/src/codec/rkyv.rs b/server_fn/src/codec/rkyv.rs index 2d9753ad12..2db8f3192e 100644 --- a/server_fn/src/codec/rkyv.rs +++ b/server_fn/src/codec/rkyv.rs @@ -5,11 +5,12 @@ use crate::{ response::{ClientRes, Res}, }; use bytes::Bytes; +use futures::StreamExt; use http::Method; use rkyv::{ de::deserializers::SharedDeserializeMap, ser::serializers::AllocSerializer, - validation::validators::DefaultValidator, Archive, CheckBytes, Deserialize, - Serialize, + validation::validators::DefaultValidator, AlignedVec, Archive, CheckBytes, + Deserialize, Serialize, }; /// Pass arguments and receive responses using `rkyv` in a `POST` request. @@ -49,8 +50,21 @@ where + Deserialize<T, SharedDeserializeMap>, { async fn from_req(req: Request) -> Result<Self, ServerFnError<CustErr>> { - let body_bytes = req.try_into_bytes().await?; - rkyv::from_bytes::<T>(body_bytes.as_ref()) + let mut aligned = AlignedVec::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())) + } + Ok(bytes) => { + for byte in bytes { + aligned.push(byte); + } + } + } + } + rkyv::from_bytes::<T>(aligned.as_ref()) .map_err(|e| ServerFnError::Args(e.to_string())) } } diff --git a/server_fn/src/request/mod.rs b/server_fn/src/request/mod.rs index 785df34a53..2da49ecf38 100644 --- a/server_fn/src/request/mod.rs +++ b/server_fn/src/request/mod.rs @@ -99,7 +99,7 @@ where self, ) -> impl Future<Output = Result<String, ServerFnError<CustErr>>> + Send; - /// Attempts to convert the body of the request into a string. + /// Attempts to convert the body of the request into a stream of bytes. fn try_into_stream( self, ) -> Result< From fdd576535a255562e6138a1aebd6dcaf9515cee6 Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Fri, 19 Jan 2024 15:14:39 -0500 Subject: [PATCH 086/100] clean up examples --- .../action-form-error-handling/src/app.rs | 11 +- .../action-form-error-handling/src/main.rs | 4 +- examples/counter_isomorphic/src/counters.rs | 13 +-- examples/counter_isomorphic/src/main.rs | 102 +++++++++--------- 4 files changed, 67 insertions(+), 63 deletions(-) diff --git a/examples/action-form-error-handling/src/app.rs b/examples/action-form-error-handling/src/app.rs index 6989756fcc..cba24375f3 100644 --- a/examples/action-form-error-handling/src/app.rs +++ b/examples/action-form-error-handling/src/app.rs @@ -28,7 +28,9 @@ pub fn App() -> impl IntoView { } #[server] -async fn do_something(should_error: Option<String>) -> Result<String, ServerFnError> { +async fn do_something( + should_error: Option<String>, +) -> Result<String, ServerFnError> { if should_error.is_none() { Ok(String::from("Successful submit")) } else { @@ -42,7 +44,12 @@ async fn do_something(should_error: Option<String>) -> Result<String, ServerFnEr #[component] fn HomePage() -> impl IntoView { let do_something_action = Action::<DoSomething, _>::server(); - let value = Signal::derive(move || do_something_action.value().get().unwrap_or_else(|| Ok(String::new()))); + let value = Signal::derive(move || { + do_something_action + .value() + .get() + .unwrap_or_else(|| Ok(String::new())) + }); Effect::new_isomorphic(move |_| { logging::log!("Got value = {:?}", value.get()); diff --git a/examples/action-form-error-handling/src/main.rs b/examples/action-form-error-handling/src/main.rs index d9cbb052e8..30f4c743d1 100644 --- a/examples/action-form-error-handling/src/main.rs +++ b/examples/action-form-error-handling/src/main.rs @@ -1,11 +1,11 @@ #[cfg(feature = "ssr")] #[actix_web::main] async fn main() -> std::io::Result<()> { + use action_form_error_handling::app::*; use actix_files::Files; use actix_web::*; use leptos::*; use leptos_actix::{generate_route_list, LeptosRoutes}; - use action_form_error_handling::app::*; let conf = get_configuration(None).await.unwrap(); let addr = conf.leptos_options.site_addr; @@ -43,8 +43,8 @@ pub fn main() { // a client-side main function is required for using `trunk serve` // prefer using `cargo leptos serve` instead // to run: `trunk serve --open --features csr` - use leptos::*; use action_form_error_handling::app::*; + use leptos::*; use wasm_bindgen::prelude::wasm_bindgen; console_error_panic_hook::set_once(); diff --git a/examples/counter_isomorphic/src/counters.rs b/examples/counter_isomorphic/src/counters.rs index 755c83fb5d..b1cbabd68c 100644 --- a/examples/counter_isomorphic/src/counters.rs +++ b/examples/counter_isomorphic/src/counters.rs @@ -118,9 +118,9 @@ pub fn Counters() -> impl IntoView { // This is the typical pattern for a CRUD app #[component] pub fn Counter() -> impl IntoView { - let dec = create_action(|_| adjust_server_count(-1, "decing".into())); - let inc = create_action(|_| adjust_server_count(1, "incing".into())); - let clear = create_action(|_| clear_server_count()); + let dec = create_action(|_: &()| adjust_server_count(-1, "decing".into())); + let inc = create_action(|_: &()| adjust_server_count(1, "incing".into())); + let clear = create_action(|_: &()| clear_server_count()); let counter = create_resource( move || { ( @@ -222,9 +222,10 @@ pub fn FormCounter() -> impl IntoView { #[component] pub fn MultiuserCounter() -> impl IntoView { let dec = - create_action(|_| adjust_server_count(-1, "dec dec goose".into())); - let inc = create_action(|_| adjust_server_count(1, "inc inc moose".into())); - let clear = create_action(|_| clear_server_count()); + create_action(|_: &()| adjust_server_count(-1, "dec dec goose".into())); + let inc = + create_action(|_: &()| adjust_server_count(1, "inc inc moose".into())); + let clear = create_action(|_: &()| clear_server_count()); #[cfg(not(feature = "ssr"))] let multiplayer_value = { diff --git a/examples/counter_isomorphic/src/main.rs b/examples/counter_isomorphic/src/main.rs index 1dd7f6828c..368efbe825 100644 --- a/examples/counter_isomorphic/src/main.rs +++ b/examples/counter_isomorphic/src/main.rs @@ -1,57 +1,53 @@ mod counters; - use leptos::*; - use actix_files::{Files}; - use actix_web::*; - use crate::counters::*; - use leptos_actix::{generate_route_list, LeptosRoutes}; - - #[get("/api/events")] - async fn counter_events() -> impl Responder { - use futures::StreamExt; - - let stream = - futures::stream::once(async { crate::counters::get_server_count().await.unwrap_or(0) }) - .chain(COUNT_CHANNEL.clone()) - .map(|value| { - Ok(web::Bytes::from(format!( - "event: message\ndata: {value}\n\n" - ))) as Result<web::Bytes> - }); - HttpResponse::Ok() - .insert_header(("Content-Type", "text/event-stream")) - .streaming(stream) - } - - #[actix_web::main] - async fn main() -> std::io::Result<()> { - - // Explicit server function registration is no longer required - // on the main branch. On 0.3.0 and earlier, uncomment the lines - // below to register the server functions. - // _ = GetServerCount::register(); - // _ = AdjustServerCount::register(); - // _ = ClearServerCount::register(); - - // Setting this to None means we'll be using cargo-leptos and its env vars. - // when not using cargo-leptos None must be replaced with Some("Cargo.toml") - let conf = get_configuration(None).await.unwrap(); - - let addr = conf.leptos_options.site_addr; - let routes = generate_route_list(Counters); - - HttpServer::new(move || { - let leptos_options = &conf.leptos_options; - let site_root = &leptos_options.site_root; +use crate::counters::*; +use actix_files::Files; +use actix_web::*; +use leptos::*; +use leptos_actix::{generate_route_list, LeptosRoutes}; + +#[get("/api/events")] +async fn counter_events() -> impl Responder { + use futures::StreamExt; + + let stream = futures::stream::once(async { + crate::counters::get_server_count().await.unwrap_or(0) + }) + .chain(COUNT_CHANNEL.clone()) + .map(|value| { + Ok(web::Bytes::from(format!( + "event: message\ndata: {value}\n\n" + ))) as Result<web::Bytes> + }); + HttpResponse::Ok() + .insert_header(("Content-Type", "text/event-stream")) + .streaming(stream) +} - App::new() - .service(counter_events) - .leptos_routes(leptos_options.to_owned(), routes.to_owned(), Counters) - .service(Files::new("/", site_root)) - //.wrap(middleware::Compress::default()) - }) - .bind(&addr)? - .run() - .await - } +#[actix_web::main] +async fn main() -> std::io::Result<()> { + // Setting this to None means we'll be using cargo-leptos and its env vars. + // when not using cargo-leptos None must be replaced with Some("Cargo.toml") + let conf = get_configuration(None).await.unwrap(); + + let addr = conf.leptos_options.site_addr; + let routes = generate_route_list(Counters); + + HttpServer::new(move || { + let leptos_options = &conf.leptos_options; + let site_root = &leptos_options.site_root; + + App::new() + .service(counter_events) + .leptos_routes( + leptos_options.to_owned(), + routes.to_owned(), + Counters, + ) + .service(Files::new("/", site_root)) + //.wrap(middleware::Compress::default()) + }) + .bind(&addr)? + .run() + .await } From 1b1e02729eb1c5892eb9e75da139fcd1b6e49743 Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Fri, 19 Jan 2024 15:17:17 -0500 Subject: [PATCH 087/100] clean up examples --- examples/counter_isomorphic/src/counters.rs | 2 +- examples/counter_isomorphic/src/main.rs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/counter_isomorphic/src/counters.rs b/examples/counter_isomorphic/src/counters.rs index b1cbabd68c..e5ab6db8be 100644 --- a/examples/counter_isomorphic/src/counters.rs +++ b/examples/counter_isomorphic/src/counters.rs @@ -5,7 +5,7 @@ use leptos_router::*; use tracing::instrument; #[cfg(feature = "ssr")] -mod ssr_imports { +pub mod ssr_imports { pub use broadcaster::BroadcastChannel; pub use once_cell::sync::OnceCell; pub use std::sync::atomic::{AtomicI32, Ordering}; diff --git a/examples/counter_isomorphic/src/main.rs b/examples/counter_isomorphic/src/main.rs index 368efbe825..42d5fbccfe 100644 --- a/examples/counter_isomorphic/src/main.rs +++ b/examples/counter_isomorphic/src/main.rs @@ -8,6 +8,7 @@ use leptos_actix::{generate_route_list, LeptosRoutes}; #[get("/api/events")] async fn counter_events() -> impl Responder { + use crate::counters::ssr_imports::*; use futures::StreamExt; let stream = futures::stream::once(async { From 46e7abf9ba2427fc8fa8c19e13e25fe80e7451db Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Fri, 19 Jan 2024 15:48:14 -0500 Subject: [PATCH 088/100] allow custom req/res/client types --- leptos_macro/src/lib.rs | 5 ++ server_fn/server_fn_macro_default/src/lib.rs | 5 ++ server_fn_macro/Cargo.toml | 1 + server_fn_macro/src/lib.rs | 78 ++++++++++++++++++-- 4 files changed, 84 insertions(+), 5 deletions(-) diff --git a/leptos_macro/src/lib.rs b/leptos_macro/src/lib.rs index 1ce7dd5336..6af2401b64 100644 --- a/leptos_macro/src/lib.rs +++ b/leptos_macro/src/lib.rs @@ -871,12 +871,15 @@ pub fn slot(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream { /// relative to the prefix (defaults to the function name followed by unique hash) /// - `input`: the encoding for the arguments (defaults to `PostUrl`) /// - `output`: the encoding for the response (defaults to `Json`) +/// - `client`: a custom `Client` implementation that will be used for this server fn /// - `encoding`: (legacy, may be deprecated in future) specifies the encoding, which may be one /// of the following (not case sensitive) /// - `"Url"`: `POST` request with URL-encoded arguments and JSON response /// - `"GetUrl"`: `GET` request with URL-encoded arguments and JSON response /// - `"Cbor"`: `POST` request with CBOR-encoded arguments and response /// - `"GetCbor"`: `GET` request with URL-encoded arguments and CBOR response +/// - `req` and `res` specify the HTTP request and response types to be used on the server (these +/// should usually only be necessary if you are integrating with a server other than Actix/Axum) /// /// ```rust,ignore /// #[server( @@ -949,6 +952,8 @@ pub fn server(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream { s.into(), Some(syn::parse_quote!(::leptos::server_fn)), "/api", + None, + None, ) { Err(e) => e.to_compile_error().into(), Ok(s) => s.to_token_stream().into(), diff --git a/server_fn/server_fn_macro_default/src/lib.rs b/server_fn/server_fn_macro_default/src/lib.rs index 9d18372839..fbd25da524 100644 --- a/server_fn/server_fn_macro_default/src/lib.rs +++ b/server_fn/server_fn_macro_default/src/lib.rs @@ -34,12 +34,15 @@ use syn::__private::ToTokens; /// relative to the prefix (defaults to the function name followed by unique hash) /// - `input`: the encoding for the arguments (defaults to `PostUrl`) /// - `output`: the encoding for the response (defaults to `Json`) +/// - `client`: a custom `Client` implementation that will be used for this server fn /// - `encoding`: (legacy, may be deprecated in future) specifies the encoding, which may be one /// of the following (not case sensitive) /// - `"Url"`: `POST` request with URL-encoded arguments and JSON response /// - `"GetUrl"`: `GET` request with URL-encoded arguments and JSON response /// - `"Cbor"`: `POST` request with CBOR-encoded arguments and response /// - `"GetCbor"`: `GET` request with URL-encoded arguments and CBOR response +/// - `req` and `res` specify the HTTP request and response types to be used on the server (these +/// should usually only be necessary if you are integrating with a server other than Actix/Axum) /// ```rust,ignore /// #[server( /// name = SomeStructName, @@ -71,6 +74,8 @@ pub fn server(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream { s.into(), Some(syn::parse_quote!(server_fns)), "/api", + None, + None, ) { Err(e) => e.to_compile_error().into(), Ok(s) => s.to_token_stream().into(), diff --git a/server_fn_macro/Cargo.toml b/server_fn_macro/Cargo.toml index 5fb3b6b561..472d640b1b 100644 --- a/server_fn_macro/Cargo.toml +++ b/server_fn_macro/Cargo.toml @@ -20,3 +20,4 @@ nightly = [] ssr = [] actix = [] axum = [] +reqwest = [] diff --git a/server_fn_macro/src/lib.rs b/server_fn_macro/src/lib.rs index 03db265357..d2b4a8cbcc 100644 --- a/server_fn_macro/src/lib.rs +++ b/server_fn_macro/src/lib.rs @@ -35,6 +35,8 @@ pub fn server_macro_impl( body: TokenStream2, server_fn_path: Option<Path>, default_path: &str, + preset_req: Option<Type>, + preset_res: Option<Type>, ) -> Result<TokenStream2> { let mut body = syn::parse::<ServerFnBody>(body.into())?; @@ -65,7 +67,12 @@ pub fn server_macro_impl( output, fn_path, builtin_encoding, + req_ty, + res_ty, + client, + custom_wrapper, } = args; + _ = custom_wrapper; // TODO: this should be used to enable custom encodings let prefix = prefix.unwrap_or_else(|| Literal::string(default_path)); let fn_path = fn_path.unwrap_or_else(|| Literal::string("")); let input_ident = match &input { @@ -380,9 +387,16 @@ pub fn server_macro_impl( PathInfo::None => quote! {}, }; - // TODO reqwest - let client = quote! { - #server_fn_path::client::browser::BrowserClient + let client = if let Some(client) = client { + client.to_token_stream() + } else if cfg!(feature = "reqwest") { + quote! { + #server_fn_path::client::reqwest::ReqwestClient + } + } else { + quote! { + #server_fn_path::client::browser::BrowserClient + } }; let req = if !cfg!(feature = "ssr") { @@ -397,11 +411,16 @@ pub fn server_macro_impl( quote! { #server_fn_path::request::actix::ActixRequest } + } else if let Some(req_ty) = req_ty { + req_ty.to_token_stream() + } else if let Some(req_ty) = preset_req { + req_ty.to_token_stream() } else { return Err(syn::Error::new( Span::call_site(), "If the `ssr` feature is enabled, either the `actix` or `axum` \ - features should also be enabled.", + features should also be enabled, or the `req = ` argument should \ + be provided to specify the request type.", )); }; let res = if !cfg!(feature = "ssr") { @@ -416,11 +435,16 @@ pub fn server_macro_impl( quote! { #server_fn_path::response::actix::ActixResponse } + } else if let Some(res_ty) = res_ty { + res_ty.to_token_stream() + } else if let Some(res_ty) = preset_res { + res_ty.to_token_stream() } else { return Err(syn::Error::new( Span::call_site(), "If the `ssr` feature is enabled, either the `actix` or `axum` \ - features should also be enabled.", + features should also be enabled, or the `res = ` argument should \ + be provided to specify the response type.", )); }; @@ -614,6 +638,10 @@ struct ServerFnArgs { input: Option<Type>, output: Option<Type>, fn_path: Option<Literal>, + req_ty: Option<Type>, + res_ty: Option<Type>, + client: Option<Type>, + custom_wrapper: Option<Type>, builtin_encoding: bool, } @@ -628,6 +656,10 @@ impl Parse for ServerFnArgs { // new arguments: can only be keyed by name let mut input: Option<Type> = None; let mut output: Option<Type> = None; + let mut req_ty: Option<Type> = None; + let mut res_ty: Option<Type> = None; + let mut client: Option<Type> = None; + let mut custom_wrapper: Option<Type> = None; let mut use_key_and_value = false; let mut arg_pos = 0; @@ -703,6 +735,38 @@ impl Parse for ServerFnArgs { )); } output = Some(stream.parse()?); + } else if key == "req" { + if req_ty.is_some() { + return Err(syn::Error::new( + key.span(), + "keyword argument repeated: `req`", + )); + } + req_ty = Some(stream.parse()?); + } else if key == "res" { + if res_ty.is_some() { + return Err(syn::Error::new( + key.span(), + "keyword argument repeated: `res`", + )); + } + res_ty = Some(stream.parse()?); + } else if key == "client" { + if client.is_some() { + return Err(syn::Error::new( + key.span(), + "keyword argument repeated: `client`", + )); + } + client = Some(stream.parse()?); + } else if key == "custom" { + if custom_wrapper.is_some() { + return Err(syn::Error::new( + key.span(), + "keyword argument repeated: `custom`", + )); + } + custom_wrapper = Some(stream.parse()?); } else { return Err(lookahead.error()); } @@ -794,6 +858,10 @@ impl Parse for ServerFnArgs { output, fn_path, builtin_encoding, + req_ty, + res_ty, + client, + custom_wrapper, }) } } From 26d9d75cf2c627f525ad3445a0fe25ed20aa22a0 Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Fri, 19 Jan 2024 15:56:57 -0500 Subject: [PATCH 089/100] cargo fmt --- examples/todo_app_sqlite_csr/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/todo_app_sqlite_csr/src/main.rs b/examples/todo_app_sqlite_csr/src/main.rs index 3ea3e9c67a..a09a0388ee 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() { let app = Router::new() // server function handlers are normally set up by .leptos_routes() // here, we're not actually doing server side rendering, so we set up a manual - // handler for the server fns + // 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)) .fallback(file_or_index_handler) From ddda7850458ef539d08ab82bdbb86a9d6160ef21 Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Fri, 19 Jan 2024 16:52:41 -0500 Subject: [PATCH 090/100] fix multipart support --- server_fn/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server_fn/Cargo.toml b/server_fn/Cargo.toml index cbb4d069d9..ce226d1bf5 100644 --- a/server_fn/Cargo.toml +++ b/server_fn/Cargo.toml @@ -94,7 +94,7 @@ browser = [ ] json = [] serde-lite = ["dep:serde-lite"] -multipart = ["dep:multer"] +multipart = ["browser", "dep:multer"] url = ["dep:serde_qs"] cbor = ["dep:ciborium"] rkyv = ["dep:rkyv"] From f19def9541b59de83f81e818f711d15a2da1049f Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Fri, 19 Jan 2024 16:55:16 -0500 Subject: [PATCH 091/100] clippy --- server_fn/src/request/reqwest.rs | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/server_fn/src/request/reqwest.rs b/server_fn/src/request/reqwest.rs index 012985470a..1352da2fc1 100644 --- a/server_fn/src/request/reqwest.rs +++ b/server_fn/src/request/reqwest.rs @@ -1,15 +1,9 @@ use super::ClientReq; -use crate::{ - client::get_server_url, - error::{ServerFnError, ServerFnErrorErr}, -}; +use crate::{client::get_server_url, error::ServerFnError}; use bytes::Bytes; -use futures::{Stream, StreamExt}; +use futures::Stream; use once_cell::sync::Lazy; -use reqwest::{ - header::{ACCEPT, CONTENT_TYPE}, - Body, -}; +use reqwest::header::{ACCEPT, CONTENT_TYPE}; pub use reqwest::{multipart::Form, Client, Method, Request, Url}; pub(crate) static CLIENT: Lazy<Client> = Lazy::new(Client::new); @@ -97,12 +91,17 @@ impl<CustErr> ClientReq<CustErr> for Request { } fn try_new_streaming( - path: &str, - accepts: &str, - content_type: &str, - body: impl Stream<Item = Bytes> + 'static, + _path: &str, + _accepts: &str, + _content_type: &str, + _body: impl Stream<Item = Bytes> + 'static, ) -> Result<Self, ServerFnError<CustErr>> { 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 + // That means the streaming types need to be wrappers over Sync streams + // However, Axum BodyDataStream is !Sync, so we can't use the same wrapper type there + /* let url = format!("{}{}", get_server_url(), path); let body = Body::wrap_stream( body.map(|chunk| Ok(chunk) as Result<Bytes, ServerFnErrorErr>), From eb45d05f3b16aef4a4850eaca06846843efe41b4 Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Fri, 19 Jan 2024 17:43:05 -0500 Subject: [PATCH 092/100] clippy --- integrations/axum/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integrations/axum/src/lib.rs b/integrations/axum/src/lib.rs index 2efd11fcd6..383150fd31 100644 --- a/integrations/axum/src/lib.rs +++ b/integrations/axum/src/lib.rs @@ -1813,7 +1813,7 @@ where .to_string(), ) })?; - T::from_request_parts(&mut parts, &state) + T::from_request_parts(&mut parts, state) .await .map_err(|e| ServerFnError::ServerError(format!("{e:?}"))) } From 70ec0c2d0a44e8b9b54c105b8fbfa324c4969196 Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Fri, 19 Jan 2024 18:02:22 -0500 Subject: [PATCH 093/100] update sso example --- examples/sso_auth_axum/Cargo.toml | 24 +- examples/sso_auth_axum/src/auth.rs | 74 ++++-- examples/sso_auth_axum/src/error_template.rs | 1 - examples/sso_auth_axum/src/fallback.rs | 84 +++--- examples/sso_auth_axum/src/lib.rs | 63 ++--- examples/sso_auth_axum/src/main.rs | 250 ++++++++++-------- examples/sso_auth_axum/src/sign_in_sign_up.rs | 41 +-- examples/sso_auth_axum/src/state.rs | 12 +- 8 files changed, 284 insertions(+), 265 deletions(-) diff --git a/examples/sso_auth_axum/Cargo.toml b/examples/sso_auth_axum/Cargo.toml index 6630aebe5f..14b2bda6c6 100644 --- a/examples/sso_auth_axum/Cargo.toml +++ b/examples/sso_auth_axum/Cargo.toml @@ -7,40 +7,39 @@ edition = "2021" crate-type = ["cdylib", "rlib"] [dependencies] -oauth2 = {version="4.4.2",optional=true} +oauth2 = { version = "4.4.2", optional = true } anyhow = "1.0.66" console_log = "1.0.0" rand = { version = "0.8.5", features = ["min_const_gen"], optional = true } console_error_panic_hook = "0.1.7" futures = "0.3.25" -cfg-if = "1.0.0" -leptos = { path = "../../leptos"} +leptos = { path = "../../leptos" } leptos_meta = { path = "../../meta" } leptos_axum = { path = "../../integrations/axum", optional = true } -leptos_router = { path = "../../router"} +leptos_router = { path = "../../router" } log = "0.4.17" simple_logger = "4.0.0" serde = { version = "1.0.148", features = ["derive"] } -serde_json = {version="1.0.108", optional = true } -axum = { version = "0.6.1", optional = true, features=["macros"] } -tower = { version = "0.4.13", optional = true } -tower-http = { version = "0.4", features = ["fs"], optional = true } +serde_json = { version = "1.0.108", optional = true } +axum = { version = "0.7", optional = true, features = ["macros"] } +tower = { version = "0.4", optional = true } +tower-http = { version = "0.5", features = ["fs"], optional = true } tokio = { version = "1.22.0", features = ["full"], optional = true } -http = { version = "0.2.8" } +http = { version = "1" } sqlx = { version = "0.7", features = [ "runtime-tokio-rustls", "sqlite", ], optional = true } thiserror = "1.0.38" wasm-bindgen = "0.2" -axum_session_auth = { version = "0.9", features = [ +axum_session_auth = { version = "0.12", features = [ "sqlite-rustls", ], optional = true } -axum_session = { version = "0.9", features = [ +axum_session = { version = "0.12", features = [ "sqlite-rustls", ], optional = true } async-trait = { version = "0.1.64", optional = true } -reqwest= {version="0.11",optional=true, features=["json"]} +reqwest = { version = "0.11", optional = true, features = ["json"] } [features] hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"] @@ -64,7 +63,6 @@ ssr = [ ] - [package.metadata.leptos] # The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name output-name = "sso_auth_axum" diff --git a/examples/sso_auth_axum/src/auth.rs b/examples/sso_auth_axum/src/auth.rs index 5519fb4f64..ce8f4d7066 100644 --- a/examples/sso_auth_axum/src/auth.rs +++ b/examples/sso_auth_axum/src/auth.rs @@ -1,14 +1,6 @@ -use cfg_if::cfg_if; use serde::{Deserialize, Serialize}; use std::collections::HashSet; -cfg_if! { -if #[cfg(feature = "ssr")] { - use sqlx::SqlitePool; - use axum_session_auth::{SessionSqlitePool, Authentication, HasPermission}; - pub type AuthSession = axum_session_auth::AuthSession<User, i64, SessionSqlitePool, SqlitePool>; -}} - #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct User { pub id: i64, @@ -28,36 +20,56 @@ impl Default for User { } } -cfg_if! { -if #[cfg(feature = "ssr")] { +#[cfg(feature = "ssr")] +pub mod ssr_imports { + use super::User; + pub use axum_session_auth::{ + Authentication, HasPermission, SessionSqlitePool, + }; + pub use sqlx::SqlitePool; + use std::collections::HashSet; + pub type AuthSession = axum_session_auth::AuthSession< + User, + i64, + SessionSqlitePool, + SqlitePool, + >; + use async_trait::async_trait; impl User { pub async fn get(id: i64, pool: &SqlitePool) -> Option<Self> { - let sqluser = sqlx::query_as::<_, SqlUser>("SELECT * FROM users WHERE id = ?") - .bind(id) - .fetch_one(pool) - .await - .ok()?; + let sqluser = sqlx::query_as::<_, SqlUser>( + "SELECT * FROM users WHERE id = ?", + ) + .bind(id) + .fetch_one(pool) + .await + .ok()?; //lets just get all the tokens the user can use, we will only use the full permissions if modifing them. let sql_user_perms = sqlx::query_as::<_, SqlPermissionTokens>( "SELECT token FROM user_permissions WHERE user_id = ?;", ) - .bind(id) - .fetch_all(pool) - .await - .ok()?; + .bind(id) + .fetch_all(pool) + .await + .ok()?; Some(sqluser.into_user(Some(sql_user_perms))) } - pub async fn get_from_email(email: &str, pool: &SqlitePool) -> Option<Self> { - let sqluser = sqlx::query_as::<_, SqlUser>("SELECT * FROM users WHERE email = ?") - .bind(email) - .fetch_one(pool) - .await - .ok()?; + pub async fn get_from_email( + email: &str, + pool: &SqlitePool, + ) -> Option<Self> { + let sqluser = sqlx::query_as::<_, SqlUser>( + "SELECT * FROM users WHERE email = ?", + ) + .bind(email) + .fetch_one(pool) + .await + .ok()?; //lets just get all the tokens the user can use, we will only use the full permissions if modifing them. let sql_user_perms = sqlx::query_as::<_, SqlPermissionTokens>( @@ -84,7 +96,10 @@ if #[cfg(feature = "ssr")] { #[async_trait] impl Authentication<User, i64, SqlitePool> for User { - async fn load_user(userid: i64, pool: Option<&SqlitePool>) -> Result<User, anyhow::Error> { + async fn load_user( + userid: i64, + pool: Option<&SqlitePool>, + ) -> Result<User, anyhow::Error> { let pool = pool.unwrap(); User::get(userid, pool) @@ -123,9 +138,11 @@ if #[cfg(feature = "ssr")] { pub secret: String, } - impl SqlUser { - pub fn into_user(self, sql_user_perms: Option<Vec<SqlPermissionTokens>>) -> User { + pub fn into_user( + self, + sql_user_perms: Option<Vec<SqlPermissionTokens>>, + ) -> User { User { id: self.id, email: self.email, @@ -141,4 +158,3 @@ if #[cfg(feature = "ssr")] { } } } -} diff --git a/examples/sso_auth_axum/src/error_template.rs b/examples/sso_auth_axum/src/error_template.rs index 9635f45d04..f9b32330d0 100644 --- a/examples/sso_auth_axum/src/error_template.rs +++ b/examples/sso_auth_axum/src/error_template.rs @@ -14,7 +14,6 @@ pub fn error_template(errors: RwSignal<Errors>) -> View { children= move | (_, error)| { let error_string = error.to_string(); view! { - <p>"Error: " {error_string}</p> } } diff --git a/examples/sso_auth_axum/src/fallback.rs b/examples/sso_auth_axum/src/fallback.rs index 2ac8e1c38b..2ded1fd4ff 100644 --- a/examples/sso_auth_axum/src/fallback.rs +++ b/examples/sso_auth_axum/src/fallback.rs @@ -1,47 +1,49 @@ -use cfg_if::cfg_if; +use crate::error_template::error_template; +use axum::{ + body::Body, + extract::State, + http::{Request, Response, StatusCode, Uri}, + response::{IntoResponse, Response as AxumResponse}, +}; +use leptos::*; +use tower::ServiceExt; +use tower_http::services::ServeDir; -cfg_if! { -if #[cfg(feature = "ssr")] { - use axum::{ - body::{boxed, Body, BoxBody}, - extract::State, - response::IntoResponse, - http::{Request, Response, StatusCode, Uri}, - }; - use axum::response::Response as AxumResponse; - use tower::ServiceExt; - use tower_http::services::ServeDir; - use leptos::*; - use crate::error_template::error_template; +pub async fn file_and_error_handler( + uri: Uri, + State(options): State<LeptosOptions>, + req: Request<Body>, +) -> AxumResponse { + let root = options.site_root.clone(); + let res = get_static_file(uri.clone(), &root).await.unwrap(); - pub async fn file_and_error_handler(uri: Uri, State(options): State<LeptosOptions>, req: Request<Body>) -> AxumResponse { - let root = options.site_root.clone(); - let res = get_static_file(uri.clone(), &root).await.unwrap(); - - if res.status() == StatusCode::OK { - res.into_response() - } else { - leptos::logging::log!("{:?}:{}",res.status(),uri); - let handler = leptos_axum::render_app_to_stream( - options.to_owned(), - || error_template(create_rw_signal(leptos::Errors::default()) - ) - ); - handler(req).await.into_response() - } + if res.status() == StatusCode::OK { + res.into_response() + } else { + leptos::logging::log!("{:?}:{}", res.status(), uri); + let handler = + leptos_axum::render_app_to_stream(options.to_owned(), || { + error_template(create_rw_signal(leptos::Errors::default())) + }); + handler(req).await.into_response() } +} - async fn get_static_file(uri: Uri, root: &str) -> Result<Response<BoxBody>, (StatusCode, String)> { - let req = Request::builder().uri(uri.clone()).body(Body::empty()).unwrap(); - // `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot` - // This path is relative to the cargo root - match ServeDir::new(root).oneshot(req).await { - Ok(res) => Ok(res.map(boxed)), - Err(err) => Err(( - StatusCode::INTERNAL_SERVER_ERROR, - format!("Something went wrong: {}", err), - )), - } - } +async fn get_static_file( + uri: Uri, + root: &str, +) -> Result<Response<Body>, (StatusCode, String)> { + let req = Request::builder() + .uri(uri.clone()) + .body(Body::empty()) + .unwrap(); + // `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot` + // This path is relative to the cargo root + match ServeDir::new(root).oneshot(req).await { + Ok(res) => Ok(res.into_response()), + Err(err) => Err(( + StatusCode::INTERNAL_SERVER_ERROR, + format!("Something went wrong: {}", err), + )), } } diff --git a/examples/sso_auth_axum/src/lib.rs b/examples/sso_auth_axum/src/lib.rs index 61ebb0dce8..8b95ca5ce8 100644 --- a/examples/sso_auth_axum/src/lib.rs +++ b/examples/sso_auth_axum/src/lib.rs @@ -1,36 +1,30 @@ -use cfg_if::cfg_if; - pub mod auth; pub mod error_template; +#[cfg(feature = "ssr")] pub mod fallback; pub mod sign_in_sign_up; +#[cfg(feature = "ssr")] pub mod state; use leptos::{leptos_dom::helpers::TimeoutHandle, *}; use leptos_meta::*; use leptos_router::*; use sign_in_sign_up::*; -cfg_if! { - if #[cfg(feature = "ssr")] { - use crate::{ - state::AppState, - auth::{AuthSession,User,SqlRefreshToken} - }; - use oauth2::{ - reqwest::async_http_client, - TokenResponse - }; - use sqlx::SqlitePool; +#[cfg(feature = "ssr")] +mod ssr_imports { + pub use crate::auth::ssr_imports::{AuthSession, SqlRefreshToken}; + pub use leptos::{use_context, ServerFnError}; + pub use oauth2::{reqwest::async_http_client, TokenResponse}; + pub use sqlx::SqlitePool; - pub fn pool() -> Result<SqlitePool, ServerFnError> { - use_context::<SqlitePool>() - .ok_or_else(|| ServerFnError::ServerError("Pool missing.".into())) - } + pub fn pool() -> Result<SqlitePool, ServerFnError> { + use_context::<SqlitePool>() + .ok_or_else(|| ServerFnError::new("Pool missing.")) + } - pub fn auth() -> Result<AuthSession, ServerFnError> { - use_context::<AuthSession>() - .ok_or_else(|| ServerFnError::ServerError("Auth session missing.".into())) - } + pub fn auth() -> Result<AuthSession, ServerFnError> { + use_context::<AuthSession>() + .ok_or_else(|| ServerFnError::new("Auth session missing.")) } } @@ -40,11 +34,14 @@ pub struct Email(RwSignal<Option<String>>); pub struct ExpiresIn(RwSignal<u64>); #[server] pub async fn refresh_token(email: String) -> Result<u64, ServerFnError> { + use crate::{auth::User, state::AppState}; + use ssr_imports::*; + let pool = pool()?; let oauth_client = expect_context::<AppState>().client; let user = User::get_from_email(&email, &pool) .await - .ok_or(ServerFnError::ServerError("User not found".to_string()))?; + .ok_or(ServerFnError::new("User not found"))?; let refresh_secret = sqlx::query_as::<_, SqlRefreshToken>( "SELECT secret FROM google_refresh_tokens WHERE user_id = ?", @@ -77,6 +74,7 @@ pub async fn refresh_token(email: String) -> Result<u64, ServerFnError> { .await?; Ok(expires_in) } + #[component] pub fn App() -> impl IntoView { provide_meta_context(); @@ -143,20 +141,11 @@ pub fn App() -> impl IntoView { } } -// Needs to be in lib.rs AFAIK because wasm-bindgen needs us to be compiling a lib. I may be wrong. -cfg_if! { - if #[cfg(feature = "hydrate")] { - use wasm_bindgen::prelude::wasm_bindgen; - use leptos::view; - - #[wasm_bindgen] - pub fn hydrate() { - _ = console_log::init_with_level(log::Level::Debug); - console_error_panic_hook::set_once(); +#[cfg(feature = "hydrate")] +#[wasm_bindgen::prelude::wasm_bindgen] +pub fn hydrate() { + _ = console_log::init_with_level(log::Level::Debug); + console_error_panic_hook::set_once(); - leptos::mount_to_body(|| { - view! { <App/> } - }); - } - } + leptos::mount_to_body(App); } diff --git a/examples/sso_auth_axum/src/main.rs b/examples/sso_auth_axum/src/main.rs index 49b7e74f6e..9a017d44b2 100644 --- a/examples/sso_auth_axum/src/main.rs +++ b/examples/sso_auth_axum/src/main.rs @@ -1,136 +1,154 @@ -use cfg_if::cfg_if; - -// boilerplate to run in different modes -cfg_if! { -if #[cfg(feature = "ssr")] { - use axum::{ - response::{IntoResponse}, - routing::get, - extract::{Path, State, RawQuery}, - http::{Request, header::HeaderMap}, - body::Body as AxumBody, - Router, - }; - use sso_auth_axum::auth::*; - use sso_auth_axum::state::AppState; - use sso_auth_axum::fallback::file_and_error_handler; - use leptos_axum::{generate_route_list, handle_server_fns_with_context, LeptosRoutes}; - use leptos::{logging::log, view, provide_context, get_configuration}; - use sqlx::{SqlitePool, sqlite::SqlitePoolOptions}; - use axum_session::{SessionConfig, SessionLayer, SessionStore,Key, SecurityMode}; - use axum_session_auth::{AuthSessionLayer, AuthConfig, SessionSqlitePool}; - - async fn server_fn_handler(State(app_state): State<AppState>, auth_session: AuthSession, path: Path<String>, headers: HeaderMap, raw_query: RawQuery, - request: Request<AxumBody>) -> impl IntoResponse { - - log!("{:?}", path); - - handle_server_fns_with_context(path, headers, raw_query, move || { +use crate::ssr_imports::*; +use axum::{ + body::Body as AxumBody, + extract::{Path, State}, + http::Request, + response::IntoResponse, + routing::get, + Router, +}; +use axum_session::{Key, SessionConfig, SessionLayer, SessionStore}; +use axum_session_auth::{AuthConfig, AuthSessionLayer, SessionSqlitePool}; +use leptos::{get_configuration, logging::log, provide_context, view}; +use leptos_axum::{ + generate_route_list, handle_server_fns_with_context, LeptosRoutes, +}; +use sqlx::{sqlite::SqlitePoolOptions, SqlitePool}; +use sso_auth_axum::{ + auth::*, fallback::file_and_error_handler, state::AppState, +}; + +async fn server_fn_handler( + State(app_state): State<AppState>, + auth_session: AuthSession, + path: Path<String>, + request: Request<AxumBody>, +) -> impl IntoResponse { + log!("{:?}", path); + + handle_server_fns_with_context( + move || { provide_context(app_state.clone()); provide_context(auth_session.clone()); provide_context(app_state.pool.clone()); - }, request).await - } - - pub async fn leptos_routes_handler( - auth_session: AuthSession, - State(app_state): State<AppState>, - axum::extract::State(option): axum::extract::State<leptos::LeptosOptions>, - request: Request<AxumBody>, - ) -> axum::response::Response { - let handler = leptos_axum::render_app_async_with_context( - option.clone(), - move || { - provide_context(app_state.clone()); - provide_context(auth_session.clone()); - provide_context(app_state.pool.clone()); - }, - move || view! { <sso_auth_axum::App/> }, - ); - - handler(request).await.into_response() - } - - - #[tokio::main] - async fn main() { - simple_logger::init_with_level(log::Level::Info).expect("couldn't initialize logging"); - - let pool = SqlitePoolOptions::new() - .connect("sqlite:sso.db") - .await - .expect("Could not make pool."); - - // Auth section - let session_config = SessionConfig::default() - .with_table_name("sessions_table") - .with_key(Key::generate()) - .with_database_key(Key::generate()) - .with_security_mode(SecurityMode::PerSession); + }, + request, + ) + .await +} - let auth_config = AuthConfig::<i64>::default(); - let session_store = SessionStore::<SessionSqlitePool>::new(Some(pool.clone().into()), session_config).await.unwrap(); +pub async fn leptos_routes_handler( + auth_session: AuthSession, + State(app_state): State<AppState>, + axum::extract::State(option): axum::extract::State<leptos::LeptosOptions>, + request: Request<AxumBody>, +) -> axum::response::Response { + let handler = leptos_axum::render_app_async_with_context( + option.clone(), + move || { + provide_context(app_state.clone()); + provide_context(auth_session.clone()); + provide_context(app_state.pool.clone()); + }, + move || view! { <sso_auth_axum::App/> }, + ); - sqlx::migrate!() - .run(&pool) - .await - .expect("could not run SQLx migrations"); + handler(request).await.into_response() +} +#[tokio::main] +async fn main() { + simple_logger::init_with_level(log::Level::Info) + .expect("couldn't initialize logging"); + + let pool = SqlitePoolOptions::new() + .connect("sqlite:sso.db") + .await + .expect("Could not make pool."); + + // Auth section + let session_config = SessionConfig::default() + .with_table_name("sessions_table") + .with_key(Key::generate()) + .with_database_key(Key::generate()); + // .with_security_mode(SecurityMode::PerSession); // FIXME did this disappear? + + let auth_config = AuthConfig::<i64>::default(); + let session_store = SessionStore::<SessionSqlitePool>::new( + Some(pool.clone().into()), + session_config, + ) + .await + .unwrap(); + sqlx::migrate!() + .run(&pool) + .await + .expect("could not run SQLx migrations"); - // Setting this to None means we'll be using cargo-leptos and its env vars - let conf = get_configuration(None).await.unwrap(); - let leptos_options = conf.leptos_options; - let addr = leptos_options.site_addr; - let routes = generate_route_list(sso_auth_axum::App); + // Setting this to None means we'll be using cargo-leptos and its env vars + let conf = get_configuration(None).await.unwrap(); + let leptos_options = conf.leptos_options; + let addr = leptos_options.site_addr; + let routes = generate_route_list(sso_auth_axum::App); - // We create our client using provided environment variables. + // We create our client using provided environment variables. let client = oauth2::basic::BasicClient::new( - oauth2::ClientId::new(std::env::var("G_AUTH_CLIENT_ID").expect("G_AUTH_CLIENT Env var to be set.")), - Some(oauth2::ClientSecret::new(std::env::var("G_AUTH_SECRET").expect("G_AUTH_SECRET Env var to be set"))), + oauth2::ClientId::new( + std::env::var("G_AUTH_CLIENT_ID") + .expect("G_AUTH_CLIENT Env var to be set."), + ), + Some(oauth2::ClientSecret::new( + std::env::var("G_AUTH_SECRET") + .expect("G_AUTH_SECRET Env var to be set"), + )), oauth2::AuthUrl::new( "https://accounts.google.com/o/oauth2/v2/auth".to_string(), ) .unwrap(), Some( - oauth2::TokenUrl::new("https://oauth2.googleapis.com/token".to_string()) - .unwrap(), + oauth2::TokenUrl::new( + "https://oauth2.googleapis.com/token".to_string(), + ) + .unwrap(), ), ) - .set_redirect_uri(oauth2::RedirectUrl::new(std::env::var("REDIRECT_URL").expect("REDIRECT_URL Env var to be set")).unwrap()); - - - let app_state = AppState{ - leptos_options, - pool: pool.clone(), - client, - }; - - // build our application with a route - let app = Router::new() - .route("/api/*fn_name", get(server_fn_handler).post(server_fn_handler)) - .leptos_routes_with_handler(routes, get(leptos_routes_handler) ) - .fallback(file_and_error_handler) - .layer(AuthSessionLayer::<User, i64, SessionSqlitePool, SqlitePool>::new(Some(pool.clone())) - .with_config(auth_config)) - .layer(SessionLayer::new(session_store)) - .with_state(app_state); + .set_redirect_uri( + oauth2::RedirectUrl::new( + std::env::var("REDIRECT_URL") + .expect("REDIRECT_URL Env var to be set"), + ) + .unwrap(), + ); - // run our app with hyper - // `axum::Server` is a re-export of `hyper::Server` - log!("listening on http://{}", &addr); - axum::Server::bind(&addr) - .serve(app.into_make_service()) - .await - .unwrap(); - } -} + let app_state = AppState { + leptos_options, + pool: pool.clone(), + client, + }; - // client-only stuff for Trunk - else { - pub fn main() { - // This example cannot be built as a trunk standalone CSR-only app. - // Only the server may directly connect to the database. - } - } + // build our application with a route + let app = Router::new() + .route( + "/api/*fn_name", + get(server_fn_handler).post(server_fn_handler), + ) + .leptos_routes_with_handler(routes, get(leptos_routes_handler)) + .fallback(file_and_error_handler) + .layer( + AuthSessionLayer::<User, i64, SessionSqlitePool, SqlitePool>::new( + Some(pool.clone()), + ) + .with_config(auth_config), + ) + .layer(SessionLayer::new(session_store)) + .with_state(app_state); + + // run our app with hyper + // `axum::Server` is a re-export of `hyper::Server` + let listener = tokio::net::TcpListener::bind(&addr).await.unwrap(); + log!("listening on http://{}", &addr); + axum::serve(listener, app.into_make_service()) + .await + .unwrap(); } diff --git a/examples/sso_auth_axum/src/sign_in_sign_up.rs b/examples/sso_auth_axum/src/sign_in_sign_up.rs index 1ce73e1ece..fda2451996 100644 --- a/examples/sso_auth_axum/src/sign_in_sign_up.rs +++ b/examples/sso_auth_axum/src/sign_in_sign_up.rs @@ -1,23 +1,23 @@ use super::*; -cfg_if! { - if #[cfg(feature="ssr")]{ - use oauth2::{ - AuthorizationCode, - TokenResponse, - reqwest::async_http_client, - CsrfToken, - Scope, - }; - use serde_json::Value; - use crate::{ - auth::{User,SqlCsrfToken}, - state::AppState - }; - } +#[cfg(feature = "ssr")] +pub mod ssr_imports { + pub use crate::{ + auth::{ssr_imports::SqlCsrfToken, User}, + state::AppState, + }; + pub use oauth2::{ + reqwest::async_http_client, AuthorizationCode, CsrfToken, Scope, + TokenResponse, + }; + pub use serde_json::Value; } + #[server] pub async fn google_sso() -> Result<String, ServerFnError> { + use crate::ssr_imports::*; + use ssr_imports::*; + let oauth_client = expect_context::<AppState>().client; let pool = pool()?; @@ -80,6 +80,9 @@ pub async fn handle_g_auth_redirect( provided_csrf: String, code: String, ) -> Result<(String, u64), ServerFnError> { + use crate::ssr_imports::*; + use ssr_imports::*; + let oauth_client = expect_context::<AppState>().client; let pool = pool()?; let auth_session = auth()?; @@ -90,9 +93,7 @@ pub async fn handle_g_auth_redirect( .bind(provided_csrf) .fetch_one(&pool) .await - .map_err(|err| { - ServerFnError::ServerError(format!("CSRF_TOKEN error : {err:?}")) - })?; + .map_err(|err| ServerFnError::new(format!("CSRF_TOKEN error : {err:?}")))?; let token_response = oauth_client .exchange_code(AuthorizationCode::new(code.clone())) @@ -118,7 +119,7 @@ pub async fn handle_g_auth_redirect( .expect("email to parse to string") .to_string() } else { - return Err(ServerFnError::ServerError(format!( + return Err(ServerFnError::new(format!( "Response from google has status of {}", response.status() ))); @@ -193,6 +194,8 @@ pub fn HandleGAuth() -> impl IntoView { #[server] pub async fn logout() -> Result<(), ServerFnError> { + use crate::ssr_imports::*; + let auth = auth()?; auth.logout_user(); leptos_axum::redirect("/"); diff --git a/examples/sso_auth_axum/src/state.rs b/examples/sso_auth_axum/src/state.rs index 27bad673b1..4bda1d5e49 100644 --- a/examples/sso_auth_axum/src/state.rs +++ b/examples/sso_auth_axum/src/state.rs @@ -1,18 +1,12 @@ -use cfg_if::cfg_if; - -cfg_if! { - if #[cfg(feature = "ssr")] { +use axum::extract::FromRef; use leptos::LeptosOptions; use sqlx::SqlitePool; -use axum::extract::FromRef; /// This takes advantage of Axum's SubStates feature by deriving FromRef. This is the only way to have more than one /// item in Axum's State. Leptos requires you to have leptosOptions in your State struct for the leptos route handlers #[derive(FromRef, Debug, Clone)] -pub struct AppState{ +pub struct AppState { pub leptos_options: LeptosOptions, pub pool: SqlitePool, - pub client:oauth2::basic::BasicClient, -} - } + pub client: oauth2::basic::BasicClient, } From 2092c40bc77c43ed2b4d34245bee92898f8688be Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Fri, 19 Jan 2024 18:21:57 -0500 Subject: [PATCH 094/100] missing derives --- server_fn/src/error.rs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/server_fn/src/error.rs b/server_fn/src/error.rs index 63071ce950..fb4e7c358a 100644 --- a/server_fn/src/error.rs +++ b/server_fn/src/error.rs @@ -61,7 +61,18 @@ impl From<ServerFnError> for Error { /// An empty value indicating that there is no custom error type associated /// with this server function. -#[derive(Debug, Deserialize, Serialize, Clone, Copy)] +#[derive( + Debug, + Deserialize, + Serialize, + PartialEq, + Eq, + Hash, + PartialOrd, + Ord, + Clone, + Copy, +)] pub struct NoCustomError; // Implement `Display` for `NoCustomError` From b4a1d90327c464bb8652e326326e7ec3b4940e04 Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Sat, 20 Jan 2024 12:32:51 -0500 Subject: [PATCH 095/100] clean up for CI --- examples/hackernews_islands_axum/src/main.rs | 2 +- examples/hackernews_js_fetch/Cargo.toml | 12 ++-- examples/hackernews_js_fetch/README.md | 2 + examples/server_fns_axum/Cargo.toml | 1 - examples/server_fns_axum/Makefile.toml | 8 +-- examples/server_fns_axum/e2e/Cargo.toml | 18 ----- examples/server_fns_axum/e2e/Makefile.toml | 20 ------ examples/server_fns_axum/e2e/README.md | 34 ---------- .../e2e/features/add_todo.feature | 16 ----- .../e2e/features/delete_todo.feature | 18 ----- .../e2e/features/open_app.feature | 12 ---- .../server_fns_axum/e2e/tests/app_suite.rs | 14 ---- .../e2e/tests/fixtures/action.rs | 60 ----------------- .../e2e/tests/fixtures/check.rs | 57 ---------------- .../e2e/tests/fixtures/find.rs | 63 ----------------- .../server_fns_axum/e2e/tests/fixtures/mod.rs | 4 -- .../e2e/tests/fixtures/world/action_steps.rs | 57 ---------------- .../e2e/tests/fixtures/world/check_steps.rs | 67 ------------------- .../e2e/tests/fixtures/world/mod.rs | 39 ----------- examples/server_fns_axum/src/app.rs | 9 +-- examples/sso_auth_axum/Cargo.toml | 3 + examples/sso_auth_axum/Makefile.toml | 15 ++--- leptos_server/src/action.rs | 4 +- leptos_server/src/multi_action.rs | 2 +- server_fn/Cargo.toml | 5 ++ server_fn/src/codec/mod.rs | 8 +-- server_fn_macro/src/lib.rs | 24 +++---- 27 files changed, 49 insertions(+), 525 deletions(-) delete mode 100644 examples/server_fns_axum/e2e/Cargo.toml delete mode 100644 examples/server_fns_axum/e2e/Makefile.toml delete mode 100644 examples/server_fns_axum/e2e/README.md delete mode 100644 examples/server_fns_axum/e2e/features/add_todo.feature delete mode 100644 examples/server_fns_axum/e2e/features/delete_todo.feature delete mode 100644 examples/server_fns_axum/e2e/features/open_app.feature delete mode 100644 examples/server_fns_axum/e2e/tests/app_suite.rs delete mode 100644 examples/server_fns_axum/e2e/tests/fixtures/action.rs delete mode 100644 examples/server_fns_axum/e2e/tests/fixtures/check.rs delete mode 100644 examples/server_fns_axum/e2e/tests/fixtures/find.rs delete mode 100644 examples/server_fns_axum/e2e/tests/fixtures/mod.rs delete mode 100644 examples/server_fns_axum/e2e/tests/fixtures/world/action_steps.rs delete mode 100644 examples/server_fns_axum/e2e/tests/fixtures/world/check_steps.rs delete mode 100644 examples/server_fns_axum/e2e/tests/fixtures/world/mod.rs diff --git a/examples/hackernews_islands_axum/src/main.rs b/examples/hackernews_islands_axum/src/main.rs index bcc1d42718..6fb0991553 100644 --- a/examples/hackernews_islands_axum/src/main.rs +++ b/examples/hackernews_islands_axum/src/main.rs @@ -21,7 +21,7 @@ async fn main() { // run our app with hyper // `axum::Server` is a re-export of `hyper::Server` - logging::log!("listening on {}", addr); + println!("listening on {}", addr); let listener = tokio::net::TcpListener::bind(&addr).await.unwrap(); axum::serve(listener, app.into_make_service()) .await diff --git a/examples/hackernews_js_fetch/Cargo.toml b/examples/hackernews_js_fetch/Cargo.toml index 698a01a560..fd582a0e7b 100644 --- a/examples/hackernews_js_fetch/Cargo.toml +++ b/examples/hackernews_js_fetch/Cargo.toml @@ -13,10 +13,14 @@ lto = true [dependencies] console_log = "1.0" console_error_panic_hook = "0.1" -leptos = { path = "../../leptos", features = ["nightly"] } -leptos_axum = { path = "../../integrations/axum", default-features = false, optional = true } -leptos_meta = { path = "../../meta", features = ["nightly"] } -leptos_router = { path = "../../router", features = ["nightly"] } +#leptos = { version = "0.5",/leptos", features = ["nightly"] } +#leptos_axum = { version = "0.5",/integrations/axum", default-features = false, optional = true } +#leptos_meta = { version = "0.5",/meta", features = ["nightly"] } +#leptos_router = { version = "0.5",/router", features = ["nightly"] } +leptos = { version = "0.5", features = ["nightly"] } +leptos_axum = { version = "0.5", default-features = false, optional = true } +leptos_meta = { version = "0.5", features = ["nightly"] } +leptos_router = { version = "0.5", features = ["nightly"] } log = "0.4" simple_logger = "4.0" serde = { version = "1.0", features = ["derive"] } diff --git a/examples/hackernews_js_fetch/README.md b/examples/hackernews_js_fetch/README.md index e1f22c225d..c23a030299 100644 --- a/examples/hackernews_js_fetch/README.md +++ b/examples/hackernews_js_fetch/README.md @@ -2,6 +2,8 @@ This example uses the basic Hacker News example as its basis, but shows how to run the server side as WASM running in a JS environment. In this example, Deno is used as the runtime. +**NOTE**: This example is slightly out of date pending an update to [`axum-js-fetch`](https://github.com/seanaye/axum-js-fetch/), which was waiting on a version of `gloo-net` that uses `http` 1.0. It still works with Leptos 0.5 and Axum 0.6, but not with the versions of Leptos (0.6 and later) that support Axum 1.0. + ## Server Side Rendering with Deno To run the Deno version, run diff --git a/examples/server_fns_axum/Cargo.toml b/examples/server_fns_axum/Cargo.toml index 77bbbea58b..26955fa13f 100644 --- a/examples/server_fns_axum/Cargo.toml +++ b/examples/server_fns_axum/Cargo.toml @@ -45,7 +45,6 @@ ssr = [ "dep:leptos_axum", "dep:notify" ] -notify = ["dep:notify"] [package.metadata.cargo-all-features] denylist = ["axum", "tower", "tower-http", "tokio", "leptos_axum"] diff --git a/examples/server_fns_axum/Makefile.toml b/examples/server_fns_axum/Makefile.toml index d5fd86f317..c750007c55 100644 --- a/examples/server_fns_axum/Makefile.toml +++ b/examples/server_fns_axum/Makefile.toml @@ -1,12 +1,8 @@ extend = [ { path = "../cargo-make/main.toml" }, - { path = "../cargo-make/cargo-leptos-webdriver-test.toml" }, + { path = "../cargo-make/cargo-leptos.toml" }, ] [env] -CLIENT_PROCESS_NAME = "todo_app_sqlite_axum" -[tasks.test-ui] -cwd = "./e2e" -command = "cargo" -args = ["make", "test-ui", "${@}"] +CLIENT_PROCESS_NAME = "server_fns_axum" diff --git a/examples/server_fns_axum/e2e/Cargo.toml b/examples/server_fns_axum/e2e/Cargo.toml deleted file mode 100644 index 7224286755..0000000000 --- a/examples/server_fns_axum/e2e/Cargo.toml +++ /dev/null @@ -1,18 +0,0 @@ -[package] -name = "todo_app_sqlite_axum_e2e" -version = "0.1.0" -edition = "2021" - -[dev-dependencies] -anyhow = "1.0.72" -async-trait = "0.1.72" -cucumber = "0.19.1" -fantoccini = "0.19.3" -pretty_assertions = "1.4.0" -serde_json = "1.0.104" -tokio = { version = "1.29.1", features = ["macros", "rt-multi-thread", "time"] } -url = "2.4.0" - -[[test]] -name = "app_suite" -harness = false # Allow Cucumber to print output instead of libtest diff --git a/examples/server_fns_axum/e2e/Makefile.toml b/examples/server_fns_axum/e2e/Makefile.toml deleted file mode 100644 index cd76be24d5..0000000000 --- a/examples/server_fns_axum/e2e/Makefile.toml +++ /dev/null @@ -1,20 +0,0 @@ -extend = { path = "../../cargo-make/main.toml" } - -[tasks.test] -env = { RUN_AUTOMATICALLY = false } -condition = { env_true = ["RUN_AUTOMATICALLY"] } - -[tasks.ci] - -[tasks.test-ui] -command = "cargo" -args = [ - "test", - "--test", - "app_suite", - "--", - "--retry", - "2", - "--fail-fast", - "${@}", -] diff --git a/examples/server_fns_axum/e2e/README.md b/examples/server_fns_axum/e2e/README.md deleted file mode 100644 index 026f2befd3..0000000000 --- a/examples/server_fns_axum/e2e/README.md +++ /dev/null @@ -1,34 +0,0 @@ -# E2E Testing - -This example demonstrates e2e testing with Rust using executable requirements. - -## Testing Stack - -| | Role | Description | -|---|---|---| -| [Cucumber](https://github.com/cucumber-rs/cucumber/tree/main) | Test Runner | Run [Gherkin](https://cucumber.io/docs/gherkin/reference/) specifications as Rust tests | -| [Fantoccini](https://github.com/jonhoo/fantoccini/tree/main) | Browser Client | Interact with web pages through WebDriver | -| [Cargo Leptos ](https://github.com/leptos-rs/cargo-leptos) | Build Tool | Compile example and start the server and end-2-end tests | -| [chromedriver](https://chromedriver.chromium.org/downloads) | WebDriver | Provide WebDriver for Chrome - -## Testing Organization - -Testing is organized around what a user can do and see/not see. Test scenarios are grouped by the **user action** and the **object** of that action. This makes it easier to locate and reason about requirements. - -Here is a brief overview of how things fit together. - -```bash -features -└── {action}_{object}.feature # Specify test scenarios -tests -├── fixtures -│ ├── action.rs # Perform a user action (click, type, etc.) -│ ├── check.rs # Assert what a user can see/not see -│ ├── find.rs # Query page elements -│ ├── mod.rs -│ └── world -│ ├── action_steps.rs # Map Gherkin steps to user actions -│ ├── check_steps.rs # Map Gherkin steps to user expectations -│ └── mod.rs -└── app_suite.rs # Test main -``` diff --git a/examples/server_fns_axum/e2e/features/add_todo.feature b/examples/server_fns_axum/e2e/features/add_todo.feature deleted file mode 100644 index b2a1331ca9..0000000000 --- a/examples/server_fns_axum/e2e/features/add_todo.feature +++ /dev/null @@ -1,16 +0,0 @@ -@add_todo -Feature: Add Todo - - Background: - Given I see the app - - @add_todo-see - Scenario: Should see the todo - Given I set the todo as Buy Bread - When I click the Add button - Then I see the todo named Buy Bread - - @add_todo-style - Scenario: Should see the pending todo - When I add a todo as Buy Oranges - Then I see the pending todo diff --git a/examples/server_fns_axum/e2e/features/delete_todo.feature b/examples/server_fns_axum/e2e/features/delete_todo.feature deleted file mode 100644 index 3c1e743d26..0000000000 --- a/examples/server_fns_axum/e2e/features/delete_todo.feature +++ /dev/null @@ -1,18 +0,0 @@ -@delete_todo -Feature: Delete Todo - - Background: - Given I see the app - - @serial - @delete_todo-remove - Scenario: Should not see the deleted todo - Given I add a todo as Buy Yogurt - When I delete the todo named Buy Yogurt - Then I do not see the todo named Buy Yogurt - - @serial - @delete_todo-message - Scenario: Should see the empty list message - When I empty the todo list - Then I see the empty list message is No tasks were found. \ No newline at end of file diff --git a/examples/server_fns_axum/e2e/features/open_app.feature b/examples/server_fns_axum/e2e/features/open_app.feature deleted file mode 100644 index f4b4e39529..0000000000 --- a/examples/server_fns_axum/e2e/features/open_app.feature +++ /dev/null @@ -1,12 +0,0 @@ -@open_app -Feature: Open App - - @open_app-title - Scenario: Should see the home page title - When I open the app - Then I see the page title is My Tasks - - @open_app-label - Scenario: Should see the input label - When I open the app - Then I see the label of the input is Add a Todo \ No newline at end of file diff --git a/examples/server_fns_axum/e2e/tests/app_suite.rs b/examples/server_fns_axum/e2e/tests/app_suite.rs deleted file mode 100644 index 5c56b6aca8..0000000000 --- a/examples/server_fns_axum/e2e/tests/app_suite.rs +++ /dev/null @@ -1,14 +0,0 @@ -mod fixtures; - -use anyhow::Result; -use cucumber::World; -use fixtures::world::AppWorld; - -#[tokio::main] -async fn main() -> Result<()> { - AppWorld::cucumber() - .fail_on_skipped() - .run_and_exit("./features") - .await; - Ok(()) -} diff --git a/examples/server_fns_axum/e2e/tests/fixtures/action.rs b/examples/server_fns_axum/e2e/tests/fixtures/action.rs deleted file mode 100644 index 79b5c685ee..0000000000 --- a/examples/server_fns_axum/e2e/tests/fixtures/action.rs +++ /dev/null @@ -1,60 +0,0 @@ -use super::{find, world::HOST}; -use anyhow::Result; -use fantoccini::Client; -use std::result::Result::Ok; -use tokio::{self, time}; - -pub async fn goto_path(client: &Client, path: &str) -> Result<()> { - let url = format!("{}{}", HOST, path); - client.goto(&url).await?; - - Ok(()) -} - -pub async fn add_todo(client: &Client, text: &str) -> Result<()> { - fill_todo(client, text).await?; - click_add_button(client).await?; - Ok(()) -} - -pub async fn fill_todo(client: &Client, text: &str) -> Result<()> { - let textbox = find::todo_input(client).await; - textbox.send_keys(text).await?; - - Ok(()) -} - -pub async fn click_add_button(client: &Client) -> Result<()> { - let add_button = find::add_button(client).await; - add_button.click().await?; - - Ok(()) -} - -pub async fn empty_todo_list(client: &Client) -> Result<()> { - let todos = find::todos(client).await; - - for _todo in todos { - let _ = delete_first_todo(client).await?; - } - - Ok(()) -} - -pub async fn delete_first_todo(client: &Client) -> Result<()> { - if let Some(element) = find::first_delete_button(client).await { - element.click().await.expect("Failed to delete todo"); - time::sleep(time::Duration::from_millis(250)).await; - } - - Ok(()) -} - -pub async fn delete_todo(client: &Client, text: &str) -> Result<()> { - if let Some(element) = find::delete_button(client, text).await { - element.click().await?; - time::sleep(time::Duration::from_millis(250)).await; - } - - Ok(()) -} diff --git a/examples/server_fns_axum/e2e/tests/fixtures/check.rs b/examples/server_fns_axum/e2e/tests/fixtures/check.rs deleted file mode 100644 index f43629b95c..0000000000 --- a/examples/server_fns_axum/e2e/tests/fixtures/check.rs +++ /dev/null @@ -1,57 +0,0 @@ -use super::find; -use anyhow::{Ok, Result}; -use fantoccini::{Client, Locator}; -use pretty_assertions::assert_eq; - -pub async fn text_on_element( - client: &Client, - selector: &str, - expected_text: &str, -) -> Result<()> { - let element = client - .wait() - .for_element(Locator::Css(selector)) - .await - .expect( - format!("Element not found by Css selector `{}`", selector) - .as_str(), - ); - - let actual = element.text().await?; - assert_eq!(&actual, expected_text); - - Ok(()) -} - -pub async fn todo_present( - client: &Client, - text: &str, - expected: bool, -) -> Result<()> { - let todo_present = is_todo_present(client, text).await; - - assert_eq!(todo_present, expected); - - Ok(()) -} - -async fn is_todo_present(client: &Client, text: &str) -> bool { - let todos = find::todos(client).await; - - for todo in todos { - let todo_title = todo.text().await.expect("Todo title not found"); - if todo_title == text { - return true; - } - } - - false -} - -pub async fn todo_is_pending(client: &Client) -> Result<()> { - if let None = find::pending_todo(client).await { - assert!(false, "Pending todo not found"); - } - - Ok(()) -} diff --git a/examples/server_fns_axum/e2e/tests/fixtures/find.rs b/examples/server_fns_axum/e2e/tests/fixtures/find.rs deleted file mode 100644 index 228fce6a28..0000000000 --- a/examples/server_fns_axum/e2e/tests/fixtures/find.rs +++ /dev/null @@ -1,63 +0,0 @@ -use fantoccini::{elements::Element, Client, Locator}; - -pub async fn todo_input(client: &Client) -> Element { - let textbox = client - .wait() - .for_element(Locator::Css("input[name='title")) - .await - .expect("Todo textbox not found"); - - textbox -} - -pub async fn add_button(client: &Client) -> Element { - let button = client - .wait() - .for_element(Locator::Css("input[value='Add']")) - .await - .expect(""); - - button -} - -pub async fn first_delete_button(client: &Client) -> Option<Element> { - if let Ok(element) = client - .wait() - .for_element(Locator::Css("li:first-child input[value='X']")) - .await - { - return Some(element); - } - - None -} - -pub async fn delete_button(client: &Client, text: &str) -> Option<Element> { - let selector = format!("//*[text()='{text}']//input[@value='X']"); - if let Ok(element) = - client.wait().for_element(Locator::XPath(&selector)).await - { - return Some(element); - } - - None -} - -pub async fn pending_todo(client: &Client) -> Option<Element> { - if let Ok(element) = - client.wait().for_element(Locator::Css(".pending")).await - { - return Some(element); - } - - None -} - -pub async fn todos(client: &Client) -> Vec<Element> { - let todos = client - .find_all(Locator::Css("li")) - .await - .expect("Todo List not found"); - - todos -} diff --git a/examples/server_fns_axum/e2e/tests/fixtures/mod.rs b/examples/server_fns_axum/e2e/tests/fixtures/mod.rs deleted file mode 100644 index 72b1bd65e4..0000000000 --- a/examples/server_fns_axum/e2e/tests/fixtures/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub mod action; -pub mod check; -pub mod find; -pub mod world; diff --git a/examples/server_fns_axum/e2e/tests/fixtures/world/action_steps.rs b/examples/server_fns_axum/e2e/tests/fixtures/world/action_steps.rs deleted file mode 100644 index 5c4e062dba..0000000000 --- a/examples/server_fns_axum/e2e/tests/fixtures/world/action_steps.rs +++ /dev/null @@ -1,57 +0,0 @@ -use crate::fixtures::{action, world::AppWorld}; -use anyhow::{Ok, Result}; -use cucumber::{given, when}; - -#[given("I see the app")] -#[when("I open the app")] -async fn i_open_the_app(world: &mut AppWorld) -> Result<()> { - let client = &world.client; - action::goto_path(client, "").await?; - - Ok(()) -} - -#[given(regex = "^I add a todo as (.*)$")] -#[when(regex = "^I add a todo as (.*)$")] -async fn i_add_a_todo_titled(world: &mut AppWorld, text: String) -> Result<()> { - let client = &world.client; - action::add_todo(client, text.as_str()).await?; - - Ok(()) -} - -#[given(regex = "^I set the todo as (.*)$")] -async fn i_set_the_todo_as(world: &mut AppWorld, text: String) -> Result<()> { - let client = &world.client; - action::fill_todo(client, &text).await?; - - Ok(()) -} - -#[when(regex = "I click the Add button$")] -async fn i_click_the_button(world: &mut AppWorld) -> Result<()> { - let client = &world.client; - action::click_add_button(client).await?; - - Ok(()) -} - -#[when(regex = "^I delete the todo named (.*)$")] -async fn i_delete_the_todo_named( - world: &mut AppWorld, - text: String, -) -> Result<()> { - let client = &world.client; - action::delete_todo(client, text.as_str()).await?; - - Ok(()) -} - -#[given("the todo list is empty")] -#[when("I empty the todo list")] -async fn i_empty_the_todo_list(world: &mut AppWorld) -> Result<()> { - let client = &world.client; - action::empty_todo_list(client).await?; - - Ok(()) -} diff --git a/examples/server_fns_axum/e2e/tests/fixtures/world/check_steps.rs b/examples/server_fns_axum/e2e/tests/fixtures/world/check_steps.rs deleted file mode 100644 index 3e51215dba..0000000000 --- a/examples/server_fns_axum/e2e/tests/fixtures/world/check_steps.rs +++ /dev/null @@ -1,67 +0,0 @@ -use crate::fixtures::{check, world::AppWorld}; -use anyhow::{Ok, Result}; -use cucumber::then; - -#[then(regex = "^I see the page title is (.*)$")] -async fn i_see_the_page_title_is( - world: &mut AppWorld, - text: String, -) -> Result<()> { - let client = &world.client; - check::text_on_element(client, "h1", &text).await?; - - Ok(()) -} - -#[then(regex = "^I see the label of the input is (.*)$")] -async fn i_see_the_label_of_the_input_is( - world: &mut AppWorld, - text: String, -) -> Result<()> { - let client = &world.client; - check::text_on_element(client, "label", &text).await?; - - Ok(()) -} - -#[then(regex = "^I see the todo named (.*)$")] -async fn i_see_the_todo_is_present( - world: &mut AppWorld, - text: String, -) -> Result<()> { - let client = &world.client; - check::todo_present(client, text.as_str(), true).await?; - - Ok(()) -} - -#[then("I see the pending todo")] -async fn i_see_the_pending_todo(world: &mut AppWorld) -> Result<()> { - let client = &world.client; - - check::todo_is_pending(client).await?; - - Ok(()) -} - -#[then(regex = "^I see the empty list message is (.*)$")] -async fn i_see_the_empty_list_message_is( - world: &mut AppWorld, - text: String, -) -> Result<()> { - let client = &world.client; - check::text_on_element(client, "ul p", &text).await?; - - Ok(()) -} - -#[then(regex = "^I do not see the todo named (.*)$")] -async fn i_do_not_see_the_todo_is_present( - world: &mut AppWorld, - text: String, -) -> Result<()> { - let client = &world.client; - check::todo_present(client, text.as_str(), false).await?; - - Ok(()) -} diff --git a/examples/server_fns_axum/e2e/tests/fixtures/world/mod.rs b/examples/server_fns_axum/e2e/tests/fixtures/world/mod.rs deleted file mode 100644 index c25a925709..0000000000 --- a/examples/server_fns_axum/e2e/tests/fixtures/world/mod.rs +++ /dev/null @@ -1,39 +0,0 @@ -pub mod action_steps; -pub mod check_steps; - -use anyhow::Result; -use cucumber::World; -use fantoccini::{ - error::NewSessionError, wd::Capabilities, Client, ClientBuilder, -}; - -pub const HOST: &str = "http://127.0.0.1:3000"; - -#[derive(Debug, World)] -#[world(init = Self::new)] -pub struct AppWorld { - pub client: Client, -} - -impl AppWorld { - async fn new() -> Result<Self, anyhow::Error> { - let webdriver_client = build_client().await?; - - Ok(Self { - client: webdriver_client, - }) - } -} - -async fn build_client() -> Result<Client, NewSessionError> { - let mut cap = Capabilities::new(); - let arg = serde_json::from_str("{\"args\": [\"-headless\"]}").unwrap(); - cap.insert("goog:chromeOptions".to_string(), arg); - - let client = ClientBuilder::native() - .capabilities(cap) - .connect("http://localhost:4444") - .await?; - - Ok(client) -} diff --git a/examples/server_fns_axum/src/app.rs b/examples/server_fns_axum/src/app.rs index e6a5b5e5ef..e21f6eae40 100644 --- a/examples/server_fns_axum/src/app.rs +++ b/examples/server_fns_axum/src/app.rs @@ -168,15 +168,12 @@ pub fn WithAnAction() -> impl IntoView { <button on:click=move |_| { let text = input_ref.get().unwrap().value(); - action.dispatch(text); + action.dispatch(text.into()); // note: technically, this `action` takes `AddRow` (the server fn type) as its // argument // - // however, `.dispatch()` takes `impl Into<I>`, and for any one-argument server - // functions, `From<_>` is implemented between the server function type and the - // type of this single argument - // - // so `action.dispatch(text)` means `action.dispatch(AddRow { text })` + // however, for any one-argument server functions, `From<_>` is implemented between + // the server function type and the type of this single argument } > Submit diff --git a/examples/sso_auth_axum/Cargo.toml b/examples/sso_auth_axum/Cargo.toml index 14b2bda6c6..b463110b0d 100644 --- a/examples/sso_auth_axum/Cargo.toml +++ b/examples/sso_auth_axum/Cargo.toml @@ -62,6 +62,9 @@ ssr = [ "dep:leptos_axum", ] +[package.metadata.cargo-all-features] +denylist = ["axum", "tower", "tower-http", "tokio", "sqlx", "leptos_axum"] +skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]] [package.metadata.leptos] # The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name diff --git a/examples/sso_auth_axum/Makefile.toml b/examples/sso_auth_axum/Makefile.toml index 15fb0d6317..4855e8a468 100644 --- a/examples/sso_auth_axum/Makefile.toml +++ b/examples/sso_auth_axum/Makefile.toml @@ -1,11 +1,8 @@ -extend = { path = "../cargo-make/main.toml" } +extend = [ + { path = "../cargo-make/main.toml" }, + { path = "../cargo-make/cargo-leptos.toml" }, +] -[tasks.build] -command = "cargo" -args = ["+nightly", "build-all-features"] -install_crate = "cargo-all-features" +[env] -[tasks.check] -command = "cargo" -args = ["+nightly", "check-all-features"] -install_crate = "cargo-all-features" +CLIENT_PROCESS_NAME = "sso_auth_axum" diff --git a/leptos_server/src/action.rs b/leptos_server/src/action.rs index f50de6c628..b5ca4e5d12 100644 --- a/leptos_server/src/action.rs +++ b/leptos_server/src/action.rs @@ -93,8 +93,8 @@ where any(debug_assertions, feature = "ssr"), tracing::instrument(level = "trace", skip_all,) )] - pub fn dispatch(&self, input: impl Into<I>) { - self.0.with_value(|a| a.dispatch(input.into())) + pub fn dispatch(&self, input: I) { + self.0.with_value(|a| a.dispatch(input)) } /// Create an [Action]. diff --git a/leptos_server/src/multi_action.rs b/leptos_server/src/multi_action.rs index 232e36b64c..d1deb7e718 100644 --- a/leptos_server/src/multi_action.rs +++ b/leptos_server/src/multi_action.rs @@ -321,7 +321,7 @@ where /// Creates an [MultiAction] that can be used to call a server function. /// -/// ```rust +/// ```rust,ignore /// # use leptos::*; /// /// #[server(MyServerFn)] diff --git a/server_fn/Cargo.toml b/server_fn/Cargo.toml index ce226d1bf5..57e68b7033 100644 --- a/server_fn/Cargo.toml +++ b/server_fn/Cargo.toml @@ -105,3 +105,8 @@ ssr = ["inventory"] [package.metadata.docs.rs] all-features = true + +# disables some feature combos for testing in CI +[package.metadata.cargo-all-features] +denylist = ["rustls", "default-tls"] +skip_feature_sets = [["actix", "axum"], ["browser", "actix"], ["browser", "axum"]] diff --git a/server_fn/src/codec/mod.rs b/server_fn/src/codec/mod.rs index 94faa8b34b..cfded1d792 100644 --- a/server_fn/src/codec/mod.rs +++ b/server_fn/src/codec/mod.rs @@ -60,7 +60,7 @@ pub use stream::*; /// /// For example, here’s the implementation for [`Json`]. /// -/// ```rust +/// ```rust,ignore /// impl<CustErr, T, Request> IntoReq<Json, Request, CustErr> for T /// where /// Request: ClientReq<CustErr>, @@ -98,7 +98,7 @@ pub trait IntoReq<Encoding, Request, CustErr> { /// /// For example, here’s the implementation for [`Json`]. /// -/// ```rust +/// ```rust,ignore /// impl<CustErr, T, Request> FromReq<Json, Request, CustErr> for T /// where /// // require the Request implement `Req` @@ -137,7 +137,7 @@ where /// /// For example, here’s the implementation for [`Json`]. /// -/// ```rust +/// ```rust,ignore /// impl<CustErr, T, Response> IntoRes<Json, Response, CustErr> for T /// where /// Response: Res<CustErr>, @@ -170,7 +170,7 @@ pub trait IntoRes<Encoding, Response, CustErr> { /// /// For example, here’s the implementation for [`Json`]. /// -/// ```rust +/// ```rust,ignore /// impl<CustErr, T, Response> FromRes<Json, Response, CustErr> for T /// where /// Response: ClientRes<CustErr> + Send, diff --git a/server_fn_macro/src/lib.rs b/server_fn_macro/src/lib.rs index d2b4a8cbcc..a14428f8a5 100644 --- a/server_fn_macro/src/lib.rs +++ b/server_fn_macro/src/lib.rs @@ -416,12 +416,12 @@ pub fn server_macro_impl( } else if let Some(req_ty) = preset_req { req_ty.to_token_stream() } else { - return Err(syn::Error::new( - Span::call_site(), - "If the `ssr` feature is enabled, either the `actix` or `axum` \ - features should also be enabled, or the `req = ` argument should \ - be provided to specify the request type.", - )); + // fall back to the browser version, to avoid erroring out + // in things like doctests + // in reality, one of the above needs to be set + quote! { + #server_fn_path::request::BrowserMockReq + } }; let res = if !cfg!(feature = "ssr") { quote! { @@ -440,12 +440,12 @@ pub fn server_macro_impl( } else if let Some(res_ty) = preset_res { res_ty.to_token_stream() } else { - return Err(syn::Error::new( - Span::call_site(), - "If the `ssr` feature is enabled, either the `actix` or `axum` \ - features should also be enabled, or the `res = ` argument should \ - be provided to specify the response type.", - )); + // fall back to the browser version, to avoid erroring out + // in things like doctests + // in reality, one of the above needs to be set + quote! { + #server_fn_path::response::BrowserMockRes + } }; // Remove any leading slashes, even if they exist (we'll add them below) From 8df46fcdb947f0e7a68a6c4b5beb11e3430144c3 Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Sat, 20 Jan 2024 12:39:16 -0500 Subject: [PATCH 096/100] examples: use old Axum version of `hackernews_js_fetch` until supported by `axum-js-fetch` --- examples/hackernews_js_fetch/Cargo.toml | 29 +++++++++++-------------- examples/hackernews_js_fetch/src/lib.rs | 7 +++--- 2 files changed, 17 insertions(+), 19 deletions(-) diff --git a/examples/hackernews_js_fetch/Cargo.toml b/examples/hackernews_js_fetch/Cargo.toml index fd582a0e7b..1d62fdf307 100644 --- a/examples/hackernews_js_fetch/Cargo.toml +++ b/examples/hackernews_js_fetch/Cargo.toml @@ -11,25 +11,22 @@ codegen-units = 1 lto = true [dependencies] -console_log = "1.0" -console_error_panic_hook = "0.1" -#leptos = { version = "0.5",/leptos", features = ["nightly"] } -#leptos_axum = { version = "0.5",/integrations/axum", default-features = false, optional = true } -#leptos_meta = { version = "0.5",/meta", features = ["nightly"] } -#leptos_router = { version = "0.5",/router", features = ["nightly"] } +console_log = "1.0.0" +console_error_panic_hook = "0.1.7" +cfg-if = "1.0.0" leptos = { version = "0.5", features = ["nightly"] } leptos_axum = { version = "0.5", default-features = false, optional = true } leptos_meta = { version = "0.5", features = ["nightly"] } leptos_router = { version = "0.5", features = ["nightly"] } -log = "0.4" -simple_logger = "4.0" -serde = { version = "1.0", features = ["derive"] } +log = "0.4.17" +simple_logger = "4.0.0" +serde = { version = "1.0.148", features = ["derive"] } tracing = "0.1" -gloo-net = { version = "0.4", features = ["http"] } -reqwest = { version = "0.11", features = ["json"] } -axum = { version = "0.7", default-features = false, optional = true } -tower = { version = "0.4", optional = true } -http = { version = "1.0", optional = true } +gloo-net = { version = "0.4.0", features = ["http"] } +reqwest = { version = "0.11.13", features = ["json"] } +axum = { version = "0.6", default-features = false, optional = true } +tower = { version = "0.4.13", optional = true } +http = { version = "0.2.11", optional = true } web-sys = { version = "0.3", features = [ "AbortController", "AbortSignal", @@ -37,10 +34,10 @@ web-sys = { version = "0.3", features = [ "Response", ] } wasm-bindgen = "0.2" -wasm-bindgen-futures = { version = "0.4", features = [ +wasm-bindgen-futures = { version = "0.4.37", features = [ "futures-core-03-stream", ], optional = true } -axum-js-fetch = { version = "0.2", optional = true } +axum-js-fetch = { version = "0.2.1", optional = true } lazy_static = "1.4.0" [features] diff --git a/examples/hackernews_js_fetch/src/lib.rs b/examples/hackernews_js_fetch/src/lib.rs index 3a262b8995..658330fd1c 100644 --- a/examples/hackernews_js_fetch/src/lib.rs +++ b/examples/hackernews_js_fetch/src/lib.rs @@ -40,7 +40,7 @@ pub fn hydrate() { #[cfg(feature = "ssr")] mod ssr_imports { use crate::App; - use axum::Router; + use axum::{routing::post, Router}; use leptos::*; use leptos_axum::{generate_route_list, LeptosRoutes}; use log::{info, Level}; @@ -62,8 +62,9 @@ mod ssr_imports { let routes = generate_route_list(App); // build our application with a route - let app: axum::Router = Router::new() - .leptos_routes(&leptos_options, routes, App) + let app: axum::Router<(), axum::body::Body> = Router::new() + .leptos_routes(&leptos_options, routes, || view! { <App/> }) + .route("/api/*fn_name", post(leptos_axum::handle_server_fns)) .with_state(leptos_options); info!("creating handler instance"); From 40da1fe94e01190d994c59078d5c4f5f5544bafa Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Sat, 20 Jan 2024 14:16:13 -0500 Subject: [PATCH 097/100] clippy --- examples/hackernews_js_fetch/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/hackernews_js_fetch/src/lib.rs b/examples/hackernews_js_fetch/src/lib.rs index 658330fd1c..5beb08b00a 100644 --- a/examples/hackernews_js_fetch/src/lib.rs +++ b/examples/hackernews_js_fetch/src/lib.rs @@ -52,7 +52,7 @@ mod ssr_imports { #[wasm_bindgen] impl Handler { pub async fn new() -> Self { - console_log::init_with_level(Level::Debug); + _ = console_log::init_with_level(Level::Debug); console_error_panic_hook::set_once(); let leptos_options = LeptosOptions::builder() From c88bfbe0a0e9ac6cf4fc30d7bfcf25684f1da0a5 Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Sat, 20 Jan 2024 14:18:25 -0500 Subject: [PATCH 098/100] tweak sets of features for CI --- server_fn/Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server_fn/Cargo.toml b/server_fn/Cargo.toml index 57e68b7033..7e693418f4 100644 --- a/server_fn/Cargo.toml +++ b/server_fn/Cargo.toml @@ -108,5 +108,5 @@ all-features = true # disables some feature combos for testing in CI [package.metadata.cargo-all-features] -denylist = ["rustls", "default-tls"] -skip_feature_sets = [["actix", "axum"], ["browser", "actix"], ["browser", "axum"]] +denylist = ["rustls", "default-tls", "form-redirects"] +skip_feature_sets = [["actix", "axum"], ["browser", "actix"], ["browser", "axum"], ["browser", "reqwest"]] From 39f5481b8cd69fd1cd001018c58ab083f6b51059 Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Sat, 20 Jan 2024 14:29:08 -0500 Subject: [PATCH 099/100] clean up in docs and rename Axum extract() to match Actix extract() --- integrations/axum/src/lib.rs | 85 ++++++++++++++++++------------------ 1 file changed, 43 insertions(+), 42 deletions(-) diff --git a/integrations/axum/src/lib.rs b/integrations/axum/src/lib.rs index 383150fd31..50e223c826 100644 --- a/integrations/axum/src/lib.rs +++ b/integrations/axum/src/lib.rs @@ -164,7 +164,7 @@ pub fn generate_request_and_parts( } /// An Axum handlers to listens for a request with Leptos server function arguments in the body, -/// run the server function if found, and return the resulting [Response]. +/// run the server function if found, and return the resulting [`Response`]. /// /// This can then be set up at an appropriate route in your application: /// @@ -224,7 +224,7 @@ macro_rules! spawn_task { } /// An Axum handlers to listens for a request with Leptos server function arguments in the body, -/// run the server function if found, and return the resulting [Response]. +/// run the server function if found, and return the resulting [`Response`]. /// /// This can then be set up at an appropriate route in your application: /// @@ -234,15 +234,16 @@ macro_rules! spawn_task { /// of one that should work much like this one. /// /// **NOTE**: If your server functions expect a context, make sure to provide it both in -/// [`handle_server_fns_with_context`] **and** in [`leptos_routes_with_context`] (or whatever +/// [`handle_server_fns_with_context`] **and** in +/// [`leptos_routes_with_context`](LeptosRoutes::leptos_routes_with_context) (or whatever /// rendering method you are using). During SSR, server functions are called by the rendering /// method, while subsequent calls from the client are handled by the server function handler. /// The same context needs to be provided to both handlers. /// /// ## Provided Context Types /// This function always provides context values including the following types: -/// - [RequestParts] -/// - [ResponseOptions] +/// - [`Parts`] +/// - [`ResponseOptions`] #[tracing::instrument(level = "trace", fields(error), skip_all)] pub async fn handle_server_fns_with_context( additional_context: impl Fn() + 'static + Clone + Send, @@ -383,10 +384,10 @@ pub type PinnedHtmlStream = /// /// ## Provided Context Types /// This function always provides context values including the following types: -/// - [RequestParts] -/// - [ResponseOptions] -/// - [MetaContext](leptos_meta::MetaContext) -/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext) +/// - [`Parts`] +/// - [`ResponseOptions`] +/// - [`MetaContext`](leptos_meta::MetaContext) +/// - [`RouterIntegrationContext`](leptos_router::RouterIntegrationContext) #[tracing::instrument(level = "trace", fields(error), skip_all)] pub fn render_app_to_stream<IV>( options: LeptosOptions, @@ -474,10 +475,10 @@ where /// /// ## Provided Context Types /// This function always provides context values including the following types: -/// - [Parts] -/// - [ResponseOptions] -/// - [MetaContext](leptos_meta::MetaContext) -/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext) +/// - [`Parts`] +/// - [`ResponseOptions`] +/// - [`MetaContext`](leptos_meta::MetaContext) +/// - [`RouterIntegrationContext`](leptos_router::RouterIntegrationContext) #[tracing::instrument(level = "trace", fields(error), skip_all)] pub fn render_app_to_stream_in_order<IV>( options: LeptosOptions, @@ -515,10 +516,10 @@ where /// /// ## Provided Context Types /// This function always provides context values including the following types: -/// - [Parts] -/// - [ResponseOptions] -/// - [MetaContext](leptos_meta::MetaContext) -/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext) +/// - [`Parts`] +/// - [`ResponseOptions`] +/// - [`MetaContext`](leptos_meta::MetaContext) +/// - [`RouterIntegrationContext`](leptos_router::RouterIntegrationContext) #[tracing::instrument(level = "trace", fields(error), skip_all)] pub fn render_app_to_stream_with_context<IV>( options: LeptosOptions, @@ -626,10 +627,10 @@ where /// /// ## Provided Context Types /// This function always provides context values including the following types: -/// - [Parts] -/// - [ResponseOptions] -/// - [MetaContext](leptos_meta::MetaContext) -/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext) +/// - [`Parts`] +/// - [`ResponseOptions`] +/// - [`MetaContext`](leptos_meta::MetaContext) +/// - [`RouterIntegrationContext`](leptos_router::RouterIntegrationContext) #[tracing::instrument(level = "trace", fields(error), skip_all)] pub fn render_app_to_stream_with_context_and_replace_blocks<IV>( options: LeptosOptions, @@ -787,10 +788,10 @@ async fn forward_stream( /// /// ## Provided Context Types /// This function always provides context values including the following types: -/// - [Parts] -/// - [ResponseOptions] -/// - [MetaContext](leptos_meta::MetaContext) -/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext) +/// - [`Parts`] +/// - [`ResponseOptions`] +/// - [`MetaContext`](leptos_meta::MetaContext) +/// - [`RouterIntegrationContext`](leptos_router::RouterIntegrationContext) #[tracing::instrument(level = "trace", fields(error), skip_all)] pub fn render_app_to_stream_in_order_with_context<IV>( options: LeptosOptions, @@ -917,10 +918,10 @@ fn provide_contexts( /// /// ## Provided Context Types /// This function always provides context values including the following types: -/// - [Parts] -/// - [ResponseOptions] -/// - [MetaContext](leptos_meta::MetaContext) -/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext) +/// - [`Parts`] +/// - [`ResponseOptions`] +/// - [`MetaContext`](leptos_meta::MetaContext) +/// - [`RouterIntegrationContext`](leptos_router::RouterIntegrationContext) #[tracing::instrument(level = "trace", fields(error), skip_all)] pub fn render_app_async<IV>( options: LeptosOptions, @@ -959,10 +960,10 @@ where /// /// ## Provided Context Types /// This function always provides context values including the following types: -/// - [Parts] -/// - [ResponseOptions] -/// - [MetaContext](leptos_meta::MetaContext) -/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext) +/// - [`Parts`] +/// - [`ResponseOptions`] +/// - [`MetaContext`](leptos_meta::MetaContext) +/// - [`RouterIntegrationContext`](leptos_router::RouterIntegrationContext) #[tracing::instrument(level = "trace", fields(error), skip_all)] pub fn render_app_async_stream_with_context<IV>( options: LeptosOptions, @@ -1088,10 +1089,10 @@ where /// /// ## Provided Context Types /// This function always provides context values including the following types: -/// - [Parts] -/// - [ResponseOptions] -/// - [MetaContext](leptos_meta::MetaContext) -/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext) +/// - [`Parts`] +/// - [`ResponseOptions`] +/// - [`MetaContext`](leptos_meta::MetaContext) +/// - [`RouterIntegrationContext`](leptos_router::RouterIntegrationContext) #[tracing::instrument(level = "trace", fields(error), skip_all)] pub fn render_app_async_with_context<IV>( options: LeptosOptions, @@ -1766,18 +1767,18 @@ fn get_leptos_pool() -> LocalPoolHandle { /// pub async fn query_extract() -> Result<MyQuery, ServerFnError> { /// use axum::{extract::Query, http::Method}; /// use leptos_axum::*; -/// let Query(query) = extractor().await?; +/// let Query(query) = extract().await?; /// /// Ok(query) /// } /// ``` -pub async fn extractor<T, CustErr>() -> Result<T, ServerFnError> +pub async fn extract<T, CustErr>() -> Result<T, ServerFnError> where T: Sized + FromRequestParts<()>, T::Rejection: Debug, CustErr: Error + 'static, { - extractor_with_state::<T, (), CustErr>(&()).await + extract_with_state::<T, (), CustErr>(&()).await } /// A helper to make it easier to use Axum extractors in server functions. This @@ -1794,12 +1795,12 @@ where /// pub async fn query_extract() -> Result<MyQuery, ServerFnError> { /// use axum::{extract::Query, http::Method}; /// use leptos_axum::*; -/// let Query(query) = extractor().await?; +/// let Query(query) = extract().await?; /// /// Ok(query) /// } /// ``` -pub async fn extractor_with_state<T, S, CustErr>( +pub async fn extract_with_state<T, S, CustErr>( state: &S, ) -> Result<T, ServerFnError> where From 567644df8ff024a292e4edda1ea2cf55673e9832 Mon Sep 17 00:00:00 2001 From: Greg Johnston <greg.johnston@gmail.com> Date: Sat, 20 Jan 2024 14:29:22 -0500 Subject: [PATCH 100/100] clarify docs here --- integrations/actix/src/lib.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/integrations/actix/src/lib.rs b/integrations/actix/src/lib.rs index e49c85c50f..da756fe275 100644 --- a/integrations/actix/src/lib.rs +++ b/integrations/actix/src/lib.rs @@ -1369,8 +1369,7 @@ impl LeptosRoutes for &mut ServiceConfig { } } -/// A helper to make it easier to use Axum extractors in server functions, with a -/// simpler API than [`extract()`]. +/// A helper to make it easier to use Axum extractors in server functions. /// /// It is generic over some type `T` that implements [`FromRequest`] and can /// therefore be used in an extractor. The compiler can often infer this type. @@ -1383,7 +1382,11 @@ impl LeptosRoutes for &mut ServiceConfig { /// pub async fn query_extract() -> Result<MyQuery, ServerFnError> { /// use actix_web::web::Query; /// use leptos_actix::*; +/// /// let Query(data) = extract().await?; +/// +/// // do something with the data +/// /// Ok(data) /// } /// ```