get rkyv working and work on custom encoding example

This commit is contained in:
Greg Johnston 2024-01-15 16:14:35 -05:00
parent 4366d786ac
commit 35e8e74dcf
13 changed files with 302 additions and 60 deletions

View file

@ -25,6 +25,8 @@ tower-http = { version = "0.5", features = ["fs"], optional = true }
tokio = { version = "1", features = ["full"], optional = true }
thiserror = "1.0"
wasm-bindgen = "0.2"
serde_toml = "0.0.1"
toml = "0.8.8"
[features]
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]

View file

@ -1,9 +1,18 @@
use crate::error_template::ErrorTemplate;
use http::{Request, Response};
use leptos::{html::Input, *};
use leptos_meta::*;
use leptos_meta::{Link, Stylesheet};
use leptos_router::*;
use serde::{Deserialize, Serialize};
use server_fn::codec::SerdeLite;
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use server_fn::{
codec::{
Encoding, FromReq, FromRes, GetUrl, IntoReq, IntoRes, Rkyv, SerdeLite,
},
error::NoCustomError,
request::{browser::BrowserRequest, BrowserMockReq, ClientReq, Req},
response::{browser::BrowserResponse, ClientRes, Res},
rkyv::AlignedVec,
};
#[cfg(feature = "ssr")]
use std::sync::{
atomic::{AtomicU8, Ordering},
@ -37,6 +46,10 @@ pub fn HomePage() -> impl IntoView {
<SpawnLocal/>
<WithAnAction/>
<WithActionForm/>
<h2>"Alternative Encodings"</h2>
<ServerFnArgumentExample/>
<RkyvExample/>
<CustomEncoding/>
}
}
@ -155,7 +168,15 @@ pub fn WithAnAction() -> impl IntoView {
<button
on:click=move |_| {
let text = input_ref.get().unwrap().value();
action.dispatch(AddRow { text });
action.dispatch(text);
// note: technically, this `action` takes `AddRow` (the server fn type) as its
// argument
//
// however, `.dispatch()` takes `impl Into<I>`, and for any one-argument server
// functions, `From<_>` is implemented between the server function type and the
// type of this single argument
//
// so `action.dispatch(text)` means `action.dispatch(AddRow { text })`
}
>
Submit
@ -195,8 +216,202 @@ pub fn WithActionForm() -> impl IntoView {
</ActionForm>
<p>You submitted: {move || format!("{:?}", action.input().get())}</p>
<p>The result was: {move || format!("{:?}", action.value().get())}</p>
<Transition>
<Transition>archive underaligned: need alignment 4 but have alignment 1
<p>Total rows: {row_count}</p>
</Transition>
}
}
/// The plain `#[server]` macro gives sensible defaults for the settings needed to create a server
/// function, but those settings can also be customized. For example, you can set a specific unique
/// path rather than the hashed path, or you can choose a different combination of input and output
/// encodings.
///
/// Arguments to the server macro can be specified as named key-value pairs, like `name = value`.
#[server(
// this server function will be exposed at /api2/custom_path
prefix = "/api2",
endpoint = "custom_path",
// it will take its arguments as a URL-encoded GET request (useful for caching)
input = GetUrl,
// it will return its output using SerdeLite
// (this needs to be enabled with the `serde-lite` feature on the `server_fn` crate
output = SerdeLite
)]
pub async fn length_of_input(input: String) -> Result<usize, ServerFnError> {
// insert a simulated wait
tokio::time::sleep(std::time::Duration::from_millis(250)).await;
Ok(input.len())
}
#[component]
pub fn ServerFnArgumentExample() -> impl IntoView {
let input_ref = NodeRef::<Input>::new();
let (result, set_result) = create_signal(0);
view! {
<h3>Custom arguments to the <code>#[server]</code> " macro"</h3>
<p>
</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 length = length_of_input(value).await.unwrap_or(0);
set_result(length);
});
}
>
Click to see length
</button>
<p>Length is {result}</p>
}
}
/// `server_fn` supports a wide variety of input and output encodings, each of which can be
/// referred to as a PascalCased struct name
/// - Toml
/// - Cbor
/// - Rkyv
/// - etc.
#[server(
input = Rkyv,
output = Rkyv
)]
pub async fn rkyv_example(input: String) -> Result<String, ServerFnError> {
// insert a simulated wait
tokio::time::sleep(std::time::Duration::from_millis(250)).await;
Ok(input.to_ascii_uppercase())
}
#[component]
pub fn RkyvExample() -> impl IntoView {
let input_ref = NodeRef::<Input>::new();
let (input, set_input) = create_signal(String::new());
let rkyv_result = create_resource(input, rkyv_example);
view! {
<h3>Using <code>rkyv</code> encoding</h3>
<p>
</p>
<input node_ref=input_ref placeholder="Type something here."/>
<button
on:click=move |_| {
let value = input_ref.get().unwrap().value();
set_input(value);
}
>
Click to see length
</button>
<p>{input}</p>
<Transition>
{rkyv_result}
</Transition>
}
}
/// 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!
struct Toml;
impl Encoding for Toml {
const CONTENT_TYPE: &'static str = "application/toml";
const METHOD: Method = Method::POST;
}
#[cfg(not(feature = "ssr"))]
type Request = BrowserMockReq;
#[cfg(feature = "ssr")]
type Request = http::Request<axum::body::Body>;
#[cfg(not(feature = "ssr"))]
type Response = BrowserMockRes;
#[cfg(feature = "ssr")]
type Response = http::Response<axum::body::Body>;
impl<T> IntoReq<Toml, BrowserRequest, NoCustomError> for T {
fn into_req(
self,
path: &str,
accepts: &str,
) -> Result<BrowserRequest, ServerFnError> {
let data = toml::to_string(&self)
.map_err(|e| ServerFnError::Serialization(e.to_string()))?;
Request::try_new_post(path, Toml::CONTENT_TYPE, accepts, data)
}
}
impl<T> FromReq<Toml, Request, NoCustomError> for T
where
T: DeserializeOwned,
{
async fn from_req(req: Request) -> Result<Self, ServerFnError> {
let string_data = req.try_into_string().await?;
toml::from_str::<Self>(&string_data)
.map_err(|e| ServerFnError::Args(e.to_string()))
}
}
impl<T> IntoRes<Toml, Response, NoCustomError> for T
where
T: Serialize + Send,
{
async fn into_res(self) -> Result<Response, ServerFnError> {
let data = toml::to_string(&self)
.map_err(|e| ServerFnError::Serialization(e.to_string()))?;
Response::try_from_string(Toml::CONTENT_TYPE, data)
}
}
impl<e> FromRes<Toml, BrowserResponse, NoCustomError> for T
where
T: DeserializeOwned + Send,
{
async fn from_res(res: BrowserResponse) -> Result<Self, ServerFnError> {
let data = res.try_into_string().await?;
toml::from_str(&data)
.map_err(|e| ServerFnError::Deserialization(e.to_string()))
}
}
#[server(
input = Toml,
output = Toml
)]
pub async fn why_not(
foo: String,
bar: String,
) -> Result<String, ServerFnError> {
// insert a simulated wait
tokio::time::sleep(std::time::Duration::from_millis(250)).await;
Ok(foo + &bar)
}
#[component]
pub fn CustomEncoding() -> impl IntoView {
let input_ref = NodeRef::<Input>::new();
let (result, set_result) = create_signal(0);
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());
set_result(new_value);
});
}
>
Submit
</button>
<p>{result}</p>
}
}

View file

@ -93,8 +93,8 @@ where
any(debug_assertions, feature = "ssr"),
tracing::instrument(level = "trace", skip_all,)
)]
pub fn dispatch(&self, input: I) {
self.0.with_value(|a| a.dispatch(input))
pub fn dispatch(&self, input: impl Into<I>) {
self.0.with_value(|a| a.dispatch(input.into()))
}
/// Create an [Action].

View file

@ -16,7 +16,7 @@ impl Encoding for Cbor {
const METHOD: Method = Method::POST;
}
impl<CustErr, T, Request> IntoReq<CustErr, Request, Cbor> for T
impl<CustErr, T, Request> IntoReq<Cbor, Request, CustErr> for T
where
Request: ClientReq<CustErr>,
T: Serialize + Send,
@ -38,7 +38,7 @@ where
}
}
impl<CustErr, T, Request> FromReq<CustErr, Request, Cbor> for T
impl<CustErr, T, Request> FromReq<Cbor, Request, CustErr> for T
where
Request: Req<CustErr> + Send + 'static,
T: DeserializeOwned,
@ -50,7 +50,7 @@ where
}
}
impl<CustErr, T, Response> IntoRes<CustErr, Response, Cbor> for T
impl<CustErr, T, Response> IntoRes<Cbor, Response, CustErr> for T
where
Response: Res<CustErr>,
T: Serialize + Send,
@ -63,7 +63,7 @@ where
}
}
impl<CustErr, T, Response> FromRes<CustErr, Response, Cbor> for T
impl<CustErr, T, Response> FromRes<Cbor, Response, CustErr> for T
where
Response: ClientRes<CustErr> + Send,
T: DeserializeOwned + Send,

View file

@ -15,7 +15,7 @@ impl Encoding for Json {
const METHOD: Method = Method::POST;
}
impl<CustErr, T, Request> IntoReq<CustErr, Request, Json> for T
impl<CustErr, T, Request> IntoReq<Json, Request, CustErr> for T
where
Request: ClientReq<CustErr>,
T: Serialize + Send,
@ -31,7 +31,7 @@ where
}
}
impl<CustErr, T, Request> FromReq<CustErr, Request, Json> for T
impl<CustErr, T, Request> FromReq<Json, Request, CustErr> for T
where
Request: Req<CustErr> + Send + 'static,
T: DeserializeOwned,
@ -43,7 +43,7 @@ where
}
}
impl<CustErr, T, Response> IntoRes<CustErr, Response, Json> for T
impl<CustErr, T, Response> IntoRes<Json, Response, CustErr> for T
where
Response: Res<CustErr>,
T: Serialize + Send,
@ -55,7 +55,7 @@ where
}
}
impl<CustErr, T, Response> FromRes<CustErr, Response, Json> for T
impl<CustErr, T, Response> FromRes<Json, Response, CustErr> for T
where
Response: ClientRes<CustErr> + Send,
T: DeserializeOwned + Send,

View file

@ -61,7 +61,7 @@ pub use stream::*;
/// For example, heres the implementation for [`Json`].
///
/// ```rust
/// impl<CustErr, T, Request> IntoReq<CustErr, Request, Json> for T
/// impl<CustErr, T, Request> IntoReq<Json, Request, CustErr> for T
/// where
/// Request: ClientReq<CustErr>,
/// T: Serialize + Send,
@ -79,7 +79,7 @@ pub use stream::*;
/// }
/// }
/// ```
pub trait IntoReq<CustErr, Request, Encoding> {
pub trait IntoReq<Encoding, Request, CustErr> {
/// Attempts to serialize the arguments into an HTTP request.
fn into_req(
self,
@ -99,7 +99,7 @@ pub trait IntoReq<CustErr, Request, Encoding> {
/// For example, heres the implementation for [`Json`].
///
/// ```rust
/// impl<CustErr, T, Request> FromReq<CustErr, Request, Json> for T
/// impl<CustErr, T, Request> FromReq<Json, Request, CustErr> for T
/// where
/// // require the Request implement `Req`
/// Request: Req<CustErr> + Send + 'static,
@ -117,7 +117,7 @@ pub trait IntoReq<CustErr, Request, Encoding> {
/// }
/// }
/// ```
pub trait FromReq<CustErr, Request, Encoding>
pub trait FromReq<Encoding, Request, CustErr>
where
Self: Sized,
{
@ -138,7 +138,7 @@ where
/// For example, heres the implementation for [`Json`].
///
/// ```rust
/// impl<CustErr, T, Response> IntoRes<CustErr, Response, Json> for T
/// impl<CustErr, T, Response> IntoRes<Json, Response, CustErr> for T
/// where
/// Response: Res<CustErr>,
/// T: Serialize + Send,
@ -152,7 +152,7 @@ where
/// }
/// }
/// ```
pub trait IntoRes<CustErr, Response, Encoding> {
pub trait IntoRes<Encoding, Response, CustErr> {
/// Attempts to serialize the output into an HTTP response.
fn into_res(
self,
@ -171,7 +171,7 @@ pub trait IntoRes<CustErr, Response, Encoding> {
/// For example, heres the implementation for [`Json`].
///
/// ```rust
/// impl<CustErr, T, Response> FromRes<CustErr, Response, Json> for T
/// impl<CustErr, T, Response> FromRes<Json, Response, CustErr> for T
/// where
/// Response: ClientRes<CustErr> + Send,
/// T: DeserializeOwned + Send,
@ -187,7 +187,7 @@ pub trait IntoRes<CustErr, Response, Encoding> {
/// }
/// }
/// ```
pub trait FromRes<CustErr, Response, Encoding>
pub trait FromRes<Encoding, Response, CustErr>
where
Self: Sized,
{

View file

@ -56,7 +56,7 @@ impl From<FormData> for MultipartData {
}
}
impl<CustErr, T, Request> IntoReq<CustErr, Request, MultipartFormData> for T
impl<CustErr, T, Request> IntoReq<MultipartFormData, Request, CustErr> for T
where
Request: ClientReq<CustErr, FormData = BrowserFormData>,
T: Into<MultipartData>,
@ -75,7 +75,7 @@ where
}
}
impl<CustErr, T, Request> FromReq<CustErr, Request, MultipartFormData> for T
impl<CustErr, T, Request> FromReq<MultipartFormData, Request, CustErr> for T
where
Request: Req<CustErr> + Send + 'static,
T: From<MultipartData>,

View file

@ -20,7 +20,7 @@ impl Encoding for Rkyv {
const METHOD: Method = Method::POST;
}
impl<CustErr, T, Request> IntoReq<CustErr, Request, Rkyv> for T
impl<CustErr, T, Request> IntoReq<Rkyv, Request, CustErr> for T
where
Request: ClientReq<CustErr>,
T: Serialize<AllocSerializer<1024>> + Send,
@ -40,7 +40,7 @@ where
}
}
impl<CustErr, T, Request> FromReq<CustErr, Request, Rkyv> for T
impl<CustErr, T, Request> FromReq<Rkyv, Request, CustErr> for T
where
Request: Req<CustErr> + Send + 'static,
T: Serialize<AllocSerializer<1024>> + Send,
@ -50,12 +50,12 @@ where
{
async fn from_req(req: Request) -> Result<Self, ServerFnError<CustErr>> {
let body_bytes = req.try_into_bytes().await?;
rkyv::from_bytes::<T>(&body_bytes)
rkyv::from_bytes::<T>(body_bytes.as_ref())
.map_err(|e| ServerFnError::Args(e.to_string()))
}
}
impl<CustErr, T, Response> IntoRes<CustErr, Response, Rkyv> for T
impl<CustErr, T, Response> IntoRes<Rkyv, Response, CustErr> for T
where
Response: Res<CustErr>,
T: Serialize<AllocSerializer<1024>> + Send,
@ -71,7 +71,7 @@ where
}
}
impl<CustErr, T, Response> FromRes<CustErr, Response, Rkyv> for T
impl<CustErr, T, Response> FromRes<Rkyv, Response, CustErr> for T
where
Response: ClientRes<CustErr> + Send,
T: Serialize<AllocSerializer<1024>> + Send,

View file

@ -15,7 +15,7 @@ impl Encoding for SerdeLite {
const METHOD: Method = Method::POST;
}
impl<CustErr, T, Request> IntoReq<CustErr, Request, SerdeLite> for T
impl<CustErr, T, Request> IntoReq<SerdeLite, Request, CustErr> for T
where
Request: ClientReq<CustErr>,
T: Serialize + Send,
@ -35,7 +35,7 @@ where
}
}
impl<CustErr, T, Request> FromReq<CustErr, Request, SerdeLite> for T
impl<CustErr, T, Request> FromReq<SerdeLite, Request, CustErr> for T
where
Request: Req<CustErr> + Send + 'static,
T: Deserialize,
@ -50,7 +50,7 @@ where
}
}
impl<CustErr, T, Response> IntoRes<CustErr, Response, SerdeLite> for T
impl<CustErr, T, Response> IntoRes<SerdeLite, Response, CustErr> for T
where
Response: Res<CustErr>,
T: Serialize + Send,
@ -66,7 +66,7 @@ where
}
}
impl<CustErr, T, Response> FromRes<CustErr, Response, SerdeLite> for T
impl<CustErr, T, Response> FromRes<SerdeLite, Response, CustErr> for T
where
Response: ClientRes<CustErr> + Send,
T: Deserialize + Send,

View file

@ -19,7 +19,7 @@ impl Encoding for Streaming {
const METHOD: Method = Method::POST;
}
/* impl<CustErr, T, Request> IntoReq<CustErr, Request, ByteStream> for T
/* impl<CustErr, T, Request> IntoReq<ByteStream, Request, CustErr> for T
where
Request: ClientReq<CustErr>,
T: Stream<Item = Bytes> + Send,
@ -29,7 +29,7 @@ where
}
} */
/* impl<CustErr, T, Request> FromReq<CustErr, Request, ByteStream> for T
/* impl<CustErr, T, Request> FromReq<ByteStream, Request, CustErr> for T
where
Request: Req<CustErr> + Send + 'static,
T: Stream<Item = Bytes> + Send,
@ -65,7 +65,7 @@ where
}
}
impl<CustErr, Response> IntoRes<CustErr, Response, Streaming>
impl<CustErr, Response> IntoRes<Streaming, Response, CustErr>
for ByteStream<CustErr>
where
Response: Res<CustErr>,
@ -76,7 +76,7 @@ where
}
}
impl<CustErr, Response> FromRes<CustErr, Response, Streaming> for ByteStream
impl<CustErr, Response> FromRes<Streaming, Response, CustErr> for ByteStream
where
Response: ClientRes<CustErr> + Send,
{
@ -122,7 +122,7 @@ where
}
}
impl<CustErr, Response> IntoRes<CustErr, Response, StreamingText>
impl<CustErr, Response> IntoRes<StreamingText, Response, CustErr>
for TextStream<CustErr>
where
Response: Res<CustErr>,
@ -136,7 +136,7 @@ where
}
}
impl<CustErr, Response> FromRes<CustErr, Response, StreamingText> for TextStream
impl<CustErr, Response> FromRes<StreamingText, Response, CustErr> for TextStream
where
Response: ClientRes<CustErr> + Send,
{

View file

@ -17,7 +17,7 @@ impl Encoding for GetUrl {
const METHOD: Method = Method::GET;
}
impl<CustErr, T, Request> IntoReq<CustErr, Request, GetUrl> for T
impl<CustErr, T, Request> IntoReq<GetUrl, Request, CustErr> for T
where
Request: ClientReq<CustErr>,
T: Serialize + Send,
@ -33,7 +33,7 @@ where
}
}
impl<CustErr, T, Request> FromReq<CustErr, Request, GetUrl> for T
impl<CustErr, T, Request> FromReq<GetUrl, Request, CustErr> for T
where
Request: Req<CustErr> + Send + 'static,
T: DeserializeOwned,
@ -51,7 +51,7 @@ impl Encoding for PostUrl {
const METHOD: Method = Method::POST;
}
impl<CustErr, T, Request> IntoReq<CustErr, Request, PostUrl> for T
impl<CustErr, T, Request> IntoReq<PostUrl, Request, CustErr> for T
where
Request: ClientReq<CustErr>,
T: Serialize + Send,
@ -67,7 +67,7 @@ where
}
}
impl<CustErr, T, Request> FromReq<CustErr, Request, PostUrl> for T
impl<CustErr, T, Request> FromReq<PostUrl, Request, CustErr> for T
where
Request: Req<CustErr> + Send + 'static,
T: DeserializeOwned,

View file

@ -132,6 +132,8 @@ use once_cell::sync::Lazy;
use redirect::RedirectHook;
use request::Req;
use response::{ClientRes, Res};
#[cfg(feature = "rkyv")]
pub use rkyv;
#[doc(hidden)]
pub use serde;
#[doc(hidden)]
@ -173,11 +175,11 @@ pub use xxhash_rust;
pub trait ServerFn
where
Self: Send
+ FromReq<Self::Error, Self::ServerRequest, Self::InputEncoding>
+ FromReq<Self::InputEncoding, Self::ServerRequest, Self::Error>
+ IntoReq<
Self::Error,
<Self::Client as Client<Self::Error>>::Request,
Self::InputEncoding,
<Self::Client as Client<Self::Error>>::Request,
Self::Error,
>,
{
/// A unique path for the server functions API endpoint, relative to the host, including its prefix.
@ -198,11 +200,11 @@ where
///
/// This needs to be converted into `ServerResponse` on the server side, and converted
/// *from* `ClientResponse` when received by the client.
type Output: IntoRes<Self::Error, Self::ServerResponse, Self::OutputEncoding>
type Output: IntoRes<Self::OutputEncoding, Self::ServerResponse, Self::Error>
+ FromRes<
Self::Error,
<Self::Client as Client<Self::Error>>::Response,
Self::OutputEncoding,
<Self::Client as Client<Self::Error>>::Response,
Self::Error,
> + Send;
/// The [`Encoding`] used in the request for arguments into the server function.

View file

@ -130,6 +130,12 @@ pub fn server_macro_impl(
}
FnArg::Typed(t) => t,
};
// strip `mut`, which is allowed in fn args but not in struct fields
if let Pat::Ident(ident) = &mut *typed_arg.pat {
ident.mutability = None;
}
// allow #[server(default)] on fields — TODO is this documented?
let mut default = false;
let mut other_attrs = Vec::new();
@ -332,27 +338,45 @@ pub fn server_macro_impl(
}
};
let (is_serde, derives) = match input_ident.as_deref() {
Some("Rkyv") => todo!("implement derives for Rkyv"),
Some("MultipartFormData") => (false, quote! {}),
enum PathInfo {
Serde,
Rkyv,
None,
}
let (path, derives) = match input_ident.as_deref() {
Some("Rkyv") => (
PathInfo::Rkyv,
quote! {
Clone, #server_fn_path::rkyv::Archive, #server_fn_path::rkyv::Serialize, #server_fn_path::rkyv::Deserialize
},
),
Some("MultipartFormData") => (PathInfo::None, quote! {}),
Some("SerdeLite") => (
true,
PathInfo::Serde,
quote! {
Clone, #server_fn_path::serde_lite::Serialize, #server_fn_path::serde_lite::Deserialize
},
),
_ => (
true,
PathInfo::Serde,
quote! {
Clone, #server_fn_path::serde::Serialize, #server_fn_path::serde::Deserialize
},
),
};
let serde_path = is_serde.then(|| {
quote! {
let addl_path = match path {
PathInfo::Serde => quote! {
#[serde(crate = #serde_path)]
},
PathInfo::Rkyv => {
let rkyv_path = format!("{server_fn_path}::rkyv");
quote! {
#[archive(crate = #rkyv_path, check_bytes)]
}
}
});
PathInfo::None => quote! {},
};
// TODO reqwest
let client = quote! {
@ -429,7 +453,6 @@ pub fn server_macro_impl(
} else {
#server_fn_path::const_format::concatcp!(
#prefix,
"/",
#fn_path
)
}
@ -453,7 +476,7 @@ pub fn server_macro_impl(
#args_docs
#docs
#[derive(Debug, #derives)]
#serde_path
#addl_path
pub struct #struct_name {
#(#fields),*
}