From c6992c7032474eaeda4bb8371cf64d22dada2981 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Sat, 1 Apr 2023 17:00:12 -0500 Subject: [PATCH] make server function API more flexible --- packages/server/Cargo.toml | 3 +- packages/server/server-macro/Cargo.toml | 3 +- packages/server/server-macro/src/lib.rs | 14 ++-- packages/server/src/adapters/axum_adapter.rs | 81 +++++++++++++------ packages/server/src/adapters/salvo_adapter.rs | 69 ++++++++++++---- packages/server/src/adapters/warp_adapter.rs | 69 +++++++++++----- packages/server/src/lib.rs | 8 +- packages/server/src/server_context.rs | 35 ++++++++ packages/server/src/server_fn.rs | 3 +- 9 files changed, 209 insertions(+), 76 deletions(-) create mode 100644 packages/server/src/server_context.rs diff --git a/packages/server/Cargo.toml b/packages/server/Cargo.toml index fa0e0592a..2485a2529 100644 --- a/packages/server/Cargo.toml +++ b/packages/server/Cargo.toml @@ -6,7 +6,7 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -server_fn = { version = "0.2.4", features = ["stable"] } +server_fn = { path = "D:/Users/Desktop/github/leptos/server_fn", features = ["stable"] } server_macro = { path = "server-macro" } # warp @@ -31,6 +31,7 @@ once_cell = "1.17.1" thiserror = "1.0.40" tokio = { version = "1.27.0", features = ["full"], optional = true } object-pool = "0.5.4" +anymap = "0.12.1" [features] default = [] diff --git a/packages/server/server-macro/Cargo.toml b/packages/server/server-macro/Cargo.toml index cb285f517..e5fb6523e 100644 --- a/packages/server/server-macro/Cargo.toml +++ b/packages/server/server-macro/Cargo.toml @@ -7,7 +7,8 @@ edition = "2021" [dependencies] quote = "1.0.26" -server_fn_macro = { version = "0.2.4", features = ["stable"] } +# server_fn_macro = { git = "https://github.com/leptos-rs/leptos", rev = "1e037ecb60965c7c55fd781fdc8de7863ffd102b", features = ["stable"] } +server_fn_macro = { path = "D:/Users/Desktop/github/leptos/server_fn_macro", features = ["stable"] } syn = { version = "1", features = ["full"] } [lib] diff --git a/packages/server/server-macro/src/lib.rs b/packages/server/server-macro/src/lib.rs index f2a2e1665..f6b4f92a2 100644 --- a/packages/server/server-macro/src/lib.rs +++ b/packages/server/server-macro/src/lib.rs @@ -2,7 +2,7 @@ use proc_macro::TokenStream; use quote::ToTokens; use server_fn_macro::*; -/// Declares that a function is a [server function](leptos_server). This means that +/// Declares that a function is a [server function](dioxus_server). This means that /// its body will only run on the server, i.e., when the `ssr` feature is enabled. /// /// If you call a server function from the client (i.e., when the `csr` or `hydrate` features @@ -19,18 +19,18 @@ use server_fn_macro::*; /// work without WebAssembly, the encoding must be `"Url"`. /// /// The server function itself can take any number of arguments, each of which should be serializable -/// and deserializable with `serde`. Optionally, its first argument can be a Leptos [Scope](leptos_reactive::Scope), +/// and deserializable with `serde`. Optionally, its first argument can be a [DioxusServerContext](dioxus_server::prelude::DioxusServerContext), /// which will be injected *on the server side.* This can be used to inject the raw HTTP request or other /// server-side context into the server function. /// /// ```ignore -/// # use leptos::*; use serde::{Serialize, Deserialize}; +/// # use dioxus_server::prelude::*; 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!() +/// todo!() /// } /// ``` /// @@ -42,7 +42,7 @@ use server_fn_macro::*; /// - **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 be [Serializable](leptos_reactive::Serializable).** +/// - **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) @@ -50,8 +50,8 @@ use server_fn_macro::*; /// They are serialized as an `application/x-www-form-urlencoded` /// form data using [`serde_urlencoded`](https://docs.rs/serde_urlencoded/latest/serde_urlencoded/) or as `application/cbor` /// using [`cbor`](https://docs.rs/cbor/latest/cbor/). -/// - **The [Scope](leptos_reactive::Scope) comes from the server.** Optionally, the first argument of a server function -/// can be a Leptos [Scope](leptos_reactive::Scope). This scope can be used to inject dependencies like the HTTP request +/// - **The [DioxusServerContext](dioxus_server::prelude::DioxusServerContext) comes from the server.** Optionally, the first argument of a server function +/// can be a [DioxusServerContext](dioxus_server::prelude::DioxusServerContext). This scope can be used to inject dependencies like the HTTP request /// or response or other server-only dependencies, but it does *not* have access to reactive state that exists in the client. #[proc_macro_attribute] pub fn server(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream { diff --git a/packages/server/src/adapters/axum_adapter.rs b/packages/server/src/adapters/axum_adapter.rs index 5eaff9bfc..8fb98b6ec 100644 --- a/packages/server/src/adapters/axum_adapter.rs +++ b/packages/server/src/adapters/axum_adapter.rs @@ -2,7 +2,8 @@ use std::{error::Error, sync::Arc}; use axum::{ body::{self, Body, BoxBody, Full}, - extract::{FromRef, State}, + extract::State, + handler::Handler, http::{HeaderMap, Request, Response, StatusCode}, response::IntoResponse, routing::{get, post}, @@ -14,61 +15,89 @@ use tokio::task::spawn_blocking; use crate::{ render::SSRState, serve::ServeConfig, - server_fn::{DioxusServerContext, DioxusServerFnRegistry, ServerFnTraitObj}, + server_context::DioxusServerContext, + server_fn::{DioxusServerFnRegistry, ServerFnTraitObj}, }; -pub trait DioxusRouterExt { +pub trait DioxusRouterExt { + fn register_server_fns_with_handler( + self, + server_fn_route: &'static str, + handler: impl Fn(Arc) -> H, + ) -> Self + where + H: Handler, + T: 'static, + S: Clone + Send + Sync + 'static; fn register_server_fns(self, server_fn_route: &'static str) -> Self; + fn serve_dioxus_application( self, - cfg: ServeConfig

, - server_fn_route: Option<&'static str>, + server_fn_route: &'static str, + cfg: impl Into>, ) -> Self; } -impl DioxusRouterExt for Router +impl DioxusRouterExt for Router where - SSRState: FromRef, S: Send + Sync + Clone + 'static, { - fn register_server_fns(self, server_fn_route: &'static str) -> Self { + fn register_server_fns_with_handler( + self, + server_fn_route: &'static str, + mut handler: impl FnMut(Arc) -> H, + ) -> Self + where + H: Handler, + T: 'static, + S: Clone + Send + Sync + 'static, + { let mut router = self; for server_fn_path in DioxusServerFnRegistry::paths_registered() { let func = DioxusServerFnRegistry::get(server_fn_path).unwrap(); let full_route = format!("{server_fn_route}/{server_fn_path}"); - router = router.route( - &full_route, - post(move |headers: HeaderMap, body: Request| async move { - server_fn_handler(DioxusServerContext {}, func.clone(), headers, body).await - }), - ); + router = router.route(&full_route, post(handler(func))); } router } + fn register_server_fns(self, server_fn_route: &'static str) -> Self { + self.register_server_fns_with_handler(server_fn_route, |func| { + move |headers: HeaderMap, body: Request| async move { + server_fn_handler(DioxusServerContext::default(), func.clone(), headers, body).await + } + }) + } + fn serve_dioxus_application( self, - cfg: ServeConfig

, - server_fn_route: Option<&'static str>, + server_fn_route: &'static str, + cfg: impl Into>, ) -> Self { use tower_http::services::ServeDir; - // Serve the dist folder and the index.html file - let serve_dir = ServeDir::new("dist"); + let cfg = cfg.into(); - self.register_server_fns(server_fn_route.unwrap_or_default()) - .route( + // Serve the dist folder and the index.html file + let serve_dir = ServeDir::new(cfg.assets_path); + + self.register_server_fns(server_fn_route) + .nest_service("/assets", serve_dir) + .route_service( "/", - get(move |State(ssr_state): State| { - let rendered = ssr_state.render(&cfg); - async move { Full::from(rendered) } - }), + get(render_handler).with_state((cfg, SSRState::default())), ) - .fallback_service(serve_dir) } } -async fn server_fn_handler( +async fn render_handler( + State((cfg, ssr_state)): State<(ServeConfig

, SSRState)>, +) -> impl IntoResponse { + let rendered = ssr_state.render(&cfg); + Full::from(rendered) +} + +pub async fn server_fn_handler( server_context: DioxusServerContext, function: Arc, headers: HeaderMap, diff --git a/packages/server/src/adapters/salvo_adapter.rs b/packages/server/src/adapters/salvo_adapter.rs index f381e0b4a..0d8a27b22 100644 --- a/packages/server/src/adapters/salvo_adapter.rs +++ b/packages/server/src/adapters/salvo_adapter.rs @@ -9,43 +9,65 @@ use server_fn::{Payload, ServerFunctionRegistry}; use tokio::task::spawn_blocking; use crate::{ - dioxus_ssr_html, + prelude::DioxusServerContext, + prelude::SSRState, serve::ServeConfig, - server_fn::{DioxusServerContext, DioxusServerFnRegistry, ServerFnTraitObj}, + server_fn::{DioxusServerFnRegistry, ServerFnTraitObj}, }; pub trait DioxusRouterExt { fn register_server_fns(self, server_fn_route: &'static str) -> Self; + fn register_server_fns_with_handler( + self, + server_fn_route: &'static str, + handler: impl Fn(Arc) -> H, + ) -> Self + where + H: Handler + 'static; fn serve_dioxus_application( self, - cfg: ServeConfig

, + server_fn_path: &'static str, + cfg: impl Into>, ) -> Self; } impl DioxusRouterExt for Router { - fn register_server_fns(self, server_fn_route: &'static str) -> Self { + fn register_server_fns_with_handler( + self, + server_fn_route: &'static str, + mut handler: impl FnMut(Arc) -> H, + ) -> Self + where + H: Handler + 'static, + { let mut router = self; for server_fn_path in DioxusServerFnRegistry::paths_registered() { let func = DioxusServerFnRegistry::get(server_fn_path).unwrap(); let full_route = format!("{server_fn_route}/{server_fn_path}"); - router = router.push(Router::with_path(&full_route).post(ServerFnHandler { - server_context: DioxusServerContext {}, - function: func, - })); + router = router.push(Router::with_path(&full_route).post(handler(func))); } router } + fn register_server_fns(self, server_fn_route: &'static str) -> Self { + self.register_server_fns_with_handler(|| ServerFnHandler { + server_context: DioxusServerContext::default(), + function: func, + }) + } + fn serve_dioxus_application( self, - cfg: ServeConfig

, + server_fn_route: &'static str, + cfg: impl Into>, ) -> Self { + let cfg = cfg.into(); // Serve the dist folder and the index.html file - let serve_dir = StaticDir::new(["dist"]); + let serve_dir = StaticDir::new([cfg.assets_path]); - self.register_server_fns(cfg.server_fn_route.unwrap_or_default()) + self.register_server_fns(server_fn_route) .push(Router::with_path("/").get(SSRHandler { cfg })) - .push(Router::with_path("<**path>").get(serve_dir)) + .push(Router::with_path("assets/<**path>").get(serve_dir)) } } @@ -58,19 +80,36 @@ impl Handler for SSRHandler

{ async fn handle( &self, _req: &mut Request, - _depot: &mut Depot, + depot: &mut Depot, res: &mut Response, _flow: &mut FlowCtrl, ) { - res.write_body(dioxus_ssr_html(&self.cfg)).unwrap(); + // Get the SSR renderer from the depot or create a new one if it doesn't exist + let renderer_pool = if let Some(renderer) = depot.obtain::() { + renderer.clone() + } else { + let renderer = SSRState::default(); + depot.inject(renderer.clone()); + renderer + }; + res.write_body(renderer_pool.render(&self.cfg)).unwrap(); } } -struct ServerFnHandler { +pub struct ServerFnHandler { server_context: DioxusServerContext, function: Arc, } +impl ServerFnHandler { + pub fn new(server_context: DioxusServerContext, function: Arc) -> Self { + Self { + server_context, + function, + } + } +} + #[handler] impl ServerFnHandler { async fn handle(&self, req: &mut Request, _depot: &mut Depot, res: &mut Response) { diff --git a/packages/server/src/adapters/warp_adapter.rs b/packages/server/src/adapters/warp_adapter.rs index 2577de61f..00ebb38fa 100644 --- a/packages/server/src/adapters/warp_adapter.rs +++ b/packages/server/src/adapters/warp_adapter.rs @@ -10,50 +10,75 @@ use warp::{ }; use crate::{ - dioxus_ssr_html, + prelude::{DioxusServerContext, SSRState}, serve::ServeConfig, - server_fn::{DioxusServerContext, DioxusServerFnRegistry, ServerFnTraitObj}, + server_fn::{DioxusServerFnRegistry, ServerFnTraitObj}, }; -pub fn register_server_fns(server_fn_route: &'static str) -> BoxedFilter<(impl Reply,)> { - let mut filter: Option> = None; +pub fn register_server_fns_with_handler( + server_fn_route: &'static str, + mut handler: H, +) -> BoxedFilter<(R,)> +where + H: FnMut(String, Arc) -> F, + F: Filter + Send + Sync + 'static, + F::Extract: Send, + R: Reply + 'static, +{ + let mut filter: Option> = None; for server_fn_path in DioxusServerFnRegistry::paths_registered() { let func = DioxusServerFnRegistry::get(server_fn_path).unwrap(); let full_route = format!("{server_fn_route}/{server_fn_path}") .trim_start_matches('/') .to_string(); - let route = path(full_route) - .and(warp::post()) - .and(warp::header::headers_cloned()) - .and(warp::body::bytes()) - .and_then(move |headers: HeaderMap, body| { - let func = func.clone(); - async move { server_fn_handler(DioxusServerContext {}, func, headers, body).await } - }) - .boxed(); + let route = handler(full_route, func.clone()).boxed(); if let Some(boxed_filter) = filter.take() { filter = Some(boxed_filter.or(route).unify().boxed()); } else { - filter = Some(route.boxed()); + filter = Some(route); } } filter.expect("No server functions found") } -pub fn serve_dioxus_application( - cfg: ServeConfig

, -) -> BoxedFilter<(impl Reply,)> { - // Serve the dist folder and the index.html file - let serve_dir = warp::fs::dir("./dist"); +pub fn register_server_fns(server_fn_route: &'static str) -> BoxedFilter<(impl Reply,)> { + register_server_fns_with_handler(server_fn_route, |full_route, func| { + path(full_route) + .and(warp::post()) + .and(warp::header::headers_cloned()) + .and(warp::body::bytes()) + .and_then(move |headers: HeaderMap, body| { + let func = func.clone(); + async move { + server_fn_handler(DioxusServerContext::default(), func, headers, body).await + } + }) + }) +} - register_server_fns(cfg.server_fn_route.unwrap_or_default()) +pub fn serve_dioxus_application( + server_fn_route: &'static str, + cfg: impl Into>, +) -> BoxedFilter<(impl Reply,)> { + let cfg = cfg.into(); + // Serve the dist folder and the index.html file + let serve_dir = warp::fs::dir(cfg.assets_path); + + register_server_fns(server_fn_route) .or(warp::path::end() .and(warp::get()) - .map(move || warp::reply::html(dioxus_ssr_html(&cfg)))) - .or(serve_dir) + .and(with_ssr_state()) + .map(move |renderer: SSRState| warp::reply::html(renderer.render(&cfg)))) + .or(warp::path("assets").and(serve_dir)) .boxed() } +fn with_ssr_state() -> impl Filter + Clone +{ + let renderer = SSRState::default(); + warp::any().map(move || renderer.clone()) +} + #[derive(Debug)] struct FailedToReadBody(String); diff --git a/packages/server/src/lib.rs b/packages/server/src/lib.rs index 9141c7199..a9c8e4939 100644 --- a/packages/server/src/lib.rs +++ b/packages/server/src/lib.rs @@ -6,6 +6,7 @@ mod adapters; pub mod render; #[cfg(feature = "ssr")] mod serve; +mod server_context; mod server_fn; pub mod prelude { @@ -18,8 +19,11 @@ pub mod prelude { #[cfg(feature = "ssr")] pub use crate::render::*; #[cfg(feature = "ssr")] - pub use crate::serve::ServeConfig; - pub use crate::server_fn::{DioxusServerContext, ServerFn}; + pub use crate::serve::{ServeConfig, ServeConfigBuilder}; + pub use crate::server_context::DioxusServerContext; + pub use crate::server_fn::ServerFn; + #[cfg(feature = "ssr")] + pub use crate::server_fn::ServerFnTraitObj; pub use server_fn::{self, ServerFn as _, ServerFnError}; pub use server_macro::*; } diff --git a/packages/server/src/server_context.rs b/packages/server/src/server_context.rs new file mode 100644 index 000000000..427ee21f8 --- /dev/null +++ b/packages/server/src/server_context.rs @@ -0,0 +1,35 @@ +use std::sync::{Arc, PoisonError, RwLock, RwLockWriteGuard}; + +use anymap::{any::Any, Map}; + +type SendSyncAnyMap = Map; + +/// A shared context for server functions. This allows you to pass data between your server and the server functions like authentication session data. +#[derive(Clone)] +pub struct DioxusServerContext { + shared_context: Arc>, +} + +impl Default for DioxusServerContext { + fn default() -> Self { + Self { + shared_context: Arc::new(RwLock::new(SendSyncAnyMap::new())), + } + } +} + +impl DioxusServerContext { + pub fn get(&self) -> Option { + self.shared_context.read().ok()?.get::().cloned() + } + + pub fn insert( + &mut self, + value: T, + ) -> Result<(), PoisonError>> { + self.shared_context + .write() + .map(|mut map| map.insert(value)) + .map(|_| ()) + } +} diff --git a/packages/server/src/server_fn.rs b/packages/server/src/server_fn.rs index 2f000adda..7170d61de 100644 --- a/packages/server/src/server_fn.rs +++ b/packages/server/src/server_fn.rs @@ -1,5 +1,4 @@ -#[derive(Clone)] -pub struct DioxusServerContext {} +use crate::server_context::DioxusServerContext; #[cfg(any(feature = "ssr", doc))] pub type ServerFnTraitObj = server_fn::ServerFnTraitObj;