reorganize fullstack
This commit is contained in:
1 parent
2763adb2d3
commit
0ef29b7d75
7 files changed
+39
-48
No files matched your search
@@ -0,0 +1,173 @@
|
||||
#[cfg(feature = "server")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "server")))]
|
||||
#[derive(Clone)]
|
||||
/// A trait object for a function that be called on serializable arguments and returns a serializable result.
|
||||
pub struct ServerFnTraitObj(server_fn::ServerFnTraitObj<()>);
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
impl std::ops::Deref for ServerFnTraitObj {
|
||||
type Target = server_fn::ServerFnTraitObj<()>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
impl std::ops::DerefMut for ServerFnTraitObj {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
impl ServerFnTraitObj {
|
||||
fn new(
|
||||
prefix: &'static str,
|
||||
url: &'static str,
|
||||
encoding: server_fn::Encoding,
|
||||
run: ServerFunction,
|
||||
) -> Self {
|
||||
Self(server_fn::ServerFnTraitObj::new(prefix, url, encoding, run))
|
||||
}
|
||||
|
||||
/// Create a new `ServerFnTraitObj` from a `server_fn::ServerFnTraitObj`.
|
||||
pub const fn from_generic_server_fn(server_fn: server_fn::ServerFnTraitObj<()>) -> Self {
|
||||
Self(server_fn)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
server_fn::inventory::collect!(ServerFnTraitObj);
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "server")))]
|
||||
/// Middleware for a server function
|
||||
pub struct ServerFnMiddleware {
|
||||
/// The prefix of the server function.
|
||||
pub prefix: &'static str,
|
||||
/// The url of the server function.
|
||||
pub url: &'static str,
|
||||
/// The middleware layers.
|
||||
pub middleware: fn() -> Vec<std::sync::Arc<dyn crate::layer::Layer>>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
pub(crate) static MIDDLEWARE: once_cell::sync::Lazy<
|
||||
std::collections::HashMap<
|
||||
(&'static str, &'static str),
|
||||
Vec<std::sync::Arc<dyn crate::layer::Layer>>,
|
||||
>,
|
||||
> = once_cell::sync::Lazy::new(|| {
|
||||
let mut map: std::collections::HashMap<
|
||||
(&'static str, &'static str),
|
||||
Vec<std::sync::Arc<dyn crate::layer::Layer>>,
|
||||
> = std::collections::HashMap::new();
|
||||
for middleware in server_fn::inventory::iter::<ServerFnMiddleware> {
|
||||
map.entry((middleware.prefix, middleware.url))
|
||||
.or_default()
|
||||
.extend((middleware.middleware)().iter().cloned());
|
||||
}
|
||||
map
|
||||
});
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
server_fn::inventory::collect!(ServerFnMiddleware);
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "server")))]
|
||||
/// A server function that can be called on serializable arguments and returns a serializable result.
|
||||
pub type ServerFunction = server_fn::SerializedFnTraitObj<()>;
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
#[allow(clippy::type_complexity)]
|
||||
static REGISTERED_SERVER_FUNCTIONS: once_cell::sync::Lazy<
|
||||
std::sync::Arc<std::sync::RwLock<std::collections::HashMap<&'static str, ServerFnTraitObj>>>,
|
||||
> = once_cell::sync::Lazy::new(|| {
|
||||
let mut map = std::collections::HashMap::new();
|
||||
for server_fn in server_fn::inventory::iter::<ServerFnTraitObj> {
|
||||
map.insert(server_fn.0.url(), server_fn.clone());
|
||||
}
|
||||
std::sync::Arc::new(std::sync::RwLock::new(map))
|
||||
});
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "server")))]
|
||||
/// The registry of all Dioxus server functions.
|
||||
pub struct DioxusServerFnRegistry;
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
impl server_fn::ServerFunctionRegistry<()> for DioxusServerFnRegistry {
|
||||
type Error = ServerRegistrationFnError;
|
||||
|
||||
fn register_explicit(
|
||||
prefix: &'static str,
|
||||
url: &'static str,
|
||||
server_function: ServerFunction,
|
||||
encoding: server_fn::Encoding,
|
||||
) -> Result<(), Self::Error> {
|
||||
// store it in the hashmap
|
||||
let mut write = REGISTERED_SERVER_FUNCTIONS
|
||||
.write()
|
||||
.map_err(|e| ServerRegistrationFnError::Poisoned(e.to_string()))?;
|
||||
let prev = write.insert(
|
||||
url,
|
||||
ServerFnTraitObj::new(prefix, url, encoding, server_function),
|
||||
);
|
||||
|
||||
// if there was already a server function with this key,
|
||||
// return Err
|
||||
match prev {
|
||||
Some(_) => Err(ServerRegistrationFnError::AlreadyRegistered(format!(
|
||||
"There was already a server function registered at {:?}. \
|
||||
This can happen if you use the same server function name \
|
||||
in two different modules
|
||||
on `stable` or in `release` mode.",
|
||||
url
|
||||
))),
|
||||
None => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the server function registered at the given URL, or `None` if no function is registered at that URL.
|
||||
fn get(url: &str) -> Option<server_fn::ServerFnTraitObj<()>> {
|
||||
REGISTERED_SERVER_FUNCTIONS
|
||||
.read()
|
||||
.ok()
|
||||
.and_then(|fns| fns.get(url).map(|inner| inner.0.clone()))
|
||||
}
|
||||
|
||||
/// Returns the server function registered at the given URL, or `None` if no function is registered at that URL.
|
||||
fn get_trait_obj(url: &str) -> Option<server_fn::ServerFnTraitObj<()>> {
|
||||
Self::get(url)
|
||||
}
|
||||
|
||||
fn get_encoding(url: &str) -> Option<server_fn::Encoding> {
|
||||
REGISTERED_SERVER_FUNCTIONS
|
||||
.read()
|
||||
.ok()
|
||||
.and_then(|fns| fns.get(url).map(|f| f.encoding()))
|
||||
}
|
||||
|
||||
/// Returns a list of all registered server functions.
|
||||
fn paths_registered() -> Vec<&'static str> {
|
||||
REGISTERED_SERVER_FUNCTIONS
|
||||
.read()
|
||||
.ok()
|
||||
.map(|fns| fns.keys().cloned().collect())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "server")))]
|
||||
/// Errors that can occur when registering a server function.
|
||||
#[derive(thiserror::Error, Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub enum ServerRegistrationFnError {
|
||||
/// The server function is already registered.
|
||||
#[error("The server function {0} is already registered")]
|
||||
AlreadyRegistered(String),
|
||||
/// The server function registry is poisoned.
|
||||
#[error("The server function registry is poisoned: {0}")]
|
||||
Poisoned(String),
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
#[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<()> {}
|
||||
@@ -0,0 +1,154 @@
|
||||
//! # 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()
|
||||
}
|
||||
Reference in new issue
Block a user