axum-htmx/src/responders.rs
2024-04-30 10:53:00 +06:00

313 lines
8.8 KiB
Rust

//! Axum responses for htmx response headers.
use std::{convert::Infallible, str::FromStr};
use axum_core::response::{IntoResponseParts, ResponseParts};
use http::{HeaderValue, Uri};
use crate::{headers, HxError};
mod location;
pub use location::*;
mod trigger;
pub use trigger::*;
mod vary;
pub use vary::*;
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-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).
///
/// See <https://htmx.org/headers/hx-push-url/> for more information.
#[derive(Debug, Clone)]
pub struct HxPushUrl(pub Uri);
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())?,
);
Ok(res)
}
}
impl From<Uri> for HxPushUrl {
fn from(uri: Uri) -> Self {
Self(uri)
}
}
impl<'a> TryFrom<&'a str> for HxPushUrl {
type Error = <Uri as FromStr>::Err;
fn try_from(value: &'a str) -> Result<Self, Self::Error> {
Ok(Self(value.parse()?))
}
}
/// 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(pub Uri);
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())?,
);
Ok(res)
}
}
impl From<Uri> for HxRedirect {
fn from(uri: Uri) -> Self {
Self(uri)
}
}
impl<'a> TryFrom<&'a str> for HxRedirect {
type Error = <Uri as FromStr>::Err;
fn try_from(value: &'a str) -> Result<Self, Self::Error> {
Ok(Self(value.parse()?))
}
}
/// 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);
impl From<bool> for HxRefresh {
fn from(value: bool) -> Self {
Self(value)
}
}
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,
if self.0 {
HeaderValue::from_static("true")
} else {
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).
///
/// See <https://htmx.org/headers/hx-replace-url/> for more information.
#[derive(Debug, Clone)]
pub struct HxReplaceUrl(pub Uri);
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())?,
);
Ok(res)
}
}
impl From<Uri> for HxReplaceUrl {
fn from(uri: Uri) -> Self {
Self(uri)
}
}
impl<'a> TryFrom<&'a str> for HxReplaceUrl {
type Error = <Uri as FromStr>::Err;
fn try_from(value: &'a str) -> Result<Self, Self::Error> {
Ok(Self(value.parse()?))
}
}
/// 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);
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());
Ok(res)
}
}
impl From<SwapOption> for HxReswap {
fn from(value: SwapOption) -> Self {
Self(value)
}
}
/// 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(pub String);
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)?,
);
Ok(res)
}
}
impl<T: Into<String>> From<T> for HxRetarget {
fn from(value: T) -> Self {
Self(value.into())
}
}
/// 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(pub String);
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)?,
);
Ok(res)
}
}
impl<T: Into<String>> From<T> for HxReselect {
fn from(value: T) -> Self {
Self(value.into())
}
}
/// 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.
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,
}
// 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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
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<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),
}
}
}