From b0caf442abf3f316c7d09975ad319ae933f26c6b Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Wed, 19 Jun 2024 03:38:06 +0200 Subject: [PATCH] Make it easier to provide context to fullstack (#2515) * pass context providers into server functions * add an example for FromContext * clean up DioxusRouterExt * fix server function context * fix fullstack desktop example * fix axum auth example * fix fullstack context type * fix context providers path --- Cargo.lock | 8 +- packages/dioxus/Cargo.toml | 3 +- packages/dioxus/src/launch.rs | 4 +- packages/dioxus/src/lib.rs | 4 + packages/fullstack/Cargo.toml | 2 - .../fullstack/examples/axum-auth/src/main.rs | 4 +- .../examples/axum-desktop/src/server.rs | 2 +- packages/fullstack/src/axum_adapter.rs | 92 ++++++++++++++----- packages/fullstack/src/config.rs | 8 +- packages/fullstack/src/launch.rs | 18 ++-- packages/fullstack/src/lib.rs | 4 +- packages/fullstack/src/server_context.rs | 67 +++++++++++--- 12 files changed, 152 insertions(+), 64 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 875c60d72..f6c6b10f3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -203,12 +203,6 @@ version = "1.0.82" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f538837af36e6f6a9be0faa67f9a314f8119e4e4b5867c6ab40ed60360142519" -[[package]] -name = "anymap" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33954243bd79057c2de7338850b85983a44588021f8a5fee574a8888c6de4344" - [[package]] name = "anymap2" version = "0.13.0" @@ -2130,6 +2124,7 @@ dependencies = [ name = "dioxus" version = "0.5.2" dependencies = [ + "axum", "criterion 0.3.6", "dioxus", "dioxus-config-macro", @@ -2416,7 +2411,6 @@ dependencies = [ name = "dioxus-fullstack" version = "0.5.2" dependencies = [ - "anymap", "async-trait", "axum", "base64 0.21.7", diff --git a/packages/dioxus/Cargo.toml b/packages/dioxus/Cargo.toml index 095f0b7dc..fee880021 100644 --- a/packages/dioxus/Cargo.toml +++ b/packages/dioxus/Cargo.toml @@ -27,6 +27,7 @@ dioxus-liveview = { workspace = true, optional = true } dioxus-ssr ={ workspace = true, optional = true } serde = { version = "1.0.136", optional = true } +axum = { workspace = true, optional = true } [target.'cfg(not(any(target_arch = "wasm32", target_os = "ios", target_os = "android")))'.dependencies] dioxus-hot-reload = { workspace = true, optional = true } @@ -50,7 +51,7 @@ web = ["dep:dioxus-web", "dioxus-fullstack?/web", "dioxus-static-site-generation ssr = ["dep:dioxus-ssr", "dioxus-router?/ssr", "dioxus-config-macro/ssr"] liveview = ["dep:dioxus-liveview", "dioxus-config-macro/liveview", "dioxus-router?/liveview"] static-generation = ["dep:dioxus-static-site-generation", "dioxus-config-macro/static-generation"] -axum = ["dioxus-fullstack?/axum", "dioxus-fullstack?/server", "dioxus-static-site-generation?/server", "ssr", "dioxus-liveview?/axum"] +axum = ["dioxus-fullstack?/axum", "dioxus-fullstack?/server", "dioxus-static-site-generation?/server", "ssr", "dioxus-liveview?/axum", "dep:axum"] # This feature just disables the no-renderer-enabled warning third-party-renderer = [] diff --git a/packages/dioxus/src/launch.rs b/packages/dioxus/src/launch.rs index de54818f6..241950abd 100644 --- a/packages/dioxus/src/launch.rs +++ b/packages/dioxus/src/launch.rs @@ -31,7 +31,7 @@ type ValidContext = SendContext; )))] type ValidContext = UnsendContext; -type SendContext = dyn Fn() -> Box + Send + Sync + 'static; +type SendContext = dyn Fn() -> Box + Send + Sync + 'static; type UnsendContext = dyn Fn() -> Box + 'static; @@ -137,7 +137,7 @@ impl LaunchBuilder { /// Inject state into the root component's context that is created on the thread that the app is launched on. pub fn with_context_provider( mut self, - state: impl Fn() -> Box + Send + Sync + 'static, + state: impl Fn() -> Box + Send + Sync + 'static, ) -> Self { self.contexts.push(Box::new(state) as Box); self diff --git a/packages/dioxus/src/lib.rs b/packages/dioxus/src/lib.rs index 86e72af4b..247b86316 100644 --- a/packages/dioxus/src/lib.rs +++ b/packages/dioxus/src/lib.rs @@ -110,6 +110,10 @@ pub mod prelude { #[cfg(feature = "router")] #[cfg_attr(docsrs, doc(cfg(feature = "router")))] pub use dioxus_router::prelude::*; + + #[cfg(feature = "axum")] + #[cfg_attr(docsrs, doc(cfg(feature = "axum")))] + pub use axum; } #[cfg(feature = "web")] diff --git a/packages/fullstack/Cargo.toml b/packages/fullstack/Cargo.toml index 7fd652ab0..3e8510b8a 100644 --- a/packages/fullstack/Cargo.toml +++ b/packages/fullstack/Cargo.toml @@ -39,7 +39,6 @@ tracing = { workspace = true } tracing-futures = { workspace = true, optional = true } once_cell = "1.17.1" tokio-util = { version = "0.7.8", features = ["rt"], optional = true } -anymap = { version = "0.12.1", optional = true } serde = "1.0.159" serde_json = { version = "1.0.95", optional = true } @@ -90,7 +89,6 @@ server = [ "dep:hyper", "dep:http", "dep:tower-layer", - "dep:anymap", "dep:tracing-futures", "dep:pin-project", "dep:thiserror", diff --git a/packages/fullstack/examples/axum-auth/src/main.rs b/packages/fullstack/examples/axum-auth/src/main.rs index 956869bbf..2da23e5f4 100644 --- a/packages/fullstack/examples/axum-auth/src/main.rs +++ b/packages/fullstack/examples/axum-auth/src/main.rs @@ -49,9 +49,7 @@ fn main() { // build our application with some routes let app = Router::new() // Server side render the application, serve static assets, and register server functions - .serve_dioxus_application(ServeConfig::builder().build(), || { - VirtualDom::new(app) - }) + .serve_dioxus_application(ServeConfig::default(), app) .await .layer( axum_session_auth::AuthSessionLayer::< diff --git a/packages/fullstack/examples/axum-desktop/src/server.rs b/packages/fullstack/examples/axum-desktop/src/server.rs index 2d195f392..e9d475078 100644 --- a/packages/fullstack/examples/axum-desktop/src/server.rs +++ b/packages/fullstack/examples/axum-desktop/src/server.rs @@ -19,7 +19,7 @@ async fn main() { axum::serve( listener, axum::Router::new() - .register_server_fns() + .register_server_functions() .into_make_service(), ) .await diff --git a/packages/fullstack/src/axum_adapter.rs b/packages/fullstack/src/axum_adapter.rs index 856751f93..228535889 100644 --- a/packages/fullstack/src/axum_adapter.rs +++ b/packages/fullstack/src/axum_adapter.rs @@ -22,7 +22,7 @@ //! listener, //! axum::Router::new() //! // Server side render the application, serve static assets, and register server functions -//! .serve_dioxus_application("", ServerConfig::new(app, ())) +//! .serve_dioxus_application(ServerConfig::new(), app) //! .into_make_service(), //! ) //! .await @@ -60,12 +60,13 @@ use axum::{ http::{Request, Response, StatusCode}, response::IntoResponse, }; -use dioxus_lib::prelude::VirtualDom; +use dioxus_lib::prelude::{Element, VirtualDom}; use futures_util::Future; use http::header::*; use std::sync::Arc; +use crate::launch::ContextProviders; use crate::prelude::*; /// A extension trait with utilities for integrating Dioxus with your Axum router. @@ -74,9 +75,8 @@ pub trait DioxusRouterExt { /// /// # Example /// ```rust - /// use dioxus_lib::prelude::*; - /// use dioxus_fullstack::prelude::*; - /// + /// # use dioxus_lib::prelude::*; + /// # use dioxus_fullstack::prelude::*; /// #[tokio::main] /// async fn main() { /// let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 8080)); @@ -84,23 +84,49 @@ pub trait DioxusRouterExt { /// .serve( /// axum::Router::new() /// // Register server functions routes with the default handler - /// .register_server_fns("") + /// .register_server_functions() /// .into_make_service(), /// ) /// .await /// .unwrap(); /// } /// ``` - fn register_server_fns(self) -> Self; + fn register_server_functions(self) -> Self + where + Self: Sized, + { + self.register_server_functions_with_context(Default::default()) + } + + /// Registers server functions with some additional context to insert into the [`DioxusServerContext`] for that handler. + /// + /// # Example + /// ```rust + /// # use dioxus_lib::prelude::*; + /// # use dioxus_fullstack::prelude::*; + /// #[tokio::main] + /// async fn main() { + /// let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 8080)); + /// axum::Server::bind(&addr) + /// .serve( + /// axum::Router::new() + /// // Register server functions routes with the default handler + /// .register_server_functions_with_context(vec![Box::new(|| 1234567890u32)]) + /// .into_make_service(), + /// ) + /// .await + /// .unwrap(); + /// } + /// ``` + fn register_server_functions_with_context(self, context_providers: ContextProviders) -> Self; /// Serves the static WASM for your Dioxus application (except the generated index.html). /// /// # Example /// ```rust - /// #![allow(non_snake_case)] - /// use dioxus_lib::prelude::*; - /// use dioxus_fullstack::prelude::*; - /// + /// # #![allow(non_snake_case)] + /// # use dioxus_lib::prelude::*; + /// # use dioxus_fullstack::prelude::*; /// #[tokio::main] /// async fn main() { /// let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 8080)); @@ -133,10 +159,9 @@ pub trait DioxusRouterExt { /// /// # Example /// ```rust - /// #![allow(non_snake_case)] - /// use dioxus_lib::prelude::*; - /// use dioxus_fullstack::prelude::*; - /// + /// # #![allow(non_snake_case)] + /// # use dioxus_lib::prelude::*; + /// # use dioxus_fullstack::prelude::*; /// #[tokio::main] /// async fn main() { /// let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 8080)); @@ -144,7 +169,7 @@ pub trait DioxusRouterExt { /// .serve( /// axum::Router::new() /// // Server side render the application, serve static assets, and register server functions - /// .serve_dioxus_application("", ServerConfig::new(app, ())) + /// .serve_dioxus_application(ServeConfig::new(), ) /// .into_make_service(), /// ) /// .await @@ -158,7 +183,7 @@ pub trait DioxusRouterExt { fn serve_dioxus_application( self, cfg: impl Into, - build_virtual_dom: impl Fn() -> VirtualDom + Send + Sync + 'static, + app: fn() -> Element, ) -> impl Future + Send + Sync where Self: Sized; @@ -168,12 +193,29 @@ impl DioxusRouterExt for Router where S: Send + Sync + Clone + 'static, { - fn register_server_fns(mut self) -> Self { + fn register_server_functions_with_context( + mut self, + context_providers: ContextProviders, + ) -> Self { use http::method::Method; + let context_providers = Arc::new(context_providers); + for (path, method) in server_fn::axum::server_fn_paths() { tracing::trace!("Registering server function: {} {}", method, path); - let handler = move |req| handle_server_fns_inner(path, || {}, req); + let context_providers = context_providers.clone(); + let handler = move |req| { + handle_server_fns_inner( + path, + move |server_context| { + for context_provider in context_providers.iter() { + let context = context_provider(); + _ = server_context.insert_any(context); + } + }, + req, + ) + }; self = match method { Method::GET => self.route(path, get(handler)), Method::POST => self.route(path, post(handler)), @@ -234,7 +276,7 @@ where fn serve_dioxus_application( self, cfg: impl Into, - build_virtual_dom: impl Fn() -> VirtualDom + Send + Sync + 'static, + app: fn() -> Element, ) -> impl Future + Send + Sync { let cfg = cfg.into(); async move { @@ -244,7 +286,7 @@ where let mut server = self .serve_static_assets(cfg.assets_path.clone()) .await - .register_server_fns(); + .register_server_functions(); #[cfg(all(feature = "hot-reload", debug_assertions))] { @@ -254,7 +296,7 @@ where server.fallback(get(render_handler).with_state(( cfg, - Arc::new(build_virtual_dom), + Arc::new(move || VirtualDom::new(app)), ssr_state, ))) } @@ -311,7 +353,7 @@ type AxumHandler = ( /// .serve( /// axum::Router::new() /// // Register server functions, etc. -/// // Note you probably want to use `register_server_fns_with_handler` +/// // Note you can use `register_server_functions_with_context` /// // to inject the context into server functions running outside /// // of an SSR render context. /// .fallback(get(render_handler_with_context).with_state(( @@ -383,7 +425,7 @@ fn report_err(e: E) -> Response { /// A handler for Dioxus server functions. This will run the server function and return the result. async fn handle_server_fns_inner( path: &str, - additional_context: impl Fn() + 'static + Clone + Send, + additional_context: impl Fn(&DioxusServerContext) + 'static + Clone + Send, req: Request, ) -> impl IntoResponse { use server_fn::middleware::Service; @@ -398,7 +440,7 @@ async fn handle_server_fns_inner( server_fn::axum::get_server_fn_service(&path_string) { let server_context = DioxusServerContext::new(Arc::new(tokio::sync::RwLock::new(parts))); - additional_context(); + additional_context(&server_context); // store Accepts and Referrer in case we need them for redirect (below) let accepts_html = req diff --git a/packages/fullstack/src/config.rs b/packages/fullstack/src/config.rs index 83ed0e43f..4f7e9d62a 100644 --- a/packages/fullstack/src/config.rs +++ b/packages/fullstack/src/config.rs @@ -116,9 +116,12 @@ impl Config { pub async fn launch_server( self, build_virtual_dom: impl Fn() -> VirtualDom + Send + Sync + 'static, + context_providers: crate::launch::ContextProviders, ) { + use std::any::Any; + let addr = self.addr; - println!("Listening on {}", addr); + println!("Listening on http://{}", addr); let cfg = self.server_cfg.build(); let server_fn_route = self.server_fn_route; @@ -129,7 +132,8 @@ impl Config { use tower::ServiceBuilder; let ssr_state = SSRState::new(&cfg); - let router = axum::Router::new().register_server_fns(); + let router = + axum::Router::new().register_server_functions_with_context(context_providers); #[cfg(not(any(feature = "desktop", feature = "mobile")))] let router = { let mut router = router.serve_static_assets(cfg.assets_path.clone()).await; diff --git a/packages/fullstack/src/launch.rs b/packages/fullstack/src/launch.rs index 3882bd3a4..41fab59f2 100644 --- a/packages/fullstack/src/launch.rs +++ b/packages/fullstack/src/launch.rs @@ -1,18 +1,21 @@ //! This module contains the `launch` function, which is the main entry point for dioxus fullstack -use std::any::Any; +use std::{any::Any, sync::Arc}; use dioxus_lib::prelude::{Element, VirtualDom}; pub use crate::Config; +pub(crate) type ContextProviders = Arc< + Vec Box + Send + Sync + 'static>>, +>; fn virtual_dom_factory( root: fn() -> Element, - contexts: Vec Box + Send + Sync>>, + contexts: ContextProviders, ) -> impl Fn() -> VirtualDom + 'static { move || { let mut vdom = VirtualDom::new(root); - for context in &contexts { + for context in &*contexts { vdom.insert_any_root_context(context()); } vdom @@ -24,15 +27,16 @@ fn virtual_dom_factory( #[allow(unused)] pub fn launch( root: fn() -> Element, - contexts: Vec Box + Send + Sync>>, + contexts: Vec Box + Send + Sync>>, platform_config: Config, ) -> ! { - let factory = virtual_dom_factory(root, contexts); + let contexts = Arc::new(contexts); + let factory = virtual_dom_factory(root, contexts.clone()); #[cfg(all(feature = "server", not(target_arch = "wasm32")))] tokio::runtime::Runtime::new() .unwrap() .block_on(async move { - platform_config.launch_server(factory).await; + platform_config.launch_server(factory, contexts).await; }); unreachable!("Launching a fullstack app should never return") @@ -43,7 +47,7 @@ pub fn launch( #[allow(unused)] pub fn launch( root: fn() -> Element, - contexts: Vec Box + Send + Sync>>, + contexts: Vec Box + Send + Sync>>, platform_config: Config, ) { let factory = virtual_dom_factory(root, contexts); diff --git a/packages/fullstack/src/lib.rs b/packages/fullstack/src/lib.rs index d73c05137..6db537a84 100644 --- a/packages/fullstack/src/lib.rs +++ b/packages/fullstack/src/lib.rs @@ -55,8 +55,8 @@ pub mod prelude { #[cfg(feature = "server")] #[cfg_attr(docsrs, doc(cfg(feature = "server")))] pub use crate::server_context::{ - extract, server_context, with_server_context, DioxusServerContext, FromServerContext, - ProvideServerContext, + extract, server_context, with_server_context, DioxusServerContext, FromContext, + FromServerContext, ProvideServerContext, }; #[cfg(feature = "server")] diff --git a/packages/fullstack/src/server_context.rs b/packages/fullstack/src/server_context.rs index 45d51b3ed..1b8a65078 100644 --- a/packages/fullstack/src/server_context.rs +++ b/packages/fullstack/src/server_context.rs @@ -1,16 +1,19 @@ use crate::html_storage::HTMLData; +use std::any::Any; +use std::collections::HashMap; use std::sync::Arc; use std::sync::RwLock; +type SendSyncAnyMap = + std::collections::HashMap>; + /// A shared context for server functions that contains information about the request and middleware state. /// This allows you to pass data between your server framework and the server functions. This can be used to pass request information or information about the state of the server. For example, you could pass authentication data though this context to your server functions. /// /// You should not construct this directly inside components. Instead use the `HasServerContext` trait to get the server context from the scope. #[derive(Clone)] pub struct DioxusServerContext { - shared_context: std::sync::Arc< - std::sync::RwLock>, - >, + shared_context: std::sync::Arc>, response_parts: std::sync::Arc>, pub(crate) parts: Arc>, html_data: Arc>, @@ -20,7 +23,7 @@ pub struct DioxusServerContext { impl Default for DioxusServerContext { fn default() -> Self { Self { - shared_context: std::sync::Arc::new(std::sync::RwLock::new(anymap::Map::new())), + shared_context: std::sync::Arc::new(std::sync::RwLock::new(HashMap::new())), response_parts: std::sync::Arc::new(RwLock::new( http::response::Response::new(()).into_parts().0, )), @@ -34,12 +37,10 @@ impl Default for DioxusServerContext { mod server_fn_impl { use super::*; + use std::any::{Any, TypeId}; use std::sync::LockResult; use std::sync::{PoisonError, RwLockReadGuard, RwLockWriteGuard}; - use anymap::{any::Any, Map}; - type SendSyncAnyMap = Map; - impl DioxusServerContext { /// Create a new server context from a request pub fn new(parts: impl Into>>) -> Self { @@ -55,17 +56,32 @@ mod server_fn_impl { /// Clone a value from the shared server context pub fn get(&self) -> Option { - self.shared_context.read().ok()?.get::().cloned() + self.shared_context + .read() + .ok()? + .get(&TypeId::of::()) + .map(|v| v.downcast_ref::().unwrap().clone()) } /// Insert a value into the shared server context pub fn insert( - &mut self, + &self, value: T, ) -> Result<(), PoisonError>> { self.shared_context .write() - .map(|mut map| map.insert(value)) + .map(|mut map| map.insert(TypeId::of::(), Box::new(value))) + .map(|_| ()) + } + + /// Insert a Boxed `Any` value into the shared server context + pub fn insert_any( + &self, + value: Box, + ) -> Result<(), PoisonError>> { + self.shared_context + .write() + .map(|mut map| map.insert((*value).type_id(), value)) .map(|_| ()) } @@ -232,14 +248,41 @@ impl std::fmt::Display for NotFoundInServerContext { impl std::error::Error for NotFoundInServerContext {} -pub struct FromContext(pub(crate) T); +/// Extract a value from the server context provided through the launch builder context or [`DioxusServerContext::insert`] +/// +/// Example: +/// ```rust, no_run +/// use dioxus::prelude::*; +/// +/// LaunchBuilder::new() +/// // You can provide context to your whole app (including server functions) with the `with_context` method on the launch builder +/// .with_context(server_only! { +/// 1234567890u32 +/// }) +/// .launch(app); +/// +/// #[server] +/// async fn read_context() -> Result { +/// // You can extract values from the server context with the `extract` function +/// let FromContext(value) = extract().await?; +/// Ok(value) +/// } +/// +/// fn app() -> Element { +/// let future = use_resource(read_context); +/// rsx! { +/// h1 { "{future:?}" } +/// } +/// } +/// ``` +pub struct FromContext(pub T); #[async_trait::async_trait] impl FromServerContext for FromContext { type Rejection = NotFoundInServerContext; async fn from_request(req: &DioxusServerContext) -> Result { - Ok(Self(req.clone().get::().ok_or({ + Ok(Self(req.get::().ok_or({ NotFoundInServerContext::(std::marker::PhantomData::) })?)) }