From a32274e28078bc74470c7f7e349b1a323054a5b1 Mon Sep 17 00:00:00 2001 From: Paul Z Date: Sun, 22 Oct 2023 15:26:03 +0200 Subject: [PATCH 1/3] first responders implementation --- Cargo.toml | 7 +- src/lib.rs | 8 + src/responders.rs | 417 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 431 insertions(+), 1 deletion(-) create mode 100644 src/responders.rs diff --git a/Cargo.toml b/Cargo.toml index 6e93665..026842c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,8 +11,9 @@ version = "0.3.1" edition = "2021" [features] -default = [] +default = [ "responders" ] guards = ["tower", "futures-core", "pin-project-lite"] +responders = ["serde", "serde_json"] [dependencies] axum = { git = "https://github.com/tokio-rs/axum", branch = "main", default-features = false } @@ -21,3 +22,7 @@ axum = { git = "https://github.com/tokio-rs/axum", branch = "main", default-feat tower = { version = "0.4", default-features = false, optional = true } futures-core = { version = "0.3", optional = true } pin-project-lite = { version = "0.2", optional = true } + +serde = { version = "1.0", features = ["derive"], optional = true } +serde_json = { version = "1.0", optional = true } + diff --git a/src/lib.rs b/src/lib.rs index 9459590..364a8f1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,8 +4,16 @@ pub mod extractors; #[cfg(feature = "guards")] pub mod guard; pub mod headers; +#[cfg(feature = "responders")] +pub mod responders; +use axum::{ + http::HeaderMap, + response::{IntoResponse, Response}, +}; pub use extractors::*; #[cfg(feature = "guards")] pub use guard::*; pub use headers::*; +#[cfg(feature = "responders")] +pub use responders::*; diff --git a/src/responders.rs b/src/responders.rs new file mode 100644 index 0000000..f1f68f6 --- /dev/null +++ b/src/responders.rs @@ -0,0 +1,417 @@ +//pub struct HxLocation() + +use std::{collections::HashMap, convert::Infallible}; + +use axum::{ + http::{header::InvalidHeaderValue, HeaderValue, StatusCode, Uri}, + response::{IntoResponse, IntoResponseParts, ResponseParts}, +}; +use serde::Serialize; +use serde_json::Value; + +use crate::headers; + +const HX_SWAP_INNER_HTML: &str = "innerHTML"; +const HX_SWAP_OUTER_HTML: &str = "outerHTML"; +const HX_SWAP_BEFORE_BEGIN: &str = "beforebegin"; +const HX_SWAP_AFTER_BEGIN: &str = "afterbegin"; +const HX_SWAP_BEFORE_END: &str = "beforeend"; +const HX_SWAP_AFTER_END: &str = "afterend"; +const HX_SWAP_DELETE: &str = "delete"; +const HX_SWAP_NONE: &str = "none"; + +/// The `HX-Location` header. +/// +/// This response header can be used to trigger a client side redirection without reloading the whole page. Instead of changing the page’s location it will act like following a hx-boost link, creating a new history entry, issuing an ajax request to the value of the header and pushing the path into history. +/// +/// +#[derive(Debug, Clone, Serialize)] +pub struct HxLocation { + /// Url to load the response from + pub path: String, + /// The source element of the request + pub source: Option, + /// An event that "triggered" the request + pub event: Option, + /// A callback that will handle the response HTML + pub handler: Option, + /// The target to swap the response into + pub target: Option, + /// How the response will be swapped in relative to the target + pub swap: Option, + /// Values to submit with the request + pub values: Option, + /// headers to submit with the request + pub headers: Option, +} + +impl HxLocation { + pub fn from_uri(uri: &Uri) -> Self { + Self { + path: uri.to_string(), + source: None, + event: None, + handler: None, + target: None, + swap: None, + values: None, + headers: None, + } + } +} + +impl IntoResponseParts for HxLocation { + type Error = HxError; + + fn into_response_parts(self, mut res: ResponseParts) -> Result { + let header_value = if self.source.is_none() + && self.event.is_none() + && self.handler.is_none() + && self.target.is_none() + && self.swap.is_none() + && self.values.is_none() + && self.headers.is_none() + { + HeaderValue::from_str(&self.path)? + } else { + HeaderValue::from_maybe_shared(serde_json::to_string(&self)?)? + }; + + res.headers_mut().insert(headers::HX_LOCATION, header_value); + Ok(res) + } +} + +/// The `HX-Push-Url` header. +/// +/// Pushes a new url into the history stack. +/// +/// Will fail if the supplied Uri contains characters that are not visible ASCII (32-127). +#[derive(Debug, Clone)] +pub struct HxPushUrl(Uri); + +impl IntoResponseParts for HxPushUrl { + type Error = HxError; + + fn into_response_parts(self, mut res: ResponseParts) -> Result { + res.headers_mut().insert( + headers::HX_PUSH_URL, + HeaderValue::from_maybe_shared(self.0.to_string())?, + ); + Ok(res) + } +} + +/// The `HX-Redirect` header. +/// +/// Can be used to do a client-side redirect to a new location. +/// +/// Will fail if the supplied Uri contains characters that are not visible ASCII (32-127). +#[derive(Debug, Clone)] +pub struct HxRedirect(Uri); + +impl IntoResponseParts for HxRedirect { + type Error = HxError; + + fn into_response_parts(self, mut res: ResponseParts) -> Result { + res.headers_mut().insert( + headers::HX_REDIRECT, + HeaderValue::from_maybe_shared(self.0.to_string())?, + ); + Ok(res) + } +} + +/// The `HX-Refresh`header. +/// +/// If set to `true` the client-side will do a full refresh of the page. +/// +/// This responder will never fail. +#[derive(Debug, Copy, Clone)] +pub struct HxRefresh(bool); + +impl IntoResponseParts for HxRefresh { + type Error = Infallible; + + fn into_response_parts(self, mut res: ResponseParts) -> Result { + res.headers_mut().insert( + headers::HX_REFRESH, + match self.0 { + true => HeaderValue::from_static("true"), + false => HeaderValue::from_static("false"), + }, + ); + Ok(res) + } +} + +/// The `HX-Replace-Url` header. +/// +/// Replaces the currelt URL in the location bar. +/// +/// Will fail if the supplied Uri contains characters that are not visible ASCII (32-127). +#[derive(Debug, Clone)] +pub struct HxReplaceUrl(Uri); + +impl IntoResponseParts for HxReplaceUrl { + type Error = HxError; + + fn into_response_parts(self, mut res: ResponseParts) -> Result { + res.headers_mut().insert( + headers::HX_REPLACE_URL, + HeaderValue::from_maybe_shared(self.0.to_string())?, + ); + Ok(res) + } +} + +/// The `HX-Reswap` header. +/// +/// Allows you to specidy how the response will be swapped. +/// +/// This responder will never fail. +#[derive(Debug, Copy, Clone)] +pub struct HxReswap(SwapOption); + +impl IntoResponseParts for HxReswap { + type Error = Infallible; + + fn into_response_parts(self, mut res: ResponseParts) -> Result { + res.headers_mut().insert(headers::HX_RESWAP, self.0.into()); + Ok(res) + } +} + +/// The `HX-Retarget` header. +/// +/// A CSS selector that updates the target of the content update to a different element on the page. +/// +/// Will fail if the supplied String contains characters that are not visible ASCII (32-127). +#[derive(Debug, Clone)] +pub struct HxRetarget(String); + +impl IntoResponseParts for HxRetarget { + type Error = HxError; + + fn into_response_parts(self, mut res: ResponseParts) -> Result { + res.headers_mut().insert( + headers::HX_RETARGET, + HeaderValue::from_maybe_shared(self.0)?, + ); + Ok(res) + } +} + +/// The `HX-Reselect` header. +/// +/// A CSS selector that allows you to choose which part of the response is used to be swapped in. Overrides an existing hx-select on the triggering element. +/// +/// Will fail if the supplied String contains characters that are not visible ASCII (32-127). +#[derive(Debug, Clone)] +pub struct HxReselect(String); + +impl IntoResponseParts for HxReselect { + type Error = HxError; + + fn into_response_parts(self, mut res: ResponseParts) -> Result { + res.headers_mut().insert( + headers::HX_RESELECT, + HeaderValue::from_maybe_shared(self.0)?, + ); + Ok(res) + } +} + +/// The `HX-Trigger` header. +/// +/// Allows you to trigger client-side events. +/// +/// Will fail if the supplied events contain produce characters that are not visible ASCII (32-127) when serializing to json. +#[derive(Debug, Clone)] +pub struct HxTrigger(Vec); + +impl IntoResponseParts for HxTrigger { + type Error = HxError; + + fn into_response_parts(self, mut res: ResponseParts) -> Result { + res.headers_mut() + .insert(headers::HX_TRIGGER, events_to_header_value(self.0)?); + Ok(res) + } +} + +/// The `HX-Trigger-After-Settle` header. +/// +/// Allows you to trigger client-side events after the settle step. +/// +/// Will fail if the supplied events contain produce characters that are not visible ASCII (32-127) when serializing to json. +#[derive(Debug, Clone)] +pub struct HxTriggerAfterSettle(Vec); + +impl IntoResponseParts for HxTriggerAfterSettle { + type Error = HxError; + + fn into_response_parts(self, mut res: ResponseParts) -> Result { + res.headers_mut().insert( + headers::HX_TRIGGER_AFTER_SETTLE, + events_to_header_value(self.0)?, + ); + Ok(res) + } +} + +/// The `HX-Trigger-After-Swap` header. +/// +/// Allows you to trigger client-side events after the swap step. +/// +/// Will fail if the supplied events contain produce characters that are not visible ASCII (32-127) when serializing to json. +#[derive(Debug, Clone)] +pub struct HxTriggerAfterSwap(Vec); + +impl IntoResponseParts for HxTriggerAfterSwap { + type Error = HxError; + + fn into_response_parts(self, mut res: ResponseParts) -> Result { + res.headers_mut().insert( + headers::HX_TRIGGER_AFTER_SWAP, + events_to_header_value(self.0)?, + ); + Ok(res) + } +} + +/// Values of the `hx-swap` attribute. +#[derive(Debug, Copy, Clone)] +pub enum SwapOption { + /// Replace the inner html of the target element. + InnerHtml, + /// Replace the entire target element with the response. + OuterHtml, + /// Insert the response before the target element. + BeforeBegin, + /// Insert the response before the first child of the target element. + AfterBegin, + /// Insert the response after the last child of the target element + BeforeEnd, + /// Insert the response after the target element + AfterEnd, + /// Deletes the target element regardless of the response + Delete, + /// Does not append content from response (out of band items will still be processed). + None, +} + +impl From for HeaderValue { + fn from(value: SwapOption) -> Self { + match value { + SwapOption::InnerHtml => HeaderValue::from_static(HX_SWAP_INNER_HTML), + SwapOption::OuterHtml => HeaderValue::from_static(HX_SWAP_OUTER_HTML), + SwapOption::BeforeBegin => HeaderValue::from_static(HX_SWAP_BEFORE_BEGIN), + SwapOption::AfterBegin => HeaderValue::from_static(HX_SWAP_AFTER_BEGIN), + SwapOption::BeforeEnd => HeaderValue::from_static(HX_SWAP_BEFORE_END), + SwapOption::AfterEnd => HeaderValue::from_static(HX_SWAP_AFTER_END), + SwapOption::Delete => HeaderValue::from_static(HX_SWAP_DELETE), + SwapOption::None => HeaderValue::from_static(HX_SWAP_NONE), + } + } +} + +// can be removed and automatically derived when https://github.com/serde-rs/serde/issues/2485 +// is implemented +impl Serialize for SwapOption { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + const UNIT_NAME: &str = "SwapOption"; + match self { + Self::InnerHtml => serializer.serialize_unit_variant(UNIT_NAME, 0, HX_SWAP_INNER_HTML), + Self::OuterHtml => serializer.serialize_unit_variant(UNIT_NAME, 1, HX_SWAP_OUTER_HTML), + Self::BeforeBegin => { + serializer.serialize_unit_variant(UNIT_NAME, 2, HX_SWAP_BEFORE_BEGIN) + } + Self::AfterBegin => { + serializer.serialize_unit_variant(UNIT_NAME, 3, HX_SWAP_AFTER_BEGIN) + } + Self::BeforeEnd => serializer.serialize_unit_variant(UNIT_NAME, 4, HX_SWAP_BEFORE_END), + Self::AfterEnd => serializer.serialize_unit_variant(UNIT_NAME, 5, HX_SWAP_AFTER_END), + Self::Delete => serializer.serialize_unit_variant(UNIT_NAME, 6, HX_SWAP_DELETE), + Self::None => serializer.serialize_unit_variant(UNIT_NAME, 7, HX_SWAP_NONE), + } + } +} + +#[derive(Debug, Clone, Serialize)] +pub struct HxEvent { + pub name: String, + pub data: Option, +} + +impl HxEvent { + pub fn new(name: String) -> Self { + Self { name, data: None } + } + pub fn new_with_data(name: String, data: T) -> Result { + let data = serde_json::to_value(data)?; + + Ok(Self { + name, + data: Some(data), + }) + } +} + +pub(crate) fn events_to_header_value(events: Vec) -> Result { + let with_data = events.iter().any(|e| e.data.is_some()); + + let header_value = if with_data { + // at least one event contains data so the header_value needs to be json encoded. + let header_value = events + .into_iter() + .map(|e| (e.name, e.data.map(|d| d.to_string()).unwrap_or_default())) + .collect::>(); + serde_json::to_string(&header_value)? + } else { + // no event contains data, the event names can be put in the header value separated + // by a comma. + events + .into_iter() + .map(|e| e.name) + .reduce(|acc, e| acc + ", " + &e) + .unwrap_or_default() + }; + + HeaderValue::from_maybe_shared(header_value).map_err(HxError::from) +} + +pub enum HxError { + InvalidHeaderValue(InvalidHeaderValue), + Serialization(serde_json::Error), +} + +impl From for HxError { + fn from(value: InvalidHeaderValue) -> Self { + Self::InvalidHeaderValue(value) + } +} + +impl From for HxError { + fn from(value: serde_json::Error) -> Self { + Self::Serialization(value) + } +} + +impl IntoResponse for HxError { + fn into_response(self) -> axum::response::Response { + match self { + Self::InvalidHeaderValue(_) => { + (StatusCode::INTERNAL_SERVER_ERROR, "invalid header value").into_response() + } + Self::Serialization(_) => ( + StatusCode::INTERNAL_SERVER_ERROR, + "failed to serialize event", + ) + .into_response(), + } + } +} From ad16b10fc65815a513ccb18f93a95adcc58d901a Mon Sep 17 00:00:00 2001 From: Paul Z Date: Sun, 22 Oct 2023 20:45:48 +0200 Subject: [PATCH 2/3] provide basic responder functionality without serde, json features with serde feature --- Cargo.toml | 4 +- src/lib.rs | 2 - src/responders.rs | 178 +++++++++------------------------- src/responders/serde.rs | 205 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 252 insertions(+), 137 deletions(-) create mode 100644 src/responders/serde.rs diff --git a/Cargo.toml b/Cargo.toml index 026842c..5d2975a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,9 +11,9 @@ version = "0.3.1" edition = "2021" [features] -default = [ "responders" ] +default = [ ] guards = ["tower", "futures-core", "pin-project-lite"] -responders = ["serde", "serde_json"] +serde = [ "dep:serde", "dep:serde_json" ] [dependencies] axum = { git = "https://github.com/tokio-rs/axum", branch = "main", default-features = false } diff --git a/src/lib.rs b/src/lib.rs index 364a8f1..4dba053 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,7 +4,6 @@ pub mod extractors; #[cfg(feature = "guards")] pub mod guard; pub mod headers; -#[cfg(feature = "responders")] pub mod responders; use axum::{ @@ -15,5 +14,4 @@ pub use extractors::*; #[cfg(feature = "guards")] pub use guard::*; pub use headers::*; -#[cfg(feature = "responders")] pub use responders::*; diff --git a/src/responders.rs b/src/responders.rs index f1f68f6..b4247a7 100644 --- a/src/responders.rs +++ b/src/responders.rs @@ -1,16 +1,15 @@ -//pub struct HxLocation() - -use std::{collections::HashMap, convert::Infallible}; +use std::convert::Infallible; use axum::{ http::{header::InvalidHeaderValue, HeaderValue, StatusCode, Uri}, response::{IntoResponse, IntoResponseParts, ResponseParts}, }; -use serde::Serialize; -use serde_json::Value; use crate::headers; +#[cfg(feature = "serde")] +pub mod serde; + const HX_SWAP_INNER_HTML: &str = "innerHTML"; const HX_SWAP_OUTER_HTML: &str = "outerHTML"; const HX_SWAP_BEFORE_BEGIN: &str = "beforebegin"; @@ -22,62 +21,20 @@ const HX_SWAP_NONE: &str = "none"; /// The `HX-Location` header. /// -/// This response header can be used to trigger a client side redirection without reloading the whole page. Instead of changing the page’s location it will act like following a hx-boost link, creating a new history entry, issuing an ajax request to the value of the header and pushing the path into history. +/// This response header can be used to trigger a client side redirection without reloading the whole page. /// -/// -#[derive(Debug, Clone, Serialize)] -pub struct HxLocation { - /// Url to load the response from - pub path: String, - /// The source element of the request - pub source: Option, - /// An event that "triggered" the request - pub event: Option, - /// A callback that will handle the response HTML - pub handler: Option, - /// The target to swap the response into - pub target: Option, - /// How the response will be swapped in relative to the target - pub swap: Option, - /// Values to submit with the request - pub values: Option, - /// headers to submit with the request - pub headers: Option, -} - -impl HxLocation { - pub fn from_uri(uri: &Uri) -> Self { - Self { - path: uri.to_string(), - source: None, - event: None, - handler: None, - target: None, - swap: None, - values: None, - headers: None, - } - } -} +/// Will fail if the supplied Uri contains characters that are not visible ASCII (32-127). +#[derive(Debug, Clone)] +pub struct HxLocation(Uri); impl IntoResponseParts for HxLocation { type Error = HxError; fn into_response_parts(self, mut res: ResponseParts) -> Result { - let header_value = if self.source.is_none() - && self.event.is_none() - && self.handler.is_none() - && self.target.is_none() - && self.swap.is_none() - && self.values.is_none() - && self.headers.is_none() - { - HeaderValue::from_str(&self.path)? - } else { - HeaderValue::from_maybe_shared(serde_json::to_string(&self)?)? - }; - - res.headers_mut().insert(headers::HX_LOCATION, header_value); + res.headers_mut().insert( + headers::HX_LOCATION, + HeaderValue::from_maybe_shared(self.0.to_string())?, + ); Ok(res) } } @@ -226,16 +183,23 @@ impl IntoResponseParts for HxReselect { /// /// Allows you to trigger client-side events. /// -/// Will fail if the supplied events contain produce characters that are not visible ASCII (32-127) when serializing to json. +/// Will fail if the supplied events contain or produce characters that are not visible ASCII (32-127) when serializing to json. #[derive(Debug, Clone)] -pub struct HxTrigger(Vec); +pub struct HxTrigger(Vec); impl IntoResponseParts for HxTrigger { type Error = HxError; fn into_response_parts(self, mut res: ResponseParts) -> Result { - res.headers_mut() - .insert(headers::HX_TRIGGER, events_to_header_value(self.0)?); + res.headers_mut().insert( + headers::HX_TRIGGER, + HeaderValue::from_maybe_shared( + self.0 + .into_iter() + .reduce(|acc, e| acc + ", " + &e) + .unwrap_or_default(), + )?, + ); Ok(res) } } @@ -244,9 +208,9 @@ impl IntoResponseParts for HxTrigger { /// /// Allows you to trigger client-side events after the settle step. /// -/// Will fail if the supplied events contain produce characters that are not visible ASCII (32-127) when serializing to json. +/// Will fail if the supplied events contain or produce characters that are not visible ASCII (32-127) when serializing to json. #[derive(Debug, Clone)] -pub struct HxTriggerAfterSettle(Vec); +pub struct HxTriggerAfterSettle(Vec); impl IntoResponseParts for HxTriggerAfterSettle { type Error = HxError; @@ -254,7 +218,12 @@ impl IntoResponseParts for HxTriggerAfterSettle { fn into_response_parts(self, mut res: ResponseParts) -> Result { res.headers_mut().insert( headers::HX_TRIGGER_AFTER_SETTLE, - events_to_header_value(self.0)?, + HeaderValue::from_maybe_shared( + self.0 + .into_iter() + .reduce(|acc, e| acc + ", " + &e) + .unwrap_or_default(), + )?, ); Ok(res) } @@ -264,9 +233,9 @@ impl IntoResponseParts for HxTriggerAfterSettle { /// /// Allows you to trigger client-side events after the swap step. /// -/// Will fail if the supplied events contain produce characters that are not visible ASCII (32-127) when serializing to json. +/// Will fail if the supplied events contain or produce characters that are not visible ASCII (32-127) when serializing to json. #[derive(Debug, Clone)] -pub struct HxTriggerAfterSwap(Vec); +pub struct HxTriggerAfterSwap(Vec); impl IntoResponseParts for HxTriggerAfterSwap { type Error = HxError; @@ -274,13 +243,19 @@ impl IntoResponseParts for HxTriggerAfterSwap { fn into_response_parts(self, mut res: ResponseParts) -> Result { res.headers_mut().insert( headers::HX_TRIGGER_AFTER_SWAP, - events_to_header_value(self.0)?, + HeaderValue::from_maybe_shared( + self.0 + .into_iter() + .reduce(|acc, e| acc + ", " + &e) + .unwrap_or_default(), + )?, ); Ok(res) } } /// Values of the `hx-swap` attribute. +// serde::Serialize is implemented in responders/serde.rs #[derive(Debug, Copy, Clone)] pub enum SwapOption { /// Replace the inner html of the target element. @@ -316,76 +291,10 @@ impl From for HeaderValue { } } -// can be removed and automatically derived when https://github.com/serde-rs/serde/issues/2485 -// is implemented -impl Serialize for SwapOption { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - const UNIT_NAME: &str = "SwapOption"; - match self { - Self::InnerHtml => serializer.serialize_unit_variant(UNIT_NAME, 0, HX_SWAP_INNER_HTML), - Self::OuterHtml => serializer.serialize_unit_variant(UNIT_NAME, 1, HX_SWAP_OUTER_HTML), - Self::BeforeBegin => { - serializer.serialize_unit_variant(UNIT_NAME, 2, HX_SWAP_BEFORE_BEGIN) - } - Self::AfterBegin => { - serializer.serialize_unit_variant(UNIT_NAME, 3, HX_SWAP_AFTER_BEGIN) - } - Self::BeforeEnd => serializer.serialize_unit_variant(UNIT_NAME, 4, HX_SWAP_BEFORE_END), - Self::AfterEnd => serializer.serialize_unit_variant(UNIT_NAME, 5, HX_SWAP_AFTER_END), - Self::Delete => serializer.serialize_unit_variant(UNIT_NAME, 6, HX_SWAP_DELETE), - Self::None => serializer.serialize_unit_variant(UNIT_NAME, 7, HX_SWAP_NONE), - } - } -} - -#[derive(Debug, Clone, Serialize)] -pub struct HxEvent { - pub name: String, - pub data: Option, -} - -impl HxEvent { - pub fn new(name: String) -> Self { - Self { name, data: None } - } - pub fn new_with_data(name: String, data: T) -> Result { - let data = serde_json::to_value(data)?; - - Ok(Self { - name, - data: Some(data), - }) - } -} - -pub(crate) fn events_to_header_value(events: Vec) -> Result { - let with_data = events.iter().any(|e| e.data.is_some()); - - let header_value = if with_data { - // at least one event contains data so the header_value needs to be json encoded. - let header_value = events - .into_iter() - .map(|e| (e.name, e.data.map(|d| d.to_string()).unwrap_or_default())) - .collect::>(); - serde_json::to_string(&header_value)? - } else { - // no event contains data, the event names can be put in the header value separated - // by a comma. - events - .into_iter() - .map(|e| e.name) - .reduce(|acc, e| acc + ", " + &e) - .unwrap_or_default() - }; - - HeaderValue::from_maybe_shared(header_value).map_err(HxError::from) -} - pub enum HxError { InvalidHeaderValue(InvalidHeaderValue), + + #[cfg(feature = "serde")] Serialization(serde_json::Error), } @@ -395,6 +304,7 @@ impl From for HxError { } } +#[cfg(feature = "serde")] impl From for HxError { fn from(value: serde_json::Error) -> Self { Self::Serialization(value) @@ -407,6 +317,8 @@ impl IntoResponse for HxError { Self::InvalidHeaderValue(_) => { (StatusCode::INTERNAL_SERVER_ERROR, "invalid header value").into_response() } + + #[cfg(feature = "serde")] Self::Serialization(_) => ( StatusCode::INTERNAL_SERVER_ERROR, "failed to serialize event", diff --git a/src/responders/serde.rs b/src/responders/serde.rs new file mode 100644 index 0000000..dc3c8c3 --- /dev/null +++ b/src/responders/serde.rs @@ -0,0 +1,205 @@ +use std::collections::HashMap; + +use axum::{ + http::{HeaderValue, Uri}, + response::{IntoResponseParts, ResponseParts}, +}; +use serde::Serialize; +use serde_json::Value; + +use crate::{ + headers, + responders::{ + HX_SWAP_AFTER_BEGIN, HX_SWAP_AFTER_END, HX_SWAP_BEFORE_BEGIN, HX_SWAP_BEFORE_END, + HX_SWAP_DELETE, HX_SWAP_INNER_HTML, HX_SWAP_NONE, HX_SWAP_OUTER_HTML, + }, + HxError, SwapOption, +}; + +/// The `HX-Location` header. +/// +/// This response header can be used to trigger a client side redirection without reloading the whole page. Instead of changing the page’s location it will act like following a hx-boost link, creating a new history entry, issuing an ajax request to the value of the header and pushing the path into history. +/// +/// Will fail if the supplied data contains or produces characters that are not visible ASCII (32-127) when serializing to json. +#[derive(Debug, Clone, Serialize)] +pub struct HxLocation { + /// Url to load the response from + pub path: String, + /// The source element of the request + pub source: Option, + /// An event that "triggered" the request + pub event: Option, + /// A callback that will handle the response HTML + pub handler: Option, + /// The target to swap the response into + pub target: Option, + /// How the response will be swapped in relative to the target + pub swap: Option, + /// Values to submit with the request + pub values: Option, + /// headers to submit with the request + pub headers: Option, +} + +impl HxLocation { + pub fn from_uri(uri: &Uri) -> Self { + Self { + path: uri.to_string(), + source: None, + event: None, + handler: None, + target: None, + swap: None, + values: None, + headers: None, + } + } +} + +impl IntoResponseParts for HxLocation { + type Error = HxError; + + fn into_response_parts(self, mut res: ResponseParts) -> Result { + let header_value = if self.source.is_none() + && self.event.is_none() + && self.handler.is_none() + && self.target.is_none() + && self.swap.is_none() + && self.values.is_none() + && self.headers.is_none() + { + HeaderValue::from_str(&self.path)? + } else { + HeaderValue::from_maybe_shared(serde_json::to_string(&self)?)? + }; + + res.headers_mut().insert(headers::HX_LOCATION, header_value); + Ok(res) + } +} + +/// The `HX-Trigger` header. +/// +/// Allows you to trigger client-side events. +/// +/// Will fail if the supplied events contain or produce characters that are not visible ASCII (32-127) when serializing to json. +#[derive(Debug, Clone)] +pub struct HxTrigger(Vec); + +impl IntoResponseParts for HxTrigger { + type Error = HxError; + + fn into_response_parts(self, mut res: ResponseParts) -> Result { + res.headers_mut() + .insert(headers::HX_TRIGGER, events_to_header_value(self.0)?); + Ok(res) + } +} + +/// The `HX-Trigger-After-Settle` header. +/// +/// Allows you to trigger client-side events after the settle step. +/// +/// Will fail if the supplied events contain or produce characters that are not visible ASCII (32-127) when serializing to json. +#[derive(Debug, Clone)] +pub struct HxTriggerAfterSettle(Vec); + +impl IntoResponseParts for HxTriggerAfterSettle { + type Error = HxError; + + fn into_response_parts(self, mut res: ResponseParts) -> Result { + res.headers_mut().insert( + headers::HX_TRIGGER_AFTER_SETTLE, + events_to_header_value(self.0)?, + ); + Ok(res) + } +} + +/// The `HX-Trigger-After-Swap` header. +/// +/// Allows you to trigger client-side events after the swap step. +/// +/// Will fail if the supplied events contain or produce characters that are not visible ASCII (32-127) when serializing to json. +#[derive(Debug, Clone)] +pub struct HxTriggerAfterSwap(Vec); + +impl IntoResponseParts for HxTriggerAfterSwap { + type Error = HxError; + + fn into_response_parts(self, mut res: ResponseParts) -> Result { + res.headers_mut().insert( + headers::HX_TRIGGER_AFTER_SWAP, + events_to_header_value(self.0)?, + ); + Ok(res) + } +} + +#[derive(Debug, Clone, Serialize)] +pub struct HxEvent { + pub name: String, + pub data: Option, +} + +impl HxEvent { + pub fn new(name: String) -> Self { + Self { name, data: None } + } + pub fn new_with_data(name: String, data: T) -> Result { + let data = serde_json::to_value(data)?; + + Ok(Self { + name, + data: Some(data), + }) + } +} + +pub(crate) fn events_to_header_value(events: Vec) -> Result { + let with_data = events.iter().any(|e| e.data.is_some()); + + let header_value = if with_data { + // at least one event contains data so the header_value needs to be json encoded. + let header_value = events + .into_iter() + .map(|e| (e.name, e.data.map(|d| d.to_string()).unwrap_or_default())) + .collect::>(); + serde_json::to_string(&header_value)? + } else { + // no event contains data, the event names can be put in the header value separated + // by a comma. + events + .into_iter() + .map(|e| e.name) + .reduce(|acc, e| acc + ", " + &e) + .unwrap_or_default() + }; + + HeaderValue::from_maybe_shared(header_value).map_err(HxError::from) +} + +// can be removed and automatically derived when https://github.com/serde-rs/serde/issues/2485 +// is implemented +impl serde::Serialize for SwapOption { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + const UNIT_NAME: &str = "SwapOption"; + match self { + Self::InnerHtml => serializer.serialize_unit_variant(UNIT_NAME, 0, HX_SWAP_INNER_HTML), + Self::OuterHtml => serializer.serialize_unit_variant(UNIT_NAME, 1, HX_SWAP_OUTER_HTML), + Self::BeforeBegin => { + serializer.serialize_unit_variant(UNIT_NAME, 2, HX_SWAP_BEFORE_BEGIN) + } + Self::AfterBegin => { + serializer.serialize_unit_variant(UNIT_NAME, 3, HX_SWAP_AFTER_BEGIN) + } + Self::BeforeEnd => serializer.serialize_unit_variant(UNIT_NAME, 4, HX_SWAP_BEFORE_END), + Self::AfterEnd => serializer.serialize_unit_variant(UNIT_NAME, 5, HX_SWAP_AFTER_END), + Self::Delete => serializer.serialize_unit_variant(UNIT_NAME, 6, HX_SWAP_DELETE), + Self::None => serializer.serialize_unit_variant(UNIT_NAME, 7, HX_SWAP_NONE), + } + } +} From e784674f2e7ff26ece883c30452b4aa05cfab8a7 Mon Sep 17 00:00:00 2001 From: Paul Z Date: Mon, 23 Oct 2023 20:43:03 +0200 Subject: [PATCH 3/3] remove responder HxTrigger name collision --- src/lib.rs | 4 ---- src/responders.rs | 28 ++++++++++++++-------------- src/responders/serde.rs | 2 +- 3 files changed, 15 insertions(+), 19 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 4dba053..c052717 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,10 +6,6 @@ pub mod guard; pub mod headers; pub mod responders; -use axum::{ - http::HeaderMap, - response::{IntoResponse, Response}, -}; pub use extractors::*; #[cfg(feature = "guards")] pub use guard::*; diff --git a/src/responders.rs b/src/responders.rs index b4247a7..9a2cf34 100644 --- a/src/responders.rs +++ b/src/responders.rs @@ -25,7 +25,7 @@ const HX_SWAP_NONE: &str = "none"; /// /// Will fail if the supplied Uri contains characters that are not visible ASCII (32-127). #[derive(Debug, Clone)] -pub struct HxLocation(Uri); +pub struct HxLocation(pub Uri); impl IntoResponseParts for HxLocation { type Error = HxError; @@ -45,7 +45,7 @@ impl IntoResponseParts for HxLocation { /// /// Will fail if the supplied Uri contains characters that are not visible ASCII (32-127). #[derive(Debug, Clone)] -pub struct HxPushUrl(Uri); +pub struct HxPushUrl(pub Uri); impl IntoResponseParts for HxPushUrl { type Error = HxError; @@ -65,7 +65,7 @@ impl IntoResponseParts for HxPushUrl { /// /// Will fail if the supplied Uri contains characters that are not visible ASCII (32-127). #[derive(Debug, Clone)] -pub struct HxRedirect(Uri); +pub struct HxRedirect(pub Uri); impl IntoResponseParts for HxRedirect { type Error = HxError; @@ -85,7 +85,7 @@ impl IntoResponseParts for HxRedirect { /// /// This responder will never fail. #[derive(Debug, Copy, Clone)] -pub struct HxRefresh(bool); +pub struct HxRefresh(pub bool); impl IntoResponseParts for HxRefresh { type Error = Infallible; @@ -108,7 +108,7 @@ impl IntoResponseParts for HxRefresh { /// /// Will fail if the supplied Uri contains characters that are not visible ASCII (32-127). #[derive(Debug, Clone)] -pub struct HxReplaceUrl(Uri); +pub struct HxReplaceUrl(pub Uri); impl IntoResponseParts for HxReplaceUrl { type Error = HxError; @@ -128,7 +128,7 @@ impl IntoResponseParts for HxReplaceUrl { /// /// This responder will never fail. #[derive(Debug, Copy, Clone)] -pub struct HxReswap(SwapOption); +pub struct HxReswap(pub SwapOption); impl IntoResponseParts for HxReswap { type Error = Infallible; @@ -145,7 +145,7 @@ impl IntoResponseParts for HxReswap { /// /// Will fail if the supplied String contains characters that are not visible ASCII (32-127). #[derive(Debug, Clone)] -pub struct HxRetarget(String); +pub struct HxRetarget(pub String); impl IntoResponseParts for HxRetarget { type Error = HxError; @@ -165,7 +165,7 @@ impl IntoResponseParts for HxRetarget { /// /// Will fail if the supplied String contains characters that are not visible ASCII (32-127). #[derive(Debug, Clone)] -pub struct HxReselect(String); +pub struct HxReselect(pub String); impl IntoResponseParts for HxReselect { type Error = HxError; @@ -185,9 +185,9 @@ impl IntoResponseParts for HxReselect { /// /// Will fail if the supplied events contain or produce characters that are not visible ASCII (32-127) when serializing to json. #[derive(Debug, Clone)] -pub struct HxTrigger(Vec); +pub struct HxResponseTrigger(pub Vec); -impl IntoResponseParts for HxTrigger { +impl IntoResponseParts for HxResponseTrigger { type Error = HxError; fn into_response_parts(self, mut res: ResponseParts) -> Result { @@ -210,9 +210,9 @@ impl IntoResponseParts for HxTrigger { /// /// Will fail if the supplied events contain or produce characters that are not visible ASCII (32-127) when serializing to json. #[derive(Debug, Clone)] -pub struct HxTriggerAfterSettle(Vec); +pub struct HxResponseTriggerAfterSettle(pub Vec); -impl IntoResponseParts for HxTriggerAfterSettle { +impl IntoResponseParts for HxResponseTriggerAfterSettle { type Error = HxError; fn into_response_parts(self, mut res: ResponseParts) -> Result { @@ -235,9 +235,9 @@ impl IntoResponseParts for HxTriggerAfterSettle { /// /// Will fail if the supplied events contain or produce characters that are not visible ASCII (32-127) when serializing to json. #[derive(Debug, Clone)] -pub struct HxTriggerAfterSwap(Vec); +pub struct HxResponseTriggerAfterSwap(pub Vec); -impl IntoResponseParts for HxTriggerAfterSwap { +impl IntoResponseParts for HxResponseTriggerAfterSwap { type Error = HxError; fn into_response_parts(self, mut res: ResponseParts) -> Result { diff --git a/src/responders/serde.rs b/src/responders/serde.rs index dc3c8c3..8d71ed7 100644 --- a/src/responders/serde.rs +++ b/src/responders/serde.rs @@ -84,7 +84,7 @@ impl IntoResponseParts for HxLocation { /// /// Will fail if the supplied events contain or produce characters that are not visible ASCII (32-127) when serializing to json. #[derive(Debug, Clone)] -pub struct HxTrigger(Vec); +pub struct HxTrigger(pub Vec); impl IntoResponseParts for HxTrigger { type Error = HxError;