feat: add support for custom encodings to `#[server]` macro (#2216) (closes #2210)

This commit is contained in:
Greg Johnston 2024-01-21 16:14:02 -05:00 committed by GitHub
parent 7d1ce45a57
commit fce2c727ab
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 174 additions and 13 deletions

View File

@ -1,10 +1,16 @@
use futures::StreamExt;
use http::Method;
use leptos::{html::Input, *};
use leptos_meta::{provide_meta_context, Link, Meta, Stylesheet};
use leptos_router::{ActionForm, Route, Router, Routes};
use server_fn::codec::{
GetUrl, MultipartData, MultipartFormData, Rkyv, SerdeLite, StreamingText,
TextStream,
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use server_fn::{
codec::{
Encoding, FromReq, FromRes, GetUrl, IntoReq, IntoRes, MultipartData,
MultipartFormData, Rkyv, SerdeLite, StreamingText, TextStream,
},
request::{ClientReq, Req},
response::{ClientRes, Res},
};
#[cfg(feature = "ssr")]
use std::sync::{
@ -50,6 +56,7 @@ pub fn HomePage() -> impl IntoView {
<RkyvExample/>
<FileUpload/>
<FileWatcher/>
<CustomEncoding/>
}
}
@ -503,3 +510,125 @@ pub fn CustomErrorTypes() -> impl IntoView {
</p>
}
}
/// Server function encodings are just types that implement a few traits.
/// This means that you can implement your own encodings, by implementing those traits!
///
/// Here, we'll create a custom encoding that serializes and deserializes the server fn
/// using TOML. Why would you ever want to do this? I don't know, but you can!
pub struct Toml;
/// A newtype wrapper around server fn data that will be TOML-encoded.
///
/// This is needed because of Rust rules around implementing foreign traits for foreign types.
/// It will be fed into the `custom = ` argument to the server fn below.
#[derive(Serialize, Deserialize)]
pub struct TomlEncoded<T>(T);
impl Encoding for Toml {
const CONTENT_TYPE: &'static str = "application/toml";
const METHOD: Method = Method::POST;
}
impl<T, Request, Err> IntoReq<Toml, Request, Err> for TomlEncoded<T>
where
Request: ClientReq<Err>,
T: Serialize,
{
fn into_req(
self,
path: &str,
accepts: &str,
) -> Result<Request, ServerFnError<Err>> {
let data = toml::to_string(&self.0)
.map_err(|e| ServerFnError::Serialization(e.to_string()))?;
Request::try_new_post(path, Toml::CONTENT_TYPE, accepts, data)
}
}
impl<T, Request, Err> FromReq<Toml, Request, Err> for TomlEncoded<T>
where
Request: Req<Err> + Send,
T: DeserializeOwned,
{
async fn from_req(req: Request) -> Result<Self, ServerFnError<Err>> {
let string_data = req.try_into_string().await?;
toml::from_str::<T>(&string_data)
.map(TomlEncoded)
.map_err(|e| ServerFnError::Args(e.to_string()))
}
}
impl<T, Response, Err> IntoRes<Toml, Response, Err> for TomlEncoded<T>
where
Response: Res<Err>,
T: Serialize + Send,
{
async fn into_res(self) -> Result<Response, ServerFnError<Err>> {
let data = toml::to_string(&self.0)
.map_err(|e| ServerFnError::Serialization(e.to_string()))?;
Response::try_from_string(Toml::CONTENT_TYPE, data)
}
}
impl<T, Response, Err> FromRes<Toml, Response, Err> for TomlEncoded<T>
where
Response: ClientRes<Err> + Send,
T: DeserializeOwned,
{
async fn from_res(res: Response) -> Result<Self, ServerFnError<Err>> {
let data = res.try_into_string().await?;
toml::from_str(&data)
.map(TomlEncoded)
.map_err(|e| ServerFnError::Deserialization(e.to_string()))
}
}
#[derive(Serialize, Deserialize)]
pub struct WhyNotResult {
original: String,
modified: String,
}
#[server(
input = Toml,
output = Toml,
custom = TomlEncoded
)]
pub async fn why_not(
original: String,
addition: String,
) -> Result<TomlEncoded<WhyNotResult>, ServerFnError> {
// insert a simulated wait
tokio::time::sleep(std::time::Duration::from_millis(250)).await;
Ok(TomlEncoded(WhyNotResult {
modified: format!("{original}{addition}"),
original,
}))
}
#[component]
pub fn CustomEncoding() -> impl IntoView {
let input_ref = NodeRef::<Input>::new();
let (result, set_result) = create_signal("foo".to_string());
view! {
<h3>Custom encodings</h3>
<p>
"This example creates a custom encoding that sends server fn data using TOML. Why? Well... why not?"
</p>
<input node_ref=input_ref placeholder="Type something here."/>
<button
on:click=move |_| {
let value = input_ref.get().unwrap().value();
spawn_local(async move {
let new_value = why_not(value, ", but in TOML!!!".to_string()).await.unwrap();
set_result(new_value.0.modified);
});
}
>
Submit
</button>
<p>{result}</p>
}
}

View File

@ -72,7 +72,6 @@ pub fn server_macro_impl(
client,
custom_wrapper,
} = args;
_ = custom_wrapper; // TODO: this should be used to enable custom encodings
let prefix = prefix.unwrap_or_else(|| Literal::string(default_path));
let fn_path = fn_path.unwrap_or_else(|| Literal::string(""));
let input_ident = match &input {
@ -117,6 +116,19 @@ pub fn server_macro_impl(
Ident::new(&upper_camel_case_name, body.ident.span())
});
// struct name, wrapped in any custom-encoding newtype wrapper
let wrapped_struct_name = if let Some(wrapper) = custom_wrapper.as_ref() {
quote! { #wrapper<#struct_name> }
} else {
quote! { #struct_name }
};
let wrapped_struct_name_turbofish =
if let Some(wrapper) = custom_wrapper.as_ref() {
quote! { #wrapper::<#struct_name> }
} else {
quote! { #struct_name }
};
// build struct for type
let mut body = body;
let fn_name = &body.ident;
@ -268,12 +280,12 @@ pub fn server_macro_impl(
#server_fn_path::inventory::submit! {{
use #server_fn_path::{ServerFn, codec::Encoding};
#server_fn_path::ServerFnTraitObj::new(
#struct_name::PATH,
<#struct_name as ServerFn>::InputEncoding::METHOD,
#wrapped_struct_name_turbofish::PATH,
<#wrapped_struct_name as ServerFn>::InputEncoding::METHOD,
|req| {
Box::pin(#struct_name::run_on_server(req))
Box::pin(#wrapped_struct_name_turbofish::run_on_server(req))
},
#struct_name::middlewares
#wrapped_struct_name_turbofish::middlewares
)
}}
}
@ -283,6 +295,16 @@ pub fn server_macro_impl(
// run_body in the trait implementation
let run_body = if cfg!(feature = "ssr") {
let destructure = if let Some(wrapper) = custom_wrapper.as_ref() {
quote! {
let #wrapper(#struct_name { #(#field_names),* }) = self;
}
} else {
quote! {
let #struct_name { #(#field_names),* } = self;
}
};
// using the impl Future syntax here is thanks to Actix
//
// if we use Actix types inside the function, here, it becomes !Send
@ -292,7 +314,7 @@ pub fn server_macro_impl(
//
// however, SendWrapper<Future<Output = T>> impls Future<Output = T>
let body = quote! {
let #struct_name { #(#field_names),* } = self;
#destructure
#dummy_name(#(#field_names),*).await
};
let body = if cfg!(feature = "actix") {
@ -333,13 +355,23 @@ pub fn server_macro_impl(
}
}
} else {
let restructure = if let Some(custom_wrapper) = custom_wrapper.as_ref()
{
quote! {
let data = #custom_wrapper(#struct_name { #(#field_names),* });
}
} else {
quote! {
let data = #struct_name { #(#field_names),* };
}
};
quote! {
#docs
#(#attrs)*
#[allow(unused_variables)]
#vis async fn #fn_name(#(#fn_args),*) #output_arrow #return_ty {
use #server_fn_path::ServerFn;
let data = #struct_name { #(#field_names),* };
#restructure
data.run_on_client().await
}
}
@ -509,7 +541,7 @@ pub fn server_macro_impl(
#from_impl
impl #server_fn_path::ServerFn for #struct_name {
impl #server_fn_path::ServerFn for #wrapped_struct_name {
const PATH: &'static str = #path;
type Client = #client;
@ -641,7 +673,7 @@ struct ServerFnArgs {
req_ty: Option<Type>,
res_ty: Option<Type>,
client: Option<Type>,
custom_wrapper: Option<Type>,
custom_wrapper: Option<Path>,
builtin_encoding: bool,
}
@ -659,7 +691,7 @@ impl Parse for ServerFnArgs {
let mut req_ty: Option<Type> = None;
let mut res_ty: Option<Type> = None;
let mut client: Option<Type> = None;
let mut custom_wrapper: Option<Type> = None;
let mut custom_wrapper: Option<Path> = None;
let mut use_key_and_value = false;
let mut arg_pos = 0;