From 1472aaa0a389561146ab241041043a5f8d46b063 Mon Sep 17 00:00:00 2001 From: SleeplessOne1917 Date: Fri, 12 Jan 2024 20:59:34 -0500 Subject: [PATCH] Work on handling server fns --- .../action-form-error-handling/src/app.rs | 3 +- integrations/actix/src/lib.rs | 157 +++++++++++------- integrations/axum/src/lib.rs | 2 +- integrations/utils/src/lib.rs | 73 +++++--- integrations/viz/src/lib.rs | 2 +- leptos/src/lib.rs | 5 +- router/src/components/form.rs | 30 ++-- server_fn/src/error.rs | 39 ----- server_fn/src/lib.rs | 46 ++++- 9 files changed, 215 insertions(+), 142 deletions(-) diff --git a/examples/action-form-error-handling/src/app.rs b/examples/action-form-error-handling/src/app.rs index 6989756fcc..ab935e4485 100644 --- a/examples/action-form-error-handling/src/app.rs +++ b/examples/action-form-error-handling/src/app.rs @@ -59,11 +59,12 @@ fn HomePage() -> impl IntoView { .to_string()) > {value} + - + } } diff --git a/integrations/actix/src/lib.rs b/integrations/actix/src/lib.rs index 6301861618..54b9311c81 100644 --- a/integrations/actix/src/lib.rs +++ b/integrations/actix/src/lib.rs @@ -20,13 +20,12 @@ use actix_web::{ use futures::{Stream, StreamExt}; use leptos::{ leptos_server::{server_fn_by_path, Payload}, - server_fn::{Encoding, query_to_errors}, + server_fn::Encoding, ssr::render_to_stream_with_prefix_undisposed_with_context_and_block_replacement, *, }; use leptos_integration_utils::{ - build_async_response, html_parts_separated, - referrer_to_url, WithServerFn, + build_async_response, html_parts_separated, referrer_to_url, WithServerFn, }; use leptos_meta::*; use leptos_router::*; @@ -36,7 +35,7 @@ use std::{ fmt::{Debug, Display}, future::Future, pin::Pin, - sync::Arc, + sync::{Arc, OnceLock}, }; #[cfg(debug_assertions)] use tracing::instrument; @@ -161,6 +160,8 @@ pub fn handle_server_fns() -> Route { handle_server_fns_with_context(|| {}) } +static REGEX_CELL: OnceLock = OnceLock::new(); + /// An Actix [struct@Route](actix_web::Route) that listens for `GET` or `POST` requests with /// Leptos server function arguments in the URL (`GET`) or body (`POST`), /// runs the server function if found, and returns the resulting [HttpResponse]. @@ -199,6 +200,20 @@ pub fn handle_server_fns_with_context( if let Some(server_fn) = server_fn_by_path(fn_name.as_str()) { let body_ref: &[u8] = &body; + let wasm_loaded = REGEX_CELL + .get_or_init(|| { + Regex::new(&format!( + "{WASM_LOADED_NAME}=(true|false)" + )) + .expect("Could not parse wasm loaded regex") + }) + .captures(std::str::from_utf8(body_ref).unwrap_or("")) + .map_or(true, |capture| { + capture + .get(1) + .map_or(true, |s| s.as_str() == "true") + }); + let runtime = create_runtime(); // Add additional info to the context of the server function @@ -230,8 +245,6 @@ pub fn handle_server_fns_with_context( Encoding::GetJSON | Encoding::GetCBOR => query, }; - leptos::logging::log!("In server fn before resp with data = {data:?}"); - let res = match server_fn.call((), data).await { Ok(serialized) => { let res_options = @@ -241,81 +254,104 @@ pub fn handle_server_fns_with_context( HttpResponse::Ok(); let res_parts = res_options.0.write(); - // if accept_header isn't set to one of these, it's a form submit - // redirect back to the referer if not redirect has been set - if accept_header != Some("application/json") - && accept_header - != Some("application/x-www-form-urlencoded") - && accept_header != Some("application/cbor") - { - leptos::logging::log!("Will redirect for form submit"); - // Location will already be set if redirect() has been used - let has_location_set = res_parts - .headers - .get(header::LOCATION) - .is_some(); - if !has_location_set { - let referer = req - .headers() - .get(header::REFERER) - .and_then(|value| value.to_str().ok()) - .unwrap_or("/"); - res = HttpResponse::SeeOther(); - res.insert_header(( - header::LOCATION, - referer, - )) - .content_type("application/json"); - } - } // Override StatusCode if it was set in a Resource or Element if let Some(status) = res_parts.status { res.status(status); } // Use provided ResponseParts headers if they exist - let _count = res_parts - .headers - .clone() - .into_iter() - .map(|(k, v)| { - res.append_header((k, v)); - }) - .count(); + for (k, v) in res_parts.headers.clone() { + res.append_header((k, v)); + } + + leptos::logging::log!( + "server fn serialized = {serialized:?}" + ); match serialized { Payload::Binary(data) => { - leptos::logging::log!("serverfn return bin = {data:?}"); res.content_type("application/cbor"); res.body(Bytes::from(data)) } Payload::Url(data) => { - leptos::logging::log!("serverfn return url = {data:?}"); res.content_type( "application/x-www-form-urlencoded", ); + + // if accept_header isn't set to one of these, it's a form submit + // redirect back to the referer if not redirect has been set + if !(wasm_loaded + || matches!( + accept_header, + Some( + "application/json" + | "application/\ + x-www-form-urlencoded" + | "application/cbor" + ) + )) + { + // Location will already be set if redirect() has been used + let has_location_set = res_parts + .headers + .get(header::LOCATION) + .is_some(); + if !has_location_set { + let referer = req + .headers() + .get(header::REFERER) + .and_then(|value| { + value.to_str().ok() + }) + .unwrap_or("/"); + let location = referrer_to_url( + referer, &fn_name, + ); + leptos::logging::log!( + "Form submit referrer = \ + {referer:?}" + ); + res = HttpResponse::SeeOther(); + res.insert_header(( + header::LOCATION, + location + .with_server_fn_success( + &data, &fn_name, + ) + .as_str(), + )) + .content_type("application/json"); + } + } + res.body(data) } Payload::Json(data) => { - leptos::logging::log!("serverfn return json = {data:?}"); res.content_type("application/json"); res.body(data) } } } Err(e) => { - let url = req - .headers() - .get(header::REFERER) - .and_then(|referrer| { - referrer_to_url(referrer, fn_name.as_str()) - }); - - if let Some(url) = url { + if !wasm_loaded { + leptos::logging::log!( + "In err with WASM loaded" + ); + let referer = req + .headers() + .get(header::REFERER) + .and_then(|value| value.to_str().ok()) + .unwrap_or("/"); + let url = referrer_to_url(referer, &fn_name); + + leptos::logging::log!( + "In err with WASM loaded and url = {url}" + ); + HttpResponse::SeeOther() .insert_header(( header::LOCATION, - url.with_server_fn( + url.with_server_fn_error( &e, fn_name.as_str(), ) @@ -330,7 +366,7 @@ pub fn handle_server_fns_with_context( } } }; - leptos::logging::log!("done serverfn with status {}", res.status()); + // clean up the scope runtime.dispose(); res @@ -768,12 +804,13 @@ fn provide_contexts(req: &HttpRequest, res_options: ResponseOptions) { provide_context(MetaContext::new()); provide_context(res_options); provide_context(req.clone()); - if let Some(query) = req.uri().query() { - leptos::logging::log!("query = {query}"); - provide_context(query_to_errors( - query - )); - } + // TODO: Fix + // if let Some(query) = req.uri().query() { + // leptos::logging::log!("query = {query}"); + // provide_context(query_to_responses( + // query + // )); + // } provide_server_redirect(redirect); #[cfg(feature = "nonce")] diff --git a/integrations/axum/src/lib.rs b/integrations/axum/src/lib.rs index d442d3780a..57b3390575 100644 --- a/integrations/axum/src/lib.rs +++ b/integrations/axum/src/lib.rs @@ -388,7 +388,7 @@ async fn handle_server_fns_inner( .status(StatusCode::SEE_OTHER) .header( header::LOCATION, - referer.with_server_fn(&e, fn_name.as_str()).as_str(), + referer.with_server_fn_error(&e, fn_name.as_str()).as_str(), ) .body(Default::default()) } else { diff --git a/integrations/utils/src/lib.rs b/integrations/utils/src/lib.rs index 0ddd7a628d..fca64f2fec 100644 --- a/integrations/utils/src/lib.rs +++ b/integrations/utils/src/lib.rs @@ -1,12 +1,13 @@ use futures::{Stream, StreamExt}; -use http::HeaderValue; use leptos::{ - nonce::use_nonce, server_fn::error::ServerFnUrlError, use_context, - RuntimeId, ServerFnError, + nonce::use_nonce, server_fn::ServerFnUrlResponse, use_context, RuntimeId, + ServerFnError, }; use leptos_config::LeptosOptions; use leptos_meta::MetaContext; use regex::Regex; +use serde::{Deserialize, Serialize}; +use std::{cmp::Eq, hash::Hash}; use url::Url; extern crate tracing; @@ -164,31 +165,63 @@ pub async fn build_async_response( format!("{head}{buf}{tail}") } -pub fn referrer_to_url(referer: &HeaderValue, fn_name: &str) -> Option { +pub fn referrer_to_url(referer: &str, fn_name: &str) -> Url { Url::parse( &Regex::new(&format!(r"(?:\?|&)?server_fn_error_{fn_name}=[^&]+")) .unwrap() - .replace(referer.to_str().ok()?, ""), + .replace(referer, ""), ) - .ok() + .expect("Could not parse URL") } -pub trait WithServerFn { - fn with_server_fn(self, error: &ServerFnError, fn_name: &str) -> Self; +pub trait WithServerFn<'de, T> +where + T: Clone + Deserialize<'de> + Hash + Eq + Serialize, +{ + fn with_server_fn_error(self, error: &ServerFnError, fn_name: &str) + -> Self; + fn with_server_fn_success(self, data: &T, fn_name: &str) -> Self; } -impl WithServerFn for Url { - fn with_server_fn(mut self, error: &ServerFnError, fn_name: &str) -> Self { - self.query_pairs_mut().append_pair( - format!("server_fn_error_{fn_name}").as_str(), - serde_qs::to_string(&ServerFnUrlError::new( - fn_name.to_owned(), - error.to_owned(), - )) - .expect("Could not serialize server fn error!") - .as_str(), - ); +impl<'de, T> WithServerFn<'de, T> for Url +where + T: Clone + Deserialize<'de> + Hash + Eq + Serialize, +{ + fn with_server_fn_error( + self, + error: &ServerFnError, + fn_name: &str, + ) -> Self { + modify_server_fn_response( + self, + ServerFnUrlResponse::::from_error(fn_name, error.clone()), + fn_name, + ) + } - self + fn with_server_fn_success(self, data: &T, fn_name: &str) -> Self { + modify_server_fn_response( + self, + ServerFnUrlResponse::new(fn_name, data.clone()), + fn_name, + ) } } + +fn modify_server_fn_response<'de, T>( + mut url: Url, + res: ServerFnUrlResponse, + fn_name: &str, +) -> Url +where + T: Clone + Deserialize<'de> + Hash + Eq + Serialize, +{ + url.query_pairs_mut().append_pair( + format!("server_fn_response_{fn_name}").as_str(), + serde_qs::to_string(&res) + .expect("Could not serialize server fn response!") + .as_str(), + ); + + url +} diff --git a/integrations/viz/src/lib.rs b/integrations/viz/src/lib.rs index 7a5c48a043..04828ef1a4 100644 --- a/integrations/viz/src/lib.rs +++ b/integrations/viz/src/lib.rs @@ -310,7 +310,7 @@ async fn handle_server_fns_inner( .status(StatusCode::SEE_OTHER) .header( header::LOCATION, - url.with_server_fn(&e, fn_name.as_str()).as_str(), + url.with_server_fn_error(&e, fn_name.as_str()).as_str(), ) .body(Default::default()) } else { diff --git a/leptos/src/lib.rs b/leptos/src/lib.rs index b48bf2622a..5c32d8a1fc 100644 --- a/leptos/src/lib.rs +++ b/leptos/src/lib.rs @@ -187,7 +187,7 @@ pub use leptos_server::{ create_server_multi_action, Action, MultiAction, ServerFn, ServerFnError, ServerFnErrorErr, }; -pub use server_fn::{self, query_to_errors, ServerFn as _}; +pub use server_fn::{self, query_to_responses, ServerFn as _}; mod error_boundary; pub use error_boundary::*; mod animated_show; @@ -352,3 +352,6 @@ where (self)(props).into_view() } } + +#[doc(hidden)] +pub const WASM_LOADED_NAME: &'static str = "leptos_client_wasm_loaded"; diff --git a/router/src/components/form.rs b/router/src/components/form.rs index b1bbb70eef..1509bfe5e6 100644 --- a/router/src/components/form.rs +++ b/router/src/components/form.rs @@ -2,7 +2,12 @@ use crate::{ hooks::has_router, use_navigate, use_resolved_path, NavigateOptions, ToHref, Url, }; -use leptos::{html::form, logging::*, server_fn::error::ServerFnUrlError, *}; +use leptos::{ + html::form, + logging::*, + server_fn::ServerFnUrlResponse, + *, +}; use serde::{de::DeserializeOwned, Serialize}; use std::{collections::HashSet, error::Error, rc::Rc}; use wasm_bindgen::{JsCast, UnwrapThrowExt}; @@ -459,17 +464,16 @@ where let action_url = effect_action_url.clone(); Effect::new_isomorphic(move |_| { - let errors = use_context::>(); - if let Some(url_error) = - errors - .map(|errors| { - errors + let results = use_context::>>(); + if let Some(result) = results + .map(|results| { + results .into_iter() - .find(|e| effect_action_url.contains(e.fn_name())) + .find(|e| effect_action_url.contains(e.name())) }) - .flatten() { - leptos::logging::log!("In iso effect with error = {url_error:?}"); - value.try_set(Some(Err(url_error.error().clone()))); + .flatten() + { + value.try_set(Some(result.get())); } }); @@ -478,10 +482,10 @@ where wasm_has_loaded.set(true); }); - view!{ + view! { Self { - Self { - internal_fn_name: fn_name, - internal_error: error, - } - } - - /// TODO: Write documentation - pub fn error(&self) -> &ServerFnError { - &self.internal_error - } - - /// TODO: Add docs - pub fn fn_name(&self) -> &str { - &self.internal_fn_name.as_ref() - } -} - -impl From for ServerFnError { - fn from(error: ServerFnUrlError) -> Self { - error.internal_error - } -} - -impl From for ServerFnErrorErr { - fn from(error: ServerFnUrlError) -> Self { - error.internal_error.into() - } -} - impl core::fmt::Display for ServerFnError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( diff --git a/server_fn/src/lib.rs b/server_fn/src/lib.rs index df4d54dabf..b53d718561 100644 --- a/server_fn/src/lib.rs +++ b/server_fn/src/lib.rs @@ -82,7 +82,6 @@ // used by the macro #[doc(hidden)] pub use const_format; -use error::ServerFnUrlError; // used by the macro #[cfg(feature = "ssr")] #[doc(hidden)] @@ -94,10 +93,10 @@ use quote::TokenStreamExt; // used by the macro #[doc(hidden)] pub use serde; -use serde::{de::DeserializeOwned, Serialize}; +use serde::{de::DeserializeOwned, Serialize, Deserialize}; pub use server_fn_macro_default::server; use url::Url; -use std::{future::Future, pin::Pin, str::FromStr, collections::HashSet}; +use std::{future::Future, pin::Pin, str::FromStr, collections::HashSet, hash::Hash, cmp::Eq}; #[cfg(any(feature = "ssr", doc))] use syn::parse_quote; // used by the macro @@ -600,6 +599,41 @@ where } } +/// TODO: Write Documentation +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)] +pub struct ServerFnUrlResponse { + data: Result, + fn_name: String, +} + +impl ServerFnUrlResponse { + /// TODO: Write Documentation + pub fn from_error(fn_name: &str, error: ServerFnError) -> Self { + Self { + fn_name: fn_name.to_owned(), + data: Err(error), + } + } + + /// TODO: Add docs + pub fn new(fn_name: &str, data: T) -> Self { + Self { + data: Ok(data), + fn_name: fn_name.to_owned(), + } + } + + /// TODO: Write documentation + pub fn get(&self) -> Result { + self.data.clone() + } + + /// TODO: Add docs + pub fn name(&self) -> &str { + &self.fn_name.as_ref() + } +} + // Lazily initialize the client to be reused for all server function calls. #[cfg(any(all(not(feature = "ssr"), not(target_arch = "wasm32")), doc))] static CLIENT: once_cell::sync::Lazy = @@ -623,7 +657,7 @@ fn get_server_url() -> &'static str { } #[doc(hidden)] -pub fn query_to_errors(query: &str) -> HashSet { +pub fn query_to_responses(query: &str) -> HashSet> { // Url::parse needs an full absolute URL to parse correctly. // Since this function is only interested in the query pairs, // the specific scheme and domain do not matter. @@ -632,8 +666,8 @@ pub fn query_to_errors(query: &str) -> HashSet { .query_pairs() .into_iter() .filter_map(|(k, v)| { - if k.starts_with("server_fn_error_") { - serde_qs::from_str::<'_, ServerFnUrlError>(v.as_ref()).ok() + if k.starts_with("server_fn_response_") { + serde_qs::from_str::<'_, ServerFnUrlResponse>(v.as_ref()).ok() } else { None }