Jelajahi Sumber

make server function API more flexible

Evan Almloff 2 tahun lalu
induk
melakukan
c6992c7032

+ 2 - 1
packages/server/Cargo.toml

@@ -6,7 +6,7 @@ edition = "2021"
 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 
 [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" }
 
 # warp
@@ -31,6 +31,7 @@ once_cell = "1.17.1"
 thiserror = "1.0.40"
 tokio = { version = "1.27.0", features = ["full"], optional = true }
 object-pool = "0.5.4"
+anymap = "0.12.1"
 
 [features]
 default = []

+ 2 - 1
packages/server/server-macro/Cargo.toml

@@ -7,7 +7,8 @@ edition = "2021"
 
 [dependencies]
 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"] }
 
 [lib]

+ 7 - 7
packages/server/server-macro/src/lib.rs

@@ -2,7 +2,7 @@ use proc_macro::TokenStream;
 use quote::ToTokens;
 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.
 ///
 /// 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"`.
 ///
 /// 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
 /// server-side context into the server function.
 ///
 /// ```ignore
-/// # use leptos::*; use serde::{Serialize, Deserialize};
+/// # use dioxus_server::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
-///   todo!()   
+///   todo!()
 /// }
 /// ```
 ///
@@ -42,7 +42,7 @@ use server_fn_macro::*;
 /// - **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
 ///   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
 ///   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)
@@ -50,8 +50,8 @@ use server_fn_macro::*;
 ///   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 [Scope](leptos_reactive::Scope) 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
+/// - **The [DioxusServerContext](dioxus_server::prelude::DioxusServerContext) comes from the server.** Optionally, the first argument of a server function
+///   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.
 #[proc_macro_attribute]
 pub fn server(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {

+ 54 - 25
packages/server/src/adapters/axum_adapter.rs

@@ -2,7 +2,8 @@ use std::{error::Error, sync::Arc};
 
 use axum::{
     body::{self, Body, BoxBody, Full},
-    extract::{FromRef, State},
+    extract::State,
+    handler::Handler,
     http::{HeaderMap, Request, Response, StatusCode},
     response::IntoResponse,
     routing::{get, post},
@@ -14,61 +15,89 @@ use tokio::task::spawn_blocking;
 use crate::{
     render::SSRState,
     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 serve_dioxus_application<P: Clone + Send + Sync + 'static>(
         self,
-        cfg: ServeConfig<P>,
-        server_fn_route: Option<&'static str>,
+        server_fn_route: &'static str,
+        cfg: impl Into<ServeConfig<P>>,
     ) -> Self;
 }
 
-impl<S> DioxusRouterExt for Router<S>
+impl<S> DioxusRouterExt<S> for Router<S>
 where
-    SSRState: FromRef<S>,
     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;
         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}");
-            router = router.route(
-                &full_route,
-                post(move |headers: HeaderMap, body: Request<Body>| async move {
-                    server_fn_handler(DioxusServerContext {}, func.clone(), headers, body).await
-                }),
-            );
+            router = router.route(&full_route, post(handler(func)));
         }
         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>(
         self,
-        cfg: ServeConfig<P>,
-        server_fn_route: Option<&'static str>,
+        server_fn_route: &'static str,
+        cfg: impl Into<ServeConfig<P>>,
     ) -> Self {
         use tower_http::services::ServeDir;
 
+        let cfg = cfg.into();
+
         // Serve the dist folder and the index.html file
-        let serve_dir = ServeDir::new("dist");
+        let serve_dir = ServeDir::new(cfg.assets_path);
 
-        self.register_server_fns(server_fn_route.unwrap_or_default())
-            .route(
+        self.register_server_fns(server_fn_route)
+            .nest_service("/assets", serve_dir)
+            .route_service(
                 "/",
-                get(move |State(ssr_state): State<SSRState>| {
-                    let rendered = ssr_state.render(&cfg);
-                    async move { Full::from(rendered) }
-                }),
+                get(render_handler).with_state((cfg, SSRState::default())),
             )
-            .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,
     function: Arc<ServerFnTraitObj>,
     headers: HeaderMap,

+ 54 - 15
packages/server/src/adapters/salvo_adapter.rs

@@ -9,43 +9,65 @@ use server_fn::{Payload, ServerFunctionRegistry};
 use tokio::task::spawn_blocking;
 
 use crate::{
-    dioxus_ssr_html,
+    prelude::DioxusServerContext,
+    prelude::SSRState,
     serve::ServeConfig,
-    server_fn::{DioxusServerContext, DioxusServerFnRegistry, ServerFnTraitObj},
+    server_fn::{DioxusServerFnRegistry, ServerFnTraitObj},
 };
 
 pub trait DioxusRouterExt {
     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>(
         self,
-        cfg: ServeConfig<P>,
+        server_fn_path: &'static str,
+        cfg: impl Into<ServeConfig<P>>,
     ) -> Self;
 }
 
 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;
         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}");
-            router = router.push(Router::with_path(&full_route).post(ServerFnHandler {
-                server_context: DioxusServerContext {},
-                function: func,
-            }));
+            router = router.push(Router::with_path(&full_route).post(handler(func)));
         }
         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>(
         self,
-        cfg: ServeConfig<P>,
+        server_fn_route: &'static str,
+        cfg: impl Into<ServeConfig<P>>,
     ) -> Self {
+        let cfg = cfg.into();
         // 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("<**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(
         &self,
         _req: &mut Request,
-        _depot: &mut Depot,
+        depot: &mut Depot,
         res: &mut Response,
         _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,
     function: Arc<ServerFnTraitObj>,
 }
 
+impl ServerFnHandler {
+    pub fn new(server_context: DioxusServerContext, function: Arc<ServerFnTraitObj>) -> Self {
+        Self {
+            server_context,
+            function,
+        }
+    }
+}
+
 #[handler]
 impl ServerFnHandler {
     async fn handle(&self, req: &mut Request, _depot: &mut Depot, res: &mut Response) {

+ 44 - 19
packages/server/src/adapters/warp_adapter.rs

@@ -10,50 +10,75 @@ use warp::{
 };
 
 use crate::{
-    dioxus_ssr_html,
+    prelude::{DioxusServerContext, SSRState},
     serve::ServeConfig,
-    server_fn::{DioxusServerContext, DioxusServerFnRegistry, ServerFnTraitObj},
+    server_fn::{DioxusServerFnRegistry, ServerFnTraitObj},
 };
 
-pub fn register_server_fns(server_fn_route: &'static str) -> BoxedFilter<(impl Reply,)> {
-    let mut filter: Option<BoxedFilter<(_,)>> = None;
+pub fn register_server_fns_with_handler<H, F, R>(
+    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() {
         let func = DioxusServerFnRegistry::get(server_fn_path).unwrap();
         let full_route = format!("{server_fn_route}/{server_fn_path}")
             .trim_start_matches('/')
             .to_string();
-        let route = path(full_route)
-            .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();
+        let route = handler(full_route, func.clone()).boxed();
         if let Some(boxed_filter) = filter.take() {
             filter = Some(boxed_filter.or(route).unify().boxed());
         } else {
-            filter = Some(route.boxed());
+            filter = Some(route);
         }
     }
     filter.expect("No server functions found")
 }
 
+pub fn register_server_fns(server_fn_route: &'static str) -> BoxedFilter<(impl Reply,)> {
+    register_server_fns_with_handler(server_fn_route, |full_route, func| {
+        path(full_route)
+            .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::default(), func, headers, body).await
+                }
+            })
+    })
+}
+
 pub fn serve_dioxus_application<P: Clone + Send + Sync + 'static>(
-    cfg: ServeConfig<P>,
+    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("./dist");
+    let serve_dir = warp::fs::dir(cfg.assets_path);
 
-    register_server_fns(cfg.server_fn_route.unwrap_or_default())
+    register_server_fns(server_fn_route)
         .or(warp::path::end()
             .and(warp::get())
-            .map(move || warp::reply::html(dioxus_ssr_html(&cfg))))
-        .or(serve_dir)
+            .and(with_ssr_state())
+            .map(move |renderer: SSRState| warp::reply::html(renderer.render(&cfg))))
+        .or(warp::path("assets").and(serve_dir))
         .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)]
 struct FailedToReadBody(String);
 

+ 6 - 2
packages/server/src/lib.rs

@@ -6,6 +6,7 @@ mod adapters;
 pub mod render;
 #[cfg(feature = "ssr")]
 mod serve;
+mod server_context;
 mod server_fn;
 
 pub mod prelude {
@@ -18,8 +19,11 @@ pub mod prelude {
     #[cfg(feature = "ssr")]
     pub use crate::render::*;
     #[cfg(feature = "ssr")]
-    pub use crate::serve::ServeConfig;
-    pub use crate::server_fn::{DioxusServerContext, ServerFn};
+    pub use crate::serve::{ServeConfig, ServeConfigBuilder};
+    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_macro::*;
 }

+ 35 - 0
packages/server/src/server_context.rs

@@ -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
packages/server/src/server_fn.rs

@@ -1,5 +1,4 @@
-#[derive(Clone)]
-pub struct DioxusServerContext {}
+use crate::server_context::DioxusServerContext;
 
 #[cfg(any(feature = "ssr", doc))]
 pub type ServerFnTraitObj = server_fn::ServerFnTraitObj<DioxusServerContext>;