diff --git a/Cargo.lock b/Cargo.lock index ea2d7ea174..b4667aa0ec 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5687,6 +5687,7 @@ dependencies = [ "base64 0.22.1", "bytes", "ciborium", + "dashmap 6.1.0", "dioxus", "dioxus-html", "dioxus-ssr", diff --git a/Cargo.toml b/Cargo.toml index b7ac046ce1..e854172296 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -353,6 +353,7 @@ pin-project = { version = "1.1.10" } postcard = { version = "1.1.3", default-features = false } serde_urlencoded = "0.7" form_urlencoded = "1.2.1" +dashmap = "6.1.0" # desktop wry = { version = "0.52.1", default-features = false } @@ -479,6 +480,7 @@ bytes = { workspace = true } futures = { workspace = true } axum-core = { workspace = true } uuid = { workspace = true, features = ["v4", "serde"] } +dashmap = { workspace = true } tower-http = { workspace = true, features = ["timeout"] } [target.'cfg(target_arch = "wasm32")'.dev-dependencies] @@ -699,6 +701,11 @@ name = "flat_router" path = "examples/06-routing/flat_router.rs" doc-scrape-examples = true +[[example]] +name = "namespaced_server_functions" +path = "examples/07-fullstack/namespaced_server_functions.rs" +doc-scrape-examples = true + [[example]] name = "middleware" path = "examples/07-fullstack/middleware.rs" diff --git a/examples/07-fullstack/namespaced_server_functions.rs b/examples/07-fullstack/namespaced_server_functions.rs new file mode 100644 index 0000000000..27f54bdfc3 --- /dev/null +++ b/examples/07-fullstack/namespaced_server_functions.rs @@ -0,0 +1,104 @@ +//! This example demonstrates how to define namespaced server functions in Dioxus Fullstack. +//! +//! Namespaced Server Functions allow you to organize your server functions into logical groups, +//! making it possible to reuse groups of functions as a library across different projects. +//! +//! Namespaced server functions are defined as methods on a struct. The struct itself is the "state" +//! of this group of functions, and can hold any data you want to share across the functions. +//! +//! Unlike regular server functions, namespaced server functions are not automatically registered +//! with `dioxus::launch`. You must explicitly mount the server functions to a given route using the +//! `Endpoint::mount` function. From the client, you can then call the functions using regular method +//! call syntax. +//! +//! Namespaces are designed to make server functions easier to modularize and reuse, making it possible +//! to create a publishable library of server functions that other developers can easily integrate into +//! their own Dioxus Fullstack applications. + +use dioxus::fullstack::Endpoint; +use dioxus::prelude::*; + +fn main() { + #[cfg(not(feature = "server"))] + dioxus::launch(app); + + // On the server, we can customize the models and mount the server functions to a specific route. + // The `.endpoint()` extension method allows you to mount an `Endpoint` to an axum router. + #[cfg(feature = "server")] + dioxus::serve(|| async move { + // + todo!() + }); +} + +// We mount a namespace of server functions to the "/api/dogs" route. +// All calls to `DOGS` from the client will be sent to this route. +static DOGS: Endpoint = Endpoint::new("/api/dogs", || PetApi { pets: todo!() }); + +/// Our server functions will be associated with this struct. +struct PetApi { + /// we can add shared state here if we want + /// e.g. a database connection pool + /// + /// Since `PetApi` exists both on the client and server, we need to conditionally include + /// the database pool only on the server. + // #[cfg(feature = "server")] + pets: dashmap::DashMap, +} + +impl PetApi { + /// List all the pets in the database. + // #[get("/")] + async fn list(&self) -> Result> { + Ok(self.pets.iter().map(|entry| entry.key().clone()).collect()) + } + + /// Get the breed of a specific pet by name. + // #[get("/{name}")] + async fn get(&self, name: String) -> Result { + Ok(self + .pets + .get(&name) + .map(|entry| entry.value().clone()) + .or_not_found("pet not found")?) + } + + /// Add a new pet to the database. + // #[post("/{name}")] + async fn add(&self, name: String, breed: String) -> Result<()> { + self.pets.insert(name, breed); + Ok(()) + } + + /// Remove a pet from the database. + // #[delete("/{name}")] + async fn remove(&self, name: String) -> Result<()> { + self.pets.remove(&name).or_not_found("pet not found")?; + Ok(()) + } + + /// Update a pet's name in the database. + #[put("/{name}")] + async fn update(&self, name: String, breed: String) -> Result<()> { + self.pets.insert(breed.clone(), breed); + Ok(()) + } +} + +/// In our app, we can call the namespaced server functions using regular method call syntax, mixing +/// loaders, actions, and other hooks as normal. +fn app() -> Element { + let pets = use_loader(|| DOGS.list())?; + let add = use_action(|name, breed| DOGS.add(name, breed)); + let remove = use_action(|name| DOGS.remove(name)); + let update = use_action(|breed| DOGS.update(breed)); + + rsx! { + div { + h1 { "My Pets" } + ul { + + } + } + } +} diff --git a/examples/07-fullstack/ssr-only/src/main.rs b/examples/07-fullstack/ssr-only/src/main.rs index b041006120..d64344b007 100644 --- a/examples/07-fullstack/ssr-only/src/main.rs +++ b/examples/07-fullstack/ssr-only/src/main.rs @@ -8,6 +8,8 @@ //! //! To run this example, simply run `cargo run --package ssr-only` and navigate to `http://localhost:8080`. +use std::any::TypeId; + use dioxus::prelude::*; fn main() { diff --git a/packages/fullstack-core/src/lib.rs b/packages/fullstack-core/src/lib.rs index e408a7bb3e..d5ea104a8b 100644 --- a/packages/fullstack-core/src/lib.rs +++ b/packages/fullstack-core/src/lib.rs @@ -11,6 +11,8 @@ mod server_future; mod streaming; mod transport; +use std::{any::Any, sync::Arc}; + pub use crate::errors::*; pub use crate::loader::*; pub use crate::server_cached::*; @@ -28,3 +30,9 @@ pub use httperror::*; #[derive(Clone, Default)] pub struct DioxusServerState {} + +impl DioxusServerState { + pub fn get_endpoint(&self) -> Option> { + todo!() + } +} diff --git a/packages/fullstack-macro/src/lib.rs b/packages/fullstack-macro/src/lib.rs index 736f4dced0..42ba335e68 100644 --- a/packages/fullstack-macro/src/lib.rs +++ b/packages/fullstack-macro/src/lib.rs @@ -228,8 +228,16 @@ fn route_impl_with_route( .retain(|attr| !attr.path().is_ident("middleware")); let server_args = route.server_args.clone(); + let mut function_on_server = function.clone(); function_on_server.sig.inputs.extend(server_args.clone()); + function_on_server.sig.ident = format_ident!("__server_fn_inner_{}", function.sig.ident); + let function_on_server_name = function_on_server.sig.ident.clone(); + + let mut self_token = function.sig.inputs.first().map(|arg| match arg { + FnArg::Receiver(receiver) => Some(receiver.self_token), + _ => None, + }); // Now we can compile the route let original_inputs = function @@ -237,7 +245,7 @@ fn route_impl_with_route( .inputs .iter() .map(|arg| match arg { - FnArg::Receiver(_receiver) => panic!("Self type is not supported"), + FnArg::Receiver(_receiver) => _receiver.to_token_stream(), FnArg::Typed(pat_type) => { quote! { #[allow(unused_mut)] @@ -365,31 +373,6 @@ fn route_impl_with_route( } }; - let as_axum_path = route.to_axum_path_string(); - - let query_endpoint = if let Some(route_lit) = route.route_lit.as_ref() { - let prefix = route - .prefix - .as_ref() - .cloned() - .unwrap_or_else(|| LitStr::new("", Span::call_site())) - .value(); - let url_without_queries = route_lit.value().split('?').next().unwrap().to_string(); - let full_url = format!( - "{}{}{}", - prefix, - if url_without_queries.starts_with("/") { - "" - } else { - "/" - }, - url_without_queries - ); - quote! { format!(#full_url, #( #path_param_args)*) } - } else { - quote! { __ENDPOINT_PATH.to_string() } - }; - let endpoint_path = { let prefix = route .prefix @@ -397,6 +380,7 @@ fn route_impl_with_route( .cloned() .unwrap_or_else(|| LitStr::new("", Span::call_site())); + let as_axum_path = route.to_axum_path_string(); let route_lit = if !as_axum_path.is_empty() { quote! { #as_axum_path } } else { @@ -433,6 +417,59 @@ fn route_impl_with_route( } }; + // The endpoint the client will query, passed to `ClientRequest` + // ie `/api/my_fnc` + // + // If there's a `&self` parameter, then we need to look up where `&self` is mounted. + let query_endpoint = if let Some(route_lit) = route.route_lit.as_ref() { + let prefix = route + .prefix + .as_ref() + .cloned() + .unwrap_or_else(|| LitStr::new("", Span::call_site())) + .value(); + let url_without_queries = route_lit.value().split('?').next().unwrap().to_string(); + let full_url = format!( + "{}{}{}", + prefix, + if url_without_queries.starts_with("/") { + "" + } else { + "/" + }, + url_without_queries + ); + quote! { format!(#full_url, #( #path_param_args )*) } + } else { + quote! { __ENDPOINT_PATH.to_string() } + }; + + let receiver = if let Some(self_token) = self_token.take() { + quote! { Self:: } + } else { + quote! {} + }; + + let extract_self = if self_token.is_some() { + quote! { + let __self = ___state.get_endpoint::().expect("Failed to get endpoint state"); + } + } else { + quote! {} + }; + + let self_args = if self_token.is_some() { + quote! { &*__self, } + } else { + quote! {} + }; + + let namespace = if self_token.is_some() { + quote! { Some(std::any::TypeId::of::()) } + } else { + quote! { None } + }; + let middleware_extra = middleware_inits .iter() .map(|init| { @@ -443,11 +480,11 @@ fn route_impl_with_route( .collect::>(); Ok(quote! { - #(#fn_docs)* - #route_docs - #[deny( - unexpected_cfgs, - reason = " + #(#fn_docs)* + #route_docs + #[deny( + unexpected_cfgs, + reason = " ========================================================================================== Using Dioxus Server Functions requires a `server` feature flag in your `Cargo.toml`. Please add the following to your `Cargo.toml`: @@ -466,114 +503,123 @@ fn route_impl_with_route( ``` ========================================================================================== " - )] - #vis async fn #fn_name #impl_generics( - #original_inputs - ) -> #out_ty #where_clause { - use dioxus_fullstack::serde as serde; - use dioxus_fullstack::{ - // concrete types - ServerFnEncoder, ServerFnDecoder, DioxusServerState, - - // "magic" traits for encoding/decoding on the client - ExtractRequest, EncodeRequest, RequestDecodeResult, RequestDecodeErr, - - // "magic" traits for encoding/decoding on the server - MakeAxumResponse, MakeAxumError, - }; + )] + #vis async fn #fn_name #impl_generics( + #original_inputs + ) -> #out_ty #where_clause { + use dioxus_fullstack::serde as serde; + use dioxus_fullstack::{ + // concrete types + ServerFnEncoder, ServerFnDecoder, DioxusServerState, + + // "magic" traits for encoding/decoding on the client + ExtractRequest, EncodeRequest, RequestDecodeResult, RequestDecodeErr, + + // "magic" traits for encoding/decoding on the server + MakeAxumResponse, MakeAxumError, + }; - _ = dioxus_fullstack::assert_is_result::<#out_ty>(); + _ = dioxus_fullstack::assert_is_result::<#out_ty>(); - #query_params_struct + #query_params_struct - #body_struct_impl + #body_struct_impl - const __ENDPOINT_PATH: &str = #endpoint_path; + const __ENDPOINT_PATH: &str = #endpoint_path; - // On the client, we make the request to the server - // We want to support extremely flexible error types and return types, making this more complex than it should - #[allow(clippy::unused_unit)] - #[cfg(not(feature = "server"))] - { - let client = dioxus_fullstack::ClientRequest::new( - dioxus_fullstack::http::Method::#method_ident, - #query_endpoint, - &__QueryParams__ { #(#query_param_names,)* }, - ); + // On the client, we make the request to the server + // We want to support extremely flexible error types and return types, making this more complex than it should + #[allow(clippy::unused_unit)] + #[cfg(not(feature = "server"))] + { + let client = dioxus_fullstack::ClientRequest::new( + dioxus_fullstack::http::Method::#method_ident, + #query_endpoint, + &__QueryParams__ { #(#query_param_names,)* }, + ); - let verify_token = (&&&&&&&&&&&&&&ServerFnEncoder::<___Body_Serialize___<#(#body_json_types,)*>, (#(#body_json_types,)*)>::new()) - .verify_can_serialize(); + let verify_token = (&&&&&&&&&&&&&&ServerFnEncoder::<___Body_Serialize___<#(#body_json_types,)*>, (#(#body_json_types,)*)>::new()) + .verify_can_serialize(); - dioxus_fullstack::assert_can_encode(verify_token); + dioxus_fullstack::assert_can_encode(verify_token); - let response = (&&&&&&&&&&&&&&ServerFnEncoder::<___Body_Serialize___<#(#body_json_types,)*>, (#(#body_json_types,)*)>::new()) - .fetch_client(client, ___Body_Serialize___ { #(#body_json_names,)* }, #unpack) - .await; + let response = (&&&&&&&&&&&&&&ServerFnEncoder::<___Body_Serialize___<#(#body_json_types,)*>, (#(#body_json_types,)*)>::new()) + .fetch_client(client, ___Body_Serialize___ { #(#body_json_names,)* }, #unpack) + .await; - let decoded = (&&&&&ServerFnDecoder::<#out_ty>::new()) - .decode_client_response(response) - .await; + let decoded = (&&&&&ServerFnDecoder::<#out_ty>::new()) + .decode_client_response(response) + .await; - let result = (&&&&&ServerFnDecoder::<#out_ty>::new()) - .decode_client_err(decoded) - .await; + let result = (&&&&&ServerFnDecoder::<#out_ty>::new()) + .decode_client_err(decoded) + .await; - return result; - } + return result; + } - // On the server, we expand the tokens and submit the function to inventory - #[cfg(feature = "server")] { - use #__axum::response::IntoResponse; - use dioxus_server::ServerFunction; + // On the server, we expand the tokens and submit the function to inventory + #[cfg(feature = "server")] { + use #__axum::response::IntoResponse; + use dioxus_server::ServerFunction; - #function_on_server + #[allow(clippy::unused_unit)] + #asyncness fn __inner__function__ #impl_generics( + ___state: #__axum::extract::State, + #path_extractor + #query_extractor + request: #__axum::extract::Request, + ) -> Result<#__axum::response::Response, #__axum::response::Response> #where_clause { + #extract_self - #[allow(clippy::unused_unit)] - #asyncness fn __inner__function__ #impl_generics( - ___state: #__axum::extract::State, - #path_extractor - #query_extractor - request: #__axum::extract::Request, - ) -> Result<#__axum::response::Response, #__axum::response::Response> #where_clause { - let ((#(#server_names,)*), ( #(#body_json_names,)* )) = (&&&&&&&&&&&&&&ServerFnEncoder::<___Body_Serialize___<#(#body_json_types,)*>, (#(#body_json_types,)*)>::new()) - .extract_axum(___state.0, request, #unpack).await?; - - let encoded = (&&&&&&ServerFnDecoder::<#out_ty>::new()) - .make_axum_response( - #fn_name #ty_generics(#(#extracted_idents,)* #(#body_json_names,)* #(#server_names,)*).await - ); - - let response = (&&&&&ServerFnDecoder::<#out_ty>::new()) - .make_axum_error(encoded); - - return response; - } + let ((#(#server_names,)*), ( #(#body_json_names,)* )) = (&&&&&&&&&&&&&&ServerFnEncoder::<___Body_Serialize___<#(#body_json_types,)*>, (#(#body_json_types,)*)>::new()) + .extract_axum(___state.0, request, #unpack).await?; - dioxus_server::inventory::submit! { - ServerFunction::new( - dioxus_server::http::Method::#method_ident, - __ENDPOINT_PATH, - || { - #__axum::routing::#http_method(__inner__function__ #ty_generics) #(#middleware_extra)* - } - ) - } + let encoded = (&&&&&&ServerFnDecoder::<#out_ty>::new()) + .make_axum_response( + #receiver #function_on_server_name #ty_generics(#self_args #(#extracted_idents,)* #(#body_json_names,)* #(#server_names,)*).await + ); - #server_defaults + let response = (&&&&&ServerFnDecoder::<#out_ty>::new()) + .make_axum_error(encoded); - return #fn_name #ty_generics( - #(#extracted_idents,)* - #(#body_json_names,)* - #(#server_names,)* - ).await; - } + return response; + } + + dioxus_server::inventory::submit! { + ServerFunction::new( + dioxus_server::http::Method::#method_ident, + __ENDPOINT_PATH, + <<<<<<< HEAD + || #__axum::routing::#http_method(__inner__function__ #ty_generics), + #namespace + ======= + || { + #__axum::routing::#http_method(__inner__function__ #ty_generics) #(#middleware_extra)* + } + >>>>>>> origin/main + ) + } + + #server_defaults + + return #function_on_server_name #ty_generics( + #(#extracted_idents,)* + #(#body_json_names,)* + #(#server_names,)* + ).await; + } - #[allow(unreachable_code)] - { - unreachable!() + #[allow(unreachable_code)] + { + unreachable!() + } } - } - }) + + #[cfg(feature = "server")] + #[doc(hidden)] + #function_on_server + }) } struct CompiledRoute { @@ -607,10 +653,6 @@ impl CompiledRoute { } PathParam::Static(lit) => path.push_str(&lit.value()), } - // if colon.is_some() { - // path.push(':'); - // } - // path.push_str(&ident.value()); } path @@ -805,7 +847,7 @@ impl CompiledRoute { Some(pat_type.clone()) } else { - unimplemented!("Self type is not supported") + None } }) .collect() @@ -842,7 +884,7 @@ impl CompiledRoute { new_pat_type.pat = Box::new(parse_quote!(#ident)); Some(new_pat_type) } else { - unimplemented!("Self type is not supported") + None } }) .collect() diff --git a/packages/fullstack-server/Cargo.toml b/packages/fullstack-server/Cargo.toml index 2488fe139e..8ecf917e55 100644 --- a/packages/fullstack-server/Cargo.toml +++ b/packages/fullstack-server/Cargo.toml @@ -31,7 +31,7 @@ generational-box = { workspace = true } axum = { workspace = true, features = ["multipart", "ws", "json", "form", "tokio", "http1", "http2", "macros"]} anyhow = { workspace = true } -dashmap = "6.1.0" +dashmap = { workspace = true } inventory = { workspace = true } dioxus-ssr = { workspace = true } diff --git a/packages/fullstack-server/src/serverfn.rs b/packages/fullstack-server/src/serverfn.rs index 6e1db515d2..677d07d86a 100644 --- a/packages/fullstack-server/src/serverfn.rs +++ b/packages/fullstack-server/src/serverfn.rs @@ -5,10 +5,7 @@ use axum::Router; use dashmap::DashMap; use dioxus_fullstack_core::DioxusServerState; use http::Method; -use std::{marker::PhantomData, sync::LazyLock}; - -pub type AxumRequest = http::Request; -pub type AxumResponse = http::Response; +use std::{any::TypeId, marker::PhantomData, sync::LazyLock}; /// A function endpoint that can be called from the client. #[derive(Clone)] @@ -16,24 +13,23 @@ pub struct ServerFunction { path: &'static str, method: Method, handler: fn() -> MethodRouter, + namespace: Option, _phantom: PhantomData, } -pub struct MakeRequest { - _phantom: PhantomData, -} - impl ServerFunction { /// Create a new server function object. pub const fn new( method: Method, path: &'static str, handler: fn() -> MethodRouter, + namespace: Option, ) -> Self { Self { path, method, handler, + namespace, _phantom: PhantomData, } } diff --git a/packages/fullstack/src/endpoint.rs b/packages/fullstack/src/endpoint.rs new file mode 100644 index 0000000000..b102dd6e0f --- /dev/null +++ b/packages/fullstack/src/endpoint.rs @@ -0,0 +1,24 @@ +//! An endpoint represents an entrypoint for a group of server functions. + +pub struct Endpoint { + path: &'static str, + _marker: std::marker::PhantomData, +} + +impl Endpoint { + /// Create a new endpoint at the given path. + pub const fn new(path: &'static str, f: fn() -> T) -> Self { + Self { + path, + _marker: std::marker::PhantomData, + } + } +} + +impl std::ops::Deref for Endpoint { + type Target = T; + + fn deref(&self) -> &Self::Target { + todo!() + } +} diff --git a/packages/fullstack/src/lib.rs b/packages/fullstack/src/lib.rs index ccc3fd855c..c3e8b5773e 100644 --- a/packages/fullstack/src/lib.rs +++ b/packages/fullstack/src/lib.rs @@ -51,6 +51,9 @@ pub use http::{HeaderMap, HeaderValue, Method}; mod client; pub use client::*; +mod endpoint; +pub use endpoint::*; + pub use axum::extract::Json; pub use axum::response::{NoContent, Redirect};