mirror of
https://github.com/leptos-rs/leptos
synced 2024-09-21 15:01:55 +00:00
Merge pull request #107 from benwis/msgpack-encoding
Binary encoding as an option for server functions
This commit is contained in:
commit
db34565959
20 changed files with 643 additions and 81 deletions
|
@ -28,6 +28,7 @@ members = [
|
|||
"examples/router",
|
||||
"examples/todomvc",
|
||||
"examples/todo-app-sqlite",
|
||||
"examples/todo-app-cbor",
|
||||
"examples/view-tests",
|
||||
|
||||
# book
|
||||
|
|
48
examples/todo-app-cbor/Cargo.toml
Normal file
48
examples/todo-app-cbor/Cargo.toml
Normal 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"]]
|
21
examples/todo-app-cbor/LICENSE
Normal file
21
examples/todo-app-cbor/LICENSE
Normal 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.
|
21
examples/todo-app-cbor/README.md
Normal file
21
examples/todo-app-cbor/README.md
Normal 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!
|
BIN
examples/todo-app-cbor/Todos.db
Normal file
BIN
examples/todo-app-cbor/Todos.db
Normal file
Binary file not shown.
|
@ -0,0 +1,6 @@
|
|||
CREATE TABLE IF NOT EXISTS todos
|
||||
(
|
||||
id INTEGER NOT NULL PRIMARY KEY,
|
||||
title VARCHAR,
|
||||
completed BOOLEAN
|
||||
);
|
22
examples/todo-app-cbor/src/lib.rs
Normal file
22
examples/todo-app-cbor/src/lib.rs
Normal 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/> }
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
45
examples/todo-app-cbor/src/main.rs
Normal file
45
examples/todo-app-cbor/src/main.rs
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
212
examples/todo-app-cbor/src/todo.rs
Normal file
212
examples/todo-app-cbor/src/todo.rs
Normal 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>
|
||||
}
|
||||
}
|
3
examples/todo-app-cbor/style.css
Normal file
3
examples/todo-app-cbor/style.css
Normal file
|
@ -0,0 +1,3 @@
|
|||
.pending {
|
||||
color: purple;
|
||||
}
|
|
@ -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() {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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()),
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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)))
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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()))
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue