diff --git a/Cargo.lock b/Cargo.lock index e62368d12..a3b1b7b32 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -622,6 +622,7 @@ dependencies = [ "matchit", "memchr", "mime", + "multer", "percent-encoding", "pin-project-lite", "rustversion", @@ -2692,7 +2693,7 @@ dependencies = [ "dioxus-router-macro", "dioxus-ssr", "gloo", - "gloo-utils", + "gloo-utils 0.1.7", "js-sys", "serde", "serde_json", @@ -4086,7 +4087,7 @@ dependencies = [ "gloo-render", "gloo-storage", "gloo-timers", - "gloo-utils", + "gloo-utils 0.1.7", "gloo-worker", ] @@ -4096,7 +4097,7 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "82b7ce3c05debe147233596904981848862b068862e9ec3e34be446077190d3f" dependencies = [ - "gloo-utils", + "gloo-utils 0.1.7", "js-sys", "serde", "wasm-bindgen", @@ -4142,7 +4143,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85725d90bf0ed47063b3930ef28e863658a7905989e9929a8708aab74a1d5e7f" dependencies = [ "gloo-events", - "gloo-utils", + "gloo-utils 0.1.7", "serde", "serde-wasm-bindgen", "serde_urlencoded", @@ -4151,26 +4152,6 @@ dependencies = [ "web-sys", ] -[[package]] -name = "gloo-net" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9902a044653b26b99f7e3693a42f171312d9be8b26b5697bd1e43ad1f8a35e10" -dependencies = [ - "futures-channel", - "futures-core", - "futures-sink", - "gloo-utils", - "js-sys", - "pin-project", - "serde", - "serde_json", - "thiserror", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - [[package]] name = "gloo-net" version = "0.3.1" @@ -4180,7 +4161,28 @@ dependencies = [ "futures-channel", "futures-core", "futures-sink", - "gloo-utils", + "gloo-utils 0.1.7", + "http 0.2.11", + "js-sys", + "pin-project", + "serde", + "serde_json", + "thiserror", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "gloo-net" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43aaa242d1239a8822c15c645f02166398da4f8b5c4bae795c1f5b44e9eee173" +dependencies = [ + "futures-channel", + "futures-core", + "futures-sink", + "gloo-utils 0.2.0", "http 0.2.11", "js-sys", "pin-project", @@ -4208,7 +4210,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d6ab60bf5dbfd6f0ed1f7843da31b41010515c745735c970e821945ca91e480" dependencies = [ - "gloo-utils", + "gloo-utils 0.1.7", "js-sys", "serde", "serde_json", @@ -4240,6 +4242,19 @@ dependencies = [ "web-sys", ] +[[package]] +name = "gloo-utils" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5555354113b18c547c1d3a98fbf7fb32a9ff4f6fa112ce823a21641a0ba3aa" +dependencies = [ + "js-sys", + "serde", + "serde_json", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "gloo-worker" version = "0.2.1" @@ -4249,7 +4264,7 @@ dependencies = [ "anymap2", "bincode", "gloo-console", - "gloo-utils", + "gloo-utils 0.1.7", "js-sys", "serde", "wasm-bindgen", @@ -6008,6 +6023,24 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "multer" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a15d522be0a9c3e46fd2632e272d178f56387bdb5c9fbb3a36c649062e9b5219" +dependencies = [ + "bytes", + "encoding_rs", + "futures-util", + "http 1.0.0", + "httparse", + "log", + "memchr", + "mime", + "spin 0.9.8", + "version_check", +] + [[package]] name = "names" version = "0.14.0" @@ -7608,6 +7641,7 @@ dependencies = [ "js-sys", "log", "mime", + "mime_guess", "native-tls", "once_cell", "percent-encoding", @@ -8097,6 +8131,15 @@ dependencies = [ "serde", ] +[[package]] +name = "send_wrapper" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73" +dependencies = [ + "futures-core", +] + [[package]] name = "separator" version = "0.4.1" @@ -8260,49 +8303,58 @@ dependencies = [ [[package]] name = "server_fn" -version = "0.5.7" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c265de965fe48e09ad8899d0ab1ffebdfa1a9914e4de5ff107b07bd94cf7541" +checksum = "97fab54d9dd2d7e9eba4efccac41d2ec3e7c6e9973d14c0486d662a32662320c" dependencies = [ - "ciborium", + "axum", + "bytes", "const_format", - "gloo-net 0.2.6", + "dashmap", + "futures", + "gloo-net 0.5.0", + "http 1.0.0", + "http-body-util", + "hyper 1.1.0", "inventory", "js-sys", - "lazy_static", "once_cell", - "proc-macro2", - "quote", "reqwest", + "send_wrapper", "serde", "serde_json", "serde_qs", "server_fn_macro_default", - "syn 2.0.49", "thiserror", + "tower", + "tower-layer", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", "xxhash-rust", ] [[package]] name = "server_fn_macro" -version = "0.5.7" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f77000541a62ceeec01eef3ee0f86c155c33dac5fae750ad04a40852c6d5469a" +checksum = "3be6011b586a0665546b7ced372b0be690d9e005d3f8524795da2843274d7720" dependencies = [ "const_format", - "proc-macro-error", + "convert_case 0.6.0", "proc-macro2", "quote", - "serde", "syn 2.0.49", "xxhash-rust", ] [[package]] name = "server_fn_macro_default" -version = "0.5.7" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a3353f22e2bcc451074d4feaa37317d9d17dff11d4311928384734ea17ab9ca" +checksum = "752ed78ec49132d154b922cf5ab6485680cab039a75740c48ea2db621ad481da" dependencies = [ "server_fn_macro", "syn 2.0.49", diff --git a/packages/fullstack/Cargo.toml b/packages/fullstack/Cargo.toml index b20d5feae..d80f7caf7 100644 --- a/packages/fullstack/Cargo.toml +++ b/packages/fullstack/Cargo.toml @@ -12,9 +12,8 @@ resolver = "2" [dependencies] # server functions -server_fn = { version = "0.5.2", default-features = false } -dioxus_server_macro = { workspace = true } - +server_fn = { version = "0.6.5", features = ["json", "url", "browser"], default-features = false } +dioxus_server_macro = { workspace = true, version = "0.6.5", default-features = false } # axum axum = { workspace = true, features = ["ws", "macros"], default-features = false, optional = true } @@ -71,7 +70,7 @@ desktop = ["dioxus-desktop"] mobile = ["dioxus-mobile"] default-tls = ["server_fn/default-tls"] rustls = ["server_fn/rustls"] -axum = ["dep:axum", "tower-http", "server"] +axum = ["dep:axum", "tower-http", "server", "server_fn/axum", "dioxus_server_macro/axum"] server = [ "server_fn/ssr", "dioxus_server_macro/server", diff --git a/packages/fullstack/src/axum_adapter.rs b/packages/fullstack/src/axum_adapter.rs index c56864728..40df37083 100644 --- a/packages/fullstack/src/axum_adapter.rs +++ b/packages/fullstack/src/axum_adapter.rs @@ -57,61 +57,25 @@ use axum::{ body::{self, Body}, extract::State, - handler::Handler, http::{Request, Response, StatusCode}, response::IntoResponse, routing::{get, post}, Router, }; use dioxus_lib::prelude::VirtualDom; -use server_fn::{Encoding, ServerFunctionRegistry}; +use server_fn::error::NoCustomError; +use server_fn::error::ServerFnErrorSerde; use std::sync::Arc; use std::sync::RwLock; +use axum::routing::*; +use http::header::*; use crate::{ prelude::*, render::SSRState, serve_config::ServeConfig, server_context::DioxusServerContext, - server_fn::collection::DioxusServerFnRegistry, }; /// A extension trait with utilities for integrating Dioxus with your Axum router. pub trait DioxusRouterExt { - /// Registers server functions with a custom handler function. This allows you to pass custom context to your server functions by generating a [`DioxusServerContext`] from the request. - /// - /// # 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_fns_with_handler("", |func| { - /// move |req: Request| async move { - /// let (parts, body) = req.into_parts(); - /// let parts: Arc = Arc::new(parts.into()); - /// let server_context = DioxusServerContext::new(parts.clone()); - /// server_fn_handler(server_context, func.clone(), parts, body).await - /// } - /// }) - /// .into_make_service(), - /// ) - /// .await - /// .unwrap(); - /// } - /// ``` - fn register_server_fns_with_handler( - self, - server_fn_route: &'static str, - handler: impl FnMut(server_fn::ServerFnTraitObj<()>) -> H, - ) -> Self - where - H: Handler, - T: 'static, - S: Clone + Send + Sync + 'static; - /// Registers server functions with the default handler. This handler function will pass an empty [`DioxusServerContext`] to your server functions. /// /// # Example @@ -133,7 +97,7 @@ pub trait DioxusRouterExt { /// .unwrap(); /// } /// ``` - fn register_server_fns(self, server_fn_route: &'static str) -> Self; + fn register_server_fns(self) -> Self; /// Register the web RSX hot reloading endpoint. This will enable hot reloading for your application in debug mode when you call [`dioxus_hot_reload::hot_reload_init`]. /// @@ -218,7 +182,6 @@ pub trait DioxusRouterExt { /// ``` fn serve_dioxus_application( self, - server_fn_route: &'static str, cfg: impl Into, build_virtual_dom: impl Fn() -> VirtualDom + Send + Sync + 'static, ) -> Self; @@ -228,51 +191,20 @@ impl DioxusRouterExt for Router where S: Send + Sync + Clone + 'static, { - fn register_server_fns_with_handler( - self, - server_fn_route: &'static str, - mut handler: impl FnMut(server_fn::ServerFnTraitObj<()>) -> 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}"); - match func.encoding() { - Encoding::Url | Encoding::Cbor => { - router = router.route(&full_route, post(handler(func))); - } - Encoding::GetJSON | Encoding::GetCBOR => { - router = router.route(&full_route, get(handler(func))); - } - } - } - router - } + fn register_server_fns(mut self) -> Self { + use http::method::Method; - fn register_server_fns(self, server_fn_route: &'static str) -> Self { - self.register_server_fns_with_handler(server_fn_route, |func| { - move |req: Request| { - let mut service = crate::server_fn_service(Default::default(), func); - async move { - let (req, body) = req.into_parts(); - let req = Request::from_parts(req, body); - let res = service.run(req); - match res.await { - Ok(res) => Ok::<_, std::convert::Infallible>(res.map(|b| b.into())), - Err(e) => { - let mut res = Response::new(Body::from(e.to_string())); - *res.status_mut() = StatusCode::INTERNAL_SERVER_ERROR; - Ok(res) - } - } - } - } - }) + for (path, method) in server_fn::axum::server_fn_paths() { + let handler = move |req| handle_server_fns_inner(path, ||{}, req); + self = match method { + Method::GET => self.route(path, get(handler)), + Method::POST => self.route(path, post(handler)), + Method::PUT => self.route(path, put(handler)), + _ => todo!() + }; + } + + self } fn serve_static_assets(mut self, assets_path: impl Into) -> Self { @@ -317,7 +249,6 @@ where fn serve_dioxus_application( self, - server_fn_route: &'static str, cfg: impl Into, build_virtual_dom: impl Fn() -> VirtualDom + Send + Sync + 'static, ) -> Self { @@ -327,7 +258,7 @@ where // Add server functions and render index.html self.serve_static_assets(cfg.assets_path.clone()) .connect_hot_reload() - .register_server_fns(server_fn_route) + .register_server_fns() .fallback(get(render_handler).with_state((cfg, Arc::new(build_virtual_dom), ssr_state))) } @@ -516,3 +447,104 @@ pub async fn hot_reload_handler(ws: axum::extract::WebSocketUpgrade) -> impl Int } }) } + +fn get_local_pool() -> tokio_util::task::LocalPoolHandle { + use once_cell::sync::OnceCell; + static LOCAL_POOL: OnceCell = OnceCell::new(); + LOCAL_POOL + .get_or_init(|| { + tokio_util::task::LocalPoolHandle::new( + std::thread::available_parallelism() + .map(Into::into) + .unwrap_or(1), + ) + }) + .clone() +} + +/// 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, + req: Request, +) -> impl IntoResponse { + use server_fn::middleware::Service; + + let (tx, rx) = tokio::sync::oneshot::channel(); + let path_string = path.to_string(); + + get_local_pool().spawn_pinned(move || async move { + let (parts, body) = req.into_parts(); + let req = Request::from_parts(parts.clone(), body); + + + let res = if let Some(mut service) = + server_fn::axum::get_server_fn_service(&path_string) + { + + let server_context = DioxusServerContext::new(Arc::new(RwLock::new(parts))); + additional_context(); + + // store Accepts and Referrer 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; + + + // 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 the response parts from the server context to the response + let mut res_options = server_context.response_parts_mut().unwrap(); + res.headers_mut().extend(res_options.headers.drain()); + + Ok(res) + } else { + Response::builder().status(StatusCode::BAD_REQUEST).body( + { + #[cfg(target_family = "wasm")] + { + Body::from(format!( + "No server function found for path: {path_string}\nYou may need to explicitly register the server function with `register_explicit`, rebuild your wasm binary to update a server function link or make sure the prefix your server and client use for server functions match.", + )) + } + #[cfg(not(target_family = "wasm"))] + { + Body::from(format!( + "No server function found for path: {path_string}\nYou may need to rebuild your wasm binary to update a server function link or make sure the prefix your server and client use for server functions match.", + )) + } + } + ) + } + .expect("could not build Response"); + + _ = tx.send(res); + }); + + rx.await.unwrap_or_else(|e| { + ( + StatusCode::INTERNAL_SERVER_ERROR, + ServerFnError::::ServerError(e.to_string()) + .ser() + .unwrap_or_default(), + ) + .into_response() + }) +} \ No newline at end of file diff --git a/packages/fullstack/src/config.rs b/packages/fullstack/src/config.rs index d03c90752..a0f7211c6 100644 --- a/packages/fullstack/src/config.rs +++ b/packages/fullstack/src/config.rs @@ -129,7 +129,7 @@ impl Config { use tower::ServiceBuilder; let ssr_state = SSRState::new(&cfg); - let router = axum::Router::new().register_server_fns(server_fn_route); + let router = axum::Router::new().register_server_fns(); #[cfg(not(any(feature = "desktop", feature = "mobile")))] let router = router .serve_static_assets(cfg.assets_path.clone()) diff --git a/packages/fullstack/src/layer.rs b/packages/fullstack/src/layer.rs deleted file mode 100644 index 82d0226cb..000000000 --- a/packages/fullstack/src/layer.rs +++ /dev/null @@ -1,86 +0,0 @@ -use std::pin::Pin; -use tracing_futures::Instrument; - -use http::{Request, Response}; - -/// A layer that wraps a service. This can be used to add additional information to the request, or response on top of some other service -pub trait Layer: Send + Sync + 'static { - /// Wrap a boxed service with this layer - fn layer(&self, inner: BoxedService) -> BoxedService; -} - -impl Layer for L -where - L: tower_layer::Layer + Sync + Send + 'static, - L::Service: Service + Send + 'static, -{ - fn layer(&self, inner: BoxedService) -> BoxedService { - BoxedService(Box::new(self.layer(inner))) - } -} - -type MyBody = axum::body::Body; - -/// A service is a function that takes a request and returns an async response -pub trait Service { - /// Run the service and produce a future that resolves to a response - fn run( - &mut self, - req: http::Request, - ) -> Pin< - Box< - dyn std::future::Future, server_fn::ServerFnError>> - + Send, - >, - >; -} - -impl Service for S -where - S: tower::Service, Response = Response>, - S::Future: Send + 'static, - S::Error: Into, -{ - fn run( - &mut self, - req: http::Request, - ) -> Pin< - Box< - dyn std::future::Future, server_fn::ServerFnError>> - + Send, - >, - > { - let fut = self.call(req).instrument(tracing::trace_span!( - "service", - "{}", - std::any::type_name::() - )); - Box::pin(async move { fut.await.map_err(|err| err.into()) }) - } -} - -/// A boxed service is a type-erased service that can be used without knowing the underlying type -pub struct BoxedService(pub Box); - -impl tower::Service> for BoxedService { - type Response = http::Response; - type Error = server_fn::ServerFnError; - type Future = Pin< - Box< - dyn std::future::Future< - Output = Result, server_fn::ServerFnError>, - > + Send, - >, - >; - - fn poll_ready( - &mut self, - _cx: &mut std::task::Context<'_>, - ) -> std::task::Poll> { - Ok(()).into() - } - - fn call(&mut self, req: Request) -> Self::Future { - self.0.run(req) - } -} diff --git a/packages/fullstack/src/lib.rs b/packages/fullstack/src/lib.rs index 8ba30a5be..d13fcb695 100644 --- a/packages/fullstack/src/lib.rs +++ b/packages/fullstack/src/lib.rs @@ -12,21 +12,14 @@ mod html_storage; #[cfg(feature = "axum")] mod axum_adapter; -#[cfg_attr(docsrs, doc(cfg(feature = "server")))] -#[cfg(feature = "server")] -pub use server_fn::service::{server_fn_service, ServerFnHandler}; - mod config; mod hooks; pub mod launch; -mod server_fn; #[cfg(all(debug_assertions, feature = "hot-reload", feature = "server"))] mod hot_reload; pub use config::*; -#[cfg(feature = "server")] -mod layer; #[cfg(feature = "server")] mod render; @@ -50,10 +43,6 @@ pub mod prelude { #[cfg_attr(docsrs, doc(cfg(not(feature = "server"))))] pub use crate::html_storage::deserialize::get_root_props_from_document; - #[cfg(feature = "server")] - #[cfg_attr(docsrs, doc(cfg(feature = "server")))] - pub use crate::layer::{Layer, Service}; - #[cfg(all(feature = "server", feature = "router"))] #[cfg_attr(docsrs, doc(cfg(all(feature = "server", feature = "router"))))] pub use crate::render::pre_cache_static_routes_with_props; @@ -79,17 +68,13 @@ pub mod prelude { pub use crate::server_context::{ extract, server_context, DioxusServerContext, FromServerContext, ProvideServerContext, }; - pub use crate::server_fn::DioxusServerFn; - - #[cfg(feature = "server")] - #[cfg_attr(docsrs, doc(cfg(feature = "server")))] - pub use crate::server_fn::collection::{ServerFnMiddleware, ServerFnTraitObj, ServerFunction}; - pub use dioxus_server_macro::*; #[cfg(feature = "server")] #[cfg_attr(docsrs, doc(cfg(feature = "server")))] pub use dioxus_ssr::incremental::IncrementalRendererConfig; + pub use server_fn::{self, ServerFn as _, ServerFnError}; + pub use dioxus_server_macro::*; } // // Warn users about overlapping features diff --git a/packages/fullstack/src/server_fn/mod.rs b/packages/fullstack/src/server_fn/mod.rs deleted file mode 100644 index e2637b23b..000000000 --- a/packages/fullstack/src/server_fn/mod.rs +++ /dev/null @@ -1,24 +0,0 @@ -#[cfg(feature = "server")] -pub(crate)mod collection; -#[cfg(feature = "server")] -pub mod service; - -/// 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. -/// -/// 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::ServerFunctionRegistry::get]. -/// -/// Technically, the trait is implemented on a type that describes the server function's arguments, not the function itself. -pub trait DioxusServerFn: server_fn::ServerFn<()> { - /// Registers the server function, allowing the client to query it by URL. - #[cfg(feature = "server")] - #[cfg_attr(docsrs, doc(cfg(feature = "server")))] - fn register_explicit() -> Result<(), server_fn::ServerFnError> { - Self::register_in_explicit::() - } -} - -impl DioxusServerFn for T where T: server_fn::ServerFn<()> {} diff --git a/packages/fullstack/src/server_fn/service.rs b/packages/fullstack/src/server_fn/service.rs deleted file mode 100644 index 22f1a176c..000000000 --- a/packages/fullstack/src/server_fn/service.rs +++ /dev/null @@ -1,154 +0,0 @@ -//! # Server function Service - -//! This module defines a service that can be used to handle server functions. - -use http::StatusCode; -use server_fn::{Encoding, Payload}; -use std::sync::{Arc, RwLock}; - -use crate::server_fn::collection::MIDDLEWARE; -use crate::{ - layer::{BoxedService, Service}, - prelude::{DioxusServerContext, ProvideServerContext}, -}; - -type AxumBody = axum::body::Body; - -/// Create a server function handler with the given server context and server function. -pub fn server_fn_service( - context: DioxusServerContext, - function: server_fn::ServerFnTraitObj<()>, -) -> crate::layer::BoxedService { - let prefix = function.prefix().to_string(); - let url = function.url().to_string(); - if let Some(middleware) = MIDDLEWARE.get(&(&prefix, &url)) { - let mut service = BoxedService(Box::new(ServerFnHandler::new(context, function))); - for middleware in middleware { - service = middleware.layer(service); - } - service - } else { - BoxedService(Box::new(ServerFnHandler::new(context, function))) - } -} - -#[derive(Clone)] -/// A default handler for server functions. It will deserialize the request body, call the server function, and serialize the response. -pub struct ServerFnHandler { - server_context: DioxusServerContext, - function: server_fn::ServerFnTraitObj<()>, -} - -impl ServerFnHandler { - /// Create a new server function handler with the given server context and server function. - pub fn new( - server_context: impl Into, - function: server_fn::ServerFnTraitObj<()>, - ) -> Self { - let server_context = server_context.into(); - Self { - server_context, - function, - } - } -} - -impl Service for ServerFnHandler { - fn run( - &mut self, - req: http::Request, - ) -> std::pin::Pin< - Box< - dyn std::future::Future< - Output = Result, server_fn::ServerFnError>, - > + Send, - >, - > { - let Self { - server_context, - function, - } = self.clone(); - Box::pin(async move { - let query = req.uri().query().unwrap_or_default().as_bytes().to_vec(); - let (parts, body) = req.into_parts(); - let body = axum::body::to_bytes(body, usize::MAX).await?.to_vec(); - let headers = &parts.headers; - let accept_header = headers.get("Accept").cloned(); - let parts = Arc::new(RwLock::new(parts)); - - // Because the future returned by `server_fn_handler` is `Send`, and the future returned by this function must be send, we need to spawn a new runtime - let pool = get_local_pool(); - let result = pool - .spawn_pinned({ - let function = function.clone(); - let mut server_context = server_context.clone(); - server_context.parts = parts; - move || async move { - let data = match function.encoding() { - Encoding::Url | Encoding::Cbor => &body, - Encoding::GetJSON | Encoding::GetCBOR => &query, - }; - let server_function_future = function.call((), data); - let server_function_future = ProvideServerContext::new( - server_function_future, - server_context.clone(), - ); - server_function_future.await - } - }) - .await?; - let mut res = http::Response::builder(); - - // Set the headers from the server context - let parts = server_context.response_parts().unwrap(); - *res.headers_mut().expect("empty headers should be valid") = parts.headers.clone(); - - let serialized = result?; - // if this is Accept: application/json then send a serialized JSON response - let accept_header = accept_header.as_ref().and_then(|value| value.to_str().ok()); - if accept_header == Some("application/json") - || accept_header - == Some( - "application/\ - x-www-form-urlencoded", - ) - || accept_header == Some("application/cbor") - { - res = res.status(StatusCode::OK); - } - - Ok(match serialized { - Payload::Binary(data) => { - res = res.header("Content-Type", "application/cbor"); - res.body(data.into())? - } - Payload::Url(data) => { - res = res.header( - "Content-Type", - "application/\ - x-www-form-urlencoded", - ); - res.body(data.into())? - } - Payload::Json(data) => { - res = res.header("Content-Type", "application/json"); - res.body(data.into())? - } - }) - }) - } -} - -fn get_local_pool() -> tokio_util::task::LocalPoolHandle { - use once_cell::sync::OnceCell; - static LOCAL_POOL: OnceCell = OnceCell::new(); - LOCAL_POOL - .get_or_init(|| { - tokio_util::task::LocalPoolHandle::new( - std::thread::available_parallelism() - .map(Into::into) - .unwrap_or(1), - ) - }) - .clone() -} diff --git a/packages/liveview/Cargo.toml b/packages/liveview/Cargo.toml index e01e6d3dd..6e7173e18 100644 --- a/packages/liveview/Cargo.toml +++ b/packages/liveview/Cargo.toml @@ -43,6 +43,7 @@ dioxus = { workspace = true } [features] default = ["hot-reload"] +axum = ["dep:axum"] hot-reload = ["dioxus-hot-reload"] [[example]] diff --git a/packages/liveview/src/launch.rs b/packages/liveview/src/launch.rs index de8142a4e..994e32769 100644 --- a/packages/liveview/src/launch.rs +++ b/packages/liveview/src/launch.rs @@ -1,7 +1,6 @@ use dioxus_core::*; use std::any::Any; -#[cfg(feature = "axum")] pub type Config = crate::Config; /// Launches the WebView and runs the event loop, with configuration and root props. diff --git a/packages/liveview/src/lib.rs b/packages/liveview/src/lib.rs index 075179d87..9ed7b85e8 100644 --- a/packages/liveview/src/lib.rs +++ b/packages/liveview/src/lib.rs @@ -15,6 +15,7 @@ mod config; mod eval; mod events; pub use config::*; +#[cfg(feature = "axum")] pub mod launch; pub trait WebsocketTx: SinkExt {} diff --git a/packages/server-macro/Cargo.toml b/packages/server-macro/Cargo.toml index 0234a5373..52c6975ff 100644 --- a/packages/server-macro/Cargo.toml +++ b/packages/server-macro/Cargo.toml @@ -17,11 +17,13 @@ proc-macro2 = "^1.0.63" quote = "^1.0.26" syn = { version = "2", features = ["full"] } convert_case = "^0.6.0" -server_fn_macro = "^0.5.2" +server_fn_macro = "^0.6.5" [lib] proc-macro = true [features] -default = [] +actix = ["server_fn_macro/actix"] +axum = ["server_fn_macro/axum"] +nightly = ["server_fn_macro/nightly"] server = ["server_fn_macro/ssr"] diff --git a/packages/server-macro/src/lib.rs b/packages/server-macro/src/lib.rs index 0f1908851..497297950 100644 --- a/packages/server-macro/src/lib.rs +++ b/packages/server-macro/src/lib.rs @@ -2,209 +2,83 @@ #![doc(html_logo_url = "https://avatars.githubusercontent.com/u/79236386")] #![doc(html_favicon_url = "https://avatars.githubusercontent.com/u/79236386")] -use convert_case::{Case, Converter}; -use proc_macro::TokenStream; -use proc_macro2::Literal; -use quote::{ToTokens, __private::TokenStream as TokenStream2}; -use server_fn_macro::*; -use syn::{ - parse::{Parse, ParseStream}, - Ident, ItemFn, Token, -}; +//! This crate contains the dioxus implementation of the #[macro@crate::server] macro without additional context from the server. +//! See the [server_fn_macro] crate for more information. -/// Declares that a function is a [server function](https://dioxuslabs.com/learn/0.4/reference/fullstack/server_functions). This means that -/// its body will only run on the server, i.e., when the `ssr` feature is enabled. +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. /// -/// 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, or three arguments to the server function: -/// 1. *Optional*: 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*: either `"Cbor"` (specifying that it should use the binary `cbor` format for -/// serialization), `"Url"` (specifying that it should be use a URL-encoded form-data string). -/// Defaults to `"Url"`. If you want to use this server function -/// using Get instead of Post methods, the encoding must be `"GetCbor"` or `"GetJson"`. -/// -/// 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 [DioxusServerContext](https::/docs.rs/dioxus-fullstack/latest/dixous_server/prelude/struct.DioxusServerContext.html), -/// 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 dioxus_fullstack::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 -/// # unimplemented!() +/// ## Usage +/// ```rust,ignore +/// #[server] +/// pub async fn blog_posts( +/// category: String, +/// ) -> Result, ServerFnError> { +/// let posts = load_posts(&category).await?; +/// // maybe do some other work +/// Ok(posts) /// } /// ``` /// -/// 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_urlencoded`](https://docs.rs/serde_urlencoded/latest/serde_urlencoded/) or as `application/cbor` -/// using [`cbor`](https://docs.rs/cbor/latest/cbor/). -/// - **The [DioxusServerContext](https::/docs.rs/dioxus-fullstack/latest/dixous_server/prelude/struct.DioxusServerContext.html) comes from the server.** Optionally, the first argument of a server function -/// can be a [DioxusServerContext](https::/docs.rs/dioxus-fullstack/latest/dixous_server/prelude/struct.DioxusServerContext.html). 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. +/// ## 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`) +/// - `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, +/// prefix = "/my_api", +/// endpoint = "my_fn", +/// input = Cbor, +/// output = Json +/// )] +/// pub async fn my_wacky_server_fn(input: Vec) -> Result { +/// todo!() +/// } +/// +/// // expands to +/// #[derive(Deserialize, Serialize)] +/// struct SomeStructName { +/// input: Vec +/// } +/// +/// 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 { - // before we pass this off to the server function macro, we apply extractors and middleware - let mut function: syn::ItemFn = match syn::parse(s).map_err(|e| e.to_compile_error()) { - Ok(f) => f, - Err(e) => return e.into(), - }; - - // extract all #[middleware] attributes - let mut middlewares: Vec = vec![]; - function.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 ItemFn { - attrs, - vis, - sig, - block, - } = function; - let mapped_body = quote::quote! { - #(#attrs)* - #vis #sig { - #block - } - }; - - let server_fn_path: syn::Path = syn::parse_quote!(::dioxus::fullstack::prelude::server_fn); - let trait_obj_wrapper: syn::Type = - syn::parse_quote!(::dioxus::fullstack::prelude::ServerFnTraitObj); - let mut args: ServerFnArgs = match syn::parse(args) { - Ok(args) => args, - Err(e) => return e.to_compile_error().into(), - }; - if args.struct_name.is_none() { - let upper_cammel_case_name = Converter::new() - .from_case(Case::Snake) - .to_case(Case::UpperCamel) - .convert(sig.ident.to_string()); - args.struct_name = Some(Ident::new(&upper_cammel_case_name, sig.ident.span())); - } - let struct_name = args.struct_name.as_ref().unwrap(); - match server_macro_impl( - quote::quote!(#args), - mapped_body, - trait_obj_wrapper, - None, - Some(server_fn_path.clone()), - ) { - Err(e) => e.to_compile_error().into(), - Ok(tokens) => quote::quote! { - #tokens - #[cfg(feature = "server")] - #server_fn_path::inventory::submit! { - ::dioxus::fullstack::prelude::ServerFnMiddleware { - prefix: #struct_name::PREFIX, - url: #struct_name::URL, - middleware: || vec![ - #( - std::sync::Arc::new(#middlewares), - ),* - ] - } - } - } - .to_token_stream() - .into(), - } -} - -#[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 }) - } -} - -struct ServerFnArgs { - struct_name: Option, - _comma: Option, - prefix: Option, - _comma2: Option, - encoding: Option, - _comma3: Option, - fn_path: Option, -} - -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, }); - tokens.extend(quote::quote! { - #struct_name - #prefix - #encoding - #fn_path - }) - } -} - -impl Parse for ServerFnArgs { - fn parse(input: ParseStream) -> syn::Result { - let struct_name = input.parse()?; - let _comma = input.parse()?; - let prefix = input.parse()?; - let _comma2 = input.parse()?; - let encoding = input.parse()?; - let _comma3 = input.parse()?; - let fn_path = input.parse()?; - - Ok(Self { - struct_name, - _comma, - prefix, - _comma2, - encoding, - _comma3, - fn_path, - }) - } + match server_macro_impl( + args.into(), + s.into(), + Some(syn::parse_quote!(server_fn)), + "/api", + None, + None, + ) { + Err(e) => e.to_compile_error().into(), + Ok(s) => s.to_token_stream().into(), + } }