make server function API more flexible

This commit is contained in:
Evan Almloff committed 2023-04-01 17:00:12 -05:00
1 parent 71ddd50963
commit c6992c7032
9 files changed
+209 -76

No files matched your search

+2 -1
View File
@@ -6,7 +6,7 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
server_fn = { version = "0.2.4", features = ["stable"] } server_fn = { path = "D:/Users/Desktop/github/leptos/server_fn", features = ["stable"] }
server_macro = { path = "server-macro" } server_macro = { path = "server-macro" }
# warp # warp
@@ -31,6 +31,7 @@ once_cell = "1.17.1"
thiserror = "1.0.40" thiserror = "1.0.40"
tokio = { version = "1.27.0", features = ["full"], optional = true } tokio = { version = "1.27.0", features = ["full"], optional = true }
object-pool = "0.5.4" object-pool = "0.5.4"
anymap = "0.12.1"
[features] [features]
default = [] default = []
+2 -1
View File
@@ -7,7 +7,8 @@ edition = "2021"
[dependencies] [dependencies]
quote = "1.0.26" quote = "1.0.26"
server_fn_macro = { version = "0.2.4", features = ["stable"] } # server_fn_macro = { git = "https://github.com/leptos-rs/leptos", rev = "1e037ecb60965c7c55fd781fdc8de7863ffd102b", features = ["stable"] }
server_fn_macro = { path = "D:/Users/Desktop/github/leptos/server_fn_macro", features = ["stable"] }
syn = { version = "1", features = ["full"] } syn = { version = "1", features = ["full"] }
[lib] [lib]
+7 -7
View File
@@ -2,7 +2,7 @@ use proc_macro::TokenStream;
use quote::ToTokens; use quote::ToTokens;
use server_fn_macro::*; use server_fn_macro::*;
/// Declares that a function is a [server function](leptos_server). This means that /// Declares that a function is a [server function](dioxus_server). This means that
/// its body will only run on the server, i.e., when the `ssr` feature is enabled. /// its body will only run on the server, i.e., when the `ssr` feature is enabled.
/// ///
/// If you call a server function from the client (i.e., when the `csr` or `hydrate` features /// If you call a server function from the client (i.e., when the `csr` or `hydrate` features
@@ -19,18 +19,18 @@ use server_fn_macro::*;
/// work without WebAssembly, the encoding must be `"Url"`. /// work without WebAssembly, the encoding must be `"Url"`.
/// ///
/// The server function itself can take any number of arguments, each of which should be serializable /// The server function itself can take any number of arguments, each of which should be serializable
/// and deserializable with `serde`. Optionally, its first argument can be a Leptos [Scope](leptos_reactive::Scope), /// and deserializable with `serde`. Optionally, its first argument can be a [DioxusServerContext](dioxus_server::prelude::DioxusServerContext),
/// which will be injected *on the server side.* This can be used to inject the raw HTTP request or other /// 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. /// server-side context into the server function.
/// ///
/// ```ignore /// ```ignore
/// # use leptos::*; use serde::{Serialize, Deserialize}; /// # use dioxus_server::prelude::*; use serde::{Serialize, Deserialize};
/// # #[derive(Serialize, Deserialize)] /// # #[derive(Serialize, Deserialize)]
/// # pub struct Post { } /// # pub struct Post { }
/// #[server(ReadPosts, "/api")] /// #[server(ReadPosts, "/api")]
/// pub async fn read_posts(how_many: u8, query: String) -> Result<Vec<Post>, ServerFnError> { /// pub async fn read_posts(how_many: u8, query: String) -> Result<Vec<Post>, ServerFnError> {
/// // do some work on the server to access the database /// // do some work on the server to access the database
/// todo!() /// todo!()
/// } /// }
/// ``` /// ```
/// ///
@@ -42,7 +42,7 @@ use server_fn_macro::*;
/// - **Server functions must return `Result<T, ServerFnError>`.** Even if the work being done /// - **Server functions must return `Result<T, ServerFnError>`.** Even if the work being done
/// inside the function body can’t fail, the processes of serialization/deserialization and the /// inside the function body can’t fail, the processes of serialization/deserialization and the
/// network call are fallible. /// network call are fallible.
/// - **Return types must be [Serializable](leptos_reactive::Serializable).** /// - **Return types must implement [`Serialize`](https://docs.rs/serde/latest/serde/trait.Serialize.html).**
/// This should be fairly obvious: we have to serialize arguments to send them to the server, and we /// 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. /// 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) /// - **Arguments must be implement [`Serialize`](https://docs.rs/serde/latest/serde/trait.Serialize.html)
@@ -50,8 +50,8 @@ use server_fn_macro::*;
/// They are serialized as an `application/x-www-form-urlencoded` /// 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` /// 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/). /// using [`cbor`](https://docs.rs/cbor/latest/cbor/).
/// - **The [Scope](leptos_reactive::Scope) comes from the server.** Optionally, the first argument of a server function /// - **The [DioxusServerContext](dioxus_server::prelude::DioxusServerContext) comes from the server.** Optionally, the first argument of a server function
/// can be a Leptos [Scope](leptos_reactive::Scope). This scope can be used to inject dependencies like the HTTP request /// can be a [DioxusServerContext](dioxus_server::prelude::DioxusServerContext). This scope can be used to inject dependencies like the HTTP request
/// or response or other server-only dependencies, but it does *not* have access to reactive state that exists in the client. /// or response or other server-only dependencies, but it does *not* have access to reactive state that exists in the client.
#[proc_macro_attribute] #[proc_macro_attribute]
pub fn server(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream { pub fn server(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
+55 -26
View File
@@ -2,7 +2,8 @@ use std::{error::Error, sync::Arc};
use axum::{ use axum::{
body::{self, Body, BoxBody, Full}, body::{self, Body, BoxBody, Full},
extract::{FromRef, State}, extract::State,
handler::Handler,
http::{HeaderMap, Request, Response, StatusCode}, http::{HeaderMap, Request, Response, StatusCode},
response::IntoResponse, response::IntoResponse,
routing::{get, post}, routing::{get, post},
@@ -14,61 +15,89 @@ use tokio::task::spawn_blocking;
use crate::{ use crate::{
render::SSRState, render::SSRState,
serve::ServeConfig, serve::ServeConfig,
server_fn::{DioxusServerContext, DioxusServerFnRegistry, ServerFnTraitObj}, server_context::DioxusServerContext,
server_fn::{DioxusServerFnRegistry, ServerFnTraitObj},
}; };
pub trait DioxusRouterExt { pub trait DioxusRouterExt<S> {
fn register_server_fns_with_handler<H, T>(
self,
server_fn_route: &'static str,
handler: impl Fn(Arc<ServerFnTraitObj>) -> H,
) -> Self
where
H: Handler<T, S>,
T: 'static,
S: Clone + Send + Sync + 'static;
fn register_server_fns(self, server_fn_route: &'static str) -> Self; fn register_server_fns(self, server_fn_route: &'static str) -> Self;
fn serve_dioxus_application<P: Clone + Send + Sync + 'static>( fn serve_dioxus_application<P: Clone + Send + Sync + 'static>(
self, self,
cfg: ServeConfig<P>, server_fn_route: &'static str,
server_fn_route: Option<&'static str>, cfg: impl Into<ServeConfig<P>>,
) -> Self; ) -> Self;
} }
impl<S> DioxusRouterExt for Router<S> impl<S> DioxusRouterExt<S> for Router<S>
where where
SSRState: FromRef<S>,
S: Send + Sync + Clone + 'static, S: Send + Sync + Clone + 'static,
{ {
fn register_server_fns(self, server_fn_route: &'static str) -> Self { fn register_server_fns_with_handler<H, T>(
self,
server_fn_route: &'static str,
mut handler: impl FnMut(Arc<ServerFnTraitObj>) -> H,
) -> Self
where
H: Handler<T, S, Body>,
T: 'static,
S: Clone + Send + Sync + 'static,
{
let mut router = self; let mut router = self;
for server_fn_path in DioxusServerFnRegistry::paths_registered() { for server_fn_path in DioxusServerFnRegistry::paths_registered() {
let func = DioxusServerFnRegistry::get(server_fn_path).unwrap(); let func = DioxusServerFnRegistry::get(server_fn_path).unwrap();
let full_route = format!("{server_fn_route}/{server_fn_path}"); let full_route = format!("{server_fn_route}/{server_fn_path}");
router = router.route( router = router.route(&full_route, post(handler(func)));
&full_route,
post(move |headers: HeaderMap, body: Request<Body>| async move {
server_fn_handler(DioxusServerContext {}, func.clone(), headers, body).await
}),
);
} }
router router
} }
fn register_server_fns(self, server_fn_route: &'static str) -> Self {
self.register_server_fns_with_handler(server_fn_route, |func| {
move |headers: HeaderMap, body: Request<Body>| async move {
server_fn_handler(DioxusServerContext::default(), func.clone(), headers, body).await
}
})
}
fn serve_dioxus_application<P: Clone + Send + Sync + 'static>( fn serve_dioxus_application<P: Clone + Send + Sync + 'static>(
self, self,
cfg: ServeConfig<P>, server_fn_route: &'static str,
server_fn_route: Option<&'static str>, cfg: impl Into<ServeConfig<P>>,
) -> Self { ) -> Self {
use tower_http::services::ServeDir; use tower_http::services::ServeDir;
// Serve the dist folder and the index.html file let cfg = cfg.into();
let serve_dir = ServeDir::new("dist");
self.register_server_fns(server_fn_route.unwrap_or_default()) // Serve the dist folder and the index.html file
.route( let serve_dir = ServeDir::new(cfg.assets_path);
self.register_server_fns(server_fn_route)
.nest_service("/assets", serve_dir)
.route_service(
"/", "/",
get(move |State(ssr_state): State<SSRState>| { get(render_handler).with_state((cfg, SSRState::default())),
let rendered = ssr_state.render(&cfg);
async move { Full::from(rendered) }
}),
) )
.fallback_service(serve_dir)
} }
} }
async fn server_fn_handler( async fn render_handler<P: Clone + Send + Sync + 'static>(
State((cfg, ssr_state)): State<(ServeConfig<P>, SSRState)>,
) -> impl IntoResponse {
let rendered = ssr_state.render(&cfg);
Full::from(rendered)
}
pub async fn server_fn_handler(
server_context: DioxusServerContext, server_context: DioxusServerContext,
function: Arc<ServerFnTraitObj>, function: Arc<ServerFnTraitObj>,
headers: HeaderMap, headers: HeaderMap,
+54 -15
View File
@@ -9,43 +9,65 @@ use server_fn::{Payload, ServerFunctionRegistry};
use tokio::task::spawn_blocking; use tokio::task::spawn_blocking;
use crate::{ use crate::{
dioxus_ssr_html, prelude::DioxusServerContext,
prelude::SSRState,
serve::ServeConfig, serve::ServeConfig,
server_fn::{DioxusServerContext, DioxusServerFnRegistry, ServerFnTraitObj}, server_fn::{DioxusServerFnRegistry, ServerFnTraitObj},
}; };
pub trait DioxusRouterExt { pub trait DioxusRouterExt {
fn register_server_fns(self, server_fn_route: &'static str) -> Self; fn register_server_fns(self, server_fn_route: &'static str) -> Self;
fn register_server_fns_with_handler<H>(
self,
server_fn_route: &'static str,
handler: impl Fn(Arc<ServerFnTraitObj>) -> H,
) -> Self
where
H: Handler + 'static;
fn serve_dioxus_application<P: Clone + Send + Sync + 'static>( fn serve_dioxus_application<P: Clone + Send + Sync + 'static>(
self, self,
cfg: ServeConfig<P>, server_fn_path: &'static str,
cfg: impl Into<ServeConfig<P>>,
) -> Self; ) -> Self;
} }
impl DioxusRouterExt for Router { impl DioxusRouterExt for Router {
fn register_server_fns(self, server_fn_route: &'static str) -> Self { fn register_server_fns_with_handler<H>(
self,
server_fn_route: &'static str,
mut handler: impl FnMut(Arc<ServerFnTraitObj>) -> H,
) -> Self
where
H: Handler + 'static,
{
let mut router = self; let mut router = self;
for server_fn_path in DioxusServerFnRegistry::paths_registered() { for server_fn_path in DioxusServerFnRegistry::paths_registered() {
let func = DioxusServerFnRegistry::get(server_fn_path).unwrap(); let func = DioxusServerFnRegistry::get(server_fn_path).unwrap();
let full_route = format!("{server_fn_route}/{server_fn_path}"); let full_route = format!("{server_fn_route}/{server_fn_path}");
router = router.push(Router::with_path(&full_route).post(ServerFnHandler { router = router.push(Router::with_path(&full_route).post(handler(func)));
server_context: DioxusServerContext {},
function: func,
}));
} }
router router
} }
fn register_server_fns(self, server_fn_route: &'static str) -> Self {
self.register_server_fns_with_handler(|| ServerFnHandler {
server_context: DioxusServerContext::default(),
function: func,
})
}
fn serve_dioxus_application<P: Clone + Send + Sync + 'static>( fn serve_dioxus_application<P: Clone + Send + Sync + 'static>(
self, self,
cfg: ServeConfig<P>, server_fn_route: &'static str,
cfg: impl Into<ServeConfig<P>>,
) -> Self { ) -> Self {
let cfg = cfg.into();
// Serve the dist folder and the index.html file // Serve the dist folder and the index.html file
let serve_dir = StaticDir::new(["dist"]); let serve_dir = StaticDir::new([cfg.assets_path]);
self.register_server_fns(cfg.server_fn_route.unwrap_or_default()) self.register_server_fns(server_fn_route)
.push(Router::with_path("/").get(SSRHandler { cfg })) .push(Router::with_path("/").get(SSRHandler { cfg }))
.push(Router::with_path("<**path>").get(serve_dir)) .push(Router::with_path("assets/<**path>").get(serve_dir))
} }
} }
@@ -58,19 +80,36 @@ impl<P: Clone + Send + Sync + 'static> Handler for SSRHandler<P> {
async fn handle( async fn handle(
&self, &self,
_req: &mut Request, _req: &mut Request,
_depot: &mut Depot, depot: &mut Depot,
res: &mut Response, res: &mut Response,
_flow: &mut FlowCtrl, _flow: &mut FlowCtrl,
) { ) {
res.write_body(dioxus_ssr_html(&self.cfg)).unwrap(); // Get the SSR renderer from the depot or create a new one if it doesn't exist
let renderer_pool = if let Some(renderer) = depot.obtain::<SSRState>() {
renderer.clone()
} else {
let renderer = SSRState::default();
depot.inject(renderer.clone());
renderer
};
res.write_body(renderer_pool.render(&self.cfg)).unwrap();
} }
} }
struct ServerFnHandler { pub struct ServerFnHandler {
server_context: DioxusServerContext, server_context: DioxusServerContext,
function: Arc<ServerFnTraitObj>, function: Arc<ServerFnTraitObj>,
} }
impl ServerFnHandler {
pub fn new(server_context: DioxusServerContext, function: Arc<ServerFnTraitObj>) -> Self {
Self {
server_context,
function,
}
}
}
#[handler] #[handler]
impl ServerFnHandler { impl ServerFnHandler {
async fn handle(&self, req: &mut Request, _depot: &mut Depot, res: &mut Response) { async fn handle(&self, req: &mut Request, _depot: &mut Depot, res: &mut Response) {
+47 -22
View File
@@ -10,50 +10,75 @@ use warp::{
}; };
use crate::{ use crate::{
dioxus_ssr_html, prelude::{DioxusServerContext, SSRState},
serve::ServeConfig, serve::ServeConfig,
server_fn::{DioxusServerContext, DioxusServerFnRegistry, ServerFnTraitObj}, server_fn::{DioxusServerFnRegistry, ServerFnTraitObj},
}; };
pub fn register_server_fns(server_fn_route: &'static str) -> BoxedFilter<(impl Reply,)> { pub fn register_server_fns_with_handler<H, F, R>(
let mut filter: Option<BoxedFilter<(_,)>> = None; server_fn_route: &'static str,
mut handler: H,
) -> BoxedFilter<(R,)>
where
H: FnMut(String, Arc<ServerFnTraitObj>) -> F,
F: Filter<Extract = (R,), Error = warp::Rejection> + Send + Sync + 'static,
F::Extract: Send,
R: Reply + 'static,
{
let mut filter: Option<BoxedFilter<F::Extract>> = None;
for server_fn_path in DioxusServerFnRegistry::paths_registered() { for server_fn_path in DioxusServerFnRegistry::paths_registered() {
let func = DioxusServerFnRegistry::get(server_fn_path).unwrap(); let func = DioxusServerFnRegistry::get(server_fn_path).unwrap();
let full_route = format!("{server_fn_route}/{server_fn_path}") let full_route = format!("{server_fn_route}/{server_fn_path}")
.trim_start_matches('/') .trim_start_matches('/')
.to_string(); .to_string();
let route = path(full_route) let route = handler(full_route, func.clone()).boxed();
.and(warp::post())
.and(warp::header::headers_cloned())
.and(warp::body::bytes())
.and_then(move |headers: HeaderMap, body| {
let func = func.clone();
async move { server_fn_handler(DioxusServerContext {}, func, headers, body).await }
})
.boxed();
if let Some(boxed_filter) = filter.take() { if let Some(boxed_filter) = filter.take() {
filter = Some(boxed_filter.or(route).unify().boxed()); filter = Some(boxed_filter.or(route).unify().boxed());
} else { } else {
filter = Some(route.boxed()); filter = Some(route);
} }
} }
filter.expect("No server functions found") filter.expect("No server functions found")
} }
pub fn serve_dioxus_application<P: Clone + Send + Sync + 'static>( pub fn register_server_fns(server_fn_route: &'static str) -> BoxedFilter<(impl Reply,)> {
cfg: ServeConfig<P>, register_server_fns_with_handler(server_fn_route, |full_route, func| {
) -> BoxedFilter<(impl Reply,)> { path(full_route)
// Serve the dist folder and the index.html file .and(warp::post())
let serve_dir = warp::fs::dir("./dist"); .and(warp::header::headers_cloned())
.and(warp::body::bytes())
.and_then(move |headers: HeaderMap, body| {
let func = func.clone();
async move {
server_fn_handler(DioxusServerContext::default(), func, headers, body).await
}
})
})
}
register_server_fns(cfg.server_fn_route.unwrap_or_default()) pub fn serve_dioxus_application<P: Clone + Send + Sync + 'static>(
server_fn_route: &'static str,
cfg: impl Into<ServeConfig<P>>,
) -> BoxedFilter<(impl Reply,)> {
let cfg = cfg.into();
// Serve the dist folder and the index.html file
let serve_dir = warp::fs::dir(cfg.assets_path);
register_server_fns(server_fn_route)
.or(warp::path::end() .or(warp::path::end()
.and(warp::get()) .and(warp::get())
.map(move || warp::reply::html(dioxus_ssr_html(&cfg)))) .and(with_ssr_state())
.or(serve_dir) .map(move |renderer: SSRState| warp::reply::html(renderer.render(&cfg))))
.or(warp::path("assets").and(serve_dir))
.boxed() .boxed()
} }
fn with_ssr_state() -> impl Filter<Extract = (SSRState,), Error = std::convert::Infallible> + Clone
{
let renderer = SSRState::default();
warp::any().map(move || renderer.clone())
}
#[derive(Debug)] #[derive(Debug)]
struct FailedToReadBody(String); struct FailedToReadBody(String);
+6 -2
View File
@@ -6,6 +6,7 @@ mod adapters;
pub mod render; pub mod render;
#[cfg(feature = "ssr")] #[cfg(feature = "ssr")]
mod serve; mod serve;
mod server_context;
mod server_fn; mod server_fn;
pub mod prelude { pub mod prelude {
@@ -18,8 +19,11 @@ pub mod prelude {
#[cfg(feature = "ssr")] #[cfg(feature = "ssr")]
pub use crate::render::*; pub use crate::render::*;
#[cfg(feature = "ssr")] #[cfg(feature = "ssr")]
pub use crate::serve::ServeConfig; pub use crate::serve::{ServeConfig, ServeConfigBuilder};
pub use crate::server_fn::{DioxusServerContext, ServerFn}; pub use crate::server_context::DioxusServerContext;
pub use crate::server_fn::ServerFn;
#[cfg(feature = "ssr")]
pub use crate::server_fn::ServerFnTraitObj;
pub use server_fn::{self, ServerFn as _, ServerFnError}; pub use server_fn::{self, ServerFn as _, ServerFnError};
pub use server_macro::*; pub use server_macro::*;
} }
+35
View File
@@ -0,0 +1,35 @@
use std::sync::{Arc, PoisonError, RwLock, RwLockWriteGuard};
use anymap::{any::Any, Map};
type SendSyncAnyMap = Map<dyn Any + Send + Sync + 'static>;
/// A shared context for server functions. This allows you to pass data between your server and the server functions like authentication session data.
#[derive(Clone)]
pub struct DioxusServerContext {
shared_context: Arc<RwLock<SendSyncAnyMap>>,
}
impl Default for DioxusServerContext {
fn default() -> Self {
Self {
shared_context: Arc::new(RwLock::new(SendSyncAnyMap::new())),
}
}
}
impl DioxusServerContext {
pub fn get<T: Any + Send + Sync + Clone + 'static>(&self) -> Option<T> {
self.shared_context.read().ok()?.get::<T>().cloned()
}
pub fn insert<T: Any + Send + Sync + 'static>(
&mut self,
value: T,
) -> Result<(), PoisonError<RwLockWriteGuard<'_, SendSyncAnyMap>>> {
self.shared_context
.write()
.map(|mut map| map.insert(value))
.map(|_| ())
}
}
+1 -2
View File
@@ -1,5 +1,4 @@
#[derive(Clone)] use crate::server_context::DioxusServerContext;
pub struct DioxusServerContext {}
#[cfg(any(feature = "ssr", doc))] #[cfg(any(feature = "ssr", doc))]
pub type ServerFnTraitObj = server_fn::ServerFnTraitObj<DioxusServerContext>; pub type ServerFnTraitObj = server_fn::ServerFnTraitObj<DioxusServerContext>;