mirror of
https://github.com/leptos-rs/leptos
synced 2024-11-10 06:44:17 +00:00
Merge in changes from main
This commit is contained in:
parent
c481e465b0
commit
c4cc3e944b
10 changed files with 418 additions and 62 deletions
|
@ -1,4 +1,4 @@
|
|||
use actix_web::*;
|
||||
use actix_web::{web::Bytes, *};
|
||||
use futures::StreamExt;
|
||||
use leptos::*;
|
||||
use leptos_meta::*;
|
||||
|
@ -62,9 +62,12 @@ 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 == Some("application/json")
|
||||
|| accept_header == Some("application/x-www-form-urlencoded")
|
||||
|| accept_header == Some("application/cbor")
|
||||
{
|
||||
res = HttpResponse::Ok()
|
||||
}
|
||||
// otherwise, it's probably a <form> submit or something: redirect back to the referrer
|
||||
else {
|
||||
|
@ -73,10 +76,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/json");
|
||||
res.body(data)
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => HttpResponse::InternalServerError().body(e.to_string()),
|
||||
|
@ -103,32 +119,40 @@ pub fn handle_server_fns() -> Route {
|
|||
/// ```
|
||||
/// use actix_web::{HttpServer, App};
|
||||
/// use leptos::*;
|
||||
/// use std::{env,net::SocketAddr};
|
||||
///
|
||||
/// #[component]
|
||||
/// fn MyApp(cx: Scope) -> Element {
|
||||
/// fn MyApp(cx: Scope) -> impl IntoView {
|
||||
/// view! { cx, <main>"Hello, world!"</main> }
|
||||
/// }
|
||||
///
|
||||
/// # if false { // don't actually try to run a server in a doctest...
|
||||
/// #[actix_web::main]
|
||||
/// async fn main() -> std::io::Result<()> {
|
||||
/// HttpServer::new(|| {
|
||||
///
|
||||
/// let addr = SocketAddr::from(([127,0,0,1],3000));
|
||||
/// HttpServer::new(move || {
|
||||
/// let render_options: RenderOptions = RenderOptions::builder().pkg_path("/pkg/leptos_example").reload_port(3001).socket_address(addr.clone()).environment(&env::var("RUST_ENV")).build();
|
||||
/// render_options.write_to_file();
|
||||
/// App::new()
|
||||
/// // {tail:.*} passes the remainder of the URL as the route
|
||||
/// // the actual routing will be handled by `leptos_router`
|
||||
/// .route("/{tail:.*}", leptos_actix::render_app_to_stream("leptos_example", |cx| view! { cx, <MyApp/> }))
|
||||
/// .route("/{tail:.*}", leptos_actix::render_app_to_stream(render_options, |cx| view! { cx, <MyApp/> }))
|
||||
/// })
|
||||
/// .bind(("127.0.0.1", 8080))?
|
||||
/// .bind(&addr)?
|
||||
/// .run()
|
||||
/// .await
|
||||
/// }
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn render_app_to_stream(
|
||||
client_pkg_name: &'static str,
|
||||
app_fn: impl Fn(leptos::Scope) -> Element + Clone + 'static,
|
||||
) -> Route {
|
||||
pub fn render_app_to_stream<IV>(
|
||||
options: RenderOptions,
|
||||
app_fn: impl Fn(leptos::Scope) -> IV + Clone + 'static,
|
||||
) -> Route
|
||||
where IV: IntoView
|
||||
{
|
||||
web::get().to(move |req: HttpRequest| {
|
||||
let options = options.clone();
|
||||
let app_fn = app_fn.clone();
|
||||
async move {
|
||||
let path = req.path();
|
||||
|
@ -148,29 +172,60 @@ pub fn render_app_to_stream(
|
|||
provide_context(cx, MetaContext::new());
|
||||
provide_context(cx, req.clone());
|
||||
|
||||
(app_fn)(cx)
|
||||
(app_fn)(cx).into_view(cx)
|
||||
}
|
||||
};
|
||||
|
||||
let head = format!(r#"<!DOCTYPE html>
|
||||
<html>
|
||||
let pkg_path = &options.pkg_path;
|
||||
let socket_ip = &options.socket_address.ip().to_string();
|
||||
let reload_port = options.reload_port;
|
||||
|
||||
let leptos_autoreload = match options.environment {
|
||||
RustEnv::DEV => format!(
|
||||
r#"
|
||||
<script crossorigin="">(function () {{
|
||||
var ws = new WebSocket('ws://{socket_ip}:{reload_port}/autoreload');
|
||||
ws.onmessage = (ev) => {{
|
||||
console.log(`Reload message: `);
|
||||
if (ev.data === 'reload') window.location.reload();
|
||||
}};
|
||||
ws.onclose = () => console.warn('Autoreload stopped. Manual reload necessary.');
|
||||
}})()
|
||||
</script>
|
||||
"#
|
||||
),
|
||||
RustEnv::PROD => "".to_string(),
|
||||
};
|
||||
|
||||
let head = format!(
|
||||
r#"<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
<script type="module">import init, {{ hydrate }} from '/pkg/{client_pkg_name}.js'; init().then(hydrate);</script>"#);
|
||||
<link rel="modulepreload" href="{pkg_path}.js">
|
||||
<link rel="preload" href="{pkg_path}.wasm" as="fetch" type="application/wasm" crossorigin="">
|
||||
<script type="module">import init, {{ hydrate }} from '{pkg_path}.js'; init('{pkg_path}.wasm').then(hydrate);</script>
|
||||
{leptos_autoreload}
|
||||
"#
|
||||
);
|
||||
|
||||
let tail = "</body></html>";
|
||||
|
||||
HttpResponse::Ok().content_type("text/html").streaming(
|
||||
futures::stream::once(async move { head.clone() })
|
||||
// TODO this leaks a runtime once per invocation
|
||||
.chain(render_to_stream(move |cx| {
|
||||
let app = app(cx);
|
||||
let html_stream = futures::stream::once(async move { head.clone() })
|
||||
.chain(render_to_stream_with_prefix(
|
||||
app,
|
||||
|cx| {
|
||||
let head = use_context::<MetaContext>(cx)
|
||||
.map(|meta| meta.dehydrate())
|
||||
.unwrap_or_default();
|
||||
format!("{head}</head><body>{app}")
|
||||
}))
|
||||
.chain(futures::stream::once(async { tail.to_string() }))
|
||||
format!("{head}</head><body>").into()
|
||||
}
|
||||
))
|
||||
.chain(futures::stream::once(async { tail.to_string() }));
|
||||
|
||||
HttpResponse::Ok().content_type("text/html").streaming(
|
||||
html_stream
|
||||
.map(|html| Ok(web::Bytes::from(html)) as Result<web::Bytes>),
|
||||
)
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ description = "Leptos is a full-stack, isomorphic Rust web framework leveraging
|
|||
readme = "../README.md"
|
||||
|
||||
[dependencies]
|
||||
leptos_config = { path = "../leptos_config", default-features = false, version = "0.0.18" }
|
||||
leptos_core = { path = "../leptos_core", default-features = false, version = "0.0.18" }
|
||||
leptos_dom = { path = "../leptos_dom", default-features = false, version = "0.0.18" }
|
||||
leptos_macro = { path = "../leptos_macro", default-features = false, version = "0.0.18" }
|
||||
|
|
|
@ -126,6 +126,7 @@
|
|||
//! # }
|
||||
//! ```
|
||||
|
||||
pub use leptos_config::*;
|
||||
pub use leptos_core::*;
|
||||
pub use leptos_dom;
|
||||
pub use leptos_dom::wasm_bindgen::{JsCast, UnwrapThrowExt};
|
||||
|
|
11
leptos_config/Cargo.toml
Normal file
11
leptos_config/Cargo.toml
Normal file
|
@ -0,0 +1,11 @@
|
|||
[package]
|
||||
name = "leptos_config"
|
||||
version = "0.0.18"
|
||||
edition = "2021"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
repository = "https://github.com/gbj/leptos"
|
||||
description = "Configuraiton for the Leptos web framework."
|
||||
|
||||
[dependencies]
|
||||
typed-builder = "0.11.0"
|
110
leptos_config/src/lib.rs
Normal file
110
leptos_config/src/lib.rs
Normal file
|
@ -0,0 +1,110 @@
|
|||
use std::{env::VarError, net::SocketAddr, str::FromStr};
|
||||
use typed_builder::TypedBuilder;
|
||||
|
||||
/// This struct serves as a convenient place to store details used for rendering.
|
||||
/// It's serialized into a file in the root called `.leptos.kdl` for cargo-leptos
|
||||
/// to watch. It's also used in our actix and axum integrations to generate the
|
||||
/// correct path for WASM, JS, and Websockets. Its goal is to be the single source
|
||||
/// of truth for render options
|
||||
#[derive(TypedBuilder, Clone)]
|
||||
pub struct RenderOptions {
|
||||
/// The path and name of the WASM and JS files generated by wasm-bindgen
|
||||
/// For example, `/pkg/app` might be a valid input if your crate name was `app`.
|
||||
#[builder(setter(into))]
|
||||
pub pkg_path: String,
|
||||
/// Used to control whether the Websocket code for code watching is included.
|
||||
/// I recommend passing in the result of `env::var("RUST_ENV")`
|
||||
#[builder(setter(into), default)]
|
||||
pub environment: RustEnv,
|
||||
/// Provides a way to control the address leptos is served from.
|
||||
/// Using an env variable here would allow you to run the same code in dev and prod
|
||||
/// Defaults to `127.0.0.1:3000`
|
||||
#[builder(setter(into), default=SocketAddr::from(([127,0,0,1], 3000)))]
|
||||
pub socket_address: SocketAddr,
|
||||
/// The port the Websocket watcher listens on. Should match the `reload_port` in cargo-leptos(if using).
|
||||
/// Defaults to `3001`
|
||||
#[builder(default = 3001)]
|
||||
pub reload_port: u32,
|
||||
}
|
||||
|
||||
impl RenderOptions {
|
||||
/// Creates a hidden file at ./.leptos_toml so cargo-leptos can monitor settings. We do not read from this file
|
||||
/// only write to it, you'll want to change the settings in your main function when you create RenderOptions
|
||||
pub fn write_to_file(&self) {
|
||||
use std::fs;
|
||||
let options = format!(
|
||||
r#"// This file is auto-generated. Changing it will have no effect on leptos. Change these by changing RenderOptions and rerunning
|
||||
RenderOptions {{
|
||||
pkg-path "{}"
|
||||
environment "{:?}"
|
||||
socket-address "{:?}"
|
||||
reload-port {:?}
|
||||
}}
|
||||
"#,
|
||||
self.pkg_path, self.environment, self.socket_address, self.reload_port
|
||||
);
|
||||
fs::write("./.leptos.kdl", options).expect("Unable to write file");
|
||||
}
|
||||
}
|
||||
/// An enum that can be used to define the environment Leptos is running in. Can be passed to RenderOptions.
|
||||
/// Setting this to the PROD variant will not include the websockets code for cargo-leptos' watch.
|
||||
/// Defaults to PROD
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum RustEnv {
|
||||
PROD,
|
||||
DEV,
|
||||
}
|
||||
|
||||
impl Default for RustEnv {
|
||||
fn default() -> Self {
|
||||
Self::PROD
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for RustEnv {
|
||||
type Err = ();
|
||||
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||
let sanitized = input.to_lowercase();
|
||||
match sanitized.as_ref() {
|
||||
"dev" => Ok(Self::DEV),
|
||||
"development" => Ok(Self::DEV),
|
||||
"prod" => Ok(Self::PROD),
|
||||
"production" => Ok(Self::PROD),
|
||||
_ => Ok(Self::PROD),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for RustEnv {
|
||||
fn from(str: &str) -> Self {
|
||||
let sanitized = str.to_lowercase();
|
||||
match sanitized.as_str() {
|
||||
"dev" => Self::DEV,
|
||||
"development" => Self::DEV,
|
||||
"prod" => Self::PROD,
|
||||
"production" => Self::PROD,
|
||||
_ => {
|
||||
panic!("Environment var is not recognized. Maybe try `dev` or `prod`")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
impl From<&Result<String, VarError>> for RustEnv {
|
||||
fn from(input: &Result<String, VarError>) -> Self {
|
||||
match input {
|
||||
Ok(str) => {
|
||||
let sanitized = str.to_lowercase();
|
||||
match sanitized.as_ref() {
|
||||
"dev" => Self::DEV,
|
||||
"development" => Self::DEV,
|
||||
"prod" => Self::PROD,
|
||||
"production" => Self::PROD,
|
||||
_ => {
|
||||
panic!("Environment var is not recognized. Maybe try `dev` or `prod`")
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_) => Self::PROD,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -10,6 +10,7 @@ description = "DOM operations for the Leptos web framework."
|
|||
[dependencies]
|
||||
cfg-if = "1"
|
||||
educe = "0.4"
|
||||
futures = "0.3"
|
||||
gloo = "0.8"
|
||||
html-escape = "0.2"
|
||||
indexmap = "1.9"
|
||||
|
@ -19,6 +20,7 @@ leptos_reactive = { path = "../leptos_reactive", default-features = false, versi
|
|||
pad-adapter = "0.1"
|
||||
paste = "1"
|
||||
rustc-hash = "1.1.0"
|
||||
serde_json = "1"
|
||||
smallvec = "1"
|
||||
tracing = "0.1"
|
||||
typed-builder = "0.11"
|
||||
|
@ -66,5 +68,6 @@ features = [
|
|||
|
||||
[features]
|
||||
default = ["web"]
|
||||
web = []
|
||||
web = ["leptos_reactive/csr", "leptos/csr"]
|
||||
ssr = ["leptos_reactive/ssr", "leptos/ssr"]
|
||||
stable = ["leptos_reactive/stable"]
|
||||
|
|
|
@ -21,6 +21,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;
|
||||
|
@ -40,7 +46,10 @@ pub fn server_macro_impl(args: proc_macro::TokenStream, s: TokenStream2) -> Resu
|
|||
if #[cfg(not(feature = "stable"))] {
|
||||
use proc_macro::Span;
|
||||
let span = Span::call_site();
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let url = format!("{}/{}", span.source_file().path().to_string_lossy(), fn_name_as_str).replace("/", "-");
|
||||
#[cfg(target_os = "windows")]
|
||||
let url = format!("{}/{}", span.source_file().path().to_string_lossy(), fn_name_as_str).replace("\\", "-");
|
||||
} else {
|
||||
let url = fn_name_as_str;
|
||||
}
|
||||
|
@ -135,6 +144,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 +170,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 +179,8 @@ pub struct ServerFnName {
|
|||
struct_name: Ident,
|
||||
_comma: Option<Token![,]>,
|
||||
prefix: Option<Literal>,
|
||||
_comma2: Option<Token![,]>,
|
||||
encoding: Encoding,
|
||||
}
|
||||
|
||||
impl Parse for ServerFnName {
|
||||
|
@ -173,11 +188,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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,6 +18,12 @@ 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" }
|
||||
|
|
|
@ -27,8 +27,9 @@
|
|||
//!
|
||||
//! ### `#[server]`
|
||||
//!
|
||||
//! The `#[server]` macro allows you to annotate a function to indicate that it should only run
|
||||
//! on the server (i.e., when you have an `ssr` feature in your crate that is enabled).
|
||||
//! The [`#[server]` macro](leptos::leptos_macro::server) allows you to annotate a function to
|
||||
//! indicate that it should only run on the server (i.e., when you have an `ssr` feature in your
|
||||
//! crate that is enabled).
|
||||
//!
|
||||
//! ```rust,ignore
|
||||
//! # use leptos_reactive::*;
|
||||
|
@ -62,15 +63,23 @@
|
|||
//! This should be fairly obvious: we have to serialize arguments to send them to the server, and we
|
||||
//! need to deserialize the result to return it to the client.
|
||||
//! - **Arguments must be implement [serde::Serialize].** They are serialized as an `application/x-www-form-urlencoded`
|
||||
//! form data using [`serde_urlencoded`](https://docs.rs/serde_urlencoded/latest/serde_urlencoded/).
|
||||
//! form data using [`serde_urlencoded`](https://docs.rs/serde_urlencoded/latest/serde_urlencoded/) or as `application/cbor`
|
||||
//! using [`cbor`](https://docs.rs/cbor/latest/cbor/).
|
||||
//! - **The [Scope](leptos_reactive::Scope) comes from the server.** Optionally, the first argument of a server function
|
||||
//! can be a Leptos [Scope](leptos_reactive::Scope). This scope can be used to inject dependencies like the HTTP request
|
||||
//! or response or other server-only dependencies, but it does *not* have access to reactive state that exists in the client.
|
||||
|
||||
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 +94,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 +103,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 +165,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 +230,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 +238,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 +260,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 +285,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 +345,69 @@ 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::ser::into_writer;
|
||||
use leptos_dom::js_sys::Uint8Array;
|
||||
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 +415,21 @@ 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 {
|
||||
let binary = resp
|
||||
.binary()
|
||||
.await
|
||||
.map_err(|e| ServerFnError::Deserialization(e.to_string()))?;
|
||||
|
||||
T::from_json(&text).map_err(|e| ServerFnError::Deserialization(e.to_string()))
|
||||
ciborium::de::from_reader(binary.as_slice())
|
||||
.map_err(|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