axum_htmx/responders/
location.rs1use axum_core::response::{IntoResponseParts, ResponseParts};
2use http::HeaderValue;
3
4use crate::{HxError, headers};
5
6#[derive(Debug, Clone)]
18pub struct HxLocation {
19 pub uri: String,
21 #[cfg(feature = "serde")]
23 #[cfg_attr(feature = "unstable", doc(cfg(feature = "serde")))]
24 pub options: LocationOptions,
25}
26
27impl HxLocation {
28 #[allow(clippy::should_implement_trait)]
30 pub fn from_str(uri: impl AsRef<str>) -> Self {
31 Self {
32 #[cfg(feature = "serde")]
33 options: LocationOptions::default(),
34 uri: uri.as_ref().to_string(),
35 }
36 }
37
38 #[cfg(feature = "serde")]
40 #[cfg_attr(feature = "unstable", doc(cfg(feature = "serde")))]
41 pub fn from_str_with_options(uri: impl AsRef<str>, options: LocationOptions) -> Self {
42 Self {
43 options,
44 uri: uri.as_ref().to_string(),
45 }
46 }
47
48 #[cfg(feature = "serde")]
49 fn into_header_with_options(self) -> Result<String, HxError> {
50 if self.options.is_default() {
51 return Ok(self.uri.to_string());
52 }
53
54 #[derive(::serde::Serialize)]
55 struct LocWithOpts {
56 path: String,
57 #[serde(flatten)]
58 opts: LocationOptions,
59 }
60
61 let loc_with_opts = LocWithOpts {
62 path: self.uri.to_string(),
63 opts: self.options,
64 };
65
66 Ok(serde_json::to_string(&loc_with_opts)?)
67 }
68}
69
70impl<'a> From<&'a str> for HxLocation {
71 fn from(uri: &'a str) -> Self {
72 Self::from_str(uri)
73 }
74}
75
76#[cfg(feature = "serde")]
77#[cfg_attr(feature = "unstable", doc(cfg(feature = "serde")))]
78impl<'a> From<(&'a str, LocationOptions)> for HxLocation {
79 fn from((uri, options): (&'a str, LocationOptions)) -> Self {
80 Self::from_str_with_options(uri, options)
81 }
82}
83
84impl IntoResponseParts for HxLocation {
85 type Error = HxError;
86
87 fn into_response_parts(self, mut res: ResponseParts) -> Result<ResponseParts, Self::Error> {
88 #[cfg(feature = "serde")]
89 let header = self.into_header_with_options()?;
90 #[cfg(not(feature = "serde"))]
91 let header = self.uri.to_string();
92
93 res.headers_mut().insert(
94 headers::HX_LOCATION,
95 HeaderValue::from_maybe_shared(header)?,
96 );
97
98 Ok(res)
99 }
100}
101
102#[cfg(feature = "serde")]
114#[cfg_attr(feature = "unstable", doc(cfg(feature = "serde")))]
115#[derive(Debug, Clone, serde::Serialize, Default)]
116pub struct LocationOptions {
117 #[serde(skip_serializing_if = "Option::is_none")]
119 pub source: Option<String>,
120 #[serde(skip_serializing_if = "Option::is_none")]
122 pub event: Option<String>,
123 #[serde(skip_serializing_if = "Option::is_none")]
125 pub handler: Option<String>,
126 #[serde(skip_serializing_if = "Option::is_none")]
128 pub target: Option<String>,
129 #[serde(skip_serializing_if = "Option::is_none")]
131 pub swap: Option<crate::SwapOption>,
132 #[serde(skip_serializing_if = "Option::is_none")]
134 pub values: Option<serde_json::Value>,
135 #[serde(skip_serializing_if = "Option::is_none")]
137 pub headers: Option<serde_json::Value>,
138 #[serde(skip)]
141 pub non_exhaustive: (),
142}
143
144#[cfg(feature = "serde")]
145#[cfg_attr(feature = "unstable", doc(cfg(feature = "serde")))]
146impl LocationOptions {
147 pub(super) fn is_default(&self) -> bool {
148 let Self {
149 source: None,
150 event: None,
151 handler: None,
152 target: None,
153 swap: None,
154 values: None,
155 headers: None,
156 non_exhaustive: (),
157 } = self
158 else {
159 return false;
160 };
161
162 true
163 }
164}
165
166#[cfg(test)]
167mod tests {
168 use super::*;
169
170 #[test]
171 #[cfg(feature = "serde")]
172 fn test_serialize_location() {
173 use crate::SwapOption;
174
175 let loc = HxLocation::from("/foo");
176 assert_eq!(loc.into_header_with_options().unwrap(), "/foo");
177
178 let loc = HxLocation::from_str_with_options(
179 "/foo",
180 LocationOptions {
181 event: Some("click".into()),
182 swap: Some(SwapOption::InnerHtml),
183 ..Default::default()
184 },
185 );
186 assert_eq!(
187 loc.into_header_with_options().unwrap(),
188 r#"{"path":"/foo","event":"click","swap":"innerHTML"}"#
189 );
190 }
191}