use axum_core::response::{IntoResponseParts, ResponseParts};
use http::HeaderValue;
use crate::{HxError, headers};
#[derive(Debug, Clone)]
pub struct HxLocation {
pub uri: String,
#[cfg(feature = "serde")]
#[cfg_attr(feature = "unstable", doc(cfg(feature = "serde")))]
pub options: LocationOptions,
}
impl HxLocation {
#[allow(clippy::should_implement_trait)]
pub fn from_str(uri: impl AsRef<str>) -> Self {
Self {
#[cfg(feature = "serde")]
options: LocationOptions::default(),
uri: uri.as_ref().to_string(),
}
}
#[cfg(feature = "serde")]
#[cfg_attr(feature = "unstable", doc(cfg(feature = "serde")))]
pub fn from_str_with_options(uri: impl AsRef<str>, options: LocationOptions) -> Self {
Self {
options,
uri: uri.as_ref().to_string(),
}
}
#[cfg(feature = "serde")]
fn into_header_with_options(self) -> Result<String, HxError> {
if self.options.is_default() {
return Ok(self.uri.to_string());
}
#[derive(::serde::Serialize)]
struct LocWithOpts {
path: String,
#[serde(flatten)]
opts: LocationOptions,
}
let loc_with_opts = LocWithOpts {
path: self.uri.to_string(),
opts: self.options,
};
Ok(serde_json::to_string(&loc_with_opts)?)
}
}
impl<'a> From<&'a str> for HxLocation {
fn from(uri: &'a str) -> Self {
Self::from_str(uri)
}
}
#[cfg(feature = "serde")]
#[cfg_attr(feature = "unstable", doc(cfg(feature = "serde")))]
impl<'a> From<(&'a str, LocationOptions)> for HxLocation {
fn from((uri, options): (&'a str, LocationOptions)) -> Self {
Self::from_str_with_options(uri, options)
}
}
impl IntoResponseParts for HxLocation {
type Error = HxError;
fn into_response_parts(self, mut res: ResponseParts) -> Result<ResponseParts, Self::Error> {
#[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(header)?,
);
Ok(res)
}
}
#[cfg(feature = "serde")]
#[cfg_attr(feature = "unstable", doc(cfg(feature = "serde")))]
#[derive(Debug, Clone, serde::Serialize, Default)]
pub struct LocationOptions {
#[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<crate::SwapOption>,
#[serde(skip_serializing_if = "Option::is_none")]
pub values: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub headers: Option<serde_json::Value>,
#[serde(skip)]
pub non_exhaustive: (),
}
#[cfg(feature = "serde")]
#[cfg_attr(feature = "unstable", doc(cfg(feature = "serde")))]
impl LocationOptions {
pub(super) fn is_default(&self) -> bool {
let Self {
source: None,
event: None,
handler: None,
target: None,
swap: None,
values: None,
headers: None,
non_exhaustive: (),
} = self
else {
return false;
};
true
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[cfg(feature = "serde")]
fn test_serialize_location() {
use crate::SwapOption;
let loc = HxLocation::from("/foo");
assert_eq!(loc.into_header_with_options().unwrap(), "/foo");
let loc = HxLocation::from_str_with_options(
"/foo",
LocationOptions {
event: Some("click".into()),
swap: Some(SwapOption::InnerHtml),
..Default::default()
},
);
assert_eq!(
loc.into_header_with_options().unwrap(),
r#"{"path":"/foo","event":"click","swap":"innerHTML"}"#
);
}
}