use std::collections::HashMap;
use headers_core::{Header, HeaderValue};
use http::{HeaderName, Uri};
use serde::{Deserialize, Serialize};
use super::{convert_header, define_header, string_header, true_header};
use crate::Swap;
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
pub struct AjaxContext {
#[serde(skip_serializing_if = "Option::is_none")]
pub source: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub event: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub handler: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub target: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub swap: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub values: Option<HashMap<String, String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub headers: Option<HashMap<String, String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub select: Option<String>,
}
define_header! {
(HX_LOCATION, "hx-location")
#[derive(Serialize, Deserialize)]
pub struct HxLocation {
#[serde(with = "http_serde::uri")]
pub path: Uri,
#[serde(flatten)]
pub context: Option<AjaxContext>,
}
}
impl Header for HxLocation {
fn name() -> &'static HeaderName {
&HX_LOCATION
}
fn decode<'i, I>(values: &mut I) -> Result<Self, headers_core::Error>
where
Self: Sized,
I: Iterator<Item = &'i HeaderValue>,
{
match (values.next(), values.next()) {
(Some(value), None) => {
serde_json::from_slice(value.as_bytes()).map_err(|_| headers_core::Error::invalid())
}
_ => Err(headers_core::Error::invalid()),
}
}
fn encode<E: Extend<HeaderValue>>(&self, values: &mut E) {
let header = match self {
Self {
path,
context: None,
} => HeaderValue::from_str(&path.to_string()).unwrap(),
Self {
context: Some(_), ..
} => {
let s = serde_json::to_string(self).unwrap();
HeaderValue::from_str(&s).unwrap()
}
};
values.extend(std::iter::once(header));
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum HxModifyHistory<M: HistoryModification> {
Uri(Uri),
NoChange,
#[doc(hidden)]
#[allow(dead_code)]
Phantom(std::marker::PhantomData<M>),
}
pub trait HistoryModification {
fn name() -> &'static HeaderName;
}
define_header! {
(HX_PUSH_URL, "hx-push-url")
#[derive(Copy)]
pub struct HxPushUrl;
}
impl HistoryModification for HxPushUrl {
fn name() -> &'static HeaderName {
&HX_PUSH_URL
}
}
define_header! {
(HX_REPLACE_URL, "hx-replace-url")
#[derive(Copy)]
pub struct HxReplaceUrl;
}
impl HistoryModification for HxReplaceUrl {
fn name() -> &'static HeaderName {
&HX_REPLACE_URL
}
}
impl<M: HistoryModification> Header for HxModifyHistory<M> {
fn name() -> &'static HeaderName {
M::name()
}
fn decode<'i, I>(values: &mut I) -> Result<Self, headers_core::Error>
where
Self: Sized,
I: Iterator<Item = &'i HeaderValue>,
{
match (values.next(), values.next()) {
(Some(value), None) => {
if value == "false" {
Ok(Self::NoChange)
} else {
value
.as_bytes()
.try_into()
.map(Self::Uri)
.map_err(|_| headers_core::Error::invalid())
}
}
_ => Err(headers_core::Error::invalid()),
}
}
fn encode<E: Extend<HeaderValue>>(&self, values: &mut E) {
let header = match self {
Self::Uri(uri) => HeaderValue::from_str(&uri.to_string()).unwrap(),
Self::NoChange => HeaderValue::from_static("false"),
Self::Phantom(_) => return,
};
values.extend(std::iter::once(header));
}
}
convert_header! {
Uri => (HX_REDIRECT, HxRedirect, "hx-redirect")
}
true_header! {
(HX_REFRESH, HxRefresh, "hx-refresh")
}
define_header! {
(HX_RESWAP, "hx-reswap")
#[derive(Copy)]
pub struct HxReswap(pub Swap);
}
impl Header for HxReswap {
fn name() -> &'static HeaderName {
&HX_RESWAP
}
fn decode<'i, I>(values: &mut I) -> Result<Self, headers_core::Error>
where
Self: Sized,
I: Iterator<Item = &'i HeaderValue>,
{
match (values.next(), values.next()) {
(Some(value), None) => value
.as_bytes()
.try_into()
.map(Self)
.map_err(|()| headers_core::Error::invalid()),
_ => Err(headers_core::Error::invalid()),
}
}
fn encode<E: Extend<HeaderValue>>(&self, values: &mut E) {
values.extend(std::iter::once(self.0.into()));
}
}
string_header! {
(HX_RETARGET, HxRetarget, "hx-retarget")
}
string_header! {
(HX_RESELECT, HxReselect, "hx-reselect")
}
define_header! {
(HX_TRIGGER, "hx-trigger")
pub enum HxTrigger<After: TriggerAfter = ()> {
List(Vec<String>),
WithDetails(HashMap<String, serde_json::Value>),
#[doc(hidden)]
#[allow(dead_code)]
Phantom(std::marker::PhantomData<After>),
}
}
pub trait TriggerAfter {
fn name() -> &'static HeaderName;
}
impl TriggerAfter for () {
fn name() -> &'static HeaderName {
&HX_TRIGGER
}
}
define_header! {
(HX_TRIGGER_AFTER_SETTLE, "hx-trigger-after-settle")
#[derive(Copy)]
pub struct AfterSettle;
}
impl TriggerAfter for AfterSettle {
fn name() -> &'static HeaderName {
&HX_TRIGGER_AFTER_SETTLE
}
}
define_header! {
(HX_TRIGGER_AFTER_SWAP, "hx-trigger-after-swap")
#[derive(Copy)]
pub struct AfterSwap;
}
impl TriggerAfter for AfterSwap {
fn name() -> &'static HeaderName {
&HX_TRIGGER_AFTER_SWAP
}
}
impl<After: TriggerAfter> Header for HxTrigger<After> {
fn name() -> &'static HeaderName {
After::name()
}
fn decode<'i, I>(values: &mut I) -> Result<Self, headers_core::Error>
where
Self: Sized,
I: Iterator<Item = &'i HeaderValue>,
{
match (values.next(), values.next()) {
(Some(value), None) => {
let bytes = value.as_bytes();
serde_json::from_slice(bytes)
.map(Self::WithDetails)
.or_else(|_| {
let items = value
.to_str()
.map_err(|_| headers_core::Error::invalid())?
.split(',')
.map(|s| s.trim().to_owned())
.collect();
Ok(Self::List(items))
})
}
_ => Err(headers_core::Error::invalid()),
}
}
fn encode<E: Extend<HeaderValue>>(&self, values: &mut E) {
let val = match self {
Self::List(list) => {
let s = list.join(", ");
HeaderValue::from_str(&s).unwrap()
}
Self::WithDetails(details) => {
let s = serde_json::to_string(details).unwrap();
HeaderValue::from_str(&s).unwrap()
}
Self::Phantom(_) => return,
};
values.extend(std::iter::once(val));
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn trigger_works() {
let val = HeaderValue::from_static(r#"{"event1":"A message", "event2":"Another message"}"#);
claims::assert_ok_eq!(
HxTrigger::<()>::decode(&mut std::iter::once(&val)),
HxTrigger::WithDetails(
vec![
("event1".to_owned(), "A message".into()),
("event2".to_owned(), "Another message".into()),
]
.into_iter()
.collect()
)
);
let val = HeaderValue::from_static("event1, event2");
claims::assert_ok_eq!(
HxTrigger::<()>::decode(&mut std::iter::once(&val)),
HxTrigger::List(vec!["event1".to_owned(), "event2".to_owned()])
);
}
}