mirror of
https://github.com/leptos-rs/leptos
synced 2024-11-10 06:44:17 +00:00
feat: make server functions framework agnostic (#596)
This commit is contained in:
parent
f46084723a
commit
0c261c0fb0
13 changed files with 773 additions and 345 deletions
|
@ -7,6 +7,7 @@ members = [
|
|||
"leptos_macro",
|
||||
"leptos_reactive",
|
||||
"leptos_server",
|
||||
"server_fn",
|
||||
|
||||
# integrations
|
||||
"integrations/actix",
|
||||
|
@ -29,6 +30,8 @@ leptos_dom = { path = "./leptos_dom", default-features = false, version = "0.2.0
|
|||
leptos_macro = { path = "./leptos_macro", default-features = false, version = "0.2.0" }
|
||||
leptos_reactive = { path = "./leptos_reactive", default-features = false, version = "0.2.0" }
|
||||
leptos_server = { path = "./leptos_server", default-features = false, version = "0.2.0" }
|
||||
server_fn = { path = "./server_fn", default-features = false, version = "0.2.0" }
|
||||
server_fn_macro = { path = "./server_fn_macro", default-features = false, version = "0.2.0" }
|
||||
leptos_config = { path = "./leptos_config", default-features = false, version = "0.2.0" }
|
||||
leptos_router = { path = "./router", version = "0.2.0" }
|
||||
leptos_meta = { path = "./meta", default-feature = false, version = "0.2.0" }
|
||||
|
|
|
@ -17,6 +17,7 @@ leptos_server = { workspace = true }
|
|||
leptos_config = { workspace = true }
|
||||
tracing = "0.1"
|
||||
typed-builder = "0.12"
|
||||
server_fn = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
leptos = { path = ".", default-features = false }
|
||||
|
|
|
@ -168,6 +168,7 @@ pub use leptos_server::{
|
|||
self, create_action, create_multi_action, create_server_action,
|
||||
create_server_multi_action, Action, MultiAction, ServerFn, ServerFnError,
|
||||
};
|
||||
pub use server_fn::{self, ServerFn as _};
|
||||
pub use typed_builder;
|
||||
mod error_boundary;
|
||||
pub use error_boundary::*;
|
||||
|
|
|
@ -24,6 +24,7 @@ syn-rsx = "0.9"
|
|||
leptos_dom = { workspace = true }
|
||||
leptos_reactive = { workspace = true }
|
||||
leptos_server = { workspace = true }
|
||||
server_fn_macro = { workspace = true }
|
||||
convert_case = "0.6.0"
|
||||
uuid = { version = "1", features = ["v4"] }
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ extern crate proc_macro_error;
|
|||
use proc_macro::TokenStream;
|
||||
use proc_macro2::TokenTree;
|
||||
use quote::ToTokens;
|
||||
use server::server_macro_impl;
|
||||
use server_fn_macro::{server_macro_impl, ServerContext};
|
||||
use syn::parse_macro_input;
|
||||
use syn_rsx::{parse, NodeAttribute, NodeElement};
|
||||
|
||||
|
@ -35,7 +35,6 @@ mod view;
|
|||
use template::render_template;
|
||||
use view::render_view;
|
||||
mod component;
|
||||
mod server;
|
||||
mod template;
|
||||
|
||||
/// The `view` macro uses RSX (like JSX, but Rust!) It follows most of the
|
||||
|
@ -678,7 +677,16 @@ pub fn component(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
|
|||
/// 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 {
|
||||
match server_macro_impl(args, s.into()) {
|
||||
let context = ServerContext {
|
||||
ty: syn::parse_quote!(Scope),
|
||||
path: syn::parse_quote!(::leptos::Scope),
|
||||
};
|
||||
match server_macro_impl(
|
||||
args.into(),
|
||||
s.into(),
|
||||
Some(context),
|
||||
Some(syn::parse_quote!(::leptos::server_fn)),
|
||||
) {
|
||||
Err(e) => e.to_compile_error().into(),
|
||||
Ok(s) => s.to_token_stream().into(),
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ readme = "../README.md"
|
|||
[dependencies]
|
||||
leptos_dom = { workspace = true }
|
||||
leptos_reactive = { workspace = true }
|
||||
server_fn = { workspace = true }
|
||||
form_urlencoded = "1"
|
||||
gloo-net = "0.2"
|
||||
js-sys = "0.3"
|
||||
|
@ -41,6 +42,7 @@ hydrate = [
|
|||
ssr = [
|
||||
#"leptos/ssr",
|
||||
"leptos_reactive/ssr",
|
||||
"server_fn/ssr",
|
||||
]
|
||||
stable = [
|
||||
#"leptos/stable",
|
||||
|
|
|
@ -79,15 +79,7 @@
|
|||
//! or response or other server-only dependencies, but it does *not* have access to reactive state that exists in the client.
|
||||
|
||||
use leptos_reactive::*;
|
||||
use proc_macro2::{Literal, TokenStream};
|
||||
use quote::TokenStreamExt;
|
||||
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||
use std::{future::Future, pin::Pin, str::FromStr};
|
||||
use syn::{
|
||||
parse::{Parse, ParseStream},
|
||||
parse_quote,
|
||||
};
|
||||
use thiserror::Error;
|
||||
pub use server_fn::{Encoding, Payload, ServerFnError};
|
||||
|
||||
mod action;
|
||||
mod multi_action;
|
||||
|
@ -100,27 +92,77 @@ use std::{
|
|||
};
|
||||
|
||||
#[cfg(any(feature = "ssr", doc))]
|
||||
type ServerFnTraitObj = dyn Fn(
|
||||
Scope,
|
||||
&[u8],
|
||||
) -> Pin<Box<dyn Future<Output = Result<Payload, ServerFnError>>>>
|
||||
+ Send
|
||||
+ Sync;
|
||||
type ServerFnTraitObj = server_fn::ServerFnTraitObj<Scope>;
|
||||
|
||||
#[cfg(any(feature = "ssr", doc))]
|
||||
lazy_static::lazy_static! {
|
||||
static ref REGISTERED_SERVER_FUNCTIONS: Arc<RwLock<HashMap<&'static str, Arc<ServerFnTraitObj>>>> = Default::default();
|
||||
}
|
||||
|
||||
/// A dual type to hold the possible Response datatypes
|
||||
#[derive(Debug)]
|
||||
pub enum Payload {
|
||||
///Encodes Data using CBOR
|
||||
Binary(Vec<u8>),
|
||||
///Encodes data in the URL
|
||||
Url(String),
|
||||
///Encodes Data using Json
|
||||
Json(String),
|
||||
#[cfg(any(feature = "ssr", doc))]
|
||||
/// The registry of all Leptos server functions.
|
||||
pub struct LeptosServerFnRegistry;
|
||||
|
||||
#[cfg(any(feature = "ssr"))]
|
||||
impl server_fn::ServerFunctionRegistry<Scope> for LeptosServerFnRegistry {
|
||||
type Error = ServerRegistrationFnError;
|
||||
|
||||
fn register(
|
||||
url: &'static str,
|
||||
server_function: Arc<ServerFnTraitObj>,
|
||||
) -> 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, 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<Arc<ServerFnTraitObj>> {
|
||||
REGISTERED_SERVER_FUNCTIONS
|
||||
.read()
|
||||
.ok()
|
||||
.and_then(|fns| fns.get(url).cloned())
|
||||
}
|
||||
|
||||
/// 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(any(feature = "ssr", doc))]
|
||||
/// 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),
|
||||
}
|
||||
|
||||
/// Attempts to find a server function registered at the given path.
|
||||
|
@ -168,67 +210,13 @@ pub enum Payload {
|
|||
/// ```
|
||||
#[cfg(any(feature = "ssr", doc))]
|
||||
pub fn server_fn_by_path(path: &str) -> Option<Arc<ServerFnTraitObj>> {
|
||||
REGISTERED_SERVER_FUNCTIONS
|
||||
.read()
|
||||
.ok()
|
||||
.and_then(|fns| fns.get(path).cloned())
|
||||
server_fn::server_fn_by_path::<Scope, LeptosServerFnRegistry>(path)
|
||||
}
|
||||
|
||||
/// Returns the set of currently-registered server function paths, for debugging purposes.
|
||||
#[cfg(any(feature = "ssr", doc))]
|
||||
pub fn server_fns_by_path() -> Vec<&'static str> {
|
||||
REGISTERED_SERVER_FUNCTIONS
|
||||
.read()
|
||||
.map(|vals| vals.keys().copied().collect())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Holds the current options for encoding types.
|
||||
/// More could be added, but they need to be serde
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum Encoding {
|
||||
/// A Binary Encoding Scheme Called Cbor
|
||||
Cbor,
|
||||
/// The Default URL-encoded encoding method
|
||||
Url,
|
||||
}
|
||||
|
||||
impl FromStr for Encoding {
|
||||
type Err = ();
|
||||
|
||||
fn from_str(input: &str) -> Result<Encoding, Self::Err> {
|
||||
match input {
|
||||
"URL" => Ok(Encoding::Url),
|
||||
"Cbor" => Ok(Encoding::Cbor),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl quote::ToTokens for Encoding {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
let option: syn::Ident = match *self {
|
||||
Encoding::Cbor => parse_quote!(Cbor),
|
||||
Encoding::Url => parse_quote!(Url),
|
||||
};
|
||||
let expansion: syn::Ident = syn::parse_quote! {
|
||||
Encoding::#option
|
||||
};
|
||||
tokens.append(expansion);
|
||||
}
|
||||
}
|
||||
|
||||
impl Parse for Encoding {
|
||||
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||
let variant_name: String = input.parse::<Literal>()?.to_string();
|
||||
|
||||
// Need doubled quotes because variant_name doubles it
|
||||
match variant_name.as_ref() {
|
||||
"\"Url\"" => Ok(Self::Url),
|
||||
"\"Cbor\"" => Ok(Self::Cbor),
|
||||
_ => panic!("Encoding Not Found"),
|
||||
}
|
||||
}
|
||||
server_fn::server_fns_by_path::<Scope, LeptosServerFnRegistry>()
|
||||
}
|
||||
|
||||
/// Defines a "server function." A server function can be called from the server or the client,
|
||||
|
@ -243,220 +231,12 @@ impl Parse for Encoding {
|
|||
/// can be queried on the server for routing purposes by calling [server_fn_by_path].
|
||||
///
|
||||
/// Technically, the trait is implemented on a type that describes the server function's arguments.
|
||||
pub trait ServerFn
|
||||
where
|
||||
Self: Serialize + DeserializeOwned + Sized + 'static,
|
||||
{
|
||||
/// The return type of the function.
|
||||
type Output: Serialize;
|
||||
|
||||
/// URL prefix that should be prepended by the client to the generated URL.
|
||||
fn prefix() -> &'static str;
|
||||
|
||||
/// The path at which the server function can be reached on the server.
|
||||
fn url() -> &'static str;
|
||||
|
||||
/// The path at which the server function can be reached on the server.
|
||||
fn encoding() -> Encoding;
|
||||
|
||||
/// Runs the function on the server.
|
||||
#[cfg(any(feature = "ssr", doc))]
|
||||
fn call_fn(
|
||||
self,
|
||||
cx: Scope,
|
||||
) -> Pin<Box<dyn Future<Output = Result<Self::Output, ServerFnError>>>>;
|
||||
|
||||
/// Runs the function on the client by sending an HTTP request to the server.
|
||||
#[cfg(any(not(feature = "ssr"), doc))]
|
||||
fn call_fn_client(
|
||||
self,
|
||||
cx: Scope,
|
||||
) -> Pin<Box<dyn Future<Output = Result<Self::Output, ServerFnError>>>>;
|
||||
|
||||
pub trait ServerFn: server_fn::ServerFn<Scope> {
|
||||
/// Registers the server function, allowing the server to query it by URL.
|
||||
#[cfg(any(feature = "ssr", doc))]
|
||||
fn register() -> Result<(), ServerFnError> {
|
||||
// create the handler for this server function
|
||||
// takes a String -> returns its async value
|
||||
|
||||
let run_server_fn = Arc::new(|cx: Scope, data: &[u8]| {
|
||||
// decode the args
|
||||
let value = match Self::encoding() {
|
||||
Encoding::Url => serde_urlencoded::from_bytes(data)
|
||||
.map_err(|e| ServerFnError::Deserialization(e.to_string())),
|
||||
Encoding::Cbor => ciborium::de::from_reader(data)
|
||||
.map_err(|e| ServerFnError::Deserialization(e.to_string())),
|
||||
};
|
||||
Box::pin(async move {
|
||||
let value: Self = match value {
|
||||
Ok(v) => v,
|
||||
Err(e) => return Err(e),
|
||||
};
|
||||
|
||||
// call the function
|
||||
let result = match value.call_fn(cx).await {
|
||||
Ok(r) => r,
|
||||
Err(e) => return Err(e),
|
||||
};
|
||||
|
||||
// serialize the output
|
||||
let result = match Self::encoding() {
|
||||
Encoding::Url => match serde_json::to_string(&result)
|
||||
.map_err(|e| {
|
||||
ServerFnError::Serialization(e.to_string())
|
||||
}) {
|
||||
Ok(r) => Payload::Url(r),
|
||||
Err(e) => return Err(e),
|
||||
},
|
||||
Encoding::Cbor => {
|
||||
let mut buffer: Vec<u8> = Vec::new();
|
||||
match ciborium::ser::into_writer(&result, &mut buffer)
|
||||
.map_err(|e| {
|
||||
ServerFnError::Serialization(e.to_string())
|
||||
}) {
|
||||
Ok(_) => Payload::Binary(buffer),
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Ok(result)
|
||||
})
|
||||
as Pin<Box<dyn Future<Output = Result<Payload, ServerFnError>>>>
|
||||
});
|
||||
|
||||
// store it in the hashmap
|
||||
let mut write = REGISTERED_SERVER_FUNCTIONS
|
||||
.write()
|
||||
.map_err(|e| ServerFnError::Registration(e.to_string()))?;
|
||||
let prev = write.insert(Self::url(), run_server_fn);
|
||||
|
||||
// if there was already a server function with this key,
|
||||
// return Err
|
||||
match prev {
|
||||
Some(_) => Err(ServerFnError::Registration(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.",
|
||||
Self::url()
|
||||
))),
|
||||
None => Ok(()),
|
||||
}
|
||||
Self::register_in::<LeptosServerFnRegistry>()
|
||||
}
|
||||
}
|
||||
|
||||
/// Type for errors that can occur when using server functions.
|
||||
#[derive(Error, Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum ServerFnError {
|
||||
/// Error while trying to register the server function (only occurs in case of poisoned RwLock).
|
||||
#[error("error while trying to register the server function: {0}")]
|
||||
Registration(String),
|
||||
/// Occurs on the client if there is a network error while trying to run function on server.
|
||||
#[error("error reaching server to call server function: {0}")]
|
||||
Request(String),
|
||||
/// Occurs when there is an error while actually running the function on the server.
|
||||
#[error("error running server function: {0}")]
|
||||
ServerError(String),
|
||||
/// Occurs on the client if there is an error deserializing the server's response.
|
||||
#[error("error deserializing server function results {0}")]
|
||||
Deserialization(String),
|
||||
/// Occurs on the client if there is an error serializing the server function arguments.
|
||||
#[error("error serializing server function results {0}")]
|
||||
Serialization(String),
|
||||
/// Occurs on the server if there is an error deserializing one of the arguments that's been sent.
|
||||
#[error("error deserializing server function arguments {0}")]
|
||||
Args(String),
|
||||
/// Occurs on the server if there's a missing argument.
|
||||
#[error("missing argument {0}")]
|
||||
MissingArg(String),
|
||||
}
|
||||
|
||||
/// Executes the HTTP call to call a server function from the client, given its URL and argument type.
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
pub async fn call_server_fn<T>(
|
||||
url: &str,
|
||||
args: impl ServerFn,
|
||||
enc: Encoding,
|
||||
) -> Result<T, ServerFnError>
|
||||
where
|
||||
T: serde::Serialize + serde::de::DeserializeOwned + Sized,
|
||||
{
|
||||
use ciborium::ser::into_writer;
|
||||
use js_sys::Uint8Array;
|
||||
use serde_json::Deserializer as JSONDeserializer;
|
||||
|
||||
#[derive(Debug)]
|
||||
enum Payload {
|
||||
Binary(Vec<u8>),
|
||||
Url(String),
|
||||
}
|
||||
let args_encoded = match &enc {
|
||||
Encoding::Url => Payload::Url(
|
||||
serde_urlencoded::to_string(&args)
|
||||
.map_err(|e| ServerFnError::Serialization(e.to_string()))?,
|
||||
),
|
||||
Encoding::Cbor => {
|
||||
let mut buffer: Vec<u8> = Vec::new();
|
||||
into_writer(&args, &mut buffer)
|
||||
.map_err(|e| ServerFnError::Serialization(e.to_string()))?;
|
||||
Payload::Binary(buffer)
|
||||
}
|
||||
};
|
||||
|
||||
let content_type_header = match &enc {
|
||||
Encoding::Url => "application/x-www-form-urlencoded",
|
||||
Encoding::Cbor => "application/cbor",
|
||||
};
|
||||
|
||||
let accept_header = match &enc {
|
||||
Encoding::Url => "application/x-www-form-urlencoded",
|
||||
Encoding::Cbor => "application/cbor",
|
||||
};
|
||||
|
||||
let resp = match args_encoded {
|
||||
Payload::Binary(b) => {
|
||||
let slice_ref: &[u8] = &b;
|
||||
let js_array = Uint8Array::from(slice_ref).buffer();
|
||||
gloo_net::http::Request::post(url)
|
||||
.header("Content-Type", content_type_header)
|
||||
.header("Accept", accept_header)
|
||||
.body(js_array)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| ServerFnError::Request(e.to_string()))?
|
||||
}
|
||||
Payload::Url(s) => gloo_net::http::Request::post(url)
|
||||
.header("Content-Type", content_type_header)
|
||||
.header("Accept", accept_header)
|
||||
.body(s)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| ServerFnError::Request(e.to_string()))?,
|
||||
};
|
||||
|
||||
// check for error status
|
||||
let status = resp.status();
|
||||
if (500..=599).contains(&status) {
|
||||
return Err(ServerFnError::ServerError(resp.status_text()));
|
||||
}
|
||||
|
||||
if enc == Encoding::Cbor {
|
||||
let binary = resp
|
||||
.binary()
|
||||
.await
|
||||
.map_err(|e| ServerFnError::Deserialization(e.to_string()))?;
|
||||
|
||||
ciborium::de::from_reader(binary.as_slice())
|
||||
.map_err(|e| ServerFnError::Deserialization(e.to_string()))
|
||||
} else {
|
||||
let text = resp
|
||||
.text()
|
||||
.await
|
||||
.map_err(|e| ServerFnError::Deserialization(e.to_string()))?;
|
||||
|
||||
let mut deserializer = JSONDeserializer::from_str(&text);
|
||||
T::deserialize(&mut deserializer)
|
||||
.map_err(|e| ServerFnError::Deserialization(e.to_string()))
|
||||
}
|
||||
}
|
||||
impl<T> ServerFn for T where T: server_fn::ServerFn<Scope> {}
|
||||
|
|
30
server_fn/Cargo.toml
Normal file
30
server_fn/Cargo.toml
Normal file
|
@ -0,0 +1,30 @@
|
|||
[package]
|
||||
name = "server_fn"
|
||||
version = { workspace = true }
|
||||
edition = "2021"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
repository = "https://github.com/leptos-rs/leptos"
|
||||
description = "RPC for any web framework."
|
||||
readme = "../README.md"
|
||||
|
||||
[dependencies]
|
||||
form_urlencoded = "1"
|
||||
gloo-net = "0.2"
|
||||
js-sys = "0.3"
|
||||
lazy_static = "1"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_urlencoded = "0.7"
|
||||
thiserror = "1"
|
||||
serde_json = "1.0.89"
|
||||
quote = "1"
|
||||
syn = { version = "1", features = ["full", "parsing", "extra-traits"] }
|
||||
proc-macro2 = "1"
|
||||
cfg-if = "1"
|
||||
ciborium = "0.2.0"
|
||||
xxhash-rust = { version = "0.8.6", features = ["const_xxh64"] }
|
||||
const_format = "0.2.30"
|
||||
server_fn_macro_default = { path = "./server_fn_macro_default" }
|
||||
|
||||
[features]
|
||||
ssr = []
|
20
server_fn/server_fn_macro_default/Cargo.toml
Normal file
20
server_fn/server_fn_macro_default/Cargo.toml
Normal file
|
@ -0,0 +1,20 @@
|
|||
[package]
|
||||
name = "server_fn_macro_default"
|
||||
version = { workspace = true }
|
||||
edition = "2021"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
repository = "https://github.com/leptos-rs/leptos"
|
||||
description = "The default implementation of the server_fn macro without a context"
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
proc-macro2 = "1"
|
||||
syn = { version = "1", features = ["full"] }
|
||||
server_fn_macro = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
server_fn = { workspace = true }
|
||||
serde = "1"
|
63
server_fn/server_fn_macro_default/src/lib.rs
Normal file
63
server_fn/server_fn_macro_default/src/lib.rs
Normal file
|
@ -0,0 +1,63 @@
|
|||
#![cfg_attr(not(feature = "stable"), feature(proc_macro_span))]
|
||||
//! This crate contains the default implementation of the #[macro@crate::server] macro without a context from the server. See the [server_fn_macro] crate for more information.
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
use proc_macro::TokenStream;
|
||||
use server_fn_macro::server_macro_impl;
|
||||
use syn::__private::ToTokens;
|
||||
|
||||
/// Declares that a function is a [server function](server_fn). This means that
|
||||
/// its body will only run on the server, i.e., when the `ssr` feature is enabled.
|
||||
///
|
||||
/// You can specify one, two, or three arguments to the server function:
|
||||
/// 1. **Required**: A type name that will be used to identify and register the server function
|
||||
/// (e.g., `MyServerFn`).
|
||||
/// 2. *Optional*: A URL prefix at which the function will be mounted when it’s registered
|
||||
/// (e.g., `"/api"`). Defaults to `"/"`.
|
||||
/// 3. *Optional*: either `"Cbor"` (specifying that it should use the binary `cbor` format for
|
||||
/// serialization) or `"Url"` (specifying that it should be use a URL-encoded form-data string).
|
||||
/// Defaults to `"Url"`. If you want to use this server function to power a `<form>` that will
|
||||
/// 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`.
|
||||
///
|
||||
/// ```ignore
|
||||
/// # use server_fn::*; 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!()
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Note the following:
|
||||
/// - You must **register** the server function by calling `T::register()` somewhere in your main function.
|
||||
/// - **Server functions must be `async`.** Even if the work being done inside the function body
|
||||
/// can run synchronously on the server, from the client’s perspective it involves an asynchronous
|
||||
/// function call.
|
||||
/// - **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 implement [Serialize](serde::Serialize).**
|
||||
/// 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)
|
||||
/// and [`DeserializeOwned`](https://docs.rs/serde/latest/serde/de/trait.DeserializeOwned.html).**
|
||||
/// 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/).
|
||||
#[proc_macro_attribute]
|
||||
pub fn server(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
|
||||
match server_macro_impl(
|
||||
args.into(),
|
||||
s.into(),
|
||||
None,
|
||||
Some(syn::parse_quote!(server_fn)),
|
||||
) {
|
||||
Err(e) => e.to_compile_error().into(),
|
||||
Ok(s) => s.to_token_stream().into(),
|
||||
}
|
||||
}
|
432
server_fn/src/lib.rs
Normal file
432
server_fn/src/lib.rs
Normal file
|
@ -0,0 +1,432 @@
|
|||
#![forbid(unsafe_code)]
|
||||
#![deny(missing_docs)]
|
||||
|
||||
//! # Server Functions
|
||||
//!
|
||||
//! This package is based on a simple idea: sometimes it’s useful to write functions
|
||||
//! that will only run on the server, and call them from the client.
|
||||
//!
|
||||
//! If you’re creating anything beyond a toy app, you’ll need to do this all the time:
|
||||
//! reading from or writing to a database that only runs on the server, running expensive
|
||||
//! computations using libraries you don’t want to ship down to the client, accessing
|
||||
//! APIs that need to be called from the server rather than the client for CORS reasons
|
||||
//! or because you need a secret API key that’s stored on the server and definitely
|
||||
//! shouldn’t be shipped down to a user’s browser.
|
||||
//!
|
||||
//! Traditionally, this is done by separating your server and client code, and by setting
|
||||
//! up something like a REST API or GraphQL API to allow your client to fetch and mutate
|
||||
//! data on the server. This is fine, but it requires you to write and maintain your code
|
||||
//! in multiple separate places (client-side code for fetching, server-side functions to run),
|
||||
//! as well as creating a third thing to manage, which is the API contract between the two.
|
||||
//!
|
||||
//! This package provides two simple primitives that allow you instead to write co-located,
|
||||
//! isomorphic server functions. (*Co-located* means you can write them in your app code so
|
||||
//! that they are “located alongside” the client code that calls them, rather than separating
|
||||
//! the client and server sides. *Isomorphic* means you can call them from the client as if
|
||||
//! you were simply calling a function; the function call has the “same shape” on the client
|
||||
//! as it does on the server.)
|
||||
//!
|
||||
//! ### `#[server]`
|
||||
//!
|
||||
//! The [`#[server]`](https://docs.rs/server_fn/latest/server_fn/attr.server.html) macro allows you to annotate a function to
|
||||
//! indicate that it should only run on the server (i.e., when you have an `ssr` feature in your
|
||||
//! crate that is enabled).
|
||||
//!
|
||||
//! **Important**: All server functions must be registered by calling [ServerFn::register_in]
|
||||
//! somewhere within your `main` function.
|
||||
//!
|
||||
//! ```rust,ignore
|
||||
//! #[server(ReadFromDB)]
|
||||
//! async fn read_posts(how_many: usize, query: String) -> Result<Vec<Posts>, ServerFnError> {
|
||||
//! // do some server-only work here to access the database
|
||||
//! let posts = ...;
|
||||
//! Ok(posts)
|
||||
//! }
|
||||
//!
|
||||
//! // call the function
|
||||
//! # #[tokio::main]
|
||||
//! # async fn main() {
|
||||
//! async {
|
||||
//! let posts = read_posts(3, "my search".to_string()).await;
|
||||
//! log::debug!("posts = {posts{:#?}");
|
||||
//! }
|
||||
//! # }
|
||||
//!
|
||||
//! // make sure you've registered it somewhere in main
|
||||
//! fn main() {
|
||||
//! _ = ReadFromDB::register();
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! If you call this function from the client, it will serialize the function arguments and `POST`
|
||||
//! them to the server as if they were the inputs in `<form method="POST">`.
|
||||
//!
|
||||
//! Here’s what you need to remember:
|
||||
//! - **Server functions must be `async`.** Even if the work being done inside the function body
|
||||
//! can run synchronously on the server, from the client’s perspective it involves an asynchronous
|
||||
//! function call.
|
||||
//! - **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 implement [Serialize](serde::Serialize).**
|
||||
//! 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 [serde::Serialize].** 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/).
|
||||
|
||||
// used by the macro
|
||||
#[doc(hidden)]
|
||||
pub use const_format;
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::TokenStreamExt;
|
||||
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||
pub use server_fn_macro_default::server;
|
||||
#[cfg(any(feature = "ssr", doc))]
|
||||
use std::sync::Arc;
|
||||
use std::{future::Future, pin::Pin, str::FromStr};
|
||||
use syn::parse_quote;
|
||||
use thiserror::Error;
|
||||
// used by the macro
|
||||
#[doc(hidden)]
|
||||
pub use xxhash_rust;
|
||||
|
||||
#[cfg(any(feature = "ssr", doc))]
|
||||
/// Something that can register a server function.
|
||||
pub trait ServerFunctionRegistry<T> {
|
||||
/// An error that can occur when registering a server function.
|
||||
type Error: std::error::Error;
|
||||
/// Registers a server function at the given URL.
|
||||
fn register(
|
||||
url: &'static str,
|
||||
server_function: Arc<ServerFnTraitObj<T>>,
|
||||
) -> Result<(), Self::Error>;
|
||||
/// Returns the server function registered at the given URL, or `None` if no function is registered at that URL.
|
||||
fn get(url: &str) -> Option<Arc<ServerFnTraitObj<T>>>;
|
||||
/// Returns a list of all registered server functions.
|
||||
fn paths_registered() -> Vec<&'static str>;
|
||||
}
|
||||
|
||||
/// A server function that can be called from the client.
|
||||
pub type ServerFnTraitObj<T> = dyn Fn(
|
||||
T,
|
||||
&[u8],
|
||||
) -> Pin<Box<dyn Future<Output = Result<Payload, ServerFnError>>>>
|
||||
+ Send
|
||||
+ Sync;
|
||||
|
||||
/// A dual type to hold the possible Response datatypes
|
||||
#[derive(Debug)]
|
||||
pub enum Payload {
|
||||
///Encodes Data using CBOR
|
||||
Binary(Vec<u8>),
|
||||
///Encodes data in the URL
|
||||
Url(String),
|
||||
///Encodes Data using Json
|
||||
Json(String),
|
||||
}
|
||||
|
||||
/// Attempts to find a server function registered at the given path.
|
||||
///
|
||||
/// This can be used by a server to handle the requests, as in the following example (using `actix-web`)
|
||||
///
|
||||
/// ```rust, ignore
|
||||
/// #[post("{tail:.*}")]
|
||||
/// async fn handle_server_fns(
|
||||
/// req: HttpRequest,
|
||||
/// params: web::Path<String>,
|
||||
/// body: web::Bytes,
|
||||
/// ) -> impl Responder {
|
||||
/// let path = params.into_inner();
|
||||
/// let accept_header = req
|
||||
/// .headers()
|
||||
/// .get("Accept")
|
||||
/// .and_then(|value| value.to_str().ok());
|
||||
///
|
||||
/// if let Some(server_fn) = server_fn_by_path::<MyRegistry>(path.as_str()) {
|
||||
/// let body: &[u8] = &body;
|
||||
/// match server_fn(&body).await {
|
||||
/// Ok(serialized) => {
|
||||
/// // if this is Accept: application/json then send a serialized JSON response
|
||||
/// if let Some("application/json") = accept_header {
|
||||
/// HttpResponse::Ok().body(serialized)
|
||||
/// }
|
||||
/// // otherwise, it's probably a <form> submit or something: redirect back to the referrer
|
||||
/// else {
|
||||
/// HttpResponse::SeeOther()
|
||||
/// .insert_header(("Location", "/"))
|
||||
/// .content_type("application/json")
|
||||
/// .body(serialized)
|
||||
/// }
|
||||
/// }
|
||||
/// Err(e) => {
|
||||
/// eprintln!("server function error: {e:#?}");
|
||||
/// HttpResponse::InternalServerError().body(e.to_string())
|
||||
/// }
|
||||
/// }
|
||||
/// } else {
|
||||
/// HttpResponse::BadRequest().body(format!("Could not find a server function at that route."))
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
#[cfg(any(feature = "ssr", doc))]
|
||||
pub fn server_fn_by_path<T: 'static, R: ServerFunctionRegistry<T>>(
|
||||
path: &str,
|
||||
) -> Option<Arc<ServerFnTraitObj<T>>> {
|
||||
R::get(path)
|
||||
}
|
||||
|
||||
/// Returns the set of currently-registered server function paths, for debugging purposes.
|
||||
#[cfg(any(feature = "ssr", doc))]
|
||||
pub fn server_fns_by_path<T: 'static, R: ServerFunctionRegistry<T>>(
|
||||
) -> Vec<&'static str> {
|
||||
R::paths_registered()
|
||||
}
|
||||
|
||||
/// Holds the current options for encoding types.
|
||||
/// More could be added, but they need to be serde
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum Encoding {
|
||||
/// A Binary Encoding Scheme Called Cbor
|
||||
Cbor,
|
||||
/// The Default URL-encoded encoding method
|
||||
Url,
|
||||
}
|
||||
|
||||
impl FromStr for Encoding {
|
||||
type Err = ();
|
||||
|
||||
fn from_str(input: &str) -> Result<Encoding, Self::Err> {
|
||||
match input {
|
||||
"URL" => Ok(Encoding::Url),
|
||||
"Cbor" => Ok(Encoding::Cbor),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl quote::ToTokens for Encoding {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
let option: syn::Ident = match *self {
|
||||
Encoding::Cbor => parse_quote!(Cbor),
|
||||
Encoding::Url => parse_quote!(Url),
|
||||
};
|
||||
let expansion: syn::Ident = syn::parse_quote! {
|
||||
Encoding::#option
|
||||
};
|
||||
tokens.append(expansion);
|
||||
}
|
||||
}
|
||||
|
||||
/// 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` (server-side-rendering) is enabled.
|
||||
///
|
||||
/// Server functions are created using the `server` macro.
|
||||
///
|
||||
/// The function should be registered by calling `ServerFn::register()`. The set of server functions
|
||||
/// can be queried on the server for routing purposes by calling [server_fn_by_path].
|
||||
///
|
||||
/// Technically, the trait is implemented on a type that describes the server function's arguments.
|
||||
pub trait ServerFn<T: 'static>
|
||||
where
|
||||
Self: Serialize + DeserializeOwned + Sized + 'static,
|
||||
{
|
||||
/// The return type of the function.
|
||||
type Output: Serialize;
|
||||
|
||||
/// URL prefix that should be prepended by the client to the generated URL.
|
||||
fn prefix() -> &'static str;
|
||||
|
||||
/// The path at which the server function can be reached on the server.
|
||||
fn url() -> &'static str;
|
||||
|
||||
/// The path at which the server function can be reached on the server.
|
||||
fn encoding() -> Encoding;
|
||||
|
||||
/// Runs the function on the server.
|
||||
#[cfg(any(feature = "ssr", doc))]
|
||||
fn call_fn(
|
||||
self,
|
||||
cx: T,
|
||||
) -> Pin<Box<dyn Future<Output = Result<Self::Output, ServerFnError>>>>;
|
||||
|
||||
/// Runs the function on the client by sending an HTTP request to the server.
|
||||
#[cfg(any(not(feature = "ssr"), doc))]
|
||||
fn call_fn_client(
|
||||
self,
|
||||
cx: T,
|
||||
) -> Pin<Box<dyn Future<Output = Result<Self::Output, ServerFnError>>>>;
|
||||
|
||||
/// Registers the server function, allowing the server to query it by URL.
|
||||
#[cfg(any(feature = "ssr", doc))]
|
||||
fn register_in<R: ServerFunctionRegistry<T>>() -> Result<(), ServerFnError>
|
||||
{
|
||||
// create the handler for this server function
|
||||
// takes a String -> returns its async value
|
||||
|
||||
let run_server_fn = Arc::new(|cx: T, data: &[u8]| {
|
||||
// decode the args
|
||||
let value = match Self::encoding() {
|
||||
Encoding::Url => serde_urlencoded::from_bytes(data)
|
||||
.map_err(|e| ServerFnError::Deserialization(e.to_string())),
|
||||
Encoding::Cbor => ciborium::de::from_reader(data)
|
||||
.map_err(|e| ServerFnError::Deserialization(e.to_string())),
|
||||
};
|
||||
Box::pin(async move {
|
||||
let value: Self = match value {
|
||||
Ok(v) => v,
|
||||
Err(e) => return Err(e),
|
||||
};
|
||||
|
||||
// call the function
|
||||
let result = match value.call_fn(cx).await {
|
||||
Ok(r) => r,
|
||||
Err(e) => return Err(e),
|
||||
};
|
||||
|
||||
// serialize the output
|
||||
let result = match Self::encoding() {
|
||||
Encoding::Url => match serde_json::to_string(&result)
|
||||
.map_err(|e| {
|
||||
ServerFnError::Serialization(e.to_string())
|
||||
}) {
|
||||
Ok(r) => Payload::Url(r),
|
||||
Err(e) => return Err(e),
|
||||
},
|
||||
Encoding::Cbor => {
|
||||
let mut buffer: Vec<u8> = Vec::new();
|
||||
match ciborium::ser::into_writer(&result, &mut buffer)
|
||||
.map_err(|e| {
|
||||
ServerFnError::Serialization(e.to_string())
|
||||
}) {
|
||||
Ok(_) => Payload::Binary(buffer),
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Ok(result)
|
||||
})
|
||||
as Pin<Box<dyn Future<Output = Result<Payload, ServerFnError>>>>
|
||||
});
|
||||
|
||||
// store it in the hashmap
|
||||
R::register(Self::url(), run_server_fn)
|
||||
.map_err(|e| ServerFnError::Registration(e.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Type for errors that can occur when using server functions.
|
||||
#[derive(Error, Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum ServerFnError {
|
||||
/// Error while trying to register the server function (only occurs in case of poisoned RwLock).
|
||||
#[error("error while trying to register the server function: {0}")]
|
||||
Registration(String),
|
||||
/// Occurs on the client if there is a network error while trying to run function on server.
|
||||
#[error("error reaching server to call server function: {0}")]
|
||||
Request(String),
|
||||
/// Occurs when there is an error while actually running the function on the server.
|
||||
#[error("error running server function: {0}")]
|
||||
ServerError(String),
|
||||
/// Occurs on the client if there is an error deserializing the server's response.
|
||||
#[error("error deserializing server function results {0}")]
|
||||
Deserialization(String),
|
||||
/// Occurs on the client if there is an error serializing the server function arguments.
|
||||
#[error("error serializing server function results {0}")]
|
||||
Serialization(String),
|
||||
/// Occurs on the server if there is an error deserializing one of the arguments that's been sent.
|
||||
#[error("error deserializing server function arguments {0}")]
|
||||
Args(String),
|
||||
/// Occurs on the server if there's a missing argument.
|
||||
#[error("missing argument {0}")]
|
||||
MissingArg(String),
|
||||
}
|
||||
|
||||
/// Executes the HTTP call to call a server function from the client, given its URL and argument type.
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
pub async fn call_server_fn<T, C: 'static>(
|
||||
url: &str,
|
||||
args: impl ServerFn<C>,
|
||||
enc: Encoding,
|
||||
) -> Result<T, ServerFnError>
|
||||
where
|
||||
T: serde::Serialize + serde::de::DeserializeOwned + Sized,
|
||||
{
|
||||
use ciborium::ser::into_writer;
|
||||
use js_sys::Uint8Array;
|
||||
use serde_json::Deserializer as JSONDeserializer;
|
||||
|
||||
#[derive(Debug)]
|
||||
enum Payload {
|
||||
Binary(Vec<u8>),
|
||||
Url(String),
|
||||
}
|
||||
let args_encoded = match &enc {
|
||||
Encoding::Url => Payload::Url(
|
||||
serde_urlencoded::to_string(&args)
|
||||
.map_err(|e| ServerFnError::Serialization(e.to_string()))?,
|
||||
),
|
||||
Encoding::Cbor => {
|
||||
let mut buffer: Vec<u8> = Vec::new();
|
||||
into_writer(&args, &mut buffer)
|
||||
.map_err(|e| ServerFnError::Serialization(e.to_string()))?;
|
||||
Payload::Binary(buffer)
|
||||
}
|
||||
};
|
||||
|
||||
let content_type_header = match &enc {
|
||||
Encoding::Url => "application/x-www-form-urlencoded",
|
||||
Encoding::Cbor => "application/cbor",
|
||||
};
|
||||
|
||||
let accept_header = match &enc {
|
||||
Encoding::Url => "application/x-www-form-urlencoded",
|
||||
Encoding::Cbor => "application/cbor",
|
||||
};
|
||||
|
||||
let resp = match args_encoded {
|
||||
Payload::Binary(b) => {
|
||||
let slice_ref: &[u8] = &b;
|
||||
let js_array = Uint8Array::from(slice_ref).buffer();
|
||||
gloo_net::http::Request::post(url)
|
||||
.header("Content-Type", content_type_header)
|
||||
.header("Accept", accept_header)
|
||||
.body(js_array)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| ServerFnError::Request(e.to_string()))?
|
||||
}
|
||||
Payload::Url(s) => gloo_net::http::Request::post(url)
|
||||
.header("Content-Type", content_type_header)
|
||||
.header("Accept", accept_header)
|
||||
.body(s)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| ServerFnError::Request(e.to_string()))?,
|
||||
};
|
||||
|
||||
// check for error status
|
||||
let status = resp.status();
|
||||
if (500..=599).contains(&status) {
|
||||
return Err(ServerFnError::ServerError(resp.status_text()));
|
||||
}
|
||||
|
||||
if enc == Encoding::Cbor {
|
||||
let binary = resp
|
||||
.binary()
|
||||
.await
|
||||
.map_err(|e| ServerFnError::Deserialization(e.to_string()))?;
|
||||
|
||||
ciborium::de::from_reader(binary.as_slice())
|
||||
.map_err(|e| ServerFnError::Deserialization(e.to_string()))
|
||||
} else {
|
||||
let text = resp
|
||||
.text()
|
||||
.await
|
||||
.map_err(|e| ServerFnError::Deserialization(e.to_string()))?;
|
||||
|
||||
let mut deserializer = JSONDeserializer::from_str(&text);
|
||||
T::deserialize(&mut deserializer)
|
||||
.map_err(|e| ServerFnError::Deserialization(e.to_string()))
|
||||
}
|
||||
}
|
20
server_fn_macro/Cargo.toml
Normal file
20
server_fn_macro/Cargo.toml
Normal file
|
@ -0,0 +1,20 @@
|
|||
[package]
|
||||
name = "server_fn_macro"
|
||||
version = { workspace = true }
|
||||
edition = "2021"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
repository = "https://github.com/leptos-rs/leptos"
|
||||
description = "RPC for any web framework."
|
||||
readme = "../README.md"
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_urlencoded = "0.7"
|
||||
serde_json = "1.0.89"
|
||||
quote = "1"
|
||||
syn = { version = "1", features = ["full", "parsing", "extra-traits"] }
|
||||
proc-macro2 = "1"
|
||||
proc-macro-error = "1"
|
||||
xxhash-rust = { version = "0.8.6", features = ["const_xxh64"] }
|
||||
const_format = "0.2.30"
|
|
@ -1,6 +1,12 @@
|
|||
use cfg_if::cfg_if;
|
||||
use leptos_server::Encoding;
|
||||
#![cfg_attr(not(feature = "stable"), feature(proc_macro_span))]
|
||||
#![forbid(unsafe_code)]
|
||||
#![deny(missing_docs)]
|
||||
//! Implementation of the server_fn macro.
|
||||
//!
|
||||
//! This crate contains the implementation of the server_fn macro. [server_macro_impl] can be used to implement custom versions of the macro for different frameworks that allow users to pass a custom context from the server to the server function.
|
||||
|
||||
use proc_macro2::{Literal, TokenStream as TokenStream2};
|
||||
use proc_macro_error::abort;
|
||||
use quote::quote;
|
||||
use syn::{
|
||||
parse::{Parse, ParseStream},
|
||||
|
@ -8,13 +14,22 @@ use syn::{
|
|||
*,
|
||||
};
|
||||
|
||||
fn fn_arg_is_cx(f: &syn::FnArg) -> bool {
|
||||
/// Discribes the custom context from the server that passed to the server function. Optionally, the first argument of a server function
|
||||
/// can be a custom context of this type. This context can be used to access the server's state within the server function.
|
||||
pub struct ServerContext {
|
||||
/// The type of the context.
|
||||
pub ty: Ident,
|
||||
/// The path to the context type. Used to reference the context type in the generated code.
|
||||
pub path: Path,
|
||||
}
|
||||
|
||||
fn fn_arg_is_cx(f: &syn::FnArg, server_context: &ServerContext) -> bool {
|
||||
if let FnArg::Typed(t) = f {
|
||||
if let Type::Path(path) = &*t.ty {
|
||||
path.path
|
||||
.segments
|
||||
.iter()
|
||||
.any(|segment| segment.ident == "Scope")
|
||||
.any(|segment| segment.ident == server_context.ty)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
|
@ -23,57 +38,81 @@ fn fn_arg_is_cx(f: &syn::FnArg) -> bool {
|
|||
}
|
||||
}
|
||||
|
||||
/// The implementation of the server_fn macro.
|
||||
/// To allow the macro to accept a custom context from the server, pass a custom server context to this function.
|
||||
/// **The Context comes from the server.** Optionally, the first argument of a server function
|
||||
/// can be a custom context. This context can be used to inject dependencies like the HTTP request
|
||||
/// or response or other server-only dependencies, but it does *not* have access to state that exists in the client.
|
||||
///
|
||||
/// The paths passed into this function are used in the generated code, so they must be in scope when the macro is called.
|
||||
///
|
||||
/// ```ignore
|
||||
/// #[proc_macro_attribute]
|
||||
/// pub fn server(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
|
||||
/// let server_context = Some(ServerContext {
|
||||
/// ty: syn::parse_quote!(MyContext),
|
||||
/// path: syn::parse_quote!(my_crate::prelude::MyContext),
|
||||
/// });
|
||||
/// match server_macro_impl(
|
||||
/// args.into(),
|
||||
/// s.into(),
|
||||
/// Some(server_context),
|
||||
/// Some(syn::parse_quote!(my_crate::exports::server_fn)),
|
||||
/// ) {
|
||||
/// Err(e) => e.to_compile_error().into(),
|
||||
/// Ok(s) => s.to_token_stream().into(),
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
pub fn server_macro_impl(
|
||||
args: proc_macro::TokenStream,
|
||||
s: TokenStream2,
|
||||
args: TokenStream2,
|
||||
body: TokenStream2,
|
||||
server_context: Option<ServerContext>,
|
||||
server_fn_path: Option<Path>,
|
||||
) -> Result<TokenStream2> {
|
||||
let ServerFnName {
|
||||
struct_name,
|
||||
prefix,
|
||||
encoding,
|
||||
..
|
||||
} = syn::parse::<ServerFnName>(args)?;
|
||||
} = syn::parse2::<ServerFnName>(args)?;
|
||||
let prefix = prefix.unwrap_or_else(|| Literal::string(""));
|
||||
let encoding = match encoding {
|
||||
Encoding::Cbor => quote! { ::leptos::leptos_server::Encoding::Cbor },
|
||||
Encoding::Url => quote! { ::leptos::leptos_server::Encoding::Url },
|
||||
};
|
||||
let encoding = quote!(#server_fn_path::#encoding);
|
||||
|
||||
let body = syn::parse::<ServerFnBody>(s.into())?;
|
||||
let body = syn::parse::<ServerFnBody>(body.into())?;
|
||||
let fn_name = &body.ident;
|
||||
let fn_name_as_str = body.ident.to_string();
|
||||
let vis = body.vis;
|
||||
let block = body.block;
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(all(not(feature = "stable"), debug_assertions))] {
|
||||
use proc_macro::Span;
|
||||
let span = Span::call_site();
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let url = format!("{}/{}", span.source_file().path().to_string_lossy(), fn_name_as_str).replace('/', "-");
|
||||
#[cfg(target_os = "windows")]
|
||||
let url = format!("{}\\{}", span.source_file().path().to_string_lossy(), fn_name_as_str).replace("\\", "-");
|
||||
} else {
|
||||
let url = fn_name_as_str;
|
||||
}
|
||||
}
|
||||
|
||||
let fields = body.inputs.iter().filter(|f| !fn_arg_is_cx(f)).map(|f| {
|
||||
let typed_arg = match f {
|
||||
FnArg::Receiver(_) => {
|
||||
abort!(f, "cannot use receiver types in server function macro")
|
||||
let fields = body
|
||||
.inputs
|
||||
.iter()
|
||||
.filter(|f| {
|
||||
if let Some(ctx) = &server_context {
|
||||
!fn_arg_is_cx(f, ctx)
|
||||
} else {
|
||||
true
|
||||
}
|
||||
FnArg::Typed(t) => t,
|
||||
};
|
||||
quote! { pub #typed_arg }
|
||||
});
|
||||
})
|
||||
.map(|f| {
|
||||
let typed_arg = match f {
|
||||
FnArg::Receiver(_) => {
|
||||
abort!(
|
||||
f,
|
||||
"cannot use receiver types in server function macro"
|
||||
)
|
||||
}
|
||||
FnArg::Typed(t) => t,
|
||||
};
|
||||
quote! { pub #typed_arg }
|
||||
});
|
||||
|
||||
let cx_arg = body.inputs.iter().next().and_then(|f| {
|
||||
if fn_arg_is_cx(f) {
|
||||
Some(f)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
server_context
|
||||
.as_ref()
|
||||
.and_then(|ctx| fn_arg_is_cx(f, ctx).then_some(f))
|
||||
});
|
||||
let cx_assign_statement = if let Some(FnArg::Typed(arg)) = cx_arg {
|
||||
if let Pat::Ident(id) = &*arg.pat {
|
||||
|
@ -100,7 +139,11 @@ pub fn server_macro_impl(
|
|||
}
|
||||
FnArg::Typed(t) => t,
|
||||
};
|
||||
let is_cx = fn_arg_is_cx(f);
|
||||
let is_cx = if let Some(ctx) = &server_context {
|
||||
!fn_arg_is_cx(f, ctx)
|
||||
} else {
|
||||
true
|
||||
};
|
||||
if is_cx {
|
||||
quote! {
|
||||
#[allow(unused)]
|
||||
|
@ -115,8 +158,12 @@ pub fn server_macro_impl(
|
|||
let field_names = body.inputs.iter().filter_map(|f| match f {
|
||||
FnArg::Receiver(_) => todo!(),
|
||||
FnArg::Typed(t) => {
|
||||
if fn_arg_is_cx(f) {
|
||||
None
|
||||
if let Some(ctx) = &server_context {
|
||||
if fn_arg_is_cx(f, ctx) {
|
||||
None
|
||||
} else {
|
||||
Some(&t.pat)
|
||||
}
|
||||
} else {
|
||||
Some(&t.pat)
|
||||
}
|
||||
|
@ -148,13 +195,24 @@ pub fn server_macro_impl(
|
|||
);
|
||||
};
|
||||
|
||||
let server_ctx_path = if let Some(ctx) = &server_context {
|
||||
let path = &ctx.path;
|
||||
quote!(#path)
|
||||
} else {
|
||||
quote!(())
|
||||
};
|
||||
|
||||
let server_fn_path = server_fn_path
|
||||
.map(|path| quote!(#path))
|
||||
.unwrap_or_else(|| quote! { server_fn });
|
||||
|
||||
Ok(quote::quote! {
|
||||
#[derive(Clone, Debug, ::serde::Serialize, ::serde::Deserialize)]
|
||||
pub struct #struct_name {
|
||||
#(#fields),*
|
||||
}
|
||||
|
||||
impl leptos::ServerFn for #struct_name {
|
||||
impl #server_fn_path::ServerFn<#server_ctx_path> for #struct_name {
|
||||
type Output = #output_ty;
|
||||
|
||||
fn prefix() -> &'static str {
|
||||
|
@ -162,22 +220,22 @@ pub fn server_macro_impl(
|
|||
}
|
||||
|
||||
fn url() -> &'static str {
|
||||
#url
|
||||
#server_fn_path::const_format::concatcp!(#fn_name_as_str, #server_fn_path::xxhash_rust::const_xxh64::xxh64(concat!(env!("CARGO_MANIFEST_DIR"), ":", file!(), ":", line!(), ":", column!()).as_bytes(), 0))
|
||||
}
|
||||
|
||||
fn encoding() -> ::leptos::leptos_server::Encoding {
|
||||
fn encoding() -> #server_fn_path::Encoding {
|
||||
#encoding
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
fn call_fn(self, cx: ::leptos::Scope) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<Self::Output, ::leptos::ServerFnError>>>> {
|
||||
fn call_fn(self, cx: #server_ctx_path) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<Self::Output, server_fn::ServerFnError>>>> {
|
||||
let #struct_name { #(#field_names),* } = self;
|
||||
#cx_assign_statement;
|
||||
Box::pin(async move { #fn_name( #cx_fn_arg #(#field_names_2),*).await })
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
fn call_fn_client(self, cx: ::leptos::Scope) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<Self::Output, ::leptos::ServerFnError>>>> {
|
||||
fn call_fn_client(self, cx: #server_ctx_path) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<Self::Output, server_fn::ServerFnError>>>> {
|
||||
let #struct_name { #(#field_names_3),* } = self;
|
||||
Box::pin(async move { #fn_name( #cx_fn_arg #(#field_names_4),*).await })
|
||||
}
|
||||
|
@ -187,21 +245,22 @@ pub fn server_macro_impl(
|
|||
#vis async fn #fn_name(#(#fn_args),*) #output_arrow #return_ty {
|
||||
#block
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
#vis async fn #fn_name(#(#fn_args_2),*) #output_arrow #return_ty {
|
||||
let prefix = #struct_name::prefix().to_string();
|
||||
let url = prefix + "/" + #struct_name::url();
|
||||
::leptos::leptos_server::call_server_fn(&url, #struct_name { #(#field_names_5),* }, #encoding).await
|
||||
#server_fn_path::call_server_fn(&url, #struct_name { #(#field_names_5),* }, #encoding).await
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub struct ServerFnName {
|
||||
struct ServerFnName {
|
||||
struct_name: Ident,
|
||||
_comma: Option<Token![,]>,
|
||||
prefix: Option<Literal>,
|
||||
_comma2: Option<Token![,]>,
|
||||
encoding: Encoding,
|
||||
encoding: Path,
|
||||
}
|
||||
|
||||
impl Parse for ServerFnName {
|
||||
|
@ -210,7 +269,14 @@ impl Parse for ServerFnName {
|
|||
let _comma = input.parse()?;
|
||||
let prefix = input.parse()?;
|
||||
let _comma2 = input.parse()?;
|
||||
let encoding = input.parse().unwrap_or(Encoding::Url);
|
||||
let encoding = input
|
||||
.parse::<Literal>()
|
||||
.map(|encoding| match encoding.to_string().as_str() {
|
||||
"\"Url\"" => syn::parse_quote!(Encoding::Url),
|
||||
"\"Cbor\"" => syn::parse_quote!(Encoding::Cbor),
|
||||
_ => abort!(encoding, "Encoding Not Found"),
|
||||
})
|
||||
.unwrap_or(syn::parse_quote!(Encoding::Url));
|
||||
|
||||
Ok(Self {
|
||||
struct_name,
|
||||
|
@ -222,7 +288,8 @@ impl Parse for ServerFnName {
|
|||
}
|
||||
}
|
||||
|
||||
pub struct ServerFnBody {
|
||||
#[allow(unused)]
|
||||
struct ServerFnBody {
|
||||
pub attrs: Vec<Attribute>,
|
||||
pub vis: syn::Visibility,
|
||||
pub async_token: Token![async],
|
Loading…
Reference in a new issue