axum-htmx/src/responders.rs

356 lines
10 KiB
Rust
Raw Normal View History

use std::convert::Infallible;
2023-10-22 15:26:03 +02:00
use axum::{
http::{header::InvalidHeaderValue, HeaderValue, StatusCode, Uri},
response::{IntoResponse, IntoResponseParts, ResponseParts},
};
use crate::headers;
#[cfg(feature = "serde")]
pub mod serde;
2023-10-22 15:26:03 +02:00
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.
///
2023-10-24 02:48:44 +02:00
/// This response header can be used to trigger a client side redirection
/// without reloading the whole page.
2023-10-22 15:26:03 +02:00
///
2023-10-24 02:48:44 +02:00
/// Will fail if the supplied Uri contains characters that are not visible ASCII
/// (32-127).
///
/// See <https://htmx.org/headers/hx-location/> for more information.
#[derive(Debug, Clone)]
pub struct HxLocation(pub Uri);
2023-10-22 15:26:03 +02:00
impl IntoResponseParts for HxLocation {
type Error = HxError;
fn into_response_parts(self, mut res: ResponseParts) -> Result<ResponseParts, Self::Error> {
res.headers_mut().insert(
headers::HX_LOCATION,
HeaderValue::from_maybe_shared(self.0.to_string())?,
);
2023-10-24 02:48:44 +02:00
2023-10-22 15:26:03 +02:00
Ok(res)
}
}
/// The `HX-Push-Url` header.
///
/// Pushes a new url into the history stack.
///
2023-10-24 02:48:44 +02:00
/// Will fail if the supplied Uri contains characters that are not visible ASCII
/// (32-127).
2023-10-22 15:26:03 +02:00
#[derive(Debug, Clone)]
pub struct HxPushUrl(pub Uri);
2023-10-22 15:26:03 +02:00
impl IntoResponseParts for HxPushUrl {
type Error = HxError;
fn into_response_parts(self, mut res: ResponseParts) -> Result<ResponseParts, Self::Error> {
res.headers_mut().insert(
headers::HX_PUSH_URL,
HeaderValue::from_maybe_shared(self.0.to_string())?,
);
2023-10-24 02:48:44 +02:00
2023-10-22 15:26:03 +02:00
Ok(res)
}
}
/// The `HX-Redirect` header.
///
/// Can be used to do a client-side redirect to a new location.
///
2023-10-24 02:48:44 +02:00
/// Will fail if the supplied Uri contains characters that are not visible ASCII
/// (32-127).
2023-10-22 15:26:03 +02:00
#[derive(Debug, Clone)]
pub struct HxRedirect(pub Uri);
2023-10-22 15:26:03 +02:00
impl IntoResponseParts for HxRedirect {
type Error = HxError;
fn into_response_parts(self, mut res: ResponseParts) -> Result<ResponseParts, Self::Error> {
res.headers_mut().insert(
headers::HX_REDIRECT,
HeaderValue::from_maybe_shared(self.0.to_string())?,
);
2023-10-24 02:48:44 +02:00
2023-10-22 15:26:03 +02:00
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(pub bool);
2023-10-22 15:26:03 +02:00
impl IntoResponseParts for HxRefresh {
type Error = Infallible;
fn into_response_parts(self, mut res: ResponseParts) -> Result<ResponseParts, Self::Error> {
res.headers_mut().insert(
headers::HX_REFRESH,
match self.0 {
true => HeaderValue::from_static("true"),
false => HeaderValue::from_static("false"),
},
);
2023-10-24 02:48:44 +02:00
2023-10-22 15:26:03 +02:00
Ok(res)
}
}
/// The `HX-Replace-Url` header.
///
/// Replaces the currelt URL in the location bar.
///
2023-10-24 02:48:44 +02:00
/// Will fail if the supplied Uri contains characters that are not visible ASCII
/// (32-127).
2023-10-22 15:26:03 +02:00
#[derive(Debug, Clone)]
pub struct HxReplaceUrl(pub Uri);
2023-10-22 15:26:03 +02:00
impl IntoResponseParts for HxReplaceUrl {
type Error = HxError;
fn into_response_parts(self, mut res: ResponseParts) -> Result<ResponseParts, Self::Error> {
res.headers_mut().insert(
headers::HX_REPLACE_URL,
HeaderValue::from_maybe_shared(self.0.to_string())?,
);
2023-10-24 02:48:44 +02:00
2023-10-22 15:26:03 +02:00
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(pub SwapOption);
2023-10-22 15:26:03 +02:00
impl IntoResponseParts for HxReswap {
type Error = Infallible;
fn into_response_parts(self, mut res: ResponseParts) -> Result<ResponseParts, Self::Error> {
res.headers_mut().insert(headers::HX_RESWAP, self.0.into());
2023-10-24 02:48:44 +02:00
2023-10-22 15:26:03 +02:00
Ok(res)
}
}
/// The `HX-Retarget` header.
///
2023-10-24 02:48:44 +02:00
/// A CSS selector that updates the target of the content update to a different
/// element on the page.
2023-10-22 15:26:03 +02:00
///
2023-10-24 02:48:44 +02:00
/// Will fail if the supplied String contains characters that are not visible
/// ASCII (32-127).
2023-10-22 15:26:03 +02:00
#[derive(Debug, Clone)]
pub struct HxRetarget(pub String);
2023-10-22 15:26:03 +02:00
impl IntoResponseParts for HxRetarget {
type Error = HxError;
fn into_response_parts(self, mut res: ResponseParts) -> Result<ResponseParts, Self::Error> {
res.headers_mut().insert(
headers::HX_RETARGET,
HeaderValue::from_maybe_shared(self.0)?,
);
2023-10-24 02:48:44 +02:00
2023-10-22 15:26:03 +02:00
Ok(res)
}
}
/// The `HX-Reselect` header.
///
2023-10-24 02:48:44 +02:00
/// 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.
2023-10-22 15:26:03 +02:00
///
2023-10-24 02:48:44 +02:00
/// Will fail if the supplied String contains characters that are not visible
/// ASCII (32-127).
2023-10-22 15:26:03 +02:00
#[derive(Debug, Clone)]
pub struct HxReselect(pub String);
2023-10-22 15:26:03 +02:00
impl IntoResponseParts for HxReselect {
type Error = HxError;
fn into_response_parts(self, mut res: ResponseParts) -> Result<ResponseParts, Self::Error> {
res.headers_mut().insert(
headers::HX_RESELECT,
HeaderValue::from_maybe_shared(self.0)?,
);
2023-10-24 02:48:44 +02:00
2023-10-22 15:26:03 +02:00
Ok(res)
}
}
/// The `HX-Trigger` header.
///
/// Allows you to trigger client-side events.
///
2023-10-24 02:48:44 +02:00
/// Will fail if the supplied events contain or produce characters that are not
/// visible ASCII (32-127) when serializing to json.
2023-10-22 15:26:03 +02:00
#[derive(Debug, Clone)]
pub struct HxResponseTrigger(pub Vec<String>);
2023-10-22 15:26:03 +02:00
impl IntoResponseParts for HxResponseTrigger {
2023-10-22 15:26:03 +02:00
type Error = HxError;
fn into_response_parts(self, mut res: ResponseParts) -> Result<ResponseParts, Self::Error> {
res.headers_mut().insert(
headers::HX_TRIGGER,
HeaderValue::from_maybe_shared(
self.0
.into_iter()
.reduce(|acc, e| acc + ", " + &e)
.unwrap_or_default(),
)?,
);
2023-10-24 02:48:44 +02:00
2023-10-22 15:26:03 +02:00
Ok(res)
}
}
/// The `HX-Trigger-After-Settle` header.
///
/// Allows you to trigger client-side events after the settle step.
///
2023-10-24 02:48:44 +02:00
/// Will fail if the supplied events contain or produce characters that are not
/// visible ASCII (32-127) when serializing to json.
2023-10-22 15:26:03 +02:00
#[derive(Debug, Clone)]
pub struct HxResponseTriggerAfterSettle(pub Vec<String>);
2023-10-22 15:26:03 +02:00
impl IntoResponseParts for HxResponseTriggerAfterSettle {
2023-10-22 15:26:03 +02:00
type Error = HxError;
fn into_response_parts(self, mut res: ResponseParts) -> Result<ResponseParts, Self::Error> {
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(),
)?,
2023-10-22 15:26:03 +02:00
);
2023-10-24 02:48:44 +02:00
2023-10-22 15:26:03 +02:00
Ok(res)
}
}
/// The `HX-Trigger-After-Swap` header.
///
/// Allows you to trigger client-side events after the swap step.
///
2023-10-24 02:48:44 +02:00
/// Will fail if the supplied events contain or produce characters that are not
/// visible ASCII (32-127) when serializing to json.
2023-10-22 15:26:03 +02:00
#[derive(Debug, Clone)]
pub struct HxResponseTriggerAfterSwap(pub Vec<String>);
2023-10-22 15:26:03 +02:00
impl IntoResponseParts for HxResponseTriggerAfterSwap {
2023-10-22 15:26:03 +02:00
type Error = HxError;
fn into_response_parts(self, mut res: ResponseParts) -> Result<ResponseParts, Self::Error> {
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(),
)?,
2023-10-22 15:26:03 +02:00
);
2023-10-24 02:48:44 +02:00
2023-10-22 15:26:03 +02:00
Ok(res)
}
}
/// Values of the `hx-swap` attribute.
// serde::Serialize is implemented in responders/serde.rs
2023-10-22 15:26:03 +02:00
#[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,
2023-10-24 02:48:44 +02:00
/// Does not append content from response (out of band items will still be
/// processed).
2023-10-22 15:26:03 +02:00
None,
}
impl From<SwapOption> 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),
}
}
}
pub enum HxError {
InvalidHeaderValue(InvalidHeaderValue),
#[cfg(feature = "serde")]
2023-10-22 15:26:03 +02:00
Serialization(serde_json::Error),
}
impl From<InvalidHeaderValue> for HxError {
fn from(value: InvalidHeaderValue) -> Self {
Self::InvalidHeaderValue(value)
}
}
#[cfg(feature = "serde")]
2023-10-22 15:26:03 +02:00
impl From<serde_json::Error> 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()
}
#[cfg(feature = "serde")]
2023-10-22 15:26:03 +02:00
Self::Serialization(_) => (
StatusCode::INTERNAL_SERVER_ERROR,
"failed to serialize event",
)
.into_response(),
}
}
}