Compare commits

..

46 Commits

Author SHA1 Message Date
Greg Johnston
ae9f090238 change: remove unused Result alias 2025-02-09 14:10:43 -05:00
zakstucke
287fc47163 "Update axum paths to 0.8 syntax" (#3555)
* Update axum paths to 0.8 syntax

* [autofix.ci] apply automated fixes

* [autofix.ci] apply automated fixes (attempt 2/3)

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2025-02-06 16:17:12 -08:00
zakstucke
8f74a6d8a0 AddAnyAttr static (#3553) 2025-02-06 14:00:25 -08:00
starmaker
597175a54b Fixing closing brace (#3539)
Most likely confglict merge artefact
2025-02-01 09:40:03 -08:00
Chris
ede25b9e3d fix: remove Default impl for LeptosOptions and ConfFile (#3522)
* fix: remove `Default` impl for `LeptosOptions` and `ConfFile`

* [autofix.ci] apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2025-02-01 09:41:44 -05:00
Greg Johnston
8f636e354a change: allow IntoFuture for Suspend::new() (closes #3509) (#3532) 2025-01-31 14:25:35 -05:00
Greg Johnston
7da64f22c4 Merge branch 'main' into leptos_0.8 2025-01-30 21:32:58 -05:00
Greg Johnston
0073ae7d8a chore: update version numbers preparing for 0.8.0-alpha 2025-01-30 21:29:54 -05:00
Georg Vienna
1ff373dbc2 feat: allow raw identifiers in Params derive macro (#3525) 2025-01-29 19:39:23 -05:00
Greg Johnston
c35c42c6e3 fix: correctly notify descendants and ancestors of store fields (closes #3523) (#3524) 2025-01-29 19:28:26 -05:00
Geoffrey Garrett
d4cbba7c63 feat(callback): implement matches method for Callback and UnsyncCallback (#3520)
* feat(callback): implement matches method for Callback and UnsyncCallback

* [autofix.ci] apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2025-01-27 20:37:11 -05:00
stefnotch
9cc8ee3c5a fix: memo recomputation (closes #3252) (#3495) 2025-01-27 20:19:16 -05:00
SleeplessOne1917
f0c5ffe55f impl IntoAttributeValue for TextProp (#3517)
* impl IntoAttributeValue for TextProp

* [autofix.ci] apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2025-01-26 14:16:58 -05:00
benwis
8465716a19 Fix formatting 2025-01-26 09:51:22 -08:00
zakstucke
0e24b2e63f AddAnyAttr working with erase_components (#3518)
* AddAnyAttr working with erase_components

* CI fixes
2025-01-26 09:51:22 -08:00
Danik Vitek
c64d205984 feat (either_of): Extent API; Implement other iterator methods; Update deps (#3478)
* Implement other iterator methods. Update deps

* Formatting

* Update Cargo.lock

* [autofix.ci] apply automated fixes

* Formatting

* Move `Either` declaration into the `tuples` macro

* Comment out non-MSRV-compliant methods

* [autofix.ci] apply automated fixes

* Formatting

* Implement mapping functions

* Fix clippy warnings

* Impl `Error`; Impl `From<Result<A, B>> for Either<B, A>`

* Fix `Error` impl

* Move `Error` impl under `#[cfg(not(feature="no_std"))] until MSRV >= 1.81

* [autofix.ci] apply automated fixes

* Make `From<Result>` compliant with `EitherOr`. Add `impl EitherOr for Either`

* fix: use fully-qualified name

* fix: `EitherOf` test

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2025-01-26 09:51:22 -08:00
Greg Johnston
f17cb98eb0 chore: update workspace dependency versions to latest (#3506) 2025-01-26 09:51:22 -08:00
Danik Vitek
30f3e82664 docs: Fix README.md & Add MSRV badge (#3480)
* Fix README.md

* Add MSRV badge

* [autofix.ci] apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2025-01-26 09:51:22 -08:00
starmaker
152d5a5c92 issue-3467 - bumping codee version to support rkyv 8 (#3504)
* issue-3467 - bumping codee version to support rkyv 8

* [autofix.ci] apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2025-01-26 09:51:22 -08:00
Sam Jude
669e1ba7fa hexagonal architecture (#3342)
Co-authored-by: Sam <@>
2025-01-26 09:51:22 -08:00
dcsturman
2ad6a086f9 Enhanced docs for reactive_stores (#3508)
Added docs on shadow traits, Option, Enum, Vec, and Box usage with Store.
2025-01-26 09:51:22 -08:00
Greg Johnston
32e58d6b66 fix: including node_ref after {..} on arbitrary components (#3503) 2025-01-26 09:51:22 -08:00
Greg Johnston
a107443104 chore(ci): add CI for leptos_0.8 branch (#3500) 2025-01-26 09:51:22 -08:00
Greg Johnston
c859b07901 feat: #[lazy] macros to support lazy loading and code splitting (#3477) 2025-01-26 09:51:22 -08:00
Greg Johnston
a9868bea2b chore: restore reactivity warning at top level of components (closes #3354) (#3499) 2025-01-26 09:51:22 -08:00
Greg Johnston
7183c2b993 fix: correctly handle ErrorBoundary through reactive views (closes #3487) (#3492) 2025-01-26 09:51:22 -08:00
Greg Johnston
7a03621db1 feat: implement unboxing support for recursive store nodes (closes #3491) (#3493) 2025-01-26 09:51:22 -08:00
benwis
586c330995 Fix formatting 2025-01-26 09:46:19 -08:00
zakstucke
72f960a026 AddAnyAttr working with erase_components (#3518)
* AddAnyAttr working with erase_components

* CI fixes
2025-01-26 09:43:32 -08:00
Danik Vitek
b62ae56094 feat (either_of): Extent API; Implement other iterator methods; Update deps (#3478)
* Implement other iterator methods. Update deps

* Formatting

* Update Cargo.lock

* [autofix.ci] apply automated fixes

* Formatting

* Move `Either` declaration into the `tuples` macro

* Comment out non-MSRV-compliant methods

* [autofix.ci] apply automated fixes

* Formatting

* Implement mapping functions

* Fix clippy warnings

* Impl `Error`; Impl `From<Result<A, B>> for Either<B, A>`

* Fix `Error` impl

* Move `Error` impl under `#[cfg(not(feature="no_std"))] until MSRV >= 1.81

* [autofix.ci] apply automated fixes

* Make `From<Result>` compliant with `EitherOr`. Add `impl EitherOr for Either`

* fix: use fully-qualified name

* fix: `EitherOf` test

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2025-01-24 21:15:29 -08:00
Greg Johnston
9ccefbbd8c chore: update workspace dependency versions to latest (#3506) 2025-01-24 21:07:20 -08:00
Danik Vitek
d1108826cc docs: Fix README.md & Add MSRV badge (#3480)
* Fix README.md

* Add MSRV badge

* [autofix.ci] apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2025-01-24 21:06:19 -08:00
starmaker
d6c4cd2a81 issue-3467 - bumping codee version to support rkyv 8 (#3504)
* issue-3467 - bumping codee version to support rkyv 8

* [autofix.ci] apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2025-01-24 20:29:55 -08:00
Sam Jude
f8acdf9168 hexagonal architecture (#3342)
Co-authored-by: Sam <@>
2025-01-24 20:29:07 -08:00
dcsturman
09bbae2a72 Enhanced docs for reactive_stores (#3508)
Added docs on shadow traits, Option, Enum, Vec, and Box usage with Store.
2025-01-24 20:23:13 -08:00
Spencer Ferris
2b589fa61f feat: Add more options for generating server fn routes (#3438)
* feat: Allow disabling server fn hash and customizing the default prefix

Allow configuring the default prefix for server function API routes. This is useful to
override the default prefix (`/api`) for all server functions without needing to manually
specify via `#[server(prefix = "...")]` on every server function.

Also, allow disabling appending the server functions' hashes to the end of their API names.
This is useful when an app's client side needs a stable server API. For example, shipping
the CSR WASM binary in a Tauri app. Tauri app releases are dependent on each platform's
distribution method (e.g., the Apple App Store or the Google Play Store), which typically
are much slower than the frequency at which a website can be updated. In addition, it's
common for users to not have the latest app version installed. In these cases, the CSR WASM
app would need to be able to continue calling the backend server function API, so the API
path needs to be consistent and not have a hash appended.

* Mark public structs as `#[non_exhaustive]` and add doc comments

* Minor refactor to pull the fn hash logic out of the `path` statement

* feat: Use module path in prefix for server fn API route

Allow including the module path of the server function in the API route. This
is an alternative strategy to prevent duplicate server function API routes
(the default strategy is to add a hash to the end of the route). Each element
of the module path will be separated by a `/`. For example, a server function
with a fully qualified name of `parent:🧒:server_fn` would have an API
route of `/api/parent/child/server_fn` (possibly with a different prefix and
a hash suffix depending on the values of the other server fn configs).

* Fix `enable_hash` if statement

* Add missing import
2025-01-24 20:11:38 -08:00
Greg Johnston
ac3352724b fix: including node_ref after {..} on arbitrary components (#3503) 2025-01-18 09:18:05 -05:00
Greg Johnston
3413825638 chore(ci): add CI for leptos_0.8 branch (#3500) 2025-01-17 16:04:36 -05:00
Greg Johnston
22b2d8ec84 feat: #[lazy] macros to support lazy loading and code splitting (#3477) 2025-01-17 15:15:12 -05:00
Greg Johnston
1c3e013a63 chore: restore reactivity warning at top level of components (closes #3354) (#3499) 2025-01-17 14:06:39 -05:00
Saber Haj Rabiee
35e6f17930 chore: upgrade axum to v0.8 (#3439) 2025-01-17 13:38:37 -05:00
Greg Johnston
d1513a4a0b feat(breaking): allow make PossibleRouteMatch dyn-safe (#3421) 2025-01-17 13:33:00 -05:00
Mario Carbajal
aa27b9e474 feat: impl Dispose for Callback types and add try_run to the Callable trait (#3371)
* impl Dispose for Callback types and add try_run to the Callable trait

* [autofix.ci] apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2025-01-17 13:32:04 -05:00
Ryo Hirayama
cfe925d58f feat: allow any type that implements FromServerFnError as a replacement of the ServerFnError in server_fn (#3274) 2025-01-17 13:30:12 -05:00
Greg Johnston
9d4b1bc4b7 fix: correctly handle ErrorBoundary through reactive views (closes #3487) (#3492) 2025-01-17 13:22:06 -05:00
Greg Johnston
f303ff9706 feat: implement unboxing support for recursive store nodes (closes #3491) (#3493) 2025-01-17 13:21:40 -05:00
176 changed files with 7668 additions and 2734 deletions

View File

@@ -4,10 +4,12 @@ on:
branches:
- main
- leptos_0.6
- leptos_0.8
pull_request:
branches:
- main
- leptos_0.6
- leptos_0.8
jobs:
get-example-changed:
uses: ./.github/workflows/get-example-changed.yml

View File

@@ -4,10 +4,12 @@ on:
branches:
- main
- leptos_0.6
- leptos_0.8
pull_request:
branches:
- main
- leptos_0.6
- leptos_0.8
jobs:
get-leptos-changed:
uses: ./.github/workflows/get-leptos-changed.yml

View File

@@ -4,10 +4,12 @@ on:
branches:
- main
- leptos_0.6
- leptos_0.8
pull_request:
branches:
- main
- leptos_0.6
- leptos_0.8
env:
DEBIAN_FRONTEND: noninteractive
jobs:

View File

@@ -4,10 +4,12 @@ on:
branches:
- main
- leptos_0.6
- leptos_0.8
pull_request:
branches:
- main
- leptos_0.6
- leptos_0.8
jobs:
get-leptos-changed:
uses: ./.github/workflows/get-leptos-changed.yml

741
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -40,7 +40,7 @@ members = [
exclude = ["benchmarks", "examples", "projects"]
[workspace.package]
version = "0.7.4"
version = "0.8.0-alpha"
edition = "2021"
rust-version = "1.76"
@@ -50,25 +50,25 @@ any_spawner = { path = "./any_spawner/", version = "0.2.0" }
const_str_slice_concat = { path = "./const_str_slice_concat", version = "0.1" }
either_of = { path = "./either_of/", version = "0.1.0" }
hydration_context = { path = "./hydration_context", version = "0.2.0" }
leptos = { path = "./leptos", version = "0.7.4" }
leptos_config = { path = "./leptos_config", version = "0.7.4" }
leptos_dom = { path = "./leptos_dom", version = "0.7.4" }
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.7.4" }
leptos_integration_utils = { path = "./integrations/utils", version = "0.7.4" }
leptos_macro = { path = "./leptos_macro", version = "0.7.4" }
leptos_router = { path = "./router", version = "0.7.4" }
leptos_router_macro = { path = "./router_macro", version = "0.7.4" }
leptos_server = { path = "./leptos_server", version = "0.7.4" }
leptos_meta = { path = "./meta", version = "0.7.4" }
leptos = { path = "./leptos", version = "0.8.0-alpha" }
leptos_config = { path = "./leptos_config", version = "0.8.0-alpha" }
leptos_dom = { path = "./leptos_dom", version = "0.8.0-alpha" }
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.8.0-alpha" }
leptos_integration_utils = { path = "./integrations/utils", version = "0.8.0-alpha" }
leptos_macro = { path = "./leptos_macro", version = "0.8.0-alpha" }
leptos_router = { path = "./router", version = "0.8.0-alpha" }
leptos_router_macro = { path = "./router_macro", version = "0.8.0-alpha" }
leptos_server = { path = "./leptos_server", version = "0.8.0-alpha" }
leptos_meta = { path = "./meta", version = "0.8.0-alpha" }
next_tuple = { path = "./next_tuple", version = "0.1.0" }
oco_ref = { path = "./oco", version = "0.2.0" }
or_poisoned = { path = "./or_poisoned", version = "0.1.0" }
reactive_graph = { path = "./reactive_graph", version = "0.1.4" }
reactive_stores = { path = "./reactive_stores", version = "0.1.3" }
reactive_stores_macro = { path = "./reactive_stores_macro", version = "0.1.0" }
server_fn = { path = "./server_fn", version = "0.7.4" }
server_fn_macro = { path = "./server_fn_macro", version = "0.7.4" }
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.7.4" }
server_fn = { path = "./server_fn", version = "0.8.0-alpha" }
server_fn_macro = { path = "./server_fn_macro", version = "0.8.0-alpha" }
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.8.0-alpha" }
tachys = { path = "./tachys", version = "0.1.4" }
[profile.release]

View File

@@ -5,6 +5,7 @@
[![crates.io](https://img.shields.io/crates/v/leptos.svg)](https://crates.io/crates/leptos)
[![docs.rs](https://docs.rs/leptos/badge.svg)](https://docs.rs/leptos)
![Crates.io MSRV](https://img.shields.io/crates/msrv/leptos)
[![Discord](https://img.shields.io/discord/1031524867910148188?color=%237289DA&label=discord)](https://discord.gg/YdRAhS7eQB)
[![Matrix](https://img.shields.io/badge/Matrix-leptos-grey?logo=matrix&labelColor=white&logoColor=black)](https://matrix.to/#/#leptos:matrix.org)
@@ -167,14 +168,14 @@ Yew is the most-used library for Rust web UI development, but there are several
- **Performance:** This has huge performance implications: Leptos is simply much faster at both creating and updating the UI than Yew is.
- **Server integration:** Yew was created in an era in which browser-rendered single-page apps (SPAs) were the dominant paradigm. While Leptos supports client-side rendering, it also focuses on integrating with the server side of your application via server functions and multiple modes of serving HTML, including out-of-order streaming.
- ### How is this different from Dioxus?
### How is this different from Dioxus?
Like Leptos, Dioxus is a framework for building UIs using web technologies. However, there are significant differences in approach and features.
- **VDOM vs. fine-grained:** While Dioxus has a performant virtual DOM (VDOM), it still uses coarse-grained/component-scoped reactivity: changing a stateful value reruns the component function and diffs the old UI against the new one. Leptos components use a different mental model, creating (and returning) actual DOM nodes and setting up a reactive system to update those DOM nodes.
- **Web vs. desktop priorities:** Dioxus uses Leptos server functions in its fullstack mode, but does not have the same `<Suspense>`-based support for things like streaming HTML rendering, or share the same focus on holistic web performance. Leptos tends to prioritize holistic web performance (streaming HTML rendering, smaller WASM binary sizes, etc.), whereas Dioxus has an unparalleled experience when building desktop apps, because your application logic runs as a native Rust binary.
- ### How is this different from Sycamore?
### How is this different from Sycamore?
Sycamore and Leptos are both heavily influenced by SolidJS. At this point, Leptos has a larger community and ecosystem and is more actively developed. Other differences:

View File

@@ -1,6 +1,6 @@
[package]
name = "throw_error"
version = "0.2.0"
version = "0.3.0"
authors = ["Greg Johnston"]
license = "MIT"
readme = "../README.md"

View File

@@ -17,11 +17,6 @@ use std::{
/* Wrapper Types */
/// This is a result type into which any error can be converted.
///
/// Results are stored as [`Error`].
pub type Result<T, E = Error> = core::result::Result<T, E>;
/// A generic wrapper for any error.
#[derive(Debug, Clone)]
#[repr(transparent)]

View File

@@ -1,6 +1,6 @@
[package]
name = "either_of"
version = "0.1.3"
version = "0.1.4"
authors = ["Greg Johnston"]
license = "MIT"
readme = "../README.md"
@@ -10,4 +10,9 @@ rust-version.workspace = true
edition.workspace = true
[dependencies]
pin-project-lite = "0.2.15"
pin-project-lite = "0.2.16"
paste = "1.0.15"
[features]
default = ["no_std"]
no_std = []

View File

@@ -1,33 +1,489 @@
#![no_std]
#![cfg_attr(feature = "no_std", no_std)]
#![forbid(unsafe_code)]
//! Utilities for working with enumerated types that contain one of `2..n` other types.
use core::{
cmp::Ordering,
fmt::Display,
future::Future,
iter::{Product, Sum},
pin::Pin,
task::{Context, Poll},
};
use paste::paste;
use pin_project_lite::pin_project;
#[cfg(not(feature = "no_std"))]
use std::error::Error; // TODO: replace with core::error::Error once MSRV is >= 1.81.0
#[derive(Debug, Clone, Copy)]
pub enum Either<A, B> {
Left(A),
Right(B),
macro_rules! tuples {
($name:ident + $fut_name:ident + $fut_proj:ident {
$($ty:ident => ($($rest_variant:ident),*) + <$($mapped_ty:ident),+>),+$(,)?
}) => {
tuples!($name + $fut_name + $fut_proj {
$($ty($ty) => ($($rest_variant),*) + <$($mapped_ty),+>),+
});
};
($name:ident + $fut_name:ident + $fut_proj:ident {
$($variant:ident($ty:ident) => ($($rest_variant:ident),*) + <$($mapped_ty:ident),+>),+$(,)?
}) => {
#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
pub enum $name<$($ty),+> {
$($variant ($ty),)+
}
impl<$($ty),+> $name<$($ty),+> {
paste! {
#[allow(clippy::too_many_arguments)]
pub fn map<$([<F $ty>]),+, $([<$ty 1>]),+>(self, $([<$variant:lower>]: [<F $ty>]),+) -> $name<$([<$ty 1>]),+>
where
$([<F $ty>]: FnOnce($ty) -> [<$ty 1>],)+
{
match self {
$($name::$variant(inner) => $name::$variant([<$variant:lower>](inner)),)+
}
}
$(
pub fn [<map_ $variant:lower>]<Fun, [<$ty 1>]>(self, f: Fun) -> $name<$($mapped_ty),+>
where
Fun: FnOnce($ty) -> [<$ty 1>],
{
match self {
$name::$variant(inner) => $name::$variant(f(inner)),
$($name::$rest_variant(inner) => $name::$rest_variant(inner),)*
}
}
pub fn [<inspect_ $variant:lower>]<Fun, [<$ty 1>]>(self, f: Fun) -> Self
where
Fun: FnOnce(&$ty),
{
if let $name::$variant(inner) = &self {
f(inner);
}
self
}
pub fn [<is_ $variant:lower>](&self) -> bool {
matches!(self, $name::$variant(_))
}
pub fn [<as_ $variant:lower>](&self) -> Option<&$ty> {
match self {
$name::$variant(inner) => Some(inner),
_ => None,
}
}
pub fn [<as_ $variant:lower _mut>](&mut self) -> Option<&mut $ty> {
match self {
$name::$variant(inner) => Some(inner),
_ => None,
}
}
pub fn [<unwrap_ $variant:lower>](self) -> $ty {
match self {
$name::$variant(inner) => inner,
_ => panic!(concat!(
"called `unwrap_", stringify!([<$variant:lower>]), "()` on a non-`", stringify!($variant), "` variant of `", stringify!($name), "`"
)),
}
}
pub fn [<into_ $variant:lower>](self) -> Result<$ty, Self> {
match self {
$name::$variant(inner) => Ok(inner),
_ => Err(self),
}
}
)+
}
}
impl<$($ty),+> Display for $name<$($ty),+>
where
$($ty: Display,)+
{
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
$($name::$variant(this) => this.fmt(f),)+
}
}
}
#[cfg(not(feature = "no_std"))]
impl<$($ty),+> Error for $name<$($ty),+>
where
$($ty: Error,)+
{
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
$($name::$variant(this) => this.source(),)+
}
}
}
impl<Item, $($ty),+> Iterator for $name<$($ty),+>
where
$($ty: Iterator<Item = Item>,)+
{
type Item = Item;
fn next(&mut self) -> Option<Self::Item> {
match self {
$($name::$variant(i) => i.next(),)+
}
}
fn size_hint(&self) -> (usize, Option<usize>) {
match self {
$($name::$variant(i) => i.size_hint(),)+
}
}
fn count(self) -> usize
where
Self: Sized,
{
match self {
$($name::$variant(i) => i.count(),)+
}
}
fn last(self) -> Option<Self::Item>
where
Self: Sized,
{
match self {
$($name::$variant(i) => i.last(),)+
}
}
fn nth(&mut self, n: usize) -> Option<Self::Item> {
match self {
$($name::$variant(i) => i.nth(n),)+
}
}
fn for_each<Fun>(self, f: Fun)
where
Self: Sized,
Fun: FnMut(Self::Item),
{
match self {
$($name::$variant(i) => i.for_each(f),)+
}
}
fn collect<Col: FromIterator<Self::Item>>(self) -> Col
where
Self: Sized,
{
match self {
$($name::$variant(i) => i.collect(),)+
}
}
fn partition<Col, Fun>(self, f: Fun) -> (Col, Col)
where
Self: Sized,
Col: Default + Extend<Self::Item>,
Fun: FnMut(&Self::Item) -> bool,
{
match self {
$($name::$variant(i) => i.partition(f),)+
}
}
fn fold<Acc, Fun>(self, init: Acc, f: Fun) -> Acc
where
Self: Sized,
Fun: FnMut(Acc, Self::Item) -> Acc,
{
match self {
$($name::$variant(i) => i.fold(init, f),)+
}
}
fn reduce<Fun>(self, f: Fun) -> Option<Self::Item>
where
Self: Sized,
Fun: FnMut(Self::Item, Self::Item) -> Self::Item,
{
match self {
$($name::$variant(i) => i.reduce(f),)+
}
}
fn all<Fun>(&mut self, f: Fun) -> bool
where
Self: Sized,
Fun: FnMut(Self::Item) -> bool,
{
match self {
$($name::$variant(i) => i.all(f),)+
}
}
fn any<Fun>(&mut self, f: Fun) -> bool
where
Self: Sized,
Fun: FnMut(Self::Item) -> bool,
{
match self {
$($name::$variant(i) => i.any(f),)+
}
}
fn find<Pre>(&mut self, predicate: Pre) -> Option<Self::Item>
where
Self: Sized,
Pre: FnMut(&Self::Item) -> bool,
{
match self {
$($name::$variant(i) => i.find(predicate),)+
}
}
fn find_map<Out, Fun>(&mut self, f: Fun) -> Option<Out>
where
Self: Sized,
Fun: FnMut(Self::Item) -> Option<Out>,
{
match self {
$($name::$variant(i) => i.find_map(f),)+
}
}
fn position<Pre>(&mut self, predicate: Pre) -> Option<usize>
where
Self: Sized,
Pre: FnMut(Self::Item) -> bool,
{
match self {
$($name::$variant(i) => i.position(predicate),)+
}
}
fn max(self) -> Option<Self::Item>
where
Self: Sized,
Self::Item: Ord,
{
match self {
$($name::$variant(i) => i.max(),)+
}
}
fn min(self) -> Option<Self::Item>
where
Self: Sized,
Self::Item: Ord,
{
match self {
$($name::$variant(i) => i.min(),)+
}
}
fn max_by_key<Key: Ord, Fun>(self, f: Fun) -> Option<Self::Item>
where
Self: Sized,
Fun: FnMut(&Self::Item) -> Key,
{
match self {
$($name::$variant(i) => i.max_by_key(f),)+
}
}
fn max_by<Cmp>(self, compare: Cmp) -> Option<Self::Item>
where
Self: Sized,
Cmp: FnMut(&Self::Item, &Self::Item) -> Ordering,
{
match self {
$($name::$variant(i) => i.max_by(compare),)+
}
}
fn min_by_key<Key: Ord, Fun>(self, f: Fun) -> Option<Self::Item>
where
Self: Sized,
Fun: FnMut(&Self::Item) -> Key,
{
match self {
$($name::$variant(i) => i.min_by_key(f),)+
}
}
fn min_by<Cmp>(self, compare: Cmp) -> Option<Self::Item>
where
Self: Sized,
Cmp: FnMut(&Self::Item, &Self::Item) -> Ordering,
{
match self {
$($name::$variant(i) => i.min_by(compare),)+
}
}
fn sum<Out>(self) -> Out
where
Self: Sized,
Out: Sum<Self::Item>,
{
match self {
$($name::$variant(i) => i.sum(),)+
}
}
fn product<Out>(self) -> Out
where
Self: Sized,
Out: Product<Self::Item>,
{
match self {
$($name::$variant(i) => i.product(),)+
}
}
fn cmp<Other>(self, other: Other) -> Ordering
where
Other: IntoIterator<Item = Self::Item>,
Self::Item: Ord,
Self: Sized,
{
match self {
$($name::$variant(i) => i.cmp(other),)+
}
}
fn partial_cmp<Other>(self, other: Other) -> Option<Ordering>
where
Other: IntoIterator,
Self::Item: PartialOrd<Other::Item>,
Self: Sized,
{
match self {
$($name::$variant(i) => i.partial_cmp(other),)+
}
}
// TODO: uncomment once MSRV is >= 1.82.0
// fn is_sorted(self) -> bool
// where
// Self: Sized,
// Self::Item: PartialOrd,
// {
// match self {
// $($name::$variant(i) => i.is_sorted(),)+
// }
// }
//
// fn is_sorted_by<Cmp>(self, compare: Cmp) -> bool
// where
// Self: Sized,
// Cmp: FnMut(&Self::Item, &Self::Item) -> bool,
// {
// match self {
// $($name::$variant(i) => i.is_sorted_by(compare),)+
// }
// }
//
// fn is_sorted_by_key<Fun, Key>(self, f: Fun) -> bool
// where
// Self: Sized,
// Fun: FnMut(Self::Item) -> Key,
// Key: PartialOrd,
// {
// match self {
// $($name::$variant(i) => i.is_sorted_by_key(f),)+
// }
// }
}
impl<Item, $($ty),+> ExactSizeIterator for $name<$($ty),+>
where
$($ty: ExactSizeIterator<Item = Item>,)+
{
fn len(&self) -> usize {
match self {
$($name::$variant(i) => i.len(),)+
}
}
}
impl<Item, $($ty),+> DoubleEndedIterator for $name<$($ty),+>
where
$($ty: DoubleEndedIterator<Item = Item>,)+
{
fn next_back(&mut self) -> Option<Self::Item> {
match self {
$($name::$variant(i) => i.next_back(),)+
}
}
fn nth_back(&mut self, n: usize) -> Option<Self::Item> {
match self {
$($name::$variant(i) => i.nth_back(n),)+
}
}
fn rfind<Pre>(&mut self, predicate: Pre) -> Option<Self::Item>
where
Pre: FnMut(&Self::Item) -> bool,
{
match self {
$($name::$variant(i) => i.rfind(predicate),)+
}
}
}
pin_project! {
#[project = $fut_proj]
pub enum $fut_name<$($ty),+> {
$($variant { #[pin] inner: $ty },)+
}
}
impl<$($ty),+> Future for $fut_name<$($ty),+>
where
$($ty: Future,)+
{
type Output = $name<$($ty::Output),+>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.project();
match this {
$($fut_proj::$variant { inner } => match inner.poll(cx) {
Poll::Pending => Poll::Pending,
Poll::Ready(inner) => Poll::Ready($name::$variant(inner)),
},)+
}
}
}
}
}
impl<Item, A, B> Iterator for Either<A, B>
where
A: Iterator<Item = Item>,
B: Iterator<Item = Item>,
{
type Item = Item;
tuples!(Either + EitherFuture + EitherFutureProj {
Left(A) => (Right) + <A1, B>,
Right(B) => (Left) + <A, B1>,
});
fn next(&mut self) -> Option<Self::Item> {
impl<A, B> Either<A, B> {
pub fn swap(self) -> Either<B, A> {
match self {
Either::Left(i) => i.next(),
Either::Right(i) => i.next(),
Either::Left(a) => Either::Right(a),
Either::Right(b) => Either::Left(b),
}
}
}
impl<A, B> From<Result<A, B>> for Either<A, B> {
fn from(value: Result<A, B>) -> Self {
match value {
Ok(left) => Either::Left(left),
Err(right) => Either::Right(right),
}
}
}
@@ -90,6 +546,20 @@ impl<T, E> EitherOr for Result<T, E> {
}
}
impl<A, B> EitherOr for Either<A, B> {
type Left = A;
type Right = B;
#[inline]
fn either_or<FA, A1, FB, B1>(self, a: FA, b: FB) -> Either<A1, B1>
where
FA: FnOnce(<Self as EitherOr>::Left) -> A1,
FB: FnOnce(<Self as EitherOr>::Right) -> B1,
{
self.map(a, b)
}
}
#[test]
fn test_either_or() {
let right = false.either_or(|_| 'a', |_| 12);
@@ -110,109 +580,177 @@ fn test_either_or() {
let result: Result<i32, _> = Err("12");
let right = result.either_or(|a| a, |b| b.chars().next());
assert!(matches!(right, Either::Right(Some('1'))));
let either = Either::<i32, char>::Left(12);
let left = either.either_or(|a| a, |b| b);
assert!(matches!(left, Either::Left(12)));
let either = Either::<i32, char>::Right('a');
let right = either.either_or(|a| a, |b| b);
assert!(matches!(right, Either::Right('a')));
}
pin_project! {
#[project = EitherFutureProj]
pub enum EitherFuture<A, B> {
Left { #[pin] inner: A },
Right { #[pin] inner: B },
}
}
impl<A, B> Future for EitherFuture<A, B>
where
A: Future,
B: Future,
{
type Output = Either<A::Output, B::Output>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.project();
match this {
EitherFutureProj::Left { inner } => match inner.poll(cx) {
Poll::Pending => Poll::Pending,
Poll::Ready(inner) => Poll::Ready(Either::Left(inner)),
},
EitherFutureProj::Right { inner } => match inner.poll(cx) {
Poll::Pending => Poll::Pending,
Poll::Ready(inner) => Poll::Ready(Either::Right(inner)),
},
}
}
}
macro_rules! tuples {
($name:ident + $fut_name:ident + $fut_proj:ident => $($ty:ident),*) => {
#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
pub enum $name<$($ty,)*> {
$($ty ($ty),)*
}
impl<$($ty,)*> Display for $name<$($ty,)*>
where
$($ty: Display,)*
{
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
$($name::$ty(this) => this.fmt(f),)*
}
}
}
impl<Item, $($ty,)*> Iterator for $name<$($ty,)*>
where
$($ty: Iterator<Item = Item>,)*
{
type Item = Item;
fn next(&mut self) -> Option<Self::Item> {
match self {
$($name::$ty(i) => i.next(),)*
}
}
}
pin_project! {
#[project = $fut_proj]
pub enum $fut_name<$($ty,)*> {
$($ty { #[pin] inner: $ty },)*
}
}
impl<$($ty,)*> Future for $fut_name<$($ty,)*>
where
$($ty: Future,)*
{
type Output = $name<$($ty::Output,)*>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.project();
match this {
$($fut_proj::$ty { inner } => match inner.poll(cx) {
Poll::Pending => Poll::Pending,
Poll::Ready(inner) => Poll::Ready($name::$ty(inner)),
},)*
}
}
}
}
}
tuples!(EitherOf3 + EitherOf3Future + EitherOf3FutureProj => A, B, C);
tuples!(EitherOf4 + EitherOf4Future + EitherOf4FutureProj => A, B, C, D);
tuples!(EitherOf5 + EitherOf5Future + EitherOf5FutureProj => A, B, C, D, E);
tuples!(EitherOf6 + EitherOf6Future + EitherOf6FutureProj => A, B, C, D, E, F);
tuples!(EitherOf7 + EitherOf7Future + EitherOf7FutureProj => A, B, C, D, E, F, G);
tuples!(EitherOf8 + EitherOf8Future + EitherOf8FutureProj => A, B, C, D, E, F, G, H);
tuples!(EitherOf9 + EitherOf9Future + EitherOf9FutureProj => A, B, C, D, E, F, G, H, I);
tuples!(EitherOf10 + EitherOf10Future + EitherOf10FutureProj => A, B, C, D, E, F, G, H, I, J);
tuples!(EitherOf11 + EitherOf11Future + EitherOf11FutureProj => A, B, C, D, E, F, G, H, I, J, K);
tuples!(EitherOf12 + EitherOf12Future + EitherOf12FutureProj => A, B, C, D, E, F, G, H, I, J, K, L);
tuples!(EitherOf13 + EitherOf13Future + EitherOf13FutureProj => A, B, C, D, E, F, G, H, I, J, K, L, M);
tuples!(EitherOf14 + EitherOf14Future + EitherOf14FutureProj => A, B, C, D, E, F, G, H, I, J, K, L, M, N);
tuples!(EitherOf15 + EitherOf15Future + EitherOf15FutureProj => A, B, C, D, E, F, G, H, I, J, K, L, M, N, O);
tuples!(EitherOf16 + EitherOf16Future + EitherOf16FutureProj => A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P);
tuples!(EitherOf3 + EitherOf3Future + EitherOf3FutureProj {
A => (B, C) + <A1, B, C>,
B => (A, C) + <A, B1, C>,
C => (A, B) + <A, B, C1>,
});
tuples!(EitherOf4 + EitherOf4Future + EitherOf4FutureProj {
A => (B, C, D) + <A1, B, C, D>,
B => (A, C, D) + <A, B1, C, D>,
C => (A, B, D) + <A, B, C1, D>,
D => (A, B, C) + <A, B, C, D1>,
});
tuples!(EitherOf5 + EitherOf5Future + EitherOf5FutureProj {
A => (B, C, D, E) + <A1, B, C, D, E>,
B => (A, C, D, E) + <A, B1, C, D, E>,
C => (A, B, D, E) + <A, B, C1, D, E>,
D => (A, B, C, E) + <A, B, C, D1, E>,
E => (A, B, C, D) + <A, B, C, D, E1>,
});
tuples!(EitherOf6 + EitherOf6Future + EitherOf6FutureProj {
A => (B, C, D, E, F) + <A1, B, C, D, E, F>,
B => (A, C, D, E, F) + <A, B1, C, D, E, F>,
C => (A, B, D, E, F) + <A, B, C1, D, E, F>,
D => (A, B, C, E, F) + <A, B, C, D1, E, F>,
E => (A, B, C, D, F) + <A, B, C, D, E1, F>,
F => (A, B, C, D, E) + <A, B, C, D, E, F1>,
});
tuples!(EitherOf7 + EitherOf7Future + EitherOf7FutureProj {
A => (B, C, D, E, F, G) + <A1, B, C, D, E, F, G>,
B => (A, C, D, E, F, G) + <A, B1, C, D, E, F, G>,
C => (A, B, D, E, F, G) + <A, B, C1, D, E, F, G>,
D => (A, B, C, E, F, G) + <A, B, C, D1, E, F, G>,
E => (A, B, C, D, F, G) + <A, B, C, D, E1, F, G>,
F => (A, B, C, D, E, G) + <A, B, C, D, E, F1, G>,
G => (A, B, C, D, E, F) + <A, B, C, D, E, F, G1>,
});
tuples!(EitherOf8 + EitherOf8Future + EitherOf8FutureProj {
A => (B, C, D, E, F, G, H) + <A1, B, C, D, E, F, G, H>,
B => (A, C, D, E, F, G, H) + <A, B1, C, D, E, F, G, H>,
C => (A, B, D, E, F, G, H) + <A, B, C1, D, E, F, G, H>,
D => (A, B, C, E, F, G, H) + <A, B, C, D1, E, F, G, H>,
E => (A, B, C, D, F, G, H) + <A, B, C, D, E1, F, G, H>,
F => (A, B, C, D, E, G, H) + <A, B, C, D, E, F1, G, H>,
G => (A, B, C, D, E, F, H) + <A, B, C, D, E, F, G1, H>,
H => (A, B, C, D, E, F, G) + <A, B, C, D, E, F, G, H1>,
});
tuples!(EitherOf9 + EitherOf9Future + EitherOf9FutureProj {
A => (B, C, D, E, F, G, H, I) + <A1, B, C, D, E, F, G, H, I>,
B => (A, C, D, E, F, G, H, I) + <A, B1, C, D, E, F, G, H, I>,
C => (A, B, D, E, F, G, H, I) + <A, B, C1, D, E, F, G, H, I>,
D => (A, B, C, E, F, G, H, I) + <A, B, C, D1, E, F, G, H, I>,
E => (A, B, C, D, F, G, H, I) + <A, B, C, D, E1, F, G, H, I>,
F => (A, B, C, D, E, G, H, I) + <A, B, C, D, E, F1, G, H, I>,
G => (A, B, C, D, E, F, H, I) + <A, B, C, D, E, F, G1, H, I>,
H => (A, B, C, D, E, F, G, I) + <A, B, C, D, E, F, G, H1, I>,
I => (A, B, C, D, E, F, G, H) + <A, B, C, D, E, F, G, H, I1>,
});
tuples!(EitherOf10 + EitherOf10Future + EitherOf10FutureProj {
A => (B, C, D, E, F, G, H, I, J) + <A1, B, C, D, E, F, G, H, I, J>,
B => (A, C, D, E, F, G, H, I, J) + <A, B1, C, D, E, F, G, H, I, J>,
C => (A, B, D, E, F, G, H, I, J) + <A, B, C1, D, E, F, G, H, I, J>,
D => (A, B, C, E, F, G, H, I, J) + <A, B, C, D1, E, F, G, H, I, J>,
E => (A, B, C, D, F, G, H, I, J) + <A, B, C, D, E1, F, G, H, I, J>,
F => (A, B, C, D, E, G, H, I, J) + <A, B, C, D, E, F1, G, H, I, J>,
G => (A, B, C, D, E, F, H, I, J) + <A, B, C, D, E, F, G1, H, I, J>,
H => (A, B, C, D, E, F, G, I, J) + <A, B, C, D, E, F, G, H1, I, J>,
I => (A, B, C, D, E, F, G, H, J) + <A, B, C, D, E, F, G, H, I1, J>,
J => (A, B, C, D, E, F, G, H, I) + <A, B, C, D, E, F, G, H, I, J1>,
});
tuples!(EitherOf11 + EitherOf11Future + EitherOf11FutureProj {
A => (B, C, D, E, F, G, H, I, J, K) + <A1, B, C, D, E, F, G, H, I, J, K>,
B => (A, C, D, E, F, G, H, I, J, K) + <A, B1, C, D, E, F, G, H, I, J, K>,
C => (A, B, D, E, F, G, H, I, J, K) + <A, B, C1, D, E, F, G, H, I, J, K>,
D => (A, B, C, E, F, G, H, I, J, K) + <A, B, C, D1, E, F, G, H, I, J, K>,
E => (A, B, C, D, F, G, H, I, J, K) + <A, B, C, D, E1, F, G, H, I, J, K>,
F => (A, B, C, D, E, G, H, I, J, K) + <A, B, C, D, E, F1, G, H, I, J, K>,
G => (A, B, C, D, E, F, H, I, J, K) + <A, B, C, D, E, F, G1, H, I, J, K>,
H => (A, B, C, D, E, F, G, I, J, K) + <A, B, C, D, E, F, G, H1, I, J, K>,
I => (A, B, C, D, E, F, G, H, J, K) + <A, B, C, D, E, F, G, H, I1, J, K>,
J => (A, B, C, D, E, F, G, H, I, K) + <A, B, C, D, E, F, G, H, I, J1, K>,
K => (A, B, C, D, E, F, G, H, I, J) + <A, B, C, D, E, F, G, H, I, J, K1>,
});
tuples!(EitherOf12 + EitherOf12Future + EitherOf12FutureProj {
A => (B, C, D, E, F, G, H, I, J, K, L) + <A1, B, C, D, E, F, G, H, I, J, K, L>,
B => (A, C, D, E, F, G, H, I, J, K, L) + <A, B1, C, D, E, F, G, H, I, J, K, L>,
C => (A, B, D, E, F, G, H, I, J, K, L) + <A, B, C1, D, E, F, G, H, I, J, K, L>,
D => (A, B, C, E, F, G, H, I, J, K, L) + <A, B, C, D1, E, F, G, H, I, J, K, L>,
E => (A, B, C, D, F, G, H, I, J, K, L) + <A, B, C, D, E1, F, G, H, I, J, K, L>,
F => (A, B, C, D, E, G, H, I, J, K, L) + <A, B, C, D, E, F1, G, H, I, J, K, L>,
G => (A, B, C, D, E, F, H, I, J, K, L) + <A, B, C, D, E, F, G1, H, I, J, K, L>,
H => (A, B, C, D, E, F, G, I, J, K, L) + <A, B, C, D, E, F, G, H1, I, J, K, L>,
I => (A, B, C, D, E, F, G, H, J, K, L) + <A, B, C, D, E, F, G, H, I1, J, K, L>,
J => (A, B, C, D, E, F, G, H, I, K, L) + <A, B, C, D, E, F, G, H, I, J1, K, L>,
K => (A, B, C, D, E, F, G, H, I, J, L) + <A, B, C, D, E, F, G, H, I, J, K1, L>,
L => (A, B, C, D, E, F, G, H, I, J, K) + <A, B, C, D, E, F, G, H, I, J, K, L1>,
});
tuples!(EitherOf13 + EitherOf13Future + EitherOf13FutureProj {
A => (B, C, D, E, F, G, H, I, J, K, L, M) + <A1, B, C, D, E, F, G, H, I, J, K, L, M>,
B => (A, C, D, E, F, G, H, I, J, K, L, M) + <A, B1, C, D, E, F, G, H, I, J, K, L, M>,
C => (A, B, D, E, F, G, H, I, J, K, L, M) + <A, B, C1, D, E, F, G, H, I, J, K, L, M>,
D => (A, B, C, E, F, G, H, I, J, K, L, M) + <A, B, C, D1, E, F, G, H, I, J, K, L, M>,
E => (A, B, C, D, F, G, H, I, J, K, L, M) + <A, B, C, D, E1, F, G, H, I, J, K, L, M>,
F => (A, B, C, D, E, G, H, I, J, K, L, M) + <A, B, C, D, E, F1, G, H, I, J, K, L, M>,
G => (A, B, C, D, E, F, H, I, J, K, L, M) + <A, B, C, D, E, F, G1, H, I, J, K, L, M>,
H => (A, B, C, D, E, F, G, I, J, K, L, M) + <A, B, C, D, E, F, G, H1, I, J, K, L, M>,
I => (A, B, C, D, E, F, G, H, J, K, L, M) + <A, B, C, D, E, F, G, H, I1, J, K, L, M>,
J => (A, B, C, D, E, F, G, H, I, K, L, M) + <A, B, C, D, E, F, G, H, I, J1, K, L, M>,
K => (A, B, C, D, E, F, G, H, I, J, L, M) + <A, B, C, D, E, F, G, H, I, J, K1, L, M>,
L => (A, B, C, D, E, F, G, H, I, J, K, M) + <A, B, C, D, E, F, G, H, I, J, K, L1, M>,
M => (A, B, C, D, E, F, G, H, I, J, K, L) + <A, B, C, D, E, F, G, H, I, J, K, L, M1>,
});
tuples!(EitherOf14 + EitherOf14Future + EitherOf14FutureProj {
A => (B, C, D, E, F, G, H, I, J, K, L, M, N) + <A1, B, C, D, E, F, G, H, I, J, K, L, M, N>,
B => (A, C, D, E, F, G, H, I, J, K, L, M, N) + <A, B1, C, D, E, F, G, H, I, J, K, L, M, N>,
C => (A, B, D, E, F, G, H, I, J, K, L, M, N) + <A, B, C1, D, E, F, G, H, I, J, K, L, M, N>,
D => (A, B, C, E, F, G, H, I, J, K, L, M, N) + <A, B, C, D1, E, F, G, H, I, J, K, L, M, N>,
E => (A, B, C, D, F, G, H, I, J, K, L, M, N) + <A, B, C, D, E1, F, G, H, I, J, K, L, M, N>,
F => (A, B, C, D, E, G, H, I, J, K, L, M, N) + <A, B, C, D, E, F1, G, H, I, J, K, L, M, N>,
G => (A, B, C, D, E, F, H, I, J, K, L, M, N) + <A, B, C, D, E, F, G1, H, I, J, K, L, M, N>,
H => (A, B, C, D, E, F, G, I, J, K, L, M, N) + <A, B, C, D, E, F, G, H1, I, J, K, L, M, N>,
I => (A, B, C, D, E, F, G, H, J, K, L, M, N) + <A, B, C, D, E, F, G, H, I1, J, K, L, M, N>,
J => (A, B, C, D, E, F, G, H, I, K, L, M, N) + <A, B, C, D, E, F, G, H, I, J1, K, L, M, N>,
K => (A, B, C, D, E, F, G, H, I, J, L, M, N) + <A, B, C, D, E, F, G, H, I, J, K1, L, M, N>,
L => (A, B, C, D, E, F, G, H, I, J, K, M, N) + <A, B, C, D, E, F, G, H, I, J, K, L1, M, N>,
M => (A, B, C, D, E, F, G, H, I, J, K, L, N) + <A, B, C, D, E, F, G, H, I, J, K, L, M1, N>,
N => (A, B, C, D, E, F, G, H, I, J, K, L, M) + <A, B, C, D, E, F, G, H, I, J, K, L, M, N1>,
});
tuples!(EitherOf15 + EitherOf15Future + EitherOf15FutureProj {
A => (B, C, D, E, F, G, H, I, J, K, L, M, N, O) + <A1, B, C, D, E, F, G, H, I, J, K, L, M, N, O>,
B => (A, C, D, E, F, G, H, I, J, K, L, M, N, O) + <A, B1, C, D, E, F, G, H, I, J, K, L, M, N, O>,
C => (A, B, D, E, F, G, H, I, J, K, L, M, N, O) + <A, B, C1, D, E, F, G, H, I, J, K, L, M, N, O>,
D => (A, B, C, E, F, G, H, I, J, K, L, M, N, O) + <A, B, C, D1, E, F, G, H, I, J, K, L, M, N, O>,
E => (A, B, C, D, F, G, H, I, J, K, L, M, N, O) + <A, B, C, D, E1, F, G, H, I, J, K, L, M, N, O>,
F => (A, B, C, D, E, G, H, I, J, K, L, M, N, O) + <A, B, C, D, E, F1, G, H, I, J, K, L, M, N, O>,
G => (A, B, C, D, E, F, H, I, J, K, L, M, N, O) + <A, B, C, D, E, F, G1, H, I, J, K, L, M, N, O>,
H => (A, B, C, D, E, F, G, I, J, K, L, M, N, O) + <A, B, C, D, E, F, G, H1, I, J, K, L, M, N, O>,
I => (A, B, C, D, E, F, G, H, J, K, L, M, N, O) + <A, B, C, D, E, F, G, H, I1, J, K, L, M, N, O>,
J => (A, B, C, D, E, F, G, H, I, K, L, M, N, O) + <A, B, C, D, E, F, G, H, I, J1, K, L, M, N, O>,
K => (A, B, C, D, E, F, G, H, I, J, L, M, N, O) + <A, B, C, D, E, F, G, H, I, J, K1, L, M, N, O>,
L => (A, B, C, D, E, F, G, H, I, J, K, M, N, O) + <A, B, C, D, E, F, G, H, I, J, K, L1, M, N, O>,
M => (A, B, C, D, E, F, G, H, I, J, K, L, N, O) + <A, B, C, D, E, F, G, H, I, J, K, L, M1, N, O>,
N => (A, B, C, D, E, F, G, H, I, J, K, L, M, O) + <A, B, C, D, E, F, G, H, I, J, K, L, M, N1, O>,
O => (A, B, C, D, E, F, G, H, I, J, K, L, M, N) + <A, B, C, D, E, F, G, H, I, J, K, L, M, N, O1>,
});
tuples!(EitherOf16 + EitherOf16Future + EitherOf16FutureProj {
A => (B, C, D, E, F, G, H, I, J, K, L, M, N, O, P) + <A1, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P>,
B => (A, C, D, E, F, G, H, I, J, K, L, M, N, O, P) + <A, B1, C, D, E, F, G, H, I, J, K, L, M, N, O, P>,
C => (A, B, D, E, F, G, H, I, J, K, L, M, N, O, P) + <A, B, C1, D, E, F, G, H, I, J, K, L, M, N, O, P>,
D => (A, B, C, E, F, G, H, I, J, K, L, M, N, O, P) + <A, B, C, D1, E, F, G, H, I, J, K, L, M, N, O, P>,
E => (A, B, C, D, F, G, H, I, J, K, L, M, N, O, P) + <A, B, C, D, E1, F, G, H, I, J, K, L, M, N, O, P>,
F => (A, B, C, D, E, G, H, I, J, K, L, M, N, O, P) + <A, B, C, D, E, F1, G, H, I, J, K, L, M, N, O, P>,
G => (A, B, C, D, E, F, H, I, J, K, L, M, N, O, P) + <A, B, C, D, E, F, G1, H, I, J, K, L, M, N, O, P>,
H => (A, B, C, D, E, F, G, I, J, K, L, M, N, O, P) + <A, B, C, D, E, F, G, H1, I, J, K, L, M, N, O, P>,
I => (A, B, C, D, E, F, G, H, J, K, L, M, N, O, P) + <A, B, C, D, E, F, G, H, I1, J, K, L, M, N, O, P>,
J => (A, B, C, D, E, F, G, H, I, K, L, M, N, O, P) + <A, B, C, D, E, F, G, H, I, J1, K, L, M, N, O, P>,
K => (A, B, C, D, E, F, G, H, I, J, L, M, N, O, P) + <A, B, C, D, E, F, G, H, I, J, K1, L, M, N, O, P>,
L => (A, B, C, D, E, F, G, H, I, J, K, M, N, O, P) + <A, B, C, D, E, F, G, H, I, J, K, L1, M, N, O, P>,
M => (A, B, C, D, E, F, G, H, I, J, K, L, N, O, P) + <A, B, C, D, E, F, G, H, I, J, K, L, M1, N, O, P>,
N => (A, B, C, D, E, F, G, H, I, J, K, L, M, O, P) + <A, B, C, D, E, F, G, H, I, J, K, L, M, N1, O, P>,
O => (A, B, C, D, E, F, G, H, I, J, K, L, M, N, P) + <A, B, C, D, E, F, G, H, I, J, K, L, M, N, O1, P>,
P => (A, B, C, D, E, F, G, H, I, J, K, L, M, N, O) + <A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P1>,
});
/// Matches over the first expression and returns an either ([`Either`], [`EitherOf3`], ... [`EitherOf8`])
/// composed of the values returned by the match arms.
@@ -303,56 +841,67 @@ macro_rules! either {
}; // if you need more eithers feel free to open a PR ;-)
}
// compile time test
#[test]
fn either_macro() {
let _: Either<&str, f64> = either!(12,
12 => "12",
_ => 0.0,
);
let _: EitherOf3<&str, f64, i32> = either!(12,
12 => "12",
13 => 0.0,
_ => 12,
);
let _: EitherOf4<&str, f64, char, i32> = either!(12,
12 => "12",
13 => 0.0,
14 => ' ',
_ => 12,
);
let _: EitherOf5<&str, f64, char, f32, i32> = either!(12,
12 => "12",
13 => 0.0,
14 => ' ',
15 => 0.0f32,
_ => 12,
);
let _: EitherOf6<&str, f64, char, f32, u8, i32> = either!(12,
12 => "12",
13 => 0.0,
14 => ' ',
15 => 0.0f32,
16 => 24u8,
_ => 12,
);
let _: EitherOf7<&str, f64, char, f32, u8, i8, i32> = either!(12,
12 => "12",
13 => 0.0,
14 => ' ',
15 => 0.0f32,
16 => 24u8,
17 => 2i8,
_ => 12,
);
let _: EitherOf8<&str, f64, char, f32, u8, i8, u32, i32> = either!(12,
12 => "12",
13 => 0.0,
14 => ' ',
15 => 0.0f32,
16 => 24u8,
17 => 2i8,
18 => 42u32,
_ => 12,
);
#[cfg(test)]
mod tests {
use super::*;
// compile time test
#[test]
fn either_macro() {
let _: Either<&str, f64> = either!(12,
12 => "12",
_ => 0.0,
);
let _: EitherOf3<&str, f64, i32> = either!(12,
12 => "12",
13 => 0.0,
_ => 12,
);
let _: EitherOf4<&str, f64, char, i32> = either!(12,
12 => "12",
13 => 0.0,
14 => ' ',
_ => 12,
);
let _: EitherOf5<&str, f64, char, f32, i32> = either!(12,
12 => "12",
13 => 0.0,
14 => ' ',
15 => 0.0f32,
_ => 12,
);
let _: EitherOf6<&str, f64, char, f32, u8, i32> = either!(12,
12 => "12",
13 => 0.0,
14 => ' ',
15 => 0.0f32,
16 => 24u8,
_ => 12,
);
let _: EitherOf7<&str, f64, char, f32, u8, i8, i32> = either!(12,
12 => "12",
13 => 0.0,
14 => ' ',
15 => 0.0f32,
16 => 24u8,
17 => 2i8,
_ => 12,
);
let _: EitherOf8<&str, f64, char, f32, u8, i8, u32, i32> = either!(12,
12 => "12",
13 => 0.0,
14 => ' ',
15 => 0.0f32,
16 => 24u8,
17 => 2i8,
18 => 42u32,
_ => 12,
);
}
#[test]
#[should_panic]
fn unwrap_wrong_either() {
Either::<i32, &str>::Left(0).unwrap_right();
}
}

View File

@@ -7,7 +7,7 @@ edition = "2021"
crate-type = ["cdylib", "rlib"]
[dependencies]
axum = { version = "0.7.5", optional = true }
axum = { version = "0.8.1", optional = true }
console_error_panic_hook = "0.1.7"
console_log = "1.0"
gloo-utils = "0.2.0"
@@ -20,18 +20,27 @@ leptos_axum = { path = "../../integrations/axum", optional = true }
leptos_router = { path = "../../router" }
serde = { version = "1.0", features = ["derive"] }
thiserror = "1.0"
tokio = { version = "1.39", features = [ "rt-multi-thread", "macros", "time" ], optional = true }
tokio = { version = "1.39", features = [
"rt-multi-thread",
"macros",
"time",
], optional = true }
tower = { version = "0.4.13", optional = true }
tower-http = { version = "0.5.2", features = ["fs"], optional = true }
wasm-bindgen = "0.2.92"
web-sys = { version = "0.3.69", features = [ "AddEventListenerOptions", "Document", "Element", "Event", "EventListener", "EventTarget", "Performance", "Window" ], optional = true }
web-sys = { version = "0.3.69", features = [
"AddEventListenerOptions",
"Document",
"Element",
"Event",
"EventListener",
"EventTarget",
"Performance",
"Window",
], optional = true }
[features]
hydrate = [
"leptos/hydrate",
"dep:js-sys",
"dep:web-sys",
]
hydrate = ["leptos/hydrate", "dep:js-sys", "dep:web-sys"]
ssr = [
"dep:axum",
"dep:http-body-util",

View File

@@ -19,7 +19,7 @@ async fn clear() {
// note that we start at the initial value of 10
let _dispose = mount_to(
test_wrapper.clone().unchecked_into(),
|| view! { <SimpleCounter initial_value=10 step=1/> },
|| view! { <SimpleCounter initial_value=10 step=1 /> },
);
// now we extract the buttons by iterating over the DOM
@@ -59,9 +59,9 @@ async fn clear() {
// .into_view() here is just a convenient way of specifying "use the regular DOM renderer"
.into_view()
// views are lazy -- they describe a DOM tree but don't create it yet
// calling .build() will actually build the DOM elements
.build()
// .build() returned an ElementState, which is a smart pointer for
// calling .build(None) will actually build the DOM elements
.build(None)
// .build(None) returned an ElementState, which is a smart pointer for
// a DOM element. So we can still just call .outer_html(), which access the outerHTML on
// the actual DOM element
.outer_html()
@@ -87,7 +87,7 @@ async fn inc() {
let _dispose = mount_to(
test_wrapper.clone().unchecked_into(),
|| view! { <SimpleCounter initial_value=0 step=1/> },
|| view! { <SimpleCounter initial_value=0 step=1 /> },
);
// You can do testing with vanilla DOM operations
@@ -150,7 +150,7 @@ async fn inc() {
}
}
.into_view()
.build()
.build(None)
.outer_html()
);
@@ -173,7 +173,7 @@ async fn inc() {
}
}
.into_view()
.build()
.build(None)
.outer_html()
);
}

View File

@@ -13,7 +13,7 @@ leptos_axum = { path = "../../integrations/axum", optional = true }
leptos_meta = { path = "../../meta" }
leptos_router = { path = "../../router" }
serde = { version = "1.0", features = ["derive"] }
axum = { version = "0.7.5", optional = true }
axum = { version = "0.8.1", optional = true }
tower = { version = "0.4.13", optional = true }
tower-http = { version = "0.5.2", features = ["fs"], optional = true }
tokio = { version = "1.39", features = ["full"], optional = true }

View File

@@ -45,7 +45,7 @@ async fn main() {
// build our application with a route
let app = Router::new()
.route("/special/:id", get(custom_handler))
.route("/special/{id}", get(custom_handler))
.leptos_routes(&leptos_options, routes, {
let leptos_options = leptos_options.clone();
move || shell(leptos_options.clone())

View File

@@ -20,7 +20,7 @@ serde = { version = "1.0", features = ["derive"] }
tracing = "0.1.40"
gloo-net = { version = "0.6.0", features = ["http"] }
reqwest = { version = "0.12.5", features = ["json"] }
axum = { version = "0.7.5", optional = true }
axum = { version = "0.8.1", optional = true }
tower = { version = "0.4.13", optional = true }
tower-http = { version = "0.5.2", features = ["fs"], optional = true }
tokio = { version = "1.39", features = ["full"], optional = true }

View File

@@ -20,7 +20,7 @@ serde = { version = "1.0", features = ["derive"] }
tracing = "0.1.40"
gloo-net = { version = "0.6.0", features = ["http"] }
reqwest = { version = "0.12.5", features = ["json"] }
axum = { version = "0.7.5", optional = true, features = ["http2"] }
axum = { version = "0.8.1", optional = true, features = ["http2"] }
tower = { version = "0.4.13", optional = true }
tower-http = { version = "0.5.2", features = [
"fs",

View File

@@ -23,7 +23,7 @@ serde = { version = "1.0", features = ["derive"] }
tracing = "0.1.40"
gloo-net = { version = "0.6.0", features = ["http"] }
reqwest = { version = "0.12.5", features = ["json"] }
axum = { version = "0.7.5", default-features = false, optional = true }
axum = { version = "0.8.1", default-features = false, optional = true }
tower = { version = "0.4.13", optional = true }
http = { version = "1.1", optional = true }
web-sys = { version = "0.3.70", features = [

View File

@@ -10,15 +10,12 @@ crate-type = ["cdylib", "rlib"]
console_error_panic_hook = "0.1.7"
futures = "0.3.30"
http = "1.1"
leptos = { path = "../../leptos", features = [
"tracing",
"islands",
] }
leptos = { path = "../../leptos", features = ["tracing", "islands"] }
server_fn = { path = "../../server_fn", features = ["serde-lite"] }
leptos_axum = { path = "../../integrations/axum", optional = true }
log = "0.4.22"
serde = { version = "1.0", features = ["derive"] }
axum = { version = "0.7.5", optional = true }
axum = { version = "0.8.1", optional = true }
tower = { version = "0.4.13", optional = true }
tower-http = { version = "0.5.2", features = ["fs"], optional = true }
tokio = { version = "1.39", features = ["full"], optional = true }

View File

@@ -10,10 +10,7 @@ crate-type = ["cdylib", "rlib"]
console_error_panic_hook = "0.1.7"
futures = "0.3.30"
http = "1.1"
leptos = { path = "../../leptos", features = [
"tracing",
"islands",
] }
leptos = { path = "../../leptos", features = ["tracing", "islands"] }
leptos_router = { path = "../../router" }
server_fn = { path = "../../server_fn", features = ["serde-lite"] }
leptos_axum = { path = "../../integrations/axum", features = [
@@ -21,7 +18,7 @@ leptos_axum = { path = "../../integrations/axum", features = [
], optional = true }
log = "0.4.22"
serde = { version = "1.0", features = ["derive"] }
axum = { version = "0.7.5", optional = true }
axum = { version = "0.8.1", optional = true }
tower = { version = "0.4.13", optional = true }
tower-http = { version = "0.5.2", features = ["fs"], optional = true }
tokio = { version = "1.39", features = ["full"], optional = true }

View File

@@ -21,7 +21,7 @@ server_fn = { path = "../../server_fn", features = [
log = "0.4.22"
simple_logger = "5.0"
serde = { version = "1.0", features = ["derive"] }
axum = { version = "0.7.5", optional = true }
axum = { version = "0.8.1", optional = true }
tower = { version = "0.4.13", optional = true }
tower-http = { version = "0.5.2", features = [
"fs",

View File

@@ -9,8 +9,9 @@ use server_fn::{
MultipartFormData, Postcard, Rkyv, SerdeLite, StreamingText,
TextStream,
},
error::{FromServerFnError, IntoAppError, ServerFnErrorErr},
request::{browser::BrowserRequest, ClientReq, Req},
response::{browser::BrowserResponse, ClientRes, Res},
response::{browser::BrowserResponse, ClientRes, TryRes},
};
use std::future::Future;
#[cfg(feature = "ssr")]
@@ -652,32 +653,72 @@ pub fn FileWatcher() -> impl IntoView {
/// implementations if you'd like. However, it's much lighter weight to use something like `strum`
/// simply to generate those trait implementations.
#[server]
pub async fn ascii_uppercase(
text: String,
) -> Result<String, ServerFnError<InvalidArgument>> {
pub async fn ascii_uppercase(text: String) -> Result<String, MyErrors> {
other_error()?;
Ok(ascii_uppercase_inner(text)?)
}
pub fn other_error() -> Result<(), String> {
Ok(())
}
pub fn ascii_uppercase_inner(text: String) -> Result<String, InvalidArgument> {
if text.len() < 5 {
Err(InvalidArgument::TooShort.into())
Err(InvalidArgument::TooShort)
} else if text.len() > 15 {
Err(InvalidArgument::TooLong.into())
Err(InvalidArgument::TooLong)
} else if text.is_ascii() {
Ok(text.to_ascii_uppercase())
} else {
Err(InvalidArgument::NotAscii.into())
Err(InvalidArgument::NotAscii)
}
}
#[server]
pub async fn ascii_uppercase_classic(
text: String,
) -> Result<String, ServerFnError<InvalidArgument>> {
Ok(ascii_uppercase_inner(text)?)
}
// The EnumString and Display derive macros are provided by strum
#[derive(Debug, Clone, EnumString, Display)]
#[derive(Debug, Clone, Display, EnumString, Serialize, Deserialize)]
pub enum InvalidArgument {
TooShort,
TooLong,
NotAscii,
}
#[derive(Debug, Clone, Display, Serialize, Deserialize)]
pub enum MyErrors {
InvalidArgument(InvalidArgument),
ServerFnError(ServerFnErrorErr),
Other(String),
}
impl From<InvalidArgument> for MyErrors {
fn from(value: InvalidArgument) -> Self {
MyErrors::InvalidArgument(value)
}
}
impl From<String> for MyErrors {
fn from(value: String) -> Self {
MyErrors::Other(value)
}
}
impl FromServerFnError for MyErrors {
fn from_server_fn_error(value: ServerFnErrorErr) -> Self {
MyErrors::ServerFnError(value)
}
}
#[component]
pub fn CustomErrorTypes() -> impl IntoView {
let input_ref = NodeRef::<Input>::new();
let (result, set_result) = signal(None);
let (result_classic, set_result_classic) = signal(None);
view! {
<h3>Using custom error types</h3>
@@ -692,14 +733,17 @@ pub fn CustomErrorTypes() -> impl IntoView {
<button on:click=move |_| {
let value = input_ref.get().unwrap().value();
spawn_local(async move {
let data = ascii_uppercase(value).await;
let data = ascii_uppercase(value.clone()).await;
let data_classic = ascii_uppercase_classic(value).await;
set_result.set(Some(data));
set_result_classic.set(Some(data_classic));
});
}>
"Submit"
</button>
<p>{move || format!("{:?}", result.get())}</p>
<p>{move || format!("{:?}", result_classic.get())}</p>
}
}
@@ -726,14 +770,12 @@ impl<T, Request, Err> IntoReq<Toml, Request, Err> for TomlEncoded<T>
where
Request: ClientReq<Err>,
T: Serialize,
Err: FromServerFnError,
{
fn into_req(
self,
path: &str,
accepts: &str,
) -> Result<Request, ServerFnError<Err>> {
let data = toml::to_string(&self.0)
.map_err(|e| ServerFnError::Serialization(e.to_string()))?;
fn into_req(self, path: &str, accepts: &str) -> Result<Request, Err> {
let data = toml::to_string(&self.0).map_err(|e| {
ServerFnErrorErr::Serialization(e.to_string()).into_app_error()
})?;
Request::try_new_post(path, Toml::CONTENT_TYPE, accepts, data)
}
}
@@ -742,23 +784,26 @@ impl<T, Request, Err> FromReq<Toml, Request, Err> for TomlEncoded<T>
where
Request: Req<Err> + Send,
T: DeserializeOwned,
Err: FromServerFnError,
{
async fn from_req(req: Request) -> Result<Self, ServerFnError<Err>> {
async fn from_req(req: Request) -> Result<Self, Err> {
let string_data = req.try_into_string().await?;
toml::from_str::<T>(&string_data)
.map(TomlEncoded)
.map_err(|e| ServerFnError::Args(e.to_string()))
.map_err(|e| ServerFnErrorErr::Args(e.to_string()).into_app_error())
}
}
impl<T, Response, Err> IntoRes<Toml, Response, Err> for TomlEncoded<T>
where
Response: Res<Err>,
Response: TryRes<Err>,
T: Serialize + Send,
Err: FromServerFnError,
{
async fn into_res(self) -> Result<Response, ServerFnError<Err>> {
let data = toml::to_string(&self.0)
.map_err(|e| ServerFnError::Serialization(e.to_string()))?;
async fn into_res(self) -> Result<Response, Err> {
let data = toml::to_string(&self.0).map_err(|e| {
ServerFnErrorErr::Serialization(e.to_string()).into_app_error()
})?;
Response::try_from_string(Toml::CONTENT_TYPE, data)
}
}
@@ -767,12 +812,13 @@ impl<T, Response, Err> FromRes<Toml, Response, Err> for TomlEncoded<T>
where
Response: ClientRes<Err> + Send,
T: DeserializeOwned,
Err: FromServerFnError,
{
async fn from_res(res: Response) -> Result<Self, ServerFnError<Err>> {
async fn from_res(res: Response) -> Result<Self, Err> {
let data = res.try_into_string().await?;
toml::from_str(&data)
.map(TomlEncoded)
.map_err(|e| ServerFnError::Deserialization(e.to_string()))
toml::from_str(&data).map(TomlEncoded).map_err(|e| {
ServerFnErrorErr::Deserialization(e.to_string()).into_app_error()
})
}
}
@@ -835,7 +881,10 @@ pub fn CustomClientExample() -> impl IntoView {
pub struct CustomClient;
// Implement the `Client` trait for it.
impl<CustErr> Client<CustErr> for CustomClient {
impl<E> Client<E> for CustomClient
where
E: FromServerFnError,
{
// BrowserRequest and BrowserResponse are the defaults used by other server functions.
// They are wrappers for the underlying Web Fetch API types.
type Request = BrowserRequest;
@@ -844,8 +893,7 @@ pub fn CustomClientExample() -> impl IntoView {
// Our custom `send()` implementation does all the work.
fn send(
req: Self::Request,
) -> impl Future<Output = Result<Self::Response, ServerFnError<CustErr>>>
+ Send {
) -> impl Future<Output = Result<Self::Response, E>> + Send {
// BrowserRequest derefs to the underlying Request type from gloo-net,
// so we can get access to the headers here
let headers = req.headers();

View File

@@ -20,7 +20,7 @@ leptos_router = { path = "../../router" }
log = "0.4.22"
serde = { version = "1.0", features = ["derive"] }
thiserror = "1.0"
axum = { version = "0.7.5", optional = true }
axum = { version = "0.8.1", optional = true }
tower = { version = "0.4.13", optional = true }
tower-http = { version = "0.5.2", features = ["fs"], optional = true }
tokio = { version = "1.39", features = [

View File

@@ -18,7 +18,7 @@ leptos_router = { path = "../../router" }
log = "0.4.22"
serde = { version = "1.0", features = ["derive"] }
thiserror = "1.0"
axum = { version = "0.7.5", optional = true }
axum = { version = "0.8.1", optional = true }
tower = { version = "0.4.13", optional = true }
tower-http = { version = "0.5.2", features = ["fs"], optional = true }
tokio = { version = "1.39", features = [
@@ -45,7 +45,7 @@ ssr = [
"dep:leptos_axum",
"leptos_router/ssr",
"dep:notify",
"dep:http"
"dep:http",
]
[profile.release]

View File

@@ -7,7 +7,7 @@ edition = "2021"
crate-type = ["cdylib", "rlib"]
[dependencies]
axum = { version = "0.7.5", optional = true }
axum = { version = "0.8.1", optional = true }
console_error_panic_hook = "0.1.7"
leptos = { path = "../../leptos" }
leptos_meta = { path = "../../meta" }

View File

@@ -16,7 +16,7 @@ leptos_axum = { path = "../../integrations/axum", optional = true }
log = "0.4.22"
simple_logger = "5.0"
serde = { version = "1.0", features = ["derive"] }
axum = { version = "0.7.5", optional = true }
axum = { version = "0.8.1", optional = true }
tower = { version = "0.4.13", optional = true }
tower-http = { version = "0.5.2", features = ["fs"], optional = true }
tokio = { version = "1.39", features = ["full"], optional = true }

View File

@@ -1,4 +1,4 @@
use crate::todo::*;
#[cfg(feature = "ssr")]
use axum::{
body::Body,
extract::Path,
@@ -8,10 +8,9 @@ use axum::{
Router,
};
use leptos::prelude::*;
use leptos_axum::{generate_route_list, LeptosRoutes};
use todo_app_sqlite_axum::*;
//Define a handler to test extractor with state
#[cfg(feature = "ssr")]
async fn custom_handler(
Path(id): Path<String>,
req: Request<Body>,
@@ -20,14 +19,16 @@ async fn custom_handler(
move || {
provide_context(id.clone());
},
TodoApp,
todo::TodoApp,
);
handler(req).await.into_response()
}
#[cfg(feature = "ssr")]
#[tokio::main]
async fn main() {
use crate::todo::ssr::db;
use crate::todo::{ssr::db, *};
use leptos_axum::{generate_route_list, LeptosRoutes};
simple_logger::init_with_level(log::Level::Error)
.expect("couldn't initialize logging");
@@ -45,7 +46,7 @@ async fn main() {
// build our application with a route
let app = Router::new()
.route("/special/:id", get(custom_handler))
.route("/special/{id}", get(custom_handler))
.leptos_routes(&leptos_options, routes, {
let leptos_options = leptos_options.clone();
move || shell(leptos_options.clone())
@@ -61,3 +62,12 @@ async fn main() {
.await
.unwrap();
}
#[cfg(not(feature = "ssr"))]
pub fn main() {
use leptos::mount::mount_to_body;
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
mount_to_body(todo::TodoApp);
}

View File

@@ -15,7 +15,7 @@ leptos_meta = { path = "../../meta" }
leptos_router = { path = "../../router" }
leptos_integration_utils = { path = "../../integrations/utils", optional = true }
serde = { version = "1.0", features = ["derive"] }
axum = { version = "0.7.5", optional = true }
axum = { version = "0.8.1", optional = true }
tower = { version = "0.5.1", features = ["util"], optional = true }
tower-http = { version = "0.6.1", features = ["fs"], optional = true }
tokio = { version = "1.39", features = ["full"], optional = true }

View File

@@ -34,7 +34,7 @@ async fn main() {
// here, we're not actually doing server side rendering, so we set up a manual
// handler for the server fns
// this should include a get() handler if you have any GetUrl-based server fns
.route("/api/*fn_name", post(leptos_axum::handle_server_fns))
.route("/api/{*fn_name}", post(leptos_axum::handle_server_fns))
.fallback(file_or_index_handler)
.with_state(leptos_options);

View File

@@ -369,7 +369,6 @@ pub fn handle_server_fns_with_context(
// actually run the server fn
let mut res = ActixResponse(
service
.0
.run(ActixRequest::from((req, payload)))
.await
.take(),

View File

@@ -11,7 +11,7 @@ edition.workspace = true
[dependencies]
any_spawner = { workspace = true, features = ["tokio"] }
hydration_context = { workspace = true }
axum = { version = "0.7.9", default-features = false, features = [
axum = { version = "0.8.1", default-features = false, features = [
"matched-path",
] }
dashmap = "6"
@@ -30,7 +30,7 @@ tower-http = "0.6.2"
tracing = { version = "0.1.41", optional = true }
[dev-dependencies]
axum = "0.7.9"
axum = "0.8.1"
tokio = { version = "1.41", features = ["net", "rt-multi-thread"] }
[features]

View File

@@ -368,8 +368,6 @@ async fn handle_server_fns_inner(
additional_context: impl Fn() + 'static + Clone + Send,
req: Request<Body>,
) -> impl IntoResponse {
use server_fn::middleware::Service;
let method = req.method().clone();
let path = req.uri().path().to_string();
let (req, parts) = generate_request_and_parts(req);
@@ -484,7 +482,7 @@ pub type PinnedHtmlStream =
tracing::instrument(level = "trace", fields(error), skip_all)
)]
pub fn render_app_to_stream<IV>(
app_fn: impl Fn() -> IV + Clone + Send + 'static,
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
) -> impl Fn(
Request<Body>,
) -> Pin<Box<dyn Future<Output = Response<Body>> + Send + 'static>>
@@ -508,7 +506,7 @@ where
)]
pub fn render_route<S, IV>(
paths: Vec<AxumRouteListing>,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
) -> impl Fn(
State<S>,
Request<Body>,
@@ -572,7 +570,7 @@ where
tracing::instrument(level = "trace", fields(error), skip_all)
)]
pub fn render_app_to_stream_in_order<IV>(
app_fn: impl Fn() -> IV + Clone + Send + 'static,
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
) -> impl Fn(
Request<Body>,
) -> Pin<Box<dyn Future<Output = Response<Body>> + Send + 'static>>
@@ -625,13 +623,14 @@ where
tracing::instrument(level = "trace", fields(error), skip_all)
)]
pub fn render_app_to_stream_with_context<IV>(
additional_context: impl Fn() + 'static + Clone + Send,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
additional_context: impl Fn() + 'static + Clone + Send + Sync,
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
) -> impl Fn(
Request<Body>,
) -> Pin<Box<dyn Future<Output = Response<Body>> + Send + 'static>>
+ Clone
+ Send
+ Sync
+ 'static
where
IV: IntoView + 'static,
@@ -654,8 +653,8 @@ where
)]
pub fn render_route_with_context<S, IV>(
paths: Vec<AxumRouteListing>,
additional_context: impl Fn() + 'static + Clone + Send,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
additional_context: impl Fn() + 'static + Clone + Send + Sync,
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
) -> impl Fn(
State<S>,
Request<Body>,
@@ -756,14 +755,15 @@ where
tracing::instrument(level = "trace", fields(error), skip_all)
)]
pub fn render_app_to_stream_with_context_and_replace_blocks<IV>(
additional_context: impl Fn() + 'static + Clone + Send,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
additional_context: impl Fn() + 'static + Clone + Send + Sync,
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
replace_blocks: bool,
) -> impl Fn(
Request<Body>,
) -> Pin<Box<dyn Future<Output = Response<Body>> + Send + 'static>>
+ Clone
+ Send
+ Sync
+ 'static
where
IV: IntoView + 'static,
@@ -823,8 +823,8 @@ where
tracing::instrument(level = "trace", fields(error), skip_all)
)]
pub fn render_app_to_stream_in_order_with_context<IV>(
additional_context: impl Fn() + 'static + Clone + Send,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
additional_context: impl Fn() + 'static + Clone + Send + Sync,
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
) -> impl Fn(
Request<Body>,
) -> Pin<Box<dyn Future<Output = Response<Body>> + Send + 'static>>
@@ -847,13 +847,17 @@ where
}
fn handle_response<IV>(
additional_context: impl Fn() + 'static + Clone + Send,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
additional_context: impl Fn() + 'static + Clone + Send + Sync,
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
stream_builder: fn(
IV,
BoxedFnOnce<PinnedStream<String>>,
) -> PinnedFuture<PinnedStream<String>>,
) -> impl Fn(Request<Body>) -> PinnedFuture<Response<Body>> + Clone + Send + 'static
) -> impl Fn(Request<Body>) -> PinnedFuture<Response<Body>>
+ Clone
+ Send
+ Sync
+ 'static
where
IV: IntoView + 'static,
{
@@ -980,7 +984,7 @@ fn provide_contexts(
tracing::instrument(level = "trace", fields(error), skip_all)
)]
pub fn render_app_async<IV>(
app_fn: impl Fn() -> IV + Clone + Send + 'static,
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
) -> impl Fn(
Request<Body>,
) -> Pin<Box<dyn Future<Output = Response<Body>> + Send + 'static>>
@@ -1034,8 +1038,8 @@ where
tracing::instrument(level = "trace", fields(error), skip_all)
)]
pub fn render_app_async_stream_with_context<IV>(
additional_context: impl Fn() + 'static + Clone + Send,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
additional_context: impl Fn() + 'static + Clone + Send + Sync,
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
) -> impl Fn(
Request<Body>,
) -> Pin<Box<dyn Future<Output = Response<Body>> + Send + 'static>>
@@ -1101,8 +1105,8 @@ where
tracing::instrument(level = "trace", fields(error), skip_all)
)]
pub fn render_app_async_with_context<IV>(
additional_context: impl Fn() + 'static + Clone + Send,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
additional_context: impl Fn() + 'static + Clone + Send + Sync,
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
) -> impl Fn(
Request<Body>,
) -> Pin<Box<dyn Future<Output = Response<Body>> + Send + 'static>>
@@ -1641,7 +1645,7 @@ where
self,
options: &S,
paths: Vec<AxumRouteListing>,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
) -> Self
where
IV: IntoView + 'static;
@@ -1656,8 +1660,8 @@ where
self,
options: &S,
paths: Vec<AxumRouteListing>,
additional_context: impl Fn() + 'static + Clone + Send,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
additional_context: impl Fn() + 'static + Clone + Send + Sync,
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
) -> Self
where
IV: IntoView + 'static;
@@ -1690,12 +1694,15 @@ impl AxumPath for Vec<PathSegment> {
match segment {
PathSegment::Static(s) => path.push_str(s),
PathSegment::Param(s) => {
path.push(':');
path.push('{');
path.push_str(s);
path.push('}');
}
PathSegment::Splat(s) => {
path.push('{');
path.push('*');
path.push_str(s);
path.push('}');
}
PathSegment::Unit => {}
PathSegment::OptionalParam(_) => {
@@ -1727,7 +1734,7 @@ where
self,
state: &S,
paths: Vec<AxumRouteListing>,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
) -> Self
where
IV: IntoView + 'static,
@@ -1743,8 +1750,8 @@ where
self,
state: &S,
paths: Vec<AxumRouteListing>,
additional_context: impl Fn() + 'static + Clone + Send,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
additional_context: impl Fn() + 'static + Clone + Send + Sync,
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
) -> Self
where
IV: IntoView + 'static,

View File

@@ -3,6 +3,7 @@ use crate::attr::{
Attribute, NextAttribute,
};
use leptos::prelude::*;
use tachys::view::any_view::ExtraAttrsMut;
/// Function stored to build/rebuild the wrapped children when attributes are added.
type ChildBuilder<T> = dyn Fn(AnyAttribute) -> T + Send + Sync + 'static;
@@ -43,7 +44,7 @@ pub fn AttributeInterceptor<Chil, T>(
) -> impl IntoView
where
Chil: Fn(AnyAttribute) -> T + Send + Sync + 'static,
T: IntoView,
T: IntoView + 'static,
{
AttributeInterceptorInner::new(children)
}
@@ -77,16 +78,20 @@ impl<T: IntoView> AttributeInterceptorInner<T, ()> {
impl<T: IntoView, A: Attribute> Render for AttributeInterceptorInner<T, A> {
type State = <T as Render>::State;
fn build(self) -> Self::State {
self.children.build()
fn build(self, extra_attrs: Option<Vec<AnyAttribute>>) -> Self::State {
self.children.build(extra_attrs)
}
fn rebuild(self, state: &mut Self::State) {
self.children.rebuild(state);
fn rebuild(
self,
state: &mut Self::State,
extra_attrs: Option<Vec<AnyAttribute>>,
) {
self.children.rebuild(state, extra_attrs);
}
}
impl<T: IntoView, A> AddAnyAttr for AttributeInterceptorInner<T, A>
impl<T: IntoView + 'static, A> AddAnyAttr for AttributeInterceptorInner<T, A>
where
A: Attribute,
{
@@ -114,19 +119,23 @@ where
}
}
impl<T: IntoView, A: Attribute> RenderHtml for AttributeInterceptorInner<T, A> {
impl<T: IntoView + 'static, A: Attribute> RenderHtml
for AttributeInterceptorInner<T, A>
{
type AsyncOutput = T::AsyncOutput;
type Owned = AttributeInterceptorInner<T, A::CloneableOwned>;
const MIN_LENGTH: usize = T::MIN_LENGTH;
fn dry_resolve(&mut self) {
self.children.dry_resolve()
fn dry_resolve(&mut self, extra_attrs: ExtraAttrsMut<'_>) {
self.children.dry_resolve(extra_attrs)
}
fn resolve(
self,
extra_attrs: ExtraAttrsMut<'_>,
) -> impl std::future::Future<Output = Self::AsyncOutput> + Send {
self.children.resolve()
self.children.resolve(extra_attrs)
}
fn to_html_with_buf(
@@ -135,16 +144,32 @@ impl<T: IntoView, A: Attribute> RenderHtml for AttributeInterceptorInner<T, A> {
position: &mut leptos::tachys::view::Position,
escape: bool,
mark_branches: bool,
extra_attrs: Option<Vec<AnyAttribute>>,
) {
self.children
.to_html_with_buf(buf, position, escape, mark_branches)
self.children.to_html_with_buf(
buf,
position,
escape,
mark_branches,
extra_attrs,
)
}
fn hydrate<const FROM_SERVER: bool>(
self,
cursor: &leptos::tachys::hydration::Cursor,
position: &leptos::tachys::view::PositionState,
extra_attrs: Option<Vec<AnyAttribute>>,
) -> Self::State {
self.children.hydrate::<FROM_SERVER>(cursor, position)
self.children
.hydrate::<FROM_SERVER>(cursor, position, extra_attrs)
}
fn into_owned(self) -> Self::Owned {
AttributeInterceptorInner {
children_builder: self.children_builder,
children: self.children,
attributes: self.attributes.into_cloneable_owned(),
}
}
}

View File

@@ -43,13 +43,20 @@
use reactive_graph::{
owner::{LocalStorage, StoredValue},
traits::WithValue,
traits::{Dispose, WithValue},
};
use std::{fmt, rc::Rc, sync::Arc};
/// A wrapper trait for calling callbacks.
pub trait Callable<In: 'static, Out: 'static = ()> {
/// calls the callback with the specified argument.
///
/// Returns None if the callback has been disposed
fn try_run(&self, input: In) -> Option<Out>;
/// calls the callback with the specified argument.
///
/// # Panics
/// Panics if you try to run a callback that has been disposed
fn run(&self, input: In) -> Out;
}
@@ -72,6 +79,12 @@ impl<In, Out> Clone for UnsyncCallback<In, Out> {
}
}
impl<In, Out> Dispose for UnsyncCallback<In, Out> {
fn dispose(self) {
self.0.dispose();
}
}
impl<In, Out> UnsyncCallback<In, Out> {
/// Creates a new callback from the given function.
pub fn new<F>(f: F) -> UnsyncCallback<In, Out>
@@ -80,9 +93,23 @@ impl<In, Out> UnsyncCallback<In, Out> {
{
Self(StoredValue::new_local(Rc::new(f)))
}
/// Returns `true` if both callbacks wrap the same underlying function pointer.
#[inline]
pub fn matches(&self, other: &Self) -> bool {
self.0.with_value(|self_value| {
other
.0
.with_value(|other_value| Rc::ptr_eq(self_value, other_value))
})
}
}
impl<In: 'static, Out: 'static> Callable<In, Out> for UnsyncCallback<In, Out> {
fn try_run(&self, input: In) -> Option<Out> {
self.0.try_with_value(|fun| fun(input))
}
fn run(&self, input: In) -> Out {
self.0.with_value(|fun| fun(input))
}
@@ -158,10 +185,12 @@ impl<In, Out> fmt::Debug for Callback<In, Out> {
}
impl<In, Out> Callable<In, Out> for Callback<In, Out> {
fn try_run(&self, input: In) -> Option<Out> {
self.0.try_with_value(|fun| fun(input))
}
fn run(&self, input: In) -> Out {
self.0
.try_with_value(|f| f(input))
.expect("called a callback that has been disposed")
self.0.with_value(|f| f(input))
}
}
@@ -171,6 +200,12 @@ impl<In, Out> Clone for Callback<In, Out> {
}
}
impl<In, Out> Dispose for Callback<In, Out> {
fn dispose(self) {
self.0.dispose();
}
}
impl<In, Out> Copy for Callback<In, Out> {}
macro_rules! impl_callable_from_fn {
@@ -212,11 +247,26 @@ impl<In: 'static, Out: 'static> Callback<In, Out> {
{
Self(StoredValue::new(Arc::new(fun)))
}
/// Returns `true` if both callbacks wrap the same underlying function pointer.
#[inline]
pub fn matches(&self, other: &Self) -> bool {
self.0
.try_with_value(|self_value| {
other.0.try_with_value(|other_value| {
Arc::ptr_eq(self_value, other_value)
})
})
.flatten()
.unwrap_or(false)
}
}
#[cfg(test)]
mod tests {
use super::Callable;
use crate::callback::{Callback, UnsyncCallback};
use reactive_graph::traits::Dispose;
struct NoClone {}
@@ -246,4 +296,48 @@ mod tests {
let _callback: UnsyncCallback<(i32, String), String> =
(|num, s| format!("{num} {s}")).into();
}
#[test]
fn sync_callback_try_run() {
let callback = Callback::new(move |arg| arg);
assert_eq!(callback.try_run((0,)), Some((0,)));
callback.dispose();
assert_eq!(callback.try_run((0,)), None);
}
#[test]
fn unsync_callback_try_run() {
let callback = UnsyncCallback::new(move |arg| arg);
assert_eq!(callback.try_run((0,)), Some((0,)));
callback.dispose();
assert_eq!(callback.try_run((0,)), None);
}
#[test]
fn callback_matches_same() {
let callback1 = Callback::new(|x: i32| x * 2);
let callback2 = callback1.clone();
assert!(callback1.matches(&callback2));
}
#[test]
fn callback_matches_different() {
let callback1 = Callback::new(|x: i32| x * 2);
let callback2 = Callback::new(|x: i32| x + 1);
assert!(!callback1.matches(&callback2));
}
#[test]
fn unsync_callback_matches_same() {
let callback1 = UnsyncCallback::new(|x: i32| x * 2);
let callback2 = callback1.clone();
assert!(callback1.matches(&callback2));
}
#[test]
fn unsync_callback_matches_different() {
let callback1 = UnsyncCallback::new(|x: i32| x * 2);
let callback2 = UnsyncCallback::new(|x: i32| x + 1);
assert!(!callback1.matches(&callback2));
}
}

View File

@@ -11,13 +11,13 @@ use reactive_graph::{
use rustc_hash::FxHashMap;
use std::{fmt::Debug, sync::Arc};
use tachys::{
html::attribute::Attribute,
html::attribute::{any_attribute::AnyAttribute, Attribute},
hydration::Cursor,
reactive_graph::OwnedView,
ssr::StreamBuilder,
view::{
add_attr::AddAnyAttr, Mountable, Position, PositionState, Render,
RenderHtml,
add_attr::AddAnyAttr, any_view::ExtraAttrsMut, Mountable, Position,
PositionState, Render, RenderHtml,
},
};
use throw_error::{Error, ErrorHook, ErrorId};
@@ -173,10 +173,10 @@ where
{
type State = RenderEffect<ErrorBoundaryViewState<Chil::State, Fal::State>>;
fn build(mut self) -> Self::State {
fn build(mut self, extra_attrs: Option<Vec<AnyAttribute>>) -> Self::State {
let hook = Arc::clone(&self.hook);
let _hook = throw_error::set_error_hook(Arc::clone(&hook));
let mut children = Some(self.children.build());
let mut children = Some(self.children.build(extra_attrs.clone()));
RenderEffect::new(
move |prev: Option<
ErrorBoundaryViewState<Chil::State, Fal::State>,
@@ -193,7 +193,8 @@ where
// yes errors, and was showing children
(false, None) => {
state.fallback = Some(
(self.fallback)(self.errors.clone()).build(),
(self.fallback)(self.errors.clone())
.build(extra_attrs.clone()),
);
state
.children
@@ -207,8 +208,10 @@ where
}
state
} else {
let fallback = (!self.errors_empty.get())
.then(|| (self.fallback)(self.errors.clone()).build());
let fallback = (!self.errors_empty.get()).then(|| {
(self.fallback)(self.errors.clone())
.build(extra_attrs.clone())
});
ErrorBoundaryViewState {
children: children.take().unwrap(),
fallback,
@@ -218,8 +221,12 @@ where
)
}
fn rebuild(self, state: &mut Self::State) {
let new = self.build();
fn rebuild(
self,
state: &mut Self::State,
extra_attrs: Option<Vec<AnyAttribute>>,
) {
let new = self.build(extra_attrs);
let mut old = std::mem::replace(state, new);
old.insert_before_this(state);
old.unmount();
@@ -268,14 +275,18 @@ where
Fal: RenderHtml + Send + 'static,
{
type AsyncOutput = ErrorBoundaryView<Chil::AsyncOutput, FalFn>;
type Owned = Self;
const MIN_LENGTH: usize = Chil::MIN_LENGTH;
fn dry_resolve(&mut self) {
self.children.dry_resolve();
fn dry_resolve(&mut self, extra_attrs: ExtraAttrsMut<'_>) {
self.children.dry_resolve(extra_attrs);
}
async fn resolve(self) -> Self::AsyncOutput {
async fn resolve(
self,
extra_attrs: ExtraAttrsMut<'_>,
) -> Self::AsyncOutput {
let ErrorBoundaryView {
hook,
boundary_id,
@@ -289,7 +300,7 @@ where
hook,
boundary_id,
errors_empty,
children: children.resolve().await,
children: children.resolve(extra_attrs).await,
fallback,
errors,
}
@@ -301,6 +312,7 @@ where
position: &mut Position,
escape: bool,
mark_branches: bool,
extra_attrs: Option<Vec<AnyAttribute>>,
) {
// first, attempt to serialize the children to HTML, then check for errors
let _hook = throw_error::set_error_hook(self.hook);
@@ -311,6 +323,7 @@ where
&mut new_pos,
escape,
mark_branches,
extra_attrs.clone(),
);
// any thrown errors would've been caught here
@@ -323,6 +336,7 @@ where
position,
escape,
mark_branches,
extra_attrs,
);
}
}
@@ -333,6 +347,7 @@ where
position: &mut Position,
escape: bool,
mark_branches: bool,
extra_attrs: Option<Vec<AnyAttribute>>,
) where
Self: Sized,
{
@@ -345,6 +360,7 @@ where
&mut new_pos,
escape,
mark_branches,
extra_attrs.clone(),
);
// any thrown errors would've been caught here
@@ -358,6 +374,7 @@ where
position,
escape,
mark_branches,
extra_attrs,
);
buf.push_sync(&fallback);
}
@@ -367,6 +384,7 @@ where
mut self,
cursor: &Cursor,
position: &PositionState,
extra_attrs: Option<Vec<AnyAttribute>>,
) -> Self::State {
let mut children = Some(self.children);
let hook = Arc::clone(&self.hook);
@@ -388,7 +406,8 @@ where
// yes errors, and was showing children
(false, None) => {
state.fallback = Some(
(self.fallback)(self.errors.clone()).build(),
(self.fallback)(self.errors.clone())
.build(extra_attrs.clone()),
);
state
.children
@@ -405,15 +424,23 @@ where
let children = children.take().unwrap();
let (children, fallback) = if self.errors_empty.get() {
(
children.hydrate::<FROM_SERVER>(&cursor, &position),
children.hydrate::<FROM_SERVER>(
&cursor,
&position,
extra_attrs.clone(),
),
None,
)
} else {
(
children.build(),
children.build(extra_attrs.clone()),
Some(
(self.fallback)(self.errors.clone())
.hydrate::<FROM_SERVER>(&cursor, &position),
.hydrate::<FROM_SERVER>(
&cursor,
&position,
extra_attrs.clone(),
),
),
)
};
@@ -423,6 +450,10 @@ where
},
)
}
fn into_owned(self) -> Self::Owned {
self
}
}
#[derive(Debug)]

View File

@@ -3,7 +3,11 @@ use leptos_dom::helpers::window;
use leptos_server::{ServerAction, ServerMultiAction};
use serde::de::DeserializeOwned;
use server_fn::{
client::Client, codec::PostUrl, request::ClientReq, ServerFn, ServerFnError,
client::Client,
codec::PostUrl,
error::{IntoAppError, ServerFnErrorErr},
request::ClientReq,
ServerFn,
};
use tachys::{
either::Either,
@@ -121,9 +125,10 @@ where
"Error converting form field into server function \
arguments: {err:?}"
);
value.set(Some(Err(ServerFnError::Serialization(
value.set(Some(Err(ServerFnErrorErr::Serialization(
err.to_string(),
))));
)
.into_app_error())));
version.update(|n| *n += 1);
}
}
@@ -187,9 +192,10 @@ where
action.dispatch(new_input);
}
Err(err) => {
action.dispatch_sync(Err(ServerFnError::Serialization(
action.dispatch_sync(Err(ServerFnErrorErr::Serialization(
err.to_string(),
)));
)
.into_app_error()));
}
}
};

View File

@@ -1,11 +1,11 @@
use std::borrow::Cow;
use tachys::{
html::attribute::Attribute,
html::attribute::{any_attribute::AnyAttribute, Attribute},
hydration::Cursor,
ssr::StreamBuilder,
view::{
add_attr::AddAnyAttr, Position, PositionState, Render, RenderHtml,
ToTemplate,
add_attr::AddAnyAttr, any_view::ExtraAttrsMut, Position, PositionState,
Render, RenderHtml, ToTemplate,
},
};
@@ -76,26 +76,34 @@ where
impl<T: Render> Render for View<T> {
type State = T::State;
fn build(self) -> Self::State {
self.inner.build()
fn build(self, extra_attrs: Option<Vec<AnyAttribute>>) -> Self::State {
self.inner.build(extra_attrs)
}
fn rebuild(self, state: &mut Self::State) {
self.inner.rebuild(state)
fn rebuild(
self,
state: &mut Self::State,
extra_attrs: Option<Vec<AnyAttribute>>,
) {
self.inner.rebuild(state, extra_attrs)
}
}
impl<T: RenderHtml> RenderHtml for View<T> {
type AsyncOutput = T::AsyncOutput;
type Owned = View<T::Owned>;
const MIN_LENGTH: usize = <T as RenderHtml>::MIN_LENGTH;
async fn resolve(self) -> Self::AsyncOutput {
self.inner.resolve().await
async fn resolve(
self,
extra_attrs: ExtraAttrsMut<'_>,
) -> Self::AsyncOutput {
self.inner.resolve(extra_attrs).await
}
fn dry_resolve(&mut self) {
self.inner.dry_resolve();
fn dry_resolve(&mut self, extra_attrs: ExtraAttrsMut<'_>) {
self.inner.dry_resolve(extra_attrs);
}
fn to_html_with_buf(
@@ -104,6 +112,7 @@ impl<T: RenderHtml> RenderHtml for View<T> {
position: &mut Position,
escape: bool,
mark_branches: bool,
extra_attrs: Option<Vec<AnyAttribute>>,
) {
#[cfg(debug_assertions)]
let vm = self.view_marker.to_owned();
@@ -112,8 +121,13 @@ impl<T: RenderHtml> RenderHtml for View<T> {
buf.push_str(&format!("<!--hot-reload|{vm}|open-->"));
}
self.inner
.to_html_with_buf(buf, position, escape, mark_branches);
self.inner.to_html_with_buf(
buf,
position,
escape,
mark_branches,
extra_attrs,
);
#[cfg(debug_assertions)]
if let Some(vm) = vm.as_ref() {
@@ -127,6 +141,7 @@ impl<T: RenderHtml> RenderHtml for View<T> {
position: &mut Position,
escape: bool,
mark_branches: bool,
extra_attrs: Option<Vec<AnyAttribute>>,
) where
Self: Sized,
{
@@ -142,6 +157,7 @@ impl<T: RenderHtml> RenderHtml for View<T> {
position,
escape,
mark_branches,
extra_attrs,
);
#[cfg(debug_assertions)]
@@ -154,8 +170,18 @@ impl<T: RenderHtml> RenderHtml for View<T> {
self,
cursor: &Cursor,
position: &PositionState,
extra_attrs: Option<Vec<AnyAttribute>>,
) -> Self::State {
self.inner.hydrate::<FROM_SERVER>(cursor, position)
self.inner
.hydrate::<FROM_SERVER>(cursor, position, extra_attrs)
}
fn into_owned(self) -> Self::Owned {
View {
inner: self.inner.into_owned(),
#[cfg(debug_assertions)]
view_marker: self.view_marker,
}
}
}

View File

@@ -172,7 +172,7 @@ pub mod prelude {
actions::*, computed::*, effect::*, graph::untrack, owner::*,
signal::*, wrappers::read::*,
};
pub use server_fn::{self, ServerFnError};
pub use server_fn::{self, error::ServerFnError};
pub use tachys::{
reactive_graph::{bind::BindAttribute, node_ref::*, Suspend},
view::{

View File

@@ -71,6 +71,7 @@ where
view.hydrate::<true>(
&Cursor::new(parent.unchecked_into()),
&PositionState::default(),
None,
)
});
@@ -124,7 +125,7 @@ where
let owner = Owner::new();
let mountable = owner.with(move || {
let view = f().into_view();
let mut mountable = view.build();
let mut mountable = view.build(None);
mountable.mount(&parent, None);
mountable
});
@@ -152,7 +153,7 @@ where
let owner = Owner::new();
let mountable = owner.with(move || {
let view = f();
let mut mountable = view.build();
let mut mountable = view.build(None);
mountable.mount(parent, None);
mountable
});

View File

@@ -19,12 +19,13 @@ use slotmap::{DefaultKey, SlotMap};
use std::sync::Arc;
use tachys::{
either::Either,
html::attribute::Attribute,
html::attribute::{any_attribute::AnyAttribute, Attribute},
hydration::Cursor,
reactive_graph::{OwnedView, OwnedViewState},
ssr::StreamBuilder,
view::{
add_attr::AddAnyAttr,
any_view::ExtraAttrsMut,
either::{EitherKeepAlive, EitherKeepAliveState},
Mountable, Position, PositionState, Render, RenderHtml,
},
@@ -162,7 +163,7 @@ where
OwnedViewState<EitherKeepAliveState<Chil::State, Fal::State>>,
>;
fn build(self) -> Self::State {
fn build(self, extra_attrs: Option<Vec<AnyAttribute>>) -> Self::State {
let mut children = Some(self.children);
let mut fallback = Some(self.fallback);
let none_pending = self.none_pending;
@@ -187,16 +188,20 @@ where
);
if let Some(mut state) = prev {
this.rebuild(&mut state);
this.rebuild(&mut state, extra_attrs.clone());
state
} else {
this.build()
this.build(extra_attrs.clone())
}
})
}
fn rebuild(self, state: &mut Self::State) {
let new = self.build();
fn rebuild(
self,
state: &mut Self::State,
extra_attrs: Option<Vec<AnyAttribute>>,
) {
let new = self.build(extra_attrs);
let mut old = std::mem::replace(state, new);
old.insert_before_this(state);
old.unmount();
@@ -247,12 +252,16 @@ where
// i.e., if this is the child of another Suspense during SSR, don't wait for it: it will handle
// itself
type AsyncOutput = Self;
type Owned = Self;
const MIN_LENGTH: usize = Chil::MIN_LENGTH;
fn dry_resolve(&mut self) {}
fn dry_resolve(&mut self, _extra_attrs: ExtraAttrsMut<'_>) {}
async fn resolve(self) -> Self::AsyncOutput {
async fn resolve(
self,
_extra_attrs: ExtraAttrsMut<'_>,
) -> Self::AsyncOutput {
self
}
@@ -262,9 +271,15 @@ where
position: &mut Position,
escape: bool,
mark_branches: bool,
extra_attrs: Option<Vec<AnyAttribute>>,
) {
self.fallback
.to_html_with_buf(buf, position, escape, mark_branches);
self.fallback.to_html_with_buf(
buf,
position,
escape,
mark_branches,
extra_attrs,
);
}
fn to_html_async_with_buf<const OUT_OF_ORDER: bool>(
@@ -273,6 +288,7 @@ where
position: &mut Position,
escape: bool,
mark_branches: bool,
mut extra_attrs: Option<Vec<AnyAttribute>>,
) where
Self: Sized,
{
@@ -297,7 +313,8 @@ where
provide_context(LocalResourceNotifier::from(local_tx));
// walk over the tree of children once to make sure that all resource loads are registered
self.children.dry_resolve();
self.children
.dry_resolve(ExtraAttrsMut::from_owned(&mut extra_attrs));
// check the set of tasks to see if it is empty, now or later
let eff = reactive_graph::effect::Effect::new_isomorphic({
@@ -313,7 +330,8 @@ where
}
});
let mut fut = Box::pin(ScopedFuture::new(ErrorHookFuture::new(
let mut fut = Box::pin(ScopedFuture::new(ErrorHookFuture::new({
let mut extra_attrs = extra_attrs.clone();
async move {
// race the local resource notifier against the set of tasks
//
@@ -340,7 +358,7 @@ where
// but in situations like a <For each=|| some_resource.snapshot()/> we actually
// want to be able to 1) synchronously read a resource's value, but still 2) wait
// for it to load before we render everything
let mut children = Box::pin(self.children.resolve().fuse());
let mut children = Box::pin(self.children.resolve(ExtraAttrsMut::from_owned(&mut extra_attrs)).fuse());
// we continue racing the children against the "do we have any local
// resources?" Future
@@ -359,8 +377,8 @@ where
}
}
}
},
)));
}
})));
match fut.as_mut().now_or_never() {
Some(Some(resolved)) => {
Either::<Fal, _>::Right(resolved)
@@ -369,6 +387,7 @@ where
position,
escape,
mark_branches,
extra_attrs,
);
}
Some(None) => {
@@ -378,6 +397,7 @@ where
position,
escape,
mark_branches,
extra_attrs,
);
}
None => {
@@ -391,12 +411,14 @@ where
self.fallback,
&mut fallback_position,
mark_branches,
extra_attrs.clone(),
);
buf.push_async_out_of_order_with_nonce(
fut,
position,
mark_branches,
nonce_or_not(),
extra_attrs,
);
} else {
buf.push_async({
@@ -412,6 +434,7 @@ where
&mut position,
escape,
mark_branches,
extra_attrs,
);
builder.finish().take_chunks()
}
@@ -426,6 +449,7 @@ where
self,
cursor: &Cursor,
position: &PositionState,
extra_attrs: Option<Vec<AnyAttribute>>,
) -> Self::State {
let cursor = cursor.to_owned();
let position = position.to_owned();
@@ -454,13 +478,21 @@ where
);
if let Some(mut state) = prev {
this.rebuild(&mut state);
this.rebuild(&mut state, extra_attrs.clone());
state
} else {
this.hydrate::<FROM_SERVER>(&cursor, &position)
this.hydrate::<FROM_SERVER>(
&cursor,
&position,
extra_attrs.clone(),
)
}
})
}
fn into_owned(self) -> Self::Owned {
self
}
}
/// A wrapper that prevents [`Suspense`] from waiting for any resource reads that happen inside
@@ -480,12 +512,16 @@ where
{
type State = T::State;
fn build(self) -> Self::State {
(self.0)().build()
fn build(self, extra_attrs: Option<Vec<AnyAttribute>>) -> Self::State {
(self.0)().build(extra_attrs)
}
fn rebuild(self, state: &mut Self::State) {
(self.0)().rebuild(state);
fn rebuild(
self,
state: &mut Self::State,
extra_attrs: Option<Vec<AnyAttribute>>,
) {
(self.0)().rebuild(state, extra_attrs);
}
}
@@ -513,12 +549,16 @@ where
T: RenderHtml + 'static,
{
type AsyncOutput = Self;
type Owned = Self;
const MIN_LENGTH: usize = T::MIN_LENGTH;
fn dry_resolve(&mut self) {}
fn dry_resolve(&mut self, _extra_attrs: ExtraAttrsMut<'_>) {}
async fn resolve(self) -> Self::AsyncOutput {
async fn resolve(
self,
_extra_attrs: ExtraAttrsMut<'_>,
) -> Self::AsyncOutput {
self
}
@@ -528,8 +568,15 @@ where
position: &mut Position,
escape: bool,
mark_branches: bool,
extra_attrs: Option<Vec<AnyAttribute>>,
) {
(self.0)().to_html_with_buf(buf, position, escape, mark_branches);
(self.0)().to_html_with_buf(
buf,
position,
escape,
mark_branches,
extra_attrs,
);
}
fn to_html_async_with_buf<const OUT_OF_ORDER: bool>(
@@ -538,6 +585,7 @@ where
position: &mut Position,
escape: bool,
mark_branches: bool,
extra_attrs: Option<Vec<AnyAttribute>>,
) where
Self: Sized,
{
@@ -546,6 +594,7 @@ where
position,
escape,
mark_branches,
extra_attrs,
);
}
@@ -553,7 +602,12 @@ where
self,
cursor: &Cursor,
position: &PositionState,
extra_attrs: Option<Vec<AnyAttribute>>,
) -> Self::State {
(self.0)().hydrate::<FROM_SERVER>(cursor, position)
(self.0)().hydrate::<FROM_SERVER>(cursor, position, extra_attrs)
}
fn into_owned(self) -> Self::Owned {
self
}
}

View File

@@ -1,5 +1,6 @@
use oco_ref::Oco;
use std::sync::Arc;
use tachys::prelude::IntoAttributeValue;
/// Describes a value that is either a static or a reactive string, i.e.,
/// a [`String`], a [`&str`], or a reactive `Fn() -> String`.
@@ -73,3 +74,11 @@ impl Default for TextProp {
Self(Arc::new(|| Oco::Borrowed("")))
}
}
impl IntoAttributeValue for TextProp {
type Output = Oco<'static, str>;
fn into_attribute_value(self) -> Self::Output {
self.get()
}
}

View File

@@ -12,8 +12,9 @@ use typed_builder::TypedBuilder;
/// A Struct to allow us to parse LeptosOptions from the file. Not really needed, most interactions should
/// occur with LeptosOptions
#[derive(Clone, Debug, serde::Deserialize, Default)]
#[derive(Clone, Debug, serde::Deserialize)]
#[serde(rename_all = "kebab-case")]
#[non_exhaustive]
pub struct ConfFile {
pub leptos_options: LeptosOptions,
}
@@ -24,9 +25,14 @@ pub struct ConfFile {
/// It shares keys with cargo-leptos, to allow for easy interoperability
#[derive(TypedBuilder, Debug, Clone, serde::Deserialize)]
#[serde(rename_all = "kebab-case")]
#[non_exhaustive]
pub struct LeptosOptions {
/// The name of the WASM and JS files generated by wasm-bindgen. Defaults to the crate name with underscores instead of dashes
#[builder(setter(into), default=default_output_name())]
/// The name of the WASM and JS files generated by wasm-bindgen.
///
/// This should match the name that will be output when building your application.
///
/// You can easily set this using `env!("CARGO_CRATE_NAME")`.
#[builder(setter(into))]
pub output_name: Arc<str>,
/// The path of the all the files generated by cargo-leptos. This defaults to '.' for convenience when integrating with other
/// tools.
@@ -78,6 +84,40 @@ pub struct LeptosOptions {
#[builder(default = default_hash_files())]
#[serde(default = "default_hash_files")]
pub hash_files: bool,
/// The default prefix to use for server functions when generating API routes. Can be
/// overridden for individual functions using `#[server(prefix = "...")]` as usual.
///
/// This is useful to override the default prefix (`/api`) for all server functions without
/// needing to manually specify via `#[server(prefix = "...")]` on every server function.
#[builder(default, setter(strip_option))]
#[serde(default)]
pub server_fn_prefix: Option<String>,
/// Whether to disable appending the server functions' hashes to the end of their API names.
///
/// This is useful when an app's client side needs a stable server API. For example, shipping
/// the CSR WASM binary in a Tauri app. Tauri app releases are dependent on each platform's
/// distribution method (e.g., the Apple App Store or the Google Play Store), which typically
/// are much slower than the frequency at which a website can be updated. In addition, it's
/// common for users to not have the latest app version installed. In these cases, the CSR WASM
/// app would need to be able to continue calling the backend server function API, so the API
/// path needs to be consistent and not have a hash appended.
///
/// Note that the hash suffixes is intended as a way to ensure duplicate API routes are created.
/// Without the hash, server functions will need to have unique names to avoid creating
/// duplicate routes. Axum will throw an error if a duplicate route is added to the router, but
/// Actix will not.
#[builder(default)]
#[serde(default)]
pub disable_server_fn_hash: bool,
/// Include the module path of the server function in the API route. This is an alternative
/// strategy to prevent duplicate server function API routes (the default strategy is to add
/// a hash to the end of the route). Each element of the module path will be separated by a `/`.
/// For example, a server function with a fully qualified name of `parent::child::server_fn`
/// would have an API route of `/api/parent/child/server_fn` (possibly with a
/// different prefix and a hash suffix depending on the values of the other server fn configs).
#[builder(default)]
#[serde(default)]
pub server_fn_mod_path: bool,
}
impl LeptosOptions {
@@ -120,20 +160,14 @@ impl LeptosOptions {
hash_file: env_w_default("LEPTOS_HASH_FILE_NAME", "hash.txt")?
.into(),
hash_files: env_w_default("LEPTOS_HASH_FILES", "false")?.parse()?,
server_fn_prefix: env_wo_default("SERVER_FN_PREFIX")?,
disable_server_fn_hash: env_wo_default("DISABLE_SERVER_FN_HASH")?
.is_some(),
server_fn_mod_path: env_wo_default("SERVER_FN_MOD_PATH")?.is_some(),
})
}
}
impl Default for LeptosOptions {
fn default() -> Self {
LeptosOptions::builder().build()
}
}
fn default_output_name() -> Arc<str> {
env!("CARGO_CRATE_NAME").replace('-', "_").into()
}
fn default_site_root() -> Arc<str> {
".".into()
}

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_macro"
version = "0.7.4"
version = "0.8.0-alpha"
authors = ["Greg Johnston"]
license = "MIT"
repository = "https://github.com/leptos-rs/leptos"
@@ -34,6 +34,7 @@ log = "0.4.22"
typed-builder = "0.20.0"
trybuild = "1.0"
leptos = { path = "../leptos" }
leptos_router = { path = "../router", features= ["ssr"] }
server_fn = { path = "../server_fn", features = ["cbor"] }
insta = "1.41"
serde = "1.0"

View File

@@ -301,7 +301,7 @@ impl ToTokens for Model {
} else if cfg!(erase_components) {
quote! {
::leptos::prelude::IntoAny::into_any(
::leptos::prelude::untrack(
::leptos::reactive::graph::untrack_with_diagnostics(
move || {
#tracing_guard_expr
#tracing_props_expr
@@ -312,7 +312,7 @@ impl ToTokens for Model {
}
} else {
quote! {
::leptos::prelude::untrack(
::leptos::reactive::graph::untrack_with_diagnostics(
move || {
#tracing_guard_expr
#tracing_props_expr

32
leptos_macro/src/lazy.rs Normal file
View File

@@ -0,0 +1,32 @@
use convert_case::{Case, Casing};
use proc_macro::TokenStream;
use proc_macro2::Ident;
use proc_macro_error2::abort;
use quote::quote;
use syn::{spanned::Spanned, ItemFn};
pub fn lazy_impl(
_args: proc_macro::TokenStream,
s: TokenStream,
) -> TokenStream {
let fun = syn::parse::<ItemFn>(s).unwrap_or_else(|e| {
abort!(e.span(), "`lazy` can only be used on a function")
});
if fun.sig.asyncness.is_none() {
abort!(
fun.sig.asyncness.span(),
"`lazy` can only be used on an async function"
)
}
let converted_name = Ident::new(
&fun.sig.ident.to_string().to_case(Case::Snake),
fun.sig.ident.span(),
);
quote! {
#[cfg_attr(feature = "split", wasm_split::wasm_split(#converted_name))]
#fun
}
.into()
}

View File

@@ -23,6 +23,7 @@ mod params;
mod view;
use crate::component::unmodified_fn_name_from_fn_name;
mod component;
mod lazy;
mod memo;
mod slice;
mod slot;
@@ -918,7 +919,7 @@ pub fn server(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
args.into(),
s.into(),
Some(syn::parse_quote!(::leptos::server_fn)),
"/api",
option_env!("SERVER_FN_PREFIX").unwrap_or("/api"),
None,
None,
) {
@@ -1002,3 +1003,17 @@ pub fn slice(input: TokenStream) -> TokenStream {
pub fn memo(input: TokenStream) -> TokenStream {
memo::memo_impl(input)
}
/// The `#[lazy]` macro marks an `async` function as a function that can be lazy-loaded from a
/// separate (WebAssembly) binary.
///
/// The first time the function is called, calling the function will first load that other binary,
/// then call the function. On subsequent call it will be called immediately, but still return
/// asynchronously to maintain the same API.
///
/// All parameters and output types should be concrete types, with no generics.
#[proc_macro_attribute]
#[proc_macro_error]
pub fn lazy(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
lazy::lazy_impl(args, s)
}

View File

@@ -13,7 +13,13 @@ pub fn params_impl(ast: &syn::DeriveInput) -> proc_macro::TokenStream {
.named
.iter()
.map(|field| {
let field_name_string = &field.ident.as_ref().expect("expected named struct fields").to_string();
let field_name_string = &field
.ident
.as_ref()
.expect("expected named struct fields")
.to_string()
.trim_start_matches("r#")
.to_owned();
let ident = &field.ident;
let ty = &field.ty;
let span = field.span();

View File

@@ -171,7 +171,7 @@ pub(crate) fn component_to_tokens(
let spreads = (!(spreads.is_empty())).then(|| {
quote! {
.add_any_attr((#(#spreads,)*))
.add_any_attr((#(#spreads,)*).into_attr())
}
});

View File

@@ -1129,6 +1129,11 @@ pub(crate) fn attribute_absolute(
::leptos::tachys::html::attribute::custom::custom_attribute(#name, #value)
}
}
else if name == "node_ref" {
quote! {
::leptos::tachys::html::node_ref::#key(#value)
}
}
else {
quote! {
::leptos::tachys::html::attribute::#key(#value)

View File

@@ -0,0 +1,28 @@
use leptos::prelude::*;
use leptos_router::params::Params;
#[derive(PartialEq, Debug, Params)]
struct UserInfo {
user_id: Option<String>,
email: Option<String>,
r#type: Option<i32>,
not_found: Option<i32>,
}
#[test]
fn params_test() {
let mut map = leptos_router::params::ParamsMap::new();
map.insert("user_id", "12".to_owned());
map.insert("email", "em@il".to_owned());
map.insert("type", "12".to_owned());
let user_info = UserInfo::from_map(&map).unwrap();
assert_eq!(
UserInfo {
email: Some("em@il".to_owned()),
user_id: Some("12".to_owned()),
r#type: Some(12),
not_found: None,
},
user_info
);
}

View File

@@ -11,7 +11,7 @@ edition.workspace = true
[dependencies]
base64 = "0.22.1"
codee = { version = "0.2.0", features = ["json_serde"] }
codee = { version = "0.3.0", features = ["json_serde"] }
hydration_context = { workspace = true }
reactive_graph = { workspace = true, features = ["hydration"] }
server_fn = { workspace = true }

View File

@@ -3,7 +3,7 @@ use reactive_graph::{
owner::use_context,
traits::DefinedAt,
};
use server_fn::{error::ServerFnErrorSerde, ServerFn, ServerFnError};
use server_fn::{error::FromServerFnError, ServerFn};
use std::{ops::Deref, panic::Location, sync::Arc};
/// An error that can be caused by a server action.
@@ -42,7 +42,7 @@ where
S: ServerFn + 'static,
S::Output: 'static,
{
inner: ArcAction<S, Result<S::Output, ServerFnError<S::Error>>>,
inner: ArcAction<S, Result<S::Output, S::Error>>,
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: &'static Location<'static>,
}
@@ -52,13 +52,14 @@ where
S: ServerFn + Clone + Send + Sync + 'static,
S::Output: Send + Sync + 'static,
S::Error: Send + Sync + 'static,
S::Error: FromServerFnError,
{
/// Creates a new [`ArcAction`] that will call the server function `S` when dispatched.
#[track_caller]
pub fn new() -> Self {
let err = use_context::<ServerActionError>().and_then(|error| {
(error.path() == S::PATH)
.then(|| ServerFnError::<S::Error>::de(error.err()))
.then(|| S::Error::de(error.err()))
.map(Err)
});
Self {
@@ -76,7 +77,7 @@ where
S: ServerFn + 'static,
S::Output: 'static,
{
type Target = ArcAction<S, Result<S::Output, ServerFnError<S::Error>>>;
type Target = ArcAction<S, Result<S::Output, S::Error>>;
fn deref(&self) -> &Self::Target {
&self.inner
@@ -131,7 +132,7 @@ where
S: ServerFn + 'static,
S::Output: 'static,
{
inner: Action<S, Result<S::Output, ServerFnError<S::Error>>>,
inner: Action<S, Result<S::Output, S::Error>>,
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: &'static Location<'static>,
}
@@ -146,7 +147,7 @@ where
pub fn new() -> Self {
let err = use_context::<ServerActionError>().and_then(|error| {
(error.path() == S::PATH)
.then(|| ServerFnError::<S::Error>::de(error.err()))
.then(|| S::Error::de(error.err()))
.map(Err)
});
Self {
@@ -182,15 +183,14 @@ where
S::Output: Send + Sync + 'static,
S::Error: Send + Sync + 'static,
{
type Target = Action<S, Result<S::Output, ServerFnError<S::Error>>>;
type Target = Action<S, Result<S::Output, S::Error>>;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl<S> From<ServerAction<S>>
for Action<S, Result<S::Output, ServerFnError<S::Error>>>
impl<S> From<ServerAction<S>> for Action<S, Result<S::Output, S::Error>>
where
S: ServerFn + 'static,
S::Output: 'static,

View File

@@ -79,12 +79,13 @@ mod view_implementations {
use reactive_graph::traits::Read;
use std::future::Future;
use tachys::{
html::attribute::Attribute,
html::attribute::{any_attribute::AnyAttribute, Attribute},
hydration::Cursor,
reactive_graph::{RenderEffectState, Suspend, SuspendState},
ssr::StreamBuilder,
view::{
add_attr::AddAnyAttr, Position, PositionState, Render, RenderHtml,
add_attr::AddAnyAttr, any_view::ExtraAttrsMut, Position,
PositionState, Render, RenderHtml,
},
};
@@ -95,12 +96,17 @@ mod view_implementations {
{
type State = RenderEffectState<SuspendState<T>>;
fn build(self) -> Self::State {
(move || Suspend::new(async move { self.await })).build()
fn build(self, extra_attrs: Option<Vec<AnyAttribute>>) -> Self::State {
(move || Suspend::new(async move { self.await })).build(extra_attrs)
}
fn rebuild(self, state: &mut Self::State) {
(move || Suspend::new(async move { self.await })).rebuild(state)
fn rebuild(
self,
state: &mut Self::State,
extra_attrs: Option<Vec<AnyAttribute>>,
) {
(move || Suspend::new(async move { self.await }))
.rebuild(state, extra_attrs)
}
}
@@ -135,15 +141,20 @@ mod view_implementations {
Ser: Send + 'static,
{
type AsyncOutput = Option<T>;
type Owned = Self;
const MIN_LENGTH: usize = 0;
fn dry_resolve(&mut self) {
fn dry_resolve(&mut self, _extra_attrs: ExtraAttrsMut<'_>) {
self.read();
}
fn resolve(self) -> impl Future<Output = Self::AsyncOutput> + Send {
(move || Suspend::new(async move { self.await })).resolve()
fn resolve(
self,
extra_attrs: ExtraAttrsMut<'_>,
) -> impl Future<Output = Self::AsyncOutput> + Send {
(move || Suspend::new(async move { self.await }))
.resolve(extra_attrs)
}
fn to_html_with_buf(
@@ -152,12 +163,14 @@ mod view_implementations {
position: &mut Position,
escape: bool,
mark_branches: bool,
extra_attrs: Option<Vec<AnyAttribute>>,
) {
(move || Suspend::new(async move { self.await })).to_html_with_buf(
buf,
position,
escape,
mark_branches,
extra_attrs,
);
}
@@ -167,6 +180,7 @@ mod view_implementations {
position: &mut Position,
escape: bool,
mark_branches: bool,
extra_attrs: Option<Vec<AnyAttribute>>,
) where
Self: Sized,
{
@@ -176,6 +190,7 @@ mod view_implementations {
position,
escape,
mark_branches,
extra_attrs,
);
}
@@ -183,9 +198,14 @@ mod view_implementations {
self,
cursor: &Cursor,
position: &PositionState,
extra_attrs: Option<Vec<AnyAttribute>>,
) -> Self::State {
(move || Suspend::new(async move { self.await }))
.hydrate::<FROM_SERVER>(cursor, position)
.hydrate::<FROM_SERVER>(cursor, position, extra_attrs)
}
fn into_owned(self) -> Self::Owned {
self
}
}
}

View File

@@ -2,7 +2,7 @@ use reactive_graph::{
actions::{ArcMultiAction, MultiAction},
traits::DefinedAt,
};
use server_fn::{ServerFn, ServerFnError};
use server_fn::ServerFn;
use std::{ops::Deref, panic::Location};
/// An [`ArcMultiAction`] that can be used to call a server function.
@@ -11,7 +11,7 @@ where
S: ServerFn + 'static,
S::Output: 'static,
{
inner: ArcMultiAction<S, Result<S::Output, ServerFnError<S::Error>>>,
inner: ArcMultiAction<S, Result<S::Output, S::Error>>,
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: &'static Location<'static>,
}
@@ -40,7 +40,7 @@ where
S: ServerFn + 'static,
S::Output: 'static,
{
type Target = ArcMultiAction<S, Result<S::Output, ServerFnError<S::Error>>>;
type Target = ArcMultiAction<S, Result<S::Output, S::Error>>;
fn deref(&self) -> &Self::Target {
&self.inner
@@ -95,13 +95,13 @@ where
S: ServerFn + 'static,
S::Output: 'static,
{
inner: MultiAction<S, Result<S::Output, ServerFnError<S::Error>>>,
inner: MultiAction<S, Result<S::Output, S::Error>>,
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: &'static Location<'static>,
}
impl<S> From<ServerMultiAction<S>>
for MultiAction<S, Result<S::Output, ServerFnError<S::Error>>>
for MultiAction<S, Result<S::Output, S::Error>>
where
S: ServerFn + 'static,
S::Output: 'static,
@@ -152,7 +152,7 @@ where
S::Output: 'static,
S::Error: 'static,
{
type Target = MultiAction<S, Result<S::Output, ServerFnError<S::Error>>>;
type Target = MultiAction<S, Result<S::Output, S::Error>>;
fn deref(&self) -> &Self::Target {
&self.inner

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_meta"
version = "0.7.4"
version = "0.8.0-alpha"
authors = ["Greg Johnston"]
license = "MIT"
repository = "https://github.com/leptos-rs/leptos"

View File

@@ -1,6 +1,9 @@
use crate::ServerMetaContext;
use leptos::{
attr::NextAttribute,
attr::{
any_attribute::{AnyAttribute, AnyAttributeState},
NextAttribute,
},
component, html,
reactive::owner::use_context,
tachys::{
@@ -8,8 +11,8 @@ use leptos::{
html::attribute::Attribute,
hydration::Cursor,
view::{
add_attr::AddAnyAttr, Mountable, Position, PositionState, Render,
RenderHtml,
add_attr::AddAnyAttr, any_view::ExtraAttrsMut, Mountable, Position,
PositionState, Render, RenderHtml,
},
},
IntoView,
@@ -58,6 +61,7 @@ where
At: Attribute,
{
attributes: At::State,
extra_attrs: Option<Vec<AnyAttributeState>>,
}
impl<At> Render for BodyView<At>
@@ -66,15 +70,27 @@ where
{
type State = BodyViewState<At>;
fn build(self) -> Self::State {
fn build(self, extra_attrs: Option<Vec<AnyAttribute>>) -> Self::State {
let el = document().body().expect("there to be a <body> element");
let attributes = self.attributes.build(&el);
BodyViewState { attributes }
let extra_attrs = extra_attrs.map(|attrs| attrs.build(&el));
BodyViewState {
attributes,
extra_attrs,
}
}
fn rebuild(self, state: &mut Self::State) {
fn rebuild(
self,
state: &mut Self::State,
extra_attrs: Option<Vec<AnyAttribute>>,
) {
self.attributes.rebuild(&mut state.attributes);
if let (Some(extra_attrs), Some(extra_attr_states)) =
(extra_attrs, &mut state.extra_attrs)
{
extra_attrs.rebuild(extra_attr_states);
}
}
}
@@ -103,17 +119,24 @@ where
At: Attribute,
{
type AsyncOutput = BodyView<At::AsyncOutput>;
type Owned = BodyView<At::CloneableOwned>;
const MIN_LENGTH: usize = At::MIN_LENGTH;
fn dry_resolve(&mut self) {
fn dry_resolve(&mut self, mut extra_attrs: ExtraAttrsMut<'_>) {
self.attributes.dry_resolve();
extra_attrs.iter_mut().for_each(Attribute::dry_resolve);
}
async fn resolve(self) -> Self::AsyncOutput {
BodyView {
attributes: self.attributes.resolve().await,
}
async fn resolve(
self,
extra_attrs: ExtraAttrsMut<'_>,
) -> Self::AsyncOutput {
let (attributes, _) = futures::join!(
self.attributes.resolve(),
ExtraAttrsMut::resolve(extra_attrs)
);
BodyView { attributes }
}
fn to_html_with_buf(
@@ -122,10 +145,15 @@ where
_position: &mut Position,
_escape: bool,
_mark_branches: bool,
extra_attrs: Option<Vec<AnyAttribute>>,
) {
if let Some(meta) = use_context::<ServerMetaContext>() {
let mut buf = String::new();
_ = html::attributes_to_html(self.attributes, &mut buf);
_ = html::attributes_to_html(
self.attributes,
extra_attrs,
&mut buf,
);
if !buf.is_empty() {
_ = meta.body.send(buf);
}
@@ -136,11 +164,23 @@ where
self,
_cursor: &Cursor,
_position: &PositionState,
extra_attrs: Option<Vec<AnyAttribute>>,
) -> Self::State {
let el = document().body().expect("there to be a <body> element");
let attributes = self.attributes.hydrate::<FROM_SERVER>(&el);
let extra_attrs =
extra_attrs.map(|attrs| attrs.hydrate::<FROM_SERVER>(&el));
BodyViewState { attributes }
BodyViewState {
attributes,
extra_attrs,
}
}
fn into_owned(self) -> Self::Owned {
BodyView {
attributes: self.attributes.into_cloneable_owned(),
}
}
}

View File

@@ -1,6 +1,9 @@
use crate::ServerMetaContext;
use leptos::{
attr::NextAttribute,
attr::{
any_attribute::{AnyAttribute, AnyAttributeState},
NextAttribute,
},
component, html,
reactive::owner::use_context,
tachys::{
@@ -8,8 +11,8 @@ use leptos::{
html::attribute::Attribute,
hydration::Cursor,
view::{
add_attr::AddAnyAttr, Mountable, Position, PositionState, Render,
RenderHtml,
add_attr::AddAnyAttr, any_view::ExtraAttrsMut, Mountable, Position,
PositionState, Render, RenderHtml,
},
},
IntoView,
@@ -55,6 +58,7 @@ where
At: Attribute,
{
attributes: At::State,
extra_attrs: Option<Vec<AnyAttributeState>>,
}
impl<At> Render for HtmlView<At>
@@ -63,18 +67,33 @@ where
{
type State = HtmlViewState<At>;
fn build(self) -> Self::State {
fn build(self, extra_attrs: Option<Vec<AnyAttribute>>) -> Self::State {
let el = document()
.document_element()
.expect("there to be a <html> element");
let attributes = self.attributes.build(&el);
let extra_attrs = extra_attrs.map(|attrs| {
attrs.into_iter().map(|attr| attr.build(&el)).collect()
});
HtmlViewState { attributes }
HtmlViewState {
attributes,
extra_attrs,
}
}
fn rebuild(self, state: &mut Self::State) {
fn rebuild(
self,
state: &mut Self::State,
extra_attrs: Option<Vec<AnyAttribute>>,
) {
self.attributes.rebuild(&mut state.attributes);
if let (Some(extra_attrs), Some(extra_attr_states)) =
(extra_attrs, &mut state.extra_attrs)
{
extra_attrs.rebuild(extra_attr_states);
}
}
}
@@ -103,17 +122,24 @@ where
At: Attribute,
{
type AsyncOutput = HtmlView<At::AsyncOutput>;
type Owned = HtmlView<At::CloneableOwned>;
const MIN_LENGTH: usize = At::MIN_LENGTH;
fn dry_resolve(&mut self) {
fn dry_resolve(&mut self, mut extra_attrs: ExtraAttrsMut<'_>) {
self.attributes.dry_resolve();
extra_attrs.iter_mut().for_each(Attribute::dry_resolve);
}
async fn resolve(self) -> Self::AsyncOutput {
HtmlView {
attributes: self.attributes.resolve().await,
}
async fn resolve(
self,
extra_attrs: ExtraAttrsMut<'_>,
) -> Self::AsyncOutput {
let (attributes, _) = futures::join!(
self.attributes.resolve(),
ExtraAttrsMut::resolve(extra_attrs)
);
HtmlView { attributes }
}
fn to_html_with_buf(
@@ -122,10 +148,15 @@ where
_position: &mut Position,
_escape: bool,
_mark_branches: bool,
extra_attrs: Option<Vec<AnyAttribute>>,
) {
if let Some(meta) = use_context::<ServerMetaContext>() {
let mut buf = String::new();
_ = html::attributes_to_html(self.attributes, &mut buf);
_ = html::attributes_to_html(
self.attributes,
extra_attrs,
&mut buf,
);
if !buf.is_empty() {
_ = meta.html.send(buf);
}
@@ -136,14 +167,30 @@ where
self,
_cursor: &Cursor,
_position: &PositionState,
extra_attrs: Option<Vec<AnyAttribute>>,
) -> Self::State {
let el = document()
.document_element()
.expect("there to be a <html> element");
let attributes = self.attributes.hydrate::<FROM_SERVER>(&el);
let extra_attrs = extra_attrs.map(|attrs| {
attrs
.into_iter()
.map(|attr| attr.hydrate::<FROM_SERVER>(&el))
.collect()
});
HtmlViewState { attributes }
HtmlViewState {
attributes,
extra_attrs,
}
}
fn into_owned(self) -> Self::Owned {
HtmlView {
attributes: self.attributes.into_cloneable_owned(),
}
}
}

View File

@@ -44,7 +44,7 @@
use futures::{Stream, StreamExt};
use leptos::{
attr::NextAttribute,
attr::{any_attribute::AnyAttribute, NextAttribute},
component,
logging::debug_warn,
oco::Oco,
@@ -57,8 +57,8 @@ use leptos::{
},
hydration::Cursor,
view::{
add_attr::AddAnyAttr, Mountable, Position, PositionState, Render,
RenderHtml,
add_attr::AddAnyAttr, any_view::ExtraAttrsMut, Mountable, Position,
PositionState, Render, RenderHtml,
},
},
IntoView,
@@ -334,6 +334,7 @@ where
&mut Position::NextChild,
false,
false,
None,
);
_ = cx.elements.send(buf); // fails only if the receiver is already dropped
} else {
@@ -390,13 +391,17 @@ where
{
type State = RegisteredMetaTagState<E, At, Ch>;
fn build(self) -> Self::State {
let state = self.el.unwrap().build();
fn build(self, extra_attrs: Option<Vec<AnyAttribute>>) -> Self::State {
let state = self.el.unwrap().build(extra_attrs);
RegisteredMetaTagState { state }
}
fn rebuild(self, state: &mut Self::State) {
self.el.unwrap().rebuild(&mut state.state);
fn rebuild(
self,
state: &mut Self::State,
extra_attrs: Option<Vec<AnyAttribute>>,
) {
self.el.unwrap().rebuild(&mut state.state, extra_attrs);
}
}
@@ -429,14 +434,18 @@ where
Ch: RenderHtml + Send,
{
type AsyncOutput = Self;
type Owned = RegisteredMetaTag<E, At::CloneableOwned, Ch::Owned>;
const MIN_LENGTH: usize = 0;
fn dry_resolve(&mut self) {
self.el.dry_resolve()
fn dry_resolve(&mut self, extra_attrs: ExtraAttrsMut<'_>) {
self.el.dry_resolve(extra_attrs)
}
async fn resolve(self) -> Self::AsyncOutput {
async fn resolve(
self,
_extra_attrs: ExtraAttrsMut<'_>,
) -> Self::AsyncOutput {
self // TODO?
}
@@ -446,6 +455,7 @@ where
_position: &mut Position,
_escape: bool,
_mark_branches: bool,
_extra_attrs: Option<Vec<AnyAttribute>>,
) {
// meta tags are rendered into the buffer stored into the context
// the value has already been taken out, when we're on the server
@@ -455,6 +465,7 @@ where
self,
_cursor: &Cursor,
_position: &PositionState,
extra_attrs: Option<Vec<AnyAttribute>>,
) -> Self::State {
let cursor = use_context::<MetaContext>()
.expect(
@@ -465,9 +476,16 @@ where
let state = self.el.unwrap().hydrate::<FROM_SERVER>(
&cursor,
&PositionState::new(Position::NextChild),
extra_attrs,
);
RegisteredMetaTagState { state }
}
fn into_owned(self) -> Self::Owned {
RegisteredMetaTag {
el: self.el.map(|inner| inner.into_owned()),
}
}
}
impl<E, At, Ch> Mountable for RegisteredMetaTagState<E, At, Ch>
@@ -520,9 +538,14 @@ struct MetaTagsView;
impl Render for MetaTagsView {
type State = ();
fn build(self) -> Self::State {}
fn build(self, _extra_attrs: Option<Vec<AnyAttribute>>) -> Self::State {}
fn rebuild(self, _state: &mut Self::State) {}
fn rebuild(
self,
_state: &mut Self::State,
_extra_attrs: Option<Vec<AnyAttribute>>,
) {
}
}
impl AddAnyAttr for MetaTagsView {
@@ -541,12 +564,16 @@ impl AddAnyAttr for MetaTagsView {
impl RenderHtml for MetaTagsView {
type AsyncOutput = Self;
type Owned = Self;
const MIN_LENGTH: usize = 0;
fn dry_resolve(&mut self) {}
fn dry_resolve(&mut self, _extra_attrs: ExtraAttrsMut<'_>) {}
async fn resolve(self) -> Self::AsyncOutput {
async fn resolve(
self,
_extra_attrs: ExtraAttrsMut<'_>,
) -> Self::AsyncOutput {
self
}
@@ -556,6 +583,7 @@ impl RenderHtml for MetaTagsView {
_position: &mut Position,
_escape: bool,
_mark_branches: bool,
_extra_attrs: Option<Vec<AnyAttribute>>,
) {
buf.push_str("<!--HEAD-->");
}
@@ -564,8 +592,13 @@ impl RenderHtml for MetaTagsView {
self,
_cursor: &Cursor,
_position: &PositionState,
_extra_attrs: Option<Vec<AnyAttribute>>,
) -> Self::State {
}
fn into_owned(self) -> Self::Owned {
self
}
}
pub(crate) trait OrDefaultNonce {

View File

@@ -1,6 +1,6 @@
use crate::{use_head, MetaContext, ServerMetaContext};
use leptos::{
attr::Attribute,
attr::{any_attribute::AnyAttribute, Attribute},
component,
oco::Oco,
reactive::{
@@ -11,8 +11,8 @@ use leptos::{
dom::document,
hydration::Cursor,
view::{
add_attr::AddAnyAttr, Mountable, Position, PositionState, Render,
RenderHtml,
add_attr::AddAnyAttr, any_view::ExtraAttrsMut, Mountable, Position,
PositionState, Render, RenderHtml,
},
},
text_prop::TextProp,
@@ -189,7 +189,7 @@ struct TitleViewState {
impl Render for TitleView {
type State = TitleViewState;
fn build(mut self) -> Self::State {
fn build(mut self, _extra_attrs: Option<Vec<AnyAttribute>>) -> Self::State {
let el = self.el();
let meta = self.meta;
if let Some(formatter) = self.formatter.take() {
@@ -213,8 +213,12 @@ impl Render for TitleView {
TitleViewState { effect }
}
fn rebuild(self, state: &mut Self::State) {
*state = self.build();
fn rebuild(
self,
state: &mut Self::State,
extra_attrs: Option<Vec<AnyAttribute>>,
) {
*state = self.build(extra_attrs);
}
}
@@ -234,12 +238,16 @@ impl AddAnyAttr for TitleView {
impl RenderHtml for TitleView {
type AsyncOutput = Self;
type Owned = Self;
const MIN_LENGTH: usize = 0;
fn dry_resolve(&mut self) {}
fn dry_resolve(&mut self, _extra_attrs: ExtraAttrsMut<'_>) {}
async fn resolve(self) -> Self::AsyncOutput {
async fn resolve(
self,
_extra_attrs: ExtraAttrsMut<'_>,
) -> Self::AsyncOutput {
self
}
@@ -249,6 +257,7 @@ impl RenderHtml for TitleView {
_position: &mut Position,
_escape: bool,
_mark_branches: bool,
_extra_attrs: Option<Vec<AnyAttribute>>,
) {
// meta tags are rendered into the buffer stored into the context
// the value has already been taken out, when we're on the server
@@ -258,6 +267,7 @@ impl RenderHtml for TitleView {
mut self,
_cursor: &Cursor,
_position: &PositionState,
_extra_attrs: Option<Vec<AnyAttribute>>,
) -> Self::State {
let el = self.el();
let meta = self.meta;
@@ -282,6 +292,10 @@ impl RenderHtml for TitleView {
});
TitleViewState { effect }
}
fn into_owned(self) -> Self::Owned {
self
}
}
impl Mountable for TitleViewState {

View File

@@ -0,0 +1,14 @@
# Generated by Cargo
# will have compiled files and executables
debug/
target/
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
Cargo.lock
# These are backup files generated by rustfmt
**/*.rs.bk
# MSVC Windows builds of rustc generate these, which store debugging information
*.pdb

View File

@@ -0,0 +1,111 @@
[package]
name = "leptos-hexagonal-design"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
leptos = { version = "0.7.0" }
leptos_router = { version = "0.7.0" }
axum = { version = "0.7", optional = true }
console_error_panic_hook = "0.1"
leptos_axum = { version = "0.7.0", optional = true }
leptos_meta = { version = "0.7.0" }
tokio = { version = "1", features = ["rt-multi-thread"], optional = true }
tower = { version = "0.4", optional = true }
tower-http = { version = "0.5", features = ["fs"], optional = true }
wasm-bindgen = "=0.2.99"
thiserror = "1"
tracing = { version = "0.1", optional = true }
http = "1"
mockall = "0.13.1"
cfg-if = "1.0.0"
serde = "1.0.215"
pin-project-lite = "0.2.15"
[features]
config_1 = []
hydrate = ["leptos/hydrate"]
ssr = [
"dep:axum",
"dep:tokio",
"dep:tower",
"dep:tower-http",
"dep:leptos_axum",
"leptos/ssr",
"leptos_meta/ssr",
"leptos_router/ssr",
"dep:tracing",
]
# Defines a size-optimized profile for the WASM bundle in release mode
[profile.wasm-release]
inherits = "release"
opt-level = 'z'
lto = true
codegen-units = 1
panic = "abort"
[package.metadata.leptos]
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
output-name = "leptos-hexagonal-design"
# The site root folder is where cargo-leptos generate all output. WARNING: all content of this folder will be erased on a rebuild. Use it in your server setup.
site-root = "target/site"
# The site-root relative folder where all compiled output (JS, WASM and CSS) is written
# Defaults to pkg
site-pkg-dir = "pkg"
# [Optional] The source CSS file. If it ends with .sass or .scss then it will be compiled by dart-sass into CSS. The CSS is optimized by Lightning CSS before being written to <site-root>/<site-pkg>/app.css
style-file = "style/main.scss"
# Assets source dir. All files found here will be copied and synchronized to site-root.
# The assets-dir cannot have a sub directory with the same name/path as site-pkg-dir.
#
# Optional. Env: LEPTOS_ASSETS_DIR.
assets-dir = "public"
# The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup.
site-addr = "127.0.0.1:3000"
# The port to use for automatic reload monitoring
reload-port = 3001
# [Optional] Command to use when running end2end tests. It will run in the end2end dir.
# [Windows] for non-WSL use "npx.cmd playwright test"
# This binary name can be checked in Powershell with Get-Command npx
end2end-cmd = "npx playwright test"
end2end-dir = "end2end"
# The browserlist query used for optimizing the CSS.
browserquery = "defaults"
# The environment Leptos will run in, usually either "DEV" or "PROD"
env = "DEV"
# The features to use when compiling the bin target
#
# Optional. Can be over-ridden with the command line parameter --bin-features
bin-features = ["ssr"]
# If the --no-default-features flag should be used when compiling the bin target
#
# Optional. Defaults to false.
bin-default-features = false
# The features to use when compiling the lib target
#
# Optional. Can be over-ridden with the command line parameter --lib-features
lib-features = ["hydrate"]
# If the --no-default-features flag should be used when compiling the lib target
#
# Optional. Defaults to false.
lib-default-features = false
# The profile to use for the lib target when compiling for release
#
# Optional. Defaults to "release".
lib-profile-release = "wasm-release"

View File

@@ -0,0 +1,24 @@
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
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 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.
For more information, please refer to <https://unlicense.org>

View File

@@ -0,0 +1,142 @@
### Leptos Hexagonal Design
This Blog Post / Github Repository is about applying principles of hexagonal design
- Isolating Business Logic from Sub Domains
- Decoupling design to improve flexibility and testablity
- Applying the principles hierachically so that sub domains which talk to external services also implement also implement hexagonal architecture
There are specific constraints that guide our design decisions
- Server Functions Can't be Generic
- Boxed Traits Objects Have overhead, so we only want to use as much generic code as possible avoid Trait Objects
The way this works is we define the functionality of our program in the main domain (i.e the business problem and processes our app is trying to solve / proceduralize). We then create sub domains and external services, although they are represented the same. External services are usually the end nodes of your app's architectural graph. Our main application builds it's service layout using configuration flags.
```rust
pub fn config() -> MainAppHandlerAlias {
cfg_if::cfg_if! {
if #[cfg(feature="open_ai_wrapper")] {
fn server_handler_config_1() -> MainAppHandler<
AuthService<PostgresDb, Redis>,
AiMessageGen<PostgresDb,OpenAiWrapper>,
> {
MainAppHandler::new_with_postgres_and_redis_open_ai()
}
server_handler_config_1()
} else {
fn server_handler_config_2() -> MainAppHandler<
AuthService<MySql, MemCache>,
OtherAiMessageGen<MySql,HuggingFaceWrapper>,
> {
MainAppHandler::new_with_my_sql_memcache_hugging_face()
}
server_handler_config_2()
}
}
}
```
And we pass in our handler which implements a trait
```rust
pub trait HandlerServerFn {
pub fn server_fn_1_inner(&self);
}
impl<S,S2> HandlerServerFn for MainAppHandler<S:SubDomain1Trait,S2:SubDomain2Trait> {
pub fn server_fn_1_inner(&self) {
// do thing
}
}
```
in our main fn we produce our applications service graph and pass it to our leptos router.
```rust
main () {
let leptos_options = conf.leptos_options;
let routes = generate_route_list(crate::app::App);
// our feature flag based config function.
let handler = config();
let handler_c = handler.clone();
// we implement FromRef<ServerState> for LeptosOptions
let server_state = ServerState {
handler,
leptos_options: leptos_options.clone(),
};
let app = Router::new()
.leptos_routes_with_context(
&server_state,
routes,
// We pass in the MainAppHandler struct as context so we can fetch it anywhere context is available on the server.
// This includes in middleware we define on server functions (see middleware.rs)
move || provide_context(handler_c.clone()),
{
let leptos_options = leptos_options.clone();
move || shell(leptos_options.clone())
},
)
.fallback(leptos_axum::file_and_error_handler::<
ServerState<HandlerStructAlias>,
_,
>(shell))
.with_state(server_state);
}
```
and then in our server functions
```rust
#[server]
pub async fn server_fn_1() -> Result<(),ServerFnError> {
// we type alias every variation of our services we plan on configuring. The alternative is using Box<dyn Trait> which isn't bad - just slower.
Ok(expect_context::<MainAppHandlerAlias>().server_fn_1_inner())
}
```
And then we can mock and service trait in any combination like so
```rust
#[tokio::test]
pub async fn test_subdomain_1_with_mocks() -> Result<(), Box<dyn Error>> {
let mut mock_external_service_1 = MockExternalServiceTrait1::new();
mock_external_service_1
.expect_external_service_1_method()
.returning(|| {
println!("Mock external service 1");
Ok(ExternalService1Data)
});
let mut mock_external_service_2 = MockExternalServiceTrait2::new();
mock_external_service_2
.expect_external_service_2_method()
.returning(|| {
println!("Mock external service 2");
Ok(ExternalService2Data)
});
let real_subdomain_1_with_mock_externals = SubDomainStruct1 {
external_service_1: mock_external_service_1,
external_service_2: mock_external_service_2,
};
let data = real_subdomain_1_with_mock_externals
.sub_domain_1_method()
.await?;
assert_eq!(data, SubDomain1Data);
Ok(())
}
```
Check out the code in the repository for a working example.
Run the tests with
` cargo test --features ssr `
and otherwise run
` cargo leptos serve `
and navigate to `127.0.0.1:3000`
here's a picture
![alt text](leptos_hexagonal_architecture.png)

View File

@@ -0,0 +1,167 @@
{
"name": "end2end",
"version": "1.0.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "end2end",
"version": "1.0.0",
"license": "ISC",
"devDependencies": {
"@playwright/test": "^1.44.1",
"@types/node": "^20.12.12",
"typescript": "^5.4.5"
}
},
"node_modules/@playwright/test": {
"version": "1.44.1",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.44.1.tgz",
"integrity": "sha512-1hZ4TNvD5z9VuhNJ/walIjvMVvYkZKf71axoF/uiAqpntQJXpG64dlXhoDXE3OczPuTuvjf/M5KWFg5VAVUS3Q==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright": "1.44.1"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=16"
}
},
"node_modules/@types/node": {
"version": "20.12.12",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.12.tgz",
"integrity": "sha512-eWLDGF/FOSPtAvEqeRAQ4C8LSA7M1I7i0ky1I8U7kD1J5ITyW3AsRhQrKVoWf5pFKZ2kILsEGJhsI9r93PYnOw==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~5.26.4"
}
},
"node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/playwright": {
"version": "1.44.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.44.1.tgz",
"integrity": "sha512-qr/0UJ5CFAtloI3avF95Y0L1xQo6r3LQArLIg/z/PoGJ6xa+EwzrwO5lpNr/09STxdHuUoP2mvuELJS+hLdtgg==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.44.1"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=16"
},
"optionalDependencies": {
"fsevents": "2.3.2"
}
},
"node_modules/playwright-core": {
"version": "1.44.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.44.1.tgz",
"integrity": "sha512-wh0JWtYTrhv1+OSsLPgFzGzt67Y7BE/ZS3jEqgGBlp2ppp1ZDj8c+9IARNW4dwf1poq5MgHreEM2KV/GuR4cFA==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=16"
}
},
"node_modules/typescript": {
"version": "5.4.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz",
"integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/undici-types": {
"version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
"dev": true,
"license": "MIT"
}
},
"dependencies": {
"@playwright/test": {
"version": "1.44.1",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.44.1.tgz",
"integrity": "sha512-1hZ4TNvD5z9VuhNJ/walIjvMVvYkZKf71axoF/uiAqpntQJXpG64dlXhoDXE3OczPuTuvjf/M5KWFg5VAVUS3Q==",
"dev": true,
"requires": {
"playwright": "1.44.1"
}
},
"@types/node": {
"version": "20.12.12",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.12.tgz",
"integrity": "sha512-eWLDGF/FOSPtAvEqeRAQ4C8LSA7M1I7i0ky1I8U7kD1J5ITyW3AsRhQrKVoWf5pFKZ2kILsEGJhsI9r93PYnOw==",
"dev": true,
"requires": {
"undici-types": "~5.26.4"
}
},
"fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"optional": true
},
"playwright": {
"version": "1.44.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.44.1.tgz",
"integrity": "sha512-qr/0UJ5CFAtloI3avF95Y0L1xQo6r3LQArLIg/z/PoGJ6xa+EwzrwO5lpNr/09STxdHuUoP2mvuELJS+hLdtgg==",
"dev": true,
"requires": {
"fsevents": "2.3.2",
"playwright-core": "1.44.1"
}
},
"playwright-core": {
"version": "1.44.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.44.1.tgz",
"integrity": "sha512-wh0JWtYTrhv1+OSsLPgFzGzt67Y7BE/ZS3jEqgGBlp2ppp1ZDj8c+9IARNW4dwf1poq5MgHreEM2KV/GuR4cFA==",
"dev": true
},
"typescript": {
"version": "5.4.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz",
"integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==",
"dev": true
},
"undici-types": {
"version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
"dev": true
}
}
}

View File

@@ -0,0 +1,15 @@
{
"name": "end2end",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@playwright/test": "^1.44.1",
"@types/node": "^20.12.12",
"typescript": "^5.4.5"
}
}

View File

@@ -0,0 +1,105 @@
import type { PlaywrightTestConfig } from "@playwright/test";
import { devices, defineConfig } from "@playwright/test";
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// require('dotenv').config();
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: "./tests",
/* Maximum time one test can run for. */
timeout: 30 * 1000,
expect: {
/**
* Maximum time expect() should wait for the condition to be met.
* For example in `await expect(locator).toHaveText();`
*/
timeout: 5000,
},
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: "html",
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */
actionTimeout: 0,
/* Base URL to use in actions like `await page.goto('/')`. */
// baseURL: 'http://localhost:3000',
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: "on-first-retry",
},
/* Configure projects for major browsers */
projects: [
{
name: "chromium",
use: {
...devices["Desktop Chrome"],
},
},
{
name: "firefox",
use: {
...devices["Desktop Firefox"],
},
},
{
name: "webkit",
use: {
...devices["Desktop Safari"],
},
},
/* Test against mobile viewports. */
// {
// name: 'Mobile Chrome',
// use: {
// ...devices['Pixel 5'],
// },
// },
// {
// name: 'Mobile Safari',
// use: {
// ...devices['iPhone 12'],
// },
// },
/* Test against branded browsers. */
// {
// name: 'Microsoft Edge',
// use: {
// channel: 'msedge',
// },
// },
// {
// name: 'Google Chrome',
// use: {
// channel: 'chrome',
// },
// },
],
/* Folder for test artifacts such as screenshots, videos, traces, etc. */
// outputDir: 'test-results/',
/* Run your local dev server before starting the tests */
// webServer: {
// command: 'npm run start',
// port: 3000,
// },
});

View File

@@ -0,0 +1,9 @@
import { test, expect } from "@playwright/test";
test("homepage has title and heading text", async ({ page }) => {
await page.goto("http://localhost:3000/");
await expect(page).toHaveTitle("Welcome to Leptos");
await expect(page.locator("h1")).toHaveText("Welcome to Leptos!");
});

View File

@@ -0,0 +1,109 @@
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig to read more about this file */
/* Projects */
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
/* Language and Environment */
"target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
// "jsx": "preserve", /* Specify what JSX code is generated. */
// "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
/* Modules */
"module": "commonjs", /* Specify what module code is generated. */
// "rootDir": "./", /* Specify the root folder within your source files. */
// "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
// "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */
// "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
// "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
// "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
// "resolveJsonModule": true, /* Enable importing .json files. */
// "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
/* JavaScript Support */
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
/* Emit */
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
// "outDir": "./", /* Specify an output folder for all emitted files. */
// "removeComments": true, /* Disable emitting comments. */
// "noEmit": true, /* Disable emitting files from a compilation. */
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
// "newLine": "crlf", /* Set the newline character for emitting files. */
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
// "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
/* Interop Constraints */
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
// "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
/* Type Checking */
"strict": true, /* Enable all strict type-checking options. */
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
/* Completeness */
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
"skipLibCheck": true /* Skip type checking all .d.ts files. */
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 292 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,87 @@
use leptos::prelude::*;
use leptos_meta::{provide_meta_context, MetaTags, Stylesheet, Title};
use leptos_router::{
components::{Route, Router, Routes},
StaticSegment,
};
#[cfg(feature = "ssr")]
use super::{server_types::HandlerStructAlias, traits::HandlerTrait};
use crate::ui_types::*;
pub fn shell(options: LeptosOptions) -> impl IntoView {
view! {
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<AutoReload options=options.clone() />
<HydrationScripts options/>
<MetaTags/>
</head>
<body>
<App/>
</body>
</html>
}
}
#[component]
pub fn App() -> impl IntoView {
// Provides context that manages stylesheets, titles, meta tags, etc.
provide_meta_context();
view! {
// injects a stylesheet into the document <head>
// id=leptos means cargo-leptos will hot-reload this stylesheet
<Stylesheet id="leptos" href="/pkg/leptos-hexagonal-design.css"/>
// sets the document title
<Title text="Welcome to Leptos"/>
// content for this welcome page
<Router>
<main>
<Routes fallback=|| "Page not found.".into_view()>
<Route path=StaticSegment("") view=HomePage/>
</Routes>
</main>
</Router>
}
}
/// Renders the home page of your application.
#[component]
fn HomePage() -> impl IntoView {
let server_fn_1 = ServerAction::<ServerFunction1>::new();
let server_fn_2 = ServerAction::<ServerFunction2>::new();
let server_fn_3 = ServerAction::<ServerFunction3>::new();
Effect::new(move |_| {
server_fn_1.dispatch(ServerFunction1 {});
server_fn_2.dispatch(ServerFunction2 {});
server_fn_3.dispatch(ServerFunction3 {});
});
}
#[server]
#[middleware(crate::middleware::SubDomain1Layer)]
pub async fn server_function_1() -> Result<UiMappingFromDomainData, ServerFnError> {
Ok(expect_context::<HandlerStructAlias>()
.server_fn_1()
.await?
.into())
}
#[server]
pub async fn server_function_2() -> Result<UiMappingFromDomainData2, ServerFnError> {
Ok(expect_context::<HandlerStructAlias>()
.server_fn_2()
.await?
.into())
}
#[server]
pub async fn server_function_3() -> Result<UiMappingFromDomainData3, ServerFnError> {
Ok(expect_context::<HandlerStructAlias>()
.server_fn_3()
.await?
.into())
}

View File

@@ -0,0 +1,23 @@
use super::server_types::*;
pub fn config() -> HandlerStructAlias {
cfg_if::cfg_if! {
if #[cfg(feature="config_1")] {
fn server_handler_config_1() -> HandlerStruct<
SubDomainStruct1<ExternalService1_1, ExternalService2_1>,
SubDomainStruct2<ExternalService1_1>,
> {
HandlerStruct::default()
}
server_handler_config_1()
} else {
fn server_handler_config_2() -> HandlerStruct<
SubDomainStruct1<ExternalService1_2, ExternalService2_2>,
SubDomainStruct2<ExternalService1_2>,
> {
HandlerStruct::new()
}
server_handler_config_2()
}
}
}

View File

@@ -0,0 +1,150 @@
pub mod app;
pub mod ui_types;
#[cfg(feature = "ssr")]
pub mod config;
#[cfg(feature = "ssr")]
pub mod middleware;
#[cfg(feature = "ssr")]
pub mod server_types;
#[cfg(feature = "ssr")]
pub mod trait_impl;
#[cfg(feature = "ssr")]
pub mod traits;
#[cfg(feature = "hydrate")]
#[wasm_bindgen::prelude::wasm_bindgen]
pub fn hydrate() {
use crate::app::*;
console_error_panic_hook::set_once();
leptos::mount::hydrate_body(App);
}
#[cfg(test)]
pub mod tests {
use super::server_types::*;
use super::traits::*;
use std::error::Error;
#[tokio::test]
pub async fn test_subdomain_1_with_mocks() -> Result<(), Box<dyn Error>> {
let mut mock_external_service_1 = MockExternalServiceTrait1::new();
mock_external_service_1
.expect_external_service_1_method()
.returning(|| {
println!("Mock external service 1");
Ok(ExternalService1Data)
});
let mut mock_external_service_2 = MockExternalServiceTrait2::new();
mock_external_service_2
.expect_external_service_2_method()
.returning(|| {
println!("Mock external service 2");
Ok(ExternalService2Data)
});
let real_subdomain_1_with_mock_externals = SubDomainStruct1 {
external_service_1: mock_external_service_1,
external_service_2: mock_external_service_2,
};
let data = real_subdomain_1_with_mock_externals
.sub_domain_1_method()
.await?;
assert_eq!(data, SubDomain1Data);
Ok(())
}
#[tokio::test]
pub async fn test_subdomain_2_with_mocks() -> Result<(), Box<dyn Error>> {
let mut mock_external_service_1 = MockExternalServiceTrait1::new();
mock_external_service_1
.expect_external_service_1_method()
.returning(|| {
println!("Mock external service 1 AGAIN");
Ok(ExternalService1Data)
});
let real_subdomain_2_with_mock_externals = SubDomainStruct2 {
external_service_1: mock_external_service_1,
};
let data = real_subdomain_2_with_mock_externals
.sub_domain_2_method()
.await?;
assert_eq!(data, SubDomain2Data);
Ok(())
}
#[tokio::test]
pub async fn test_handler_with_mocks() -> Result<(), Box<dyn Error>> {
let mut mock_subdomain_1_trait = MockSubDomainTrait1::new();
mock_subdomain_1_trait
.expect_sub_domain_1_method()
.returning(|| {
println!("Mock Subdomain 1");
Ok(SubDomain1Data)
});
let mut mock_subdomain_2_trait = MockSubDomainTrait2::new();
mock_subdomain_2_trait
.expect_sub_domain_2_method()
.returning(|| {
println!("Mock Subdomain 2");
Ok(SubDomain2Data)
});
let real_handler_with_mock_subdomains = HandlerStruct {
sub_domain_1: mock_subdomain_1_trait,
sub_domain_2: mock_subdomain_2_trait,
};
let data = real_handler_with_mock_subdomains.server_fn_1().await?;
assert_eq!(data, DomainData);
let data = real_handler_with_mock_subdomains.server_fn_2().await?;
assert_eq!(data, DomainData2);
let data = real_handler_with_mock_subdomains.server_fn_3().await?;
assert_eq!(data, DomainData3);
Ok(())
}
fn mock_subdomain_1() -> SubDomainStruct1<MockExternalServiceTrait1, MockExternalServiceTrait2>
{
let mut mock_external_service_1 = MockExternalServiceTrait1::new();
mock_external_service_1
.expect_external_service_1_method()
.returning(|| {
println!("Mock external service 1");
Ok(ExternalService1Data)
});
let mut mock_external_service_2 = MockExternalServiceTrait2::new();
mock_external_service_2
.expect_external_service_2_method()
.returning(|| {
println!("Mock external service 2");
Ok(ExternalService2Data)
});
let real_subdomain_1_with_mock_externals = SubDomainStruct1 {
external_service_1: mock_external_service_1,
external_service_2: mock_external_service_2,
};
real_subdomain_1_with_mock_externals
}
#[tokio::test]
pub async fn test_handler_with_mock_and_real_mix() -> Result<(), Box<dyn Error>> {
let sub_domain_1 = mock_subdomain_1();
let mut mock_subdomain_2_trait = MockSubDomainTrait2::new();
mock_subdomain_2_trait
.expect_sub_domain_2_method()
.returning(|| {
println!("Mock Subdomain 2");
Ok(SubDomain2Data)
});
let real_handler = HandlerStruct {
sub_domain_1,
sub_domain_2: mock_subdomain_2_trait,
};
let data = real_handler.server_fn_1().await?;
assert_eq!(data, DomainData);
let data = real_handler.server_fn_2().await?;
assert_eq!(data, DomainData2);
let data = real_handler.server_fn_3().await?;
assert_eq!(data, DomainData3);
Ok(())
}
}

View File

@@ -0,0 +1,52 @@
#[cfg(feature = "ssr")]
#[tokio::main]
async fn main() {
use axum::Router;
use leptos::logging::log;
use leptos::prelude::*;
use leptos_axum::{generate_route_list, LeptosRoutes};
use leptos_hexagonal_design::{
app::*,
config::config,
server_types::{HandlerStructAlias, ServerState},
};
let conf = get_configuration(None).unwrap();
let addr = conf.leptos_options.site_addr;
let leptos_options = conf.leptos_options;
let routes = generate_route_list(App);
let handler = config();
let handler_c = handler.clone();
let server_state = ServerState {
handler,
leptos_options: leptos_options.clone(),
};
let app = Router::new()
.leptos_routes_with_context(
&server_state,
routes,
move || provide_context(handler_c.clone()),
{
let leptos_options = leptos_options.clone();
move || shell(leptos_options.clone())
},
)
.fallback(leptos_axum::file_and_error_handler::<
ServerState<HandlerStructAlias>,
_,
>(shell))
.with_state(server_state);
log!("listening on http://{}", &addr);
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
axum::serve(listener, app.into_make_service())
.await
.unwrap();
}
#[cfg(not(feature = "ssr"))]
pub fn main() {
// no client-side main function
// unless we want this to work with e.g., Trunk for pure client-side testing
// see lib.rs for hydration function instead
}

View File

@@ -0,0 +1,84 @@
use axum::{
body::Body,
http::{Request, Response},
};
use leptos::prelude::expect_context;
use std::{
future::Future,
pin::Pin,
task::{Context, Poll},
};
use tower::{Layer, Service};
use crate::{
server_types::{HandlerStructAlias, ServerState},
traits::SubDomainTrait1,
};
use pin_project_lite::pin_project;
#[derive(Clone)]
pub struct SubDomain1Layer;
impl<S> Layer<S> for SubDomain1Layer {
type Service = SubDomain1MiddleWare<S>;
fn layer(&self, inner: S) -> Self::Service {
SubDomain1MiddleWare { inner }
}
}
pub struct SubDomain1MiddleWare<S> {
inner: S,
}
impl<S, ReqBody> Service<Request<ReqBody>> for SubDomain1MiddleWare<S>
where
S: Service<Request<ReqBody>, Response = Response<Body>>,
S::Error: std::fmt::Debug,
S::Future: Send + 'static,
{
type Response = S::Response;
type Error = S::Error;
type Future = SubDomain1Future<S::Future>;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.inner.poll_ready(cx)
}
fn call(&mut self, req: Request<ReqBody>) -> Self::Future {
let req_fut = self.inner.call(req);
SubDomain1Future { req_fut }
}
}
pin_project! {
pub struct SubDomain1Future<F> {
#[pin]
req_fut: F,
}
}
impl<F, Err> Future for SubDomain1Future<F>
where
F: Future<Output = Result<Response<Body>, Err>>,
{
type Output = Result<Response<Body>, Err>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.project();
let subdomain_1 = expect_context::<ServerState<HandlerStructAlias>>()
.handler
.sub_domain_1;
let mut subdomain_1_fut = subdomain_1.sub_domain_1_method();
match Pin::as_mut(&mut subdomain_1_fut).poll(cx) {
Poll::Ready(Ok(_)) => {
println!("Middleware for Subdomain 1 Passed, calling request...");
this.req_fut.poll(cx)
}
Poll::Ready(Err(_)) => Poll::Ready(Ok(Response::builder()
.status(http::StatusCode::FORBIDDEN)
.body(Body::from("Access denied"))
.unwrap())),
Poll::Pending => Poll::Pending,
}
}
}

View File

@@ -0,0 +1,102 @@
use super::traits::*;
use leptos::config::LeptosOptions;
use thiserror::Error;
#[derive(Clone)]
pub struct ServerState<Handler: HandlerTrait> {
pub handler: Handler,
pub leptos_options: LeptosOptions,
}
#[cfg(feature = "config_1")]
pub type HandlerStructAlias = HandlerStruct<
SubDomainStruct1<ExternalService1_1, ExternalService2_1>,
SubDomainStruct2<ExternalService1_1>,
>;
#[cfg(not(feature = "config_1"))]
pub type HandlerStructAlias = HandlerStruct<
SubDomainStruct1<ExternalService1_2, ExternalService2_2>,
SubDomainStruct2<ExternalService1_2>,
>;
#[derive(Clone, Default)]
pub struct HandlerStruct<SubDomain1: SubDomainTrait1, SubDomain2: SubDomainTrait2> {
pub sub_domain_1: SubDomain1,
pub sub_domain_2: SubDomain2,
}
#[derive(Clone, Default)]
pub struct SubDomainStruct1<
ExternalService1: ExternalServiceTrait1,
ExternalService2: ExternalServiceTrait2,
> {
pub external_service_1: ExternalService1,
pub external_service_2: ExternalService2,
}
#[derive(Clone, Default)]
pub struct SubDomainStruct2<ExternalService1: ExternalServiceTrait1> {
pub external_service_1: ExternalService1,
}
#[derive(Clone, Default)]
pub struct ExternalService1_1;
#[derive(Clone, Default)]
pub struct ExternalService1_2;
#[derive(Clone, Default)]
pub struct ExternalService2_1;
#[derive(Clone, Default)]
pub struct ExternalService2_2;
#[derive(Clone, Default)]
pub struct ExternalService1;
#[derive(Clone, PartialEq, Debug)]
pub struct DomainData;
#[derive(Clone, PartialEq, Debug)]
pub struct DomainData2;
#[derive(Clone, PartialEq, Debug)]
pub struct DomainData3;
#[derive(Clone, PartialEq, Debug)]
pub struct SubDomain1Data;
#[derive(Clone, PartialEq, Debug)]
pub struct SubDomain2Data;
#[derive(Clone)]
pub struct ExternalService1Data;
#[derive(Clone)]
pub struct ExternalService2Data;
#[derive(Clone, Error, Debug)]
pub enum DomainError {
#[error("Underlying Subdomain 1 Error")]
SubDomain1Error(#[from] SubDomain1Error),
#[error("Underlying Subdomain 2 Error")]
SubDomain2Error(#[from] SubDomain2Error),
}
#[derive(Clone, Error, Debug)]
pub enum SubDomain1Error {
#[error("Sub Domain 1 Error")]
SubDomain1Error,
#[error("Underlying Service 1")]
ExternalService1Error(#[from] ExternalService1Error),
#[error("Underlying Service 2")]
ExternalService2Error(#[from] ExternalService2Error),
}
#[derive(Clone, Error, Debug)]
pub enum SubDomain2Error {
#[error("Sub Domain 2 Error")]
SubDomain2Error,
#[error("Underlying Service 1")]
ExternalService1Error(#[from] ExternalService1Error),
}
#[derive(Clone, Error, Debug)]
pub enum ExternalService1Error {
#[error("Service 1 Error")]
Error,
}
#[derive(Clone, Error, Debug)]
pub enum ExternalService2Error {
#[error("Service 2 Error")]
Error,
}

View File

@@ -0,0 +1,149 @@
use crate::ui_types::*;
use super::server_types::*;
use super::traits::*;
use axum::async_trait;
use axum::extract::FromRef;
use leptos::config::LeptosOptions;
// So we can pass our server state as state into our leptos router.
impl<Handler: HandlerTrait + Clone> FromRef<ServerState<Handler>> for LeptosOptions {
fn from_ref(input: &ServerState<Handler>) -> Self {
input.leptos_options.clone()
}
}
#[async_trait]
impl<SubDomain1, SubDomain2> HandlerTrait for HandlerStruct<SubDomain1, SubDomain2>
where
SubDomain1: SubDomainTrait1 + Send + Sync,
SubDomain2: SubDomainTrait2 + Send + Sync,
{
async fn server_fn_1(&self) -> Result<DomainData, DomainError> {
Ok(self.sub_domain_1.sub_domain_1_method().await?.into())
}
async fn server_fn_2(&self) -> Result<DomainData2, DomainError> {
Ok(self.sub_domain_2.sub_domain_2_method().await?.into())
}
async fn server_fn_3(&self) -> Result<DomainData3, DomainError> {
Ok((
self.sub_domain_1.sub_domain_1_method().await?,
self.sub_domain_2.sub_domain_2_method().await?,
)
.into())
}
}
#[async_trait]
impl<ExternalService1, ExternalService2> SubDomainTrait1
for SubDomainStruct1<ExternalService1, ExternalService2>
where
ExternalService1: ExternalServiceTrait1 + Send + Sync,
ExternalService2: ExternalServiceTrait2 + Send + Sync,
{
async fn sub_domain_1_method(&self) -> Result<SubDomain1Data, SubDomain1Error> {
Ok((
self.external_service_1.external_service_1_method().await?,
self.external_service_2.external_service_2_method().await?,
)
.into())
}
}
#[async_trait]
impl<ExternalService1> SubDomainTrait2 for SubDomainStruct2<ExternalService1>
where
ExternalService1: ExternalServiceTrait1 + Send + Sync,
{
async fn sub_domain_2_method(&self) -> Result<SubDomain2Data, SubDomain2Error> {
Ok(self
.external_service_1
.external_service_1_method()
.await?
.into())
}
}
#[async_trait]
impl ExternalServiceTrait1 for ExternalService1_1 {
async fn external_service_1_method(
&self,
) -> Result<ExternalService1Data, ExternalService1Error> {
println!("External Service 1 From External Service 1_1");
Ok(ExternalService1Data)
}
}
#[async_trait]
impl ExternalServiceTrait1 for ExternalService1_2 {
async fn external_service_1_method(
&self,
) -> Result<ExternalService1Data, ExternalService1Error> {
println!("External Service 1 From External Service 1_2");
Ok(ExternalService1Data)
}
}
#[async_trait]
impl ExternalServiceTrait2 for ExternalService2_1 {
async fn external_service_2_method(
&self,
) -> Result<ExternalService2Data, ExternalService2Error> {
println!("External Service 2 From External Service 2_1");
Ok(ExternalService2Data)
}
}
#[async_trait]
impl ExternalServiceTrait2 for ExternalService2_2 {
async fn external_service_2_method(
&self,
) -> Result<ExternalService2Data, ExternalService2Error> {
println!("External Service 2 From External Service 2_2");
Ok(ExternalService2Data)
}
}
// Sub Domain mapping
impl From<(ExternalService1Data, ExternalService2Data)> for SubDomain1Data {
fn from(_: (ExternalService1Data, ExternalService2Data)) -> Self {
Self
}
}
impl From<ExternalService1Data> for SubDomain2Data {
fn from(_: ExternalService1Data) -> Self {
Self
}
}
// Domain Mapping
impl From<SubDomain1Data> for DomainData {
fn from(_: SubDomain1Data) -> Self {
Self
}
}
impl From<SubDomain2Data> for DomainData2 {
fn from(_: SubDomain2Data) -> Self {
Self
}
}
impl From<(SubDomain1Data, SubDomain2Data)> for DomainData3 {
fn from(_: (SubDomain1Data, SubDomain2Data)) -> Self {
Self
}
}
// Ui Mapping
impl From<DomainData> for UiMappingFromDomainData {
fn from(_: DomainData) -> Self {
Self
}
}
impl From<DomainData2> for UiMappingFromDomainData2 {
fn from(_: DomainData2) -> Self {
Self
}
}
impl From<DomainData3> for UiMappingFromDomainData3 {
fn from(_: DomainData3) -> Self {
Self
}
}

View File

@@ -0,0 +1,42 @@
use super::server_types::*;
use axum::async_trait;
use mockall::automock;
pub trait New {
fn new() -> Self;
}
#[automock]
#[async_trait]
pub trait HandlerTrait {
async fn server_fn_1(&self) -> Result<DomainData, DomainError>;
async fn server_fn_2(&self) -> Result<DomainData2, DomainError>;
async fn server_fn_3(&self) -> Result<DomainData3, DomainError>;
}
#[automock]
#[async_trait]
pub trait SubDomainTrait1 {
async fn sub_domain_1_method(&self) -> Result<SubDomain1Data, SubDomain1Error>;
}
#[automock]
#[async_trait]
pub trait SubDomainTrait2 {
async fn sub_domain_2_method(&self) -> Result<SubDomain2Data, SubDomain2Error>;
}
#[automock]
#[async_trait]
pub trait ExternalServiceTrait1 {
async fn external_service_1_method(
&self,
) -> Result<ExternalService1Data, ExternalService1Error>;
}
#[automock]
#[async_trait]
pub trait ExternalServiceTrait2 {
async fn external_service_2_method(
&self,
) -> Result<ExternalService2Data, ExternalService2Error>;
}

View File

@@ -0,0 +1,8 @@
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
pub struct UiMappingFromDomainData;
#[derive(Serialize, Deserialize)]
pub struct UiMappingFromDomainData2;
#[derive(Serialize, Deserialize)]
pub struct UiMappingFromDomainData3;

View File

@@ -0,0 +1,4 @@
body {
font-family: sans-serif;
text-align: center;
}

View File

@@ -15,7 +15,6 @@ use crate::{
};
pub use arc_memo::*;
pub use async_derived::*;
pub(crate) use inner::MemoInner;
pub use memo::*;
pub use selector::*;

View File

@@ -12,11 +12,10 @@ use crate::{
traits::{DefinedAt, Get, IsDisposed, ReadUntracked},
};
use core::fmt::Debug;
use or_poisoned::OrPoisoned;
use std::{
hash::Hash,
panic::Location,
sync::{Arc, RwLock, Weak},
sync::{Arc, Weak},
};
/// An efficient derived reactive value based on other reactive values.
@@ -95,7 +94,7 @@ where
{
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: &'static Location<'static>,
inner: Arc<RwLock<MemoInner<T, S>>>,
inner: Arc<MemoInner<T, S>>,
}
impl<T: 'static> ArcMemo<T, SyncStorage>
@@ -161,7 +160,7 @@ where
Weak::clone(weak) as Weak<dyn Subscriber + Send + Sync>,
);
RwLock::new(MemoInner::new(Arc::new(fun), subscriber))
MemoInner::new(Arc::new(fun), subscriber)
});
Self {
#[cfg(any(debug_assertions, leptos_debuginfo))]
@@ -312,15 +311,11 @@ where
S: Storage<T>,
{
fn add_source(&self, source: AnySource) {
self.inner.write().or_poisoned().sources.insert(source);
self.inner.add_source(source);
}
fn clear_sources(&self, subscriber: &AnySubscriber) {
self.inner
.write()
.or_poisoned()
.sources
.clear_sources(subscriber);
self.inner.clear_sources(subscriber);
}
}
@@ -328,15 +323,15 @@ impl<T: 'static, S> ReadUntracked for ArcMemo<T, S>
where
S: Storage<T>,
{
type Value = ReadGuard<T, Mapped<Plain<MemoInner<T, S>>, T>>;
type Value = ReadGuard<T, Mapped<Plain<Option<S::Wrapped>>, T>>;
fn try_read_untracked(&self) -> Option<Self::Value> {
self.update_if_necessary();
Mapped::try_new(Arc::clone(&self.inner), |t| {
Mapped::try_new(Arc::clone(&self.inner.value), |t| {
// safe to unwrap here because update_if_necessary
// guarantees the value is Some
t.value.as_ref().unwrap().as_borrowed()
t.as_ref().unwrap().as_borrowed()
})
.map(ReadGuard::new)
}

View File

@@ -15,10 +15,15 @@ pub struct MemoInner<T, S>
where
S: Storage<T>,
{
pub(crate) value: Option<S::Wrapped>,
/// Must always be aquired *after* the reactivity lock
pub(crate) value: Arc<RwLock<Option<S::Wrapped>>>,
#[allow(clippy::type_complexity)]
pub(crate) fun: Arc<dyn Fn(Option<T>) -> (T, bool) + Send + Sync>,
pub(crate) owner: Owner,
pub(crate) reactivity: RwLock<MemoInnerReactivity>,
}
pub(crate) struct MemoInnerReactivity {
pub(crate) state: ReactiveNodeState,
pub(crate) sources: SourceSet,
pub(crate) subscribers: SubscriberSet,
@@ -44,40 +49,44 @@ where
any_subscriber: AnySubscriber,
) -> Self {
Self {
value: None,
value: Arc::new(RwLock::new(None)),
fun,
owner: Owner::new(),
state: ReactiveNodeState::Dirty,
sources: Default::default(),
subscribers: SubscriberSet::new(),
any_subscriber,
reactivity: RwLock::new(MemoInnerReactivity {
state: ReactiveNodeState::Dirty,
sources: Default::default(),
subscribers: SubscriberSet::new(),
any_subscriber,
}),
}
}
}
impl<T: 'static, S> ReactiveNode for RwLock<MemoInner<T, S>>
impl<T: 'static, S> ReactiveNode for MemoInner<T, S>
where
S: Storage<T>,
{
fn mark_dirty(&self) {
self.write().or_poisoned().state = ReactiveNodeState::Dirty;
self.reactivity.write().or_poisoned().state = ReactiveNodeState::Dirty;
self.mark_subscribers_check();
}
fn mark_check(&self) {
{
let mut lock = self.write().or_poisoned();
let mut lock = self.reactivity.write().or_poisoned();
if lock.state != ReactiveNodeState::Dirty {
lock.state = ReactiveNodeState::Check;
}
}
for sub in (&self.read().or_poisoned().subscribers).into_iter() {
for sub in
(&self.reactivity.read().or_poisoned().subscribers).into_iter()
{
sub.mark_check();
}
}
fn mark_subscribers_check(&self) {
let lock = self.read().or_poisoned();
let lock = self.reactivity.read().or_poisoned();
for sub in (&lock.subscribers).into_iter() {
sub.mark_check();
}
@@ -85,7 +94,7 @@ where
fn update_if_necessary(&self) -> bool {
let (state, sources) = {
let inner = self.read().or_poisoned();
let inner = self.reactivity.read().or_poisoned();
(inner.state, inner.sources.clone())
};
@@ -94,32 +103,37 @@ where
ReactiveNodeState::Dirty => true,
ReactiveNodeState::Check => (&sources).into_iter().any(|source| {
source.update_if_necessary()
|| self.read().or_poisoned().state
|| self.reactivity.read().or_poisoned().state
== ReactiveNodeState::Dirty
}),
};
if needs_update {
let (fun, value, owner) = {
let mut lock = self.write().or_poisoned();
(lock.fun.clone(), lock.value.take(), lock.owner.clone())
};
let fun = self.fun.clone();
let owner = self.owner.clone();
// No deadlock risk, because we only hold the value lock.
let value = self.value.write().or_poisoned().take();
let any_subscriber =
{ self.read().or_poisoned().any_subscriber.clone() };
{ self.reactivity.read().or_poisoned().any_subscriber.clone() };
any_subscriber.clear_sources(&any_subscriber);
let (new_value, changed) = owner.with_cleanup(|| {
any_subscriber
.with_observer(|| fun(value.map(StorageAccess::into_taken)))
});
let mut lock = self.write().or_poisoned();
lock.value = Some(S::wrap(new_value));
lock.state = ReactiveNodeState::Clean;
// Two locks are aquired, so order matters.
let mut reactivity_lock = self.reactivity.write().or_poisoned();
{
// Safety: Can block endlessly if the user is has a ReadGuard on the value
let mut value_lock = self.value.write().or_poisoned();
*value_lock = Some(S::wrap(new_value));
}
reactivity_lock.state = ReactiveNodeState::Clean;
if changed {
let subs = lock.subscribers.clone();
drop(lock);
let subs = reactivity_lock.subscribers.clone();
drop(reactivity_lock);
for sub in subs {
// don't trigger reruns of effects/memos
// basically: if one of the observers has triggered this memo to
@@ -128,49 +142,54 @@ where
sub.mark_dirty();
}
}
} else {
drop(reactivity_lock);
}
changed
} else {
if let Ok(mut lock) = self.try_write() {
lock.state = ReactiveNodeState::Clean;
}
let mut lock = self.reactivity.write().or_poisoned();
lock.state = ReactiveNodeState::Clean;
false
}
}
}
impl<T: 'static, S> Source for RwLock<MemoInner<T, S>>
impl<T: 'static, S> Source for MemoInner<T, S>
where
S: Storage<T>,
{
fn add_subscriber(&self, subscriber: AnySubscriber) {
if let Ok(mut lock) = self.try_write() {
lock.subscribers.subscribe(subscriber);
}
let mut lock = self.reactivity.write().or_poisoned();
lock.subscribers.subscribe(subscriber);
}
fn remove_subscriber(&self, subscriber: &AnySubscriber) {
self.write()
self.reactivity
.write()
.or_poisoned()
.subscribers
.unsubscribe(subscriber);
}
fn clear_subscribers(&self) {
self.write().or_poisoned().subscribers.take();
self.reactivity.write().or_poisoned().subscribers.take();
}
}
impl<T: 'static, S> Subscriber for RwLock<MemoInner<T, S>>
impl<T: 'static, S> Subscriber for MemoInner<T, S>
where
S: Storage<T>,
{
fn add_source(&self, source: AnySource) {
self.write().or_poisoned().sources.insert(source);
self.reactivity.write().or_poisoned().sources.insert(source);
}
fn clear_sources(&self, subscriber: &AnySubscriber) {
self.write().or_poisoned().sources.clear_sources(subscriber);
self.reactivity
.write()
.or_poisoned()
.sources
.clear_sources(subscriber);
}
}

View File

@@ -1,4 +1,4 @@
use super::{inner::MemoInner, ArcMemo};
use super::ArcMemo;
use crate::{
owner::{ArenaItem, FromLocal, LocalStorage, Storage, SyncStorage},
signal::{
@@ -306,7 +306,8 @@ where
T: 'static,
S: Storage<ArcMemo<T, S>> + Storage<T>,
{
type Value = ReadGuard<T, Mapped<Plain<MemoInner<T, S>>, T>>;
type Value =
ReadGuard<T, Mapped<Plain<Option<<S as Storage<T>>::Wrapped>>, T>>;
fn try_read_untracked(&self) -> Option<Self::Value> {
self.inner

View File

@@ -135,6 +135,13 @@ pub fn untrack<T>(fun: impl FnOnce() -> T) -> T {
fun()
}
#[doc(hidden)]
#[track_caller]
pub fn untrack_with_diagnostics<T>(fun: impl FnOnce() -> T) -> T {
let _prev = Observer::take();
fun()
}
/// Converts a [`Subscriber`] to a type-erased [`AnySubscriber`].
pub trait ToAnySubscriber {
/// Converts this type to its type-erased equivalent.

View File

@@ -1,7 +1,7 @@
use super::subscriber_traits::AsSubscriberSet;
use crate::{
graph::{ReactiveNode, SubscriberSet},
traits::{DefinedAt, IsDisposed, Notify},
traits::{DefinedAt, IsDisposed, Notify, Track},
};
use std::{
fmt::{Debug, Formatter, Result},
@@ -69,6 +69,22 @@ impl AsSubscriberSet for ArcTrigger {
}
}
impl Notify for Vec<ArcTrigger> {
fn notify(&self) {
for trigger in self {
trigger.notify();
}
}
}
impl Track for Vec<ArcTrigger> {
fn track(&self) {
for trigger in self {
trigger.track();
}
}
}
impl DefinedAt for ArcTrigger {
#[inline(always)]
fn defined_at(&self) -> Option<&'static Location<'static>> {

View File

@@ -3,7 +3,7 @@
/// Types that abstract over signals with values that can be read.
pub mod read {
use crate::{
computed::{ArcMemo, Memo, MemoInner},
computed::{ArcMemo, Memo},
graph::untrack,
owner::{
ArcStoredValue, ArenaItem, FromLocal, LocalStorage, Storage,
@@ -1735,22 +1735,40 @@ pub mod read {
}
/// The content of a [`Signal`] wrapper read guard, variable depending on the signal type.
#[derive(Debug)]
pub enum SignalReadGuard<T: 'static, S: Storage<T>> {
/// A read signal guard.
Read(ReadGuard<T, Plain<T>>),
#[allow(clippy::type_complexity)]
/// A memo guard.
Memo(ReadGuard<T, Mapped<Plain<MemoInner<T, S>>, T>>),
Memo(
ReadGuard<T, Mapped<Plain<Option<<S as Storage<T>>::Wrapped>>, T>>,
),
/// A fake guard for derived signals, the content had to actually be cloned, so it's not a guard but we pretend it is.
Owned(T),
}
impl<T: 'static + std::fmt::Debug, S: Storage<T> + std::fmt::Debug>
std::fmt::Debug for SignalReadGuard<T, S>
where
<S as Storage<T>>::Wrapped: std::fmt::Debug,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Read(arg0) => f.debug_tuple("Read").field(arg0).finish(),
Self::Memo(arg0) => f.debug_tuple("Memo").field(arg0).finish(),
Self::Owned(arg0) => {
f.debug_tuple("Owned").field(arg0).finish()
}
}
}
}
impl<T, S> Clone for SignalReadGuard<T, S>
where
S: Storage<T>,
T: Clone,
Plain<T>: Clone,
Mapped<Plain<MemoInner<T, S>>, T>: Clone,
Mapped<Plain<Option<<S as Storage<T>>::Wrapped>>, T>: Clone,
{
fn clone(&self) -> Self {
match self {

View File

@@ -444,6 +444,69 @@ fn unsync_derived_signal_and_memo() {
assert_eq!(f.get_untracked(), 6);
}
#[cfg(feature = "effects")]
#[tokio::test]
async fn test_memo_multiple_read_guards() {
// regression test for https://github.com/leptos-rs/leptos/issues/3158
let owner = Owner::new();
owner.set();
use imports::*;
_ = Executor::init_tokio();
let owner = Owner::new();
owner.set();
task::LocalSet::new()
.run_until(async {
let memo = Memo::<i32>::new_with_compare(|_| 42, |_, _| true);
Effect::new(move |_| {
let guard_a = memo.read();
let guard_b = memo.read();
assert_eq!(guard_a, 42);
assert_eq!(guard_b, 42);
});
Executor::tick().await;
})
.await
}
#[cfg(feature = "effects")]
#[tokio::test]
async fn test_memo_read_guard_held() {
// regression test for https://github.com/leptos-rs/leptos/issues/3252
let owner = Owner::new();
owner.set();
use imports::*;
_ = Executor::init_tokio();
let owner = Owner::new();
owner.set();
task::LocalSet::new()
.run_until(async {
let source = RwSignal::new(0);
let directly_derived =
Memo::new_with_compare(move |_| source.get(), |_, _| true);
let indirect = Memo::new_with_compare(
move |_| directly_derived.get(),
|_, _| true,
);
Effect::new(move |_| {
let direct_value = directly_derived.read();
let indirect_value = indirect.get();
assert_eq!(direct_value, indirect_value);
});
Executor::tick().await;
source.set(1);
Executor::tick().await;
source.set(2);
Executor::tick().await;
})
.await
}
#[test]
fn memo_updates_even_if_not_read_until_later() {
#![allow(clippy::bool_assert_comparison)]

View File

@@ -23,6 +23,7 @@ tokio = { version = "1.41", features = ["rt-multi-thread", "macros"] }
tokio-test = { version = "0.4.4" }
any_spawner = { workspace = true, features = ["futures-executor", "tokio"] }
reactive_graph = { workspace = true, features = ["effects"] }
leptos = { path = "../leptos", features = ["csr"] }
[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(leptos_debuginfo)'] }

View File

@@ -1,7 +1,7 @@
use crate::{
path::{StorePath, StorePathSegment},
ArcStore, AtIndex, AtKeyed, KeyMap, KeyedSubfield, Store, StoreField,
StoreFieldTrigger, Subfield,
ArcStore, AtIndex, AtKeyed, DerefedField, KeyMap, KeyedSubfield, Store,
StoreField, StoreFieldTrigger, Subfield,
};
use reactive_graph::{
owner::Storage,
@@ -202,6 +202,43 @@ where
}
}
impl<Inner, T> From<DerefedField<Inner>> for ArcField<T>
where
Inner: Clone + StoreField + Send + Sync + 'static,
Inner::Value: Deref<Target = T> + DerefMut,
T: Sized + 'static,
{
#[track_caller]
fn from(value: DerefedField<Inner>) -> Self {
ArcField {
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: Location::caller(),
path: value.path().into_iter().collect(),
trigger: value.get_trigger(value.path().into_iter().collect()),
get_trigger: Arc::new({
let value = value.clone();
move |path| value.get_trigger(path)
}),
read: Arc::new({
let value = value.clone();
move || value.reader().map(StoreFieldReader::new)
}),
write: Arc::new({
let value = value.clone();
move || value.writer().map(StoreFieldWriter::new)
}),
keys: Arc::new({
let value = value.clone();
move || value.keys()
}),
track_field: Arc::new({
let value = value.clone();
move || value.track_field()
}),
}
}
}
impl<Inner, Prev> From<AtIndex<Inner, Prev>> for ArcField<Prev::Output>
where
AtIndex<Inner, Prev>: Clone,

View File

@@ -0,0 +1,165 @@
use crate::{
path::{StorePath, StorePathSegment},
store_field::StoreField,
KeyMap, StoreFieldTrigger,
};
use reactive_graph::{
signal::guards::{Mapped, MappedMut},
traits::{
DefinedAt, IsDisposed, Notify, ReadUntracked, Track, UntrackableGuard,
Write,
},
};
use std::{
ops::{Deref, DerefMut},
panic::Location,
};
/// Maps a store field that is a smart pointer to a subfield of the dereferenced inner type.
pub trait DerefField
where
Self: StoreField,
Self::Value: Deref + DerefMut,
<Self::Value as Deref>::Target: Sized + 'static,
{
/// Returns a new store field with the value mapped to the target type of dereferencing this
/// field
///
/// For example, if you have a store field with a `Box<T>`, `.deref_field()` will return a
/// new store field containing a `T`.
fn deref_field(self) -> DerefedField<Self>;
}
impl<S> DerefField for S
where
S: StoreField,
S::Value: Deref + DerefMut,
<S::Value as Deref>::Target: Sized + 'static,
{
#[track_caller]
fn deref_field(self) -> DerefedField<Self> {
DerefedField {
#[cfg(debug_assertions)]
defined_at: Location::caller(),
inner: self,
}
}
}
/// A wrapper from a store field containing a smart pointer to a store field containing the
/// dereferenced target type of that smart pointer.
#[derive(Debug, Copy, Clone)]
pub struct DerefedField<S> {
inner: S,
#[cfg(debug_assertions)]
defined_at: &'static Location<'static>,
}
impl<S> StoreField for DerefedField<S>
where
S: StoreField,
S::Value: Deref + DerefMut,
<S::Value as Deref>::Target: Sized + 'static,
{
type Value = <S::Value as Deref>::Target;
type Reader = Mapped<S::Reader, Self::Value>;
type Writer = MappedMut<S::Writer, Self::Value>;
fn get_trigger(&self, path: StorePath) -> StoreFieldTrigger {
self.inner.get_trigger(path)
}
fn path(&self) -> impl IntoIterator<Item = StorePathSegment> {
self.inner.path()
}
fn reader(&self) -> Option<Self::Reader> {
let inner = self.inner.reader()?;
Some(Mapped::new_with_guard(inner, |n| n.deref()))
}
fn writer(&self) -> Option<Self::Writer> {
let inner = self.inner.writer()?;
Some(MappedMut::new(inner, |n| n.deref(), |n| n.deref_mut()))
}
#[inline(always)]
fn keys(&self) -> Option<KeyMap> {
self.inner.keys()
}
}
impl<S> DefinedAt for DerefedField<S>
where
S: StoreField,
S::Value: Deref + DerefMut,
<S::Value as Deref>::Target: Sized + 'static,
{
fn defined_at(&self) -> Option<&'static Location<'static>> {
#[cfg(debug_assertions)]
{
Some(self.defined_at)
}
#[cfg(not(debug_assertions))]
{
None
}
}
}
impl<S> IsDisposed for DerefedField<S>
where
S: IsDisposed,
{
fn is_disposed(&self) -> bool {
self.inner.is_disposed()
}
}
impl<S> Notify for DerefedField<S>
where
S: StoreField,
S::Value: Deref + DerefMut,
<S::Value as Deref>::Target: Sized + 'static,
{
fn notify(&self) {
let trigger = self.get_trigger(self.path().into_iter().collect());
trigger.this.notify();
trigger.children.notify();
}
}
impl<S> Track for DerefedField<S>
where
S: StoreField,
S::Value: Deref + DerefMut,
<S::Value as Deref>::Target: Sized + 'static,
{
fn track(&self) {
self.track_field();
}
}
impl<S> ReadUntracked for DerefedField<S>
where
S: StoreField,
S::Value: Deref + DerefMut,
<S::Value as Deref>::Target: Sized + 'static,
{
type Value = <Self as StoreField>::Reader;
fn try_read_untracked(&self) -> Option<Self::Value> {
self.reader()
}
}
impl<S> Write for DerefedField<S>
where
S: StoreField,
S::Value: Deref + DerefMut,
<S::Value as Deref>::Target: Sized + 'static,
{
type Value = <S::Value as Deref>::Target;
fn try_write(&self) -> Option<impl UntrackableGuard<Target = Self::Value>> {
self.writer()
}
fn try_write_untracked(
&self,
) -> Option<impl DerefMut<Target = Self::Value>> {
self.writer().map(|mut writer| {
writer.untrack();
writer
})
}
}

View File

@@ -1,8 +1,8 @@
use crate::{
arc_field::{StoreFieldReader, StoreFieldWriter},
path::{StorePath, StorePathSegment},
ArcField, ArcStore, AtIndex, AtKeyed, KeyMap, KeyedSubfield, Store,
StoreField, StoreFieldTrigger, Subfield,
ArcField, ArcStore, AtIndex, AtKeyed, DerefedField, KeyMap, KeyedSubfield,
Store, StoreField, StoreFieldTrigger, Subfield,
};
use reactive_graph::{
owner::{ArenaItem, Storage, SyncStorage},
@@ -15,7 +15,7 @@ use reactive_graph::{
use std::{
fmt::Debug,
hash::Hash,
ops::{DerefMut, IndexMut},
ops::{Deref, DerefMut, IndexMut},
panic::Location,
};
@@ -115,6 +115,22 @@ where
}
}
impl<Inner, T> From<DerefedField<Inner>> for Field<T>
where
Inner: Clone + StoreField + Send + Sync + 'static,
Inner::Value: Deref<Target = T> + DerefMut,
T: Sized + 'static,
{
#[track_caller]
fn from(value: DerefedField<Inner>) -> Self {
Field {
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: Location::caller(),
inner: ArenaItem::new_with_storage(value.into()),
}
}
}
impl<Inner, Prev, S> From<AtIndex<Inner, Prev>> for Field<Prev::Output, S>
where
AtIndex<Inner, Prev>: Clone,

View File

@@ -67,7 +67,141 @@
//! completed: false,
//! });
//! ```
//! ### Generated traits
//! The [`Store`](macro@Store) macro generates traits for each `struct` to which it is applied. When working
//! within a single file more module, this is not an issue. However, when working with multiple modules
//! or files, one needs to `use` the generated traits. The general pattern is that for each `struct`
//! named `Foo`, the macro generates a trait named `FooStoreFields`. For example:
//! ```rust
//! pub mod foo {
//! use reactive_stores::Store;
//! #[derive(Store)]
//! pub struct Foo {
//! field: i32,
//! }
//! }
//!
//! pub mod user {
//! use leptos::prelude::*;
//! use reactive_stores::Field;
//! // Using FooStore fields here.
//! use crate::foo::{ Foo, FooStoreFields };
//!
//! #[component]
//! pub fn UseFoo(foo: Field<Foo>) {
//! // Without FooStoreFields, foo.field() would fail to compile.
//! println!("field: {}", foo.field().read());
//! }
//! }
//!
//! # fn main() {
//! # }
//! ```
//!
//! ### Additional field types
//!
//! Most of the time, your structs will have fields as in the example above: the struct is comprised
//! of primitive types, builtin types like [String], or other structs that implement [Store](struct@Store) or [Field].
//! However, there are some special cases that require some additional understanding.
//!
//! #### Option
//! [`Option<T>`](std::option::Option) behaves pretty much as you would expect, utilizing [.is_some()](std::option::Option::is_some)
//! and [.is_none()](std::option::Option::is_none) to check the value and [.unwrap()](OptionStoreExt::unwrap) method to access the inner value. The [OptionStoreExt]
//! trait is required to use the [.unwrap()](OptionStoreExt::unwrap) method. Here is a quick example:
//! ```rust
//! // Including the trait OptionStoreExt here is required to use unwrap()
//! use reactive_stores::{OptionStoreExt, Store};
//! use reactive_graph::traits::{Get, Read};
//!
//! #[derive(Store)]
//! struct StructWithOption {
//! opt_field: Option<i32>,
//! }
//!
//! fn describe(store: &Store<StructWithOption>) -> String {
//! if store.opt_field().read().is_some() {
//! // Note here we need to use OptionStoreExt or unwrap() would not compile
//! format!("store has a value {}", store.opt_field().unwrap().get())
//! } else {
//! format!("store has no value")
//! }
//! }
//! let none_store = Store::new(StructWithOption { opt_field: None });
//! let some_store = Store::new(StructWithOption { opt_field: Some(42)});
//!
//! assert_eq!(describe(&none_store), "store has no value");
//! assert_eq!(describe(&some_store), "store has a value 42");
//! ```
//! #### Vec
//! [`Vec<T>`](std::vec::Vec) requires some special treatment when trying to access
//! elements of the vector directly. Use the [StoreFieldIterator::at_unkeyed()] method to
//! access a particular value in a [struct@Store] or [Field] for a [std::vec::Vec]. For example:
//! ```rust
//! # use reactive_stores::Store;
//! // Needed to use at_unkeyed() on Vec
//! use reactive_stores::StoreFieldIter;
//! use crate::reactive_stores::StoreFieldIterator;
//! use reactive_graph::traits::Read;
//! use reactive_graph::traits::Get;
//!
//! #[derive(Store)]
//! struct StructWithVec {
//! vec_field: Vec<i32>,
//! }
//!
//! let store = Store::new(StructWithVec { vec_field: vec![1, 2, 3] });
//!
//! assert_eq!(store.vec_field().at_unkeyed(0).get(), 1);
//! assert_eq!(store.vec_field().at_unkeyed(1).get(), 2);
//! assert_eq!(store.vec_field().at_unkeyed(2).get(), 3);
//! ```
//! #### Enum
//! Enumerated types behave a bit differently as the [`Store`](macro@Store) macro builds underlying traits instead of alternate
//! enumerated structures. Each element in an `Enum` generates methods to access it in the store: a
//! method with the name of the field gives a boolean if the `Enum` is that variant, and possible accessor
//! methods for anonymous fields of that variant. For example:
//! ```rust
//! use reactive_stores::Store;
//! use reactive_graph::traits::{Read, Get};
//!
//! #[derive(Store)]
//! enum Choices {
//! First,
//! Second(String),
//! }
//!
//! let choice_one = Store::new(Choices::First);
//! let choice_two = Store::new(Choices::Second("hello".to_string()));
//!
//! assert!(choice_one.first());
//! assert!(!choice_one.second());
//! // Note the use of the accessor method here .second_0()
//! assert_eq!(choice_two.second_0().unwrap().get(), "hello");
//! ```
//! #### Box
//! [`Box<T>`](std::boxed::Box) also requires some special treatment in how you dereference elements of the Box, especially
//! when trying to build a recursive data structure. [DerefField](trait@DerefField) provides a [.deref_value()](DerefField::deref_field) method to access
//! the inner value. For example:
//! ```rust
//! // Note here we need to use DerefField to use deref_field() and OptionStoreExt to use unwrap()
//! use reactive_stores::{Store, DerefField, OptionStoreExt};
//! use reactive_graph::traits::{ Read, Get };
//!
//! #[derive(Store)]
//! struct List {
//! value: i32,
//! #[store]
//! child: Option<Box<List>>,
//! }
//!
//! let tree = Store::new(List {
//! value: 1,
//! child: Some(Box::new(List { value: 2, child: None })),
//! });
//!
//! assert_eq!(tree.child().unwrap().deref_field().value().get(), 2);
//! ```
//! ### Implementation Notes
//!
//! Every struct field can be understood as an index. For example, given the following definition
@@ -131,6 +265,7 @@ use std::{
};
mod arc_field;
mod deref;
mod field;
mod iter;
mod keyed;
@@ -141,6 +276,7 @@ mod store_field;
mod subfield;
pub use arc_field::ArcField;
pub use deref::*;
pub use field::Field;
pub use iter::*;
pub use keyed::*;
@@ -621,6 +757,7 @@ mod tests {
use crate::{self as reactive_stores, Patch, Store, StoreFieldIterator};
use reactive_graph::{
effect::Effect,
owner::StoredValue,
traits::{Read, ReadUntracked, Set, Update, Write},
};
use std::sync::{
@@ -951,4 +1088,118 @@ mod tests {
pub struct StructWithOption {
opt_field: Option<Todo>,
}
// regression test for https://github.com/leptos-rs/leptos/issues/3523
#[tokio::test]
async fn notifying_all_descendants() {
use reactive_graph::traits::*;
_ = any_spawner::Executor::init_tokio();
#[derive(Debug, Clone, Store, Patch, Default)]
struct Foo {
id: i32,
bar: Bar,
}
#[derive(Debug, Clone, Store, Patch, Default)]
struct Bar {
bar_signature: i32,
baz: Baz,
}
#[derive(Debug, Clone, Store, Patch, Default)]
struct Baz {
more_data: i32,
baw: Baw,
}
#[derive(Debug, Clone, Store, Patch, Default)]
struct Baw {
more_data: i32,
end: i32,
}
let store = Store::new(Foo {
id: 42,
bar: Bar {
bar_signature: 69,
baz: Baz {
more_data: 9999,
baw: Baw {
more_data: 22,
end: 1112,
},
},
},
});
let store_runs = StoredValue::new(0);
let id_runs = StoredValue::new(0);
let bar_runs = StoredValue::new(0);
let bar_signature_runs = StoredValue::new(0);
let bar_baz_runs = StoredValue::new(0);
let more_data_runs = StoredValue::new(0);
let baz_baw_end_runs = StoredValue::new(0);
Effect::new_sync(move |_| {
println!("foo: {:?}", store.get());
*store_runs.write_value() += 1;
});
Effect::new_sync(move |_| {
println!("foo.id: {:?}", store.id().get());
*id_runs.write_value() += 1;
});
Effect::new_sync(move |_| {
println!("foo.bar: {:?}", store.bar().get());
*bar_runs.write_value() += 1;
});
Effect::new_sync(move |_| {
println!(
"foo.bar.bar_signature: {:?}",
store.bar().bar_signature().get()
);
*bar_signature_runs.write_value() += 1;
});
Effect::new_sync(move |_| {
println!("foo.bar.baz: {:?}", store.bar().baz().get());
*bar_baz_runs.write_value() += 1;
});
Effect::new_sync(move |_| {
println!(
"foo.bar.baz.more_data: {:?}",
store.bar().baz().more_data().get()
);
*more_data_runs.write_value() += 1;
});
Effect::new_sync(move |_| {
println!(
"foo.bar.baz.baw.end: {:?}",
store.bar().baz().baw().end().get()
);
*baz_baw_end_runs.write_value() += 1;
});
println!("[INITIAL EFFECT RUN]");
tick().await;
println!("\n\n[SETTING STORE]");
store.set(Default::default());
tick().await;
println!("\n\n[SETTING STORE.BAR.BAZ]");
store.bar().baz().set(Default::default());
tick().await;
assert_eq!(store_runs.get_value(), 3);
assert_eq!(id_runs.get_value(), 2);
assert_eq!(bar_runs.get_value(), 3);
assert_eq!(bar_signature_runs.get_value(), 2);
assert_eq!(bar_baz_runs.get_value(), 3);
assert_eq!(more_data_runs.get_value(), 3);
assert_eq!(baz_baw_end_runs.get_value(), 3);
}
}

View File

@@ -34,6 +34,16 @@ impl StorePath {
*last = segment.into();
}
}
/// Returns `true` if the path contains no elements.
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
/// Returns the number of elements in the path.
pub fn len(&self) -> usize {
self.0.len()
}
}
/// One segment of a [`StorePath`].

View File

@@ -74,7 +74,7 @@ where
{
type Value = T;
type Reader = Mapped<Inner::Reader, T>;
type Writer = MappedMut<WriteGuard<ArcTrigger, Inner::Writer>, T>;
type Writer = MappedMut<WriteGuard<Vec<ArcTrigger>, Inner::Writer>, T>;
fn path(&self) -> impl IntoIterator<Item = StorePathSegment> {
self.inner
@@ -94,8 +94,24 @@ where
fn writer(&self) -> Option<Self::Writer> {
let trigger = self.get_trigger(self.path().into_iter().collect());
let inner = WriteGuard::new(trigger.children, self.inner.writer()?);
Some(MappedMut::new(inner, self.read, self.write))
let mut parent = self.inner.writer()?;
parent.untrack();
let mut full_path = self.path().into_iter().collect::<StorePath>();
full_path.pop();
let mut triggers = Vec::with_capacity(full_path.len());
triggers.push(trigger.this.clone());
loop {
let inner = self.get_trigger(full_path.clone());
triggers.push(inner.children.clone());
if full_path.is_empty() {
break;
}
full_path.pop();
}
let guard = WriteGuard::new(triggers, parent);
Some(MappedMut::new(guard, self.read, self.write))
}
#[inline(always)]
@@ -105,10 +121,19 @@ where
#[track_caller]
fn track_field(&self) {
let inner = self
.inner
.get_trigger(self.inner.path().into_iter().collect());
inner.this.track();
let mut full_path = self.path().into_iter().collect::<StorePath>();
// tracks `this` for all ancestors: i.e., it will track any change that is made
// directly to one of its ancestors, but not a change made to a *child* of an ancestor
// (which would end up with every subfield tracking its own siblings, because they are
// children of its parent)
loop {
let inner = self.get_trigger(full_path.clone());
inner.this.track();
if full_path.is_empty() {
break;
}
full_path.pop();
}
let trigger = self.get_trigger(self.path().into_iter().collect());
trigger.this.track();
trigger.children.track();

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_router"
version = "0.7.4"
version = "0.8.0-alpha"
authors = ["Greg Johnston", "Ben Wishovich"]
license = "MIT"
readme = "../README.md"

View File

@@ -10,6 +10,7 @@ use crate::{
use any_spawner::Executor;
use either_of::Either;
use futures::FutureExt;
use leptos::attr::any_attribute::AnyAttribute;
use reactive_graph::{
computed::{ArcMemo, ScopedFuture},
owner::{provide_context, Owner},
@@ -25,7 +26,7 @@ use tachys::{
ssr::StreamBuilder,
view::{
add_attr::AddAnyAttr,
any_view::{AnyView, AnyViewState, IntoAny},
any_view::{AnyView, AnyViewState, ExtraAttrsMut, IntoAny},
Mountable, Position, PositionState, Render, RenderHtml,
},
};
@@ -79,7 +80,7 @@ where
{
type State = Rc<RefCell<FlatRoutesViewState>>;
fn build(self) -> Self::State {
fn build(self, extra_attrs: Option<Vec<AnyAttribute>>) -> Self::State {
let FlatRoutesView {
current_url,
routes,
@@ -117,7 +118,7 @@ where
match new_match {
None => Rc::new(RefCell::new(FlatRoutesViewState {
view: fallback().into_any().build(),
view: fallback().into_any().build(extra_attrs),
id,
owner,
params,
@@ -150,7 +151,7 @@ where
match view.as_mut().now_or_never() {
Some(view) => Rc::new(RefCell::new(FlatRoutesViewState {
view: view.into_any().build(),
view: view.into_any().build(extra_attrs),
id,
owner,
params,
@@ -161,7 +162,7 @@ where
None => {
let state =
Rc::new(RefCell::new(FlatRoutesViewState {
view: ().into_any().build(),
view: ().into_any().build(extra_attrs.clone()),
id,
owner,
params,
@@ -174,8 +175,10 @@ where
let state = Rc::clone(&state);
async move {
let view = view.await;
view.into_any()
.rebuild(&mut state.borrow_mut().view);
view.into_any().rebuild(
&mut state.borrow_mut().view,
extra_attrs,
);
}
});
@@ -186,7 +189,11 @@ where
}
}
fn rebuild(self, state: &mut Self::State) {
fn rebuild(
self,
state: &mut Self::State,
extra_attrs: Option<Vec<AnyAttribute>>,
) {
let FlatRoutesView {
current_url,
location,
@@ -264,7 +271,9 @@ where
provide_context(url);
provide_context(params_memo);
provide_context(Matched(ArcMemo::from(new_matched)));
fallback().into_any().rebuild(&mut state.borrow_mut().view)
fallback()
.into_any()
.rebuild(&mut state.borrow_mut().view, extra_attrs)
});
if let Some(location) = location {
location.ready_to_complete();
@@ -314,8 +323,10 @@ where
== spawned_path
{
let rebuild = move || {
view.into_any()
.rebuild(&mut state.borrow_mut().view);
view.into_any().rebuild(
&mut state.borrow_mut().view,
extra_attrs,
);
};
if transition {
start_view_transition(0, is_back, rebuild);
@@ -343,7 +354,7 @@ impl<Loc, Defs, FalFn, Fal> AddAnyAttr for FlatRoutesView<Loc, Defs, FalFn>
where
Loc: LocationProvider + Send,
Defs: MatchNestedRoutes + Send + 'static,
FalFn: FnOnce() -> Fal + Send,
FalFn: FnOnce() -> Fal + Send + 'static,
Fal: RenderHtml + 'static,
{
type Output<SomeNewAttr: leptos::attr::Attribute> =
@@ -416,16 +427,20 @@ impl<Loc, Defs, FalFn, Fal> RenderHtml for FlatRoutesView<Loc, Defs, FalFn>
where
Loc: LocationProvider + Send,
Defs: MatchNestedRoutes + Send + 'static,
FalFn: FnOnce() -> Fal + Send,
FalFn: FnOnce() -> Fal + Send + 'static,
Fal: RenderHtml + 'static,
{
type AsyncOutput = Self;
type Owned = Self;
const MIN_LENGTH: usize = <Either<Fal, AnyView> as RenderHtml>::MIN_LENGTH;
fn dry_resolve(&mut self) {}
fn dry_resolve(&mut self, _extra_attrs: ExtraAttrsMut<'_>) {}
async fn resolve(self) -> Self::AsyncOutput {
async fn resolve(
self,
_extra_attrs: ExtraAttrsMut<'_>,
) -> Self::AsyncOutput {
self
}
@@ -435,6 +450,7 @@ where
position: &mut Position,
escape: bool,
mark_branches: bool,
extra_attrs: Option<Vec<AnyAttribute>>,
) {
// if this is being run on the server for the first time, generating all possible routes
if RouteList::is_generating() {
@@ -481,7 +497,13 @@ where
RouteList::register(RouteList::from(routes));
} else {
let view = self.choose_ssr();
view.to_html_with_buf(buf, position, escape, mark_branches);
view.to_html_with_buf(
buf,
position,
escape,
mark_branches,
extra_attrs,
);
}
}
@@ -491,6 +513,7 @@ where
position: &mut Position,
escape: bool,
mark_branches: bool,
extra_attrs: Option<Vec<AnyAttribute>>,
) where
Self: Sized,
{
@@ -500,6 +523,7 @@ where
position,
escape,
mark_branches,
extra_attrs,
)
}
@@ -507,6 +531,7 @@ where
self,
cursor: &Cursor,
position: &PositionState,
extra_attrs: Option<Vec<AnyAttribute>>,
) -> Self::State {
// this can be mostly the same as the build() implementation, but with hydrate()
//
@@ -551,9 +576,11 @@ where
match new_match {
None => Rc::new(RefCell::new(FlatRoutesViewState {
view: fallback()
.into_any()
.hydrate::<FROM_SERVER>(cursor, position),
view: fallback().into_any().hydrate::<FROM_SERVER>(
cursor,
position,
extra_attrs,
),
id,
owner,
params,
@@ -586,9 +613,11 @@ where
match view.as_mut().now_or_never() {
Some(view) => Rc::new(RefCell::new(FlatRoutesViewState {
view: view
.into_any()
.hydrate::<FROM_SERVER>(cursor, position),
view: view.into_any().hydrate::<FROM_SERVER>(
cursor,
position,
extra_attrs,
),
id,
owner,
params,
@@ -604,4 +633,8 @@ where
}
}
}
fn into_owned(self) -> Self::Owned {
self
}
}

View File

@@ -1,4 +1,5 @@
use super::{PartialPathMatch, PathSegment};
use std::sync::Arc;
mod param_segments;
mod static_segment;
mod tuples;
@@ -11,9 +12,37 @@ pub use static_segment::*;
/// This is a "horizontal" matching: i.e., it treats a tuple of route segments
/// as subsequent segments of the URL and tries to match them all.
pub trait PossibleRouteMatch {
const OPTIONAL: bool = false;
fn optional(&self) -> bool;
fn test<'a>(&self, path: &'a str) -> Option<PartialPathMatch<'a>>;
fn generate_path(&self, path: &mut Vec<PathSegment>);
}
impl PossibleRouteMatch for Box<dyn PossibleRouteMatch + Send + Sync> {
fn optional(&self) -> bool {
(**self).optional()
}
fn test<'a>(&self, path: &'a str) -> Option<PartialPathMatch<'a>> {
(**self).test(path)
}
fn generate_path(&self, path: &mut Vec<PathSegment>) {
(**self).generate_path(path);
}
}
impl PossibleRouteMatch for Arc<dyn PossibleRouteMatch + Send + Sync> {
fn optional(&self) -> bool {
(**self).optional()
}
fn test<'a>(&self, path: &'a str) -> Option<PartialPathMatch<'a>> {
(**self).test(path)
}
fn generate_path(&self, path: &mut Vec<PathSegment>) {
(**self).generate_path(path);
}
}

View File

@@ -35,6 +35,10 @@ use std::borrow::Cow;
pub struct ParamSegment(pub &'static str);
impl PossibleRouteMatch for ParamSegment {
fn optional(&self) -> bool {
false
}
fn test<'a>(&self, path: &'a str) -> Option<PartialPathMatch<'a>> {
let mut matched_len = 0;
let mut param_offset = 0;
@@ -121,6 +125,10 @@ impl PossibleRouteMatch for ParamSegment {
pub struct WildcardSegment(pub &'static str);
impl PossibleRouteMatch for WildcardSegment {
fn optional(&self) -> bool {
false
}
fn test<'a>(&self, path: &'a str) -> Option<PartialPathMatch<'a>> {
let mut matched_len = 0;
let mut param_offset = 0;
@@ -158,7 +166,9 @@ impl PossibleRouteMatch for WildcardSegment {
pub struct OptionalParamSegment(pub &'static str);
impl PossibleRouteMatch for OptionalParamSegment {
const OPTIONAL: bool = true;
fn optional(&self) -> bool {
true
}
fn test<'a>(&self, path: &'a str) -> Option<PartialPathMatch<'a>> {
let mut matched_len = 0;

View File

@@ -2,6 +2,10 @@ use super::{PartialPathMatch, PathSegment, PossibleRouteMatch};
use std::fmt::Debug;
impl PossibleRouteMatch for () {
fn optional(&self) -> bool {
false
}
fn test<'a>(&self, path: &'a str) -> Option<PartialPathMatch<'a>> {
Some(PartialPathMatch::new(path, vec![], ""))
}
@@ -54,6 +58,10 @@ impl AsPath for &'static str {
pub struct StaticSegment<T: AsPath>(pub T);
impl<T: AsPath> PossibleRouteMatch for StaticSegment<T> {
fn optional(&self) -> bool {
false
}
fn test<'a>(&self, path: &'a str) -> Option<PartialPathMatch<'a>> {
let mut matched_len = 0;
let mut test = path.chars().peekable();

Some files were not shown because too many files have changed in this diff Show More