From a22339936174542f19e9e21432e6ce6692fe4d08 Mon Sep 17 00:00:00 2001
From: ItsEthra <107059409+ItsEthra@users.noreply.github.com>
Date: Fri, 1 Dec 2023 21:40:34 +0300
Subject: [PATCH 01/11] Code dedup
---
src/responders.rs | 284 ++++++++++++++++++++++++++++++++++------
src/responders/serde.rs | 254 ++++-------------------------------
2 files changed, 269 insertions(+), 269 deletions(-)
diff --git a/src/responders.rs b/src/responders.rs
index 99aef61..0b40e9e 100644
--- a/src/responders.rs
+++ b/src/responders.rs
@@ -1,6 +1,6 @@
//! Axum responses for htmx response headers.
-use std::convert::Infallible;
+use std::{convert::Infallible, str::FromStr};
use axum_core::response::{IntoResponse, IntoResponseParts, ResponseParts};
use http::{header::InvalidHeaderValue, HeaderValue, StatusCode, Uri};
@@ -32,15 +32,71 @@ const HX_SWAP_NONE: &str = "none";
///
/// See for more information.
#[derive(Debug, Clone)]
-pub struct HxLocation(pub Uri);
+pub struct HxLocation {
+ /// Uri of the new location.
+ pub uri: Uri,
+ /// Extra options.
+ #[cfg(feature = "serde")]
+ #[cfg_attr(feature = "unstable", doc(cfg(feature = "serde")))]
+ pub options: serde::LocationOptions,
+}
+
+impl HxLocation {
+ pub fn from_uri(uri: Uri) -> Self {
+ Self {
+ #[cfg(feature = "serde")]
+ options: serde::LocationOptions::default(),
+ uri,
+ }
+ }
+
+ #[cfg(feature = "serde")]
+ #[cfg_attr(feature = "unstable", doc(cfg(feature = "serde")))]
+ pub fn from_uri_with_options(uri: Uri, options: serde::LocationOptions) -> Self {
+ Self { uri, options }
+ }
+
+ #[cfg(feature = "serde")]
+ fn into_header_with_options(self) -> Result {
+ if self.options.is_default() {
+ return Ok(self.uri.to_string());
+ }
+
+ #[derive(::serde::Serialize)]
+ struct LocWithOpts {
+ path: String,
+ #[serde(flatten)]
+ opts: serde::LocationOptions,
+ }
+
+ let loc_with_opts = LocWithOpts {
+ path: self.uri.to_string(),
+ opts: self.options,
+ };
+ Ok(serde_json::to_string(&loc_with_opts)?)
+ }
+}
+
+impl<'a> TryFrom<&'a str> for HxLocation {
+ type Error = ::Err;
+
+ fn try_from(value: &'a str) -> Result {
+ Ok(Self::from_uri(Uri::from_str(value)?))
+ }
+}
impl IntoResponseParts for HxLocation {
type Error = HxError;
fn into_response_parts(self, mut res: ResponseParts) -> Result {
+ #[cfg(feature = "serde")]
+ let header = self.into_header_with_options()?;
+ #[cfg(not(feature = "serde"))]
+ let header = self.uri.to_string();
+
res.headers_mut().insert(
headers::HX_LOCATION,
- HeaderValue::from_maybe_shared(self.0.to_string())?,
+ HeaderValue::from_maybe_shared(header)?,
);
Ok(res)
@@ -71,6 +127,14 @@ impl IntoResponseParts for HxPushUrl {
}
}
+impl<'a> TryFrom<&'a str> for HxPushUrl {
+ type Error = ::Err;
+
+ fn try_from(value: &'a str) -> Result {
+ Ok(Self(value.parse()?))
+ }
+}
+
/// The `HX-Redirect` header.
///
/// Can be used to do a client-side redirect to a new location.
@@ -93,6 +157,14 @@ impl IntoResponseParts for HxRedirect {
}
}
+impl<'a> TryFrom<&'a str> for HxRedirect {
+ type Error = ::Err;
+
+ fn try_from(value: &'a str) -> Result {
+ Ok(Self(value.parse()?))
+ }
+}
+
/// The `HX-Refresh`header.
///
/// If set to `true` the client-side will do a full refresh of the page.
@@ -142,6 +214,14 @@ impl IntoResponseParts for HxReplaceUrl {
}
}
+impl<'a> TryFrom<&'a str> for HxReplaceUrl {
+ type Error = ::Err;
+
+ fn try_from(value: &'a str) -> Result {
+ Ok(Self(value.parse()?))
+ }
+}
+
/// The `HX-Reswap` header.
///
/// Allows you to specidy how the response will be swapped.
@@ -206,6 +286,71 @@ impl IntoResponseParts for HxReselect {
}
}
+/// Represents a client-side event carrying optional data.
+#[derive(Debug, Clone, ::serde::Serialize)]
+pub struct HxEvent {
+ pub name: String,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ #[cfg(feature = "serde")]
+ #[cfg_attr(feature = "unstable", doc(cfg(feature = "serde")))]
+ pub data: Option<::serde_json::Value>,
+}
+
+impl HxEvent {
+ /// Creates new event with no associated data.
+ pub fn new(name: String) -> Self {
+ Self {
+ name: name.to_string(),
+ data: None,
+ }
+ }
+
+ /// Creates new event with event data.
+ #[cfg(feature = "serde")]
+ #[cfg_attr(feature = "unstable", doc(cfg(feature = "serde")))]
+ pub fn new_with_data(
+ name: impl ToString,
+ data: T,
+ ) -> Result {
+ let data = serde_json::to_value(data)?;
+
+ Ok(Self {
+ name: name.to_string(),
+ data: Some(data),
+ })
+ }
+}
+
+#[cfg(feature = "serde")]
+fn events_to_header_value(events: Vec) -> Result {
+ use std::collections::HashMap;
+
+ use serde_json::Value;
+
+ 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.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)
+}
+
/// The `HX-Trigger` header.
///
/// Allows you to trigger client-side events. If you intend to add data to your
@@ -217,15 +362,15 @@ impl IntoResponseParts for HxReselect {
///
/// See for more information.
#[derive(Debug, Clone)]
-pub struct HxResponseTrigger(pub Vec);
+pub struct HxResponseTrigger(pub Vec);
impl From for HxResponseTrigger
where
T: IntoIterator,
- T::Item: ToString,
+ T::Item: Into,
{
fn from(value: T) -> Self {
- Self(value.into_iter().map(|s| s.to_string()).collect())
+ Self(value.into_iter().map(Into::into).collect())
}
}
@@ -233,15 +378,8 @@ impl IntoResponseParts for HxResponseTrigger {
type Error = HxError;
fn into_response_parts(self, mut res: ResponseParts) -> Result {
- res.headers_mut().insert(
- headers::HX_TRIGGER,
- HeaderValue::from_maybe_shared(
- self.0
- .into_iter()
- .reduce(|acc, e| acc + ", " + &e)
- .unwrap_or_default(),
- )?,
- );
+ res.headers_mut()
+ .insert(headers::HX_TRIGGER, events_to_header_value(self.0)?);
Ok(res)
}
@@ -259,15 +397,15 @@ impl IntoResponseParts for HxResponseTrigger {
///
/// See for more information.
#[derive(Debug, Clone)]
-pub struct HxResponseTriggerAfterSettle(pub Vec);
+pub struct HxResponseTriggerAfterSettle(pub Vec);
impl From for HxResponseTriggerAfterSettle
where
T: IntoIterator,
- T::Item: ToString,
+ T::Item: Into,
{
fn from(value: T) -> Self {
- Self(value.into_iter().map(|s| s.to_string()).collect())
+ Self(value.into_iter().map(Into::into).collect())
}
}
@@ -275,15 +413,8 @@ impl IntoResponseParts for HxResponseTriggerAfterSettle {
type Error = HxError;
fn into_response_parts(self, mut res: ResponseParts) -> Result {
- res.headers_mut().insert(
- headers::HX_TRIGGER_AFTER_SETTLE,
- HeaderValue::from_maybe_shared(
- self.0
- .into_iter()
- .reduce(|acc, e| acc + ", " + &e)
- .unwrap_or_default(),
- )?,
- );
+ res.headers_mut()
+ .insert(headers::HX_TRIGGER, events_to_header_value(self.0)?);
Ok(res)
}
@@ -300,15 +431,15 @@ impl IntoResponseParts for HxResponseTriggerAfterSettle {
///
/// See for more information.
#[derive(Debug, Clone)]
-pub struct HxResponseTriggerAfterSwap(pub Vec);
+pub struct HxResponseTriggerAfterSwap(pub Vec);
impl From for HxResponseTriggerAfterSwap
where
T: IntoIterator,
- T::Item: ToString,
+ T::Item: Into,
{
fn from(value: T) -> Self {
- Self(value.into_iter().map(|s| s.to_string()).collect())
+ Self(value.into_iter().map(Into::into).collect())
}
}
@@ -316,15 +447,8 @@ impl IntoResponseParts for HxResponseTriggerAfterSwap {
type Error = HxError;
fn into_response_parts(self, mut res: ResponseParts) -> Result {
- res.headers_mut().insert(
- headers::HX_TRIGGER_AFTER_SWAP,
- HeaderValue::from_maybe_shared(
- self.0
- .into_iter()
- .reduce(|acc, e| acc + ", " + &e)
- .unwrap_or_default(),
- )?,
- );
+ res.headers_mut()
+ .insert(headers::HX_TRIGGER, events_to_header_value(self.0)?);
Ok(res)
}
@@ -353,6 +477,32 @@ pub enum SwapOption {
None,
}
+// can be removed and automatically derived when
+// https://github.com/serde-rs/serde/issues/2485 is implemented
+#[cfg(feature = "serde")]
+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),
+ }
+ }
+}
+
impl From for HeaderValue {
fn from(value: SwapOption) -> Self {
match value {
@@ -373,7 +523,8 @@ pub enum HxError {
InvalidHeaderValue(InvalidHeaderValue),
#[cfg(feature = "serde")]
- Serialization(serde_json::Error),
+ #[cfg_attr(feature = "unstable", doc(cfg(feature = "serde")))]
+ Json(serde_json::Error),
}
impl From for HxError {
@@ -383,9 +534,10 @@ impl From for HxError {
}
#[cfg(feature = "serde")]
+#[cfg_attr(feature = "unstable", doc(cfg(feature = "serde")))]
impl From for HxError {
fn from(value: serde_json::Error) -> Self {
- Self::Serialization(value)
+ Self::Json(value)
}
}
@@ -397,7 +549,7 @@ impl IntoResponse for HxError {
}
#[cfg(feature = "serde")]
- Self::Serialization(_) => (
+ Self::Json(_) => (
StatusCode::INTERNAL_SERVER_ERROR,
"failed to serialize event",
)
@@ -405,3 +557,51 @@ impl IntoResponse for HxError {
}
}
}
+
+#[cfg(test)]
+mod tests {
+ use http::HeaderValue;
+ use serde_json::json;
+
+ use crate::{responders::events_to_header_value, HxEvent};
+
+ #[test]
+ #[cfg(feature = "serde")]
+ fn test_serialize_location() {
+ use crate::{serde::LocationOptions, HxLocation, SwapOption::InnerHtml};
+
+ let loc = HxLocation::try_from("/foo").unwrap();
+ assert_eq!(loc.into_header_with_options().unwrap(), "/foo");
+
+ let loc = HxLocation::from_uri_with_options(
+ "/foo".parse().unwrap(),
+ LocationOptions {
+ event: Some("click".into()),
+ swap: Some(InnerHtml),
+ ..Default::default()
+ },
+ );
+ assert_eq!(
+ loc.into_header_with_options().unwrap(),
+ r#"{"path":"/foo","event":"click","swap":"innerHTML"}"#
+ );
+ }
+
+ #[test]
+ fn valid_event_to_header_encoding() {
+ let evt = HxEvent::new_with_data(
+ "my-event",
+ json!({"level": "info", "message": {
+ "body": "This is a test message.",
+ "title": "Hello, world!",
+ }}),
+ )
+ .unwrap();
+
+ let header_value = events_to_header_value(vec![evt]).unwrap();
+
+ let expected_value = r#"{"my-event":{"level":"info","message":{"body":"This is a test message.","title":"Hello, world!"}}}"#;
+
+ assert_eq!(header_value, HeaderValue::from_static(expected_value));
+ }
+}
diff --git a/src/responders/serde.rs b/src/responders/serde.rs
index 7bfb875..18f997e 100644
--- a/src/responders/serde.rs
+++ b/src/responders/serde.rs
@@ -1,54 +1,47 @@
-use std::collections::HashMap;
-
-use axum_core::response::{IntoResponseParts, ResponseParts};
-use http::{HeaderValue, Uri};
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,
-};
+use crate::SwapOption;
-/// The `HX-Location` header.
+/// More options for `HX-Location` header.
///
-/// This response header can be used to trigger a client side redirection
-/// without reloading the whole page. If you only intend to redirect to the
-/// `document.body`, as opposed to a specific target, you can use
-/// `axum_htmx::HxResponseLocation` instead.
-///
-/// Will fail if the supplied data contains or produces characters that are not
-/// visible ASCII (32-127) when serializing to JSON.
-///
-/// See for more information.
-#[derive(Debug, Clone, Serialize)]
-pub struct HxLocation {
- /// Url to load the response from.
- pub path: String,
+/// - `source` - the source element of the request
+/// - `event` - an event that “triggered” the request
+/// - `handler` - a callback that will handle the response HTML
+/// - `target` - the target to swap the response into
+/// - `swap` - how the response will be swapped in relative to the target
+/// - `values` - values to submit with the request
+/// - `headers` - headers to submit with the request
+/// - `select` - allows you to select the content you want swapped from a response
+#[derive(Debug, Clone, Serialize, Default)]
+#[non_exhaustive]
+pub struct LocationOptions {
/// The source element of the request.
+ #[serde(skip_serializing_if = "Option::is_none")]
pub source: Option,
/// An event that "triggered" the request.
+ #[serde(skip_serializing_if = "Option::is_none")]
pub event: Option,
/// A callback that will handle the response HTML.
+ #[serde(skip_serializing_if = "Option::is_none")]
pub handler: Option,
/// The target to swap the response into.
+ #[serde(skip_serializing_if = "Option::is_none")]
pub target: Option,
/// How the response will be swapped in relative to the target.
+ #[serde(skip_serializing_if = "Option::is_none")]
pub swap: Option,
/// Values to submit with the request.
+ #[serde(skip_serializing_if = "Option::is_none")]
pub values: Option,
/// Headers to submit with the request.
+ #[serde(skip_serializing_if = "Option::is_none")]
pub headers: Option,
}
-impl HxLocation {
- pub fn from_uri(uri: &Uri) -> Self {
- Self {
- path: uri.to_string(),
+impl LocationOptions {
+ pub(super) fn is_default(&self) -> bool {
+ let Self {
source: None,
event: None,
handler: None,
@@ -56,204 +49,11 @@ impl HxLocation {
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)?)?
+ } = self
+ else {
+ return false;
};
- res.headers_mut().insert(headers::HX_LOCATION, header_value);
-
- Ok(res)
- }
-}
-
-/// The `HX-Trigger` header.
-///
-/// Allows you to trigger client-side events. If you only need to send bare
-/// events, you can use `axum_htmx::HxResponseTrigger` instead.
-///
-/// Will fail if the supplied events contain or produce characters that are not
-/// visible ASCII (32-127) when serializing to JSON.
-///
-/// See for more information.
-#[derive(Debug, Clone)]
-pub struct HxResponseTrigger(pub Vec);
-
-impl IntoResponseParts for HxResponseTrigger {
- 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. If you only
-/// intend to send bare events, you can use
-/// `axum_htmx::HxResponseTriggerAfterSettle` instead.
-///
-/// Will fail if the supplied events contain or produce characters that are not
-/// visible ASCII (32-127) when serializing to JSON.
-///
-/// See for more information.
-#[derive(Debug, Clone)]
-pub struct HxResponseTriggerAfterSettle(pub Vec);
-
-impl IntoResponseParts for HxResponseTriggerAfterSettle {
- 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. If you only
-/// need to send bare events, you can use
-/// `axum_htmx::HxResponseTriggerAfterSwao` instead.
-///
-/// Will fail if the supplied events contain or produce characters that are not
-/// visible ASCII (32-127) when serializing to JSON.
-///
-/// See for more information.
-#[derive(Debug, Clone)]
-pub struct HxResponseTriggerAfterSwap(pub Vec);
-
-impl IntoResponseParts for HxResponseTriggerAfterSwap {
- 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)
- }
-}
-
-/// Represents a client-side event carrying optional data.
-#[derive(Debug, Clone, Serialize)]
-pub struct HxEvent {
- pub name: String,
- pub data: Option,
-}
-
-impl HxEvent {
- pub fn new(name: String) -> Self {
- Self {
- name: name.to_string(),
- data: None,
- }
- }
-
- pub fn new_with_data(name: &str, data: T) -> Result {
- let data = serde_json::to_value(data)?;
-
- Ok(Self {
- name: name.to_string(),
- 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.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),
- }
- }
-}
-
-#[cfg(test)]
-mod tests {
- use serde_json::json;
-
- use super::*;
-
- #[test]
- fn valid_event_to_header_encoding() {
- let evt = HxEvent::new_with_data(
- "my-event",
- json!({"level": "info", "message": {
- "body": "This is a test message.",
- "title": "Hello, world!",
- }}),
- )
- .unwrap();
-
- let header_value = events_to_header_value(vec![evt]).unwrap();
-
- let expected_value = r#"{"my-event":{"level":"info","message":{"body":"This is a test message.","title":"Hello, world!"}}}"#;
-
- assert_eq!(header_value, HeaderValue::from_static(expected_value));
+ true
}
}
From 1dfff29673e3beb13e741883292ebad2c942c248 Mon Sep 17 00:00:00 2001
From: ItsEthra <107059409+ItsEthra@users.noreply.github.com>
Date: Fri, 1 Dec 2023 22:05:31 +0300
Subject: [PATCH 02/11] Moved HxLocation && HxTrigger* into different modules
---
src/responders.rs | 306 +------------------------------------
src/responders/location.rs | 175 +++++++++++++++++++++
src/responders/serde.rs | 59 -------
src/responders/trigger.rs | 214 ++++++++++++++++++++++++++
4 files changed, 393 insertions(+), 361 deletions(-)
create mode 100644 src/responders/location.rs
delete mode 100644 src/responders/serde.rs
create mode 100644 src/responders/trigger.rs
diff --git a/src/responders.rs b/src/responders.rs
index 0b40e9e..beebc10 100644
--- a/src/responders.rs
+++ b/src/responders.rs
@@ -7,9 +7,10 @@ use http::{header::InvalidHeaderValue, HeaderValue, StatusCode, Uri};
use crate::headers;
-#[cfg(feature = "serde")]
-#[cfg_attr(feature = "unstable", doc(cfg(feature = "serde")))]
-pub mod serde;
+mod location;
+pub use location::*;
+mod trigger;
+pub use trigger::*;
const HX_SWAP_INNER_HTML: &str = "innerHTML";
const HX_SWAP_OUTER_HTML: &str = "outerHTML";
@@ -20,89 +21,6 @@ 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. If you intend to redirect to a specific
-/// target on the page, you must enable the `serde` feature flag and use
-/// `axum_htmx::responders::serde::HxLocation` instead.
-///
-/// Will fail if the supplied Uri contains characters that are not visible ASCII
-/// (32-127).
-///
-/// See for more information.
-#[derive(Debug, Clone)]
-pub struct HxLocation {
- /// Uri of the new location.
- pub uri: Uri,
- /// Extra options.
- #[cfg(feature = "serde")]
- #[cfg_attr(feature = "unstable", doc(cfg(feature = "serde")))]
- pub options: serde::LocationOptions,
-}
-
-impl HxLocation {
- pub fn from_uri(uri: Uri) -> Self {
- Self {
- #[cfg(feature = "serde")]
- options: serde::LocationOptions::default(),
- uri,
- }
- }
-
- #[cfg(feature = "serde")]
- #[cfg_attr(feature = "unstable", doc(cfg(feature = "serde")))]
- pub fn from_uri_with_options(uri: Uri, options: serde::LocationOptions) -> Self {
- Self { uri, options }
- }
-
- #[cfg(feature = "serde")]
- fn into_header_with_options(self) -> Result {
- if self.options.is_default() {
- return Ok(self.uri.to_string());
- }
-
- #[derive(::serde::Serialize)]
- struct LocWithOpts {
- path: String,
- #[serde(flatten)]
- opts: serde::LocationOptions,
- }
-
- let loc_with_opts = LocWithOpts {
- path: self.uri.to_string(),
- opts: self.options,
- };
- Ok(serde_json::to_string(&loc_with_opts)?)
- }
-}
-
-impl<'a> TryFrom<&'a str> for HxLocation {
- type Error = ::Err;
-
- fn try_from(value: &'a str) -> Result {
- Ok(Self::from_uri(Uri::from_str(value)?))
- }
-}
-
-impl IntoResponseParts for HxLocation {
- type Error = HxError;
-
- fn into_response_parts(self, mut res: ResponseParts) -> Result {
- #[cfg(feature = "serde")]
- let header = self.into_header_with_options()?;
- #[cfg(not(feature = "serde"))]
- let header = self.uri.to_string();
-
- res.headers_mut().insert(
- headers::HX_LOCATION,
- HeaderValue::from_maybe_shared(header)?,
- );
-
- Ok(res)
- }
-}
-
/// The `HX-Push-Url` header.
///
/// Pushes a new url into the history stack.
@@ -286,174 +204,6 @@ impl IntoResponseParts for HxReselect {
}
}
-/// Represents a client-side event carrying optional data.
-#[derive(Debug, Clone, ::serde::Serialize)]
-pub struct HxEvent {
- pub name: String,
- #[serde(skip_serializing_if = "Option::is_none")]
- #[cfg(feature = "serde")]
- #[cfg_attr(feature = "unstable", doc(cfg(feature = "serde")))]
- pub data: Option<::serde_json::Value>,
-}
-
-impl HxEvent {
- /// Creates new event with no associated data.
- pub fn new(name: String) -> Self {
- Self {
- name: name.to_string(),
- data: None,
- }
- }
-
- /// Creates new event with event data.
- #[cfg(feature = "serde")]
- #[cfg_attr(feature = "unstable", doc(cfg(feature = "serde")))]
- pub fn new_with_data(
- name: impl ToString,
- data: T,
- ) -> Result {
- let data = serde_json::to_value(data)?;
-
- Ok(Self {
- name: name.to_string(),
- data: Some(data),
- })
- }
-}
-
-#[cfg(feature = "serde")]
-fn events_to_header_value(events: Vec) -> Result {
- use std::collections::HashMap;
-
- use serde_json::Value;
-
- 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.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)
-}
-
-/// The `HX-Trigger` header.
-///
-/// Allows you to trigger client-side events. If you intend to add data to your
-/// events, you must enable the `serde` feature flag and use
-/// `axum_htmx::responders::serde::HxResponseTrigger` instead.
-///
-/// Will fail if the supplied events contain or produce characters that are not
-/// visible ASCII (32-127) when serializing to JSON.
-///
-/// See for more information.
-#[derive(Debug, Clone)]
-pub struct HxResponseTrigger(pub Vec);
-
-impl From for HxResponseTrigger
-where
- T: IntoIterator,
- T::Item: Into,
-{
- fn from(value: T) -> Self {
- Self(value.into_iter().map(Into::into).collect())
- }
-}
-
-impl IntoResponseParts for HxResponseTrigger {
- 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. If you
-/// intend to add data to your events, you must enable the `serde` feature flag
-/// and use `axum_htmx::responders::serde::HxResponseTriggerAfterSettle`
-/// instead.
-///
-/// Will fail if the supplied events contain or produce characters that are not
-/// visible ASCII (32-127) when serializing to JSON.
-///
-/// See for more information.
-#[derive(Debug, Clone)]
-pub struct HxResponseTriggerAfterSettle(pub Vec);
-
-impl From for HxResponseTriggerAfterSettle
-where
- T: IntoIterator,
- T::Item: Into,
-{
- fn from(value: T) -> Self {
- Self(value.into_iter().map(Into::into).collect())
- }
-}
-
-impl IntoResponseParts for HxResponseTriggerAfterSettle {
- 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-Swap` header.
-///
-/// Allows you to trigger client-side events after the swap step. If you intend
-/// to add data to your events, you must enable the `serde` feature flag and use
-/// `axum_htmx::responders::serde::HxResponseTriggerAfterSwap` instead.
-///
-/// Will fail if the supplied events contain or produce characters that are not
-/// visible ASCII (32-127) when serializing to JSON.
-///
-/// See for more information.
-#[derive(Debug, Clone)]
-pub struct HxResponseTriggerAfterSwap(pub Vec);
-
-impl From for HxResponseTriggerAfterSwap
-where
- T: IntoIterator,
- T::Item: Into,
-{
- fn from(value: T) -> Self {
- Self(value.into_iter().map(Into::into).collect())
- }
-}
-
-impl IntoResponseParts for HxResponseTriggerAfterSwap {
- 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)
- }
-}
-
/// Values of the `hx-swap` attribute.
// serde::Serialize is implemented in responders/serde.rs
#[derive(Debug, Copy, Clone)]
@@ -557,51 +307,3 @@ impl IntoResponse for HxError {
}
}
}
-
-#[cfg(test)]
-mod tests {
- use http::HeaderValue;
- use serde_json::json;
-
- use crate::{responders::events_to_header_value, HxEvent};
-
- #[test]
- #[cfg(feature = "serde")]
- fn test_serialize_location() {
- use crate::{serde::LocationOptions, HxLocation, SwapOption::InnerHtml};
-
- let loc = HxLocation::try_from("/foo").unwrap();
- assert_eq!(loc.into_header_with_options().unwrap(), "/foo");
-
- let loc = HxLocation::from_uri_with_options(
- "/foo".parse().unwrap(),
- LocationOptions {
- event: Some("click".into()),
- swap: Some(InnerHtml),
- ..Default::default()
- },
- );
- assert_eq!(
- loc.into_header_with_options().unwrap(),
- r#"{"path":"/foo","event":"click","swap":"innerHTML"}"#
- );
- }
-
- #[test]
- fn valid_event_to_header_encoding() {
- let evt = HxEvent::new_with_data(
- "my-event",
- json!({"level": "info", "message": {
- "body": "This is a test message.",
- "title": "Hello, world!",
- }}),
- )
- .unwrap();
-
- let header_value = events_to_header_value(vec![evt]).unwrap();
-
- let expected_value = r#"{"my-event":{"level":"info","message":{"body":"This is a test message.","title":"Hello, world!"}}}"#;
-
- assert_eq!(header_value, HeaderValue::from_static(expected_value));
- }
-}
diff --git a/src/responders/location.rs b/src/responders/location.rs
new file mode 100644
index 0000000..8ec740b
--- /dev/null
+++ b/src/responders/location.rs
@@ -0,0 +1,175 @@
+use std::str::FromStr;
+
+use axum_core::response::{IntoResponseParts, ResponseParts};
+use http::{HeaderValue, Uri};
+
+use crate::{headers, HxError};
+
+/// The `HX-Location` header.
+///
+/// This response header can be used to trigger a client side redirection
+/// without reloading the whole page. If you intend to redirect to a specific
+/// target on the page, you must enable the `serde` feature flag and specify
+/// [`LocationOptions`].
+///
+/// Will fail if the supplied Uri contains characters that are not visible ASCII
+/// (32-127).
+///
+/// See for more information.
+#[derive(Debug, Clone)]
+pub struct HxLocation {
+ /// Uri of the new location.
+ pub uri: Uri,
+ /// Extra options.
+ #[cfg(feature = "serde")]
+ #[cfg_attr(feature = "unstable", doc(cfg(feature = "serde")))]
+ pub options: LocationOptions,
+}
+
+impl HxLocation {
+ pub fn from_uri(uri: Uri) -> Self {
+ Self {
+ #[cfg(feature = "serde")]
+ options: LocationOptions::default(),
+ uri,
+ }
+ }
+
+ #[cfg(feature = "serde")]
+ #[cfg_attr(feature = "unstable", doc(cfg(feature = "serde")))]
+ pub fn from_uri_with_options(uri: Uri, options: LocationOptions) -> Self {
+ Self { uri, options }
+ }
+
+ #[cfg(feature = "serde")]
+ fn into_header_with_options(self) -> Result {
+ if self.options.is_default() {
+ return Ok(self.uri.to_string());
+ }
+
+ #[derive(::serde::Serialize)]
+ struct LocWithOpts {
+ path: String,
+ #[serde(flatten)]
+ opts: LocationOptions,
+ }
+
+ let loc_with_opts = LocWithOpts {
+ path: self.uri.to_string(),
+ opts: self.options,
+ };
+ Ok(serde_json::to_string(&loc_with_opts)?)
+ }
+}
+
+impl<'a> TryFrom<&'a str> for HxLocation {
+ type Error = ::Err;
+
+ fn try_from(value: &'a str) -> Result {
+ Ok(Self::from_uri(Uri::from_str(value)?))
+ }
+}
+
+impl IntoResponseParts for HxLocation {
+ type Error = HxError;
+
+ fn into_response_parts(self, mut res: ResponseParts) -> Result {
+ #[cfg(feature = "serde")]
+ let header = self.into_header_with_options()?;
+ #[cfg(not(feature = "serde"))]
+ let header = self.uri.to_string();
+
+ res.headers_mut().insert(
+ headers::HX_LOCATION,
+ HeaderValue::from_maybe_shared(header)?,
+ );
+
+ Ok(res)
+ }
+}
+
+/// More options for `HX-Location` header.
+///
+/// - `source` - the source element of the request
+/// - `event` - an event that “triggered” the request
+/// - `handler` - a callback that will handle the response HTML
+/// - `target` - the target to swap the response into
+/// - `swap` - how the response will be swapped in relative to the target
+/// - `values` - values to submit with the request
+/// - `headers` - headers to submit with the request
+/// - `select` - allows you to select the content you want swapped from a response
+#[cfg(feature = "serde")]
+#[cfg_attr(feature = "unstable", doc(cfg(feature = "serde")))]
+#[derive(Debug, Clone, serde::Serialize, Default)]
+#[non_exhaustive]
+pub struct LocationOptions {
+ /// The source element of the request.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub source: Option,
+ /// An event that "triggered" the request.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub event: Option,
+ /// A callback that will handle the response HTML.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub handler: Option,
+ /// The target to swap the response into.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub target: Option,
+ /// How the response will be swapped in relative to the target.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub swap: Option,
+ /// Values to submit with the request.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub values: Option,
+ /// Headers to submit with the request.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub headers: Option,
+}
+
+#[cfg(feature = "serde")]
+#[cfg_attr(feature = "unstable", doc(cfg(feature = "serde")))]
+impl LocationOptions {
+ pub(super) fn is_default(&self) -> bool {
+ let Self {
+ source: None,
+ event: None,
+ handler: None,
+ target: None,
+ swap: None,
+ values: None,
+ headers: None,
+ } = self
+ else {
+ return false;
+ };
+
+ true
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ #[cfg(feature = "serde")]
+ fn test_serialize_location() {
+ use crate::SwapOption;
+
+ let loc = HxLocation::try_from("/foo").unwrap();
+ assert_eq!(loc.into_header_with_options().unwrap(), "/foo");
+
+ let loc = HxLocation::from_uri_with_options(
+ "/foo".parse().unwrap(),
+ LocationOptions {
+ event: Some("click".into()),
+ swap: Some(SwapOption::InnerHtml),
+ ..Default::default()
+ },
+ );
+ assert_eq!(
+ loc.into_header_with_options().unwrap(),
+ r#"{"path":"/foo","event":"click","swap":"innerHTML"}"#
+ );
+ }
+}
diff --git a/src/responders/serde.rs b/src/responders/serde.rs
deleted file mode 100644
index 18f997e..0000000
--- a/src/responders/serde.rs
+++ /dev/null
@@ -1,59 +0,0 @@
-use serde::Serialize;
-use serde_json::Value;
-
-use crate::SwapOption;
-
-/// More options for `HX-Location` header.
-///
-/// - `source` - the source element of the request
-/// - `event` - an event that “triggered” the request
-/// - `handler` - a callback that will handle the response HTML
-/// - `target` - the target to swap the response into
-/// - `swap` - how the response will be swapped in relative to the target
-/// - `values` - values to submit with the request
-/// - `headers` - headers to submit with the request
-/// - `select` - allows you to select the content you want swapped from a response
-#[derive(Debug, Clone, Serialize, Default)]
-#[non_exhaustive]
-pub struct LocationOptions {
- /// The source element of the request.
- #[serde(skip_serializing_if = "Option::is_none")]
- pub source: Option,
- /// An event that "triggered" the request.
- #[serde(skip_serializing_if = "Option::is_none")]
- pub event: Option,
- /// A callback that will handle the response HTML.
- #[serde(skip_serializing_if = "Option::is_none")]
- pub handler: Option,
- /// The target to swap the response into.
- #[serde(skip_serializing_if = "Option::is_none")]
- pub target: Option,
- /// How the response will be swapped in relative to the target.
- #[serde(skip_serializing_if = "Option::is_none")]
- pub swap: Option,
- /// Values to submit with the request.
- #[serde(skip_serializing_if = "Option::is_none")]
- pub values: Option,
- /// Headers to submit with the request.
- #[serde(skip_serializing_if = "Option::is_none")]
- pub headers: Option,
-}
-
-impl LocationOptions {
- pub(super) fn is_default(&self) -> bool {
- let Self {
- source: None,
- event: None,
- handler: None,
- target: None,
- swap: None,
- values: None,
- headers: None,
- } = self
- else {
- return false;
- };
-
- true
- }
-}
diff --git a/src/responders/trigger.rs b/src/responders/trigger.rs
new file mode 100644
index 0000000..647a75c
--- /dev/null
+++ b/src/responders/trigger.rs
@@ -0,0 +1,214 @@
+use axum_core::response::{IntoResponseParts, ResponseParts};
+
+use crate::{headers, HxError};
+
+/// Represents a client-side event carrying optional data.
+#[derive(Debug, Clone)]
+#[cfg_attr(feature = "serde", derive(serde::Serialize))]
+pub struct HxEvent {
+ pub name: String,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ #[cfg(feature = "serde")]
+ #[cfg_attr(feature = "unstable", doc(cfg(feature = "serde")))]
+ pub data: Option,
+}
+
+impl HxEvent {
+ /// Creates new event with no associated data.
+ pub fn new(name: String) -> Self {
+ Self {
+ name: name.to_string(),
+ #[cfg(feature = "serde")]
+ data: None,
+ }
+ }
+
+ /// Creates new event with event data.
+ #[cfg(feature = "serde")]
+ #[cfg_attr(feature = "unstable", doc(cfg(feature = "serde")))]
+ pub fn new_with_data(
+ name: impl AsRef,
+ data: T,
+ ) -> Result {
+ let data = serde_json::to_value(data)?;
+
+ Ok(Self {
+ name: name.as_ref().to_owned(),
+ #[cfg(feature = "serde")]
+ data: Some(data),
+ })
+ }
+}
+
+impl> From for HxEvent {
+ fn from(name: N) -> Self {
+ Self {
+ name: name.as_ref().to_owned(),
+ #[cfg(feature = "serde")]
+ data: None,
+ }
+ }
+}
+
+#[cfg(not(feature = "serde"))]
+fn events_to_header_value(events: Vec) -> Result {
+ let header = events
+ .into_iter()
+ .map(|HxEvent { name }| name)
+ .collect::>()
+ .join(", ");
+ http::HeaderValue::from_str(&header).map_err(Into::into)
+}
+
+#[cfg(feature = "serde")]
+fn events_to_header_value(events: Vec) -> Result {
+ use std::collections::HashMap;
+
+ use http::HeaderValue;
+ use serde_json::Value;
+
+ 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.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)
+}
+
+/// 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.
+///
+/// See for more information.
+#[derive(Debug, Clone)]
+pub struct HxResponseTrigger(pub Vec);
+
+impl From for HxResponseTrigger
+where
+ T: IntoIterator,
+ T::Item: Into,
+{
+ fn from(value: T) -> Self {
+ Self(value.into_iter().map(Into::into).collect())
+ }
+}
+
+impl IntoResponseParts for HxResponseTrigger {
+ 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.
+///
+/// See for more information.
+#[derive(Debug, Clone)]
+pub struct HxResponseTriggerAfterSettle(pub Vec);
+
+impl From for HxResponseTriggerAfterSettle
+where
+ T: IntoIterator,
+ T::Item: Into,
+{
+ fn from(value: T) -> Self {
+ Self(value.into_iter().map(Into::into).collect())
+ }
+}
+
+impl IntoResponseParts for HxResponseTriggerAfterSettle {
+ 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-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.
+///
+/// See for more information.
+#[derive(Debug, Clone)]
+pub struct HxResponseTriggerAfterSwap(pub Vec);
+
+impl From for HxResponseTriggerAfterSwap
+where
+ T: IntoIterator,
+ T::Item: Into,
+{
+ fn from(value: T) -> Self {
+ Self(value.into_iter().map(Into::into).collect())
+ }
+}
+
+impl IntoResponseParts for HxResponseTriggerAfterSwap {
+ 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)
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use http::HeaderValue;
+ use serde_json::json;
+
+ use super::*;
+
+ #[test]
+ fn valid_event_to_header_encoding() {
+ let evt = HxEvent::new_with_data(
+ "my-event",
+ json!({"level": "info", "message": {
+ "body": "This is a test message.",
+ "title": "Hello, world!",
+ }}),
+ )
+ .unwrap();
+
+ let header_value = events_to_header_value(vec![evt]).unwrap();
+
+ let expected_value = r#"{"my-event":{"level":"info","message":{"body":"This is a test message.","title":"Hello, world!"}}}"#;
+
+ assert_eq!(header_value, HeaderValue::from_static(expected_value));
+ }
+}
From f07518d5a28bf617d43b07d0147918c1772707db Mon Sep 17 00:00:00 2001
From: ItsEthra <107059409+ItsEthra@users.noreply.github.com>
Date: Fri, 1 Dec 2023 22:15:10 +0300
Subject: [PATCH 03/11] Fixed docs
---
Cargo.toml | 3 +++
README.md | 22 ++++++++++------------
src/responders/trigger.rs | 23 ++++++++++++++++-------
3 files changed, 29 insertions(+), 19 deletions(-)
diff --git a/Cargo.toml b/Cargo.toml
index 4882769..6a2cbf8 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -30,5 +30,8 @@ pin-project-lite = { version = "0.2", optional = true }
serde = { version = "1", features = ["derive"], optional = true }
serde_json = { version = "1", optional = true }
+[dev-dependencies]
+axum = { version = "0.7.1", default-features = false }
+
[package.metadata.docs.rs]
all-features = true
diff --git a/README.md b/README.md
index e49a677..1ced135 100644
--- a/README.md
+++ b/README.md
@@ -74,11 +74,9 @@ any of your responses.
| `HX-Reswap` | `HxReswap` | `axum_htmx::responders::SwapOption` |
| `HX-Retarget` | `HxRetarget` | `String` |
| `HX-Reselect` | `HxReselect` | `String` |
-| `HX-Trigger` | `HxResponseTrigger` | `String` or `axum_htmx::serde::HxEvent`* |
-| `HX-Trigger-After-Settle` | `HxResponseTriggerAfterSettle` | `String` or `axum_htmx::serde::HxEvent`* |
-| `HX-Trigger-After-Swap` | `HxResponseTriggerAfterSwap` | `String` or `axum_htmx::serde::HxEvent`* |
-
-_* requires the `serde` feature flag to be enabled._
+| `HX-Trigger` | `HxResponseTrigger` | `axum_htmx::serde::HxEvent` |
+| `HX-Trigger-After-Settle` | `HxResponseTriggerAfterSettle` | `axum_htmx::serde::HxEvent` |
+| `HX-Trigger-After-Swap` | `HxResponseTriggerAfterSwap` | `axum_htmx::serde::HxEvent` |
## Request Guards
@@ -137,7 +135,7 @@ use axum_htmx::HxResponseTrigger;
async fn index() -> (&'static str, HxResponseTrigger) {
(
"Hello, world!",
- HxResponseTrigger(vec!["my-event".to_string()]),
+ HxResponseTrigger::from(["my-event", "second-event"]),
)
}
```
@@ -146,16 +144,16 @@ async fn index() -> (&'static str, HxResponseTrigger) {
can use via the `serde` feature flag and the `HxEvent` type.
```rust
-use axum_htmx::serde::HxEvent;
use serde_json::json;
// Note that we are using `HxResponseTrigger` from the `axum_htmx::serde` module
// instead of the root module.
-use axum_htmx::serde::HxResponseTrigger;
+use axum_htmx::{HxEvent, HxResponseTrigger};
async fn index() -> (&'static str, HxResponseTrigger) {
let event = HxEvent::new_with_data(
"my-event",
+ // May be any object that implements `serde::Serialize`
json!({"level": "info", "message": {
"title": "Hello, world!",
"body": "This is a test message.",
@@ -188,10 +186,10 @@ fn router_two() -> Router {
## Feature Flags
-| Flag | Default | Description | Dependencies |
-|----------|----------|------------------------------------------------------------------------------------|---------------------------------------------|
-| `guards` | Disabled | Adds request guard layers. | `tower`, `futures-core`, `pin-project-lite` |
-| `serde` | Disabled | Adds serde support for the `HxResponseTrigger*` and `HxLocation` response headers. | `serde`, `serde_json` |
+| Flag | Default | Description | Dependencies |
+|----------|----------|------------------------------------------------------------|---------------------------------------------|
+| `guards` | Disabled | Adds request guard layers. | `tower`, `futures-core`, `pin-project-lite` |
+| `serde` | Disabled | Adds serde support for the `HxEvent` and `LocationOptions` | `serde`, `serde_json` |
## Contributing
diff --git a/src/responders/trigger.rs b/src/responders/trigger.rs
index 647a75c..82cb71f 100644
--- a/src/responders/trigger.rs
+++ b/src/responders/trigger.rs
@@ -23,7 +23,7 @@ impl HxEvent {
}
}
- /// Creates new event with event data.
+ /// Creates new event with data.
#[cfg(feature = "serde")]
#[cfg_attr(feature = "unstable", doc(cfg(feature = "serde")))]
pub fn new_with_data(
@@ -116,8 +116,10 @@ impl IntoResponseParts for HxResponseTrigger {
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)?);
+ if !self.0.is_empty() {
+ res.headers_mut()
+ .insert(headers::HX_TRIGGER, events_to_header_value(self.0)?);
+ }
Ok(res)
}
@@ -148,8 +150,10 @@ impl IntoResponseParts for HxResponseTriggerAfterSettle {
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)?);
+ if !self.0.is_empty() {
+ res.headers_mut()
+ .insert(headers::HX_TRIGGER, events_to_header_value(self.0)?);
+ }
Ok(res)
}
@@ -180,8 +184,10 @@ impl IntoResponseParts for HxResponseTriggerAfterSwap {
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)?);
+ if !self.0.is_empty() {
+ res.headers_mut()
+ .insert(headers::HX_TRIGGER, events_to_header_value(self.0)?);
+ }
Ok(res)
}
@@ -210,5 +216,8 @@ mod tests {
let expected_value = r#"{"my-event":{"level":"info","message":{"body":"This is a test message.","title":"Hello, world!"}}}"#;
assert_eq!(header_value, HeaderValue::from_static(expected_value));
+
+ let value = events_to_header_value(HxResponseTrigger::from(["foo", "bar"]).0).unwrap();
+ assert_eq!(value, HeaderValue::from_static("foo, bar"));
}
}
From a04e131a692fcdf18d3fc6f3dabaa3ff1c8194f5 Mon Sep 17 00:00:00 2001
From: ItsEthra <107059409+ItsEthra@users.noreply.github.com>
Date: Sun, 3 Dec 2023 15:03:08 +0300
Subject: [PATCH 04/11] Combined HxResposeTrigger* into one struct
---
README.md | 6 +-
src/responders/trigger.rs | 128 ++++++++++++++------------------------
2 files changed, 51 insertions(+), 83 deletions(-)
diff --git a/README.md b/README.md
index 1ced135..51cf392 100644
--- a/README.md
+++ b/README.md
@@ -74,9 +74,9 @@ any of your responses.
| `HX-Reswap` | `HxReswap` | `axum_htmx::responders::SwapOption` |
| `HX-Retarget` | `HxRetarget` | `String` |
| `HX-Reselect` | `HxReselect` | `String` |
-| `HX-Trigger` | `HxResponseTrigger` | `axum_htmx::serde::HxEvent` |
-| `HX-Trigger-After-Settle` | `HxResponseTriggerAfterSettle` | `axum_htmx::serde::HxEvent` |
-| `HX-Trigger-After-Swap` | `HxResponseTriggerAfterSwap` | `axum_htmx::serde::HxEvent` |
+| `HX-Trigger` | `HxResponseTrigger` | `axum_htmx::serde::HxEvent` |
+| `HX-Trigger-After-Settle` | `HxResponseTrigger` | `axum_htmx::serde::HxEvent` |
+| `HX-Trigger-After-Swap` | `HxResponseTrigger` | `axum_htmx::serde::HxEvent` |
## Request Guards
diff --git a/src/responders/trigger.rs b/src/responders/trigger.rs
index 82cb71f..343d216 100644
--- a/src/responders/trigger.rs
+++ b/src/responders/trigger.rs
@@ -91,24 +91,53 @@ fn events_to_header_value(events: Vec) -> Result for more information.
#[derive(Debug, Clone)]
-pub struct HxResponseTrigger(pub Vec);
+pub struct HxResponseTrigger {
+ pub mode: TriggerMode,
+ pub events: Vec,
+}
-impl From for HxResponseTrigger
-where
- T: IntoIterator,
- T::Item: Into,
-{
- fn from(value: T) -> Self {
- Self(value.into_iter().map(Into::into).collect())
+impl HxResponseTrigger {
+ /// Creates new [trigger](https://htmx.org/headers/hx-trigger/) with specified mode and events.
+ pub fn new>(mode: TriggerMode, events: impl IntoIterator- ) -> Self {
+ Self {
+ mode,
+ events: events.into_iter().map(Into::into).collect(),
+ }
+ }
+
+ /// Creates new [normal](https://htmx.org/headers/hx-trigger/) trigger from events.
+ pub fn normal>(events: impl IntoIterator
- ) -> Self {
+ Self::new(TriggerMode::Normal, events)
+ }
+
+ /// Creates new [after settle](https://htmx.org/headers/hx-trigger/) trigger from events.
+ pub fn after_settle>(events: impl IntoIterator
- ) -> Self {
+ Self::new(TriggerMode::AfterSettle, events)
+ }
+
+ /// Creates new [after swap](https://htmx.org/headers/hx-trigger/) trigger from events.
+ pub fn after_swap>(events: impl IntoIterator
- ) -> Self {
+ Self::new(TriggerMode::AfterSwap, events)
}
}
@@ -116,77 +145,15 @@ impl IntoResponseParts for HxResponseTrigger {
type Error = HxError;
fn into_response_parts(self, mut res: ResponseParts) -> Result {
- if !self.0.is_empty() {
+ if !self.events.is_empty() {
+ let header = match self.mode {
+ TriggerMode::Normal => headers::HX_TRIGGER,
+ TriggerMode::AfterSettle => headers::HX_TRIGGER_AFTER_SETTLE,
+ TriggerMode::AfterSwap => headers::HX_TRIGGER_AFTER_SETTLE,
+ };
+
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.
-///
-/// See for more information.
-#[derive(Debug, Clone)]
-pub struct HxResponseTriggerAfterSettle(pub Vec);
-
-impl From for HxResponseTriggerAfterSettle
-where
- T: IntoIterator,
- T::Item: Into,
-{
- fn from(value: T) -> Self {
- Self(value.into_iter().map(Into::into).collect())
- }
-}
-
-impl IntoResponseParts for HxResponseTriggerAfterSettle {
- type Error = HxError;
-
- fn into_response_parts(self, mut res: ResponseParts) -> Result {
- if !self.0.is_empty() {
- res.headers_mut()
- .insert(headers::HX_TRIGGER, 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.
-///
-/// See for more information.
-#[derive(Debug, Clone)]
-pub struct HxResponseTriggerAfterSwap(pub Vec);
-
-impl From for HxResponseTriggerAfterSwap
-where
- T: IntoIterator,
- T::Item: Into,
-{
- fn from(value: T) -> Self {
- Self(value.into_iter().map(Into::into).collect())
- }
-}
-
-impl IntoResponseParts for HxResponseTriggerAfterSwap {
- type Error = HxError;
-
- fn into_response_parts(self, mut res: ResponseParts) -> Result {
- if !self.0.is_empty() {
- res.headers_mut()
- .insert(headers::HX_TRIGGER, events_to_header_value(self.0)?);
+ .insert(header, events_to_header_value(self.events)?);
}
Ok(res)
@@ -217,7 +184,8 @@ mod tests {
assert_eq!(header_value, HeaderValue::from_static(expected_value));
- let value = events_to_header_value(HxResponseTrigger::from(["foo", "bar"]).0).unwrap();
+ let value =
+ events_to_header_value(HxResponseTrigger::normal(["foo", "bar"]).events).unwrap();
assert_eq!(value, HeaderValue::from_static("foo, bar"));
}
}
From 140a74c0715c2004ebf6af5948983a52ce540f4f Mon Sep 17 00:00:00 2001
From: ItsEthra <107059409+ItsEthra@users.noreply.github.com>
Date: Sun, 3 Dec 2023 15:13:07 +0300
Subject: [PATCH 05/11] Use http::Uri in HxCurrentUrl
---
src/extractors.rs | 14 ++++++++------
1 file changed, 8 insertions(+), 6 deletions(-)
diff --git a/src/extractors.rs b/src/extractors.rs
index 7e7304f..bb366f1 100644
--- a/src/extractors.rs
+++ b/src/extractors.rs
@@ -42,10 +42,10 @@ where
/// This is set on every request made by htmx itself. As its name implies, it
/// just contains the current url.
///
-/// This extractor will always return a value. If the header is not present, it
-/// will return `None`.
+/// This extractor will always return a value. If the header is not present, or extractor fails to parse the url
+/// it will return `None`.
#[derive(Debug, Clone)]
-pub struct HxCurrentUrl(pub Option);
+pub struct HxCurrentUrl(pub Option);
#[async_trait]
impl
FromRequestParts for HxCurrentUrl
@@ -56,9 +56,11 @@ where
async fn from_request_parts(parts: &mut Parts, _: &S) -> Result {
if let Some(url) = parts.headers.get(HX_CURRENT_URL) {
- if let Ok(url) = url.to_str() {
- return Ok(HxCurrentUrl(Some(url.to_string())));
- }
+ let url = url
+ .to_str()
+ .ok()
+ .and_then(|url| url.parse::().ok());
+ return Ok(HxCurrentUrl(url));
}
return Ok(HxCurrentUrl(None));
From d7a8ee55b1ee3428e32c9136c8fc1f0d9c71644a Mon Sep 17 00:00:00 2001
From: ItsEthra <107059409+ItsEthra@users.noreply.github.com>
Date: Sun, 3 Dec 2023 15:16:33 +0300
Subject: [PATCH 06/11] Hidden ResponseFuture type
---
src/guard.rs | 58 ++++++++++++++++++++++++++++------------------------
1 file changed, 31 insertions(+), 27 deletions(-)
diff --git a/src/guard.rs b/src/guard.rs
index d4a8421..73044d8 100644
--- a/src/guard.rs
+++ b/src/guard.rs
@@ -63,7 +63,7 @@ where
{
type Response = S::Response;
type Error = S::Error;
- type Future = ResponseFuture<'a, S::Future>;
+ type Future = private::ResponseFuture<'a, S::Future>;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> {
self.inner.poll_ready(cx)
@@ -77,7 +77,7 @@ where
let response_future = self.inner.call(req);
- ResponseFuture {
+ private::ResponseFuture {
response_future,
hx_request: self.hx_request,
layer: self.layer.clone(),
@@ -85,36 +85,40 @@ where
}
}
-pin_project! {
- pub struct ResponseFuture<'a, F> {
- #[pin]
- response_future: F,
- hx_request: bool,
- layer: HxRequestGuardLayer<'a>,
+mod private {
+ use super::*;
+
+ pin_project! {
+ pub struct ResponseFuture<'a, F> {
+ #[pin]
+ pub(super) response_future: F,
+ pub(super) hx_request: bool,
+ pub(super) layer: HxRequestGuardLayer<'a>,
+ }
}
-}
-impl<'a, F, B, E> Future for ResponseFuture<'a, F>
-where
- F: Future