mirror of
https://github.com/robertwayne/axum-htmx
synced 2024-11-26 05:09:35 +01:00
provide basic responder functionality without serde, json features with serde feature
This commit is contained in:
parent
a32274e280
commit
ad16b10fc6
4 changed files with 252 additions and 137 deletions
|
@ -11,9 +11,9 @@ version = "0.3.1"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = [ "responders" ]
|
default = [ ]
|
||||||
guards = ["tower", "futures-core", "pin-project-lite"]
|
guards = ["tower", "futures-core", "pin-project-lite"]
|
||||||
responders = ["serde", "serde_json"]
|
serde = [ "dep:serde", "dep:serde_json" ]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
axum = { git = "https://github.com/tokio-rs/axum", branch = "main", default-features = false }
|
axum = { git = "https://github.com/tokio-rs/axum", branch = "main", default-features = false }
|
||||||
|
|
|
@ -4,7 +4,6 @@ pub mod extractors;
|
||||||
#[cfg(feature = "guards")]
|
#[cfg(feature = "guards")]
|
||||||
pub mod guard;
|
pub mod guard;
|
||||||
pub mod headers;
|
pub mod headers;
|
||||||
#[cfg(feature = "responders")]
|
|
||||||
pub mod responders;
|
pub mod responders;
|
||||||
|
|
||||||
use axum::{
|
use axum::{
|
||||||
|
@ -15,5 +14,4 @@ pub use extractors::*;
|
||||||
#[cfg(feature = "guards")]
|
#[cfg(feature = "guards")]
|
||||||
pub use guard::*;
|
pub use guard::*;
|
||||||
pub use headers::*;
|
pub use headers::*;
|
||||||
#[cfg(feature = "responders")]
|
|
||||||
pub use responders::*;
|
pub use responders::*;
|
||||||
|
|
|
@ -1,16 +1,15 @@
|
||||||
//pub struct HxLocation()
|
use std::convert::Infallible;
|
||||||
|
|
||||||
use std::{collections::HashMap, convert::Infallible};
|
|
||||||
|
|
||||||
use axum::{
|
use axum::{
|
||||||
http::{header::InvalidHeaderValue, HeaderValue, StatusCode, Uri},
|
http::{header::InvalidHeaderValue, HeaderValue, StatusCode, Uri},
|
||||||
response::{IntoResponse, IntoResponseParts, ResponseParts},
|
response::{IntoResponse, IntoResponseParts, ResponseParts},
|
||||||
};
|
};
|
||||||
use serde::Serialize;
|
|
||||||
use serde_json::Value;
|
|
||||||
|
|
||||||
use crate::headers;
|
use crate::headers;
|
||||||
|
|
||||||
|
#[cfg(feature = "serde")]
|
||||||
|
pub mod serde;
|
||||||
|
|
||||||
const HX_SWAP_INNER_HTML: &str = "innerHTML";
|
const HX_SWAP_INNER_HTML: &str = "innerHTML";
|
||||||
const HX_SWAP_OUTER_HTML: &str = "outerHTML";
|
const HX_SWAP_OUTER_HTML: &str = "outerHTML";
|
||||||
const HX_SWAP_BEFORE_BEGIN: &str = "beforebegin";
|
const HX_SWAP_BEFORE_BEGIN: &str = "beforebegin";
|
||||||
|
@ -22,62 +21,20 @@ const HX_SWAP_NONE: &str = "none";
|
||||||
|
|
||||||
/// The `HX-Location` header.
|
/// The `HX-Location` header.
|
||||||
///
|
///
|
||||||
/// This response header can be used to trigger a client side redirection without reloading the whole page. Instead of changing the page’s location it will act like following a hx-boost link, creating a new history entry, issuing an ajax request to the value of the header and pushing the path into history.
|
/// This response header can be used to trigger a client side redirection without reloading the whole page.
|
||||||
///
|
///
|
||||||
///
|
/// Will fail if the supplied Uri contains characters that are not visible ASCII (32-127).
|
||||||
#[derive(Debug, Clone, Serialize)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct HxLocation {
|
pub struct HxLocation(Uri);
|
||||||
/// Url to load the response from
|
|
||||||
pub path: String,
|
|
||||||
/// The source element of the request
|
|
||||||
pub source: Option<String>,
|
|
||||||
/// An event that "triggered" the request
|
|
||||||
pub event: Option<String>,
|
|
||||||
/// A callback that will handle the response HTML
|
|
||||||
pub handler: Option<String>,
|
|
||||||
/// The target to swap the response into
|
|
||||||
pub target: Option<String>,
|
|
||||||
/// How the response will be swapped in relative to the target
|
|
||||||
pub swap: Option<SwapOption>,
|
|
||||||
/// Values to submit with the request
|
|
||||||
pub values: Option<Value>,
|
|
||||||
/// headers to submit with the request
|
|
||||||
pub headers: Option<Value>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl HxLocation {
|
|
||||||
pub fn from_uri(uri: &Uri) -> Self {
|
|
||||||
Self {
|
|
||||||
path: uri.to_string(),
|
|
||||||
source: None,
|
|
||||||
event: None,
|
|
||||||
handler: None,
|
|
||||||
target: None,
|
|
||||||
swap: None,
|
|
||||||
values: None,
|
|
||||||
headers: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl IntoResponseParts for HxLocation {
|
impl IntoResponseParts for HxLocation {
|
||||||
type Error = HxError;
|
type Error = HxError;
|
||||||
|
|
||||||
fn into_response_parts(self, mut res: ResponseParts) -> Result<ResponseParts, Self::Error> {
|
fn into_response_parts(self, mut res: ResponseParts) -> Result<ResponseParts, Self::Error> {
|
||||||
let header_value = if self.source.is_none()
|
res.headers_mut().insert(
|
||||||
&& self.event.is_none()
|
headers::HX_LOCATION,
|
||||||
&& self.handler.is_none()
|
HeaderValue::from_maybe_shared(self.0.to_string())?,
|
||||||
&& self.target.is_none()
|
);
|
||||||
&& self.swap.is_none()
|
|
||||||
&& self.values.is_none()
|
|
||||||
&& self.headers.is_none()
|
|
||||||
{
|
|
||||||
HeaderValue::from_str(&self.path)?
|
|
||||||
} else {
|
|
||||||
HeaderValue::from_maybe_shared(serde_json::to_string(&self)?)?
|
|
||||||
};
|
|
||||||
|
|
||||||
res.headers_mut().insert(headers::HX_LOCATION, header_value);
|
|
||||||
Ok(res)
|
Ok(res)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -226,16 +183,23 @@ impl IntoResponseParts for HxReselect {
|
||||||
///
|
///
|
||||||
/// Allows you to trigger client-side events.
|
/// Allows you to trigger client-side events.
|
||||||
///
|
///
|
||||||
/// Will fail if the supplied events contain produce characters that are not visible ASCII (32-127) when serializing to json.
|
/// Will fail if the supplied events contain or produce characters that are not visible ASCII (32-127) when serializing to json.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct HxTrigger(Vec<HxEvent>);
|
pub struct HxTrigger(Vec<String>);
|
||||||
|
|
||||||
impl IntoResponseParts for HxTrigger {
|
impl IntoResponseParts for HxTrigger {
|
||||||
type Error = HxError;
|
type Error = HxError;
|
||||||
|
|
||||||
fn into_response_parts(self, mut res: ResponseParts) -> Result<ResponseParts, Self::Error> {
|
fn into_response_parts(self, mut res: ResponseParts) -> Result<ResponseParts, Self::Error> {
|
||||||
res.headers_mut()
|
res.headers_mut().insert(
|
||||||
.insert(headers::HX_TRIGGER, events_to_header_value(self.0)?);
|
headers::HX_TRIGGER,
|
||||||
|
HeaderValue::from_maybe_shared(
|
||||||
|
self.0
|
||||||
|
.into_iter()
|
||||||
|
.reduce(|acc, e| acc + ", " + &e)
|
||||||
|
.unwrap_or_default(),
|
||||||
|
)?,
|
||||||
|
);
|
||||||
Ok(res)
|
Ok(res)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -244,9 +208,9 @@ impl IntoResponseParts for HxTrigger {
|
||||||
///
|
///
|
||||||
/// Allows you to trigger client-side events after the settle step.
|
/// Allows you to trigger client-side events after the settle step.
|
||||||
///
|
///
|
||||||
/// Will fail if the supplied events contain produce characters that are not visible ASCII (32-127) when serializing to json.
|
/// Will fail if the supplied events contain or produce characters that are not visible ASCII (32-127) when serializing to json.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct HxTriggerAfterSettle(Vec<HxEvent>);
|
pub struct HxTriggerAfterSettle(Vec<String>);
|
||||||
|
|
||||||
impl IntoResponseParts for HxTriggerAfterSettle {
|
impl IntoResponseParts for HxTriggerAfterSettle {
|
||||||
type Error = HxError;
|
type Error = HxError;
|
||||||
|
@ -254,7 +218,12 @@ impl IntoResponseParts for HxTriggerAfterSettle {
|
||||||
fn into_response_parts(self, mut res: ResponseParts) -> Result<ResponseParts, Self::Error> {
|
fn into_response_parts(self, mut res: ResponseParts) -> Result<ResponseParts, Self::Error> {
|
||||||
res.headers_mut().insert(
|
res.headers_mut().insert(
|
||||||
headers::HX_TRIGGER_AFTER_SETTLE,
|
headers::HX_TRIGGER_AFTER_SETTLE,
|
||||||
events_to_header_value(self.0)?,
|
HeaderValue::from_maybe_shared(
|
||||||
|
self.0
|
||||||
|
.into_iter()
|
||||||
|
.reduce(|acc, e| acc + ", " + &e)
|
||||||
|
.unwrap_or_default(),
|
||||||
|
)?,
|
||||||
);
|
);
|
||||||
Ok(res)
|
Ok(res)
|
||||||
}
|
}
|
||||||
|
@ -264,9 +233,9 @@ impl IntoResponseParts for HxTriggerAfterSettle {
|
||||||
///
|
///
|
||||||
/// Allows you to trigger client-side events after the swap step.
|
/// Allows you to trigger client-side events after the swap step.
|
||||||
///
|
///
|
||||||
/// Will fail if the supplied events contain produce characters that are not visible ASCII (32-127) when serializing to json.
|
/// Will fail if the supplied events contain or produce characters that are not visible ASCII (32-127) when serializing to json.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct HxTriggerAfterSwap(Vec<HxEvent>);
|
pub struct HxTriggerAfterSwap(Vec<String>);
|
||||||
|
|
||||||
impl IntoResponseParts for HxTriggerAfterSwap {
|
impl IntoResponseParts for HxTriggerAfterSwap {
|
||||||
type Error = HxError;
|
type Error = HxError;
|
||||||
|
@ -274,13 +243,19 @@ impl IntoResponseParts for HxTriggerAfterSwap {
|
||||||
fn into_response_parts(self, mut res: ResponseParts) -> Result<ResponseParts, Self::Error> {
|
fn into_response_parts(self, mut res: ResponseParts) -> Result<ResponseParts, Self::Error> {
|
||||||
res.headers_mut().insert(
|
res.headers_mut().insert(
|
||||||
headers::HX_TRIGGER_AFTER_SWAP,
|
headers::HX_TRIGGER_AFTER_SWAP,
|
||||||
events_to_header_value(self.0)?,
|
HeaderValue::from_maybe_shared(
|
||||||
|
self.0
|
||||||
|
.into_iter()
|
||||||
|
.reduce(|acc, e| acc + ", " + &e)
|
||||||
|
.unwrap_or_default(),
|
||||||
|
)?,
|
||||||
);
|
);
|
||||||
Ok(res)
|
Ok(res)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Values of the `hx-swap` attribute.
|
/// Values of the `hx-swap` attribute.
|
||||||
|
// serde::Serialize is implemented in responders/serde.rs
|
||||||
#[derive(Debug, Copy, Clone)]
|
#[derive(Debug, Copy, Clone)]
|
||||||
pub enum SwapOption {
|
pub enum SwapOption {
|
||||||
/// Replace the inner html of the target element.
|
/// Replace the inner html of the target element.
|
||||||
|
@ -316,76 +291,10 @@ impl From<SwapOption> for HeaderValue {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// can be removed and automatically derived when https://github.com/serde-rs/serde/issues/2485
|
|
||||||
// is implemented
|
|
||||||
impl Serialize for SwapOption {
|
|
||||||
fn serialize<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),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize)]
|
|
||||||
pub struct HxEvent {
|
|
||||||
pub name: String,
|
|
||||||
pub data: Option<Value>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl HxEvent {
|
|
||||||
pub fn new<T: Serialize>(name: String) -> Self {
|
|
||||||
Self { name, data: None }
|
|
||||||
}
|
|
||||||
pub fn new_with_data<T: Serialize>(name: String, data: T) -> Result<Self, serde_json::Error> {
|
|
||||||
let data = serde_json::to_value(data)?;
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
name,
|
|
||||||
data: Some(data),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn events_to_header_value(events: Vec<HxEvent>) -> Result<HeaderValue, HxError> {
|
|
||||||
let with_data = events.iter().any(|e| e.data.is_some());
|
|
||||||
|
|
||||||
let header_value = if with_data {
|
|
||||||
// at least one event contains data so the header_value needs to be json encoded.
|
|
||||||
let header_value = events
|
|
||||||
.into_iter()
|
|
||||||
.map(|e| (e.name, e.data.map(|d| d.to_string()).unwrap_or_default()))
|
|
||||||
.collect::<HashMap<_, _>>();
|
|
||||||
serde_json::to_string(&header_value)?
|
|
||||||
} else {
|
|
||||||
// no event contains data, the event names can be put in the header value separated
|
|
||||||
// by a comma.
|
|
||||||
events
|
|
||||||
.into_iter()
|
|
||||||
.map(|e| e.name)
|
|
||||||
.reduce(|acc, e| acc + ", " + &e)
|
|
||||||
.unwrap_or_default()
|
|
||||||
};
|
|
||||||
|
|
||||||
HeaderValue::from_maybe_shared(header_value).map_err(HxError::from)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum HxError {
|
pub enum HxError {
|
||||||
InvalidHeaderValue(InvalidHeaderValue),
|
InvalidHeaderValue(InvalidHeaderValue),
|
||||||
|
|
||||||
|
#[cfg(feature = "serde")]
|
||||||
Serialization(serde_json::Error),
|
Serialization(serde_json::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -395,6 +304,7 @@ impl From<InvalidHeaderValue> for HxError {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "serde")]
|
||||||
impl From<serde_json::Error> for HxError {
|
impl From<serde_json::Error> for HxError {
|
||||||
fn from(value: serde_json::Error) -> Self {
|
fn from(value: serde_json::Error) -> Self {
|
||||||
Self::Serialization(value)
|
Self::Serialization(value)
|
||||||
|
@ -407,6 +317,8 @@ impl IntoResponse for HxError {
|
||||||
Self::InvalidHeaderValue(_) => {
|
Self::InvalidHeaderValue(_) => {
|
||||||
(StatusCode::INTERNAL_SERVER_ERROR, "invalid header value").into_response()
|
(StatusCode::INTERNAL_SERVER_ERROR, "invalid header value").into_response()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "serde")]
|
||||||
Self::Serialization(_) => (
|
Self::Serialization(_) => (
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
"failed to serialize event",
|
"failed to serialize event",
|
||||||
|
|
205
src/responders/serde.rs
Normal file
205
src/responders/serde.rs
Normal file
|
@ -0,0 +1,205 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use axum::{
|
||||||
|
http::{HeaderValue, Uri},
|
||||||
|
response::{IntoResponseParts, ResponseParts},
|
||||||
|
};
|
||||||
|
use serde::Serialize;
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
headers,
|
||||||
|
responders::{
|
||||||
|
HX_SWAP_AFTER_BEGIN, HX_SWAP_AFTER_END, HX_SWAP_BEFORE_BEGIN, HX_SWAP_BEFORE_END,
|
||||||
|
HX_SWAP_DELETE, HX_SWAP_INNER_HTML, HX_SWAP_NONE, HX_SWAP_OUTER_HTML,
|
||||||
|
},
|
||||||
|
HxError, SwapOption,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// The `HX-Location` header.
|
||||||
|
///
|
||||||
|
/// This response header can be used to trigger a client side redirection without reloading the whole page. Instead of changing the page’s location it will act like following a hx-boost link, creating a new history entry, issuing an ajax request to the value of the header and pushing the path into history.
|
||||||
|
///
|
||||||
|
/// Will fail if the supplied data contains or produces characters that are not visible ASCII (32-127) when serializing to json.
|
||||||
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
pub struct HxLocation {
|
||||||
|
/// Url to load the response from
|
||||||
|
pub path: String,
|
||||||
|
/// The source element of the request
|
||||||
|
pub source: Option<String>,
|
||||||
|
/// An event that "triggered" the request
|
||||||
|
pub event: Option<String>,
|
||||||
|
/// A callback that will handle the response HTML
|
||||||
|
pub handler: Option<String>,
|
||||||
|
/// The target to swap the response into
|
||||||
|
pub target: Option<String>,
|
||||||
|
/// How the response will be swapped in relative to the target
|
||||||
|
pub swap: Option<SwapOption>,
|
||||||
|
/// Values to submit with the request
|
||||||
|
pub values: Option<Value>,
|
||||||
|
/// headers to submit with the request
|
||||||
|
pub headers: Option<Value>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HxLocation {
|
||||||
|
pub fn from_uri(uri: &Uri) -> Self {
|
||||||
|
Self {
|
||||||
|
path: uri.to_string(),
|
||||||
|
source: None,
|
||||||
|
event: None,
|
||||||
|
handler: None,
|
||||||
|
target: None,
|
||||||
|
swap: None,
|
||||||
|
values: None,
|
||||||
|
headers: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoResponseParts for HxLocation {
|
||||||
|
type Error = HxError;
|
||||||
|
|
||||||
|
fn into_response_parts(self, mut res: ResponseParts) -> Result<ResponseParts, Self::Error> {
|
||||||
|
let header_value = if self.source.is_none()
|
||||||
|
&& self.event.is_none()
|
||||||
|
&& self.handler.is_none()
|
||||||
|
&& self.target.is_none()
|
||||||
|
&& self.swap.is_none()
|
||||||
|
&& self.values.is_none()
|
||||||
|
&& self.headers.is_none()
|
||||||
|
{
|
||||||
|
HeaderValue::from_str(&self.path)?
|
||||||
|
} else {
|
||||||
|
HeaderValue::from_maybe_shared(serde_json::to_string(&self)?)?
|
||||||
|
};
|
||||||
|
|
||||||
|
res.headers_mut().insert(headers::HX_LOCATION, header_value);
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The `HX-Trigger` header.
|
||||||
|
///
|
||||||
|
/// Allows you to trigger client-side events.
|
||||||
|
///
|
||||||
|
/// Will fail if the supplied events contain or produce characters that are not visible ASCII (32-127) when serializing to json.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct HxTrigger(Vec<HxEvent>);
|
||||||
|
|
||||||
|
impl IntoResponseParts for HxTrigger {
|
||||||
|
type Error = HxError;
|
||||||
|
|
||||||
|
fn into_response_parts(self, mut res: ResponseParts) -> Result<ResponseParts, Self::Error> {
|
||||||
|
res.headers_mut()
|
||||||
|
.insert(headers::HX_TRIGGER, events_to_header_value(self.0)?);
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The `HX-Trigger-After-Settle` header.
|
||||||
|
///
|
||||||
|
/// Allows you to trigger client-side events after the settle step.
|
||||||
|
///
|
||||||
|
/// Will fail if the supplied events contain or produce characters that are not visible ASCII (32-127) when serializing to json.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct HxTriggerAfterSettle(Vec<HxEvent>);
|
||||||
|
|
||||||
|
impl IntoResponseParts for HxTriggerAfterSettle {
|
||||||
|
type Error = HxError;
|
||||||
|
|
||||||
|
fn into_response_parts(self, mut res: ResponseParts) -> Result<ResponseParts, Self::Error> {
|
||||||
|
res.headers_mut().insert(
|
||||||
|
headers::HX_TRIGGER_AFTER_SETTLE,
|
||||||
|
events_to_header_value(self.0)?,
|
||||||
|
);
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The `HX-Trigger-After-Swap` header.
|
||||||
|
///
|
||||||
|
/// Allows you to trigger client-side events after the swap step.
|
||||||
|
///
|
||||||
|
/// Will fail if the supplied events contain or produce characters that are not visible ASCII (32-127) when serializing to json.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct HxTriggerAfterSwap(Vec<HxEvent>);
|
||||||
|
|
||||||
|
impl IntoResponseParts for HxTriggerAfterSwap {
|
||||||
|
type Error = HxError;
|
||||||
|
|
||||||
|
fn into_response_parts(self, mut res: ResponseParts) -> Result<ResponseParts, Self::Error> {
|
||||||
|
res.headers_mut().insert(
|
||||||
|
headers::HX_TRIGGER_AFTER_SWAP,
|
||||||
|
events_to_header_value(self.0)?,
|
||||||
|
);
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
pub struct HxEvent {
|
||||||
|
pub name: String,
|
||||||
|
pub data: Option<Value>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HxEvent {
|
||||||
|
pub fn new<T: Serialize>(name: String) -> Self {
|
||||||
|
Self { name, data: None }
|
||||||
|
}
|
||||||
|
pub fn new_with_data<T: Serialize>(name: String, data: T) -> Result<Self, serde_json::Error> {
|
||||||
|
let data = serde_json::to_value(data)?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
name,
|
||||||
|
data: Some(data),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn events_to_header_value(events: Vec<HxEvent>) -> Result<HeaderValue, HxError> {
|
||||||
|
let with_data = events.iter().any(|e| e.data.is_some());
|
||||||
|
|
||||||
|
let header_value = if with_data {
|
||||||
|
// at least one event contains data so the header_value needs to be json encoded.
|
||||||
|
let header_value = events
|
||||||
|
.into_iter()
|
||||||
|
.map(|e| (e.name, e.data.map(|d| d.to_string()).unwrap_or_default()))
|
||||||
|
.collect::<HashMap<_, _>>();
|
||||||
|
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<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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue