mirror of
https://github.com/leptos-rs/leptos
synced 2024-11-10 06:44:17 +00:00
This commit is contained in:
parent
7d1ce45a57
commit
fce2c727ab
2 changed files with 174 additions and 13 deletions
|
@ -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>
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in a new issue