1use http::HeaderValue;
2use http::response::Parts;
3use serde::Serialize;
4use serde_json::Value;
5use topcoat_core::{context::Cx, error::Result};
6use topcoat_router::IntoResponseParts;
7
8use crate::SwapOption;
9use crate::header;
10
11#[derive(Debug, Clone, PartialEq, Eq)]
32pub struct HxLocation {
33 pub path: String,
35 pub options: LocationOptions,
37}
38
39impl HxLocation {
40 #[must_use]
42 pub fn new(path: impl Into<String>) -> Self {
43 Self {
44 path: path.into(),
45 options: LocationOptions::default(),
46 }
47 }
48
49 #[must_use]
51 pub fn source(mut self, source: impl Into<String>) -> Self {
52 self.options.source = Some(source.into());
53 self
54 }
55
56 #[must_use]
58 pub fn event(mut self, event: impl Into<String>) -> Self {
59 self.options.event = Some(event.into());
60 self
61 }
62
63 #[must_use]
65 pub fn handler(mut self, handler: impl Into<String>) -> Self {
66 self.options.handler = Some(handler.into());
67 self
68 }
69
70 #[must_use]
72 pub fn target(mut self, target: impl Into<String>) -> Self {
73 self.options.target = Some(target.into());
74 self
75 }
76
77 #[must_use]
79 pub fn swap(mut self, swap: SwapOption) -> Self {
80 self.options.swap = Some(swap);
81 self
82 }
83
84 #[must_use]
86 pub fn select(mut self, select: impl Into<String>) -> Self {
87 self.options.select = Some(select.into());
88 self
89 }
90
91 #[must_use]
93 pub fn values(mut self, values: Value) -> Self {
94 self.options.values = Some(values);
95 self
96 }
97
98 #[must_use]
100 pub fn headers(mut self, headers: Value) -> Self {
101 self.options.headers = Some(headers);
102 self
103 }
104
105 fn header_value(self) -> Result<HeaderValue> {
108 if self.options.is_empty() {
109 return Ok(HeaderValue::from_str(&self.path)?);
110 }
111
112 let mut object = match serde_json::to_value(&self.options)? {
113 Value::Object(map) => map,
114 _ => serde_json::Map::new(),
115 };
116 object.insert("path".to_owned(), Value::String(self.path));
117 Ok(HeaderValue::from_str(&serde_json::to_string(&object)?)?)
118 }
119}
120
121impl From<&str> for HxLocation {
122 fn from(path: &str) -> Self {
123 Self::new(path)
124 }
125}
126
127impl From<String> for HxLocation {
128 fn from(path: String) -> Self {
129 Self::new(path)
130 }
131}
132
133impl IntoResponseParts for HxLocation {
134 fn into_response_parts(self, _cx: &Cx, parts: &mut Parts) -> Result<()> {
135 parts
136 .headers
137 .insert(header::HX_LOCATION, self.header_value()?);
138 Ok(())
139 }
140}
141
142#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize)]
148pub struct LocationOptions {
149 #[serde(skip_serializing_if = "Option::is_none")]
151 pub source: Option<String>,
152 #[serde(skip_serializing_if = "Option::is_none")]
154 pub event: Option<String>,
155 #[serde(skip_serializing_if = "Option::is_none")]
157 pub handler: Option<String>,
158 #[serde(skip_serializing_if = "Option::is_none")]
160 pub target: Option<String>,
161 #[serde(skip_serializing_if = "Option::is_none")]
163 pub swap: Option<SwapOption>,
164 #[serde(skip_serializing_if = "Option::is_none")]
166 pub select: Option<String>,
167 #[serde(skip_serializing_if = "Option::is_none")]
169 pub values: Option<Value>,
170 #[serde(skip_serializing_if = "Option::is_none")]
172 pub headers: Option<Value>,
173}
174
175impl LocationOptions {
176 fn is_empty(&self) -> bool {
179 *self == Self::default()
180 }
181}
182
183#[cfg(test)]
184mod tests {
185 use super::*;
186
187 fn header_value(location: HxLocation) -> String {
188 let mut parts = http::Response::new(()).into_parts().0;
189 location
190 .into_response_parts(&Cx::default(), &mut parts)
191 .unwrap();
192 parts
193 .headers
194 .get(header::HX_LOCATION)
195 .unwrap()
196 .to_str()
197 .unwrap()
198 .to_owned()
199 }
200
201 #[test]
202 fn plain_path_when_no_options() {
203 assert_eq!(header_value(HxLocation::new("/home")), "/home");
204 }
205
206 #[test]
207 fn options_serialize_as_json_with_path() {
208 let value = header_value(
209 HxLocation::new("/home")
210 .target("#main")
211 .swap(SwapOption::InnerHtml),
212 );
213 let json: Value = serde_json::from_str(&value).unwrap();
214 assert_eq!(json["path"], "/home");
215 assert_eq!(json["target"], "#main");
216 assert_eq!(json["swap"], "innerHTML");
217 assert!(json.get("source").is_none());
219 }
220}