Merge pull request #107 from benwis/msgpack-encoding

Binary encoding as an option for server functions
This commit is contained in:
Greg Johnston 2022-11-25 22:44:53 -05:00 committed by GitHub
commit db34565959
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 643 additions and 81 deletions

View file

@ -28,6 +28,7 @@ members = [
"examples/router",
"examples/todomvc",
"examples/todo-app-sqlite",
"examples/todo-app-cbor",
"examples/view-tests",
# book

View file

@ -0,0 +1,48 @@
[package]
name = "todo-app-cbor"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
actix-files = { version = "0.6", optional = true }
actix-web = { version = "4", optional = true, features = ["openssl", "macros"] }
anyhow = "1"
broadcaster = "1"
console_log = "0.2"
console_error_panic_hook = "0.1"
serde = { version = "1", features = ["derive"] }
futures = "0.3"
cfg-if = "1"
leptos = { path = "../../../leptos/leptos", default-features = false, features = [
"serde",
] }
leptos_actix = { path = "../../../leptos/integrations/actix", optional = true }
leptos_meta = { path = "../../../leptos/meta", default-features = false }
leptos_router = { path = "../../../leptos/router", default-features = false }
log = "0.4"
simple_logger = "2"
gloo = { git = "https://github.com/rustwasm/gloo" }
sqlx = { version = "0.6", features = [
"runtime-tokio-rustls",
"sqlite",
], optional = true }
[features]
default = ["ssr"]
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
ssr = [
"dep:actix-files",
"dep:actix-web",
"dep:sqlx",
"leptos/ssr",
"leptos_actix",
"leptos_meta/ssr",
"leptos_router/ssr",
]
[package.metadata.cargo-all-features]
denylist = ["actix-files", "actix-web", "leptos_actix", "sqlx"]
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]]

View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 Greg Johnston
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -0,0 +1,21 @@
# Leptos Counter Isomorphic Example
This example demonstrates how to use a server functions and multi-actions to build a simple todo app.
## Server Side Rendering With Hydration
To run it as a server side app with hydration, first you should run
```bash
wasm-pack build --target=web --no-default-features --features=hydrate
```
to generate the Webassembly to provide hydration features for the server.
Then run the server with `cargo run` to serve the server side rendered HTML and the WASM bundle for hydration.
```bash
cargo run
```
> Note that if your hydration code changes, you will have to rerun the wasm-pack command above
> This should be temporary, and vastly improve once cargo-leptos becomes ready for prime time!

Binary file not shown.

View file

@ -0,0 +1,6 @@
CREATE TABLE IF NOT EXISTS todos
(
id INTEGER NOT NULL PRIMARY KEY,
title VARCHAR,
completed BOOLEAN
);

View file

@ -0,0 +1,22 @@
use cfg_if::cfg_if;
pub mod todo;
// Needs to be in lib.rs AFAIK because wasm-bindgen needs us to be compiling a lib. I may be wrong.
cfg_if! {
if #[cfg(feature = "hydrate")] {
use leptos::*;
use wasm_bindgen::prelude::wasm_bindgen;
use crate::todo::*;
#[wasm_bindgen]
pub fn hydrate() {
console_error_panic_hook::set_once();
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
leptos::hydrate(body().unwrap(), |cx| {
view! { cx, <TodoApp/> }
});
}
}
}

View file

@ -0,0 +1,45 @@
use cfg_if::cfg_if;
use leptos::*;
mod todo;
// boilerplate to run in different modes
cfg_if! {
// server-only stuff
if #[cfg(feature = "ssr")] {
use actix_files::{Files};
use actix_web::*;
use crate::todo::*;
#[get("/style.css")]
async fn css() -> impl Responder {
actix_files::NamedFile::open_async("./style.css").await
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
let mut conn = db().await.expect("couldn't connect to DB");
sqlx::migrate!()
.run(&mut conn)
.await
.expect("could not run SQLx migrations");
crate::todo::register_server_functions();
HttpServer::new(|| {
App::new()
.service(Files::new("/pkg", "./pkg"))
.service(css)
.route("/api/{tail:.*}", leptos_actix::handle_server_fns())
.route("/{tail:.*}", leptos_actix::render_app_to_stream("todo_app_cbor", |cx| view! { cx, <TodoApp/> }))
//.wrap(middleware::Compress::default())
})
.bind(("127.0.0.1", 8081))?
.run()
.await
}
} else {
fn main() {
// no client-side main function
}
}
}

View file

@ -0,0 +1,212 @@
use cfg_if::cfg_if;
use leptos::*;
use leptos_meta::*;
use leptos_router::*;
use serde::{Deserialize, Serialize};
cfg_if! {
if #[cfg(feature = "ssr")] {
use sqlx::{Connection, SqliteConnection};
pub async fn db() -> Result<SqliteConnection, ServerFnError> {
Ok(SqliteConnection::connect("sqlite:Todos.db").await.map_err(|e| ServerFnError::ServerError(e.to_string()))?)
}
pub fn register_server_functions() {
_ = GetTodos::register();
_ = AddTodo::register();
_ = DeleteTodo::register();
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow)]
pub struct Todo {
id: u16,
title: String,
completed: bool,
}
} else {
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Todo {
id: u16,
title: String,
completed: bool,
}
}
}
#[server(GetTodos, "/api", "Url")]
pub async fn get_todos(cx: Scope) -> Result<Vec<Todo>, ServerFnError> {
// this is just an example of how to access server context injected in the handlers
let req =
use_context::<actix_web::HttpRequest>(cx).expect("couldn't get HttpRequest from context");
println!("req.path = {:?}", req.path());
use futures::TryStreamExt;
let mut conn = db().await?;
let mut todos = Vec::new();
let mut rows = sqlx::query_as::<_, Todo>("SELECT * FROM todos").fetch(&mut conn);
while let Some(row) = rows
.try_next()
.await
.map_err(|e| ServerFnError::ServerError(e.to_string()))?
{
todos.push(row);
}
Ok(todos)
}
#[server(AddTodo, "/api", "Cbor")]
pub async fn add_todo(title: String) -> Result<(), ServerFnError> {
let mut conn = db().await?;
// fake API delay
std::thread::sleep(std::time::Duration::from_millis(1250));
sqlx::query("INSERT INTO todos (title, completed) VALUES ($1, false)")
.bind(title)
.execute(&mut conn)
.await
.map(|_| ())
.map_err(|e| ServerFnError::ServerError(e.to_string()))
}
#[server(DeleteTodo, "/api")]
pub async fn delete_todo(id: u16) -> Result<(), ServerFnError> {
let mut conn = db().await?;
sqlx::query("DELETE FROM todos WHERE id = $1")
.bind(id)
.execute(&mut conn)
.await
.map(|_| ())
.map_err(|e| ServerFnError::ServerError(e.to_string()))
}
#[component]
pub fn TodoApp(cx: Scope) -> Element {
view! {
cx,
<div>
<Stylesheet href="/style.css"/>
<Router>
<header>
<h1>"My Tasks"</h1>
</header>
<main>
<Routes>
<Route path="" element=|cx| view! {
cx,
<Todos/>
}/>
</Routes>
</main>
</Router>
</div>
}
}
#[component]
pub fn Todos(cx: Scope) -> Element {
let add_todo = create_server_multi_action::<AddTodo>(cx);
let delete_todo = create_server_action::<DeleteTodo>(cx);
let submissions = add_todo.submissions();
// track mutations that should lead us to refresh the list
let add_changed = add_todo.version;
let todo_deleted = delete_todo.version;
// list of todos is loaded from the server in reaction to changes
let todos = create_resource(
cx,
move || (add_changed(), todo_deleted()),
move |_| get_todos(cx),
);
view! {
cx,
<div>
<MultiActionForm action=add_todo>
<label>
"Add a Todo"
<input type="text" name="title"/>
</label>
<input type="submit" value="Add"/>
</MultiActionForm>
<div>
<Suspense fallback=view! {cx, <p>"Loading..."</p> }>
{
let delete_todo = delete_todo.clone();
move || {
let existing_todos = {
let delete_todo = delete_todo.clone();
move || {
todos
.read()
.map({
let delete_todo = delete_todo.clone();
move |todos| match todos {
Err(e) => {
vec![view! { cx, <pre class="error">"Server Error: " {e.to_string()}</pre>}]
}
Ok(todos) => {
if todos.is_empty() {
vec![view! { cx, <p>"No tasks were found."</p> }]
} else {
todos
.into_iter()
.map({
let delete_todo = delete_todo.clone();
move |todo| {
let delete_todo = delete_todo.clone();
view! {
cx,
<li>
{todo.title}
<ActionForm action=delete_todo.clone()>
<input type="hidden" name="id" value=todo.id/>
<input type="submit" value="X"/>
</ActionForm>
</li>
}
}
})
.collect::<Vec<_>>()
}
}
}
})
.unwrap_or_default()
}
};
let pending_todos = move || {
submissions
.get()
.into_iter()
.filter(|submission| submission.pending().get())
.map(|submission| {
view! {
cx,
<li class="pending">{move || submission.input.get().map(|data| data.title) }</li>
}
})
.collect::<Vec<_>>()
};
view! {
cx,
<ul>
<div>{existing_todos}</div>
<div>{pending_todos}</div>
</ul>
}
}
}
</Suspense>
</div>
</div>
}
}

View file

@ -0,0 +1,3 @@
.pending {
color: purple;
}

View file

@ -1,5 +1,4 @@
use cfg_if::cfg_if;
use leptos::*;
pub mod todo;
// Needs to be in lib.rs AFAIK because wasm-bindgen needs us to be compiling a lib. I may be wrong.
@ -7,6 +6,7 @@ cfg_if! {
if #[cfg(feature = "hydrate")] {
use wasm_bindgen::prelude::wasm_bindgen;
use crate::todo::*;
use leptos::*;
#[wasm_bindgen]
pub fn hydrate() {

View file

@ -13,9 +13,9 @@ cfg_if! {
}
pub fn register_server_functions() {
GetTodos::register();
AddTodo::register();
DeleteTodo::register();
_ = GetTodos::register();
_ = AddTodo::register();
_ = DeleteTodo::register();
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow)]
@ -65,14 +65,12 @@ pub async fn add_todo(title: String) -> Result<(), ServerFnError> {
// fake API delay
std::thread::sleep(std::time::Duration::from_millis(1250));
match sqlx::query("INSERT INTO todos (title, completed) VALUES ($1, false)")
sqlx::query("INSERT INTO todos (title, completed) VALUES ($1, false)")
.bind(title)
.execute(&mut conn)
.await
{
Ok(row) => Ok(()),
Err(e) => Err(ServerFnError::ServerError(e.to_string())),
}
.map(|_| ())
.map_err(|e| ServerFnError::ServerError(e.to_string()))
}
#[server(DeleteTodo, "/api")]
@ -168,7 +166,7 @@ pub fn Todos(cx: Scope) -> Element {
<li>
{todo.title}
<ActionForm action=delete_todo.clone()>
<input type="hidden" name="id" value={todo.id}/>
<input type="hidden" name="id" value=todo.id/>
<input type="submit" value="X"/>
</ActionForm>
</li>

View file

@ -1,4 +1,4 @@
use actix_web::*;
use actix_web::{web::Bytes, *};
use futures::StreamExt;
use leptos::*;
use leptos_meta::*;
@ -62,9 +62,9 @@ pub fn handle_server_fns() -> Route {
disposer.dispose();
runtime.dispose();
// if this is Accept: application/json then send a serialized JSON response
if let Some("application/json") = accept_header {
HttpResponse::Ok().body(serialized)
let mut res: HttpResponseBuilder;
if accept_header.is_some() {
res = HttpResponse::Ok()
}
// otherwise, it's probably a <form> submit or something: redirect back to the referrer
else {
@ -73,10 +73,23 @@ pub fn handle_server_fns() -> Route {
.get("Referer")
.and_then(|value| value.to_str().ok())
.unwrap_or("/");
HttpResponse::SeeOther()
.insert_header(("Location", referer))
.content_type("application/json")
.body(serialized)
res = HttpResponse::SeeOther();
res.insert_header(("Location", referer))
.content_type("application/json");
};
match serialized {
Payload::Binary(data) => {
res.content_type("application/cbor");
res.body(Bytes::from(data))
}
Payload::Url(data) => {
res.content_type("application/x-www-form-urlencoded");
res.body(data)
}
Payload::Json(data) => {
res.content_type("application/jsoon");
res.body(data)
}
}
}
Err(e) => HttpResponse::InternalServerError().body(e.to_string()),

View file

@ -1,5 +1,5 @@
use axum::{
body::{Body, Bytes, StreamBody},
body::{Body, BoxBody, Bytes, Full, HttpBody, StreamBody},
extract::Path,
http::{HeaderMap, HeaderValue, Request, StatusCode},
response::{IntoResponse, Response},
@ -70,8 +70,9 @@ pub async fn handle_server_fns(
// if this is Accept: application/json then send a serialized JSON response
let accept_header =
headers.get("Accept").and_then(|value| value.to_str().ok());
let mut res = Response::builder();
if let Some("application/json") = accept_header {
Response::builder().status(StatusCode::OK).body(serialized)
res = res.status(StatusCode::OK);
}
// otherwise, it's probably a <form> submit or something: redirect back to the referrer
else {
@ -79,21 +80,35 @@ pub async fn handle_server_fns(
.get("Referer")
.and_then(|value| value.to_str().ok())
.unwrap_or("/");
Response::builder()
res = res
.status(StatusCode::SEE_OTHER)
.header("Location", referer)
.header("Location", referer);
}
match serialized {
Payload::Binary(data) => res
.header("Content-Type", "application/cbor")
.body(Full::from(data)),
Payload::Url(data) => res
.header(
"Content-Type",
"application/x-www-form-urlencoded",
)
.body(Full::from(data)),
Payload::Json(data) => res
.header("Content-Type", "application/json")
.body(serialized)
.body(Full::from(data)),
}
}
Err(e) => Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.body(e.to_string()),
.body(Full::from(e.to_string())),
}
} else {
Response::builder()
.status(StatusCode::BAD_REQUEST)
.body("Could not find a server function at that route.".to_string())
.body(Full::from(
"Could not find a server function at that route.".to_string(),
))
}
.expect("could not build Response");
@ -235,7 +250,7 @@ pub fn render_app_to_stream(
let stream = futures::stream::once(async move { head.clone() })
.chain(rx)
.chain(futures::stream::once(async { tail.to_string() }))
.map(|html| Ok(Bytes::from(html)) as io::Result<Bytes>);
.map(|html| Ok(Bytes::from(html)));
StreamBody::new(Box::pin(stream) as PinnedHtmlStream)
}
})

View file

@ -20,6 +20,7 @@ syn-rsx = "0.9"
uuid = { version = "1", features = ["v4"] }
leptos_dom = { path = "../leptos_dom", version = "0.0.18" }
leptos_reactive = { path = "../leptos_reactive", version = "0.0.18" }
leptos_server = { path = "../leptos_server", version = "0.0.18" }
lazy_static = "1.4"
[dev-dependencies]

View file

@ -1,4 +1,5 @@
use cfg_if::cfg_if;
use leptos_server::Encoding;
use proc_macro2::{Literal, TokenStream as TokenStream2};
use quote::quote;
use syn::{
@ -26,9 +27,14 @@ pub fn server_macro_impl(args: proc_macro::TokenStream, s: TokenStream2) -> Resu
let ServerFnName {
struct_name,
prefix,
encoding,
..
} = syn::parse::<ServerFnName>(args)?;
let prefix = prefix.unwrap_or_else(|| Literal::string(""));
let encoding = match encoding {
Encoding::Cbor => quote! { ::leptos::Encoding::Cbor },
Encoding::Url => quote! { ::leptos::Encoding::Url },
};
let body = syn::parse::<ServerFnBody>(s.into())?;
let fn_name = &body.ident;
@ -135,6 +141,10 @@ pub fn server_macro_impl(args: proc_macro::TokenStream, s: TokenStream2) -> Resu
#url
}
fn encoding() -> ::leptos::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>>>> {
let #struct_name { #(#field_names),* } = self;
@ -157,7 +167,7 @@ pub fn server_macro_impl(args: proc_macro::TokenStream, s: TokenStream2) -> Resu
#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::call_server_fn(&url, #struct_name { #(#field_names_5),* }).await
::leptos::call_server_fn(&url, #struct_name { #(#field_names_5),* }, #encoding).await
}
})
}
@ -166,6 +176,8 @@ pub struct ServerFnName {
struct_name: Ident,
_comma: Option<Token![,]>,
prefix: Option<Literal>,
_comma2: Option<Token![,]>,
encoding: Encoding,
}
impl Parse for ServerFnName {
@ -173,11 +185,15 @@ impl Parse for ServerFnName {
let struct_name = input.parse()?;
let _comma = input.parse()?;
let prefix = input.parse()?;
let _comma2 = input.parse()?;
let encoding = input.parse().unwrap_or(Encoding::Url);
Ok(Self {
struct_name,
_comma,
prefix,
_comma2,
encoding,
})
}
}

View file

@ -30,6 +30,7 @@ web-sys = { version = "0.3", features = [
"Window",
] }
cfg-if = "1.0.0"
rmp-serde = "1.1.1"
[dev-dependencies]
tokio-test = "0.4"

View file

@ -15,7 +15,8 @@ pub enum SerializationError {
Deserialize(Rc<dyn std::error::Error>),
}
/// Describes an object that can be serialized to or from JSON.
/// Describes an object that can be serialized to or from a supported format
/// Currently those are JSON and Cbor
///
/// This is primarily used for serializing and deserializing [Resource](crate::Resource)s
/// so they can begin on the server and be resolved on the client, but can be used
@ -92,6 +93,7 @@ cfg_if! {
fn from_json(json: &str) -> Result<Self, SerializationError> {
serde_json::from_str(json).map_err(|e| SerializationError::Deserialize(Rc::new(e)))
}
}
}
}

View file

@ -18,36 +18,22 @@ log = "0.4"
serde = { version = "1", features = ["derive"] }
serde_urlencoded = "0.7"
thiserror = "1"
rmp-serde = "1.1.1"
serde_json = "1.0.89"
quote = "1"
syn = { version = "1", features = ["full", "parsing", "extra-traits"] }
proc-macro2 = "1.0.47"
ciborium = "0.2.0"
[dev-dependencies]
leptos_macro = { path = "../leptos_macro", default-features = false, version = "0.0" }
leptos = { path = "../leptos", default-features = false, version = "0.0" }
[features]
csr = [
"leptos_dom/csr",
"leptos_reactive/csr",
"leptos_macro/csr",
"leptos/csr",
]
hydrate = [
"leptos_dom/hydrate",
"leptos_reactive/hydrate",
"leptos_macro/hydrate",
"leptos/hydrate",
]
ssr = [
"leptos_dom/ssr",
"leptos_reactive/ssr",
"leptos_macro/ssr",
"leptos/csr",
]
stable = [
"leptos_dom/stable",
"leptos_reactive/stable",
"leptos_macro/stable",
"leptos/stable",
]
csr = ["leptos_dom/csr", "leptos_reactive/csr", "leptos_macro/csr", "leptos/csr"]
hydrate = ["leptos_dom/hydrate", "leptos_reactive/hydrate", "leptos_macro/hydrate", "leptos/hydrate"]
ssr = ["leptos_dom/ssr", "leptos_reactive/ssr", "leptos_macro/ssr", "leptos/csr"]
stable = ["leptos_dom/stable", "leptos_reactive/stable", "leptos_macro/stable", "leptos/stable"]
[package.metadata.cargo-all-features]
denylist = ["stable"]

View file

@ -69,8 +69,15 @@
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};
use std::{future::Future, pin::Pin, str::FromStr};
use syn::{
parse::{Parse, ParseStream},
parse_quote,
};
use thiserror::Error;
mod action;
@ -85,7 +92,7 @@ use std::{
};
#[cfg(any(feature = "ssr", doc))]
type ServerFnTraitObj = dyn Fn(Scope, &[u8]) -> Pin<Box<dyn Future<Output = Result<String, ServerFnError>>>>
type ServerFnTraitObj = dyn Fn(Scope, &[u8]) -> Pin<Box<dyn Future<Output = Result<Payload, ServerFnError>>>>
+ Send
+ Sync;
@ -94,6 +101,17 @@ 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),
}
/// 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`)
@ -145,6 +163,54 @@ pub fn server_fn_by_path(path: &str) -> Option<Arc<ServerFnTraitObj>> {
.and_then(|fns| fns.get(path).cloned())
}
/// 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"),
}
}
}
/// 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` is enabled.
///
@ -162,7 +228,7 @@ where
Self: Serialize + DeserializeOwned + Sized + 'static,
{
/// The return type of the function.
type Output: Serializable;
type Output: Serialize;
/// URL prefix that should be prepended by the client to the generated URL.
fn prefix() -> &'static str;
@ -170,6 +236,9 @@ where
/// 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(
@ -189,12 +258,20 @@ where
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 = serde_urlencoded::from_bytes::<Self>(data)
.map_err(|e| ServerFnError::Deserialization(e.to_string()));
let value = match Self::encoding() {
Encoding::Url => serde_urlencoded::from_bytes(data)
.map_err(|e| ServerFnError::Deserialization(e.to_string())),
Encoding::Cbor => {
println!("Deserialize Cbor!: {:x?}", &data);
ciborium::de::from_reader(data)
.map_err(|e| ServerFnError::Deserialization(e.to_string()))
}
};
Box::pin(async move {
let value = match value {
let value: Self = match value {
Ok(v) => v,
Err(e) => return Err(e),
};
@ -206,16 +283,26 @@ where
};
// serialize the output
let result = match result
.to_json()
.map_err(|e| ServerFnError::Serialization(e.to_string()))
{
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<String, ServerFnError>>>>
}) as Pin<Box<dyn Future<Output = Result<Payload, ServerFnError>>>>
});
// store it in the hashmap
@ -256,20 +343,70 @@ pub enum ServerFnError {
/// 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) -> Result<T, ServerFnError>
pub async fn call_server_fn<T>(
url: &str,
args: impl ServerFn,
enc: Encoding,
) -> Result<T, ServerFnError>
where
T: Serializable + Sized,
T: serde::Serialize + serde::de::DeserializeOwned + Sized,
{
let args_form_data = serde_urlencoded::to_string(&args)
.map_err(|e| ServerFnError::Serialization(e.to_string()))?;
use ciborium::{de::from_reader, ser::into_writer};
use leptos_dom::js_sys::Uint8Array;
//use leptos_dom::log;
use serde_json::Deserializer as JSONDeserializer;
let resp = gloo_net::http::Request::post(url)
.header("Content-Type", "application/x-www-form-urlencoded")
.header("Accept", "application/json")
.body(args_form_data)
.send()
.await
.map_err(|e| ServerFnError::Request(e.to_string()))?;
#[derive(Debug)]
enum Payload {
Binary(Vec<u8>),
Url(String),
}
// log!("ARGS TO ENCODE: {:#}", &args);
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)
}
};
//log!("ENCODED DATA: {:#?}", args_encoded);
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();
@ -277,10 +414,24 @@ where
return Err(ServerFnError::ServerError(resp.status_text()));
}
let text = resp
.text()
.await
.map_err(|e| ServerFnError::Deserialization(e.to_string()))?;
if enc == Encoding::Cbor {
//log!("FUNCTION RESPONSE CBOR");
let binary = resp
.binary()
.await
.map_err(|e| ServerFnError::Deserialization(e.to_string()))?;
//log!("REAL SERVER RESPONSE: {:#?}", &binary);
T::from_json(&text).map_err(|e| ServerFnError::Deserialization(e.to_string()))
ciborium::de::from_reader(binary.as_slice()).map_err(|e| {
//log!("Failed to DECODE: {}", &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()))
}
}