reorganize fullstack

This commit is contained in:
Evan Almloff committed 2024-02-16 09:03:35 -06:00
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),
}
+24
View File
@@ -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<()> {}
+154
View File
@@ -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()
}