bump server functions

This commit is contained in:
Evan Almloff 2024-02-16 12:17:01 -06:00
parent 6879507652
commit e745212157
13 changed files with 297 additions and 616 deletions

134
Cargo.lock generated
View file

@ -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",

View file

@ -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",

View file

@ -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<S> {
/// 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<Body>| async move {
/// let (parts, body) = req.into_parts();
/// let parts: Arc<http::request::Parts> = 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<H, T>(
self,
server_fn_route: &'static str,
handler: impl FnMut(server_fn::ServerFnTraitObj<()>) -> H,
) -> Self
where
H: Handler<T, S>,
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<S> {
/// .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<S> {
/// ```
fn serve_dioxus_application(
self,
server_fn_route: &'static str,
cfg: impl Into<ServeConfig>,
build_virtual_dom: impl Fn() -> VirtualDom + Send + Sync + 'static,
) -> Self;
@ -228,51 +191,20 @@ impl<S> DioxusRouterExt<S> for Router<S>
where
S: Send + Sync + Clone + 'static,
{
fn register_server_fns_with_handler<H, T>(
self,
server_fn_route: &'static str,
mut handler: impl FnMut(server_fn::ServerFnTraitObj<()>) -> H,
) -> Self
where
H: Handler<T, S>,
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<Body>| {
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<std::path::PathBuf>) -> Self {
@ -317,7 +249,6 @@ where
fn serve_dioxus_application(
self,
server_fn_route: &'static str,
cfg: impl Into<ServeConfig>,
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<tokio_util::task::LocalPoolHandle> = 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<Body>,
) -> 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::<NoCustomError>::ServerError(e.to_string())
.ser()
.unwrap_or_default(),
)
.into_response()
})
}

View file

@ -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())

View file

@ -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<L> Layer for L
where
L: tower_layer::Layer<BoxedService> + 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<MyBody>,
) -> Pin<
Box<
dyn std::future::Future<Output = Result<Response<MyBody>, server_fn::ServerFnError>>
+ Send,
>,
>;
}
impl<S> Service for S
where
S: tower::Service<http::Request<MyBody>, Response = Response<MyBody>>,
S::Future: Send + 'static,
S::Error: Into<server_fn::ServerFnError>,
{
fn run(
&mut self,
req: http::Request<MyBody>,
) -> Pin<
Box<
dyn std::future::Future<Output = Result<Response<MyBody>, server_fn::ServerFnError>>
+ Send,
>,
> {
let fut = self.call(req).instrument(tracing::trace_span!(
"service",
"{}",
std::any::type_name::<S>()
));
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<dyn Service + Send>);
impl tower::Service<http::Request<MyBody>> for BoxedService {
type Response = http::Response<MyBody>;
type Error = server_fn::ServerFnError;
type Future = Pin<
Box<
dyn std::future::Future<
Output = Result<http::Response<MyBody>, server_fn::ServerFnError>,
> + Send,
>,
>;
fn poll_ready(
&mut self,
_cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Result<(), Self::Error>> {
Ok(()).into()
}
fn call(&mut self, req: Request<MyBody>) -> Self::Future {
self.0.run(req)
}
}

View file

@ -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

View file

@ -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::<crate::server_fn::collection::DioxusServerFnRegistry>()
}
}
impl<T> DioxusServerFn for T where T: server_fn::ServerFn<()> {}

View file

@ -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<DioxusServerContext>,
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<AxumBody>,
) -> std::pin::Pin<
Box<
dyn std::future::Future<
Output = Result<http::Response<AxumBody>, 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<tokio_util::task::LocalPoolHandle> = OnceCell::new();
LOCAL_POOL
.get_or_init(|| {
tokio_util::task::LocalPoolHandle::new(
std::thread::available_parallelism()
.map(Into::into)
.unwrap_or(1),
)
})
.clone()
}

View file

@ -43,6 +43,7 @@ dioxus = { workspace = true }
[features]
default = ["hot-reload"]
axum = ["dep:axum"]
hot-reload = ["dioxus-hot-reload"]
[[example]]

View file

@ -1,7 +1,6 @@
use dioxus_core::*;
use std::any::Any;
#[cfg(feature = "axum")]
pub type Config = crate::Config<axum::Router>;
/// Launches the WebView and runs the event loop, with configuration and root props.

View file

@ -15,6 +15,7 @@ mod config;
mod eval;
mod events;
pub use config::*;
#[cfg(feature = "axum")]
pub mod launch;
pub trait WebsocketTx: SinkExt<String, Error = LiveViewError> {}

View file

@ -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"]

View file

@ -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 its 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<Vec<Post>, ServerFnError> {
/// // do some work on the server to access the database
/// # unimplemented!()
/// ## Usage
/// ```rust,ignore
/// #[server]
/// pub async fn blog_posts(
/// category: String,
/// ) -> Result<Vec<BlogPost>, ServerFnError> {
/// let posts = load_posts(&category).await?;
/// // maybe do some other work
/// Ok(posts)
/// }
/// ```
///
/// 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 clients perspective it involves an asynchronous
/// function call.
/// - **Server functions must return `Result<T, ServerFnError>`.** Even if the work being done
/// inside the function body cant 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 functions 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<String>) -> Result<usize, ServerFnError> {
/// todo!()
/// }
///
/// // expands to
/// #[derive(Deserialize, Serialize)]
/// struct SomeStructName {
/// input: Vec<String>
/// }
///
/// impl ServerFn for SomeStructName {
/// const PATH: &'static str = "/my_api/my_fn";
///
/// // etc.
/// }
/// ```
#[proc_macro_attribute]
pub fn server(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
// 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<Middleware> = 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<Self> {
let arg: syn::Expr = input.parse()?;
Ok(Middleware { expr: arg })
}
}
struct ServerFnArgs {
struct_name: Option<Ident>,
_comma: Option<Token![,]>,
prefix: Option<Literal>,
_comma2: Option<Token![,]>,
encoding: Option<Literal>,
_comma3: Option<Token![,]>,
fn_path: Option<Literal>,
}
impl ToTokens for ServerFnArgs {
fn to_tokens(&self, tokens: &mut TokenStream2) {
let struct_name = self.struct_name.as_ref().map(|s| quote::quote! { #s, });
let prefix = self.prefix.as_ref().map(|p| quote::quote! { #p, });
let encoding = self.encoding.as_ref().map(|e| quote::quote! { #e, });
let fn_path = self.fn_path.as_ref().map(|f| quote::quote! { #f, });
tokens.extend(quote::quote! {
#struct_name
#prefix
#encoding
#fn_path
})
}
}
impl Parse for ServerFnArgs {
fn parse(input: ParseStream) -> syn::Result<Self> {
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(),
}
}