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