Compare commits

...

27 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
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
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
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
106 changed files with 3900 additions and 2089 deletions

552
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

@@ -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

@@ -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>
@@ -93,6 +106,10 @@ impl<In, Out> UnsyncCallback<In, Out> {
}
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))
}
@@ -168,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))
}
}
@@ -181,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 {
@@ -239,7 +264,9 @@ impl<In: 'static, Out: 'static> Callback<In, Out> {
#[cfg(test)]
mod tests {
use super::Callable;
use crate::callback::{Callback, UnsyncCallback};
use reactive_graph::traits::Dispose;
struct NoClone {}
@@ -270,6 +297,22 @@ mod tests {
(|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);

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

@@ -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"

View File

@@ -919,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,
) {

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

@@ -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();

View File

@@ -8,14 +8,20 @@ macro_rules! tuples {
$first: PossibleRouteMatch,
$($ty: PossibleRouteMatch),*,
{
fn test<'a>(&self, path: &'a str) -> Option<PartialPathMatch<'a>> {
// on the first run, include all optionals
let mut include_optionals = {
[$first::OPTIONAL, $($ty::OPTIONAL),*].into_iter().filter(|n| *n).count()
};
fn optional(&self) -> bool {
#[allow(non_snake_case)]
let ($first, $($ty,)*) = &self;
[$first.optional(), $($ty.optional()),*].into_iter().any(|n| n)
}
fn test<'a>(&self, path: &'a str) -> Option<PartialPathMatch<'a>> {
#[allow(non_snake_case)]
let ($first, $($ty,)*) = &self;
// on the first run, include all optionals
let mut include_optionals = {
[$first.optional(), $($ty.optional()),*].into_iter().filter(|n| *n).count()
};
loop {
let mut nth_field = 0;
@@ -25,7 +31,7 @@ macro_rules! tuples {
let mut p = Vec::new();
let mut m = String::new();
if !$first::OPTIONAL || nth_field < include_optionals {
if !$first.optional() || nth_field < include_optionals {
match $first.test(r) {
None => {
return None;
@@ -40,16 +46,16 @@ macro_rules! tuples {
matched_len += m.len();
$(
if $ty::OPTIONAL {
if $ty.optional() {
nth_field += 1;
}
if !$ty::OPTIONAL || nth_field < include_optionals {
if !$ty.optional() || nth_field < include_optionals {
let PartialPathMatch {
remaining,
matched,
params
} = match $ty.test(r) {
None => if $ty::OPTIONAL {
None => if $ty.optional() {
return None;
} else {
if include_optionals == 0 {
@@ -90,6 +96,10 @@ where
Self: core::fmt::Debug,
A: PossibleRouteMatch,
{
fn optional(&self) -> bool {
self.0.optional()
}
fn test<'a>(&self, path: &'a str) -> Option<PartialPathMatch<'a>> {
let remaining = path;
let PartialPathMatch {

View File

@@ -151,7 +151,7 @@ impl<Segments, Children, Data, View> MatchNestedRoutes
for NestedRoute<Segments, Children, Data, View>
where
Self: 'static,
Segments: PossibleRouteMatch + std::fmt::Debug,
Segments: PossibleRouteMatch,
Children: MatchNestedRoutes,
Children::Match: MatchParams,
Children: 'static,

View File

@@ -10,7 +10,7 @@ use crate::{
use any_spawner::Executor;
use either_of::{Either, EitherOf3};
use futures::{channel::oneshot, future::join_all, FutureExt};
use leptos::{component, oco::Oco};
use leptos::{attr::any_attribute::AnyAttribute, component, oco::Oco};
use or_poisoned::OrPoisoned;
use reactive_graph::{
computed::{ArcMemo, ScopedFuture},
@@ -36,7 +36,7 @@ use tachys::{
ssr::StreamBuilder,
view::{
add_attr::AddAnyAttr,
any_view::{AnyView, IntoAny},
any_view::{AnyView, ExtraAttrsMut, IntoAny},
either::EitherOf3State,
Mountable, Position, PositionState, Render, RenderHtml,
},
@@ -76,7 +76,7 @@ where
// TODO support fallback while loading
type State = NestedRouteViewState<Fal>;
fn build(self) -> Self::State {
fn build(self, extra_attrs: Option<Vec<AnyAttribute>>) -> Self::State {
let NestedRoutesView {
routes,
outer_owner,
@@ -95,7 +95,7 @@ where
let new_match = routes.match_route(url.path());
// start with an empty view because we'll be loading routes async
let view = EitherOf3::A(()).build();
let view = EitherOf3::A(()).build(extra_attrs.clone());
let view = Rc::new(RefCell::new(view));
let matched_view = match new_match {
None => EitherOf3::B(fallback()),
@@ -120,7 +120,7 @@ where
for trigger in triggers {
trigger.notify();
}
matched_view.rebuild(&mut *view.borrow_mut());
matched_view.rebuild(&mut *view.borrow_mut(), extra_attrs);
})
});
@@ -132,7 +132,11 @@ where
}
}
fn rebuild(self, state: &mut Self::State) {
fn rebuild(
self,
state: &mut Self::State,
extra_attrs: Option<Vec<AnyAttribute>>,
) {
let url_snapshot = self.current_url.get_untracked();
// if the path is the same, we do not need to re-route
@@ -154,7 +158,7 @@ where
match new_match {
None => {
EitherOf3::<(), Fal, AnyView>::B((self.fallback)())
.rebuild(&mut state.view.borrow_mut());
.rebuild(&mut state.view.borrow_mut(), extra_attrs);
state.outlets.clear();
if let Some(loc) = self.location {
loc.ready_to_complete();
@@ -213,7 +217,10 @@ where
if matches!(state.view.borrow().state, EitherOf3::B(_)) {
self.outer_owner.with(|| {
EitherOf3::<(), Fal, AnyView>::C(Outlet().into_any())
.rebuild(&mut *state.view.borrow_mut());
.rebuild(
&mut *state.view.borrow_mut(),
extra_attrs,
);
})
}
}
@@ -228,8 +235,8 @@ where
impl<Loc, Defs, Fal, FalFn> AddAnyAttr for NestedRoutesView<Loc, Defs, FalFn>
where
Loc: LocationProvider + Send,
Defs: MatchNestedRoutes + Send,
FalFn: FnOnce() -> Fal + Send,
Defs: MatchNestedRoutes + Send + 'static,
FalFn: FnOnce() -> Fal + Send + 'static,
Fal: RenderHtml + 'static,
{
type Output<SomeNewAttr: leptos::attr::Attribute> =
@@ -249,17 +256,21 @@ where
impl<Loc, Defs, FalFn, Fal> RenderHtml for NestedRoutesView<Loc, Defs, FalFn>
where
Loc: LocationProvider + Send,
Defs: MatchNestedRoutes + Send,
FalFn: FnOnce() -> Fal + Send,
Defs: MatchNestedRoutes + Send + 'static,
FalFn: FnOnce() -> Fal + Send + 'static,
Fal: RenderHtml + 'static,
{
type AsyncOutput = Self;
type Owned = Self;
const MIN_LENGTH: usize = 0; // TODO
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
}
@@ -269,6 +280,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() {
@@ -348,7 +360,13 @@ where
outer_owner.with(|| Either::Right(Outlet().into_any()))
}
};
view.to_html_with_buf(buf, position, escape, mark_branches);
view.to_html_with_buf(
buf,
position,
escape,
mark_branches,
extra_attrs,
);
}
}
@@ -358,6 +376,7 @@ where
position: &mut Position,
escape: bool,
mark_branches: bool,
extra_attrs: Option<Vec<AnyAttribute>>,
) where
Self: Sized,
{
@@ -400,6 +419,7 @@ where
position,
escape,
mark_branches,
extra_attrs,
);
}
@@ -407,6 +427,7 @@ where
self,
cursor: &Cursor,
position: &PositionState,
extra_attrs: Option<Vec<AnyAttribute>>,
) -> Self::State {
let NestedRoutesView {
routes,
@@ -446,7 +467,7 @@ where
outer_owner.with(|| EitherOf3::C(Outlet().into_any()))
}
}
.hydrate::<FROM_SERVER>(cursor, position),
.hydrate::<FROM_SERVER>(cursor, position, extra_attrs),
));
NestedRouteViewState {
@@ -456,6 +477,10 @@ where
view,
}
}
fn into_owned(self) -> Self::Owned {
self
}
}
type OutletViewFn = Box<dyn Fn() -> Suspend<AnyView> + Send>;

View File

@@ -89,7 +89,7 @@ where
type State =
ReactiveRouterInnerState<Rndr, Loc, Defs, FallbackFn, Fallback>;
fn build(self) -> Self::State {
fn build(self, _extra_attrs: Option<Vec<AnyAttribute>>) -> Self::State {
let (prev_id, inner) = self.inner.fallback_or_view();
let owner = self.owner.with(Owner::new);
ReactiveRouterInnerState {
@@ -100,7 +100,11 @@ where
}
}
fn rebuild(self, state: &mut Self::State) {
fn rebuild(
self,
state: &mut Self::State,
_extra_attrs: Option<Vec<AnyAttribute>>,
) {
let (new_id, view) = self.inner.fallback_or_view();
if new_id != state.prev_id {
state.owner = self.owner.with(Owner::new)
@@ -130,6 +134,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() {
@@ -156,6 +161,7 @@ where
position: &mut Position,
escape: bool,
mark_branches: bool,
_extra_attrs: Option<Vec<AnyAttribute>>,
) where
Self: Sized,
{
@@ -169,6 +175,7 @@ where
self,
cursor: &Cursor,
position: &PositionState,
_extra_attrs: Option<Vec<AnyAttribute>>,
) -> Self::State {
let (prev_id, inner) = self.inner.fallback_or_view();
let owner = self.owner.with(Owner::new);
@@ -280,7 +287,7 @@ where
{
type State = ReactiveRouteState<View::State>;
fn build(self) -> Self::State {
fn build(self, extra_attrs: Option<Vec<AnyAttribute>>) -> Self::State {
let MatchedRoute {
search_params,
params,
@@ -291,14 +298,19 @@ where
params: ArcRwSignal::new(params),
matched: ArcRwSignal::new(matched),
};
let view_state = untrack(|| (self.view_fn)(&matched).build());
let view_state =
untrack(|| (self.view_fn)(&matched).build(extra_attrs.clone()));
ReactiveRouteState {
matched,
view_state,
}
}
fn rebuild(mut self, state: &mut Self::State) {
fn rebuild(
mut self,
state: &mut Self::State,
_extra_attrs: Option<Vec<AnyAttribute>>,
) {
let ReactiveRouteState { matched, .. } = state;
matched
.search_params
@@ -324,6 +336,7 @@ where
position: &mut Position,
escape: bool,
mark_branches: bool,
extra_attrs: Option<Vec<AnyAttribute>>,
) {
let MatchedRoute {
search_params,
@@ -336,7 +349,12 @@ where
matched: ArcRwSignal::new(matched),
};
untrack(|| {
(self.view_fn)(&matched).to_html_with_buf(buf, position, escape)
(self.view_fn)(&matched).to_html_with_buf(
buf,
position,
escape,
extra_attrs.clone(),
)
});
}
@@ -346,6 +364,7 @@ where
position: &mut Position,
escape: bool,
mark_branches: bool,
extra_attrs: Option<Vec<AnyAttribute>>,
) where
Self: Sized,
{
@@ -360,8 +379,12 @@ where
matched: ArcRwSignal::new(matched),
};
untrack(|| {
(self.view_fn)(&matched)
.to_html_async_with_buf::<OUT_OF_ORDER>(buf, position, escape)
(self.view_fn)(&matched).to_html_async_with_buf::<OUT_OF_ORDER>(
buf,
position,
escape,
extra_attrs.clone(),
)
});
}
@@ -369,6 +392,7 @@ where
self,
cursor: &Cursor,
position: &PositionState,
extra_attrs: Option<Vec<AnyAttribute>>,
) -> Self::State {
let MatchedRoute {
search_params,
@@ -381,7 +405,11 @@ where
matched: ArcRwSignal::new(matched),
};
let view_state = untrack(|| {
(self.view_fn)(&matched).hydrate::<FROM_SERVER>(cursor, position)
(self.view_fn)(&matched).hydrate::<FROM_SERVER>(
cursor,
position,
extra_attrs.clone(),
)
});
ReactiveRouteState {
matched,

View File

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

View File

@@ -14,6 +14,7 @@ throw_error = { workspace = true }
server_fn_macro_default = { workspace = true }
# used for hashing paths in #[server] macro
const_format = "0.2.33"
const-str = "0.5.7"
xxhash-rust = { version = "0.8.12", features = ["const_xxh64"] }
# used across multiple features
serde = { version = "1.0", features = ["derive"] }
@@ -30,7 +31,7 @@ once_cell = "1.20"
actix-web = { version = "4.9", optional = true }
# axum
axum = { version = "0.7.9", optional = true, default-features = false, features = [
axum = { version = "0.8.1", optional = true, default-features = false, features = [
"multipart",
] }
tower = { version = "0.5.1", optional = true }
@@ -53,6 +54,7 @@ bytes = "1.9"
http-body-util = { version = "0.1.2", optional = true }
rkyv = { version = "0.8.9", optional = true }
rmp-serde = { version = "1.3.0", optional = true }
base64 = { version = "0.22.1" }
# client
gloo-net = { version = "0.6.0", optional = true }
@@ -231,4 +233,4 @@ skip_feature_sets = [
]
[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(leptos_debuginfo)'] }
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(leptos_debuginfo)'] }

View File

@@ -74,7 +74,7 @@ pub fn server(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
args.into(),
s.into(),
Some(syn::parse_quote!(server_fns)),
"/api",
option_env!("SERVER_FN_PREFIX").unwrap_or("/api"),
None,
None,
) {

View File

@@ -1,4 +1,4 @@
use crate::{error::ServerFnError, request::ClientReq, response::ClientRes};
use crate::{request::ClientReq, response::ClientRes};
use std::{future::Future, sync::OnceLock};
static ROOT_URL: OnceLock<&'static str> = OnceLock::new();
@@ -21,16 +21,16 @@ pub fn get_server_url() -> &'static str {
/// This trait is implemented for things like a browser `fetch` request or for
/// the `reqwest` trait. It should almost never be necessary to implement it
/// yourself, unless youre trying to use an alternative HTTP crate on the client side.
pub trait Client<CustErr> {
pub trait Client<E> {
/// The type of a request sent by this client.
type Request: ClientReq<CustErr> + Send;
type Request: ClientReq<E> + Send;
/// The type of a response received by this client.
type Response: ClientRes<CustErr> + Send;
type Response: ClientRes<E> + Send;
/// Sends the request and receives a response.
fn send(
req: Self::Request,
) -> impl Future<Output = Result<Self::Response, ServerFnError<CustErr>>> + Send;
) -> impl Future<Output = Result<Self::Response, E>> + Send;
}
#[cfg(feature = "browser")]
@@ -38,24 +38,23 @@ pub trait Client<CustErr> {
pub mod browser {
use super::Client;
use crate::{
error::ServerFnError,
error::{FromServerFnError, IntoAppError, ServerFnErrorErr},
request::browser::{BrowserRequest, RequestInner},
response::browser::BrowserResponse,
};
use send_wrapper::SendWrapper;
use std::future::Future;
/// Implements [`Client`] for a `fetch` request in the browser.
/// Implements [`Client`] for a `fetch` request in the browser.
pub struct BrowserClient;
impl<CustErr> Client<CustErr> for BrowserClient {
impl<E: FromServerFnError> Client<E> for BrowserClient {
type Request = BrowserRequest;
type Response = BrowserResponse;
fn send(
req: Self::Request,
) -> impl Future<Output = Result<Self::Response, ServerFnError<CustErr>>>
+ Send {
) -> impl Future<Output = Result<Self::Response, E>> + Send {
SendWrapper::new(async move {
let req = req.0.take();
let RequestInner {
@@ -66,7 +65,10 @@ pub mod browser {
.send()
.await
.map(|res| BrowserResponse(SendWrapper::new(res)))
.map_err(|e| ServerFnError::Request(e.to_string()));
.map_err(|e| {
ServerFnErrorErr::Request(e.to_string())
.into_app_error()
});
// at this point, the future has successfully resolved without being dropped, so we
// can prevent the `AbortController` from firing
@@ -83,7 +85,10 @@ pub mod browser {
/// Implements [`Client`] for a request made by [`reqwest`].
pub mod reqwest {
use super::Client;
use crate::{error::ServerFnError, request::reqwest::CLIENT};
use crate::{
error::{FromServerFnError, IntoAppError, ServerFnErrorErr},
request::reqwest::CLIENT,
};
use futures::TryFutureExt;
use reqwest::{Request, Response};
use std::future::Future;
@@ -91,17 +96,16 @@ pub mod reqwest {
/// Implements [`Client`] for a request made by [`reqwest`].
pub struct ReqwestClient;
impl<CustErr> Client<CustErr> for ReqwestClient {
impl<E: FromServerFnError> Client<E> for ReqwestClient {
type Request = Request;
type Response = Response;
fn send(
req: Self::Request,
) -> impl Future<Output = Result<Self::Response, ServerFnError<CustErr>>>
+ Send {
CLIENT
.execute(req)
.map_err(|e| ServerFnError::Request(e.to_string()))
) -> impl Future<Output = Result<Self::Response, E>> + Send {
CLIENT.execute(req).map_err(|e| {
ServerFnErrorErr::Request(e.to_string()).into_app_error()
})
}
}
}

View File

@@ -1,8 +1,8 @@
use super::{Encoding, FromReq, FromRes, IntoReq, IntoRes};
use crate::{
error::ServerFnError,
error::{FromServerFnError, IntoAppError, ServerFnErrorErr},
request::{ClientReq, Req},
response::{ClientRes, Res},
response::{ClientRes, TryRes},
};
use bytes::Bytes;
use http::Method;
@@ -16,19 +16,17 @@ impl Encoding for Cbor {
const METHOD: Method = Method::POST;
}
impl<CustErr, T, Request> IntoReq<Cbor, Request, CustErr> for T
impl<E, T, Request> IntoReq<Cbor, Request, E> for T
where
Request: ClientReq<CustErr>,
Request: ClientReq<E>,
T: Serialize + Send,
E: FromServerFnError,
{
fn into_req(
self,
path: &str,
accepts: &str,
) -> Result<Request, ServerFnError<CustErr>> {
fn into_req(self, path: &str, accepts: &str) -> Result<Request, E> {
let mut buffer: Vec<u8> = Vec::new();
ciborium::ser::into_writer(&self, &mut buffer)
.map_err(|e| ServerFnError::Serialization(e.to_string()))?;
ciborium::ser::into_writer(&self, &mut buffer).map_err(|e| {
ServerFnErrorErr::Serialization(e.to_string()).into_app_error()
})?;
Request::try_new_post_bytes(
path,
accepts,
@@ -38,40 +36,44 @@ where
}
}
impl<CustErr, T, Request> FromReq<Cbor, Request, CustErr> for T
impl<E, T, Request> FromReq<Cbor, Request, E> for T
where
Request: Req<CustErr> + Send + 'static,
Request: Req<E> + Send + 'static,
T: DeserializeOwned,
E: FromServerFnError,
{
async fn from_req(req: Request) -> Result<Self, ServerFnError<CustErr>> {
async fn from_req(req: Request) -> Result<Self, E> {
let body_bytes = req.try_into_bytes().await?;
ciborium::de::from_reader(body_bytes.as_ref())
.map_err(|e| ServerFnError::Args(e.to_string()))
.map_err(|e| ServerFnErrorErr::Args(e.to_string()).into_app_error())
}
}
impl<CustErr, T, Response> IntoRes<Cbor, Response, CustErr> for T
impl<E, T, Response> IntoRes<Cbor, Response, E> for T
where
Response: Res<CustErr>,
Response: TryRes<E>,
T: Serialize + Send,
E: FromServerFnError,
{
async fn into_res(self) -> Result<Response, ServerFnError<CustErr>> {
async fn into_res(self) -> Result<Response, E> {
let mut buffer: Vec<u8> = Vec::new();
ciborium::ser::into_writer(&self, &mut buffer)
.map_err(|e| ServerFnError::Serialization(e.to_string()))?;
ciborium::ser::into_writer(&self, &mut buffer).map_err(|e| {
ServerFnErrorErr::Serialization(e.to_string()).into_app_error()
})?;
Response::try_from_bytes(Cbor::CONTENT_TYPE, Bytes::from(buffer))
}
}
impl<CustErr, T, Response> FromRes<Cbor, Response, CustErr> for T
impl<E, T, Response> FromRes<Cbor, Response, E> for T
where
Response: ClientRes<CustErr> + Send,
Response: ClientRes<E> + Send,
T: DeserializeOwned + Send,
E: FromServerFnError,
{
async fn from_res(res: Response) -> Result<Self, ServerFnError<CustErr>> {
async fn from_res(res: Response) -> Result<Self, E> {
let data = res.try_into_bytes().await?;
ciborium::de::from_reader(data.as_ref())
.map_err(|e| ServerFnError::Args(e.to_string()))
.map_err(|e| ServerFnErrorErr::Args(e.to_string()).into_app_error())
}
}
@@ -114,20 +116,20 @@ where
<ResponseBody as HttpBody>::Data: Send ,
<RequestBody as HttpBody>::Data: Send ,
{
async fn from_req(req: http::Request<RequestBody>) -> Result<Self, ServerFnError<CustErr>> {
async fn from_req(req: http::Request<RequestBody>) -> Result<Self, ServerFnError<E>> {
let (_parts, body) = req.into_parts();
let body_bytes = body
.collect()
.await
.map(|c| c.to_bytes())
.map_err(|e| ServerFnError::Deserialization(e.to_string()))?;
.map_err(|e| ServerFnErrorErr::Deserialization(e.to_string()).into())?;
let data = ciborium::de::from_reader(body_bytes.as_ref())
.map_err(|e| ServerFnError::Args(e.to_string()))?;
.map_err(|e| ServerFnErrorErr::Args(e.to_string()).into())?;
Ok(data)
}
async fn into_req(self) -> Result<http::Request<Body>, ServerFnError<CustErr>> {
async fn into_req(self) -> Result<http::Request<Body>, ServerFnError<E>> {
let mut buffer: Vec<u8> = Vec::new();
ciborium::ser::into_writer(&self, &mut buffer)?;
let req = http::Request::builder()
@@ -139,17 +141,17 @@ where
.body(Body::from(buffer))?;
Ok(req)
}
async fn from_res(res: http::Response<ResponseBody>) -> Result<Self, ServerFnError<CustErr>> {
async fn from_res(res: http::Response<ResponseBody>) -> Result<Self, ServerFnError<E>> {
let (_parts, body) = res.into_parts();
let body_bytes = body
.collect()
.await
.map(|c| c.to_bytes())
.map_err(|e| ServerFnError::Deserialization(e.to_string()))?;
.map_err(|e| ServerFnErrorErr::Deserialization(e.to_string()).into())?;
ciborium::de::from_reader(body_bytes.as_ref())
.map_err(|e| ServerFnError::Args(e.to_string()))
.map_err(|e| ServerFnErrorErr::Args(e.to_string()).into())
}
async fn into_res(self) -> http::Response<Body> {

View File

@@ -1,8 +1,8 @@
use super::{Encoding, FromReq, FromRes, Streaming};
use crate::{
error::{NoCustomError, ServerFnError},
error::{FromServerFnError, IntoAppError, ServerFnErrorErr},
request::{ClientReq, Req},
response::{ClientRes, Res},
response::{ClientRes, TryRes},
IntoReq, IntoRes,
};
use bytes::Bytes;
@@ -18,55 +18,58 @@ impl Encoding for Json {
const METHOD: Method = Method::POST;
}
impl<CustErr, T, Request> IntoReq<Json, Request, CustErr> for T
impl<E, T, Request> IntoReq<Json, Request, E> for T
where
Request: ClientReq<CustErr>,
Request: ClientReq<E>,
T: Serialize + Send,
E: FromServerFnError,
{
fn into_req(
self,
path: &str,
accepts: &str,
) -> Result<Request, ServerFnError<CustErr>> {
let data = serde_json::to_string(&self)
.map_err(|e| ServerFnError::Serialization(e.to_string()))?;
fn into_req(self, path: &str, accepts: &str) -> Result<Request, E> {
let data = serde_json::to_string(&self).map_err(|e| {
ServerFnErrorErr::Serialization(e.to_string()).into_app_error()
})?;
Request::try_new_post(path, accepts, Json::CONTENT_TYPE, data)
}
}
impl<CustErr, T, Request> FromReq<Json, Request, CustErr> for T
impl<E, T, Request> FromReq<Json, Request, E> for T
where
Request: Req<CustErr> + Send + 'static,
Request: Req<E> + Send + 'static,
T: DeserializeOwned,
E: FromServerFnError,
{
async fn from_req(req: Request) -> Result<Self, ServerFnError<CustErr>> {
async fn from_req(req: Request) -> Result<Self, E> {
let string_data = req.try_into_string().await?;
serde_json::from_str::<Self>(&string_data)
.map_err(|e| ServerFnError::Args(e.to_string()))
.map_err(|e| ServerFnErrorErr::Args(e.to_string()).into_app_error())
}
}
impl<CustErr, T, Response> IntoRes<Json, Response, CustErr> for T
impl<E, T, Response> IntoRes<Json, Response, E> for T
where
Response: Res<CustErr>,
Response: TryRes<E>,
T: Serialize + Send,
E: FromServerFnError,
{
async fn into_res(self) -> Result<Response, ServerFnError<CustErr>> {
let data = serde_json::to_string(&self)
.map_err(|e| ServerFnError::Serialization(e.to_string()))?;
async fn into_res(self) -> Result<Response, E> {
let data = serde_json::to_string(&self).map_err(|e| {
ServerFnErrorErr::Serialization(e.to_string()).into_app_error()
})?;
Response::try_from_string(Json::CONTENT_TYPE, data)
}
}
impl<CustErr, T, Response> FromRes<Json, Response, CustErr> for T
impl<E, T, Response> FromRes<Json, Response, E> for T
where
Response: ClientRes<CustErr> + Send,
Response: ClientRes<E> + Send,
T: DeserializeOwned + Send,
E: FromServerFnError,
{
async fn from_res(res: Response) -> Result<Self, ServerFnError<CustErr>> {
async fn from_res(res: Response) -> Result<Self, E> {
let data = res.try_into_string().await?;
serde_json::from_str(&data)
.map_err(|e| ServerFnError::Deserialization(e.to_string()))
serde_json::from_str(&data).map_err(|e| {
ServerFnErrorErr::Deserialization(e.to_string()).into_app_error()
})
}
}
@@ -102,35 +105,31 @@ impl Encoding for StreamingJson {
/// end before the output will begin.
///
/// Streaming requests are only allowed over HTTP2 or HTTP3.
pub struct JsonStream<T, CustErr = NoCustomError>(
Pin<Box<dyn Stream<Item = Result<T, ServerFnError<CustErr>>> + Send>>,
);
pub struct JsonStream<T, E>(Pin<Box<dyn Stream<Item = Result<T, E>> + Send>>);
impl<T, CustErr> std::fmt::Debug for JsonStream<T, CustErr> {
impl<T, E> std::fmt::Debug for JsonStream<T, E> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_tuple("JsonStream").finish()
}
}
impl<T> JsonStream<T> {
impl<T, E> JsonStream<T, E> {
/// Creates a new `ByteStream` from the given stream.
pub fn new(
value: impl Stream<Item = Result<T, ServerFnError>> + Send + 'static,
value: impl Stream<Item = Result<T, E>> + Send + 'static,
) -> Self {
Self(Box::pin(value.map(|value| value)))
}
}
impl<T, CustErr> JsonStream<T, CustErr> {
impl<T, E> JsonStream<T, E> {
/// Consumes the wrapper, returning a stream of text.
pub fn into_inner(
self,
) -> impl Stream<Item = Result<T, ServerFnError<CustErr>>> + Send {
pub fn into_inner(self) -> impl Stream<Item = Result<T, E>> + Send {
self.0
}
}
impl<S, T: 'static, CustErr: 'static> From<S> for JsonStream<T, CustErr>
impl<S, T: 'static, E: 'static> From<S> for JsonStream<T, E>
where
S: Stream<Item = T> + Send + 'static,
{
@@ -139,18 +138,15 @@ where
}
}
impl<CustErr, S, T, Request> IntoReq<StreamingJson, Request, CustErr> for S
impl<E, S, T, Request> IntoReq<StreamingJson, Request, E> for S
where
Request: ClientReq<CustErr>,
Request: ClientReq<E>,
S: Stream<Item = T> + Send + 'static,
T: Serialize + 'static,
E: FromServerFnError + Serialize,
{
fn into_req(
self,
path: &str,
accepts: &str,
) -> Result<Request, ServerFnError<CustErr>> {
let data: JsonStream<T> = self.into();
fn into_req(self, path: &str, accepts: &str) -> Result<Request, E> {
let data: JsonStream<T, E> = self.into();
Request::try_new_streaming(
path,
accepts,
@@ -164,56 +160,61 @@ where
}
}
impl<CustErr, T, S, Request> FromReq<StreamingJson, Request, CustErr> for S
impl<E, T, S, Request> FromReq<StreamingJson, Request, E> for S
where
Request: Req<CustErr> + Send + 'static,
Request: Req<E> + Send + 'static,
// The additional `Stream<Item = T>` bound is never used, but it is required to avoid an error where `T` is unconstrained
S: Stream<Item = T> + From<JsonStream<T>> + Send + 'static,
S: Stream<Item = T> + From<JsonStream<T, E>> + Send + 'static,
T: DeserializeOwned + 'static,
E: FromServerFnError,
{
async fn from_req(req: Request) -> Result<Self, ServerFnError<CustErr>> {
async fn from_req(req: Request) -> Result<Self, E> {
let data = req.try_into_stream()?;
let s = JsonStream::new(data.map(|chunk| {
chunk.and_then(|bytes| {
serde_json::from_slice(bytes.as_ref())
.map_err(|e| ServerFnError::Deserialization(e.to_string()))
serde_json::from_slice(bytes.as_ref()).map_err(|e| {
ServerFnErrorErr::Deserialization(e.to_string())
.into_app_error()
})
})
}));
Ok(s.into())
}
}
impl<CustErr, T, Response> IntoRes<StreamingJson, Response, CustErr>
for JsonStream<T, CustErr>
impl<E, T, Response> IntoRes<StreamingJson, Response, E> for JsonStream<T, E>
where
Response: Res<CustErr>,
CustErr: 'static,
Response: TryRes<E>,
T: Serialize + 'static,
E: FromServerFnError,
{
async fn into_res(self) -> Result<Response, ServerFnError<CustErr>> {
async fn into_res(self) -> Result<Response, E> {
Response::try_from_stream(
Streaming::CONTENT_TYPE,
self.into_inner().map(|value| {
serde_json::to_vec(&value?)
.map(Bytes::from)
.map_err(|e| ServerFnError::Serialization(e.to_string()))
serde_json::to_vec(&value?).map(Bytes::from).map_err(|e| {
ServerFnErrorErr::Serialization(e.to_string())
.into_app_error()
})
}),
)
}
}
impl<CustErr, T, Response> FromRes<StreamingJson, Response, CustErr>
for JsonStream<T>
impl<E, T, Response> FromRes<StreamingJson, Response, E> for JsonStream<T, E>
where
Response: ClientRes<CustErr> + Send,
Response: ClientRes<E> + Send,
T: DeserializeOwned,
E: FromServerFnError,
{
async fn from_res(res: Response) -> Result<Self, ServerFnError<CustErr>> {
async fn from_res(res: Response) -> Result<Self, E> {
let stream = res.try_into_stream()?;
Ok(JsonStream::new(stream.map(|chunk| {
chunk.and_then(|bytes| {
serde_json::from_slice(bytes.as_ref())
.map_err(|e| ServerFnError::Deserialization(e.to_string()))
serde_json::from_slice(bytes.as_ref()).map_err(|e| {
ServerFnErrorErr::Deserialization(e.to_string())
.into_app_error()
})
})
})))
}

View File

@@ -55,7 +55,6 @@ mod postcard;
pub use postcard::*;
mod stream;
use crate::error::ServerFnError;
use futures::Future;
use http::Method;
pub use stream::*;
@@ -71,31 +70,27 @@ pub use stream::*;
/// For example, heres the implementation for [`Json`].
///
/// ```rust,ignore
/// impl<CustErr, T, Request> IntoReq<Json, Request, CustErr> for T
/// impl<E, T, Request> IntoReq<Json, Request, E> for T
/// where
/// Request: ClientReq<CustErr>,
/// Request: ClientReq<E>,
/// T: Serialize + Send,
/// {
/// fn into_req(
/// self,
/// path: &str,
/// accepts: &str,
/// ) -> Result<Request, ServerFnError<CustErr>> {
/// ) -> Result<Request, E> {
/// // try to serialize the data
/// let data = serde_json::to_string(&self)
/// .map_err(|e| ServerFnError::Serialization(e.to_string()))?;
/// .map_err(|e| ServerFnErrorErr::Serialization(e.to_string()).into_app_error())?;
/// // and use it as the body of a POST request
/// Request::try_new_post(path, accepts, Json::CONTENT_TYPE, data)
/// }
/// }
/// ```
pub trait IntoReq<Encoding, Request, CustErr> {
pub trait IntoReq<Encoding, Request, E> {
/// Attempts to serialize the arguments into an HTTP request.
fn into_req(
self,
path: &str,
accepts: &str,
) -> Result<Request, ServerFnError<CustErr>>;
fn into_req(self, path: &str, accepts: &str) -> Result<Request, E>;
}
/// Deserializes an HTTP request into the data type, on the server.
@@ -109,32 +104,31 @@ pub trait IntoReq<Encoding, Request, CustErr> {
/// For example, heres the implementation for [`Json`].
///
/// ```rust,ignore
/// impl<CustErr, T, Request> FromReq<Json, Request, CustErr> for T
/// impl<E, T, Request> FromReq<Json, Request, E> for T
/// where
/// // require the Request implement `Req`
/// Request: Req<CustErr> + Send + 'static,
/// Request: Req<E> + Send + 'static,
/// // require that the type can be deserialized with `serde`
/// T: DeserializeOwned,
/// E: FromServerFnError,
/// {
/// async fn from_req(
/// req: Request,
/// ) -> Result<Self, ServerFnError<CustErr>> {
/// ) -> Result<Self, E> {
/// // try to convert the body of the request into a `String`
/// let string_data = req.try_into_string().await?;
/// // deserialize the data
/// serde_json::from_str::<Self>(&string_data)
/// .map_err(|e| ServerFnError::Args(e.to_string()))
/// serde_json::from_str(&string_data)
/// .map_err(|e| ServerFnErrorErr::Args(e.to_string()).into_app_error())
/// }
/// }
/// ```
pub trait FromReq<Encoding, Request, CustErr>
pub trait FromReq<Encoding, Request, E>
where
Self: Sized,
{
/// Attempts to deserialize the arguments from a request.
fn from_req(
req: Request,
) -> impl Future<Output = Result<Self, ServerFnError<CustErr>>> + Send;
fn from_req(req: Request) -> impl Future<Output = Result<Self, E>> + Send;
}
/// Serializes the data type into an HTTP response.
@@ -148,25 +142,24 @@ where
/// For example, heres the implementation for [`Json`].
///
/// ```rust,ignore
/// impl<CustErr, T, Response> IntoRes<Json, Response, CustErr> for T
/// impl<E, T, Response> IntoRes<Json, Response, E> for T
/// where
/// Response: Res<CustErr>,
/// Response: Res<E>,
/// T: Serialize + Send,
/// E: FromServerFnError,
/// {
/// async fn into_res(self) -> Result<Response, ServerFnError<CustErr>> {
/// async fn into_res(self) -> Result<Response, E> {
/// // try to serialize the data
/// let data = serde_json::to_string(&self)
/// .map_err(|e| ServerFnError::Serialization(e.to_string()))?;
/// .map_err(|e| ServerFnErrorErr::Serialization(e.to_string()).into())?;
/// // and use it as the body of a response
/// Response::try_from_string(Json::CONTENT_TYPE, data)
/// }
/// }
/// ```
pub trait IntoRes<Encoding, Response, CustErr> {
pub trait IntoRes<Encoding, Response, E> {
/// Attempts to serialize the output into an HTTP response.
fn into_res(
self,
) -> impl Future<Output = Result<Response, ServerFnError<CustErr>>> + Send;
fn into_res(self) -> impl Future<Output = Result<Response, E>> + Send;
}
/// Deserializes the data type from an HTTP response.
@@ -181,30 +174,29 @@ pub trait IntoRes<Encoding, Response, CustErr> {
/// For example, heres the implementation for [`Json`].
///
/// ```rust,ignore
/// impl<CustErr, T, Response> FromRes<Json, Response, CustErr> for T
/// impl<E, T, Response> FromRes<Json, Response, E> for T
/// where
/// Response: ClientRes<CustErr> + Send,
/// Response: ClientRes<E> + Send,
/// T: DeserializeOwned + Send,
/// E: FromServerFnError,
/// {
/// async fn from_res(
/// res: Response,
/// ) -> Result<Self, ServerFnError<CustErr>> {
/// ) -> Result<Self, E> {
/// // extracts the request body
/// let data = res.try_into_string().await?;
/// // and tries to deserialize it as JSON
/// serde_json::from_str(&data)
/// .map_err(|e| ServerFnError::Deserialization(e.to_string()))
/// .map_err(|e| ServerFnErrorErr::Deserialization(e.to_string()).into_app_error())
/// }
/// }
/// ```
pub trait FromRes<Encoding, Response, CustErr>
pub trait FromRes<Encoding, Response, E>
where
Self: Sized,
{
/// Attempts to deserialize the outputs from a response.
fn from_res(
res: Response,
) -> impl Future<Output = Result<Self, ServerFnError<CustErr>>> + Send;
fn from_res(res: Response) -> impl Future<Output = Result<Self, E>> + Send;
}
/// Defines a particular encoding format, which can be used for serializing or deserializing data.

View File

@@ -1,8 +1,8 @@
use super::{Encoding, FromReq, FromRes, IntoReq, IntoRes};
use crate::{
error::ServerFnError,
error::{FromServerFnError, IntoAppError, ServerFnErrorErr},
request::{ClientReq, Req},
response::{ClientRes, Res},
response::{ClientRes, TryRes},
};
use bytes::Bytes;
use http::Method;
@@ -16,18 +16,16 @@ impl Encoding for MsgPack {
const METHOD: Method = Method::POST;
}
impl<T, Request, Err> IntoReq<MsgPack, Request, Err> for T
impl<T, Request, E> IntoReq<MsgPack, Request, E> for T
where
Request: ClientReq<Err>,
Request: ClientReq<E>,
T: Serialize,
E: FromServerFnError,
{
fn into_req(
self,
path: &str,
accepts: &str,
) -> Result<Request, ServerFnError<Err>> {
let data = rmp_serde::to_vec(&self)
.map_err(|e| ServerFnError::Serialization(e.to_string()))?;
fn into_req(self, path: &str, accepts: &str) -> Result<Request, E> {
let data = rmp_serde::to_vec(&self).map_err(|e| {
ServerFnErrorErr::Serialization(e.to_string()).into_app_error()
})?;
Request::try_new_post_bytes(
path,
MsgPack::CONTENT_TYPE,
@@ -37,38 +35,43 @@ where
}
}
impl<T, Request, Err> FromReq<MsgPack, Request, Err> for T
impl<T, Request, E> FromReq<MsgPack, Request, E> for T
where
Request: Req<Err> + Send,
Request: Req<E> + Send,
T: DeserializeOwned,
E: FromServerFnError,
{
async fn from_req(req: Request) -> Result<Self, ServerFnError<Err>> {
async fn from_req(req: Request) -> Result<Self, E> {
let data = req.try_into_bytes().await?;
rmp_serde::from_slice::<T>(&data)
.map_err(|e| ServerFnError::Args(e.to_string()))
.map_err(|e| ServerFnErrorErr::Args(e.to_string()).into_app_error())
}
}
impl<T, Response, Err> IntoRes<MsgPack, Response, Err> for T
impl<T, Response, E> IntoRes<MsgPack, Response, E> for T
where
Response: Res<Err>,
Response: TryRes<E>,
T: Serialize + Send,
E: FromServerFnError,
{
async fn into_res(self) -> Result<Response, ServerFnError<Err>> {
let data = rmp_serde::to_vec(&self)
.map_err(|e| ServerFnError::Serialization(e.to_string()))?;
async fn into_res(self) -> Result<Response, E> {
let data = rmp_serde::to_vec(&self).map_err(|e| {
ServerFnErrorErr::Serialization(e.to_string()).into_app_error()
})?;
Response::try_from_bytes(MsgPack::CONTENT_TYPE, Bytes::from(data))
}
}
impl<T, Response, Err> FromRes<MsgPack, Response, Err> for T
impl<T, Response, E> FromRes<MsgPack, Response, E> for T
where
Response: ClientRes<Err> + Send,
Response: ClientRes<E> + Send,
T: DeserializeOwned,
E: FromServerFnError,
{
async fn from_res(res: Response) -> Result<Self, ServerFnError<Err>> {
async fn from_res(res: Response) -> Result<Self, E> {
let data = res.try_into_bytes().await?;
rmp_serde::from_slice(&data)
.map_err(|e| ServerFnError::Deserialization(e.to_string()))
rmp_serde::from_slice(&data).map_err(|e| {
ServerFnErrorErr::Deserialization(e.to_string()).into_app_error()
})
}
}

View File

@@ -1,6 +1,6 @@
use super::{Encoding, FromReq};
use crate::{
error::ServerFnError,
error::FromServerFnError,
request::{browser::BrowserFormData, ClientReq, Req},
IntoReq,
};
@@ -56,16 +56,12 @@ impl From<FormData> for MultipartData {
}
}
impl<CustErr, T, Request> IntoReq<MultipartFormData, Request, CustErr> for T
impl<E, T, Request> IntoReq<MultipartFormData, Request, E> for T
where
Request: ClientReq<CustErr, FormData = BrowserFormData>,
Request: ClientReq<E, FormData = BrowserFormData>,
T: Into<MultipartData>,
{
fn into_req(
self,
path: &str,
accepts: &str,
) -> Result<Request, ServerFnError<CustErr>> {
fn into_req(self, path: &str, accepts: &str) -> Result<Request, E> {
let multi = self.into();
Request::try_new_multipart(
path,
@@ -75,20 +71,20 @@ where
}
}
impl<CustErr, T, Request> FromReq<MultipartFormData, Request, CustErr> for T
impl<E, T, Request> FromReq<MultipartFormData, Request, E> for T
where
Request: Req<CustErr> + Send + 'static,
Request: Req<E> + Send + 'static,
T: From<MultipartData>,
CustErr: 'static,
E: FromServerFnError,
{
async fn from_req(req: Request) -> Result<Self, ServerFnError<CustErr>> {
async fn from_req(req: Request) -> Result<Self, E> {
let boundary = req
.to_content_type()
.and_then(|ct| multer::parse_boundary(ct).ok())
.expect("couldn't parse boundary");
let stream = req.try_into_stream()?;
let data = multer::Multipart::new(
stream.map(|data| data.map_err(|e| e.to_string())),
stream.map(|data| data.map_err(|e| e.ser())),
boundary,
);
Ok(MultipartData::Server(data).into())

View File

@@ -1,8 +1,8 @@
use super::{Encoding, FromReq, FromRes, IntoReq, IntoRes};
use crate::{
error::ServerFnError,
error::{FromServerFnError, IntoAppError, ServerFnErrorErr},
request::{ClientReq, Req},
response::{ClientRes, Res},
response::{ClientRes, TryRes},
};
use bytes::Bytes;
use http::Method;
@@ -16,18 +16,16 @@ impl Encoding for Postcard {
const METHOD: Method = Method::POST;
}
impl<T, Request, Err> IntoReq<Postcard, Request, Err> for T
impl<T, Request, E> IntoReq<Postcard, Request, E> for T
where
Request: ClientReq<Err>,
Request: ClientReq<E>,
T: Serialize,
E: FromServerFnError,
{
fn into_req(
self,
path: &str,
accepts: &str,
) -> Result<Request, ServerFnError<Err>> {
let data = postcard::to_allocvec(&self)
.map_err(|e| ServerFnError::Serialization(e.to_string()))?;
fn into_req(self, path: &str, accepts: &str) -> Result<Request, E> {
let data = postcard::to_allocvec(&self).map_err(|e| {
ServerFnErrorErr::Serialization(e.to_string()).into_app_error()
})?;
Request::try_new_post_bytes(
path,
Postcard::CONTENT_TYPE,
@@ -37,38 +35,43 @@ where
}
}
impl<T, Request, Err> FromReq<Postcard, Request, Err> for T
impl<T, Request, E> FromReq<Postcard, Request, E> for T
where
Request: Req<Err> + Send,
Request: Req<E> + Send,
T: DeserializeOwned,
E: FromServerFnError,
{
async fn from_req(req: Request) -> Result<Self, ServerFnError<Err>> {
async fn from_req(req: Request) -> Result<Self, E> {
let data = req.try_into_bytes().await?;
postcard::from_bytes::<T>(&data)
.map_err(|e| ServerFnError::Args(e.to_string()))
.map_err(|e| ServerFnErrorErr::Args(e.to_string()).into_app_error())
}
}
impl<T, Response, Err> IntoRes<Postcard, Response, Err> for T
impl<T, Response, E> IntoRes<Postcard, Response, E> for T
where
Response: Res<Err>,
Response: TryRes<E>,
T: Serialize + Send,
E: FromServerFnError,
{
async fn into_res(self) -> Result<Response, ServerFnError<Err>> {
let data = postcard::to_allocvec(&self)
.map_err(|e| ServerFnError::Serialization(e.to_string()))?;
async fn into_res(self) -> Result<Response, E> {
let data = postcard::to_allocvec(&self).map_err(|e| {
ServerFnErrorErr::Serialization(e.to_string()).into_app_error()
})?;
Response::try_from_bytes(Postcard::CONTENT_TYPE, Bytes::from(data))
}
}
impl<T, Response, Err> FromRes<Postcard, Response, Err> for T
impl<T, Response, E> FromRes<Postcard, Response, E> for T
where
Response: ClientRes<Err> + Send,
Response: ClientRes<E> + Send,
T: DeserializeOwned,
E: FromServerFnError,
{
async fn from_res(res: Response) -> Result<Self, ServerFnError<Err>> {
async fn from_res(res: Response) -> Result<Self, E> {
let data = res.try_into_bytes().await?;
postcard::from_bytes(&data)
.map_err(|e| ServerFnError::Deserialization(e.to_string()))
postcard::from_bytes(&data).map_err(|e| {
ServerFnErrorErr::Deserialization(e.to_string()).into_app_error()
})
}
}

View File

@@ -1,8 +1,8 @@
use super::{Encoding, FromReq, FromRes, IntoReq, IntoRes};
use crate::{
error::ServerFnError,
error::{FromServerFnError, IntoAppError, ServerFnErrorErr},
request::{ClientReq, Req},
response::{ClientRes, Res},
response::{ClientRes, TryRes},
};
use bytes::Bytes;
use futures::StreamExt;
@@ -29,39 +29,38 @@ impl Encoding for Rkyv {
const METHOD: Method = Method::POST;
}
impl<CustErr, T, Request> IntoReq<Rkyv, Request, CustErr> for T
impl<E, T, Request> IntoReq<Rkyv, Request, E> for T
where
Request: ClientReq<CustErr>,
Request: ClientReq<E>,
T: Archive + for<'a> Serialize<RkyvSerializer<'a>>,
T::Archived: Deserialize<T, RkyvDeserializer>
+ for<'a> CheckBytes<RkyvValidator<'a>>,
E: FromServerFnError,
{
fn into_req(
self,
path: &str,
accepts: &str,
) -> Result<Request, ServerFnError<CustErr>> {
let encoded = rkyv::to_bytes::<rancor::Error>(&self)
.map_err(|e| ServerFnError::Serialization(e.to_string()))?;
fn into_req(self, path: &str, accepts: &str) -> Result<Request, E> {
let encoded = rkyv::to_bytes::<rancor::Error>(&self).map_err(|e| {
ServerFnErrorErr::Serialization(e.to_string()).into_app_error()
})?;
let bytes = Bytes::copy_from_slice(encoded.as_ref());
Request::try_new_post_bytes(path, accepts, Rkyv::CONTENT_TYPE, bytes)
}
}
impl<CustErr, T, Request> FromReq<Rkyv, Request, CustErr> for T
impl<E, T, Request> FromReq<Rkyv, Request, E> for T
where
Request: Req<CustErr> + Send + 'static,
Request: Req<E> + Send + 'static,
T: Archive + for<'a> Serialize<RkyvSerializer<'a>>,
T::Archived: Deserialize<T, RkyvDeserializer>
+ for<'a> CheckBytes<RkyvValidator<'a>>,
E: FromServerFnError,
{
async fn from_req(req: Request) -> Result<Self, ServerFnError<CustErr>> {
async fn from_req(req: Request) -> Result<Self, E> {
let mut aligned = AlignedVec::<1024>::new();
let mut body_stream = Box::pin(req.try_into_stream()?);
while let Some(chunk) = body_stream.next().await {
match chunk {
Err(e) => {
return Err(ServerFnError::Deserialization(e.to_string()))
return Err(e);
}
Ok(bytes) => {
for byte in bytes {
@@ -71,36 +70,40 @@ where
}
}
rkyv::from_bytes::<T, rancor::Error>(aligned.as_ref())
.map_err(|e| ServerFnError::Args(e.to_string()))
.map_err(|e| ServerFnErrorErr::Args(e.to_string()).into_app_error())
}
}
impl<CustErr, T, Response> IntoRes<Rkyv, Response, CustErr> for T
impl<E, T, Response> IntoRes<Rkyv, Response, E> for T
where
Response: Res<CustErr>,
Response: TryRes<E>,
T: Send,
T: Archive + for<'a> Serialize<RkyvSerializer<'a>>,
T::Archived: Deserialize<T, RkyvDeserializer>
+ for<'a> CheckBytes<RkyvValidator<'a>>,
E: FromServerFnError,
{
async fn into_res(self) -> Result<Response, ServerFnError<CustErr>> {
let encoded = rkyv::to_bytes::<rancor::Error>(&self)
.map_err(|e| ServerFnError::Serialization(format!("{e:?}")))?;
async fn into_res(self) -> Result<Response, E> {
let encoded = rkyv::to_bytes::<rancor::Error>(&self).map_err(|e| {
ServerFnErrorErr::Serialization(format!("{e:?}")).into_app_error()
})?;
let bytes = Bytes::copy_from_slice(encoded.as_ref());
Response::try_from_bytes(Rkyv::CONTENT_TYPE, bytes)
}
}
impl<CustErr, T, Response> FromRes<Rkyv, Response, CustErr> for T
impl<E, T, Response> FromRes<Rkyv, Response, E> for T
where
Response: ClientRes<CustErr> + Send,
Response: ClientRes<E> + Send,
T: Archive + for<'a> Serialize<RkyvSerializer<'a>>,
T::Archived: Deserialize<T, RkyvDeserializer>
+ for<'a> CheckBytes<RkyvValidator<'a>>,
E: FromServerFnError,
{
async fn from_res(res: Response) -> Result<Self, ServerFnError<CustErr>> {
async fn from_res(res: Response) -> Result<Self, E> {
let data = res.try_into_bytes().await?;
rkyv::from_bytes::<T, rancor::Error>(&data)
.map_err(|e| ServerFnError::Deserialization(e.to_string()))
rkyv::from_bytes::<T, rancor::Error>(&data).map_err(|e| {
ServerFnErrorErr::Deserialization(e.to_string()).into_app_error()
})
}
}

View File

@@ -1,8 +1,8 @@
use super::{Encoding, FromReq, FromRes};
use crate::{
error::ServerFnError,
error::{FromServerFnError, IntoAppError, ServerFnErrorErr},
request::{ClientReq, Req},
response::{ClientRes, Res},
response::{ClientRes, TryRes},
IntoReq, IntoRes,
};
use http::Method;
@@ -15,68 +15,68 @@ impl Encoding for SerdeLite {
const METHOD: Method = Method::POST;
}
impl<CustErr, T, Request> IntoReq<SerdeLite, Request, CustErr> for T
impl<E, T, Request> IntoReq<SerdeLite, Request, E> for T
where
Request: ClientReq<CustErr>,
Request: ClientReq<E>,
T: Serialize + Send,
E: FromServerFnError,
{
fn into_req(
self,
path: &str,
accepts: &str,
) -> Result<Request, ServerFnError<CustErr>> {
let data = serde_json::to_string(
&self
.serialize()
.map_err(|e| ServerFnError::Serialization(e.to_string()))?,
)
.map_err(|e| ServerFnError::Serialization(e.to_string()))?;
fn into_req(self, path: &str, accepts: &str) -> Result<Request, E> {
let data = serde_json::to_string(&self.serialize().map_err(|e| {
ServerFnErrorErr::Serialization(e.to_string()).into_app_error()
})?)
.map_err(|e| {
ServerFnErrorErr::Serialization(e.to_string()).into_app_error()
})?;
Request::try_new_post(path, accepts, SerdeLite::CONTENT_TYPE, data)
}
}
impl<CustErr, T, Request> FromReq<SerdeLite, Request, CustErr> for T
impl<E, T, Request> FromReq<SerdeLite, Request, E> for T
where
Request: Req<CustErr> + Send + 'static,
Request: Req<E> + Send + 'static,
T: Deserialize,
E: FromServerFnError,
{
async fn from_req(req: Request) -> Result<Self, ServerFnError<CustErr>> {
async fn from_req(req: Request) -> Result<Self, E> {
let string_data = req.try_into_string().await?;
Self::deserialize(
&serde_json::from_str(&string_data)
.map_err(|e| ServerFnError::Args(e.to_string()))?,
)
.map_err(|e| ServerFnError::Args(e.to_string()))
Self::deserialize(&serde_json::from_str(&string_data).map_err(|e| {
ServerFnErrorErr::Args(e.to_string()).into_app_error()
})?)
.map_err(|e| ServerFnErrorErr::Args(e.to_string()).into_app_error())
}
}
impl<CustErr, T, Response> IntoRes<SerdeLite, Response, CustErr> for T
impl<E, T, Response> IntoRes<SerdeLite, Response, E> for T
where
Response: Res<CustErr>,
Response: TryRes<E>,
T: Serialize + Send,
E: FromServerFnError,
{
async fn into_res(self) -> Result<Response, ServerFnError<CustErr>> {
let data = serde_json::to_string(
&self
.serialize()
.map_err(|e| ServerFnError::Serialization(e.to_string()))?,
)
.map_err(|e| ServerFnError::Serialization(e.to_string()))?;
async fn into_res(self) -> Result<Response, E> {
let data = serde_json::to_string(&self.serialize().map_err(|e| {
ServerFnErrorErr::Serialization(e.to_string()).into_app_error()
})?)
.map_err(|e| {
ServerFnErrorErr::Serialization(e.to_string()).into_app_error()
})?;
Response::try_from_string(SerdeLite::CONTENT_TYPE, data)
}
}
impl<CustErr, T, Response> FromRes<SerdeLite, Response, CustErr> for T
impl<E, T, Response> FromRes<SerdeLite, Response, E> for T
where
Response: ClientRes<CustErr> + Send,
Response: ClientRes<E> + Send,
T: Deserialize + Send,
E: FromServerFnError,
{
async fn from_res(res: Response) -> Result<Self, ServerFnError<CustErr>> {
async fn from_res(res: Response) -> Result<Self, E> {
let data = res.try_into_string().await?;
Self::deserialize(
&serde_json::from_str(&data)
.map_err(|e| ServerFnError::Args(e.to_string()))?,
)
.map_err(|e| ServerFnError::Deserialization(e.to_string()))
Self::deserialize(&serde_json::from_str(&data).map_err(|e| {
ServerFnErrorErr::Args(e.to_string()).into_app_error()
})?)
.map_err(|e| {
ServerFnErrorErr::Deserialization(e.to_string()).into_app_error()
})
}
}

View File

@@ -1,9 +1,9 @@
use super::{Encoding, FromReq, FromRes, IntoReq};
use crate::{
error::{NoCustomError, ServerFnError},
error::{FromServerFnError, IntoAppError, ServerFnErrorErr},
request::{ClientReq, Req},
response::{ClientRes, Res},
IntoRes,
response::{ClientRes, TryRes},
IntoRes, ServerFnError,
};
use bytes::Bytes;
use futures::{Stream, StreamExt};
@@ -29,26 +29,22 @@ impl Encoding for Streaming {
const METHOD: Method = Method::POST;
}
impl<CustErr, T, Request> IntoReq<Streaming, Request, CustErr> for T
impl<E, T, Request> IntoReq<Streaming, Request, E> for T
where
Request: ClientReq<CustErr>,
Request: ClientReq<E>,
T: Stream<Item = Bytes> + Send + Sync + 'static,
{
fn into_req(
self,
path: &str,
accepts: &str,
) -> Result<Request, ServerFnError<CustErr>> {
fn into_req(self, path: &str, accepts: &str) -> Result<Request, E> {
Request::try_new_streaming(path, accepts, Streaming::CONTENT_TYPE, self)
}
}
impl<CustErr, T, Request> FromReq<Streaming, Request, CustErr> for T
impl<E, T, Request> FromReq<Streaming, Request, E> for T
where
Request: Req<CustErr> + Send + 'static,
T: From<ByteStream> + 'static,
Request: Req<E> + Send + 'static,
T: From<ByteStream<E>> + 'static,
{
async fn from_req(req: Request) -> Result<Self, ServerFnError<CustErr>> {
async fn from_req(req: Request) -> Result<Self, E> {
let data = req.try_into_stream()?;
let s = ByteStream::new(data);
Ok(s.into())
@@ -67,29 +63,25 @@ where
/// end before the output will begin.
///
/// Streaming requests are only allowed over HTTP2 or HTTP3.
pub struct ByteStream<CustErr = NoCustomError>(
Pin<Box<dyn Stream<Item = Result<Bytes, ServerFnError<CustErr>>> + Send>>,
);
pub struct ByteStream<E>(Pin<Box<dyn Stream<Item = Result<Bytes, E>> + Send>>);
impl<CustErr> ByteStream<CustErr> {
impl<E> ByteStream<E> {
/// Consumes the wrapper, returning a stream of bytes.
pub fn into_inner(
self,
) -> impl Stream<Item = Result<Bytes, ServerFnError<CustErr>>> + Send {
pub fn into_inner(self) -> impl Stream<Item = Result<Bytes, E>> + Send {
self.0
}
}
impl<CustErr> Debug for ByteStream<CustErr> {
impl<E> Debug for ByteStream<E> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_tuple("ByteStream").finish()
}
}
impl ByteStream {
impl<E> ByteStream<E> {
/// Creates a new `ByteStream` from the given stream.
pub fn new<T>(
value: impl Stream<Item = Result<T, ServerFnError>> + Send + 'static,
value: impl Stream<Item = Result<T, E>> + Send + 'static,
) -> Self
where
T: Into<Bytes>,
@@ -98,7 +90,7 @@ impl ByteStream {
}
}
impl<S, T> From<S> for ByteStream
impl<E, S, T> From<S> for ByteStream<E>
where
S: Stream<Item = T> + Send + 'static,
T: Into<Bytes>,
@@ -108,22 +100,21 @@ where
}
}
impl<CustErr, Response> IntoRes<Streaming, Response, CustErr>
for ByteStream<CustErr>
impl<E, Response> IntoRes<Streaming, Response, E> for ByteStream<E>
where
Response: Res<CustErr>,
CustErr: 'static,
Response: TryRes<E>,
E: 'static,
{
async fn into_res(self) -> Result<Response, ServerFnError<CustErr>> {
async fn into_res(self) -> Result<Response, E> {
Response::try_from_stream(Streaming::CONTENT_TYPE, self.into_inner())
}
}
impl<CustErr, Response> FromRes<Streaming, Response, CustErr> for ByteStream
impl<E, Response> FromRes<Streaming, Response, E> for ByteStream<E>
where
Response: ClientRes<CustErr> + Send,
Response: ClientRes<E> + Send,
{
async fn from_res(res: Response) -> Result<Self, ServerFnError<CustErr>> {
async fn from_res(res: Response) -> Result<Self, E> {
let stream = res.try_into_stream()?;
Ok(ByteStream(Box::pin(stream)))
}
@@ -160,35 +151,33 @@ impl Encoding for StreamingText {
/// end before the output will begin.
///
/// Streaming requests are only allowed over HTTP2 or HTTP3.
pub struct TextStream<CustErr = NoCustomError>(
Pin<Box<dyn Stream<Item = Result<String, ServerFnError<CustErr>>> + Send>>,
pub struct TextStream<E = ServerFnError>(
Pin<Box<dyn Stream<Item = Result<String, E>> + Send>>,
);
impl<CustErr> Debug for TextStream<CustErr> {
impl<E> Debug for TextStream<E> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_tuple("TextStream").finish()
}
}
impl TextStream {
impl<E> TextStream<E> {
/// Creates a new `ByteStream` from the given stream.
pub fn new(
value: impl Stream<Item = Result<String, ServerFnError>> + Send + 'static,
value: impl Stream<Item = Result<String, E>> + Send + 'static,
) -> Self {
Self(Box::pin(value.map(|value| value)))
}
}
impl<CustErr> TextStream<CustErr> {
impl<E> TextStream<E> {
/// Consumes the wrapper, returning a stream of text.
pub fn into_inner(
self,
) -> impl Stream<Item = Result<String, ServerFnError<CustErr>>> + Send {
pub fn into_inner(self) -> impl Stream<Item = Result<String, E>> + Send {
self.0
}
}
impl<S, T> From<S> for TextStream
impl<E, S, T> From<S> for TextStream<E>
where
S: Stream<Item = T> + Send + 'static,
T: Into<String>,
@@ -198,16 +187,13 @@ where
}
}
impl<CustErr, T, Request> IntoReq<StreamingText, Request, CustErr> for T
impl<E, T, Request> IntoReq<StreamingText, Request, E> for T
where
Request: ClientReq<CustErr>,
T: Into<TextStream>,
Request: ClientReq<E>,
T: Into<TextStream<E>>,
E: 'static,
{
fn into_req(
self,
path: &str,
accepts: &str,
) -> Result<Request, ServerFnError<CustErr>> {
fn into_req(self, path: &str, accepts: &str) -> Result<Request, E> {
let data = self.into();
Request::try_new_streaming(
path,
@@ -218,30 +204,32 @@ where
}
}
impl<CustErr, T, Request> FromReq<StreamingText, Request, CustErr> for T
impl<E, T, Request> FromReq<StreamingText, Request, E> for T
where
Request: Req<CustErr> + Send + 'static,
T: From<TextStream> + 'static,
Request: Req<E> + Send + 'static,
T: From<TextStream<E>> + 'static,
E: FromServerFnError,
{
async fn from_req(req: Request) -> Result<Self, ServerFnError<CustErr>> {
async fn from_req(req: Request) -> Result<Self, E> {
let data = req.try_into_stream()?;
let s = TextStream::new(data.map(|chunk| {
chunk.and_then(|bytes| {
String::from_utf8(bytes.to_vec())
.map_err(|e| ServerFnError::Deserialization(e.to_string()))
String::from_utf8(bytes.to_vec()).map_err(|e| {
ServerFnErrorErr::Deserialization(e.to_string())
.into_app_error()
})
})
}));
Ok(s.into())
}
}
impl<CustErr, Response> IntoRes<StreamingText, Response, CustErr>
for TextStream<CustErr>
impl<E, Response> IntoRes<StreamingText, Response, E> for TextStream<E>
where
Response: Res<CustErr>,
CustErr: 'static,
Response: TryRes<E>,
E: 'static,
{
async fn into_res(self) -> Result<Response, ServerFnError<CustErr>> {
async fn into_res(self) -> Result<Response, E> {
Response::try_from_stream(
Streaming::CONTENT_TYPE,
self.into_inner().map(|stream| stream.map(Into::into)),
@@ -249,16 +237,19 @@ where
}
}
impl<CustErr, Response> FromRes<StreamingText, Response, CustErr> for TextStream
impl<E, Response> FromRes<StreamingText, Response, E> for TextStream<E>
where
Response: ClientRes<CustErr> + Send,
Response: ClientRes<E> + Send,
E: FromServerFnError,
{
async fn from_res(res: Response) -> Result<Self, ServerFnError<CustErr>> {
async fn from_res(res: Response) -> Result<Self, E> {
let stream = res.try_into_stream()?;
Ok(TextStream(Box::pin(stream.map(|chunk| {
chunk.and_then(|bytes| {
String::from_utf8(bytes.into())
.map_err(|e| ServerFnError::Deserialization(e.to_string()))
String::from_utf8(bytes.into()).map_err(|e| {
ServerFnErrorErr::Deserialization(e.to_string())
.into_app_error()
})
})
}))))
}

View File

@@ -1,6 +1,6 @@
use super::{Encoding, FromReq, IntoReq};
use crate::{
error::ServerFnError,
error::{FromServerFnError, IntoAppError, ServerFnErrorErr},
request::{ClientReq, Req},
};
use http::Method;
@@ -17,32 +17,33 @@ impl Encoding for GetUrl {
const METHOD: Method = Method::GET;
}
impl<CustErr, T, Request> IntoReq<GetUrl, Request, CustErr> for T
impl<E, T, Request> IntoReq<GetUrl, Request, E> for T
where
Request: ClientReq<CustErr>,
Request: ClientReq<E>,
T: Serialize + Send,
E: FromServerFnError,
{
fn into_req(
self,
path: &str,
accepts: &str,
) -> Result<Request, ServerFnError<CustErr>> {
let data = serde_qs::to_string(&self)
.map_err(|e| ServerFnError::Serialization(e.to_string()))?;
fn into_req(self, path: &str, accepts: &str) -> Result<Request, E> {
let data = serde_qs::to_string(&self).map_err(|e| {
ServerFnErrorErr::Serialization(e.to_string()).into_app_error()
})?;
Request::try_new_get(path, accepts, GetUrl::CONTENT_TYPE, &data)
}
}
impl<CustErr, T, Request> FromReq<GetUrl, Request, CustErr> for T
impl<E, T, Request> FromReq<GetUrl, Request, E> for T
where
Request: Req<CustErr> + Send + 'static,
Request: Req<E> + Send + 'static,
T: DeserializeOwned,
E: FromServerFnError,
{
async fn from_req(req: Request) -> Result<Self, ServerFnError<CustErr>> {
async fn from_req(req: Request) -> Result<Self, E> {
let string_data = req.as_query().unwrap_or_default();
let args = serde_qs::Config::new(5, false)
.deserialize_str::<Self>(string_data)
.map_err(|e| ServerFnError::Args(e.to_string()))?;
.map_err(|e| {
ServerFnErrorErr::Args(e.to_string()).into_app_error()
})?;
Ok(args)
}
}
@@ -52,32 +53,33 @@ impl Encoding for PostUrl {
const METHOD: Method = Method::POST;
}
impl<CustErr, T, Request> IntoReq<PostUrl, Request, CustErr> for T
impl<E, T, Request> IntoReq<PostUrl, Request, E> for T
where
Request: ClientReq<CustErr>,
Request: ClientReq<E>,
T: Serialize + Send,
E: FromServerFnError,
{
fn into_req(
self,
path: &str,
accepts: &str,
) -> Result<Request, ServerFnError<CustErr>> {
let qs = serde_qs::to_string(&self)
.map_err(|e| ServerFnError::Serialization(e.to_string()))?;
fn into_req(self, path: &str, accepts: &str) -> Result<Request, E> {
let qs = serde_qs::to_string(&self).map_err(|e| {
ServerFnErrorErr::Serialization(e.to_string()).into_app_error()
})?;
Request::try_new_post(path, accepts, PostUrl::CONTENT_TYPE, qs)
}
}
impl<CustErr, T, Request> FromReq<PostUrl, Request, CustErr> for T
impl<E, T, Request> FromReq<PostUrl, Request, E> for T
where
Request: Req<CustErr> + Send + 'static,
Request: Req<E> + Send + 'static,
T: DeserializeOwned,
E: FromServerFnError,
{
async fn from_req(req: Request) -> Result<Self, ServerFnError<CustErr>> {
async fn from_req(req: Request) -> Result<Self, E> {
let string_data = req.try_into_string().await?;
let args = serde_qs::Config::new(5, false)
.deserialize_str::<Self>(&string_data)
.map_err(|e| ServerFnError::Args(e.to_string()))?;
.map_err(|e| {
ServerFnErrorErr::Args(e.to_string()).into_app_error()
})?;
Ok(args)
}
}
@@ -86,18 +88,18 @@ where
impl<T, Request, Response> Codec<Request, Response, GetUrlJson> for T
where
T: DeserializeOwned + Serialize + Send,
Request: Req<CustErr> + Send,
Response: Res<CustErr> + Send,
Request: Req<E> + Send,
Response: Res<E> + Send,
{
async fn from_req(req: Request) -> Result<Self, ServerFnError<CustErr>> {
async fn from_req(req: Request) -> Result<Self, ServerFnError<E>> {
let string_data = req.try_into_string()?;
let args = serde_json::from_str::<Self>(&string_data)
.map_err(|e| ServerFnError::Args(e.to_string()))?;
.map_err(|e| ServerFnErrorErr::Args(e.to_string()).into())?;
Ok(args)
}
async fn into_req(self) -> Result<Request, ServerFnError<CustErr>> {
async fn into_req(self) -> Result<Request, ServerFnError<E>> {
/* let qs = serde_qs::to_string(&self)?;
let req = http::Request::builder()
.method("GET")
@@ -110,7 +112,7 @@ where
todo!()
}
async fn from_res(res: Response) -> Result<Self, ServerFnError<CustErr>> {
async fn from_res(res: Response) -> Result<Self, ServerFnError<E>> {
todo!()
/* let (_parts, body) = res.into_parts();
@@ -118,7 +120,7 @@ where
.collect()
.await
.map(|c| c.to_bytes())
.map_err(|e| ServerFnError::Deserialization(e.to_string()))?;
.map_err(|e| ServerFnErrorErr::Deserialization(e.to_string()).into())?;
let string_data = String::from_utf8(body_bytes.to_vec())?;
serde_json::from_str(&string_data)
.map_err(|e| ServerFnError::Deserialization(e.to_string())) */

View File

@@ -1,7 +1,9 @@
use serde::{Deserialize, Serialize};
#![allow(deprecated)]
use base64::{engine::general_purpose::URL_SAFE, Engine as _};
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use std::{
fmt,
fmt::{Display, Write},
fmt::{self, Display, Write},
str::FromStr,
};
use thiserror::Error;
@@ -13,7 +15,7 @@ pub const SERVER_FN_ERROR_HEADER: &str = "serverfnerror";
impl From<ServerFnError> for Error {
fn from(e: ServerFnError) -> Self {
Error::from(ServerFnErrorErr::from(e))
Error::from(ServerFnErrorWrapper(e))
}
}
@@ -35,6 +37,11 @@ impl From<ServerFnError> for Error {
feature = "rkyv",
derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
)]
#[deprecated(
since = "0.8.0",
note = "Now server_fn can return any error type other than ServerFnError, \
so the WrappedServerError variant will be removed in 0.9.0"
)]
pub struct NoCustomError;
// Implement `Display` for `NoCustomError`
@@ -55,11 +62,21 @@ impl FromStr for NoCustomError {
/// Wraps some error type, which may implement any of [`Error`](trait@std::error::Error), [`Clone`], or
/// [`Display`].
#[derive(Debug)]
#[deprecated(
since = "0.8.0",
note = "Now server_fn can return any error type other than ServerFnError, \
so the WrappedServerError variant will be removed in 0.9.0"
)]
pub struct WrapError<T>(pub T);
/// A helper macro to convert a variety of different types into `ServerFnError`.
/// This should mostly be used if you are implementing `From<ServerFnError>` for `YourError`.
#[macro_export]
#[deprecated(
since = "0.8.0",
note = "Now server_fn can return any error type other than ServerFnError, \
so the WrappedServerError variant will be removed in 0.9.0"
)]
macro_rules! server_fn_error {
() => {{
use $crate::{ViaError, WrapError};
@@ -75,6 +92,12 @@ macro_rules! server_fn_error {
/// This trait serves as the conversion method between a variety of types
/// and [`ServerFnError`].
#[deprecated(
since = "0.8.0",
note = "Now server_fn can return any error type other than ServerFnError, \
so users should place their custom error type instead of \
ServerFnError"
)]
pub trait ViaError<E> {
/// Converts something into an error.
fn to_server_error(&self) -> ServerFnError<E>;
@@ -90,6 +113,7 @@ impl<E: ServerFnErrorKind + std::error::Error + Clone> ViaError<E>
}
// A type tag for ServerFnError so we can special case it
#[deprecated]
pub(crate) trait ServerFnErrorKind {}
impl ServerFnErrorKind for ServerFnError {}
@@ -131,7 +155,8 @@ impl<E> ViaError<E> for WrapError<E> {
}
}
/// Type for errors that can occur when using server functions.
/// A type that can be used as the return type of the server function for easy error conversion with `?` operator.
/// This type can be replaced with any other error type that implements `FromServerFnError`.
///
/// Unlike [`ServerFnErrorErr`], this does not implement [`Error`](trait@std::error::Error).
/// This means that other error types can easily be converted into it using the
@@ -142,6 +167,12 @@ impl<E> ViaError<E> for WrapError<E> {
derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
)]
pub enum ServerFnError<E = NoCustomError> {
#[deprecated(
since = "0.8.0",
note = "Now server_fn can return any error type other than \
ServerFnError, so users should place their custom error type \
instead of ServerFnError"
)]
/// A user-defined custom error type, which defaults to [`NoCustomError`].
WrappedServerError(E),
/// Error while trying to register the server function (only occurs in case of poisoned RwLock).
@@ -152,6 +183,8 @@ pub enum ServerFnError<E = NoCustomError> {
Response(String),
/// Occurs when there is an error while actually running the function on the server.
ServerError(String),
/// Occurs when there is an error while actually running the middleware on the server.
MiddlewareError(String),
/// Occurs on the client if there is an error deserializing the server's response.
Deserialization(String),
/// Occurs on the client if there is an error serializing the server function arguments.
@@ -198,6 +231,8 @@ where
),
ServerFnError::ServerError(s) =>
format!("error running server function: {s}"),
ServerFnError::MiddlewareError(s) =>
format!("error running middleware: {s}"),
ServerFnError::Deserialization(s) =>
format!("error deserializing server function results: {s}"),
ServerFnError::Serialization(s) =>
@@ -214,30 +249,45 @@ where
}
}
/// A serializable custom server function error type.
///
/// This is implemented for all types that implement [`FromStr`] + [`Display`].
///
/// This means you do not necessarily need the overhead of `serde` for a custom error type.
/// Instead, you can use something like `strum` to derive `FromStr` and `Display` for your
/// custom error type.
///
/// This is implemented for the default [`ServerFnError`], which uses [`NoCustomError`].
pub trait ServerFnErrorSerde: Sized {
/// Converts the custom error type to a [`String`].
fn ser(&self) -> Result<String, std::fmt::Error>;
/// Deserializes the custom error type from a [`String`].
fn de(data: &str) -> Self;
}
impl<CustErr> ServerFnErrorSerde for ServerFnError<CustErr>
impl<CustErr> FromServerFnError for ServerFnError<CustErr>
where
CustErr: FromStr + Display,
CustErr: std::fmt::Debug
+ Display
+ Serialize
+ DeserializeOwned
+ 'static
+ FromStr
+ Display,
{
fn ser(&self) -> Result<String, std::fmt::Error> {
fn from_server_fn_error(value: ServerFnErrorErr) -> Self {
match value {
ServerFnErrorErr::Registration(value) => {
ServerFnError::Registration(value)
}
ServerFnErrorErr::Request(value) => ServerFnError::Request(value),
ServerFnErrorErr::ServerError(value) => {
ServerFnError::ServerError(value)
}
ServerFnErrorErr::MiddlewareError(value) => {
ServerFnError::MiddlewareError(value)
}
ServerFnErrorErr::Deserialization(value) => {
ServerFnError::Deserialization(value)
}
ServerFnErrorErr::Serialization(value) => {
ServerFnError::Serialization(value)
}
ServerFnErrorErr::Args(value) => ServerFnError::Args(value),
ServerFnErrorErr::MissingArg(value) => {
ServerFnError::MissingArg(value)
}
ServerFnErrorErr::Response(value) => ServerFnError::Response(value),
}
}
fn ser(&self) -> String {
let mut buf = String::new();
match self {
let result = match self {
ServerFnError::WrappedServerError(e) => {
write!(&mut buf, "WrappedServerFn|{e}")
}
@@ -249,6 +299,9 @@ where
ServerFnError::ServerError(e) => {
write!(&mut buf, "ServerError|{e}")
}
ServerFnError::MiddlewareError(e) => {
write!(&mut buf, "MiddlewareError|{e}")
}
ServerFnError::Deserialization(e) => {
write!(&mut buf, "Deserialization|{e}")
}
@@ -259,8 +312,11 @@ where
ServerFnError::MissingArg(e) => {
write!(&mut buf, "MissingArg|{e}")
}
}?;
Ok(buf)
};
match result {
Ok(()) => buf,
Err(_) => "Serialization|".to_string(),
}
}
fn de(data: &str) -> Self {
@@ -311,20 +367,13 @@ where
}
}
/// Type for errors that can occur when using server functions.
///
/// Unlike [`ServerFnError`], this implements [`std::error::Error`]. This means
/// it can be used in situations in which the `Error` trait is required, but its
/// not possible to create a blanket implementation that converts other errors into
/// this type.
///
/// [`ServerFnError`] and [`ServerFnErrorErr`] mutually implement [`From`], so
/// it is easy to convert between the two types.
#[derive(Error, Debug, Clone, PartialEq, Eq)]
pub enum ServerFnErrorErr<E = NoCustomError> {
/// A user-defined custom error type, which defaults to [`NoCustomError`].
#[error("internal error: {0}")]
WrappedServerError(E),
/// Type for errors that can occur when using server functions. If you need to return a custom error type from a server function, implement `FromServerFnError` for your custom error type.
#[derive(Error, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
)]
pub enum ServerFnErrorErr {
/// Error while trying to register the server function (only occurs in case of poisoned RwLock).
#[error("error while trying to register the server function: {0}")]
Registration(String),
@@ -334,6 +383,9 @@ pub enum ServerFnErrorErr<E = NoCustomError> {
/// Occurs when there is an error while actually running the function on the server.
#[error("error running server function: {0}")]
ServerError(String),
/// Occurs when there is an error while actually running the middleware on the server.
#[error("error running middleware: {0}")]
MiddlewareError(String),
/// Occurs on the client if there is an error deserializing the server's response.
#[error("error deserializing server function results: {0}")]
Deserialization(String),
@@ -351,34 +403,6 @@ pub enum ServerFnErrorErr<E = NoCustomError> {
Response(String),
}
impl<CustErr> From<ServerFnError<CustErr>> for ServerFnErrorErr<CustErr> {
fn from(value: ServerFnError<CustErr>) -> Self {
match value {
ServerFnError::Registration(value) => {
ServerFnErrorErr::Registration(value)
}
ServerFnError::Request(value) => ServerFnErrorErr::Request(value),
ServerFnError::ServerError(value) => {
ServerFnErrorErr::ServerError(value)
}
ServerFnError::Deserialization(value) => {
ServerFnErrorErr::Deserialization(value)
}
ServerFnError::Serialization(value) => {
ServerFnErrorErr::Serialization(value)
}
ServerFnError::Args(value) => ServerFnErrorErr::Args(value),
ServerFnError::MissingArg(value) => {
ServerFnErrorErr::MissingArg(value)
}
ServerFnError::WrappedServerError(value) => {
ServerFnErrorErr::WrappedServerError(value)
}
ServerFnError::Response(value) => ServerFnErrorErr::Response(value),
}
}
}
/// Associates a particular server function error with the server function
/// found at a particular path.
///
@@ -386,15 +410,15 @@ impl<CustErr> From<ServerFnError<CustErr>> for ServerFnErrorErr<CustErr> {
/// without JavaScript/WASM supported, by encoding it in the URL as a query string.
/// This is useful for progressive enhancement.
#[derive(Debug)]
pub struct ServerFnUrlError<CustErr> {
pub struct ServerFnUrlError<E> {
path: String,
error: ServerFnError<CustErr>,
error: E,
}
impl<CustErr> ServerFnUrlError<CustErr> {
impl<E: FromServerFnError> ServerFnUrlError<E> {
/// Creates a new structure associating the server function at some path
/// with a particular error.
pub fn new(path: impl Display, error: ServerFnError<CustErr>) -> Self {
pub fn new(path: impl Display, error: E) -> Self {
Self {
path: path.to_string(),
error,
@@ -402,7 +426,7 @@ impl<CustErr> ServerFnUrlError<CustErr> {
}
/// The error itself.
pub fn error(&self) -> &ServerFnError<CustErr> {
pub fn error(&self) -> &E {
&self.error
}
@@ -412,17 +436,11 @@ impl<CustErr> ServerFnUrlError<CustErr> {
}
/// Adds an encoded form of this server function error to the given base URL.
pub fn to_url(&self, base: &str) -> Result<Url, url::ParseError>
where
CustErr: FromStr + Display,
{
pub fn to_url(&self, base: &str) -> Result<Url, url::ParseError> {
let mut url = Url::parse(base)?;
url.query_pairs_mut()
.append_pair("__path", &self.path)
.append_pair(
"__err",
&ServerFnErrorSerde::ser(&self.error).unwrap_or_default(),
);
.append_pair("__err", &URL_SAFE.encode(self.error.ser()));
Ok(url)
}
@@ -448,16 +466,102 @@ impl<CustErr> ServerFnUrlError<CustErr> {
*path = url.to_string();
}
}
/// Decodes an error from a URL.
pub fn decode_err(err: &str) -> E {
let decoded = match URL_SAFE.decode(err) {
Ok(decoded) => decoded,
Err(err) => {
return ServerFnErrorErr::Deserialization(err.to_string())
.into_app_error();
}
};
let s = match String::from_utf8(decoded) {
Ok(s) => s,
Err(err) => {
return ServerFnErrorErr::Deserialization(err.to_string())
.into_app_error();
}
};
E::de(&s)
}
}
impl<CustErr> From<ServerFnUrlError<CustErr>> for ServerFnError<CustErr> {
fn from(error: ServerFnUrlError<CustErr>) -> Self {
impl<E> From<ServerFnUrlError<E>> for ServerFnError<E> {
fn from(error: ServerFnUrlError<E>) -> Self {
error.error.into()
}
}
impl<E> From<ServerFnUrlError<ServerFnError<E>>> for ServerFnError<E> {
fn from(error: ServerFnUrlError<ServerFnError<E>>) -> Self {
error.error
}
}
impl<CustErr> From<ServerFnUrlError<CustErr>> for ServerFnErrorErr<CustErr> {
fn from(error: ServerFnUrlError<CustErr>) -> Self {
error.error.into()
#[derive(Debug)]
#[doc(hidden)]
/// Only used instantly only when a framework needs E: Error.
pub struct ServerFnErrorWrapper<E: FromServerFnError>(pub E);
impl<E: FromServerFnError> Display for ServerFnErrorWrapper<E> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0.ser())
}
}
impl<E: FromServerFnError> std::error::Error for ServerFnErrorWrapper<E> {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
None
}
}
/// A trait for types that can be returned from a server function.
pub trait FromServerFnError:
std::fmt::Debug + Serialize + DeserializeOwned + 'static
{
/// Converts a [`ServerFnErrorErr`] into the application-specific custom error type.
fn from_server_fn_error(value: ServerFnErrorErr) -> Self;
/// Converts the custom error type to a [`String`]. Defaults to serializing to JSON.
fn ser(&self) -> String {
serde_json::to_string(self).unwrap_or_else(|e| {
serde_json::to_string(&Self::from_server_fn_error(
ServerFnErrorErr::Serialization(e.to_string()),
))
.expect(
"error serializing should success at least with the \
Serialization error",
)
})
}
/// Deserializes the custom error type from a [`&str`]. Defaults to deserializing from JSON.
fn de(data: &str) -> Self {
serde_json::from_str(data).unwrap_or_else(|e| {
ServerFnErrorErr::Deserialization(e.to_string()).into_app_error()
})
}
}
/// A helper trait for converting a [`ServerFnErrorErr`] into an application-specific custom error type that implements [`FromServerFnError`].
pub trait IntoAppError<E> {
/// Converts a [`ServerFnErrorErr`] into the application-specific custom error type.
fn into_app_error(self) -> E;
}
impl<E> IntoAppError<E> for ServerFnErrorErr
where
E: FromServerFnError,
{
fn into_app_error(self) -> E {
E::from_server_fn_error(self)
}
}
#[test]
fn assert_from_server_fn_error_impl() {
fn assert_impl<T: FromServerFnError>() {}
assert_impl::<ServerFnError>();
}

View File

@@ -130,17 +130,19 @@ use client::Client;
use codec::{Encoding, FromReq, FromRes, IntoReq, IntoRes};
#[doc(hidden)]
pub use const_format;
#[doc(hidden)]
pub use const_str;
use dashmap::DashMap;
pub use error::ServerFnError;
use error::ServerFnErrorSerde;
#[cfg(feature = "form-redirects")]
use error::ServerFnUrlError;
use error::{FromServerFnError, ServerFnErrorErr};
use http::Method;
use middleware::{Layer, Service};
use middleware::{BoxedService, Layer, Service};
use once_cell::sync::Lazy;
use redirect::RedirectHook;
use request::Req;
use response::{ClientRes, Res};
use response::{ClientRes, Res, TryRes};
#[cfg(feature = "rkyv")]
pub use rkyv;
#[doc(hidden)]
@@ -148,7 +150,7 @@ pub use serde;
#[doc(hidden)]
#[cfg(feature = "serde-lite")]
pub use serde_lite;
use std::{fmt::Display, future::Future, pin::Pin, str::FromStr, sync::Arc};
use std::{future::Future, pin::Pin, sync::Arc};
#[doc(hidden)]
pub use xxhash_rust;
@@ -203,7 +205,7 @@ where
type ServerRequest: Req<Self::Error> + Send;
/// The type of the HTTP response returned by the server function on the server side.
type ServerResponse: Res<Self::Error> + Send;
type ServerResponse: Res + TryRes<Self::Error> + Send;
/// The return type of the server function.
///
@@ -222,9 +224,8 @@ where
/// The [`Encoding`] used in the response for the result of the server function.
type OutputEncoding: Encoding;
/// The type of the custom error on [`ServerFnError`], if any. (If there is no
/// custom error type, this can be `NoCustomError` by default.)
type Error: FromStr + Display;
/// The type of the error on the server function. Typically [`ServerFnError`], but allowed to be any type that implements [`FromServerFnError`].
type Error: FromServerFnError;
/// Returns [`Self::PATH`].
fn url() -> &'static str {
@@ -240,7 +241,7 @@ where
/// The body of the server function. This will only run on the server.
fn run_body(
self,
) -> impl Future<Output = Result<Self::Output, ServerFnError<Self::Error>>> + Send;
) -> impl Future<Output = Result<Self::Output, Self::Error>> + Send;
#[doc(hidden)]
fn run_on_server(
@@ -265,7 +266,10 @@ where
.map(|res| (res, None))
.unwrap_or_else(|e| {
(
Self::ServerResponse::error_response(Self::PATH, &e),
Self::ServerResponse::error_response(
Self::PATH,
e.ser(),
),
Some(e),
)
});
@@ -298,8 +302,7 @@ where
#[doc(hidden)]
fn run_on_client(
self,
) -> impl Future<Output = Result<Self::Output, ServerFnError<Self::Error>>> + Send
{
) -> impl Future<Output = Result<Self::Output, Self::Error>> + Send {
async move {
// create and send request on client
let req =
@@ -313,8 +316,7 @@ where
fn run_on_client_with_req(
req: <Self::Client as Client<Self::Error>>::Request,
redirect_hook: Option<&RedirectHook>,
) -> impl Future<Output = Result<Self::Output, ServerFnError<Self::Error>>> + Send
{
) -> impl Future<Output = Result<Self::Output, Self::Error>> + Send {
async move {
let res = Self::Client::send(req).await?;
@@ -325,7 +327,7 @@ where
// if it returns an error status, deserialize the error using FromStr
let res = if (400..=599).contains(&status) {
let text = res.try_into_string().await?;
Err(ServerFnError::<Self::Error>::de(&text))
Err(Self::Error::de(&text))
} else {
// otherwise, deserialize the body as is
Ok(Self::Output::from_res(res).await)
@@ -345,9 +347,8 @@ where
#[doc(hidden)]
fn execute_on_server(
req: Self::ServerRequest,
) -> impl Future<
Output = Result<Self::ServerResponse, ServerFnError<Self::Error>>,
> + Send {
) -> impl Future<Output = Result<Self::ServerResponse, Self::Error>> + Send
{
async {
let this = Self::from_req(req).await?;
let output = this.run_body().await?;
@@ -387,21 +388,20 @@ pub struct ServerFnTraitObj<Req, Res> {
method: Method,
handler: fn(Req) -> Pin<Box<dyn Future<Output = Res> + Send>>,
middleware: fn() -> MiddlewareSet<Req, Res>,
ser: fn(ServerFnErrorErr) -> String,
}
impl<Req, Res> ServerFnTraitObj<Req, Res> {
/// Converts the relevant parts of a server function into a trait object.
pub const fn new(
path: &'static str,
method: Method,
pub const fn new<S: ServerFn<ServerRequest = Req, ServerResponse = Res>>(
handler: fn(Req) -> Pin<Box<dyn Future<Output = Res> + Send>>,
middleware: fn() -> MiddlewareSet<Req, Res>,
) -> Self {
Self {
path,
method,
path: S::PATH,
method: S::InputEncoding::METHOD,
handler,
middleware,
middleware: S::middlewares,
ser: |e| S::Error::from_server_fn_error(e).ser(),
}
}
@@ -424,6 +424,16 @@ impl<Req, Res> ServerFnTraitObj<Req, Res> {
pub fn middleware(&self) -> MiddlewareSet<Req, Res> {
(self.middleware)()
}
/// Converts the server function into a boxed service.
pub fn boxed(self) -> BoxedService<Req, Res>
where
Self: Service<Req, Res>,
Req: 'static,
Res: 'static,
{
BoxedService::new(self.ser, self)
}
}
impl<Req, Res> Service<Req, Res> for ServerFnTraitObj<Req, Res>
@@ -431,7 +441,11 @@ where
Req: Send + 'static,
Res: 'static,
{
fn run(&mut self, req: Req) -> Pin<Box<dyn Future<Output = Res> + Send>> {
fn run(
&mut self,
req: Req,
_ser: fn(ServerFnErrorErr) -> String,
) -> Pin<Box<dyn Future<Output = Res> + Send>> {
let handler = self.handler;
Box::pin(async move { handler(req).await })
}
@@ -444,6 +458,7 @@ impl<Req, Res> Clone for ServerFnTraitObj<Req, Res> {
method: self.method.clone(),
handler: self.handler,
middleware: self.middleware,
ser: self.ser,
}
}
}
@@ -467,8 +482,8 @@ impl<Req: 'static, Res: 'static> inventory::Collect
#[cfg(feature = "axum-no-default")]
pub mod axum {
use crate::{
middleware::{BoxedService, Service},
Encoding, LazyServerFnMap, ServerFn, ServerFnTraitObj,
middleware::BoxedService, Encoding, LazyServerFnMap, ServerFn,
ServerFnTraitObj,
};
use axum::body::Body;
use http::{Method, Request, Response, StatusCode};
@@ -490,12 +505,7 @@ pub mod axum {
{
REGISTERED_SERVER_FUNCTIONS.insert(
(T::PATH.into(), T::InputEncoding::METHOD),
ServerFnTraitObj::new(
T::PATH,
T::InputEncoding::METHOD,
|req| Box::pin(T::run_on_server(req)),
T::middlewares,
),
ServerFnTraitObj::new::<T>(|req| Box::pin(T::run_on_server(req))),
);
}
@@ -539,7 +549,7 @@ pub mod axum {
let key = (path.into(), method);
REGISTERED_SERVER_FUNCTIONS.get(&key).map(|server_fn| {
let middleware = (server_fn.middleware)();
let mut service = BoxedService::new(server_fn.clone());
let mut service = server_fn.clone().boxed();
for middleware in middleware {
service = middleware.layer(service);
}
@@ -578,12 +588,7 @@ pub mod actix {
{
REGISTERED_SERVER_FUNCTIONS.insert(
(T::PATH.into(), T::InputEncoding::METHOD),
ServerFnTraitObj::new(
T::PATH,
T::InputEncoding::METHOD,
|req| Box::pin(T::run_on_server(req)),
T::middlewares,
),
ServerFnTraitObj::new::<T>(|req| Box::pin(T::run_on_server(req))),
);
}
@@ -603,7 +608,6 @@ pub mod actix {
let method = req.method();
if let Some(mut service) = get_server_fn_service(path, method) {
service
.0
.run(ActixRequest::from((req, payload)))
.await
.0
@@ -644,7 +648,7 @@ pub mod actix {
REGISTERED_SERVER_FUNCTIONS.get(&(path.into(), method)).map(
|server_fn| {
let middleware = (server_fn.middleware)();
let mut service = BoxedService::new(server_fn.clone());
let mut service = server_fn.clone().boxed();
for middleware in middleware {
service = middleware.layer(service);
}

View File

@@ -1,3 +1,4 @@
use crate::error::ServerFnErrorErr;
use std::{future::Future, pin::Pin};
/// An abstraction over a middleware layer, which can be used to add additional
@@ -8,12 +9,31 @@ pub trait Layer<Req, Res>: Send + Sync + 'static {
}
/// A type-erased service, which takes an HTTP request and returns a response.
pub struct BoxedService<Req, Res>(pub Box<dyn Service<Req, Res> + Send>);
pub struct BoxedService<Req, Res> {
/// A function that converts a [`ServerFnErrorErr`] into a string.
pub ser: fn(ServerFnErrorErr) -> String,
/// The inner service.
pub service: Box<dyn Service<Req, Res> + Send>,
}
impl<Req, Res> BoxedService<Req, Res> {
/// Constructs a type-erased service from this service.
pub fn new(service: impl Service<Req, Res> + Send + 'static) -> Self {
Self(Box::new(service))
pub fn new(
ser: fn(ServerFnErrorErr) -> String,
service: impl Service<Req, Res> + Send + 'static,
) -> Self {
Self {
ser,
service: Box::new(service),
}
}
/// Converts a request into a response by running the inner service.
pub fn run(
&mut self,
req: Req,
) -> Pin<Box<dyn Future<Output = Res> + Send>> {
self.service.run(req, self.ser)
}
}
@@ -23,37 +43,36 @@ pub trait Service<Request, Response> {
fn run(
&mut self,
req: Request,
ser: fn(ServerFnErrorErr) -> String,
) -> Pin<Box<dyn Future<Output = Response> + Send>>;
}
#[cfg(feature = "axum-no-default")]
mod axum {
use super::{BoxedService, Service};
use crate::{response::Res, ServerFnError};
use crate::{error::ServerFnErrorErr, response::Res, ServerFnError};
use axum::body::Body;
use http::{Request, Response};
use std::{
fmt::{Debug, Display},
future::Future,
pin::Pin,
};
use std::{future::Future, pin::Pin};
impl<S> super::Service<Request<Body>, Response<Body>> for S
where
S: tower::Service<Request<Body>, Response = Response<Body>>,
S::Future: Send + 'static,
S::Error: Into<ServerFnError> + Send + Debug + Display + Sync + 'static,
S::Error: std::fmt::Display + Send + 'static,
{
fn run(
&mut self,
req: Request<Body>,
ser: fn(ServerFnErrorErr) -> String,
) -> Pin<Box<dyn Future<Output = Response<Body>> + Send>> {
let path = req.uri().path().to_string();
let inner = self.call(req);
Box::pin(async move {
inner.await.unwrap_or_else(|e| {
let err = ServerFnError::new(e);
Response::<Body>::error_response(&path, &err)
let err =
ser(ServerFnErrorErr::MiddlewareError(e.to_string()));
Response::<Body>::error_response(&path, err)
})
})
}
@@ -80,7 +99,7 @@ mod axum {
}
fn call(&mut self, req: Request<Body>) -> Self::Future {
let inner = self.0.run(req);
let inner = self.service.run(req, self.ser);
Box::pin(async move { Ok(inner.await) })
}
}
@@ -97,7 +116,7 @@ mod axum {
&self,
inner: BoxedService<Request<Body>, Response<Body>>,
) -> BoxedService<Request<Body>, Response<Body>> {
BoxedService(Box::new(self.layer(inner)))
BoxedService::new(inner.ser, self.layer(inner))
}
}
}
@@ -105,33 +124,31 @@ mod axum {
#[cfg(feature = "actix")]
mod actix {
use crate::{
error::ServerFnErrorErr,
request::actix::ActixRequest,
response::{actix::ActixResponse, Res},
ServerFnError,
};
use actix_web::{HttpRequest, HttpResponse};
use std::{
fmt::{Debug, Display},
future::Future,
pin::Pin,
};
use std::{future::Future, pin::Pin};
impl<S> super::Service<HttpRequest, HttpResponse> for S
where
S: actix_web::dev::Service<HttpRequest, Response = HttpResponse>,
S::Future: Send + 'static,
S::Error: Into<ServerFnError> + Debug + Display + 'static,
S::Error: std::fmt::Display + Send + 'static,
{
fn run(
&mut self,
req: HttpRequest,
ser: fn(ServerFnErrorErr) -> String,
) -> Pin<Box<dyn Future<Output = HttpResponse> + Send>> {
let path = req.uri().path().to_string();
let inner = self.call(req);
Box::pin(async move {
inner.await.unwrap_or_else(|e| {
let err = ServerFnError::new(e);
ActixResponse::error_response(&path, &err).take()
let err =
ser(ServerFnErrorErr::MiddlewareError(e.to_string()));
ActixResponse::error_response(&path, err).take()
})
})
}
@@ -141,18 +158,20 @@ mod actix {
where
S: actix_web::dev::Service<HttpRequest, Response = HttpResponse>,
S::Future: Send + 'static,
S::Error: Into<ServerFnError> + Debug + Display + 'static,
S::Error: std::fmt::Display + Send + 'static,
{
fn run(
&mut self,
req: ActixRequest,
ser: fn(ServerFnErrorErr) -> String,
) -> Pin<Box<dyn Future<Output = ActixResponse> + Send>> {
let path = req.0 .0.uri().path().to_string();
let inner = self.call(req.0.take().0);
Box::pin(async move {
ActixResponse::from(inner.await.unwrap_or_else(|e| {
let err = ServerFnError::new(e);
ActixResponse::error_response(&path, &err).take()
let err =
ser(ServerFnErrorErr::MiddlewareError(e.to_string()));
ActixResponse::error_response(&path, err).take()
}))
})
}

View File

@@ -1,4 +1,8 @@
use crate::{error::ServerFnError, request::Req};
use crate::{
error::{FromServerFnError, IntoAppError, ServerFnErrorErr},
request::Req,
ServerFnError,
};
use actix_web::{web::Payload, HttpRequest};
use bytes::Bytes;
use futures::{Stream, StreamExt};
@@ -33,9 +37,9 @@ impl From<(HttpRequest, Payload)> for ActixRequest {
}
}
impl<CustErr> Req<CustErr> for ActixRequest
impl<E> Req<E> for ActixRequest
where
CustErr: 'static,
E: FromServerFnError,
{
fn as_query(&self) -> Option<&str> {
self.0 .0.uri().query()
@@ -53,44 +57,39 @@ where
self.header("Referer")
}
fn try_into_bytes(
self,
) -> impl Future<Output = Result<Bytes, ServerFnError<CustErr>>> + Send
{
fn try_into_bytes(self) -> impl Future<Output = Result<Bytes, E>> + Send {
// Actix is going to keep this on a single thread anyway so it's fine to wrap it
// with SendWrapper, which makes it `Send` but will panic if it moves to another thread
SendWrapper::new(async move {
let payload = self.0.take().1;
payload
.to_bytes()
.await
.map_err(|e| ServerFnError::Deserialization(e.to_string()))
payload.to_bytes().await.map_err(|e| {
ServerFnErrorErr::Deserialization(e.to_string())
.into_app_error()
})
})
}
fn try_into_string(
self,
) -> impl Future<Output = Result<String, ServerFnError<CustErr>>> + Send
{
fn try_into_string(self) -> impl Future<Output = Result<String, E>> + Send {
// Actix is going to keep this on a single thread anyway so it's fine to wrap it
// with SendWrapper, which makes it `Send` but will panic if it moves to another thread
SendWrapper::new(async move {
let payload = self.0.take().1;
let bytes = payload
.to_bytes()
.await
.map_err(|e| ServerFnError::Deserialization(e.to_string()))?;
String::from_utf8(bytes.into())
.map_err(|e| ServerFnError::Deserialization(e.to_string()))
let bytes = payload.to_bytes().await.map_err(|e| {
E::from_server_fn_error(ServerFnErrorErr::Deserialization(
e.to_string(),
))
})?;
String::from_utf8(bytes.into()).map_err(|e| {
E::from_server_fn_error(ServerFnErrorErr::Deserialization(
e.to_string(),
))
})
})
}
fn try_into_stream(
self,
) -> Result<
impl Stream<Item = Result<Bytes, ServerFnError>> + Send,
ServerFnError<CustErr>,
> {
) -> Result<impl Stream<Item = Result<Bytes, E>> + Send, E> {
let payload = self.0.take().1;
let stream = payload.map(|res| {
res.map_err(|e| ServerFnError::Deserialization(e.to_string()))

View File

@@ -1,4 +1,7 @@
use crate::{error::ServerFnError, request::Req};
use crate::{
error::{FromServerFnError, IntoAppError, ServerFnErrorErr},
request::Req,
};
use axum::body::{Body, Bytes};
use futures::{Stream, StreamExt};
use http::{
@@ -8,9 +11,9 @@ use http::{
use http_body_util::BodyExt;
use std::borrow::Cow;
impl<CustErr> Req<CustErr> for Request<Body>
impl<E> Req<E> for Request<Body>
where
CustErr: 'static,
E: FromServerFnError,
{
fn as_query(&self) -> Option<&str> {
self.uri().query()
@@ -34,29 +37,29 @@ where
.map(|h| String::from_utf8_lossy(h.as_bytes()))
}
async fn try_into_bytes(self) -> Result<Bytes, ServerFnError<CustErr>> {
async fn try_into_bytes(self) -> Result<Bytes, E> {
let (_parts, body) = self.into_parts();
body.collect()
.await
.map(|c| c.to_bytes())
.map_err(|e| ServerFnError::Deserialization(e.to_string()))
body.collect().await.map(|c| c.to_bytes()).map_err(|e| {
ServerFnErrorErr::Deserialization(e.to_string()).into_app_error()
})
}
async fn try_into_string(self) -> Result<String, ServerFnError<CustErr>> {
async fn try_into_string(self) -> Result<String, E> {
let bytes = self.try_into_bytes().await?;
String::from_utf8(bytes.to_vec())
.map_err(|e| ServerFnError::Deserialization(e.to_string()))
String::from_utf8(bytes.to_vec()).map_err(|e| {
ServerFnErrorErr::Deserialization(e.to_string()).into_app_error()
})
}
fn try_into_stream(
self,
) -> Result<
impl Stream<Item = Result<Bytes, ServerFnError>> + Send + 'static,
ServerFnError<CustErr>,
> {
) -> Result<impl Stream<Item = Result<Bytes, E>> + Send + 'static, E> {
Ok(self.into_body().into_data_stream().map(|chunk| {
chunk.map_err(|e| ServerFnError::Deserialization(e.to_string()))
chunk.map_err(|e| {
ServerFnErrorErr::Deserialization(e.to_string())
.into_app_error()
})
}))
}
}

View File

@@ -1,5 +1,8 @@
use super::ClientReq;
use crate::{client::get_server_url, error::ServerFnError};
use crate::{
client::get_server_url,
error::{FromServerFnError, ServerFnErrorErr},
};
use bytes::Bytes;
use futures::{Stream, StreamExt};
pub use gloo_net::http::Request;
@@ -83,7 +86,10 @@ fn abort_signal() -> (Option<AbortOnDrop>, Option<AbortSignal>) {
(ctrl.map(|ctrl| AbortOnDrop(Some(ctrl))), signal)
}
impl<CustErr> ClientReq<CustErr> for BrowserRequest {
impl<E> ClientReq<E> for BrowserRequest
where
E: FromServerFnError,
{
type FormData = BrowserFormData;
fn try_new_get(
@@ -91,7 +97,7 @@ impl<CustErr> ClientReq<CustErr> for BrowserRequest {
accepts: &str,
content_type: &str,
query: &str,
) -> Result<Self, ServerFnError<CustErr>> {
) -> Result<Self, E> {
let (abort_ctrl, abort_signal) = abort_signal();
let server_url = get_server_url();
let mut url = String::with_capacity(
@@ -107,7 +113,11 @@ impl<CustErr> ClientReq<CustErr> for BrowserRequest {
.header("Accept", accepts)
.abort_signal(abort_signal.as_ref())
.build()
.map_err(|e| ServerFnError::Request(e.to_string()))?,
.map_err(|e| {
E::from_server_fn_error(ServerFnErrorErr::Request(
e.to_string(),
))
})?,
abort_ctrl,
})))
}
@@ -117,7 +127,7 @@ impl<CustErr> ClientReq<CustErr> for BrowserRequest {
accepts: &str,
content_type: &str,
body: String,
) -> Result<Self, ServerFnError<CustErr>> {
) -> Result<Self, E> {
let (abort_ctrl, abort_signal) = abort_signal();
let server_url = get_server_url();
let mut url = String::with_capacity(server_url.len() + path.len());
@@ -129,7 +139,11 @@ impl<CustErr> ClientReq<CustErr> for BrowserRequest {
.header("Accept", accepts)
.abort_signal(abort_signal.as_ref())
.body(body)
.map_err(|e| ServerFnError::Request(e.to_string()))?,
.map_err(|e| {
E::from_server_fn_error(ServerFnErrorErr::Request(
e.to_string(),
))
})?,
abort_ctrl,
})))
}
@@ -139,7 +153,7 @@ impl<CustErr> ClientReq<CustErr> for BrowserRequest {
accepts: &str,
content_type: &str,
body: Bytes,
) -> Result<Self, ServerFnError<CustErr>> {
) -> Result<Self, E> {
let (abort_ctrl, abort_signal) = abort_signal();
let server_url = get_server_url();
let mut url = String::with_capacity(server_url.len() + path.len());
@@ -153,7 +167,11 @@ impl<CustErr> ClientReq<CustErr> for BrowserRequest {
.header("Accept", accepts)
.abort_signal(abort_signal.as_ref())
.body(body)
.map_err(|e| ServerFnError::Request(e.to_string()))?,
.map_err(|e| {
E::from_server_fn_error(ServerFnErrorErr::Request(
e.to_string(),
))
})?,
abort_ctrl,
})))
}
@@ -162,7 +180,7 @@ impl<CustErr> ClientReq<CustErr> for BrowserRequest {
path: &str,
accepts: &str,
body: Self::FormData,
) -> Result<Self, ServerFnError<CustErr>> {
) -> Result<Self, E> {
let (abort_ctrl, abort_signal) = abort_signal();
let server_url = get_server_url();
let mut url = String::with_capacity(server_url.len() + path.len());
@@ -173,7 +191,11 @@ impl<CustErr> ClientReq<CustErr> for BrowserRequest {
.header("Accept", accepts)
.abort_signal(abort_signal.as_ref())
.body(body.0.take())
.map_err(|e| ServerFnError::Request(e.to_string()))?,
.map_err(|e| {
E::from_server_fn_error(ServerFnErrorErr::Request(
e.to_string(),
))
})?,
abort_ctrl,
})))
}
@@ -183,17 +205,17 @@ impl<CustErr> ClientReq<CustErr> for BrowserRequest {
accepts: &str,
content_type: &str,
body: Self::FormData,
) -> Result<Self, ServerFnError<CustErr>> {
) -> Result<Self, E> {
let (abort_ctrl, abort_signal) = abort_signal();
let form_data = body.0.take();
let url_params =
UrlSearchParams::new_with_str_sequence_sequence(&form_data)
.map_err(|e| {
ServerFnError::Serialization(e.as_string().unwrap_or_else(
|| {
E::from_server_fn_error(ServerFnErrorErr::Serialization(
e.as_string().unwrap_or_else(|| {
"Could not serialize FormData to URLSearchParams"
.to_string()
},
}),
))
})?;
Ok(Self(SendWrapper::new(RequestInner {
@@ -202,7 +224,11 @@ impl<CustErr> ClientReq<CustErr> for BrowserRequest {
.header("Accept", accepts)
.abort_signal(abort_signal.as_ref())
.body(url_params)
.map_err(|e| ServerFnError::Request(e.to_string()))?,
.map_err(|e| {
E::from_server_fn_error(ServerFnErrorErr::Request(
e.to_string(),
))
})?,
abort_ctrl,
})))
}
@@ -212,11 +238,16 @@ impl<CustErr> ClientReq<CustErr> for BrowserRequest {
accepts: &str,
content_type: &str,
body: impl Stream<Item = Bytes> + 'static,
) -> Result<Self, ServerFnError<CustErr>> {
) -> Result<Self, E> {
// TODO abort signal
let (request, abort_ctrl) =
streaming_request(path, accepts, content_type, body)
.map_err(|e| ServerFnError::Request(format!("{e:?}")))?;
streaming_request(path, accepts, content_type, body).map_err(
|e| {
E::from_server_fn_error(ServerFnErrorErr::Request(format!(
"{e:?}"
)))
},
)?;
Ok(Self(SendWrapper::new(RequestInner {
request,
abort_ctrl,

View File

@@ -12,7 +12,10 @@
//! * `wasm32-wasip*` integration crate `leptos_wasi` is using this
//! crate under the hood.
use crate::request::Req;
use crate::{
error::{FromServerFnError, IntoAppError, ServerFnErrorErr},
request::Req,
};
use bytes::Bytes;
use futures::{
stream::{self, Stream},
@@ -21,30 +24,23 @@ use futures::{
use http::Request;
use std::borrow::Cow;
impl<CustErr> Req<CustErr> for Request<Bytes>
impl<E> Req<E> for Request<Bytes>
where
CustErr: 'static,
E: FromServerFnError,
{
async fn try_into_bytes(
self,
) -> Result<Bytes, crate::ServerFnError<CustErr>> {
async fn try_into_bytes(self) -> Result<Bytes, E> {
Ok(self.into_body())
}
async fn try_into_string(
self,
) -> Result<String, crate::ServerFnError<CustErr>> {
async fn try_into_string(self) -> Result<String, E> {
String::from_utf8(self.into_body().into()).map_err(|err| {
crate::ServerFnError::Deserialization(err.to_string())
ServerFnErrorErr::Deserialization(err.to_string()).into_app_error()
})
}
fn try_into_stream(
self,
) -> Result<
impl Stream<Item = Result<Bytes, crate::ServerFnError>> + Send + 'static,
crate::ServerFnError<CustErr>,
> {
) -> Result<impl Stream<Item = Result<Bytes, E>> + Send + 'static, E> {
Ok(stream::iter(self.into_body())
.ready_chunks(16)
.map(|chunk| Ok(Bytes::from(chunk))))

View File

@@ -1,4 +1,3 @@
use crate::error::ServerFnError;
use bytes::Bytes;
use futures::Stream;
use std::{borrow::Cow, future::Future};
@@ -19,7 +18,7 @@ pub mod generic;
pub mod reqwest;
/// Represents a request as made by the client.
pub trait ClientReq<CustErr>
pub trait ClientReq<E>
where
Self: Sized,
{
@@ -32,7 +31,7 @@ where
content_type: &str,
accepts: &str,
query: &str,
) -> Result<Self, ServerFnError<CustErr>>;
) -> Result<Self, E>;
/// Attempts to construct a new `POST` request with a text body.
fn try_new_post(
@@ -40,7 +39,7 @@ where
content_type: &str,
accepts: &str,
body: String,
) -> Result<Self, ServerFnError<CustErr>>;
) -> Result<Self, E>;
/// Attempts to construct a new `POST` request with a binary body.
fn try_new_post_bytes(
@@ -48,7 +47,7 @@ where
content_type: &str,
accepts: &str,
body: Bytes,
) -> Result<Self, ServerFnError<CustErr>>;
) -> Result<Self, E>;
/// Attempts to construct a new `POST` request with form data as the body.
fn try_new_post_form_data(
@@ -56,14 +55,14 @@ where
accepts: &str,
content_type: &str,
body: Self::FormData,
) -> Result<Self, ServerFnError<CustErr>>;
) -> Result<Self, E>;
/// Attempts to construct a new `POST` request with a multipart body.
fn try_new_multipart(
path: &str,
accepts: &str,
body: Self::FormData,
) -> Result<Self, ServerFnError<CustErr>>;
) -> Result<Self, E>;
/// Attempts to construct a new `POST` request with a streaming body.
fn try_new_streaming(
@@ -71,11 +70,11 @@ where
accepts: &str,
content_type: &str,
body: impl Stream<Item = Bytes> + Send + 'static,
) -> Result<Self, ServerFnError<CustErr>>;
) -> Result<Self, E>;
}
/// Represents the request as received by the server.
pub trait Req<CustErr>
pub trait Req<E>
where
Self: Sized,
{
@@ -92,32 +91,22 @@ where
fn referer(&self) -> Option<Cow<'_, str>>;
/// Attempts to extract the body of the request into [`Bytes`].
fn try_into_bytes(
self,
) -> impl Future<Output = Result<Bytes, ServerFnError<CustErr>>> + Send;
fn try_into_bytes(self) -> impl Future<Output = Result<Bytes, E>> + Send;
/// Attempts to convert the body of the request into a string.
fn try_into_string(
self,
) -> impl Future<Output = Result<String, ServerFnError<CustErr>>> + Send;
fn try_into_string(self) -> impl Future<Output = Result<String, E>> + Send;
/// Attempts to convert the body of the request into a stream of bytes.
fn try_into_stream(
self,
) -> Result<
impl Stream<Item = Result<Bytes, ServerFnError>> + Send + 'static,
ServerFnError<CustErr>,
>;
) -> Result<impl Stream<Item = Result<Bytes, E>> + Send + 'static, E>;
}
/// A mocked request type that can be used in place of the actual server request,
/// when compiling for the browser.
pub struct BrowserMockReq;
impl<CustErr> Req<CustErr> for BrowserMockReq
where
CustErr: 'static,
{
impl<E: 'static> Req<E> for BrowserMockReq {
fn as_query(&self) -> Option<&str> {
unreachable!()
}
@@ -133,20 +122,17 @@ where
fn referer(&self) -> Option<Cow<'_, str>> {
unreachable!()
}
async fn try_into_bytes(self) -> Result<Bytes, ServerFnError<CustErr>> {
async fn try_into_bytes(self) -> Result<Bytes, E> {
unreachable!()
}
async fn try_into_string(self) -> Result<String, ServerFnError<CustErr>> {
async fn try_into_string(self) -> Result<String, E> {
unreachable!()
}
fn try_into_stream(
self,
) -> Result<
impl Stream<Item = Result<Bytes, ServerFnError>> + Send,
ServerFnError<CustErr>,
> {
) -> Result<impl Stream<Item = Result<Bytes, E>> + Send, E> {
Ok(futures::stream::once(async { unreachable!() }))
}
}

View File

@@ -1,5 +1,8 @@
use super::ClientReq;
use crate::{client::get_server_url, error::ServerFnError};
use crate::{
client::get_server_url,
error::{FromServerFnError, IntoAppError, ServerFnErrorErr},
};
use bytes::Bytes;
use futures::Stream;
use once_cell::sync::Lazy;
@@ -8,7 +11,10 @@ pub use reqwest::{multipart::Form, Client, Method, Request, Url};
pub(crate) static CLIENT: Lazy<Client> = Lazy::new(Client::new);
impl<CustErr> ClientReq<CustErr> for Request {
impl<E> ClientReq<E> for Request
where
E: FromServerFnError,
{
type FormData = Form;
fn try_new_get(
@@ -16,17 +22,22 @@ impl<CustErr> ClientReq<CustErr> for Request {
accepts: &str,
content_type: &str,
query: &str,
) -> Result<Self, ServerFnError<CustErr>> {
) -> Result<Self, E> {
let url = format!("{}{}", get_server_url(), path);
let mut url = Url::try_from(url.as_str())
.map_err(|e| ServerFnError::Request(e.to_string()))?;
let mut url = Url::try_from(url.as_str()).map_err(|e| {
E::from_server_fn_error(ServerFnErrorErr::Request(e.to_string()))
})?;
url.set_query(Some(query));
let req = CLIENT
.get(url)
.header(CONTENT_TYPE, content_type)
.header(ACCEPT, accepts)
.build()
.map_err(|e| ServerFnError::Request(e.to_string()))?;
.map_err(|e| {
E::from_server_fn_error(ServerFnErrorErr::Request(
e.to_string(),
))
})?;
Ok(req)
}
@@ -35,7 +46,7 @@ impl<CustErr> ClientReq<CustErr> for Request {
accepts: &str,
content_type: &str,
body: String,
) -> Result<Self, ServerFnError<CustErr>> {
) -> Result<Self, E> {
let url = format!("{}{}", get_server_url(), path);
CLIENT
.post(url)
@@ -43,7 +54,9 @@ impl<CustErr> ClientReq<CustErr> for Request {
.header(ACCEPT, accepts)
.body(body)
.build()
.map_err(|e| ServerFnError::Request(e.to_string()))
.map_err(|e| {
ServerFnErrorErr::Request(e.to_string()).into_app_error()
})
}
fn try_new_post_bytes(
@@ -51,7 +64,7 @@ impl<CustErr> ClientReq<CustErr> for Request {
accepts: &str,
content_type: &str,
body: Bytes,
) -> Result<Self, ServerFnError<CustErr>> {
) -> Result<Self, E> {
let url = format!("{}{}", get_server_url(), path);
CLIENT
.post(url)
@@ -59,20 +72,24 @@ impl<CustErr> ClientReq<CustErr> for Request {
.header(ACCEPT, accepts)
.body(body)
.build()
.map_err(|e| ServerFnError::Request(e.to_string()))
.map_err(|e| {
ServerFnErrorErr::Request(e.to_string()).into_app_error()
})
}
fn try_new_multipart(
path: &str,
accepts: &str,
body: Self::FormData,
) -> Result<Self, ServerFnError<CustErr>> {
) -> Result<Self, E> {
CLIENT
.post(path)
.header(ACCEPT, accepts)
.multipart(body)
.build()
.map_err(|e| ServerFnError::Request(e.to_string()))
.map_err(|e| {
ServerFnErrorErr::Request(e.to_string()).into_app_error()
})
}
fn try_new_post_form_data(
@@ -80,14 +97,16 @@ impl<CustErr> ClientReq<CustErr> for Request {
accepts: &str,
content_type: &str,
body: Self::FormData,
) -> Result<Self, ServerFnError<CustErr>> {
) -> Result<Self, E> {
CLIENT
.post(path)
.header(CONTENT_TYPE, content_type)
.header(ACCEPT, accepts)
.multipart(body)
.build()
.map_err(|e| ServerFnError::Request(e.to_string()))
.map_err(|e| {
ServerFnErrorErr::Request(e.to_string()).into_app_error()
})
}
fn try_new_streaming(
@@ -95,7 +114,7 @@ impl<CustErr> ClientReq<CustErr> for Request {
_accepts: &str,
_content_type: &str,
_body: impl Stream<Item = Bytes> + 'static,
) -> Result<Self, ServerFnError<CustErr>> {
) -> Result<Self, E> {
todo!("Streaming requests are not yet implemented for reqwest.")
// We run into a fundamental issue here.
// To be a reqwest body, the type must be Sync
@@ -112,7 +131,7 @@ impl<CustErr> ClientReq<CustErr> for Request {
.header(ACCEPT, accepts)
.body(body)
.build()
.map_err(|e| ServerFnError::Request(e.to_string()))
.map_err(|e| ServerFnErrorErr::Request(e.to_string()).into())
}*/
}
}

View File

@@ -8,7 +8,7 @@ use http::{
use http_body_util::BodyExt;
use std::borrow::Cow;
impl<CustErr> Req<CustErr> for IncomingRequest
impl<E> Req<E> for IncomingRequest
where
CustErr: 'static,
{
@@ -34,29 +34,31 @@ where
.map(|h| String::from_utf8_lossy(h.as_bytes()))
}
async fn try_into_bytes(self) -> Result<Bytes, ServerFnError<CustErr>> {
async fn try_into_bytes(self) -> Result<Bytes, E> {
let (_parts, body) = self.into_parts();
body.collect()
.await
.map(|c| c.to_bytes())
.map_err(|e| ServerFnError::Deserialization(e.to_string()))
body.collect().await.map(|c| c.to_bytes()).map_err(|e| {
ServerFnErrorErr::Deserialization(e.to_string()).into()
})
}
async fn try_into_string(self) -> Result<String, ServerFnError<CustErr>> {
async fn try_into_string(self) -> Result<String, E> {
let bytes = self.try_into_bytes().await?;
String::from_utf8(bytes.to_vec())
.map_err(|e| ServerFnError::Deserialization(e.to_string()))
String::from_utf8(bytes.to_vec()).map_err(|e| {
ServerFnErrorErr::Deserialization(e.to_string()).into()
})
}
fn try_into_stream(
self,
) -> Result<
impl Stream<Item = Result<Bytes, ServerFnError>> + Send + 'static,
ServerFnError<CustErr>,
E,
> {
Ok(self.into_body().into_data_stream().map(|chunk| {
chunk.map_err(|e| ServerFnError::Deserialization(e.to_string()))
chunk.map_err(|e| {
ServerFnErrorErr::Deserialization(e.to_string()).into()
})
}))
}
}

View File

@@ -1,6 +1,6 @@
use super::Res;
use super::{Res, TryRes};
use crate::error::{
ServerFnError, ServerFnErrorErr, ServerFnErrorSerde, SERVER_FN_ERROR_HEADER,
FromServerFnError, ServerFnErrorWrapper, SERVER_FN_ERROR_HEADER,
};
use actix_web::{
http::{
@@ -13,10 +13,6 @@ use actix_web::{
use bytes::Bytes;
use futures::{Stream, StreamExt};
use send_wrapper::SendWrapper;
use std::{
fmt::{Debug, Display},
str::FromStr,
};
/// A wrapped Actix response.
///
@@ -38,14 +34,11 @@ impl From<HttpResponse> for ActixResponse {
}
}
impl<CustErr> Res<CustErr> for ActixResponse
impl<E> TryRes<E> for ActixResponse
where
CustErr: FromStr + Display + Debug + 'static,
E: FromServerFnError,
{
fn try_from_string(
content_type: &str,
data: String,
) -> Result<Self, ServerFnError<CustErr>> {
fn try_from_string(content_type: &str, data: String) -> Result<Self, E> {
let mut builder = HttpResponse::build(StatusCode::OK);
Ok(ActixResponse(SendWrapper::new(
builder
@@ -54,10 +47,7 @@ where
)))
}
fn try_from_bytes(
content_type: &str,
data: Bytes,
) -> Result<Self, ServerFnError<CustErr>> {
fn try_from_bytes(content_type: &str, data: Bytes) -> Result<Self, E> {
let mut builder = HttpResponse::build(StatusCode::OK);
Ok(ActixResponse(SendWrapper::new(
builder
@@ -68,23 +58,23 @@ where
fn try_from_stream(
content_type: &str,
data: impl Stream<Item = Result<Bytes, ServerFnError<CustErr>>> + 'static,
) -> Result<Self, ServerFnError<CustErr>> {
data: impl Stream<Item = Result<Bytes, E>> + 'static,
) -> Result<Self, E> {
let mut builder = HttpResponse::build(StatusCode::OK);
Ok(ActixResponse(SendWrapper::new(
builder
.insert_header((header::CONTENT_TYPE, content_type))
.streaming(
data.map(|data| data.map_err(ServerFnErrorErr::from)),
),
.streaming(data.map(|data| data.map_err(ServerFnErrorWrapper))),
)))
}
}
fn error_response(path: &str, err: &ServerFnError<CustErr>) -> Self {
impl Res for ActixResponse {
fn error_response(path: &str, err: String) -> Self {
ActixResponse(SendWrapper::new(
HttpResponse::build(StatusCode::INTERNAL_SERVER_ERROR)
.append_header((SERVER_FN_ERROR_HEADER, path))
.body(err.ser().unwrap_or_else(|_| err.to_string())),
.body(err),
))
}

View File

@@ -1,5 +1,8 @@
use super::ClientRes;
use crate::{error::ServerFnError, redirect::REDIRECT_HEADER};
use crate::{
error::{FromServerFnError, IntoAppError, ServerFnErrorErr},
redirect::REDIRECT_HEADER,
};
use bytes::Bytes;
use futures::{Stream, StreamExt};
pub use gloo_net::http::Response;
@@ -12,48 +15,39 @@ use wasm_streams::ReadableStream;
/// The response to a `fetch` request made in the browser.
pub struct BrowserResponse(pub(crate) SendWrapper<Response>);
impl<CustErr> ClientRes<CustErr> for BrowserResponse {
fn try_into_string(
self,
) -> impl Future<Output = Result<String, ServerFnError<CustErr>>> + Send
{
impl<E: FromServerFnError> ClientRes<E> for BrowserResponse {
fn try_into_string(self) -> impl Future<Output = Result<String, E>> + Send {
// the browser won't send this async work between threads (because it's single-threaded)
// so we can safely wrap this
SendWrapper::new(async move {
self.0
.text()
.await
.map_err(|e| ServerFnError::Deserialization(e.to_string()))
self.0.text().await.map_err(|e| {
ServerFnErrorErr::Deserialization(e.to_string())
.into_app_error()
})
})
}
fn try_into_bytes(
self,
) -> impl Future<Output = Result<Bytes, ServerFnError<CustErr>>> + Send
{
fn try_into_bytes(self) -> impl Future<Output = Result<Bytes, E>> + Send {
// the browser won't send this async work between threads (because it's single-threaded)
// so we can safely wrap this
SendWrapper::new(async move {
self.0
.binary()
.await
.map(Bytes::from)
.map_err(|e| ServerFnError::Deserialization(e.to_string()))
self.0.binary().await.map(Bytes::from).map_err(|e| {
ServerFnErrorErr::Deserialization(e.to_string())
.into_app_error()
})
})
}
fn try_into_stream(
self,
) -> Result<
impl Stream<Item = Result<Bytes, ServerFnError>> + Send + 'static,
ServerFnError<CustErr>,
> {
) -> Result<impl Stream<Item = Result<Bytes, E>> + Send + 'static, E> {
let stream = ReadableStream::from_raw(self.0.body().unwrap())
.into_stream()
.map(|data| match data {
Err(e) => {
web_sys::console::error_1(&e);
Err(ServerFnError::Request(format!("{e:?}")))
Err(ServerFnErrorErr::Request(format!("{e:?}"))
.into_app_error())
}
Ok(data) => {
let data = data.unchecked_into::<Uint8Array>();

View File

@@ -12,18 +12,15 @@
//! * `wasm32-wasip*` integration crate `leptos_wasi` is using this
//! crate under the hood.
use super::Res;
use super::{Res, TryRes};
use crate::error::{
ServerFnError, ServerFnErrorErr, ServerFnErrorSerde, SERVER_FN_ERROR_HEADER,
FromServerFnError, IntoAppError, ServerFnErrorErr, ServerFnErrorWrapper,
SERVER_FN_ERROR_HEADER,
};
use bytes::Bytes;
use futures::{Stream, TryStreamExt};
use http::{header, HeaderValue, Response, StatusCode};
use std::{
fmt::{Debug, Display},
pin::Pin,
str::FromStr,
};
use std::pin::Pin;
use throw_error::Error;
/// The Body of a Response whose *execution model* can be
@@ -44,55 +41,55 @@ impl From<String> for Body {
}
}
impl<CustErr> Res<CustErr> for Response<Body>
impl<E> TryRes<E> for Response<Body>
where
CustErr: Send + Sync + Debug + FromStr + Display + 'static,
E: Send + Sync + FromServerFnError,
{
fn try_from_string(
content_type: &str,
data: String,
) -> Result<Self, ServerFnError<CustErr>> {
fn try_from_string(content_type: &str, data: String) -> Result<Self, E> {
let builder = http::Response::builder();
builder
.status(200)
.header(http::header::CONTENT_TYPE, content_type)
.body(data.into())
.map_err(|e| ServerFnError::Response(e.to_string()))
.map_err(|e| {
ServerFnErrorErr::Response(e.to_string()).into_app_error()
})
}
fn try_from_bytes(
content_type: &str,
data: Bytes,
) -> Result<Self, ServerFnError<CustErr>> {
fn try_from_bytes(content_type: &str, data: Bytes) -> Result<Self, E> {
let builder = http::Response::builder();
builder
.status(200)
.header(http::header::CONTENT_TYPE, content_type)
.body(Body::Sync(data))
.map_err(|e| ServerFnError::Response(e.to_string()))
.map_err(|e| {
ServerFnErrorErr::Response(e.to_string()).into_app_error()
})
}
fn try_from_stream(
content_type: &str,
data: impl Stream<Item = Result<Bytes, ServerFnError<CustErr>>>
+ Send
+ 'static,
) -> Result<Self, ServerFnError<CustErr>> {
data: impl Stream<Item = Result<Bytes, E>> + Send + 'static,
) -> Result<Self, E> {
let builder = http::Response::builder();
builder
.status(200)
.header(http::header::CONTENT_TYPE, content_type)
.body(Body::Async(Box::pin(
data.map_err(ServerFnErrorErr::from).map_err(Error::from),
data.map_err(ServerFnErrorWrapper).map_err(Error::from),
)))
.map_err(|e| ServerFnError::Response(e.to_string()))
.map_err(|e| {
ServerFnErrorErr::Response(e.to_string()).into_app_error()
})
}
}
fn error_response(path: &str, err: &ServerFnError<CustErr>) -> Self {
impl Res for Response<Body> {
fn error_response(path: &str, err: String) -> Self {
Response::builder()
.status(http::StatusCode::INTERNAL_SERVER_ERROR)
.header(SERVER_FN_ERROR_HEADER, path)
.body(err.ser().unwrap_or_else(|_| err.to_string()).into())
.body(err.into())
.unwrap()
}

View File

@@ -1,65 +1,61 @@
use super::Res;
use super::{Res, TryRes};
use crate::error::{
ServerFnError, ServerFnErrorErr, ServerFnErrorSerde, SERVER_FN_ERROR_HEADER,
FromServerFnError, IntoAppError, ServerFnErrorErr, ServerFnErrorWrapper,
SERVER_FN_ERROR_HEADER,
};
use axum::body::Body;
use bytes::Bytes;
use futures::{Stream, StreamExt};
use futures::{Stream, TryStreamExt};
use http::{header, HeaderValue, Response, StatusCode};
use std::{
fmt::{Debug, Display},
str::FromStr,
};
impl<CustErr> Res<CustErr> for Response<Body>
impl<E> TryRes<E> for Response<Body>
where
CustErr: Send + Sync + Debug + FromStr + Display + 'static,
E: Send + Sync + FromServerFnError,
{
fn try_from_string(
content_type: &str,
data: String,
) -> Result<Self, ServerFnError<CustErr>> {
fn try_from_string(content_type: &str, data: String) -> Result<Self, E> {
let builder = http::Response::builder();
builder
.status(200)
.header(http::header::CONTENT_TYPE, content_type)
.body(Body::from(data))
.map_err(|e| ServerFnError::Response(e.to_string()))
.map_err(|e| {
ServerFnErrorErr::Response(e.to_string()).into_app_error()
})
}
fn try_from_bytes(
content_type: &str,
data: Bytes,
) -> Result<Self, ServerFnError<CustErr>> {
fn try_from_bytes(content_type: &str, data: Bytes) -> Result<Self, E> {
let builder = http::Response::builder();
builder
.status(200)
.header(http::header::CONTENT_TYPE, content_type)
.body(Body::from(data))
.map_err(|e| ServerFnError::Response(e.to_string()))
.map_err(|e| {
ServerFnErrorErr::Response(e.to_string()).into_app_error()
})
}
fn try_from_stream(
content_type: &str,
data: impl Stream<Item = Result<Bytes, ServerFnError<CustErr>>>
+ Send
+ 'static,
) -> Result<Self, ServerFnError<CustErr>> {
let body =
Body::from_stream(data.map(|n| n.map_err(ServerFnErrorErr::from)));
data: impl Stream<Item = Result<Bytes, E>> + Send + 'static,
) -> Result<Self, E> {
let body = Body::from_stream(data.map_err(|e| ServerFnErrorWrapper(e)));
let builder = http::Response::builder();
builder
.status(200)
.header(http::header::CONTENT_TYPE, content_type)
.body(body)
.map_err(|e| ServerFnError::Response(e.to_string()))
.map_err(|e| {
ServerFnErrorErr::Response(e.to_string()).into_app_error()
})
}
}
fn error_response(path: &str, err: &ServerFnError<CustErr>) -> Self {
impl Res for Response<Body> {
fn error_response(path: &str, err: String) -> Self {
Response::builder()
.status(http::StatusCode::INTERNAL_SERVER_ERROR)
.header(SERVER_FN_ERROR_HEADER, path)
.body(err.ser().unwrap_or_else(|_| err.to_string()).into())
.body(err.into())
.unwrap()
}

View File

@@ -13,62 +13,49 @@ pub mod http;
#[cfg(feature = "reqwest")]
pub mod reqwest;
use crate::error::ServerFnError;
use bytes::Bytes;
use futures::Stream;
use std::future::Future;
/// Represents the response as created by the server;
pub trait Res<CustErr>
pub trait TryRes<E>
where
Self: Sized,
{
/// Attempts to convert a UTF-8 string into an HTTP response.
fn try_from_string(
content_type: &str,
data: String,
) -> Result<Self, ServerFnError<CustErr>>;
fn try_from_string(content_type: &str, data: String) -> Result<Self, E>;
/// Attempts to convert a binary blob represented as bytes into an HTTP response.
fn try_from_bytes(
content_type: &str,
data: Bytes,
) -> Result<Self, ServerFnError<CustErr>>;
fn try_from_bytes(content_type: &str, data: Bytes) -> Result<Self, E>;
/// Attempts to convert a stream of bytes into an HTTP response.
fn try_from_stream(
content_type: &str,
data: impl Stream<Item = Result<Bytes, ServerFnError<CustErr>>>
+ Send
+ 'static,
) -> Result<Self, ServerFnError<CustErr>>;
data: impl Stream<Item = Result<Bytes, E>> + Send + 'static,
) -> Result<Self, E>;
}
/// Represents the response as created by the server;
pub trait Res {
/// Converts an error into a response, with a `500` status code and the error text as its body.
fn error_response(path: &str, err: &ServerFnError<CustErr>) -> Self;
fn error_response(path: &str, err: String) -> Self;
/// Redirect the response by setting a 302 code and Location header.
fn redirect(&mut self, path: &str);
}
/// Represents the response as received by the client.
pub trait ClientRes<CustErr> {
pub trait ClientRes<E> {
/// Attempts to extract a UTF-8 string from an HTTP response.
fn try_into_string(
self,
) -> impl Future<Output = Result<String, ServerFnError<CustErr>>> + Send;
fn try_into_string(self) -> impl Future<Output = Result<String, E>> + Send;
/// Attempts to extract a binary blob from an HTTP response.
fn try_into_bytes(
self,
) -> impl Future<Output = Result<Bytes, ServerFnError<CustErr>>> + Send;
fn try_into_bytes(self) -> impl Future<Output = Result<Bytes, E>> + Send;
/// Attempts to extract a binary stream from an HTTP response.
fn try_into_stream(
self,
) -> Result<
impl Stream<Item = Result<Bytes, ServerFnError>> + Send + Sync + 'static,
ServerFnError<CustErr>,
>;
) -> Result<impl Stream<Item = Result<Bytes, E>> + Send + Sync + 'static, E>;
/// HTTP status code of the response.
fn status(&self) -> u16;
@@ -91,29 +78,25 @@ pub trait ClientRes<CustErr> {
/// server response type when compiling for the client.
pub struct BrowserMockRes;
impl<CustErr> Res<CustErr> for BrowserMockRes {
fn try_from_string(
_content_type: &str,
_data: String,
) -> Result<Self, ServerFnError<CustErr>> {
impl<E> TryRes<E> for BrowserMockRes {
fn try_from_string(_content_type: &str, _data: String) -> Result<Self, E> {
unreachable!()
}
fn try_from_bytes(
_content_type: &str,
_data: Bytes,
) -> Result<Self, ServerFnError<CustErr>> {
unreachable!()
}
fn error_response(_path: &str, _err: &ServerFnError<CustErr>) -> Self {
fn try_from_bytes(_content_type: &str, _data: Bytes) -> Result<Self, E> {
unreachable!()
}
fn try_from_stream(
_content_type: &str,
_data: impl Stream<Item = Result<Bytes, ServerFnError<CustErr>>>,
) -> Result<Self, ServerFnError<CustErr>> {
_data: impl Stream<Item = Result<Bytes, E>>,
) -> Result<Self, E> {
unreachable!()
}
}
impl Res for BrowserMockRes {
fn error_response(_path: &str, _err: String) -> Self {
unreachable!()
}

View File

@@ -1,31 +1,28 @@
use super::ClientRes;
use crate::error::ServerFnError;
use crate::error::{FromServerFnError, IntoAppError, ServerFnErrorErr};
use bytes::Bytes;
use futures::{Stream, TryStreamExt};
use reqwest::Response;
impl<CustErr> ClientRes<CustErr> for Response {
async fn try_into_string(self) -> Result<String, ServerFnError<CustErr>> {
self.text()
.await
.map_err(|e| ServerFnError::Deserialization(e.to_string()))
impl<E: FromServerFnError> ClientRes<E> for Response {
async fn try_into_string(self) -> Result<String, E> {
self.text().await.map_err(|e| {
ServerFnErrorErr::Deserialization(e.to_string()).into_app_error()
})
}
async fn try_into_bytes(self) -> Result<Bytes, ServerFnError<CustErr>> {
self.bytes()
.await
.map_err(|e| ServerFnError::Deserialization(e.to_string()))
async fn try_into_bytes(self) -> Result<Bytes, E> {
self.bytes().await.map_err(|e| {
ServerFnErrorErr::Deserialization(e.to_string()).into_app_error()
})
}
fn try_into_stream(
self,
) -> Result<
impl Stream<Item = Result<Bytes, ServerFnError>> + Send + 'static,
ServerFnError<CustErr>,
> {
Ok(self
.bytes_stream()
.map_err(|e| ServerFnError::Response(e.to_string())))
) -> Result<impl Stream<Item = Result<Bytes, E>> + Send + 'static, E> {
Ok(self.bytes_stream().map_err(|e| {
ServerFnErrorErr::Response(e.to_string()).into_app_error()
}))
}
fn status(&self) -> u16 {

View File

@@ -230,6 +230,7 @@ pub fn server_macro_impl(
None => Some("PostUrl".to_string()),
_ => None,
};
let input = input
.map(|n| {
if builtin_encoding {
@@ -382,13 +383,8 @@ pub fn server_macro_impl(
quote! {
#server_fn_path::inventory::submit! {{
use #server_fn_path::{ServerFn, codec::Encoding};
#server_fn_path::ServerFnTraitObj::new(
#wrapped_struct_name_turbofish::PATH,
<#wrapped_struct_name as ServerFn>::InputEncoding::METHOD,
|req| {
Box::pin(#wrapped_struct_name_turbofish::run_on_server(req))
},
#wrapped_struct_name_turbofish::middlewares
#server_fn_path::ServerFnTraitObj::new::<#wrapped_struct_name>(
|req| Box::pin(#wrapped_struct_name_turbofish::run_on_server(req)),
)
}}
}
@@ -609,20 +605,44 @@ pub fn server_macro_impl(
} else {
quote! { concat!("/", #fn_path) }
};
let enable_server_fn_mod_path = option_env!("SERVER_FN_MOD_PATH").is_some();
let mod_path = if enable_server_fn_mod_path {
quote! {
#server_fn_path::const_format::concatcp!(
#server_fn_path::const_str::replace!(module_path!(), "::", "/"),
"/"
)
}
} else {
quote! { "" }
};
let enable_hash = option_env!("DISABLE_SERVER_FN_HASH").is_none();
let hash = if enable_hash {
quote! {
#server_fn_path::xxhash_rust::const_xxh64::xxh64(
concat!(env!(#key_env_var), ":", file!(), ":", line!(), ":", column!()).as_bytes(),
0
)
}
} else {
quote! { "" }
};
let path = quote! {
if #fn_path.is_empty() {
#server_fn_path::const_format::concatcp!(
#prefix,
"/",
#mod_path,
#fn_name_as_str,
#server_fn_path::xxhash_rust::const_xxh64::xxh64(
concat!(env!(#key_env_var), ":", file!(), ":", line!(), ":", column!()).as_bytes(),
0
)
#hash
)
} else {
#server_fn_path::const_format::concatcp!(
#prefix,
#mod_path,
#fn_path
)
}
@@ -730,12 +750,12 @@ fn output_type(return_ty: &Type) -> Result<&GenericArgument> {
Err(syn::Error::new(
return_ty.span(),
"server functions should return Result<T, ServerFnError> or Result<T, \
ServerFnError<E>>",
"server functions should return Result<T, E> where E: \
FromServerFnError",
))
}
fn err_type(return_ty: &Type) -> Result<Option<&GenericArgument>> {
fn err_type(return_ty: &Type) -> Result<Option<&Type>> {
if let syn::Type::Path(pat) = &return_ty {
if pat.path.segments[0].ident == "Result" {
if let PathArguments::AngleBracketed(args) =
@@ -746,25 +766,8 @@ fn err_type(return_ty: &Type) -> Result<Option<&GenericArgument>> {
return Ok(None);
}
// Result<T, _>
else if let GenericArgument::Type(Type::Path(pat)) =
&args.args[1]
{
if let Some(segment) = pat.path.segments.last() {
if segment.ident == "ServerFnError" {
let args = &segment.arguments;
match args {
// Result<T, ServerFnError>
PathArguments::None => return Ok(None),
// Result<T, ServerFnError<E>>
PathArguments::AngleBracketed(args) => {
if args.args.len() == 1 {
return Ok(Some(&args.args[0]));
}
}
_ => {}
}
}
}
else if let GenericArgument::Type(ty) = &args.args[1] {
return Ok(Some(ty));
}
}
}
@@ -772,8 +775,8 @@ fn err_type(return_ty: &Type) -> Result<Option<&GenericArgument>> {
Err(syn::Error::new(
return_ty.span(),
"server functions should return Result<T, ServerFnError> or Result<T, \
ServerFnError<E>>",
"server functions should return Result<T, E> where E: \
FromServerFnError",
))
}

View File

@@ -1,3 +1,4 @@
#![allow(unused_mut)]
use super::{Attribute, NextAttribute};
use dyn_clone::DynClone;
use std::{
@@ -35,6 +36,10 @@ pub struct AnyAttribute {
type_id: TypeId,
html_len: usize,
value: Box<dyn DynAttr>,
/// Temporary attribute set during the resolving cycle, to resolve only once.
pub(crate) resolved: bool,
#[cfg(feature = "ssr")]
to_html: fn(
Box<dyn DynAttr>,
@@ -197,6 +202,7 @@ where
type_id: TypeId::of::<T::CloneableOwned>(),
html_len: value.html_len(),
value,
resolved: false,
#[cfg(feature = "ssr")]
to_html,
build,
@@ -303,10 +309,13 @@ impl Attribute for AnyAttribute {
);
}
async fn resolve(self) -> Self::AsyncOutput {
async fn resolve(mut self) -> Self::AsyncOutput {
#[cfg(feature = "ssr")]
{
(self.resolve)(self.value).await
let res = (self.resolve)(self.value).await;
// Used by batch_resolve_items_with_extra_attrs() for optimisations.
self.resolved = true;
res
}
#[cfg(not(feature = "ssr"))]
panic!(

View File

@@ -24,7 +24,7 @@ use web_sys::Element;
/// let view = element.on(ev::click, move |_| /* ... */);
///
/// // `element` now contains the actual element
/// let element = element.build();
/// let element = element.build(None);
/// let remove = element.on(ev::blur, move |_| /* ... */);
/// ```
pub trait ElementExt {

View File

@@ -6,14 +6,14 @@ use crate::{
renderer::{CastFrom, Rndr},
ssr::StreamBuilder,
view::{
add_attr::AddAnyAttr, IntoRender, Mountable, Position, PositionState,
Render, RenderHtml, ToTemplate,
add_attr::AddAnyAttr, any_view::ExtraAttrsMut, IntoRender, Mountable,
Position, PositionState, Render, RenderHtml, ToTemplate,
},
};
use const_str_slice_concat::{
const_concat, const_concat_with_prefix, str_from_buffer,
};
use futures::future::join;
use futures::future::join3;
use next_tuple::NextTuple;
use std::ops::Deref;
@@ -21,7 +21,10 @@ mod custom;
mod element_ext;
mod elements;
mod inner_html;
use super::attribute::{escape_attr, NextAttribute};
use super::attribute::{
any_attribute::{AnyAttribute, AnyAttributeState},
escape_attr, NextAttribute,
};
pub use custom::*;
pub use element_ext::*;
pub use elements::*;
@@ -183,31 +186,42 @@ where
{
type State = ElementState<At::State, Ch::State>;
fn rebuild(self, state: &mut Self::State) {
fn rebuild(
self,
state: &mut Self::State,
extra_attrs: Option<Vec<AnyAttribute>>,
) {
let ElementState {
attrs, children, ..
} = state;
self.attributes.rebuild(attrs);
if let (Some(extra_attrs), Some(extra_attr_states)) =
(extra_attrs, &mut state.extra_attrs)
{
extra_attrs.rebuild(extra_attr_states);
}
if let Some(children) = children {
self.children.rebuild(children);
self.children.rebuild(children, None);
}
}
fn build(self) -> Self::State {
fn build(self, extra_attrs: Option<Vec<AnyAttribute>>) -> Self::State {
let el = Rndr::create_element(self.tag.tag(), E::NAMESPACE);
let attrs = self.attributes.build(&el);
let extra_attrs = extra_attrs.map(|attrs| attrs.build(&el));
let children = if E::SELF_CLOSING {
None
} else {
let mut children = self.children.build();
let mut children = self.children.build(None);
children.mount(&el, None);
Some(children)
};
ElementState {
el,
attrs,
children,
attrs,
extra_attrs,
}
}
}
@@ -219,6 +233,7 @@ where
Ch: RenderHtml + Send,
{
type AsyncOutput = HtmlElement<E, At::AsyncOutput, Ch::AsyncOutput>;
type Owned = HtmlElement<E, At::CloneableOwned, Ch::Owned>;
const MIN_LENGTH: usize = if E::SELF_CLOSING {
3 // < ... />
@@ -233,14 +248,22 @@ where
+ E::TAG.len()
};
fn dry_resolve(&mut self) {
fn dry_resolve(&mut self, mut extra_attrs: ExtraAttrsMut<'_>) {
self.attributes.dry_resolve();
self.children.dry_resolve();
extra_attrs.iter_mut().for_each(Attribute::dry_resolve);
self.children.dry_resolve(ExtraAttrsMut::default());
}
async fn resolve(self) -> Self::AsyncOutput {
let (attributes, children) =
join(self.attributes.resolve(), self.children.resolve()).await;
async fn resolve(
self,
extra_attrs: ExtraAttrsMut<'_>,
) -> Self::AsyncOutput {
let (attributes, _, children) = join3(
self.attributes.resolve(),
ExtraAttrsMut::resolve(extra_attrs),
self.children.resolve(ExtraAttrsMut::default()),
)
.await;
HtmlElement {
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: self.defined_at,
@@ -250,7 +273,7 @@ where
}
}
fn html_len(&self) -> usize {
fn html_len(&self, extra_attrs: Option<Vec<&AnyAttribute>>) -> usize {
if E::SELF_CLOSING {
3 // < ... />
+ E::TAG.len()
@@ -259,7 +282,10 @@ where
2 // < ... >
+ E::TAG.len()
+ self.attributes.html_len()
+ self.children.html_len()
+ extra_attrs.map(|attrs| {
attrs.into_iter().map(Attribute::html_len).sum::<usize>()
}).unwrap_or(0)
+ self.children.html_len(None)
+ 3 // </ ... >
+ E::TAG.len()
}
@@ -271,12 +297,13 @@ where
position: &mut Position,
_escape: bool,
mark_branches: bool,
extra_attrs: Option<Vec<AnyAttribute>>,
) {
// opening tag
buf.push('<');
buf.push_str(self.tag.tag());
let inner_html = attributes_to_html(self.attributes, buf);
let inner_html = attributes_to_html(self.attributes, extra_attrs, buf);
buf.push('>');
@@ -291,6 +318,7 @@ where
position,
E::ESCAPE_CHILDREN,
mark_branches,
None,
);
}
@@ -308,6 +336,7 @@ where
position: &mut Position,
_escape: bool,
mark_branches: bool,
extra_attrs: Option<Vec<AnyAttribute>>,
) where
Self: Sized,
{
@@ -316,7 +345,8 @@ where
buf.push('<');
buf.push_str(self.tag.tag());
let inner_html = attributes_to_html(self.attributes, &mut buf);
let inner_html =
attributes_to_html(self.attributes, extra_attrs, &mut buf);
buf.push('>');
buffer.push_sync(&buf);
@@ -332,6 +362,7 @@ where
position,
E::ESCAPE_CHILDREN,
mark_branches,
None,
);
}
@@ -349,6 +380,7 @@ where
self,
cursor: &Cursor,
position: &PositionState,
extra_attrs: Option<Vec<AnyAttribute>>,
) -> Self::State {
#[cfg(any(debug_assertions, leptos_debuginfo))]
{
@@ -373,13 +405,15 @@ where
});
let attrs = self.attributes.hydrate::<FROM_SERVER>(&el);
let extra_attrs = extra_attrs
.map(|attrs| Attribute::hydrate::<FROM_SERVER>(attrs, &el));
// hydrate children
let children = if !Ch::EXISTS || !E::ESCAPE_CHILDREN {
None
} else {
position.set(Position::FirstChild);
Some(self.children.hydrate::<FROM_SERVER>(cursor, position))
Some(self.children.hydrate::<FROM_SERVER>(cursor, position, None))
};
// go to next sibling
@@ -393,14 +427,29 @@ where
ElementState {
el,
attrs,
children,
attrs,
extra_attrs,
}
}
fn into_owned(self) -> Self::Owned {
HtmlElement {
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: self.defined_at,
tag: self.tag,
attributes: self.attributes.into_cloneable_owned(),
children: self.children.into_owned(),
}
}
}
/// Renders an [`Attribute`] (which can be one or more HTML attributes) into an HTML buffer.
pub fn attributes_to_html<At>(attr: At, buf: &mut String) -> String
pub fn attributes_to_html<At>(
attr: At,
extra_attrs: Option<Vec<AnyAttribute>>,
buf: &mut String,
) -> String
where
At: Attribute,
{
@@ -419,6 +468,11 @@ where
// inject regular attributes, and fill class and style
attr.to_html(buf, &mut class, &mut style, &mut inner_html);
if let Some(extra_attrs) = extra_attrs {
for attr in extra_attrs {
attr.to_html(buf, &mut class, &mut style, &mut inner_html);
}
}
if !class.is_empty() {
buf.push(' ');
@@ -439,8 +493,10 @@ where
/// The retained view state for an HTML element.
pub struct ElementState<At, Ch> {
pub(crate) el: crate::renderer::types::Element,
pub(crate) attrs: At,
pub(crate) children: Option<Ch>,
attrs: At,
extra_attrs: Option<Vec<AnyAttributeState>>,
}
impl<At, Ch> Deref for ElementState<At, Ch> {
@@ -583,7 +639,7 @@ mod tests {
fn mock_dom_creates_element() {
let el: HtmlElement<Main, _, _, MockDom> =
main().child(p().id("test").lang("en").child("Hello, world!"));
let el = el.build();
let el = el.build(None);
assert_eq!(
el.el.to_debug_html(),
"<main><p id=\"test\" lang=\"en\">Hello, world!</p></main>"
@@ -597,7 +653,7 @@ mod tests {
em().child("beautiful"),
" world!",
)));
let el = el.build();
let el = el.build(None);
assert_eq!(
el.el.to_debug_html(),
"<main><p>Hello, <em>beautiful</em> world!</p></main>"

View File

@@ -1,9 +1,11 @@
use super::attribute::Attribute;
use super::attribute::{any_attribute::AnyAttribute, Attribute};
use crate::{
hydration::Cursor,
prelude::{Render, RenderHtml},
ssr::StreamBuilder,
view::{add_attr::AddAnyAttr, Position, PositionState},
view::{
add_attr::AddAnyAttr, any_view::ExtraAttrsMut, Position, PositionState,
},
};
/// An island of interactivity in an otherwise-inert HTML document.
@@ -59,12 +61,16 @@ where
{
type State = View::State;
fn build(self) -> Self::State {
self.view.build()
fn build(self, extra_attrs: Option<Vec<AnyAttribute>>) -> Self::State {
self.view.build(extra_attrs)
}
fn rebuild(self, state: &mut Self::State) {
self.view.rebuild(state);
fn rebuild(
self,
state: &mut Self::State,
extra_attrs: Option<Vec<AnyAttribute>>,
) {
self.view.rebuild(state, extra_attrs);
}
}
@@ -100,6 +106,7 @@ where
View: RenderHtml,
{
type AsyncOutput = Island<View::AsyncOutput>;
type Owned = Island<View::Owned>;
const MIN_LENGTH: usize = ISLAND_TAG.len() * 2
+ "<>".len()
@@ -107,11 +114,14 @@ where
+ "data-component".len()
+ View::MIN_LENGTH;
fn dry_resolve(&mut self) {
self.view.dry_resolve()
fn dry_resolve(&mut self, extra_attrs: ExtraAttrsMut<'_>) {
self.view.dry_resolve(extra_attrs)
}
async fn resolve(self) -> Self::AsyncOutput {
async fn resolve(
self,
extra_attrs: ExtraAttrsMut<'_>,
) -> Self::AsyncOutput {
let Island {
component,
props_json,
@@ -120,7 +130,7 @@ where
Island {
component,
props_json,
view: view.resolve().await,
view: view.resolve(extra_attrs).await,
}
}
@@ -130,10 +140,16 @@ where
position: &mut Position,
escape: bool,
mark_branches: bool,
extra_attrs: Option<Vec<AnyAttribute>>,
) {
Self::open_tag(self.component, &self.props_json, buf);
self.view
.to_html_with_buf(buf, position, escape, mark_branches);
self.view.to_html_with_buf(
buf,
position,
escape,
mark_branches,
extra_attrs,
);
Self::close_tag(buf);
}
@@ -143,6 +159,7 @@ where
position: &mut Position,
escape: bool,
mark_branches: bool,
extra_attrs: Option<Vec<AnyAttribute>>,
) where
Self: Sized,
{
@@ -157,6 +174,7 @@ where
position,
escape,
mark_branches,
extra_attrs,
);
// and insert the closing tag synchronously
@@ -169,6 +187,7 @@ where
self,
cursor: &Cursor,
position: &PositionState,
extra_attrs: Option<Vec<AnyAttribute>>,
) -> Self::State {
if position.get() == Position::FirstChild {
cursor.child();
@@ -177,7 +196,16 @@ where
}
position.set(Position::FirstChild);
self.view.hydrate::<FROM_SERVER>(cursor, position)
self.view
.hydrate::<FROM_SERVER>(cursor, position, extra_attrs)
}
fn into_owned(self) -> Self::Owned {
Island {
component: self.component,
props_json: self.props_json,
view: self.view.into_owned(),
}
}
}
@@ -227,9 +255,14 @@ where
{
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<View> AddAnyAttr for IslandChildren<View>
@@ -259,20 +292,24 @@ where
View: RenderHtml,
{
type AsyncOutput = IslandChildren<View::AsyncOutput>;
type Owned = IslandChildren<View::Owned>;
const MIN_LENGTH: usize = ISLAND_CHILDREN_TAG.len() * 2
+ "<>".len()
+ "</>".len()
+ View::MIN_LENGTH;
fn dry_resolve(&mut self) {
self.view.dry_resolve()
fn dry_resolve(&mut self, extra_attrs: ExtraAttrsMut<'_>) {
self.view.dry_resolve(extra_attrs)
}
async fn resolve(self) -> Self::AsyncOutput {
async fn resolve(
self,
extra_attrs: ExtraAttrsMut<'_>,
) -> Self::AsyncOutput {
let IslandChildren { view, on_hydrate } = self;
IslandChildren {
view: view.resolve().await,
view: view.resolve(extra_attrs).await,
on_hydrate,
}
}
@@ -283,10 +320,16 @@ where
position: &mut Position,
escape: bool,
mark_branches: bool,
extra_attrs: Option<Vec<AnyAttribute>>,
) {
Self::open_tag(buf);
self.view
.to_html_with_buf(buf, position, escape, mark_branches);
self.view.to_html_with_buf(
buf,
position,
escape,
mark_branches,
extra_attrs,
);
Self::close_tag(buf);
}
@@ -296,6 +339,7 @@ where
position: &mut Position,
escape: bool,
mark_branches: bool,
extra_attrs: Option<Vec<AnyAttribute>>,
) where
Self: Sized,
{
@@ -310,6 +354,7 @@ where
position,
escape,
mark_branches,
extra_attrs,
);
// and insert the closing tag synchronously
@@ -322,6 +367,7 @@ where
self,
cursor: &Cursor,
position: &PositionState,
_extra_attrs: Option<Vec<AnyAttribute>>,
) -> Self::State {
// island children aren't hydrated
// we update the walk to pass over them
@@ -356,4 +402,11 @@ where
);
}
}
fn into_owned(self) -> Self::Owned {
IslandChildren {
view: self.view.into_owned(),
on_hydrate: self.on_hydrate,
}
}
}

View File

@@ -7,8 +7,11 @@ use crate::{
dom::{Element, Node},
CastFrom, Rndr,
},
view::{Position, PositionState, Render, RenderHtml},
view::{
any_view::ExtraAttrsMut, Position, PositionState, Render, RenderHtml,
},
};
use attribute::any_attribute::AnyAttribute;
use std::borrow::Cow;
/// Types for HTML attributes.
@@ -43,21 +46,30 @@ pub fn doctype(value: &'static str) -> Doctype {
impl Render for Doctype {
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>>,
) {
}
}
no_attrs!(Doctype);
impl RenderHtml for Doctype {
type AsyncOutput = Self;
type Owned = Self;
const MIN_LENGTH: usize = "<!DOCTYPE html>".len();
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
}
@@ -67,6 +79,7 @@ impl RenderHtml for Doctype {
_position: &mut Position,
_escape: bool,
_mark_branches: bool,
_extra_attrs: Option<Vec<AnyAttribute>>,
) {
buf.push_str("<!DOCTYPE ");
buf.push_str(self.value);
@@ -77,8 +90,13 @@ impl RenderHtml for Doctype {
self,
_cursor: &Cursor,
_position: &PositionState,
_extra_attrs: Option<Vec<AnyAttribute>>,
) -> Self::State {
}
fn into_owned(self) -> Self::Owned {
self
}
}
/// An element that contains no interactivity, and whose contents can be known at compile time.
@@ -113,12 +131,16 @@ impl Mountable for InertElementState {
impl Render for InertElement {
type State = InertElementState;
fn build(self) -> Self::State {
fn build(self, _extra_attrs: Option<Vec<AnyAttribute>>) -> Self::State {
let el = Rndr::create_element_from_html(&self.html);
InertElementState(self.html, el)
}
fn rebuild(self, state: &mut Self::State) {
fn rebuild(
self,
state: &mut Self::State,
_extra_attrs: Option<Vec<AnyAttribute>>,
) {
let InertElementState(prev, el) = state;
if &self.html != prev {
let mut new_el = Rndr::create_element_from_html(&self.html);
@@ -149,16 +171,17 @@ impl AddAnyAttr for InertElement {
impl RenderHtml for InertElement {
type AsyncOutput = Self;
type Owned = Self;
const MIN_LENGTH: usize = 0;
fn html_len(&self) -> usize {
fn html_len(&self, _extra_attrs: Option<Vec<&AnyAttribute>>) -> usize {
self.html.len()
}
fn dry_resolve(&mut self) {}
fn dry_resolve(&mut self, _extra_attrs: ExtraAttrsMut<'_>) {}
async fn resolve(self) -> Self {
async fn resolve(self, _extra_attrs: ExtraAttrsMut<'_>) -> Self {
self
}
@@ -168,6 +191,7 @@ impl RenderHtml for InertElement {
position: &mut Position,
_escape: bool,
_mark_branches: bool,
_extra_attrs: Option<Vec<AnyAttribute>>,
) {
buf.push_str(&self.html);
*position = Position::NextChild;
@@ -177,6 +201,7 @@ impl RenderHtml for InertElement {
self,
cursor: &Cursor,
position: &PositionState,
_extra_attrs: Option<Vec<AnyAttribute>>,
) -> Self::State {
let curr_position = position.get();
if curr_position == Position::FirstChild {
@@ -189,4 +214,8 @@ impl RenderHtml for InertElement {
position.set(Position::NextChild);
InertElementState(self.html, el)
}
fn into_owned(self) -> Self::Owned {
self
}
}

View File

@@ -1,10 +1,17 @@
use crate::{
html::{attribute::AttributeValue, class::IntoClass, style::IntoStyle},
html::{
attribute::{any_attribute::AnyAttribute, AttributeValue},
class::IntoClass,
style::IntoStyle,
},
hydration::Cursor,
no_attrs,
prelude::{Mountable, Render, RenderHtml},
renderer::Rndr,
view::{strings::StrState, Position, PositionState, ToTemplate},
view::{
any_view::ExtraAttrsMut, strings::StrState, Position, PositionState,
ToTemplate,
},
};
use oco_ref::Oco;
@@ -17,12 +24,16 @@ pub struct OcoStrState {
impl Render for Oco<'static, str> {
type State = OcoStrState;
fn build(self) -> Self::State {
fn build(self, _extra_attrs: Option<Vec<AnyAttribute>>) -> Self::State {
let node = Rndr::create_text_node(&self);
OcoStrState { node, str: self }
}
fn rebuild(self, state: &mut Self::State) {
fn rebuild(
self,
state: &mut Self::State,
_extra_attrs: Option<Vec<AnyAttribute>>,
) {
let OcoStrState { node, str } = state;
if &self != str {
Rndr::set_text(node, &self);
@@ -35,12 +46,16 @@ no_attrs!(Oco<'static, str>);
impl RenderHtml for Oco<'static, str> {
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
}
@@ -50,6 +65,7 @@ impl RenderHtml for Oco<'static, str> {
position: &mut Position,
escape: bool,
mark_branches: bool,
extra_attrs: Option<Vec<AnyAttribute>>,
) {
<&str as RenderHtml>::to_html_with_buf(
&self,
@@ -57,6 +73,7 @@ impl RenderHtml for Oco<'static, str> {
position,
escape,
mark_branches,
extra_attrs,
)
}
@@ -64,13 +81,21 @@ impl RenderHtml for Oco<'static, str> {
self,
cursor: &Cursor,
position: &PositionState,
extra_attrs: Option<Vec<AnyAttribute>>,
) -> Self::State {
let this: &str = self.as_ref();
let StrState { node, .. } = <&str as RenderHtml>::hydrate::<FROM_SERVER>(
this, cursor, position,
this,
cursor,
position,
extra_attrs,
);
OcoStrState { node, str: self }
}
fn into_owned(self) -> <Self as RenderHtml>::Owned {
self
}
}
impl ToTemplate for Oco<'static, str> {

View File

@@ -352,7 +352,7 @@ where
})
}
fn rebuild(self, state: &mut Self::State) {
fn rebuild(self, state: &mut Self::State, _extra_attrs: Option<Vec<AnyAttribute>>) {
let (names, mut f) = self;
let prev_value = state.take_value();
@@ -433,7 +433,7 @@ where
<String as IntoClass>::build(self.deref().to_owned(), el)
}
fn rebuild(self, state: &mut Self::State) {
fn rebuild(self, state: &mut Self::State, _extra_attrs: Option<Vec<AnyAttribute>>) {
<String as IntoClass>::rebuild(self.deref().to_owned(), state)
}
@@ -447,7 +447,7 @@ where
fn dry_resolve(&mut self) {}
async fn resolve(self) -> Self::AsyncOutput {
async fn resolve(self, _extra_attrs: Option<Vec<AnyAttribute>>) -> Self::AsyncOutput {
self
}
}
@@ -489,7 +489,7 @@ where
)
}
fn rebuild(self, state: &mut Self::State) {
fn rebuild(self, state: &mut Self::State, _extra_attrs: Option<Vec<AnyAttribute>>) {
<(&'static str, bool) as IntoClass>::rebuild(
(self.0, *self.1.deref()),
state,
@@ -506,7 +506,7 @@ where
fn dry_resolve(&mut self) {}
async fn resolve(self) -> Self::AsyncOutput {
async fn resolve(self, _extra_attrs: Option<Vec<AnyAttribute>>) -> Self::AsyncOutput {
self
}
}
@@ -901,7 +901,7 @@ where
state
}
fn rebuild(self, state: &mut Self::State) {
fn rebuild(self, state: &mut Self::State, _extra_attrs: Option<Vec<AnyAttribute>>) {
reactive_graph::spawn_local_scoped({
let state = Rc::clone(state);
async move {
@@ -925,7 +925,7 @@ where
fn dry_resolve(&mut self) {}
async fn resolve(self) -> Self::AsyncOutput {
async fn resolve(self, _extra_attrs: Option<Vec<AnyAttribute>>) -> Self::AsyncOutput {
self.inner.await
}
}

View File

@@ -1,11 +1,11 @@
use crate::{
html::attribute::{Attribute, AttributeValue},
html::attribute::{any_attribute::AnyAttribute, Attribute, AttributeValue},
hydration::Cursor,
renderer::Rndr,
ssr::StreamBuilder,
view::{
add_attr::AddAnyAttr, Mountable, Position, PositionState, Render,
RenderHtml, ToTemplate,
add_attr::AddAnyAttr, any_view::ExtraAttrsMut, Mountable, Position,
PositionState, Render, RenderHtml, ToTemplate,
},
};
use reactive_graph::effect::RenderEffect;
@@ -57,7 +57,7 @@ where
type State = RenderEffectState<V::State>;
#[track_caller]
fn build(mut self) -> Self::State {
fn build(mut self, extra_attrs: Option<Vec<AnyAttribute>>) -> Self::State {
let hook = throw_error::get_error_hook();
RenderEffect::new(move |prev| {
let _guard = hook
@@ -65,18 +65,22 @@ where
.map(|h| throw_error::set_error_hook(Arc::clone(h)));
let value = self.invoke();
if let Some(mut state) = prev {
value.rebuild(&mut state);
value.rebuild(&mut state, extra_attrs.clone());
state
} else {
value.build()
value.build(extra_attrs.clone())
}
})
.into()
}
#[track_caller]
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();
@@ -128,18 +132,22 @@ where
V::State: 'static,
{
type AsyncOutput = V::AsyncOutput;
type Owned = F;
const MIN_LENGTH: usize = 0;
fn dry_resolve(&mut self) {
self.invoke().dry_resolve();
fn dry_resolve(&mut self, extra_attrs: ExtraAttrsMut<'_>) {
self.invoke().dry_resolve(extra_attrs);
}
async fn resolve(mut self) -> Self::AsyncOutput {
self.invoke().resolve().await
async fn resolve(
mut self,
extra_attrs: ExtraAttrsMut<'_>,
) -> Self::AsyncOutput {
self.invoke().resolve(extra_attrs).await
}
fn html_len(&self) -> usize {
fn html_len(&self, _extra_attrs: Option<Vec<&AnyAttribute>>) -> usize {
V::MIN_LENGTH
}
@@ -149,9 +157,16 @@ where
position: &mut Position,
escape: bool,
mark_branches: bool,
extra_attrs: Option<Vec<AnyAttribute>>,
) {
let value = self.invoke();
value.to_html_with_buf(buf, position, escape, mark_branches)
value.to_html_with_buf(
buf,
position,
escape,
mark_branches,
extra_attrs,
)
}
fn to_html_async_with_buf<const OUT_OF_ORDER: bool>(
@@ -160,6 +175,7 @@ where
position: &mut Position,
escape: bool,
mark_branches: bool,
extra_attrs: Option<Vec<AnyAttribute>>,
) where
Self: Sized,
{
@@ -169,6 +185,7 @@ where
position,
escape,
mark_branches,
extra_attrs,
);
}
@@ -176,6 +193,7 @@ where
mut self,
cursor: &Cursor,
position: &PositionState,
extra_attrs: Option<Vec<AnyAttribute>>,
) -> Self::State {
let cursor = cursor.clone();
let position = position.clone();
@@ -186,14 +204,22 @@ where
.map(|h| throw_error::set_error_hook(Arc::clone(h)));
let value = self.invoke();
if let Some(mut state) = prev {
value.rebuild(&mut state);
value.rebuild(&mut state, extra_attrs.clone());
state
} else {
value.hydrate::<FROM_SERVER>(&cursor, &position)
value.hydrate::<FROM_SERVER>(
&cursor,
&position,
extra_attrs.clone(),
)
}
})
.into()
}
fn into_owned(self) -> Self::Owned {
self
}
}
impl<F, V> AddAnyAttr for F
@@ -507,12 +533,14 @@ where
mod stable {
use super::RenderEffectState;
use crate::{
html::attribute::{Attribute, AttributeValue},
html::attribute::{
any_attribute::AnyAttribute, Attribute, AttributeValue,
},
hydration::Cursor,
ssr::StreamBuilder,
view::{
add_attr::AddAnyAttr, Mountable, Position, PositionState, Render,
RenderHtml,
add_attr::AddAnyAttr, any_view::ExtraAttrsMut, Mountable, Position,
PositionState, Render, RenderHtml,
},
};
#[allow(deprecated)]
@@ -537,13 +565,20 @@ mod stable {
type State = RenderEffectState<V::State>;
#[track_caller]
fn build(self) -> Self::State {
(move || self.get()).build()
fn build(
self,
extra_attrs: Option<Vec<AnyAttribute>>,
) -> Self::State {
(move || self.get()).build(extra_attrs)
}
#[track_caller]
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();
@@ -576,20 +611,27 @@ mod stable {
V::State: 'static,
{
type AsyncOutput = Self;
type Owned = Self;
const MIN_LENGTH: usize = 0;
fn dry_resolve(&mut self) {
fn dry_resolve(&mut self, _extra_attrs: ExtraAttrsMut<'_>) {
if $dry_resolve {
_ = self.get();
}
}
async fn resolve(self) -> Self::AsyncOutput {
async fn resolve(
self,
_extra_attrs: ExtraAttrsMut<'_>,
) -> Self::AsyncOutput {
self
}
fn html_len(&self) -> usize {
fn html_len(
&self,
_extra_attrs: Option<Vec<&AnyAttribute>>,
) -> usize {
V::MIN_LENGTH
}
@@ -599,9 +641,16 @@ mod stable {
position: &mut Position,
escape: bool,
mark_branches: bool,
extra_attrs: Option<Vec<AnyAttribute>>,
) {
let value = self.get();
value.to_html_with_buf(buf, position, escape, mark_branches)
value.to_html_with_buf(
buf,
position,
escape,
mark_branches,
extra_attrs,
)
}
fn to_html_async_with_buf<const OUT_OF_ORDER: bool>(
@@ -610,6 +659,7 @@ mod stable {
position: &mut Position,
escape: bool,
mark_branches: bool,
extra_attrs: Option<Vec<AnyAttribute>>,
) where
Self: Sized,
{
@@ -619,6 +669,7 @@ mod stable {
position,
escape,
mark_branches,
extra_attrs,
);
}
@@ -626,9 +677,17 @@ mod stable {
self,
cursor: &Cursor,
position: &PositionState,
extra_attrs: Option<Vec<AnyAttribute>>,
) -> Self::State {
(move || self.get())
.hydrate::<FROM_SERVER>(cursor, position)
(move || self.get()).hydrate::<FROM_SERVER>(
cursor,
position,
extra_attrs,
)
}
fn into_owned(self) -> Self::Owned {
self
}
}
@@ -705,13 +764,20 @@ mod stable {
type State = RenderEffectState<V::State>;
#[track_caller]
fn build(self) -> Self::State {
(move || self.get()).build()
fn build(
self,
extra_attrs: Option<Vec<AnyAttribute>>,
) -> Self::State {
(move || self.get()).build(extra_attrs)
}
#[track_caller]
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();
@@ -750,20 +816,27 @@ mod stable {
V::State: 'static,
{
type AsyncOutput = Self;
type Owned = Self;
const MIN_LENGTH: usize = 0;
fn dry_resolve(&mut self) {
fn dry_resolve(&mut self, _extra_attrs: ExtraAttrsMut<'_>) {
if $dry_resolve {
_ = self.get();
}
}
async fn resolve(self) -> Self::AsyncOutput {
async fn resolve(
self,
_extra_attrs: ExtraAttrsMut<'_>,
) -> Self::AsyncOutput {
self
}
fn html_len(&self) -> usize {
fn html_len(
&self,
_extra_attrs: Option<Vec<&AnyAttribute>>,
) -> usize {
V::MIN_LENGTH
}
@@ -773,9 +846,16 @@ mod stable {
position: &mut Position,
escape: bool,
mark_branches: bool,
extra_attrs: Option<Vec<AnyAttribute>>,
) {
let value = self.get();
value.to_html_with_buf(buf, position, escape, mark_branches)
value.to_html_with_buf(
buf,
position,
escape,
mark_branches,
extra_attrs,
)
}
fn to_html_async_with_buf<const OUT_OF_ORDER: bool>(
@@ -784,6 +864,7 @@ mod stable {
position: &mut Position,
escape: bool,
mark_branches: bool,
extra_attrs: Option<Vec<AnyAttribute>>,
) where
Self: Sized,
{
@@ -793,6 +874,7 @@ mod stable {
position,
escape,
mark_branches,
extra_attrs,
);
}
@@ -800,9 +882,17 @@ mod stable {
self,
cursor: &Cursor,
position: &PositionState,
extra_attrs: Option<Vec<AnyAttribute>>,
) -> Self::State {
(move || self.get())
.hydrate::<FROM_SERVER>(cursor, position)
(move || self.get()).hydrate::<FROM_SERVER>(
cursor,
position,
extra_attrs,
)
}
fn into_owned(self) -> Self::Owned {
self
}
}
@@ -895,7 +985,7 @@ mod tests {
let count = RwSignal::new(0);
let app: HtmlElement<_, _, _, MockDom> =
button((), move || count.get().to_string());
let el = app.build();
let el = app.build(None);
assert_eq!(el.el.to_debug_html(), "<button>0</button>");
rt.dispose();
}
@@ -906,7 +996,7 @@ mod tests {
let count = RwSignal::new(0);
let app: HtmlElement<_, _, _, MockDom> =
button((), move || count.get().to_string());
let el = app.build();
let el = app.build(None);
assert_eq!(el.el.to_debug_html(), "<button>0</button>");
count.set(1);
assert_eq!(el.el.to_debug_html(), "<button>1</button>");
@@ -924,7 +1014,7 @@ mod tests {
("Hello, my ", move || count.get().to_string(), " friends."),
),
);
let el = app.build();
let el = app.build(None);
assert_eq!(
el.el.to_debug_html(),
"<main><button>Hello, my 0 friends.</button></main>"

View File

@@ -1,9 +1,12 @@
use crate::{
html::attribute::Attribute,
html::attribute::{any_attribute::AnyAttribute, Attribute},
hydration::Cursor,
prelude::Mountable,
ssr::StreamBuilder,
view::{add_attr::AddAnyAttr, Position, PositionState, Render, RenderHtml},
view::{
add_attr::AddAnyAttr, any_view::ExtraAttrsMut, Position, PositionState,
Render, RenderHtml,
},
};
use reactive_graph::{computed::ScopedFuture, owner::Owner};
@@ -53,14 +56,18 @@ where
{
type State = OwnedViewState<T::State>;
fn build(self) -> Self::State {
let state = self.owner.with(|| self.view.build());
fn build(self, extra_attrs: Option<Vec<AnyAttribute>>) -> Self::State {
let state = self.owner.with(|| self.view.build(extra_attrs));
OwnedViewState::new(state, self.owner)
}
fn rebuild(self, state: &mut Self::State) {
fn rebuild(
self,
state: &mut Self::State,
extra_attrs: Option<Vec<AnyAttribute>>,
) {
let OwnedView { owner, view, .. } = self;
owner.with(|| view.rebuild(&mut state.state));
owner.with(|| view.rebuild(&mut state.state, extra_attrs));
state.owner = owner;
}
}
@@ -92,6 +99,7 @@ where
{
// TODO
type AsyncOutput = OwnedView<T::AsyncOutput>;
type Owned = OwnedView<T::Owned>;
const MIN_LENGTH: usize = T::MIN_LENGTH;
@@ -101,10 +109,16 @@ where
position: &mut Position,
escape: bool,
mark_branches: bool,
extra_attrs: Option<Vec<AnyAttribute>>,
) {
self.owner.with(|| {
self.view
.to_html_with_buf(buf, position, escape, mark_branches)
self.view.to_html_with_buf(
buf,
position,
escape,
mark_branches,
extra_attrs,
)
});
}
@@ -114,6 +128,7 @@ where
position: &mut Position,
escape: bool,
mark_branches: bool,
extra_attrs: Option<Vec<AnyAttribute>>,
) where
Self: Sized,
{
@@ -123,6 +138,7 @@ where
position,
escape,
mark_branches,
extra_attrs,
)
});
@@ -137,23 +153,39 @@ where
self,
cursor: &Cursor,
position: &PositionState,
extra_attrs: Option<Vec<AnyAttribute>>,
) -> Self::State {
let state = self
.owner
.with(|| self.view.hydrate::<FROM_SERVER>(cursor, position));
let state = self.owner.with(|| {
self.view
.hydrate::<FROM_SERVER>(cursor, position, extra_attrs)
});
OwnedViewState::new(state, self.owner)
}
async fn resolve(self) -> Self::AsyncOutput {
async fn resolve(
self,
extra_attrs: ExtraAttrsMut<'_>,
) -> Self::AsyncOutput {
let OwnedView { owner, view } = self;
let view = owner
.with(|| ScopedFuture::new(async move { view.resolve().await }))
.with(|| {
ScopedFuture::new(
async move { view.resolve(extra_attrs).await },
)
})
.await;
OwnedView { owner, view }
}
fn dry_resolve(&mut self) {
self.owner.with(|| self.view.dry_resolve());
fn dry_resolve(&mut self, extra_attrs: ExtraAttrsMut<'_>) {
self.owner.with(|| self.view.dry_resolve(extra_attrs));
}
fn into_owned(self) -> Self::Owned {
OwnedView {
owner: self.owner,
view: self.view.into_owned(),
}
}
}

View File

@@ -1,10 +1,10 @@
use crate::{
html::attribute::Attribute,
html::attribute::{any_attribute::AnyAttribute, Attribute},
hydration::Cursor,
ssr::StreamBuilder,
view::{
add_attr::AddAnyAttr, iterators::OptionState, Mountable, Position,
PositionState, Render, RenderHtml,
add_attr::AddAnyAttr, any_view::ExtraAttrsMut, iterators::OptionState,
Mountable, Position, PositionState, Render, RenderHtml,
},
};
use any_spawner::Executor;
@@ -27,7 +27,7 @@ use reactive_graph::{
use std::{
cell::RefCell,
fmt::Debug,
future::Future,
future::{Future, IntoFuture},
mem,
pin::Pin,
rc::Rc,
@@ -115,11 +115,15 @@ impl ToAnySubscriber for SuspendSubscriber {
impl<T> Suspend<T> {
/// Creates a new suspended view.
pub fn new(fut: impl Future<Output = T> + Send + 'static) -> Self {
pub fn new<Fut>(fut: Fut) -> Self
where
Fut: IntoFuture<Output = T>,
Fut::IntoFuture: Send + 'static,
{
let subscriber = SuspendSubscriber::new();
let any_subscriber = subscriber.to_any_subscriber();
let inner =
any_subscriber.with_observer(|| Box::pin(ScopedFuture::new(fut)));
let inner = any_subscriber
.with_observer(|| Box::pin(ScopedFuture::new(fut.into_future())));
Self { subscriber, inner }
}
}
@@ -165,7 +169,7 @@ where
{
type State = SuspendState<T>;
fn build(self) -> Self::State {
fn build(self, extra_attrs: Option<Vec<AnyAttribute>>) -> Self::State {
let Self { subscriber, inner } = self;
// create a Future that will be aborted on on_cleanup
@@ -180,7 +184,7 @@ where
// otherwise, start with the fallback
let initial = fut.as_mut().now_or_never().and_then(Result::ok);
let initially_pending = initial.is_none();
let inner = Rc::new(RefCell::new(initial.build()));
let inner = Rc::new(RefCell::new(initial.build(extra_attrs.clone())));
// get a unique ID if there's a SuspenseContext
let id = use_context::<SuspenseContext>().map(|sc| sc.task_id());
@@ -201,7 +205,8 @@ where
drop(id);
if let Ok(value) = value {
Some(value).rebuild(&mut *state.borrow_mut());
Some(value)
.rebuild(&mut *state.borrow_mut(), extra_attrs);
}
subscriber.forward();
@@ -214,7 +219,11 @@ where
SuspendState { inner }
}
fn rebuild(self, state: &mut Self::State) {
fn rebuild(
self,
state: &mut Self::State,
extra_attrs: Option<Vec<AnyAttribute>>,
) {
let Self { subscriber, inner } = self;
// create a Future that will be aborted on on_cleanup
@@ -244,7 +253,7 @@ where
// has no parent
Executor::tick().await;
if let Ok(value) = value {
Some(value).rebuild(&mut *state.borrow_mut());
Some(value).rebuild(&mut *state.borrow_mut(), extra_attrs);
}
subscriber.forward();
@@ -280,6 +289,7 @@ where
T: RenderHtml + Sized + 'static,
{
type AsyncOutput = Option<T>;
type Owned = Self;
const MIN_LENGTH: usize = T::MIN_LENGTH;
@@ -289,12 +299,19 @@ where
position: &mut Position,
escape: bool,
mark_branches: bool,
extra_attrs: Option<Vec<AnyAttribute>>,
) {
// TODO wrap this with a Suspense as needed
// currently this is just used for Routes, which creates a Suspend but never actually needs
// it (because we don't lazy-load routes on the server)
if let Some(inner) = self.inner.now_or_never() {
inner.to_html_with_buf(buf, position, escape, mark_branches);
inner.to_html_with_buf(
buf,
position,
escape,
mark_branches,
extra_attrs,
);
}
}
@@ -304,6 +321,7 @@ where
position: &mut Position,
escape: bool,
mark_branches: bool,
extra_attrs: Option<Vec<AnyAttribute>>,
) where
Self: Sized,
{
@@ -314,6 +332,7 @@ where
position,
escape,
mark_branches,
extra_attrs,
),
None => {
if use_context::<SuspenseContext>().is_none() {
@@ -338,6 +357,7 @@ where
(),
&mut fallback_position,
mark_branches,
extra_attrs.clone(),
);
// TODO in 0.8: this should include a nonce
@@ -349,6 +369,7 @@ where
fut,
position,
mark_branches,
extra_attrs,
);
} else {
buf.push_async({
@@ -361,6 +382,7 @@ where
&mut position,
escape,
mark_branches,
extra_attrs,
);
builder.finish().take_chunks()
}
@@ -377,6 +399,7 @@ where
self,
cursor: &Cursor,
position: &PositionState,
extra_attrs: Option<Vec<AnyAttribute>>,
) -> Self::State {
let Self { subscriber, inner } = self;
@@ -392,9 +415,11 @@ where
// otherwise, start with the fallback
let initial = fut.as_mut().now_or_never().and_then(Result::ok);
let initially_pending = initial.is_none();
let inner = Rc::new(RefCell::new(
initial.hydrate::<FROM_SERVER>(cursor, position),
));
let inner = Rc::new(RefCell::new(initial.hydrate::<FROM_SERVER>(
cursor,
position,
extra_attrs.clone(),
)));
// get a unique ID if there's a SuspenseContext
let id = use_context::<SuspenseContext>().map(|sc| sc.task_id());
@@ -415,7 +440,8 @@ where
drop(id);
if let Ok(value) = value {
Some(value).rebuild(&mut *state.borrow_mut());
Some(value)
.rebuild(&mut *state.borrow_mut(), extra_attrs);
}
subscriber.forward();
@@ -428,11 +454,14 @@ where
SuspendState { inner }
}
async fn resolve(self) -> Self::AsyncOutput {
async fn resolve(
self,
_extra_attrs: ExtraAttrsMut<'_>,
) -> Self::AsyncOutput {
Some(self.inner.await)
}
fn dry_resolve(&mut self) {
fn dry_resolve(&mut self, _extra_attrs: ExtraAttrsMut<'_>) {
// this is a little crazy, but if a Suspend is immediately available, we end up
// with a situation where polling it multiple times (here in dry_resolve and then in
// resolve) causes a runtime panic
@@ -452,4 +481,8 @@ where
as Pin<Box<dyn Future<Output = T> + Send>>;
}
}
fn into_owned(self) -> Self::Owned {
self
}
}

View File

@@ -1,4 +1,7 @@
use crate::view::{Position, RenderHtml};
use crate::{
html::attribute::any_attribute::AnyAttribute,
view::{Position, RenderHtml},
};
use futures::Stream;
use std::{
collections::VecDeque,
@@ -103,6 +106,7 @@ impl StreamBuilder {
fallback: View,
position: &mut Position,
mark_branches: bool,
extra_attrs: Option<Vec<AnyAttribute>>,
) where
View: RenderHtml,
{
@@ -112,6 +116,7 @@ impl StreamBuilder {
position,
true,
mark_branches,
extra_attrs,
);
self.write_chunk_marker(false);
*position = Position::NextChild;
@@ -160,6 +165,7 @@ impl StreamBuilder {
view: impl Future<Output = Option<View>> + Send + 'static,
position: &mut Position,
mark_branches: bool,
extra_attrs: Option<Vec<AnyAttribute>>,
) where
View: RenderHtml,
{
@@ -168,6 +174,7 @@ impl StreamBuilder {
position,
mark_branches,
None,
extra_attrs,
);
}
@@ -178,6 +185,7 @@ impl StreamBuilder {
position: &mut Position,
mark_branches: bool,
nonce: Option<Arc<str>>,
extra_attrs: Option<Vec<AnyAttribute>>,
) where
View: RenderHtml,
{
@@ -207,6 +215,7 @@ impl StreamBuilder {
&mut position,
true,
mark_branches,
extra_attrs,
);
}
let chunks = subbuilder.finish().take_chunks();

View File

@@ -1,3 +1,5 @@
#![allow(unused_mut)]
#![allow(clippy::type_complexity)]
#[cfg(feature = "ssr")]
use super::MarkBranch;
use super::{
@@ -5,7 +7,12 @@ use super::{
RenderHtml,
};
use crate::{
html::attribute::Attribute, hydration::Cursor, ssr::StreamBuilder,
html::attribute::{
any_attribute::{AnyAttribute, IntoAnyAttribute},
Attribute,
},
hydration::Cursor,
ssr::StreamBuilder,
};
use std::{
any::{Any, TypeId},
@@ -27,38 +34,60 @@ use std::{future::Future, pin::Pin};
pub struct AnyView {
type_id: TypeId,
value: Box<dyn Any + Send>,
build: fn(Box<dyn Any>) -> AnyViewState,
rebuild: fn(TypeId, Box<dyn Any>, &mut AnyViewState),
// Without erasure, tuples of attrs created by default cause too much type explosion to enable.
#[cfg(erase_components)]
add_any_attr: fn(
Box<dyn Any>,
crate::html::attribute::any_attribute::AnyAttribute,
) -> AnyView,
extra_attrs: Vec<AnyAttribute>,
build: fn(Box<dyn Any>, Option<Vec<AnyAttribute>>) -> AnyViewState,
rebuild: fn(Box<dyn Any>, &mut AnyViewState, Option<Vec<AnyAttribute>>),
// The fields below are cfg-gated so they will not be included in WASM bundles if not needed.
// Ordinarily, the compiler can simply omit this dead code because the methods are not called.
// With this type-erased wrapper, however, the compiler is not *always* able to correctly
// eliminate that code.
#[cfg(feature = "ssr")]
html_len: usize,
html_len: fn(&Box<dyn Any + Send>, Option<Vec<&AnyAttribute>>) -> usize,
#[cfg(feature = "ssr")]
to_html: fn(Box<dyn Any>, &mut String, &mut Position, bool, bool),
to_html: fn(
Box<dyn Any>,
&mut String,
&mut Position,
bool,
bool,
Option<Vec<AnyAttribute>>,
),
#[cfg(feature = "ssr")]
to_html_async:
fn(Box<dyn Any>, &mut StreamBuilder, &mut Position, bool, bool),
to_html_async: fn(
Box<dyn Any>,
&mut StreamBuilder,
&mut Position,
bool,
bool,
Option<Vec<AnyAttribute>>,
),
#[cfg(feature = "ssr")]
to_html_async_ooo:
fn(Box<dyn Any>, &mut StreamBuilder, &mut Position, bool, bool),
to_html_async_ooo: fn(
Box<dyn Any>,
&mut StreamBuilder,
&mut Position,
bool,
bool,
Option<Vec<AnyAttribute>>,
),
#[cfg(feature = "ssr")]
#[allow(clippy::type_complexity)]
resolve: fn(Box<dyn Any>) -> Pin<Box<dyn Future<Output = AnyView> + Send>>,
resolve: for<'a> fn(
Box<dyn Any>,
ExtraAttrsMut<'a>,
)
-> Pin<Box<dyn Future<Output = AnyView> + Send + 'a>>,
#[cfg(feature = "ssr")]
dry_resolve: fn(&mut Box<dyn Any + Send>),
dry_resolve: fn(&mut Box<dyn Any + Send>, ExtraAttrsMut<'_>),
#[cfg(feature = "hydrate")]
#[cfg(feature = "hydrate")]
#[allow(clippy::type_complexity)]
hydrate_from_server:
fn(Box<dyn Any>, &Cursor, &PositionState) -> AnyViewState,
hydrate_from_server: fn(
Box<dyn Any>,
&Cursor,
&PositionState,
Option<Vec<AnyAttribute>>,
) -> AnyViewState,
}
/// Retained view state for [`AnyView`].
@@ -128,50 +157,64 @@ where
state.insert_before_this(child)
}
#[cfg(feature = "ssr")]
fn resolve<'a, T>(
value: Box<dyn Any>,
extra_attrs: ExtraAttrsMut<'a>,
) -> Pin<Box<dyn Future<Output = AnyView> + Send + 'a>>
where
T: RenderHtml + 'static,
{
let value = value
.downcast::<T>()
.expect("AnyView::resolve could not be downcast");
Box::pin(async move { value.resolve(extra_attrs).await.into_any() })
}
impl<T> IntoAny for T
where
T: Send,
T: RenderHtml + 'static,
T::State: 'static,
T: RenderHtml,
T::Owned: Send,
{
fn into_any(self) -> AnyView {
#[cfg(feature = "ssr")]
let html_len = self.html_len();
let value = Box::new(self) as Box<dyn Any + Send>;
let value = Box::new(self.into_owned()) as Box<dyn Any + Send>;
match value.downcast::<AnyView>() {
// if it's already an AnyView, we don't need to double-wrap it
Ok(any_view) => *any_view,
Err(value) => {
#[cfg(feature = "ssr")]
let dry_resolve = |value: &mut Box<dyn Any + Send>| {
let value = value
.downcast_mut::<T>()
.expect("AnyView::resolve could not be downcast");
value.dry_resolve();
};
let html_len =
|value: &Box<dyn Any + Send>, extra_attrs: Option<Vec<&AnyAttribute>>| {
let value = value
.downcast_ref::<T::Owned>()
.expect("AnyView::html_len could not be downcast");
value.html_len(extra_attrs)
};
#[cfg(feature = "ssr")]
let resolve = |value: Box<dyn Any>| {
let value = value
.downcast::<T>()
.expect("AnyView::resolve could not be downcast");
Box::pin(async move { value.resolve().await.into_any() })
as Pin<Box<dyn Future<Output = AnyView> + Send>>
};
let dry_resolve =
|value: &mut Box<dyn Any + Send>,
extra_attrs: ExtraAttrsMut<'_>| {
let value = value
.downcast_mut::<T::Owned>()
.expect("AnyView::resolve could not be downcast");
value.dry_resolve(extra_attrs);
};
#[cfg(feature = "ssr")]
let to_html =
|value: Box<dyn Any>,
buf: &mut String,
position: &mut Position,
escape: bool,
mark_branches: bool| {
mark_branches: bool,
extra_attrs: Option<Vec<AnyAttribute>>| {
let type_id = mark_branches
.then(|| format!("{:?}", TypeId::of::<T>()))
.then(|| format!("{:?}", TypeId::of::<T::Owned>()))
.unwrap_or_default();
let value = value
.downcast::<T>()
.downcast::<T::Owned>()
.expect("AnyView::to_html could not be downcast");
if mark_branches {
buf.open_branch(&type_id);
@@ -181,6 +224,7 @@ where
position,
escape,
mark_branches,
extra_attrs,
);
if mark_branches {
buf.close_branch(&type_id);
@@ -192,12 +236,13 @@ where
buf: &mut StreamBuilder,
position: &mut Position,
escape: bool,
mark_branches: bool| {
mark_branches: bool,
extra_attrs: Option<Vec<AnyAttribute>>| {
let type_id = mark_branches
.then(|| format!("{:?}", TypeId::of::<T>()))
.then(|| format!("{:?}", TypeId::of::<T::Owned>()))
.unwrap_or_default();
let value = value
.downcast::<T>()
.downcast::<T::Owned>()
.expect("AnyView::to_html could not be downcast");
if mark_branches {
buf.open_branch(&type_id);
@@ -207,108 +252,98 @@ where
position,
escape,
mark_branches,
extra_attrs,
);
if mark_branches {
buf.close_branch(&type_id);
}
};
#[cfg(feature = "ssr")]
let to_html_async_ooo =
|value: Box<dyn Any>,
buf: &mut StreamBuilder,
position: &mut Position,
escape: bool,
mark_branches: bool| {
let value = value
.downcast::<T>()
.expect("AnyView::to_html could not be downcast");
value.to_html_async_with_buf::<true>(
buf,
position,
escape,
mark_branches,
);
};
let build = |value: Box<dyn Any>| {
let to_html_async_ooo = |value: Box<dyn Any>,
buf: &mut StreamBuilder,
position: &mut Position,
escape: bool,
mark_branches: bool,
extra_attrs: Option<
Vec<AnyAttribute>,
>| {
let value = value
.downcast::<T>()
.downcast::<T::Owned>()
.expect("AnyView::to_html could not be downcast");
value.to_html_async_with_buf::<true>(
buf,
position,
escape,
mark_branches,
extra_attrs,
);
};
let build = |value: Box<dyn Any>, extra_attrs: Option<Vec<AnyAttribute>>| {
let value = value
.downcast::<T::Owned>()
.expect("AnyView::build couldn't downcast");
let state = Box::new(value.build());
let state = Box::new(value.build(extra_attrs));
AnyViewState {
type_id: TypeId::of::<T>(),
type_id: TypeId::of::<T::Owned>(),
state,
mount: mount_any::<T>,
unmount: unmount_any::<T>,
insert_before_this: insert_before_this::<T>,
mount: mount_any::<T::Owned>,
unmount: unmount_any::<T::Owned>,
insert_before_this: insert_before_this::<T::Owned>,
}
};
#[cfg(feature = "hydrate")]
let hydrate_from_server =
|value: Box<dyn Any>,
cursor: &Cursor,
position: &PositionState| {
let value = value.downcast::<T>().expect(
"AnyView::hydrate_from_server couldn't downcast",
);
let state =
Box::new(value.hydrate::<true>(cursor, position));
let hydrate_from_server = |value: Box<dyn Any>,
cursor: &Cursor,
position: &PositionState,
extra_attrs: Option<
Vec<AnyAttribute>,
>| {
let value = value.downcast::<T::Owned>().expect(
"AnyView::hydrate_from_server couldn't downcast",
);
let state = Box::new(value.hydrate::<true>(
cursor,
position,
extra_attrs,
));
AnyViewState {
type_id: TypeId::of::<T>(),
state,
AnyViewState {
type_id: TypeId::of::<T::Owned>(),
state,
mount: mount_any::<T>,
unmount: unmount_any::<T>,
insert_before_this: insert_before_this::<T>,
}
};
let rebuild =
|new_type_id: TypeId,
value: Box<dyn Any>,
state: &mut AnyViewState| {
let value = value
.downcast::<T>()
.expect("AnyView::rebuild couldn't downcast value");
if new_type_id == state.type_id {
let state = state.state.downcast_mut().expect(
"AnyView::rebuild couldn't downcast state",
);
value.rebuild(state);
} else {
let mut new = value.into_any().build();
state.insert_before_this(&mut new);
state.unmount();
*state = new;
}
};
// Without erasure, tuples of attrs created by default cause too much type explosion to enable.
#[cfg(erase_components)]
let add_any_attr = |value: Box<dyn Any>, attr: crate::html::attribute::any_attribute::AnyAttribute| {
let value = value
.downcast::<T>()
.expect("AnyView::add_any_attr could not be downcast");
value.add_any_attr(attr).into_any()
mount: mount_any::<T::Owned>,
unmount: unmount_any::<T::Owned>,
insert_before_this: insert_before_this::<T::Owned>,
}
};
let rebuild =
|value: Box<dyn Any>,
state: &mut AnyViewState, extra_attrs: Option<Vec<AnyAttribute>>| {
let value = value
.downcast::<T::Owned>()
.expect("AnyView::rebuild couldn't downcast value");
let state = state.state.downcast_mut().expect(
"AnyView::rebuild couldn't downcast state",
);
value.rebuild(state, extra_attrs);
};
AnyView {
type_id: TypeId::of::<T>(),
type_id: TypeId::of::<T::Owned>(),
value,
extra_attrs: vec![],
build,
rebuild,
// Without erasure, tuples of attrs created by default cause too much type explosion to enable.
#[cfg(erase_components)]
add_any_attr,
#[cfg(feature = "ssr")]
resolve,
#[cfg(feature = "ssr")]
dry_resolve,
#[cfg(feature = "ssr")]
html_len,
#[cfg(feature = "ssr")]
resolve: resolve::<T::Owned>,
#[cfg(feature = "ssr")]
dry_resolve,
#[cfg(feature = "ssr")]
to_html,
#[cfg(feature = "ssr")]
to_html_async,
@@ -322,15 +357,144 @@ where
}
}
/// Ignore, this is a hack for pre use<..> syntax.
/// https://github.com/rust-lang/rfcs/blob/master/text/3498-lifetime-capture-rules-2024.md#the-captures-trick
pub trait __Captures<T: ?Sized> {}
impl<T: ?Sized, U: ?Sized> __Captures<T> for U {}
/// A mutable view into the extra attributes stored in an [`AnyView`].
#[derive(Default)]
pub struct ExtraAttrsMut<'a>(Option<Vec<&'a mut Vec<AnyAttribute>>>);
impl<'a> ExtraAttrsMut<'a> {
/// Create a new mutable view from owned attributes.
pub fn from_owned(extra_attrs: &'a mut Option<Vec<AnyAttribute>>) -> Self {
match extra_attrs {
Some(extra_attrs) => {
if extra_attrs.is_empty() {
Self(None)
} else {
Self(Some(vec![extra_attrs]))
}
}
None => Self(None),
}
}
#[cfg(feature = "ssr")]
fn add_layer<'b>(
mut self,
extra_attrs: &'b mut Vec<AnyAttribute>,
) -> ExtraAttrsMut<'b>
where
'a: 'b,
{
match (self.0, extra_attrs.is_empty()) {
(Some(mut extra), false) => {
extra.push(extra_attrs);
ExtraAttrsMut(Some(extra))
}
(Some(mut extra), true) => {
self.0 = Some(extra);
self
}
(None, false) => ExtraAttrsMut(Some(vec![extra_attrs])),
(None, true) => ExtraAttrsMut(None),
}
}
/// Check if there are any extra attributes.
pub fn is_some(&self) -> bool {
match &self.0 {
Some(extra) => extra.is_empty(),
None => true,
}
}
/// "clone" the mutable view, to allow reuse in e.g. a for loop.
/// The same as .as_deref_mut() on Option<&mut T>.
pub fn as_deref_mut(&mut self) -> ExtraAttrsMut<'_> {
ExtraAttrsMut(
self.0
.as_mut()
.map(|inner| inner.iter_mut().map(|v| &mut **v).collect()),
)
}
/// Iterate over the extra attributes.
pub fn iter_mut(
&mut self,
) -> impl Iterator<Item = &mut AnyAttribute> + __Captures<&'a ()> + '_ {
match &mut self.0 {
Some(inner) => itertools::Either::Left(
inner.iter_mut().flat_map(|v| v.iter_mut()),
),
None => itertools::Either::Right(std::iter::empty()),
}
}
/// Call [`RenderHtml::resolve`] on any extra attributes in parallel.
pub async fn resolve(self) {
if let Some(extra_attr_groups) = self.0 {
futures::future::join_all(extra_attr_groups.into_iter().map(
|extra_attrs| async move {
*extra_attrs =
Attribute::resolve(std::mem::take(extra_attrs)).await;
},
))
.await;
}
}
}
fn combine_owned_extra_attrs(
parent_extra_attrs: Option<Vec<AnyAttribute>>,
extra_attrs: Vec<AnyAttribute>,
) -> Option<Vec<AnyAttribute>> {
let extra_attrs = if let Some(mut parent_extra_attrs) = parent_extra_attrs {
for attr in extra_attrs {
parent_extra_attrs.push(attr);
}
parent_extra_attrs
} else {
extra_attrs
};
if extra_attrs.is_empty() {
None
} else {
Some(extra_attrs)
}
}
impl Render for AnyView {
type State = AnyViewState;
fn build(self) -> Self::State {
(self.build)(self.value)
fn build(self, extra_attrs: Option<Vec<AnyAttribute>>) -> Self::State {
(self.build)(
self.value,
combine_owned_extra_attrs(extra_attrs, self.extra_attrs),
)
}
fn rebuild(self, state: &mut Self::State) {
(self.rebuild)(self.type_id, self.value, state)
fn rebuild(
self,
state: &mut Self::State,
extra_attrs: Option<Vec<AnyAttribute>>,
) {
if self.type_id == state.type_id {
(self.rebuild)(
self.value,
state,
combine_owned_extra_attrs(extra_attrs, self.extra_attrs),
)
} else {
let mut new = (self.build)(
self.value,
combine_owned_extra_attrs(extra_attrs, self.extra_attrs),
);
state.insert_before_this(&mut new);
state.unmount();
*state = new;
}
}
}
@@ -339,52 +503,60 @@ impl AddAnyAttr for AnyView {
#[allow(unused_variables)]
fn add_any_attr<NewAttr: Attribute>(
self,
mut self,
attr: NewAttr,
) -> Self::Output<NewAttr>
where
Self::Output<NewAttr>: RenderHtml,
{
// Without erasure, tuples of attrs created by default cause too much type explosion to enable.
#[cfg(erase_components)]
{
use crate::html::attribute::any_attribute::IntoAnyAttribute;
let attr = attr.into_cloneable_owned();
(self.add_any_attr)(self.value, attr.into_any_attr())
}
#[cfg(not(erase_components))]
{
self
}
self.extra_attrs
.push(attr.into_cloneable_owned().into_any_attr());
self
}
}
impl RenderHtml for AnyView {
type AsyncOutput = Self;
type Owned = Self;
fn dry_resolve(&mut self) {
fn dry_resolve(&mut self, extra_attrs: ExtraAttrsMut<'_>) {
#[cfg(feature = "ssr")]
{
(self.dry_resolve)(&mut self.value)
(self.dry_resolve)(
&mut self.value,
extra_attrs.add_layer(&mut self.extra_attrs),
);
}
#[cfg(not(feature = "ssr"))]
panic!(
"You are rendering AnyView to HTML without the `ssr` feature \
enabled."
);
{
_ = extra_attrs;
panic!(
"You are rendering AnyView to HTML without the `ssr` feature \
enabled."
);
}
}
async fn resolve(self) -> Self::AsyncOutput {
async fn resolve(
mut self,
extra_attrs: ExtraAttrsMut<'_>,
) -> Self::AsyncOutput {
#[cfg(feature = "ssr")]
{
(self.resolve)(self.value).await
(self.resolve)(
self.value,
extra_attrs.add_layer(&mut self.extra_attrs),
)
.await
}
#[cfg(not(feature = "ssr"))]
panic!(
"You are rendering AnyView to HTML without the `ssr` feature \
enabled."
);
{
_ = extra_attrs;
panic!(
"You are rendering AnyView to HTML without the `ssr` feature \
enabled."
);
}
}
const MIN_LENGTH: usize = 0;
@@ -395,15 +567,26 @@ impl RenderHtml for AnyView {
position: &mut Position,
escape: bool,
mark_branches: bool,
extra_attrs: Option<Vec<AnyAttribute>>,
) {
#[cfg(feature = "ssr")]
(self.to_html)(self.value, buf, position, escape, mark_branches);
{
(self.to_html)(
self.value,
buf,
position,
escape,
mark_branches,
combine_owned_extra_attrs(extra_attrs, self.extra_attrs),
);
}
#[cfg(not(feature = "ssr"))]
{
_ = mark_branches;
_ = buf;
_ = position;
_ = escape;
_ = extra_attrs;
panic!(
"You are rendering AnyView to HTML without the `ssr` feature \
enabled."
@@ -417,26 +600,31 @@ impl RenderHtml for AnyView {
position: &mut Position,
escape: bool,
mark_branches: bool,
extra_attrs: Option<Vec<AnyAttribute>>,
) where
Self: Sized,
{
#[cfg(feature = "ssr")]
if OUT_OF_ORDER {
(self.to_html_async_ooo)(
self.value,
buf,
position,
escape,
mark_branches,
);
} else {
(self.to_html_async)(
self.value,
buf,
position,
escape,
mark_branches,
);
{
if OUT_OF_ORDER {
(self.to_html_async_ooo)(
self.value,
buf,
position,
escape,
mark_branches,
combine_owned_extra_attrs(extra_attrs, self.extra_attrs),
);
} else {
(self.to_html_async)(
self.value,
buf,
position,
escape,
mark_branches,
combine_owned_extra_attrs(extra_attrs, self.extra_attrs),
);
}
}
#[cfg(not(feature = "ssr"))]
{
@@ -444,6 +632,7 @@ impl RenderHtml for AnyView {
_ = position;
_ = escape;
_ = mark_branches;
_ = extra_attrs;
panic!(
"You are rendering AnyView to HTML without the `ssr` feature \
enabled."
@@ -455,20 +644,29 @@ impl RenderHtml for AnyView {
self,
cursor: &Cursor,
position: &PositionState,
extra_attrs: Option<Vec<AnyAttribute>>,
) -> Self::State {
#[cfg(feature = "hydrate")]
if FROM_SERVER {
(self.hydrate_from_server)(self.value, cursor, position)
} else {
panic!(
"hydrating AnyView from inside a ViewTemplate is not \
supported."
);
{
if FROM_SERVER {
(self.hydrate_from_server)(
self.value,
cursor,
position,
combine_owned_extra_attrs(extra_attrs, self.extra_attrs),
)
} else {
panic!(
"hydrating AnyView from inside a ViewTemplate is not \
supported."
);
}
}
#[cfg(not(feature = "hydrate"))]
{
_ = cursor;
_ = position;
_ = extra_attrs;
panic!(
"You are trying to hydrate AnyView without the `hydrate` \
feature enabled."
@@ -476,16 +674,34 @@ impl RenderHtml for AnyView {
}
}
fn html_len(&self) -> usize {
fn html_len(&self, extra_attrs: Option<Vec<&AnyAttribute>>) -> usize {
#[cfg(feature = "ssr")]
{
self.html_len
(self.html_len)(
&self.value,
match (extra_attrs, self.extra_attrs.is_empty()) {
(Some(mut extra_attrs), false) => {
for attr in &self.extra_attrs {
extra_attrs.push(attr);
}
Some(extra_attrs)
}
(Some(extra_attrs), true) => Some(extra_attrs),
(None, false) => Some(self.extra_attrs.iter().collect()),
(None, true) => None,
},
)
}
#[cfg(not(feature = "ssr"))]
{
_ = extra_attrs;
0
}
}
fn into_owned(self) -> Self::Owned {
self
}
}
impl Mountable for AnyViewState {

View File

@@ -1,9 +1,11 @@
use super::{
add_attr::AddAnyAttr, MarkBranch, Mountable, Position, PositionState,
Render, RenderHtml,
add_attr::AddAnyAttr, any_view::ExtraAttrsMut, MarkBranch, Mountable,
Position, PositionState, Render, RenderHtml,
};
use crate::{
html::attribute::Attribute, hydration::Cursor, ssr::StreamBuilder,
html::attribute::{any_attribute::AnyAttribute, Attribute},
hydration::Cursor,
ssr::StreamBuilder,
};
use either_of::*;
use futures::future::join;
@@ -15,29 +17,33 @@ where
{
type State = Either<A::State, B::State>;
fn build(self) -> Self::State {
fn build(self, extra_attrs: Option<Vec<AnyAttribute>>) -> Self::State {
match self {
Either::Left(left) => Either::Left(left.build()),
Either::Right(right) => Either::Right(right.build()),
Either::Left(left) => Either::Left(left.build(extra_attrs)),
Either::Right(right) => Either::Right(right.build(extra_attrs)),
}
}
fn rebuild(self, state: &mut Self::State) {
fn rebuild(
self,
state: &mut Self::State,
extra_attrs: Option<Vec<AnyAttribute>>,
) {
match (self, &mut *state) {
(Either::Left(new), Either::Left(old)) => {
new.rebuild(old);
new.rebuild(old, extra_attrs);
}
(Either::Right(new), Either::Right(old)) => {
new.rebuild(old);
new.rebuild(old, extra_attrs);
}
(Either::Right(new), Either::Left(old)) => {
let mut new_state = new.build();
let mut new_state = new.build(extra_attrs);
old.insert_before_this(&mut new_state);
old.unmount();
*state = Either::Right(new_state);
}
(Either::Left(new), Either::Right(old)) => {
let mut new_state = new.build();
let mut new_state = new.build(extra_attrs);
old.insert_before_this(&mut new_state);
old.unmount();
*state = Either::Left(new_state);
@@ -120,28 +126,34 @@ where
B: RenderHtml,
{
type AsyncOutput = Either<A::AsyncOutput, B::AsyncOutput>;
type Owned = Either<A::Owned, B::Owned>;
fn dry_resolve(&mut self) {
fn dry_resolve(&mut self, extra_attrs: ExtraAttrsMut<'_>) {
match self {
Either::Left(left) => left.dry_resolve(),
Either::Right(right) => right.dry_resolve(),
Either::Left(left) => left.dry_resolve(extra_attrs),
Either::Right(right) => right.dry_resolve(extra_attrs),
}
}
async fn resolve(self) -> Self::AsyncOutput {
async fn resolve(
self,
extra_attrs: ExtraAttrsMut<'_>,
) -> Self::AsyncOutput {
match self {
Either::Left(left) => Either::Left(left.resolve().await),
Either::Right(right) => Either::Right(right.resolve().await),
Either::Left(left) => Either::Left(left.resolve(extra_attrs).await),
Either::Right(right) => {
Either::Right(right.resolve(extra_attrs).await)
}
}
}
const MIN_LENGTH: usize = max_usize(&[A::MIN_LENGTH, B::MIN_LENGTH]);
#[inline(always)]
fn html_len(&self) -> usize {
fn html_len(&self, extra_attrs: Option<Vec<&AnyAttribute>>) -> usize {
match self {
Either::Left(i) => i.html_len(),
Either::Right(i) => i.html_len(),
Either::Left(i) => i.html_len(extra_attrs),
Either::Right(i) => i.html_len(extra_attrs),
}
}
@@ -151,13 +163,20 @@ where
position: &mut Position,
escape: bool,
mark_branches: bool,
extra_attrs: Option<Vec<AnyAttribute>>,
) {
match self {
Either::Left(left) => {
if mark_branches {
buf.open_branch("0");
}
left.to_html_with_buf(buf, position, escape, mark_branches);
left.to_html_with_buf(
buf,
position,
escape,
mark_branches,
extra_attrs,
);
if mark_branches {
buf.close_branch("0");
}
@@ -166,7 +185,13 @@ where
if mark_branches {
buf.open_branch("1");
}
right.to_html_with_buf(buf, position, escape, mark_branches);
right.to_html_with_buf(
buf,
position,
escape,
mark_branches,
extra_attrs,
);
if mark_branches {
buf.close_branch("1");
}
@@ -180,6 +205,7 @@ where
position: &mut Position,
escape: bool,
mark_branches: bool,
extra_attrs: Option<Vec<AnyAttribute>>,
) where
Self: Sized,
{
@@ -193,6 +219,7 @@ where
position,
escape,
mark_branches,
extra_attrs,
);
if mark_branches {
buf.close_branch("0");
@@ -207,6 +234,7 @@ where
position,
escape,
mark_branches,
extra_attrs,
);
if mark_branches {
buf.close_branch("1");
@@ -219,14 +247,24 @@ where
self,
cursor: &Cursor,
position: &PositionState,
extra_attrs: Option<Vec<AnyAttribute>>,
) -> Self::State {
match self {
Either::Left(left) => {
Either::Left(left.hydrate::<FROM_SERVER>(cursor, position))
}
Either::Right(right) => {
Either::Right(right.hydrate::<FROM_SERVER>(cursor, position))
}
Either::Left(left) => Either::Left(left.hydrate::<FROM_SERVER>(
cursor,
position,
extra_attrs,
)),
Either::Right(right) => Either::Right(
right.hydrate::<FROM_SERVER>(cursor, position, extra_attrs),
),
}
}
fn into_owned(self) -> Self::Owned {
match self {
Either::Left(left) => Either::Left(left.into_owned()),
Either::Right(right) => Either::Right(right.into_owned()),
}
}
}
@@ -255,25 +293,29 @@ where
{
type State = EitherKeepAliveState<A::State, B::State>;
fn build(self) -> Self::State {
fn build(self, extra_attrs: Option<Vec<AnyAttribute>>) -> Self::State {
let showing_b = self.show_b;
let a = self.a.map(Render::build);
let b = self.b.map(Render::build);
let a = self.a.map(|val| Render::build(val, extra_attrs.clone()));
let b = self.b.map(|val| Render::build(val, extra_attrs));
EitherKeepAliveState { a, b, showing_b }
}
fn rebuild(self, state: &mut Self::State) {
fn rebuild(
self,
state: &mut Self::State,
extra_attrs: Option<Vec<AnyAttribute>>,
) {
// set or update A -- `None` just means "no change"
match (self.a, &mut state.a) {
(Some(new), Some(old)) => new.rebuild(old),
(Some(new), None) => state.a = Some(new.build()),
(Some(new), Some(old)) => new.rebuild(old, extra_attrs.clone()),
(Some(new), None) => state.a = Some(new.build(extra_attrs.clone())),
_ => {}
}
// set or update B
match (self.b, &mut state.b) {
(Some(new), Some(old)) => new.rebuild(old),
(Some(new), None) => state.b = Some(new.build()),
(Some(new), Some(old)) => new.rebuild(old, extra_attrs),
(Some(new), None) => state.b = Some(new.build(extra_attrs)),
_ => {}
}
@@ -333,35 +375,58 @@ where
B: RenderHtml,
{
type AsyncOutput = EitherKeepAlive<A::AsyncOutput, B::AsyncOutput>;
type Owned = EitherKeepAlive<A::Owned, B::Owned>;
const MIN_LENGTH: usize = 0;
fn dry_resolve(&mut self) {
fn dry_resolve(&mut self, mut extra_attrs: ExtraAttrsMut<'_>) {
if let Some(inner) = &mut self.a {
inner.dry_resolve();
inner.dry_resolve(extra_attrs.as_deref_mut());
}
if let Some(inner) = &mut self.b {
inner.dry_resolve();
inner.dry_resolve(extra_attrs);
}
}
async fn resolve(self) -> Self::AsyncOutput {
async fn resolve(
self,
mut extra_attrs: ExtraAttrsMut<'_>,
) -> Self::AsyncOutput {
let EitherKeepAlive { a, b, show_b } = self;
let (a, b) = join(
async move {
match a {
Some(a) => Some(a.resolve().await),
None => None,
}
},
async move {
match b {
Some(b) => Some(b.resolve().await),
None => None,
}
},
)
.await;
// Has to be sequential if extra attrs are present:
let (a, b) = if extra_attrs.is_some() {
let a = match a {
Some(a) => Some(a.resolve(extra_attrs.as_deref_mut()).await),
None => None,
};
let b = match b {
Some(b) => Some(b.resolve(extra_attrs.as_deref_mut()).await),
None => None,
};
(a, b)
} else {
join(
async move {
match a {
Some(a) => {
Some(a.resolve(ExtraAttrsMut::default()).await)
}
None => None,
}
},
async move {
match b {
Some(b) => {
Some(b.resolve(ExtraAttrsMut::default()).await)
}
None => None,
}
},
)
.await
};
EitherKeepAlive { a, b, show_b }
}
@@ -371,15 +436,28 @@ where
position: &mut Position,
escape: bool,
mark_branches: bool,
extra_attrs: Option<Vec<AnyAttribute>>,
) {
if self.show_b {
self.b
.expect("rendering B to HTML without filling it")
.to_html_with_buf(buf, position, escape, mark_branches);
.to_html_with_buf(
buf,
position,
escape,
mark_branches,
extra_attrs,
);
} else {
self.a
.expect("rendering A to HTML without filling it")
.to_html_with_buf(buf, position, escape, mark_branches);
.to_html_with_buf(
buf,
position,
escape,
mark_branches,
extra_attrs,
);
}
}
@@ -389,6 +467,7 @@ where
position: &mut Position,
escape: bool,
mark_branches: bool,
extra_attrs: Option<Vec<AnyAttribute>>,
) where
Self: Sized,
{
@@ -400,6 +479,7 @@ where
position,
escape,
mark_branches,
extra_attrs,
);
} else {
self.a
@@ -409,6 +489,7 @@ where
position,
escape,
mark_branches,
extra_attrs,
);
}
}
@@ -417,25 +498,34 @@ where
self,
cursor: &Cursor,
position: &PositionState,
extra_attrs: Option<Vec<AnyAttribute>>,
) -> Self::State {
let showing_b = self.show_b;
let a = self.a.map(|a| {
if showing_b {
a.build()
a.build(extra_attrs.clone())
} else {
a.hydrate::<FROM_SERVER>(cursor, position)
a.hydrate::<FROM_SERVER>(cursor, position, extra_attrs.clone())
}
});
let b = self.b.map(|b| {
if showing_b {
b.hydrate::<FROM_SERVER>(cursor, position)
b.hydrate::<FROM_SERVER>(cursor, position, extra_attrs)
} else {
b.build()
b.build(extra_attrs)
}
});
EitherKeepAliveState { showing_b, a, b }
}
fn into_owned(self) -> Self::Owned {
EitherKeepAlive {
a: self.a.map(|a| a.into_owned()),
b: self.b.map(|b| b.into_owned()),
show_b: self.show_b,
}
}
}
impl<A, B> Mountable for EitherKeepAliveState<A, B>
@@ -535,20 +625,20 @@ macro_rules! tuples {
type State = [<EitherOf $num State>]<$($ty,)*>;
fn build(self) -> Self::State {
fn build(self, extra_attrs: Option<Vec<AnyAttribute>>) -> Self::State {
let state = match self {
$([<EitherOf $num>]::$ty(this) => [<EitherOf $num>]::$ty(this.build()),)*
$([<EitherOf $num>]::$ty(this) => [<EitherOf $num>]::$ty(this.build(extra_attrs)),)*
};
Self::State { state }
}
fn rebuild(self, state: &mut Self::State) {
fn rebuild(self, state: &mut Self::State, extra_attrs: Option<Vec<AnyAttribute>>) {
let new_state = match (self, &mut state.state) {
// rebuild same state and return early
$(([<EitherOf $num>]::$ty(new), [<EitherOf $num>]::$ty(old)) => { return new.rebuild(old); },)*
$(([<EitherOf $num>]::$ty(new), [<EitherOf $num>]::$ty(old)) => { return new.rebuild(old, extra_attrs); },)*
// or mount new state
$(([<EitherOf $num>]::$ty(new), _) => {
let mut new = new.build();
let mut new = new.build(extra_attrs);
state.insert_before_this(&mut new);
[<EitherOf $num>]::$ty(new)
},)*
@@ -592,38 +682,39 @@ macro_rules! tuples {
{
type AsyncOutput = [<EitherOf $num>]<$($ty::AsyncOutput,)*>;
type Owned = [<EitherOf $num>]<$($ty::Owned,)*>;
const MIN_LENGTH: usize = max_usize(&[$($ty ::MIN_LENGTH,)*]);
fn dry_resolve(&mut self) {
fn dry_resolve(&mut self, extra_attrs: ExtraAttrsMut<'_>) {
match self {
$([<EitherOf $num>]::$ty(this) => {
this.dry_resolve();
this.dry_resolve(extra_attrs);
})*
}
}
async fn resolve(self) -> Self::AsyncOutput {
async fn resolve(self, extra_attrs: ExtraAttrsMut<'_>) -> Self::AsyncOutput {
match self {
$([<EitherOf $num>]::$ty(this) => [<EitherOf $num>]::$ty(this.resolve().await),)*
$([<EitherOf $num>]::$ty(this) => [<EitherOf $num>]::$ty(this.resolve(extra_attrs).await),)*
}
}
#[inline(always)]
fn html_len(&self) -> usize {
fn html_len(&self, extra_attrs: Option<Vec<&AnyAttribute>>) -> usize {
match self {
$([<EitherOf $num>]::$ty(i) => i.html_len(),)*
$([<EitherOf $num>]::$ty(i) => i.html_len(extra_attrs),)*
}
}
fn to_html_with_buf(self, buf: &mut String, position: &mut Position, escape: bool, mark_branches: bool) {
fn to_html_with_buf(self, buf: &mut String, position: &mut Position, escape: bool, mark_branches: bool, extra_attrs: Option<Vec<AnyAttribute>>) {
match self {
$([<EitherOf $num>]::$ty(this) => {
if mark_branches {
buf.open_branch(stringify!($ty));
}
this.to_html_with_buf(buf, position, escape, mark_branches);
this.to_html_with_buf(buf, position, escape, mark_branches, extra_attrs);
if mark_branches {
buf.close_branch(stringify!($ty));
}
@@ -633,7 +724,7 @@ macro_rules! tuples {
fn to_html_async_with_buf<const OUT_OF_ORDER: bool>(
self,
buf: &mut StreamBuilder, position: &mut Position, escape: bool, mark_branches: bool) where
buf: &mut StreamBuilder, position: &mut Position, escape: bool, mark_branches: bool, extra_attrs: Option<Vec<AnyAttribute>>) where
Self: Sized,
{
match self {
@@ -641,7 +732,7 @@ macro_rules! tuples {
if mark_branches {
buf.open_branch(stringify!($ty));
}
this.to_html_async_with_buf::<OUT_OF_ORDER>(buf, position, escape, mark_branches);
this.to_html_async_with_buf::<OUT_OF_ORDER>(buf, position, escape, mark_branches, extra_attrs);
if mark_branches {
buf.close_branch(stringify!($ty));
}
@@ -653,15 +744,24 @@ macro_rules! tuples {
self,
cursor: &Cursor,
position: &PositionState,
extra_attrs: Option<Vec<AnyAttribute>>,
) -> Self::State {
let state = match self {
$([<EitherOf $num>]::$ty(this) => {
[<EitherOf $num>]::$ty(this.hydrate::<FROM_SERVER>(cursor, position))
[<EitherOf $num>]::$ty(this.hydrate::<FROM_SERVER>(cursor, position, extra_attrs))
})*
};
Self::State { state }
}
fn into_owned(self) -> Self::Owned {
match self {
$([<EitherOf $num>]::$ty(this) => {
[<EitherOf $num>]::$ty(this.into_owned())
})*
}
}
}
}
}

View File

@@ -1,6 +1,9 @@
use super::{add_attr::AddAnyAttr, Position, PositionState, RenderHtml};
use super::{
add_attr::AddAnyAttr, any_view::ExtraAttrsMut, Position, PositionState,
RenderHtml,
};
use crate::{
html::attribute::Attribute,
html::attribute::{any_attribute::AnyAttribute, Attribute},
hydration::Cursor,
ssr::StreamBuilder,
view::{iterators::OptionState, Mountable, Render},
@@ -16,19 +19,23 @@ where
{
type State = ResultState<T>;
fn build(self) -> Self::State {
fn build(self, extra_attrs: Option<Vec<AnyAttribute>>) -> Self::State {
let hook = throw_error::get_error_hook();
let (state, error) = match self {
Ok(view) => (Either::Left(view.build()), None),
Ok(view) => (Either::Left(view.build(extra_attrs)), None),
Err(e) => (
Either::Right(Render::build(())),
Either::Right(Render::build((), None)),
Some(throw_error::throw(e.into())),
),
};
ResultState { state, error, hook }
}
fn rebuild(self, state: &mut Self::State) {
fn rebuild(
self,
state: &mut Self::State,
extra_attrs: Option<Vec<AnyAttribute>>,
) {
let _guard = state.hook.clone().map(throw_error::set_error_hook);
match (&mut state.state, self) {
// both errors: throw the new error and replace
@@ -37,11 +44,11 @@ where
}
// both Ok: need to rebuild child
(Either::Left(old), Ok(new)) => {
T::rebuild(new, old);
T::rebuild(new, old, extra_attrs);
}
// Ok => Err: unmount, replace with marker, and throw
(Either::Left(old), Err(err)) => {
let mut new_state = Render::build(());
let mut new_state = Render::build((), None);
old.insert_before_this(&mut new_state);
old.unmount();
state.state = Either::Right(new_state);
@@ -52,7 +59,7 @@ where
if let Some(err) = state.error.take() {
throw_error::clear(&err);
}
let mut new_state = new.build();
let mut new_state = new.build(extra_attrs);
old.insert_before_this(&mut new_state);
old.unmount();
state.state = Either::Left(new_state);
@@ -132,25 +139,29 @@ where
E: Into<AnyError> + Send + 'static,
{
type AsyncOutput = Result<T::AsyncOutput, E>;
type Owned = Result<T::Owned, E>;
const MIN_LENGTH: usize = T::MIN_LENGTH;
fn dry_resolve(&mut self) {
fn dry_resolve(&mut self, extra_attrs: ExtraAttrsMut<'_>) {
if let Ok(inner) = self.as_mut() {
inner.dry_resolve()
inner.dry_resolve(extra_attrs)
}
}
async fn resolve(self) -> Self::AsyncOutput {
async fn resolve(
self,
extra_attrs: ExtraAttrsMut<'_>,
) -> Self::AsyncOutput {
match self {
Ok(view) => Ok(view.resolve().await),
Ok(view) => Ok(view.resolve(extra_attrs).await),
Err(e) => Err(e),
}
}
fn html_len(&self) -> usize {
fn html_len(&self, extra_attrs: Option<Vec<&AnyAttribute>>) -> usize {
match self {
Ok(i) => i.html_len() + 3,
Ok(i) => i.html_len(extra_attrs) + 3,
Err(_) => 0,
}
}
@@ -161,11 +172,16 @@ where
position: &mut super::Position,
escape: bool,
mark_branches: bool,
extra_attrs: Option<Vec<AnyAttribute>>,
) {
match self {
Ok(inner) => {
inner.to_html_with_buf(buf, position, escape, mark_branches)
}
Ok(inner) => inner.to_html_with_buf(
buf,
position,
escape,
mark_branches,
extra_attrs,
),
Err(e) => {
buf.push_str("<!>");
throw_error::throw(e);
@@ -179,6 +195,7 @@ where
position: &mut Position,
escape: bool,
mark_branches: bool,
extra_attrs: Option<Vec<AnyAttribute>>,
) where
Self: Sized,
{
@@ -188,6 +205,7 @@ where
position,
escape,
mark_branches,
extra_attrs,
),
Err(e) => {
buf.push_sync("<!>");
@@ -200,19 +218,35 @@ where
self,
cursor: &Cursor,
position: &PositionState,
extra_attrs: Option<Vec<AnyAttribute>>,
) -> Self::State {
let hook = throw_error::get_error_hook();
let (state, error) = match self {
Ok(view) => (
Either::Left(view.hydrate::<FROM_SERVER>(cursor, position)),
Either::Left(view.hydrate::<FROM_SERVER>(
cursor,
position,
extra_attrs,
)),
None,
),
Err(e) => {
let state =
RenderHtml::hydrate::<FROM_SERVER>((), cursor, position);
let state = RenderHtml::hydrate::<FROM_SERVER>(
(),
cursor,
position,
extra_attrs,
);
(Either::Right(state), Some(throw_error::throw(e.into())))
}
};
ResultState { state, error, hook }
}
fn into_owned(self) -> Self::Owned {
match self {
Ok(view) => Ok(view.into_owned()),
Err(e) => Err(e),
}
}
}

View File

@@ -1,9 +1,12 @@
use super::{
add_attr::AddAnyAttr, Mountable, Position, PositionState, Render,
RenderHtml,
add_attr::AddAnyAttr, any_view::ExtraAttrsMut,
batch_resolve_items_with_extra_attrs, Mountable, Position, PositionState,
Render, RenderHtml,
};
use crate::{
html::attribute::Attribute, hydration::Cursor, renderer::Rndr,
html::attribute::{any_attribute::AnyAttribute, Attribute},
hydration::Cursor,
renderer::Rndr,
ssr::StreamBuilder,
};
use either_of::Either;
@@ -18,20 +21,24 @@ where
{
type State = OptionState<T>;
fn build(self) -> Self::State {
fn build(self, extra_attrs: Option<Vec<AnyAttribute>>) -> Self::State {
match self {
Some(value) => Either::Left(value),
None => Either::Right(()),
}
.build()
.build(extra_attrs)
}
fn rebuild(self, state: &mut Self::State) {
fn rebuild(
self,
state: &mut Self::State,
extra_attrs: Option<Vec<AnyAttribute>>,
) {
match self {
Some(value) => Either::Left(value),
None => Either::Right(()),
}
.rebuild(state)
.rebuild(state, extra_attrs)
}
}
@@ -58,25 +65,29 @@ where
T: RenderHtml,
{
type AsyncOutput = Option<T::AsyncOutput>;
type Owned = Option<T::Owned>;
const MIN_LENGTH: usize = T::MIN_LENGTH;
fn dry_resolve(&mut self) {
fn dry_resolve(&mut self, extra_attrs: ExtraAttrsMut<'_>) {
if let Some(inner) = self.as_mut() {
inner.dry_resolve();
inner.dry_resolve(extra_attrs);
}
}
async fn resolve(self) -> Self::AsyncOutput {
async fn resolve(
self,
extra_attrs: ExtraAttrsMut<'_>,
) -> Self::AsyncOutput {
match self {
None => None,
Some(value) => Some(value.resolve().await),
Some(value) => Some(value.resolve(extra_attrs).await),
}
}
fn html_len(&self) -> usize {
fn html_len(&self, extra_attrs: Option<Vec<&AnyAttribute>>) -> usize {
match self {
Some(i) => i.html_len() + 3,
Some(i) => i.html_len(extra_attrs) + 3,
None => 3,
}
}
@@ -87,12 +98,19 @@ where
position: &mut Position,
escape: bool,
mark_branches: bool,
extra_attrs: Option<Vec<AnyAttribute>>,
) {
match self {
Some(value) => Either::Left(value),
None => Either::Right(()),
}
.to_html_with_buf(buf, position, escape, mark_branches)
.to_html_with_buf(
buf,
position,
escape,
mark_branches,
extra_attrs,
)
}
fn to_html_async_with_buf<const OUT_OF_ORDER: bool>(
@@ -101,6 +119,7 @@ where
position: &mut Position,
escape: bool,
mark_branches: bool,
extra_attrs: Option<Vec<AnyAttribute>>,
) where
Self: Sized,
{
@@ -113,6 +132,7 @@ where
position,
escape,
mark_branches,
extra_attrs,
)
}
@@ -121,12 +141,17 @@ where
self,
cursor: &Cursor,
position: &PositionState,
extra_attrs: Option<Vec<AnyAttribute>>,
) -> Self::State {
match self {
Some(value) => Either::Left(value),
None => Either::Right(()),
}
.hydrate::<FROM_SERVER>(cursor, position)
.hydrate::<FROM_SERVER>(cursor, position, extra_attrs)
}
fn into_owned(self) -> Self::Owned {
self.map(RenderHtml::into_owned)
}
}
@@ -136,20 +161,27 @@ where
{
type State = VecState<T::State>;
fn build(self) -> Self::State {
fn build(self, extra_attrs: Option<Vec<AnyAttribute>>) -> Self::State {
let marker = Rndr::create_placeholder();
VecState {
states: self.into_iter().map(T::build).collect(),
states: self
.into_iter()
.map(|val| T::build(val, extra_attrs.clone()))
.collect(),
marker,
}
}
fn rebuild(self, state: &mut Self::State) {
fn rebuild(
self,
state: &mut Self::State,
extra_attrs: Option<Vec<AnyAttribute>>,
) {
let VecState { states, marker } = state;
let old = states;
// this is an unkeyed diff
if old.is_empty() {
let mut new = self.build().states;
let mut new = self.build(extra_attrs).states;
for item in new.iter_mut() {
Rndr::mount_before(item, marker.as_ref());
}
@@ -166,10 +198,10 @@ where
for item in self.into_iter().zip_longest(old.iter_mut()) {
match item {
itertools::EitherOrBoth::Both(new, old) => {
T::rebuild(new, old)
T::rebuild(new, old, extra_attrs.clone())
}
itertools::EitherOrBoth::Left(new) => {
let mut new_state = new.build();
let mut new_state = new.build(extra_attrs.clone());
Rndr::mount_before(&mut new_state, marker.as_ref());
adds.push(new_state);
}
@@ -255,24 +287,31 @@ where
T: RenderHtml,
{
type AsyncOutput = Vec<T::AsyncOutput>;
type Owned = Vec<T::Owned>;
const MIN_LENGTH: usize = 0;
fn dry_resolve(&mut self) {
fn dry_resolve(&mut self, mut extra_attrs: ExtraAttrsMut<'_>) {
for inner in self.iter_mut() {
inner.dry_resolve();
inner.dry_resolve(extra_attrs.as_deref_mut());
}
}
async fn resolve(self) -> Self::AsyncOutput {
futures::future::join_all(self.into_iter().map(T::resolve))
async fn resolve(
self,
extra_attrs: ExtraAttrsMut<'_>,
) -> Self::AsyncOutput {
batch_resolve_items_with_extra_attrs(self, extra_attrs)
.await
.into_iter()
.collect()
}
fn html_len(&self) -> usize {
self.iter().map(|n| n.html_len()).sum::<usize>() + 3
fn html_len(&self, extra_attrs: Option<Vec<&AnyAttribute>>) -> usize {
self.iter()
.map(|n| n.html_len(extra_attrs.clone()))
.sum::<usize>()
+ 3
}
fn to_html_with_buf(
@@ -281,13 +320,26 @@ where
position: &mut Position,
escape: bool,
mark_branches: bool,
extra_attrs: Option<Vec<AnyAttribute>>,
) {
let mut children = self.into_iter();
if let Some(first) = children.next() {
first.to_html_with_buf(buf, position, escape, mark_branches);
first.to_html_with_buf(
buf,
position,
escape,
mark_branches,
extra_attrs.clone(),
);
}
for child in children {
child.to_html_with_buf(buf, position, escape, mark_branches);
child.to_html_with_buf(
buf,
position,
escape,
mark_branches,
extra_attrs.clone(),
);
}
buf.push_str("<!>");
}
@@ -298,6 +350,7 @@ where
position: &mut Position,
escape: bool,
mark_branches: bool,
extra_attrs: Option<Vec<AnyAttribute>>,
) where
Self: Sized,
{
@@ -308,6 +361,7 @@ where
position,
escape,
mark_branches,
extra_attrs.clone(),
);
}
for child in children {
@@ -316,6 +370,7 @@ where
position,
escape,
mark_branches,
extra_attrs.clone(),
);
}
buf.push_sync("<!>");
@@ -325,16 +380,27 @@ where
self,
cursor: &Cursor,
position: &PositionState,
extra_attrs: Option<Vec<AnyAttribute>>,
) -> Self::State {
let states = self
.into_iter()
.map(|child| child.hydrate::<FROM_SERVER>(cursor, position))
.map(|child| {
child.hydrate::<FROM_SERVER>(
cursor,
position,
extra_attrs.clone(),
)
})
.collect();
let marker = cursor.next_placeholder(position);
VecState { states, marker }
}
fn into_owned(self) -> Self::Owned {
self.into_iter().map(RenderHtml::into_owned).collect()
}
}
impl<T, const N: usize> Render for [T; N]
@@ -343,19 +409,23 @@ where
{
type State = ArrayState<T::State, N>;
fn build(self) -> Self::State {
fn build(self, extra_attrs: Option<Vec<AnyAttribute>>) -> Self::State {
Self::State {
states: self.map(T::build),
states: self.map(|val| T::build(val, extra_attrs.clone())),
}
}
fn rebuild(self, state: &mut Self::State) {
fn rebuild(
self,
state: &mut Self::State,
extra_attrs: Option<Vec<AnyAttribute>>,
) {
let Self::State { states } = state;
let old = states;
// this is an unkeyed diff
self.into_iter()
.zip(old.iter_mut())
.for_each(|(new, old)| T::rebuild(new, old));
.for_each(|(new, old)| T::rebuild(new, old, extra_attrs.clone()));
}
}
@@ -417,17 +487,21 @@ where
T: RenderHtml,
{
type AsyncOutput = [T::AsyncOutput; N];
type Owned = Vec<T::Owned>;
const MIN_LENGTH: usize = 0;
fn dry_resolve(&mut self) {
fn dry_resolve(&mut self, mut extra_attrs: ExtraAttrsMut<'_>) {
for inner in self.iter_mut() {
inner.dry_resolve();
inner.dry_resolve(extra_attrs.as_deref_mut());
}
}
async fn resolve(self) -> Self::AsyncOutput {
futures::future::join_all(self.into_iter().map(T::resolve))
async fn resolve(
self,
extra_attrs: ExtraAttrsMut<'_>,
) -> Self::AsyncOutput {
batch_resolve_items_with_extra_attrs(self, extra_attrs)
.await
.into_iter()
.collect::<Vec<_>>()
@@ -435,8 +509,10 @@ where
.unwrap_or_else(|_| unreachable!())
}
fn html_len(&self) -> usize {
self.iter().map(RenderHtml::html_len).sum::<usize>()
fn html_len(&self, extra_attrs: Option<Vec<&AnyAttribute>>) -> usize {
self.iter()
.map(|val| RenderHtml::html_len(val, extra_attrs.clone()))
.sum::<usize>()
}
fn to_html_with_buf(
@@ -445,9 +521,16 @@ where
position: &mut Position,
escape: bool,
mark_branches: bool,
extra_attrs: Option<Vec<AnyAttribute>>,
) {
for child in self.into_iter() {
child.to_html_with_buf(buf, position, escape, mark_branches);
child.to_html_with_buf(
buf,
position,
escape,
mark_branches,
extra_attrs.clone(),
);
}
}
@@ -457,6 +540,7 @@ where
position: &mut Position,
escape: bool,
mark_branches: bool,
extra_attrs: Option<Vec<AnyAttribute>>,
) where
Self: Sized,
{
@@ -466,6 +550,7 @@ where
position,
escape,
mark_branches,
extra_attrs.clone(),
);
}
}
@@ -474,9 +559,17 @@ where
self,
cursor: &Cursor,
position: &PositionState,
extra_attrs: Option<Vec<AnyAttribute>>,
) -> Self::State {
let states =
self.map(|child| child.hydrate::<FROM_SERVER>(cursor, position));
let states = self.map(|child| {
child.hydrate::<FROM_SERVER>(cursor, position, extra_attrs.clone())
});
ArrayState { states }
}
fn into_owned(self) -> Self::Owned {
self.into_iter()
.map(RenderHtml::into_owned)
.collect::<Vec<_>>()
}
}

View File

@@ -1,9 +1,10 @@
use super::{
add_attr::AddAnyAttr, Mountable, Position, PositionState, Render,
RenderHtml,
add_attr::AddAnyAttr, any_view::ExtraAttrsMut,
batch_resolve_items_with_extra_attrs, Mountable, Position, PositionState,
Render, RenderHtml,
};
use crate::{
html::attribute::Attribute,
html::attribute::{any_attribute::AnyAttribute, Attribute},
hydration::Cursor,
renderer::{CastFrom, Rndr},
ssr::StreamBuilder,
@@ -75,7 +76,7 @@ where
type State = KeyedState<K, VFS, V>;
// TODO fallible state and try_build()/try_rebuild() here
fn build(self) -> Self::State {
fn build(self, extra_attrs: Option<Vec<AnyAttribute>>) -> Self::State {
let items = self.items.into_iter();
let (capacity, _) = items.size_hint();
let mut hashed_items =
@@ -84,7 +85,8 @@ where
for (index, item) in items.enumerate() {
hashed_items.insert((self.key_fn)(&item));
let (set_index, view) = (self.view_fn)(index, item);
rendered_items.push(Some((set_index, view.build())));
rendered_items
.push(Some((set_index, view.build(extra_attrs.clone()))));
}
KeyedState {
parent: None,
@@ -94,7 +96,11 @@ where
}
}
fn rebuild(self, state: &mut Self::State) {
fn rebuild(
self,
state: &mut Self::State,
extra_attrs: Option<Vec<AnyAttribute>>,
) {
let KeyedState {
parent,
marker,
@@ -123,6 +129,7 @@ where
rendered_items,
&self.view_fn,
items,
extra_attrs,
);
*hashed_items = new_hashed_items;
@@ -131,9 +138,9 @@ where
impl<T, I, K, KF, VF, VFS, V> AddAnyAttr for Keyed<T, I, K, KF, VF, VFS, V>
where
I: IntoIterator<Item = T> + Send,
I: IntoIterator<Item = T> + Send + 'static,
K: Eq + Hash + 'static,
KF: Fn(&T) -> K + Send,
KF: Fn(&T) -> K + Send + 'static,
V: RenderHtml,
V: 'static,
VF: Fn(usize, T) -> (VFS, V) + Send + 'static,
@@ -184,29 +191,38 @@ where
impl<T, I, K, KF, VF, VFS, V> RenderHtml for Keyed<T, I, K, KF, VF, VFS, V>
where
I: IntoIterator<Item = T> + Send,
I: IntoIterator<Item = T> + Send + 'static,
K: Eq + Hash + 'static,
KF: Fn(&T) -> K + Send,
KF: Fn(&T) -> K + Send + 'static,
V: RenderHtml + 'static,
VF: Fn(usize, T) -> (VFS, V) + Send + 'static,
VFS: Fn(usize) + 'static,
T: 'static,
{
type AsyncOutput = Vec<V::AsyncOutput>; // TODO
type Owned = Keyed<T, I, K, KF, VF, VFS, V>;
const MIN_LENGTH: usize = 0;
fn dry_resolve(&mut self) {
fn dry_resolve(&mut self, _extra_attrs: ExtraAttrsMut<'_>) {
// TODO...
}
async fn resolve(self) -> Self::AsyncOutput {
futures::future::join_all(self.items.into_iter().enumerate().map(
|(index, item)| {
let (_, view) = (self.view_fn)(index, item);
view.resolve()
},
))
async fn resolve(
self,
extra_attrs: ExtraAttrsMut<'_>,
) -> Self::AsyncOutput {
batch_resolve_items_with_extra_attrs(
self.items
.into_iter()
.enumerate()
.map(|(index, item)| {
let (_, view) = (self.view_fn)(index, item);
view
})
.collect::<Vec<_>>(),
extra_attrs,
)
.await
.into_iter()
.collect::<Vec<_>>()
@@ -218,10 +234,17 @@ where
position: &mut Position,
escape: bool,
mark_branches: bool,
extra_attrs: Option<Vec<AnyAttribute>>,
) {
for (index, item) in self.items.into_iter().enumerate() {
let (_, item) = (self.view_fn)(index, item);
item.to_html_with_buf(buf, position, escape, mark_branches);
item.to_html_with_buf(
buf,
position,
escape,
mark_branches,
extra_attrs.clone(),
);
*position = Position::NextChild;
}
buf.push_str("<!>");
@@ -233,6 +256,7 @@ where
position: &mut Position,
escape: bool,
mark_branches: bool,
extra_attrs: Option<Vec<AnyAttribute>>,
) {
for (index, item) in self.items.into_iter().enumerate() {
let (_, item) = (self.view_fn)(index, item);
@@ -241,6 +265,7 @@ where
position,
escape,
mark_branches,
extra_attrs.clone(),
);
*position = Position::NextChild;
}
@@ -251,6 +276,7 @@ where
self,
cursor: &Cursor,
position: &PositionState,
extra_attrs: Option<Vec<AnyAttribute>>,
) -> Self::State {
// get parent and position
let current = cursor.current();
@@ -272,7 +298,11 @@ where
for (index, item) in items.enumerate() {
hashed_items.insert((self.key_fn)(&item));
let (set_index, view) = (self.view_fn)(index, item);
let item = view.hydrate::<FROM_SERVER>(cursor, position);
let item = view.hydrate::<FROM_SERVER>(
cursor,
position,
extra_attrs.clone(),
);
rendered_items.push(Some((set_index, item)));
}
let marker = cursor.next_placeholder(position);
@@ -283,6 +313,10 @@ where
rendered_items,
}
}
fn into_owned(self) -> Self::Owned {
self
}
}
impl<K, VFS, V> Mountable for KeyedState<K, VFS, V>
@@ -510,6 +544,7 @@ fn apply_diff<T, VFS, V>(
children: &mut Vec<Option<(VFS, V::State)>>,
view_fn: impl Fn(usize, T) -> (VFS, V),
mut items: Vec<Option<T>>,
extra_attrs: Option<Vec<AnyAttribute>>,
) where
VFS: Fn(usize),
V: Render,
@@ -583,7 +618,7 @@ fn apply_diff<T, VFS, V>(
for DiffOpAdd { at, mode } in add_cmds {
let item = items[at].take().unwrap();
let (set_index, item) = view_fn(at, item);
let mut item = item.build();
let mut item = item.build(extra_attrs.clone());
match mode {
DiffOpAddMode::Normal => {
@@ -694,7 +729,7 @@ mod tests {
#[test]
fn keyed_creates_list() {
let el = ul((), keyed(1..=3, |k| *k, item));
let el_state = el.build();
let el_state = el.build(None);
assert_eq!(
el_state.el.to_debug_html(),
"<ul><li>1</li><li>2</li><li>3</li></ul>"
@@ -704,7 +739,7 @@ mod tests {
#[test]
fn adding_items_updates_list() {
let el = ul((), keyed(1..=3, |k| *k, item));
let mut el_state = el.build();
let mut el_state = el.build(None);
let el = ul((), keyed(1..=5, |k| *k, item));
el.rebuild(&mut el_state);
assert_eq!(
@@ -716,7 +751,7 @@ mod tests {
#[test]
fn removing_items_updates_list() {
let el = ul((), keyed(1..=3, |k| *k, item));
let mut el_state = el.build();
let mut el_state = el.build(None);
let el = ul((), keyed(1..=2, |k| *k, item));
el.rebuild(&mut el_state);
assert_eq!(
@@ -728,7 +763,7 @@ mod tests {
#[test]
fn swapping_items_updates_list() {
let el = ul((), keyed([1, 2, 3, 4, 5], |k| *k, item));
let mut el_state = el.build();
let mut el_state = el.build(None);
let el = ul((), keyed([1, 4, 3, 2, 5], |k| *k, item));
el.rebuild(&mut el_state);
assert_eq!(
@@ -740,7 +775,7 @@ mod tests {
#[test]
fn swapping_and_removing_orders_correctly() {
let el = ul((), keyed([1, 2, 3, 4, 5], |k| *k, item));
let mut el_state = el.build();
let mut el_state = el.build(None);
let el = ul((), keyed([1, 4, 3, 5], |k| *k, item));
el.rebuild(&mut el_state);
assert_eq!(
@@ -752,7 +787,7 @@ mod tests {
#[test]
fn arbitrarily_hard_adjustment() {
let el = ul((), keyed([1, 2, 3, 4, 5], |k| *k, item));
let mut el_state = el.build();
let mut el_state = el.build(None);
let el = ul((), keyed([2, 4, 3], |k| *k, item));
el.rebuild(&mut el_state);
assert_eq!(
@@ -764,7 +799,7 @@ mod tests {
#[test]
fn a_series_of_moves() {
let el = ul((), keyed([1, 2, 3, 4, 5], |k| *k, item));
let mut el_state = el.build();
let mut el_state = el.build(None);
let el = ul((), keyed([2, 4, 3], |k| *k, item));
el.rebuild(&mut el_state);
let el = ul((), keyed([1, 7, 5, 11, 13, 17], |k| *k, item));
@@ -784,7 +819,7 @@ mod tests {
#[test]
fn clearing_works() {
let el = ul((), keyed([1, 2, 3, 4, 5], |k| *k, item));
let mut el_state = el.build();
let mut el_state = el.build(None);
let el = ul((), keyed([], |k| *k, item));
el.rebuild(&mut el_state);
assert_eq!(el_state.el.to_debug_html(), "<ul></ul>");

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