feat: make server functions framework agnostic (#596)

This commit is contained in:
ealmloff 2023-03-01 19:56:30 -06:00 committed by GitHub
parent f46084723a
commit 0c261c0fb0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 773 additions and 345 deletions

View file

@ -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" }

View file

@ -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 }

View file

@ -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::*;

View file

@ -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"] }

View file

@ -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(),
}

View file

@ -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",

View file

@ -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
View 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 = []

View 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"

View 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 its 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 clients perspective it involves an asynchronous
/// function call.
/// - **Server functions must return `Result<T, ServerFnError>`.** Even if the work being done
/// inside the function body cant 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
View file

@ -0,0 +1,432 @@
#![forbid(unsafe_code)]
#![deny(missing_docs)]
//! # Server Functions
//!
//! This package is based on a simple idea: sometimes its useful to write functions
//! that will only run on the server, and call them from the client.
//!
//! If youre creating anything beyond a toy app, youll 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 dont 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 thats stored on the server and definitely
//! shouldnt be shipped down to a users 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">`.
//!
//! Heres 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 clients perspective it involves an asynchronous
//! function call.
//! - **Server functions must return `Result<T, ServerFnError>`.** Even if the work being done
//! inside the function body cant 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()))
}
}

View 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"

View file

@ -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],