Compare commits

..

2 Commits

Author SHA1 Message Date
Greg Johnston
8f8b6dc440 cargo fmt 2023-05-03 08:49:12 -04:00
Greg Johnston
af561abdf8 fix: suppress warning when loading local resource not under <Suspense/> in hydrate mode 2023-05-03 08:48:20 -04:00
22 changed files with 152 additions and 343 deletions

View File

@@ -1,5 +1,3 @@
extend = [{ path = "../cargo-make/common.toml" }]
[tasks.build]
command = "cargo"
args = ["+nightly", "build-all-features"]

View File

@@ -1,27 +1,27 @@
use cfg_if::cfg_if;
use leptos::*;
use leptos_meta::*;
use leptos_router::*;
use leptos_meta::*;
cfg_if! {
if #[cfg(feature = "ssr")] {
use std::sync::atomic::{AtomicI32, Ordering};
use broadcaster::BroadcastChannel;
static COUNT: AtomicI32 = AtomicI32::new(0);
#[cfg(feature = "ssr")]
use std::sync::atomic::{AtomicI32, Ordering};
lazy_static::lazy_static! {
pub static ref COUNT_CHANNEL: BroadcastChannel<i32> = BroadcastChannel::new();
}
#[cfg(feature = "ssr")]
use broadcaster::BroadcastChannel;
pub fn register_server_functions() {
_ = GetServerCount::register();
_ = AdjustServerCount::register();
_ = ClearServerCount::register();
}
}
#[cfg(feature = "ssr")]
pub fn register_server_functions() {
_ = GetServerCount::register();
_ = AdjustServerCount::register();
_ = ClearServerCount::register();
}
#[cfg(feature = "ssr")]
static COUNT: AtomicI32 = AtomicI32::new(0);
#[cfg(feature = "ssr")]
lazy_static::lazy_static! {
pub static ref COUNT_CHANNEL: BroadcastChannel<i32> = BroadcastChannel::new();
}
// "/api" is an optional prefix that allows you to locate server functions wherever you'd like on the server
#[server(GetServerCount, "/api")]
pub async fn get_server_count() -> Result<i32, ServerFnError> {
@@ -29,10 +29,7 @@ pub async fn get_server_count() -> Result<i32, ServerFnError> {
}
#[server(AdjustServerCount, "/api")]
pub async fn adjust_server_count(
delta: i32,
msg: String,
) -> Result<i32, ServerFnError> {
pub async fn adjust_server_count(delta: i32, msg: String) -> Result<i32, ServerFnError> {
let new = COUNT.load(Ordering::Relaxed) + delta;
COUNT.store(new, Ordering::Relaxed);
_ = COUNT_CHANNEL.send(&new).await;
@@ -49,49 +46,36 @@ pub async fn clear_server_count() -> Result<i32, ServerFnError> {
#[component]
pub fn Counters(cx: Scope) -> impl IntoView {
provide_meta_context(cx);
view! { cx,
view! {
cx,
<Router>
<header>
<h1>"Server-Side Counters"</h1>
<p>"Each of these counters stores its data in the same variable on the server."</p>
<p>
"The value is shared across connections. Try opening this is another browser tab to see what I mean."
</p>
<p>"The value is shared across connections. Try opening this is another browser tab to see what I mean."</p>
</header>
<nav>
<ul>
<li>
<A href="">"Simple"</A>
</li>
<li>
<A href="form">"Form-Based"</A>
</li>
<li>
<A href="multi">"Multi-User"</A>
</li>
<li><A href="">"Simple"</A></li>
<li><A href="form">"Form-Based"</A></li>
<li><A href="multi">"Multi-User"</A></li>
</ul>
</nav>
<Link rel="shortcut icon" type_="image/ico" href="/favicon.ico"/>
<main>
<Routes>
<Route
path=""
view=|cx| {
view! { cx, <Counter/> }
}
/>
<Route
path="form"
view=|cx| {
view! { cx, <FormCounter/> }
}
/>
<Route
path="multi"
view=|cx| {
view! { cx, <MultiuserCounter/> }
}
/>
<Route path="" view=|cx| view! {
cx,
<Counter/>
}/>
<Route path="form" view=|cx| view! {
cx,
<FormCounter/>
}/>
<Route path="multi" view=|cx| view! {
cx,
<MultiuserCounter/>
}/>
</Routes>
</main>
</Router>
@@ -109,47 +93,33 @@ pub fn Counter(cx: Scope) -> impl IntoView {
let clear = create_action(cx, |_| clear_server_count());
let counter = create_resource(
cx,
move || {
(
dec.version().get(),
inc.version().get(),
clear.version().get(),
)
},
move || (dec.version().get(), inc.version().get(), clear.version().get()),
|_| get_server_count(),
);
let value = move || {
let value = move || counter.read(cx).map(|count| count.unwrap_or(0)).unwrap_or(0);
let error_msg = move || {
counter
.read(cx)
.map(|count| count.unwrap_or(0))
.unwrap_or(0)
};
let error_msg = move || {
counter.read(cx).and_then(|res| match res {
Ok(_) => None,
Err(e) => Some(e),
})
.map(|res| match res {
Ok(_) => None,
Err(e) => Some(e),
})
.flatten()
};
view! { cx,
view! {
cx,
<div>
<h2>"Simple Counter"</h2>
<p>
"This counter sets the value on the server and automatically reloads the new value."
</p>
<p>"This counter sets the value on the server and automatically reloads the new value."</p>
<div>
<button on:click=move |_| clear.dispatch(())>"Clear"</button>
<button on:click=move |_| dec.dispatch(())>"-1"</button>
<span>"Value: " {value} "!"</span>
<button on:click=move |_| inc.dispatch(())>"+1"</button>
</div>
{move || {
error_msg()
.map(|msg| {
view! { cx, <p>"Error: " {msg.to_string()}</p> }
})
}}
{move || error_msg().map(|msg| view! { cx, <p>"Error: " {msg.to_string()}</p>})}
</div>
}
}
@@ -172,15 +142,19 @@ pub fn FormCounter(cx: Scope) -> impl IntoView {
);
let value = move || {
log::debug!("FormCounter looking for value");
counter.read(cx).and_then(|n| n.ok()).unwrap_or(0)
counter
.read(cx)
.map(|n| n.ok())
.flatten()
.map(|n| n)
.unwrap_or(0)
};
view! { cx,
view! {
cx,
<div>
<h2>"Form Counter"</h2>
<p>
"This counter uses forms to set the value on the server. When progressively enhanced, it should behave identically to the “Simple Counter.”"
</p>
<p>"This counter uses forms to set the value on the server. When progressively enhanced, it should behave identically to the “Simple Counter.”"</p>
<div>
// calling a server function is the same as POSTing to its API URL
// so we can just do that with a form and button
@@ -211,32 +185,26 @@ pub fn FormCounter(cx: Scope) -> impl IntoView {
// This is the primitive pattern for live chat, collaborative editing, etc.
#[component]
pub fn MultiuserCounter(cx: Scope) -> impl IntoView {
let dec =
create_action(cx, |_| adjust_server_count(-1, "dec dec goose".into()));
let inc =
create_action(cx, |_| adjust_server_count(1, "inc inc moose".into()));
let dec = create_action(cx, |_| adjust_server_count(-1, "dec dec goose".into()));
let inc = create_action(cx, |_| adjust_server_count(1, "inc inc moose".into()));
let clear = create_action(cx, |_| clear_server_count());
#[cfg(not(feature = "ssr"))]
let multiplayer_value = {
use futures::StreamExt;
let mut source =
gloo_net::eventsource::futures::EventSource::new("/api/events")
.expect("couldn't connect to SSE stream");
let mut source = gloo_net::eventsource::futures::EventSource::new("/api/events")
.expect("couldn't connect to SSE stream");
let s = create_signal_from_stream(
cx,
source
.subscribe("message")
.unwrap()
.map(|value| match value {
Ok(value) => value
.1
.data()
.as_string()
.expect("expected string value"),
source.subscribe("message").unwrap().map(|value| {
match value {
Ok(value) => {
value.1.data().as_string().expect("expected string value")
},
Err(_) => "0".to_string(),
}),
}
})
);
on_cleanup(cx, move || source.close());
@@ -244,20 +212,18 @@ pub fn MultiuserCounter(cx: Scope) -> impl IntoView {
};
#[cfg(feature = "ssr")]
let (multiplayer_value, _) = create_signal(cx, None::<i32>);
let (multiplayer_value, _) =
create_signal(cx, None::<i32>);
view! { cx,
view! {
cx,
<div>
<h2>"Multi-User Counter"</h2>
<p>
"This one uses server-sent events (SSE) to live-update when other users make changes."
</p>
<p>"This one uses server-sent events (SSE) to live-update when other users make changes."</p>
<div>
<button on:click=move |_| clear.dispatch(())>"Clear"</button>
<button on:click=move |_| dec.dispatch(())>"-1"</button>
<span>
"Multiplayer Value: " {move || multiplayer_value.get().unwrap_or_default()}
</span>
<span>"Multiplayer Value: " {move || multiplayer_value.get().unwrap_or_default().to_string()}</span>
<button on:click=move |_| inc.dispatch(())>"+1"</button>
</div>
</div>

View File

@@ -1,10 +1,10 @@
use cfg_if::cfg_if;
use leptos::*;
pub mod counters;
// 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::counters::*;

View File

@@ -1,11 +1,11 @@
use cfg_if::cfg_if;
use leptos::*;
mod counters;
// boilerplate to run in different modes
cfg_if! {
// server-only stuff
if #[cfg(feature = "ssr")] {
use leptos::*;
use actix_files::{Files};
use actix_web::*;
use crate::counters::*;

View File

@@ -8,7 +8,7 @@ crate-type = ["cdylib", "rlib"]
[dependencies]
actix-files = { version = "0.6.2", optional = true }
actix-web = { version = "4.2.1", features = ["macros"] }
actix-web = { version = "4.2.1", optional = true, features = ["macros"] }
anyhow = "1.0.68"
broadcaster = "1.0.0"
console_log = "1.0.0"
@@ -19,7 +19,7 @@ cfg-if = "1.0.0"
leptos = { path = "../../leptos", default-features = false, features = [
"serde",
] }
leptos_actix = { path = "../../integrations/actix" }
leptos_actix = { path = "../../integrations/actix", optional = true }
leptos_meta = { path = "../../meta", default-features = false }
leptos_router = { path = "../../router", default-features = false }
log = "0.4.17"
@@ -36,8 +36,10 @@ 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",
]

View File

@@ -107,9 +107,6 @@ pub fn TodoApp(cx: Scope) -> impl IntoView {
cx,
<Todos/>
}/>
<Api path="bananas" route=web::get().to(|req: HttpRequest| async move {
req.path()
})
</Routes>
</main>
</Router>

View File

@@ -16,7 +16,7 @@ use actix_web::{
use futures::{Stream, StreamExt};
use http::StatusCode;
use leptos::{
leptos_dom::{Transparent, ssr::render_to_stream_with_prefix_undisposed_with_context},
leptos_dom::ssr::render_to_stream_with_prefix_undisposed_with_context,
leptos_server::{server_fn_by_path, Payload},
server_fn::Encoding,
*,
@@ -922,7 +922,7 @@ pub fn generate_route_list_with_exclusions<IV>(
where
IV: IntoView + 'static,
{
let (mut routes, mut api_routes) = leptos_router::generate_route_list_inner(app_fn);
let mut routes = leptos_router::generate_route_list_inner(app_fn);
// Empty strings screw with Actix pathing, they need to be "/"
routes = routes
@@ -1113,14 +1113,6 @@ where
}
}
/// Defines an API route, which mounts the given route handler at this path.
#[component(transparent)]
pub fn Api<P>(cx: leptos::Scope, path: P, route: actix_web::Route) -> impl IntoView
where P: Into<String>
{
Transparent::new(ApiRouteListing::new(path.into(), route))
}
/// A helper to make it easier to use Axum extractors in server functions. This takes
/// a handler function as its argument. The handler follows similar rules to an Actix
/// [Handler](actix_web::Handler): it is an async function that receives arguments that

View File

@@ -240,3 +240,24 @@ pub fn component_props_builder<P: Props>(
) -> <P as Props>::Builder {
<P as Props>::builder()
}
#[cfg(all(not(doc), feature = "csr", feature = "ssr"))]
compile_error!(
"You have both `csr` and `ssr` enabled as features, which may cause \
issues like <Suspense/>` failing to work silently. `csr` is enabled by \
default on `leptos`, and can be disabled by adding `default-features = \
false` to your `leptos` dependency."
);
#[cfg(all(not(doc), feature = "hydrate", feature = "ssr"))]
compile_error!(
"You have both `hydrate` and `ssr` enabled as features, which may cause \
issues like <Suspense/>` failing to work silently."
);
#[cfg(all(not(doc), feature = "hydrate", feature = "csr"))]
compile_error!(
"You have both `hydrate` and `csr` enabled as features, which may cause \
issues. `csr` is enabled by default on `leptos`, and can be disabled by \
adding `default-features = false` to your `leptos` dependency."
);

View File

@@ -904,40 +904,6 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
}
}
/// Optionally adds an event listener to this element.
///
/// ## Example
/// ```rust
/// # use leptos::*;
/// #[component]
/// pub fn Input(
/// cx: Scope,
/// #[prop(optional)] value: Option<RwSignal<String>>,
/// ) -> impl IntoView {
/// view! { cx, <input/> }
/// // only add event if `value` is `Some(signal)`
/// .optional_event(
/// ev::input,
/// value.map(|value| move |ev| value.set(event_target_value(&ev))),
/// )
/// }
/// #
/// ```
#[track_caller]
#[inline(always)]
pub fn optional_event<E: EventDescriptor + 'static>(
self,
event: E,
#[allow(unused_mut)] // used for tracing in debug
mut event_handler: Option<impl FnMut(E::EventType) + 'static>,
) -> Self {
if let Some(event_handler) = event_handler {
self.on(event, event_handler)
} else {
self
}
}
/// Adds a child to this element.
#[track_caller]
pub fn child(self, child: impl IntoView) -> Self {

View File

@@ -834,15 +834,6 @@ where
F: FnOnce(Scope) -> N + 'static,
N: IntoView,
{
#[cfg(all(feature = "web", feature = "ssr"))]
crate::console_warn(
"You have both `csr` and `ssr` or `hydrate` and `ssr` enabled as \
features, which may cause issues like <Suspense/>` failing to work \
silently. `csr` is enabled by default on `leptos`, and can be \
disabled by adding `default-features = false` to your `leptos` \
dependency.",
);
cfg_if! {
if #[cfg(all(target_arch = "wasm32", feature = "web"))] {
mount_to(crate::document().body().expect("body element to exist"), f)

View File

@@ -271,15 +271,6 @@ impl View {
instrument(level = "info", skip_all,)
)]
pub fn render_to_string(self, _cx: Scope) -> Cow<'static, str> {
#[cfg(all(feature = "web", feature = "ssr"))]
crate::console_error(
"\n[DANGER] You have both `csr` and `ssr` or `hydrate` and `ssr` \
enabled as features, which may cause issues like <Suspense/>` \
failing to work silently. `csr` is enabled by default on \
`leptos`, and can be disabled by adding `default-features = \
false` to your `leptos` dependency.\n",
);
self.render_to_string_helper(false)
}

View File

@@ -55,15 +55,6 @@ pub fn render_to_stream_in_order_with_prefix(
view: impl FnOnce(Scope) -> View + 'static,
prefix: impl FnOnce(Scope) -> Cow<'static, str> + 'static,
) -> impl Stream<Item = String> {
#[cfg(all(feature = "web", feature = "ssr"))]
crate::console_error(
"\n[DANGER] You have both `csr` and `ssr` or `hydrate` and `ssr` \
enabled as features, which may cause issues like <Suspense/>` \
failing to work silently. `csr` is enabled by default on `leptos`, \
and can be disabled by adding `default-features = false` to your \
`leptos` dependency.\n",
);
let (stream, runtime, _) =
render_to_stream_in_order_with_prefix_undisposed_with_context(
view,

View File

@@ -7,10 +7,9 @@ use itertools::Itertools;
use proc_macro2::{Ident, Span, TokenStream};
use quote::{format_ident, quote_spanned, ToTokens, TokenStreamExt};
use syn::{
parse::Parse, parse_quote, spanned::Spanned,
AngleBracketedGenericArguments, Attribute, FnArg, GenericArgument, Item,
ItemFn, Lit, LitStr, Meta, MetaNameValue, Pat, PatIdent, Path,
PathArguments, ReturnType, Stmt, Type, TypePath, Visibility,
parse::Parse, parse_quote, AngleBracketedGenericArguments, Attribute,
FnArg, GenericArgument, ItemFn, Lit, LitStr, Meta, MetaNameValue, Pat,
PatIdent, Path, PathArguments, ReturnType, Type, TypePath, Visibility,
};
pub struct Model {
@@ -130,25 +129,6 @@ impl ToTokens for Model {
let mut body = body.to_owned();
// check for components that end ;
if !is_transparent {
let ends_semi =
body.block.stmts.iter().last().and_then(|stmt| match stmt {
Stmt::Item(Item::Macro(mac)) => mac.semi_token.as_ref(),
_ => None,
});
if let Some(semi) = ends_semi {
proc_macro_error::emit_error!(
semi.span(),
"A component that ends with a `view!` macro followed by a \
semicolon will return (), an empty view. This is usually \
an accident, not intentional, so we prevent it. If youd \
like to return (), you can do it it explicitly by \
returning () as the last item from the component."
);
}
}
body.sig.ident = format_ident!("__{}", body.sig.ident);
#[allow(clippy::redundant_clone)] // false positive
let body_name = body.sig.ident.clone();

View File

@@ -816,7 +816,7 @@ pub fn slot(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
/// - **Arguments must be implement [`Serialize`](https://docs.rs/serde/latest/serde/trait.Serialize.html)
/// and [`DeserializeOwned`](https://docs.rs/serde/latest/serde/de/trait.DeserializeOwned.html).**
/// They are serialized as an `application/x-www-form-urlencoded`
/// form data using [`serde_qs`](https://docs.rs/serde_qs/latest/serde_qs/) or as `application/cbor`
/// form data using [`serde_html_form`](https://docs.rs/serde_html_form/latest/serde_html_form/) or as `application/cbor`
/// using [`cbor`](https://docs.rs/cbor/latest/cbor/). **Note**: You should explicitly include `serde` with the
/// `derive` feature enabled in your `Cargo.toml`. You can do this by running `cargo add serde --features=derive`.
/// - **The `Scope` comes from the server.** Optionally, the first argument of a server function

View File

@@ -72,7 +72,7 @@
//! 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_qs`](https://docs.rs/serde_qs/latest/serde_qs/) or as `application/cbor`
//! form data using [`serde_html_form`](https://docs.rs/serde_html_form/latest/serde_html_form/) or as `application/cbor`
//! using [`cbor`](https://docs.rs/cbor/latest/cbor/). **Note**: You should explicitly include `serde` with the
//! `derive` feature enabled in your `Cargo.toml`. You can do this by running `cargo add serde --features=derive`.
//! - **The [Scope](leptos_reactive::Scope) comes from the server.** Optionally, the first argument of a server function

View File

@@ -21,7 +21,7 @@ regex = { version = "1", optional = true }
url = { version = "2", optional = true }
percent-encoding = "2"
thiserror = "1"
serde_qs = "0.12"
serde_html_form = "0.2"
serde = "1"
tracing = "0.1"
js-sys = { version = "0.3" }

View File

@@ -537,12 +537,14 @@ where
Self: Sized + serde::de::DeserializeOwned,
{
/// Tries to deserialize the data, given only the `submit` event.
fn from_event(ev: &web_sys::Event) -> Result<Self, serde_qs::Error>;
fn from_event(
ev: &web_sys::Event,
) -> Result<Self, serde_html_form::de::Error>;
/// Tries to deserialize the data, given the actual form data.
fn from_form_data(
form_data: &web_sys::FormData,
) -> Result<Self, serde_qs::Error>;
) -> Result<Self, serde_html_form::de::Error>;
}
impl<T> FromFormData for T
@@ -553,7 +555,9 @@ where
any(debug_assertions, feature = "ssr"),
tracing::instrument(level = "trace", skip_all,)
)]
fn from_event(ev: &web_sys::Event) -> Result<Self, serde_qs::Error> {
fn from_event(
ev: &web_sys::Event,
) -> Result<Self, serde_html_form::de::Error> {
let (form, _, _, _) = extract_form_attributes(ev);
let form_data = web_sys::FormData::new_with_form(&form).unwrap_throw();
@@ -566,11 +570,11 @@ where
)]
fn from_form_data(
form_data: &web_sys::FormData,
) -> Result<Self, serde_qs::Error> {
) -> Result<Self, serde_html_form::de::Error> {
let data =
web_sys::UrlSearchParams::new_with_str_sequence_sequence(form_data)
.unwrap_throw();
let data = data.to_string().as_string().unwrap_or_default();
serde_qs::from_str::<Self>(&data)
serde_html_form::from_str::<Self>(&data)
}
}

View File

@@ -5,7 +5,6 @@ use crate::{
RouteDefinition, RouteMatch,
},
use_is_back_navigation, RouteContext, RouterContext,
ApiRouteListing
};
use leptos::{leptos_dom::HydrationCtx, *};
use std::{
@@ -44,7 +43,7 @@ pub fn Routes(
#[cfg(feature = "ssr")]
if let Some(context) = use_context::<crate::PossibleBranchContext>(cx) {
Branches::with(&base, |branches| {
*context.ui.borrow_mut() = branches.to_vec();
*context.0.borrow_mut() = branches.to_vec()
});
}
@@ -119,7 +118,7 @@ pub fn AnimatedRoutes(
#[cfg(feature = "ssr")]
if let Some(context) = use_context::<crate::PossibleBranchContext>(cx) {
Branches::with(&base, |branches| {
*context.ui.borrow_mut() = branches.to_vec()
*context.0.borrow_mut() = branches.to_vec()
});
}
@@ -225,12 +224,8 @@ pub fn AnimatedRoutes(
pub(crate) struct Branches;
type AppRoutes = (Vec<Branch>, Vec<ApiRouteListing>);
thread_local! {
// map is indexed by base
// this allows multiple apps per server
static BRANCHES: RefCell<HashMap<String, AppRoutes>> = Default::default();
static BRANCHES: RefCell<HashMap<String, Vec<Branch>>> = RefCell::new(HashMap::new());
}
impl Branches {
@@ -238,30 +233,29 @@ impl Branches {
BRANCHES.with(|branches| {
let mut current = branches.borrow_mut();
if !current.contains_key(base) {
let mut branches = (Vec::new(), Vec::new());
let mut route_defs = Vec::new();
let mut api_routes = Vec::new();
for child in children
let mut branches = Vec::new();
let children = children
.as_children()
.iter() {
let transparent = child.as_transparent();
if let Some(def) = transparent.and_then(|t| t.downcast_ref::<RouteDefinition>()) {
route_defs.push(def.clone());
} else if let Some(def) = transparent.and_then(|t| t.downcast_ref::<ApiRouteListing>()) {
api_routes.push(def.clone());
} else {
.iter()
.filter_map(|child| {
let def = child
.as_transparent()
.and_then(|t| t.downcast_ref::<RouteDefinition>());
if def.is_none() {
warn!(
"[NOTE] The <Routes/> component should \
include *only* <Route/> or <ProtectedRoute/> or <ApiRoute/> \
include *only* <Route/>or <ProtectedRoute/> \
components, or some \
#[component(transparent)] that returns a \
RouteDefinition."
);
}
}
def
})
.cloned()
.collect::<Vec<_>>();
create_branches(
&route_defs,
&children,
base,
&mut Vec::new(),
&mut branches,
@@ -278,18 +272,7 @@ impl Branches {
"Branches::initialize() should be called before \
Branches::with()",
);
cb(&branches.0)
})
}
pub fn with_api_routes<T>(base: &str, cb: impl FnOnce(&[ApiRouteListing]) -> T) -> T {
BRANCHES.with(|branches| {
let branches = branches.borrow();
let branches = branches.get(base).expect(
"Branches::initialize() should be called before \
Branches::with()",
);
cb(&branches.1)
cb(branches)
})
}
}
@@ -498,7 +481,7 @@ fn create_branches(
route_defs: &[RouteDefinition],
base: &str,
stack: &mut Vec<RouteData>,
branches: &mut (Vec<Branch>, Vec<ApiRouteListing>),
branches: &mut Vec<Branch>,
) {
for def in route_defs {
let routes = create_routes(def, base);
@@ -506,8 +489,8 @@ fn create_branches(
stack.push(route.clone());
if def.children.is_empty() {
let branch = create_branch(stack, branches.0.len());
branches.0.push(branch);
let branch = create_branch(stack, branches.len());
branches.push(branch);
} else {
create_branches(&def.children, &route.pattern, stack, branches);
}
@@ -517,7 +500,7 @@ fn create_branches(
}
if stack.is_empty() {
branches.0.sort_by_key(|branch| Reverse(branch.score));
branches.sort_by_key(|branch| Reverse(branch.score));
}
}

View File

@@ -2,24 +2,14 @@ use crate::{
Branch, Method, RouterIntegrationContext, ServerIntegration, SsrMode,
};
use leptos::*;
use std::{any::Any, cell::RefCell, collections::HashSet, rc::Rc, sync::Arc};
use std::{cell::RefCell, collections::HashSet, rc::Rc};
/// Context to contain all possible routes.
#[derive(Clone, Default, Debug)]
pub struct PossibleBranchContext {
pub(crate) ui: Rc<RefCell<Vec<Branch>>>,
pub(crate) api: Rc<RefCell<Vec<ApiRouteListing>>>
}
#[derive(Clone, Debug)]
/// A route that this application can serve.
pub enum PossibleRouteListing {
View(RouteListing),
Api(ApiRouteListing)
}
pub struct PossibleBranchContext(pub(crate) Rc<RefCell<Vec<Branch>>>);
#[derive(Clone, Debug, Default, PartialEq, Eq)]
/// Route listing for a component-based view.
/// A route that this application can serve.
pub struct RouteListing {
path: String,
mode: SsrMode,
@@ -56,69 +46,12 @@ impl RouteListing {
}
}
#[derive(Clone)]
/// Route listing for an API route.
pub struct ApiRouteListing {
path: String,
methods: Option<HashSet<Method>>,
// this will be downcast by the implementation
handler: Arc<dyn Any>
}
impl std::fmt::Debug for ApiRouteListing {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ApiRouteListing").field("path", &self.path).field("methods", &self.methods).finish()
}
}
impl ApiRouteListing {
/// Create an API route listing from its parts.
pub fn new<T: 'static>(
path: impl ToString,
handler: T
) -> Self {
Self {
path: path.to_string(),
methods: None,
handler: Arc::new(handler)
}
}
/// Create an API route listing from its parts.
pub fn new_with_methods<T: 'static>(
path: impl ToString,
methods: impl IntoIterator<Item = Method>,
handler: T
) -> Self {
Self {
path: path.to_string(),
methods: Some(methods.into_iter().collect()),
handler: Arc::new(handler)
}
}
/// The path this route handles.
pub fn path(&self) -> &str {
&self.path
}
/// The HTTP request methods this path can handle.
pub fn methods(&self) -> impl Iterator<Item = Method> + '_ {
self.methods.iter().flatten().copied()
}
/// The handler for a request at this route
pub fn handler<T: 'static>(&self) -> Option<&T> {
self.handler.downcast_ref::<T>()
}
}
/// Generates a list of all routes this application could possibly serve. This returns the raw routes in the leptos_router
/// format. Odds are you want `generate_route_list()` from either the actix, axum, or viz integrations if you want
/// to work with their router
pub fn generate_route_list_inner<IV>(
app_fn: impl FnOnce(Scope) -> IV + 'static,
) -> (Vec<RouteListing>, Vec<ApiRouteListing>)
) -> Vec<RouteListing>
where
IV: IntoView + 'static,
{
@@ -136,8 +69,8 @@ where
_ = app_fn(cx).into_view(cx);
leptos::suppress_resource_load(false);
let ui_branches = branches.ui.borrow();
let ui = ui_branches
let branches = branches.0.borrow();
branches
.iter()
.flat_map(|branch| {
let mode = branch
@@ -160,12 +93,6 @@ where
methods: methods.clone(),
})
})
.collect();
let api_branches = branches.api.borrow();
let api = api_branches
.iter()
.cloned()
.collect();
(ui, api)
.collect()
})
}

View File

@@ -11,7 +11,7 @@ readme = "../README.md"
[dependencies]
server_fn_macro_default = { workspace = true }
serde = { version = "1", features = ["derive"] }
serde_qs = "0.12"
serde_html_form = "0.2"
thiserror = "1"
serde_json = "1"
quote = "1"

View File

@@ -49,7 +49,7 @@ use syn::__private::ToTokens;
/// - **Arguments must be implement [`Serialize`](https://docs.rs/serde/latest/serde/trait.Serialize.html)
/// and [`DeserializeOwned`](https://docs.rs/serde/latest/serde/de/trait.DeserializeOwned.html).**
/// They are serialized as an `application/x-www-form-urlencoded`
/// form data using [`serde_qs`](https://docs.rs/serde_qs/latest/serde_qs/) or as `application/cbor`
/// form data using [`serde_html_form`](https://docs.rs/serde_html_form/latest/serde_html_form/) or as `application/cbor`
/// using [`cbor`](https://docs.rs/cbor/latest/cbor/).
#[proc_macro_attribute]
pub fn server(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {

View File

@@ -75,7 +75,7 @@
//! 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_qs`](https://docs.rs/serde_qs/latest/serde_qs/) or as `application/cbor`
//! form data using [`serde_html_form`](https://docs.rs/serde_html_form/latest/serde_html_form/) or as `application/cbor`
//! using [`cbor`](https://docs.rs/cbor/latest/cbor/).
// used by the macro
@@ -308,7 +308,7 @@ where
// decode the args
let value = match Self::encoding() {
Encoding::Url | Encoding::GetJSON | Encoding::GetCBOR => {
serde_qs::from_bytes(data).map_err(|e| {
serde_html_form::from_bytes(data).map_err(|e| {
ServerFnError::Deserialization(e.to_string())
})
}
@@ -408,7 +408,7 @@ where
}
let args_encoded = match &enc {
Encoding::Url | Encoding::GetJSON | Encoding::GetCBOR => Payload::Url(
serde_qs::to_string(&args)
serde_html_form::to_string(&args)
.map_err(|e| ServerFnError::Serialization(e.to_string()))?,
),
Encoding::Cbor => {