#![deny(missing_docs)]
#![forbid(unsafe_code)]
pub use form_urlencoded;
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;
mod action;
mod multi_action;
pub use action::*;
pub use multi_action::*;
#[cfg(any(feature = "ssr", doc))]
use std::{
collections::HashMap,
sync::{Arc, RwLock},
};
#[cfg(any(feature = "ssr", doc))]
type ServerFnTraitObj = dyn Fn(Scope, &[u8]) -> Pin<Box<dyn Future<Output = Result<Payload, ServerFnError>>>>
+ Send
+ Sync;
#[cfg(any(feature = "ssr", doc))]
lazy_static::lazy_static! {
static ref REGISTERED_SERVER_FUNCTIONS: Arc<RwLock<HashMap<&'static str, Arc<ServerFnTraitObj>>>> = Default::default();
}
#[derive(Debug)]
pub enum Payload {
Binary(Vec<u8>),
Url(String),
Json(String),
}
#[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())
}
#[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()
}
#[derive(Debug, PartialEq)]
pub enum Encoding {
Cbor,
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();
match variant_name.as_ref() {
"\"Url\"" => Ok(Self::Url),
"\"Cbor\"" => Ok(Self::Cbor),
_ => panic!("Encoding Not Found"),
}
}
}
pub trait ServerFn
where
Self: Serialize + DeserializeOwned + Sized + 'static,
{
type Output: Serialize;
fn prefix() -> &'static str;
fn url() -> &'static str;
fn encoding() -> Encoding;
#[cfg(any(feature = "ssr", doc))]
fn call_fn(
self,
cx: Scope,
) -> Pin<Box<dyn Future<Output = Result<Self::Output, ServerFnError>>>>;
#[cfg(any(not(feature = "ssr"), doc))]
fn call_fn_client(
self,
cx: Scope,
) -> Pin<Box<dyn Future<Output = Result<Self::Output, ServerFnError>>>>;
#[cfg(any(feature = "ssr", doc))]
fn register() -> Result<(), ServerFnError> {
let run_server_fn = Arc::new(|cx: Scope, data: &[u8]| {
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),
};
let result = match value.call_fn(cx).await {
Ok(r) => r,
Err(e) => return Err(e),
};
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>>>>
});
let mut write = REGISTERED_SERVER_FUNCTIONS
.write()
.map_err(|e| ServerFnError::Registration(e.to_string()))?;
let prev = write.insert(Self::url(), run_server_fn);
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(()),
}
}
}
#[derive(Error, Debug, Clone, Serialize, Deserialize)]
pub enum ServerFnError {
#[error("error while trying to register the server function: {0}")]
Registration(String),
#[error("error reaching server to call server function: {0}")]
Request(String),
#[error("error running server function: {0}")]
ServerError(String),
#[error("error deserializing server function results {0}")]
Deserialization(String),
#[error("error serializing server function results {0}")]
Serialization(String),
#[error("error deserializing server function arguments {0}")]
Args(String),
#[error("missing argument {0}")]
MissingArg(String),
}
#[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 leptos_dom::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()))?,
};
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()))
}
}