diff --git a/Cargo.lock b/Cargo.lock index 94aac6462..f9d3e00ba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3102,10 +3102,14 @@ dependencies = [ name = "dioxus_server_macro" version = "0.6.0-alpha.2" dependencies = [ + "axum 0.7.5", + "dioxus", "proc-macro2", "quote", + "serde", "server_fn_macro", "syn 2.0.74", + "tower-http", ] [[package]] diff --git a/examples/fullstack-streaming/Cargo.toml b/examples/fullstack-streaming/Cargo.toml index 29eba049a..9c13395cb 100644 --- a/examples/fullstack-streaming/Cargo.toml +++ b/examples/fullstack-streaming/Cargo.toml @@ -20,5 +20,5 @@ once_cell = "1.19.0" [features] default = [] -server = ["dioxus/axum", "tokio"] +server = ["dioxus/axum", "dep:tokio"] web = ["dioxus/web"] diff --git a/packages/dioxus/src/launch.rs b/packages/dioxus/src/launch.rs index 3e08c8578..56143b499 100644 --- a/packages/dioxus/src/launch.rs +++ b/packages/dioxus/src/launch.rs @@ -279,10 +279,11 @@ mod current_platform { { fn into_config(self, config: &mut Option) { match config { - Some(config) => config.set_server_cfg(self), + Some(config) => config.set_server_config(self), None => { - *config = - Some(crate::launch::current_platform::Config::new().with_server_cfg(self)) + *config = Some( + crate::launch::current_platform::Config::new().with_server_config(self), + ) } } } diff --git a/packages/fullstack/README.md b/packages/fullstack/README.md index b982681ff..fa0f5c0d2 100644 --- a/packages/fullstack/README.md +++ b/packages/fullstack/README.md @@ -63,6 +63,80 @@ async fn get_meaning(of: String) -> Result, ServerFnError> { } ``` +## Axum Integration + +If you have an existing Axum router or you need more control over the server, you can use the [`DioxusRouterExt`](https://docs.rs/dioxus-fullstack/0.6.0-alpha.2/dioxus_fullstack/prelude/trait.DioxusRouterExt.html) trait to integrate with your existing Axum router. + +First, make sure your `axum` dependency is optional and enabled by the server feature flag. Axum cannot be compiled to wasm, so if it is enabled by default, it will cause a compile error: + +```toml +[dependencies] +dioxus = { version = "*", features = ["fullstack"] } +axum = { version = "0.7.0", optional = true } + +[features] +server = ["dep:axum", "dioxus/server"] +web = ["dioxus/web"] +``` + +Then we can set up dioxus with the axum server: + +```rust, no_run +#![allow(non_snake_case)] +use dioxus::prelude::*; + +// The entry point for the server +#[cfg(feature = "server")] +fn main() { + // Get the address the server should run on. If the CLI is running, the CLI proxies fullstack into the main address + // and we use the generated address the CLI gives us + let cli_args = dioxus_cli_config::RuntimeCLIArguments::from_cli(); + let address = cli_args + .as_ref() + .map(|args| args.fullstack_address().address()) + .unwrap_or_else(SocketAddr::from(([127, 0, 0, 1], 8080))); + + // Set up the axum router + let router = axum::Router::new() + // You can add a dioxus application to the router with the `serve_dioxus_application` method + // This will add a fallback route to the router that will serve your component and server functions + .serve_dioxus_application(ServeConfigBuilder::default(), App); + + // Finally, we can launch the server + let router = router.into_make_service(); + let listener = tokio::net::TcpListener::bind(address).await.unwrap(); + axum::serve(listener, router).await.unwrap(); +} + +// For any other platform, we just launch the app +#[cfg(not(feature = "server"))] +fn main() { + launch(App); +} + +#[component] +fn App() -> Element { + let mut meaning = use_signal(|| None); + + rsx! { + h1 { "Meaning of life: {meaning:?}" } + button { + onclick: move |_| async move { + if let Ok(data) = get_meaning("life the universe and everything".into()).await { + meaning.set(data); + } + }, + "Run a server function" + } + } +} + +#[server] +async fn get_meaning(of: String) -> Result, ServerFnError> { + Ok(of.contains("life").then(|| 42)) +} +``` + ## Getting Started To get started with full stack Dioxus, check out our [getting started guide](https://dioxuslabs.com/learn/0.5/getting_started), or the [full stack examples](https://github.com/DioxusLabs/dioxus/tree/master/examples). diff --git a/packages/fullstack/docs/request_origin.md b/packages/fullstack/docs/request_origin.md new file mode 100644 index 000000000..f0e052ab7 --- /dev/null +++ b/packages/fullstack/docs/request_origin.md @@ -0,0 +1,40 @@ +This method interacts with information from the current request. The request may come from: + +1. The initial SSR render if this method called from a [`Component`](dioxus_lib::prelude::component) or a [`server`](crate::prelude::server) function that is called during the initial render + +```rust +#[component] +fn PrintHtmlRequestInfo() -> Element { + // The server context only exists on the server, so we need to put it behind a server_only! config + server_only! { + // Since we are calling this from a component, the server context that is returned will be from + // the html request for ssr rendering + println!("headers are {:?}", server_context().request_parts().headers); + } + rsx! {} +} +``` + +2. A request to a [`server`](crate::prelude::server) function called directly from the client (either on desktop/mobile or on the web frontend after the initial render) + +```rust +#[server] +async fn read_headers() -> Result<(), ServerFnError> { + // Since we are calling this from a server function, the server context that is may be from the + // initial request or a request from the client + println!("headers are {:?}", server_context().request_parts().headers); + Ok(()) +} + +#[component] +fn CallServerFunction() -> Element { + rsx! { + button { + // If you click the button, the server function will be called and the server context will be + // from the client request + onclick: move |_| read_headers().await, + "Call server function" + } + } +} +``` diff --git a/packages/fullstack/src/axum_adapter.rs b/packages/fullstack/src/axum_adapter.rs index 4d3a1c613..e0aaa85d3 100644 --- a/packages/fullstack/src/axum_adapter.rs +++ b/packages/fullstack/src/axum_adapter.rs @@ -21,7 +21,7 @@ //! listener, //! axum::Router::new() //! // Server side render the application, serve static assets, and register server functions -//! .serve_dioxus_application(ServeConfig::new().unwrap(), app) +//! .serve_dioxus_application(ServeConfigBuilder::default(), app) //! .into_make_service(), //! ) //! .await @@ -160,8 +160,10 @@ pub trait DioxusRouterExt { /// rsx! { "Hello World" } /// } /// ``` - fn serve_dioxus_application(self, cfg: impl Into, app: fn() -> Element) -> Self + fn serve_dioxus_application(self, cfg: Cfg, app: fn() -> Element) -> Self where + Cfg: TryInto, + Error: std::error::Error, Self: Sized; } @@ -243,18 +245,27 @@ where self } - fn serve_dioxus_application(self, cfg: impl Into, app: fn() -> Element) -> Self { - let cfg = cfg.into(); - - let ssr_state = SSRState::new(&cfg); - + fn serve_dioxus_application(self, cfg: Cfg, app: fn() -> Element) -> Self + where + Cfg: TryInto, + Error: std::error::Error, + { // Add server functions and render index.html let server = self.serve_static_assets().register_server_functions(); - server.fallback( - get(render_handler) - .with_state(RenderHandleState::new(cfg, app).with_ssr_state(ssr_state)), - ) + match cfg.try_into() { + Ok(cfg) => { + let ssr_state = SSRState::new(&cfg); + server.fallback( + get(render_handler) + .with_state(RenderHandleState::new(cfg, app).with_ssr_state(ssr_state)), + ) + } + Err(err) => { + tracing::trace!("Failed to create render handler. This is expected if you are only using fullstack for desktop/mobile server functions: {}", err); + server + } + } } } @@ -343,7 +354,7 @@ impl RenderHandleState { /// // to inject the context into server functions running outside /// // of an SSR render context. /// .fallback(get(render_handler) -/// .with_state(RenderHandleState::new(app)) +/// .with_state(RenderHandleState::new(ServeConfig::new().unwrap(), app)) /// ) /// .into_make_service(); /// let listener = tokio::net::TcpListener::bind(addr).await.unwrap(); diff --git a/packages/fullstack/src/config.rs b/packages/fullstack/src/config.rs index 52d805111..6b82ddaeb 100644 --- a/packages/fullstack/src/config.rs +++ b/packages/fullstack/src/config.rs @@ -5,6 +5,34 @@ use dioxus_lib::prelude::*; use std::sync::Arc; /// Settings for a fullstack app. +/// +/// Depending on what features are enabled, you can pass in configurations for each client platform as well as the server: +/// ```rust, no_run +/// # fn app() -> Element { todo!() } +/// use dioxus::prelude::*; +/// +/// let mut cfg = dioxus::fullstack::Config::new(); +/// +/// // Only set the server config if the server feature is enabled +/// server_only! { +/// cfg = cfg.with_server_config(ServeConfigBuilder::default()); +/// } +/// +/// // Only set the web config if the web feature is enabled +/// web! { +/// cfg = cfg.with_web_config(dioxus::web::Config::default()); +/// } +/// +/// // Only set the desktop config if the desktop feature is enabled +/// desktop! { +/// cfg = cfg.with_desktop_config(dioxus::desktop::Config::default()); +/// } +/// +/// // Finally, launch the app with the config +/// LaunchBuilder::new() +/// .with_cfg(cfg) +/// .launch(app); +/// ``` pub struct Config { #[cfg(feature = "server")] pub(crate) server_cfg: ServeConfigBuilder, @@ -41,7 +69,25 @@ impl Config { Self::default() } - /// Set the incremental renderer config. + /// Set the incremental renderer config. The incremental config can be used to improve + /// performance of heavy routes by caching the rendered html in memory and/or the file system. + /// + /// ```rust, no_run + /// # fn app() -> Element { todo!() } + /// use dioxus::prelude::*; + /// + /// let mut cfg = dioxus::fullstack::Config::new(); + /// + /// // Only set the server config if the server feature is enabled + /// server_only! { + /// cfg = cfg.incremental(IncrementalRendererConfig::default().with_memory_cache_limit(10000)); + /// } + /// + /// // Finally, launch the app with the config + /// LaunchBuilder::new() + /// .with_cfg(cfg) + /// .launch(app); + /// ``` #[cfg(feature = "server")] #[cfg_attr(docsrs, doc(cfg(feature = "server")))] pub fn incremental(self, cfg: IncrementalRendererConfig) -> Self { @@ -51,28 +97,94 @@ impl Config { } } - /// Set the server config. + /// Set the server config + /// ```rust, no_run + /// # fn app() -> Element { todo!() } + /// use dioxus::prelude::*; + /// + /// let mut cfg = dioxus::fullstack::Config::new(); + /// + /// // Only set the server config if the server feature is enabled + /// server_only! { + /// cfg = cfg.with_server_config(ServeConfigBuilder::default()); + /// } + /// + /// // Finally, launch the app with the config + /// LaunchBuilder::new() + /// .with_cfg(cfg) + /// .launch(app); + /// ``` #[cfg(feature = "server")] #[cfg_attr(docsrs, doc(cfg(feature = "server")))] - pub fn with_server_cfg(self, server_cfg: ServeConfigBuilder) -> Self { + pub fn with_server_config(self, server_cfg: ServeConfigBuilder) -> Self { Self { server_cfg, ..self } } - /// Set the server config. + /// Set the server config by modifying the config in place + /// + /// ```rust, no_run + /// # fn app() -> Element { todo!() } + /// use dioxus::prelude::*; + /// + /// let mut cfg = dioxus::fullstack::Config::new(); + /// + /// // Only set the server config if the server feature is enabled + /// server_only! { + /// cfg.set_server_config(ServeConfigBuilder::default()); + /// } + /// + /// // Finally, launch the app with the config + /// LaunchBuilder::new() + /// .with_cfg(cfg) + /// .launch(app); + /// ``` #[cfg(feature = "server")] #[cfg_attr(docsrs, doc(cfg(feature = "server")))] - pub fn set_server_cfg(&mut self, server_cfg: ServeConfigBuilder) { + pub fn set_server_config(&mut self, server_cfg: ServeConfigBuilder) { self.server_cfg = server_cfg; } - /// Set the web config. + /// Set the web config + /// ```rust, no_run + /// # fn app() -> Element { todo!() } + /// use dioxus::prelude::*; + /// + /// let mut cfg = dioxus::fullstack::Config::new(); + /// + /// // Only set the web config if the server feature is enabled + /// web! { + /// cfg = cfg.with_web_config(dioxus::web::Config::default()); + /// } + /// + /// // Finally, launch the app with the config + /// LaunchBuilder::new() + /// .with_cfg(cfg) + /// .launch(app); + /// ``` #[cfg(feature = "web")] #[cfg_attr(docsrs, doc(cfg(feature = "web")))] pub fn with_web_config(self, web_cfg: dioxus_web::Config) -> Self { Self { web_cfg, ..self } } - /// Set the web config. + /// Set the server config by modifying the config in place + /// + /// ```rust, no_run + /// # fn app() -> Element { todo!() } + /// use dioxus::prelude::*; + /// + /// let mut cfg = dioxus::fullstack::Config::new(); + /// + /// // Only set the web config if the server feature is enabled + /// web! { + /// cfg.set_web_config(dioxus::web::Config::default()); + /// } + /// + /// // Finally, launch the app with the config + /// LaunchBuilder::new() + /// .with_cfg(cfg) + /// .launch(app); + /// ``` #[cfg(feature = "web")] #[cfg_attr(docsrs, doc(cfg(feature = "web")))] pub fn set_web_config(&mut self, web_cfg: dioxus_web::Config) { @@ -80,6 +192,22 @@ impl Config { } /// Set the desktop config + /// ```rust, no_run + /// # fn app() -> Element { todo!() } + /// use dioxus::prelude::*; + /// + /// let mut cfg = dioxus::fullstack::Config::new(); + /// + /// // Only set the desktop config if the server feature is enabled + /// desktop! { + /// cfg = cfg.with_desktop_config(dioxus::desktop::Config::default()); + /// } + /// + /// // Finally, launch the app with the config + /// LaunchBuilder::new() + /// .with_cfg(cfg) + /// .launch(app); + /// ``` #[cfg(feature = "desktop")] #[cfg_attr(docsrs, doc(cfg(feature = "desktop")))] pub fn with_desktop_config(self, desktop_cfg: dioxus_desktop::Config) -> Self { @@ -89,17 +217,73 @@ impl Config { } } - /// Set the desktop config. + /// Set the desktop config by modifying the config in place + /// + /// ```rust, no_run + /// # fn app() -> Element { todo!() } + /// use dioxus::prelude::*; + /// + /// let mut cfg = dioxus::fullstack::Config::new(); + /// + /// // Only set the desktop config if the server feature is enabled + /// desktop! { + /// cfg.set_desktop_config(dioxus::desktop::Config::default()); + /// } + /// + /// // Finally, launch the app with the config + /// LaunchBuilder::new() + /// .with_cfg(cfg) + /// .launch(app); + /// ``` #[cfg(feature = "desktop")] #[cfg_attr(docsrs, doc(cfg(feature = "desktop")))] pub fn set_desktop_config(&mut self, desktop_cfg: dioxus_desktop::Config) { self.desktop_cfg = desktop_cfg; } + /// Set the mobile config + /// ```rust, no_run + /// # fn app() -> Element { todo!() } + /// use dioxus::prelude::*; + /// + /// let mut cfg = dioxus::fullstack::Config::new(); + /// + /// // Only set the mobile config if the server feature is enabled + /// mobile! { + /// cfg = cfg.with_mobile_cfg(dioxus::mobile::Config::default()); + /// } + /// + /// // Finally, launch the app with the config + /// LaunchBuilder::new() + /// .with_cfg(cfg) + /// .launch(app); /// Set the mobile config. #[cfg(feature = "mobile")] #[cfg_attr(docsrs, doc(cfg(feature = "mobile")))] pub fn with_mobile_cfg(self, mobile_cfg: dioxus_mobile::Config) -> Self { Self { mobile_cfg, ..self } } + + /// Set the mobile config by modifying the config in place + /// ```rust, no_run + /// # fn app() -> Element { todo!() } + /// use dioxus::prelude::*; + /// + /// let mut cfg = dioxus::fullstack::Config::new(); + /// + /// // Only set the mobile config if the server feature is enabled + /// mobile! { + /// cfg.set_mobile_cfg(dioxus::mobile::Config::default()); + /// } + /// + /// // Finally, launch the app with the config + /// LaunchBuilder::new() + /// .with_cfg(cfg) + /// .launch(app); + /// Set the mobile config. + #[cfg(feature = "mobile")] + #[cfg_attr(docsrs, doc(cfg(feature = "mobile")))] + pub fn set_mobile_cfg(&mut self, mobile_cfg: dioxus_mobile::Config) { + self.mobile_cfg = mobile_cfg; + } } diff --git a/packages/fullstack/src/serve_config.rs b/packages/fullstack/src/serve_config.rs index 3f1482b95..272a50fc0 100644 --- a/packages/fullstack/src/serve_config.rs +++ b/packages/fullstack/src/serve_config.rs @@ -15,7 +15,7 @@ pub struct ServeConfigBuilder { } impl ServeConfigBuilder { - /// Create a new ServeConfigBuilder with the root component and props to render on the server. + /// Create a new ServeConfigBuilder with incremental static generation disabled and the default index.html settings pub fn new() -> Self { Self { root_id: None, @@ -25,7 +25,25 @@ impl ServeConfigBuilder { } } - /// Enable incremental static generation + /// Enable incremental static generation. Incremental static generation caches the + /// rendered html in memory and/or the file system. It can be used to improve performance of heavy routes. + /// + /// ```rust, no_run + /// # fn app() -> Element { todo!() } + /// use dioxus::prelude::*; + /// + /// let mut cfg = dioxus::fullstack::Config::new(); + /// + /// // Only set the server config if the server feature is enabled + /// server_only! { + /// cfg = cfg.with_server_cfg(ServeConfigBuilder::default().incremental(IncrementalRendererConfig::default())); + /// } + /// + /// // Finally, launch the app with the config + /// LaunchBuilder::new() + /// .with_cfg(cfg) + /// .launch(app); + /// ``` pub fn incremental(mut self, cfg: dioxus_isrg::IncrementalRendererConfig) -> Self { self.incremental = Some(cfg); self @@ -44,12 +62,56 @@ impl ServeConfigBuilder { } /// Set the id of the root element in the index.html file to place the prerendered content into. (defaults to main) + /// + /// # Example + /// + /// If your index.html file looks like this: + /// ```html + /// + /// + /// + /// My App + /// + /// + ///
+ /// + /// + /// ``` + /// + /// You can set the root id to `"my-custom-root"` to render the app into that element: + /// + /// ```rust, no_run + /// # fn app() -> Element { todo!() } + /// use dioxus::prelude::*; + /// + /// let mut cfg = dioxus::fullstack::Config::new(); + /// + /// // Only set the server config if the server feature is enabled + /// server_only! { + /// cfg = cfg.with_server_cfg(ServeConfigBuilder::default().root_id("app")); + /// } + /// + /// // You also need to set the root id in your web config + /// web! { + /// cfg = cfg.with_web_config(dioxus::web::Config::default().rootname("app")); + /// } + /// + /// // And desktop config + /// desktop! { + /// cfg = cfg.with_desktop_config(dioxus::desktop::Config::default().with_root_name("app")); + /// } + /// + /// // Finally, launch the app with the config + /// LaunchBuilder::new() + /// .with_cfg(cfg) + /// .launch(app); + /// ``` pub fn root_id(mut self, root_id: &'static str) -> Self { self.root_id = Some(root_id); self } - /// Build the ServeConfig + /// Build the ServeConfig. This may fail if the index.html file is not found. pub fn build(self) -> Result { // The CLI always bundles static assets into the exe/public directory let public_path = public_path(); @@ -75,6 +137,14 @@ impl ServeConfigBuilder { } } +impl TryInto for ServeConfigBuilder { + type Error = UnableToLoadIndex; + + fn try_into(self) -> Result { + self.build() + } +} + /// Get the path to the public assets directory to serve static files from pub(crate) fn public_path() -> PathBuf { // The CLI always bundles static assets into the exe/public directory diff --git a/packages/fullstack/src/server_context.rs b/packages/fullstack/src/server_context.rs index cf3c95640..02efa363d 100644 --- a/packages/fullstack/src/server_context.rs +++ b/packages/fullstack/src/server_context.rs @@ -7,9 +7,21 @@ 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. +/// You should not construct this directly inside components or server functions. Instead use [`server_context()`] to get the server context from the current request. +/// +/// # Example +/// +/// ```rust, no_run +/// # use dioxus::prelude::*; +/// #[server] +/// async fn read_headers() -> Result<(), ServerFnError> { +/// let server_context = server_context(); +/// let headers: http::HeaderMap = server_context.extract().await?; +/// println!("{:?}", headers); +/// Ok(()) +/// } +/// ``` #[derive(Clone)] pub struct DioxusServerContext { shared_context: std::sync::Arc>, @@ -59,7 +71,34 @@ mod server_fn_impl { } } - /// Clone a value from the shared server context + /// Clone a value from the shared server context. If you are using [`DioxusRouterExt`](crate::prelude::DioxusRouterExt), any values you insert into + /// the launch context will also be available in the server context. + /// + /// 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 fn get(&self) -> Option { self.shared_context .read() @@ -68,10 +107,10 @@ mod server_fn_impl { } /// Insert a value into the shared server context + /// + /// pub fn insert(&self, value: T) { - self.shared_context - .write() - .insert(TypeId::of::(), Box::new(value)); + self.insert_any(Box::new(value)); } /// Insert a Boxed `Any` value into the shared server context @@ -82,33 +121,112 @@ mod server_fn_impl { } /// Get the response parts from the server context + /// + #[doc = include_str!("../docs/request_origin.md")] + /// + /// # Example + /// + /// ```rust, no_run + /// # use dioxus::prelude::*; + /// #[server] + /// async fn set_headers() -> Result<(), ServerFnError> { + /// let server_context = server_context(); + /// let cookies = server_context.response_parts() + /// .headers() + /// .get("Cookie") + /// .ok_or_else(|| ServerFnError::msg("failed to find Cookie header in the response"))?; + /// println!("{:?}", cookies); + /// Ok(()) + /// } + /// ``` pub fn response_parts(&self) -> RwLockReadGuard<'_, http::response::Parts> { self.response_parts.read() } /// Get the response parts from the server context + /// + #[doc = include_str!("../docs/request_origin.md")] + /// + /// # Example + /// + /// ```rust, no_run + /// # use dioxus::prelude::*; + /// #[server] + /// async fn set_headers() -> Result<(), ServerFnError> { + /// let server_context = server_context(); + /// server_context.response_parts_mut() + /// .headers_mut() + /// .insert("Cookie", "dioxus=fullstack"); + /// Ok(()) + /// } + /// ``` pub fn response_parts_mut(&self) -> RwLockWriteGuard<'_, http::response::Parts> { self.response_parts.write() } - /// Get the request that triggered: - /// - The initial SSR render if called from a ScopeState or ServerFn - /// - The server function to be called if called from a server function after the initial render + /// Get the request parts + /// + #[doc = include_str!("../docs/request_origin.md")] + /// + /// # Example + /// + /// ```rust, no_run + /// # use dioxus::prelude::*; + /// #[server] + /// async fn read_headers() -> Result<(), ServerFnError> { + /// let server_context = server_context(); + /// let id: &i32 = server_context.request_parts() + /// .extensions + /// .get() + /// .ok_or_else(|| ServerFnError::msg("failed to find i32 extension in the request"))?; + /// println!("{:?}", id); + /// Ok(()) + /// } + /// ``` pub fn request_parts(&self) -> parking_lot::RwLockReadGuard<'_, http::request::Parts> { self.parts.read() } - /// Get the request that triggered: - /// - The initial SSR render if called from a ScopeState or ServerFn - /// - The server function to be called if called from a server function after the initial render + /// Get the request parts mutably + /// + #[doc = include_str!("../docs/request_origin.md")] + /// + /// # Example + /// + /// ```rust, no_run + /// # use dioxus::prelude::*; + /// #[server] + /// async fn read_headers() -> Result<(), ServerFnError> { + /// let server_context = server_context(); + /// let id: i32 = server_context.request_parts_mut() + /// .extensions + /// .remove() + /// .ok_or_else(|| ServerFnError::msg("failed to find i32 extension in the request"))?; + /// println!("{:?}", id); + /// Ok(()) + /// } + /// ``` pub fn request_parts_mut(&self) -> parking_lot::RwLockWriteGuard<'_, http::request::Parts> { self.parts.write() } - /// Extract some part from the request - pub async fn extract>( - &self, - ) -> Result { + /// Extract part of the request. + /// + #[doc = include_str!("../docs/request_origin.md")] + /// + /// # Example + /// + /// ```rust, no_run + /// # use dioxus::prelude::*; + /// #[server] + /// async fn read_headers() -> Result<(), ServerFnError> { + /// let server_context = server_context(); + /// let headers: http::HeaderMap = server_context.extract().await?; + /// println!("{:?}", headers); + /// Ok(()) + /// } + /// ``` + pub async fn extract>(&self) -> Result { T::from_request(self).await } } @@ -175,7 +293,7 @@ impl std::future::Future for ProvideServerContext { #[async_trait::async_trait] pub trait FromServerContext: Sized { /// The error type returned when extraction fails. This type must implement `std::error::Error`. - type Rejection: std::error::Error; + type Rejection; /// Extract this type from the server context. async fn from_request(req: &DioxusServerContext) -> Result; diff --git a/packages/fullstack/src/server_fn/collection.rs b/packages/fullstack/src/server_fn/collection.rs deleted file mode 100644 index d932d8ee8..000000000 --- a/packages/fullstack/src/server_fn/collection.rs +++ /dev/null @@ -1,174 +0,0 @@ -#[cfg(feature = "server")] -#[cfg_attr(docsrs, doc(cfg(feature = "server")))] -#[derive(Clone)] -/// A trait object for a function that be called on serializable arguments and returns a serializable result. -pub struct ServerFnTraitObj(server_fn::ServerFnTraitObj<()>); - -#[cfg(feature = "server")] -impl std::ops::Deref for ServerFnTraitObj { - type Target = server_fn::ServerFnTraitObj<()>; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -#[cfg(feature = "server")] -impl std::ops::DerefMut for ServerFnTraitObj { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } -} - -#[cfg(feature = "server")] -impl ServerFnTraitObj { - fn new( - prefix: &'static str, - url: &'static str, - encoding: server_fn::Encoding, - run: ServerFunction, - ) -> Self { - Self(server_fn::ServerFnTraitObj::new(prefix, url, encoding, run)) - } - - /// 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 = "server")] -server_fn::inventory::collect!(ServerFnTraitObj); - -#[cfg(feature = "server")] -#[cfg_attr(docsrs, doc(cfg(feature = "server")))] -/// Middleware for a server function -pub struct ServerFnMiddleware { - /// The prefix of the server function. - pub prefix: &'static str, - /// The url of the server function. - pub url: &'static str, - /// The middleware layers. - pub middleware: fn() -> Vec>, -} - -#[cfg(feature = "server")] -pub(crate) static MIDDLEWARE: once_cell::sync::Lazy< - std::collections::HashMap< - (&'static str, &'static str), - Vec>, - >, -> = once_cell::sync::Lazy::new(|| { - let mut map: std::collections::HashMap< - (&'static str, &'static str), - Vec>, - > = std::collections::HashMap::new(); - for middleware in server_fn::inventory::iter:: { - map.entry((middleware.prefix, middleware.url)) - .or_default() - .extend((middleware.middleware)().iter().cloned()); - } - map -}); - -#[cfg(feature = "server")] -server_fn::inventory::collect!(ServerFnMiddleware); - -#[cfg(feature = "server")] -#[cfg_attr(docsrs, doc(cfg(feature = "server")))] -/// A server function that can be called on serializable arguments and returns a serializable result. -pub type ServerFunction = server_fn::SerializedFnTraitObj<()>; - -#[cfg(feature = "server")] -#[allow(clippy::type_complexity)] -static REGISTERED_SERVER_FUNCTIONS: once_cell::sync::Lazy< - std::sync::Arc>>, -> = once_cell::sync::Lazy::new(|| { - let mut map = std::collections::HashMap::new(); - for server_fn in server_fn::inventory::iter:: { - map.insert(server_fn.0.url(), server_fn.clone()); - } - std::sync::Arc::new(std::sync::RwLock::new(map)) -}); - -#[cfg(feature = "server")] -#[cfg_attr(docsrs, doc(cfg(feature = "server")))] -/// The registry of all Dioxus server functions. -pub struct DioxusServerFnRegistry; - -#[cfg(feature = "server")] -impl server_fn::ServerFunctionRegistry<()> for DioxusServerFnRegistry { - type Error = ServerRegistrationFnError; - - fn register_explicit( - prefix: &'static str, - url: &'static str, - server_function: ServerFunction, - encoding: server_fn::Encoding, - ) -> Result<(), Self::Error> { - // store it in the hashmap - let mut write = REGISTERED_SERVER_FUNCTIONS - .write() - .map_err(|e| ServerRegistrationFnError::Poisoned(e.to_string()))?; - let prev = write.insert( - url, - 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> { - REGISTERED_SERVER_FUNCTIONS - .read() - .ok() - .and_then(|fns| fns.get(url).map(|inner| inner.0.clone())) - } - - /// 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> { - Self::get(url) - } - - fn get_encoding(url: &str) -> Option { - REGISTERED_SERVER_FUNCTIONS - .read() - .ok() - .and_then(|fns| fns.get(url).map(|f| f.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(feature = "server")] -#[cfg_attr(docsrs, doc(cfg(feature = "server")))] -/// Errors that can occur when registering a server function. -#[derive(thiserror::Error, Debug, Clone, serde::Serialize, serde::Deserialize)] -#[non_exhaustive] -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), -} diff --git a/packages/server-macro/Cargo.toml b/packages/server-macro/Cargo.toml index 33e0fabde..dbcb69b7b 100644 --- a/packages/server-macro/Cargo.toml +++ b/packages/server-macro/Cargo.toml @@ -18,6 +18,12 @@ quote = "^1.0.26" syn = { workspace = true, features = ["full"] } server_fn_macro = "0.6.11" +[dev-dependencies] +dioxus = { workspace = true, features = ["fullstack"] } +serde = { workspace = true, features = ["derive"] } +tower-http = { workspace = true, features = ["timeout"]} +axum = { workspace = true } + [lib] proc-macro = true diff --git a/packages/server-macro/src/lib.rs b/packages/server-macro/src/lib.rs index a397a1da8..d3d50f164 100644 --- a/packages/server-macro/src/lib.rs +++ b/packages/server-macro/src/lib.rs @@ -15,6 +15,10 @@ use syn::__private::ToTokens; /// /// ## Usage /// ```rust,ignore +/// # use dioxus::prelude::*; +/// # #[derive(serde::Deserialize, serde::Serialize)] +/// # pub struct BlogPost; +/// # async fn load_posts(category: &str) -> Result, ServerFnError> { unimplemented!() } /// #[server] /// pub async fn blog_posts( /// category: String, @@ -68,6 +72,75 @@ use syn::__private::ToTokens; /// // etc. /// } /// ``` +/// +/// ## Adding layers to server functions +/// +/// Layers allow you to transform the request and response of a server function. You can use layers +/// to add authentication, logging, or other functionality to your server functions. Server functions integrate +/// with the tower ecosystem, so you can use any layer that is compatible with tower. +/// +/// Common layers include: +/// - [`tower_http::trace::TraceLayer`](https://docs.rs/tower-http/latest/tower_http/trace/struct.TraceLayer.html) for tracing requests and responses +/// - [`tower_http::compression::CompressionLayer`](https://docs.rs/tower-http/latest/tower_http/compression/struct.CompressionLayer.html) for compressing large responses +/// - [`tower_http::cors::CorsLayer`](https://docs.rs/tower-http/latest/tower_http/cors/struct.CorsLayer.html) for adding CORS headers to responses +/// - [`tower_http::timeout::TimeoutLayer`](https://docs.rs/tower-http/latest/tower_http/timeout/struct.TimeoutLayer.html) for adding timeouts to requests +/// - [`tower_sessions::service::SessionManagerLayer`](https://docs.rs/tower-sessions/0.13.0/tower_sessions/service/struct.SessionManagerLayer.html) for adding session management to requests +/// +/// You can add a tower [`Layer`](https://docs.rs/tower/latest/tower/trait.Layer.html) to your server function with the middleware attribute: +/// +/// ```rust,ignore +/// # use dioxus::prelude::*; +/// #[server] +/// // The TraceLayer will log all requests to the console +/// #[middleware(tower_http::timeout::TimeoutLayer::new(std::time::Duration::from_secs(5)))] +/// pub async fn my_wacky_server_fn(input: Vec) -> Result { +/// unimplemented!() +/// } +/// ``` +/// +/// ## Extracting additional data from requests +/// +/// Server functions automatically handle serialization and deserialization of arguments and responses. +/// However, you may want to extract additional data from the request, such as the user's session or +/// authentication information. You can do this with the `extract` function. This function returns any +/// type that implements the [`FromRequestParts`](https://docs.rs/axum/latest/axum/extract/trait.FromRequestParts.html) +/// trait: +/// +/// ```rust,ignore +/// # use dioxus::prelude::*; +/// #[server] +/// pub async fn my_wacky_server_fn(input: Vec) -> Result { +/// let headers: axum::http::header::HeaderMap = extract().await?; +/// Ok(format!("The server got a request with headers: {:?}", headers)) +/// } +/// ``` +/// +/// ## Sharing data with server functions +/// +/// You may need to share context with your server functions like a database pool. Server +/// functions can access any context provided through the launch builder. You can access +/// this context with the `FromContext` extractor: +/// +/// ```rust,ignore +/// # use dioxus::prelude::*; +/// # fn app() -> Element { unimplemented!() } +/// #[derive(Clone, Copy, Debug)] +/// struct DatabasePool; +/// +/// fn main() { +/// LaunchBuilder::new() +/// .with_context(server_only! { +/// DatabasePool +/// }) +/// .launch(app); +/// } +/// +/// #[server] +/// pub async fn my_wacky_server_fn(input: Vec) -> Result { +/// let FromContext(pool): FromContext = extract().await?; +/// Ok(format!("The server read {:?} from the shared context", pool)) +/// } +/// ``` #[proc_macro_attribute] pub fn server(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream { match server_macro_impl(