1use axum::{
10 http::header::CONTENT_TYPE,
11 response::{Html, IntoResponse, Response},
12};
13use std::fmt::Write;
14
15#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
19pub enum SwapStrategy {
20 #[default]
22 InnerHTML,
23 OuterHTML,
25 BeforeEnd,
27 AfterBegin,
29 BeforeBegin,
31 AfterEnd,
33 Delete,
35 None,
37}
38
39impl SwapStrategy {
40 #[must_use]
42 pub const fn as_str(&self) -> &'static str {
43 match self {
44 Self::InnerHTML => "innerHTML",
45 Self::OuterHTML => "outerHTML",
46 Self::BeforeEnd => "beforeend",
47 Self::AfterBegin => "afterbegin",
48 Self::BeforeBegin => "beforebegin",
49 Self::AfterEnd => "afterend",
50 Self::Delete => "delete",
51 Self::None => "none",
52 }
53 }
54
55 #[must_use]
57 pub const fn oob_value(&self) -> &'static str {
58 match self {
59 Self::InnerHTML => "true", _ => self.as_str(),
61 }
62 }
63}
64
65#[derive(Debug, Default, Clone)]
88pub struct HxSwapOob {
89 targets: Vec<OobTarget>,
90 primary_content: Option<String>,
92}
93
94#[derive(Debug, Clone)]
95struct OobTarget {
96 id: String,
97 content: String,
98 strategy: SwapStrategy,
99}
100
101impl HxSwapOob {
102 #[must_use]
104 pub fn new() -> Self {
105 Self::default()
106 }
107
108 #[must_use]
113 pub fn with_primary(content: impl Into<String>) -> Self {
114 Self {
115 targets: Vec::new(),
116 primary_content: Some(content.into()),
117 }
118 }
119
120 pub fn set_primary(&mut self, content: impl Into<String>) -> &mut Self {
122 self.primary_content = Some(content.into());
123 self
124 }
125
126 pub fn add(&mut self, id: impl Into<String>, content: impl Into<String>, strategy: SwapStrategy) -> &mut Self {
134 self.targets.push(OobTarget {
135 id: id.into(),
136 content: content.into(),
137 strategy,
138 });
139 self
140 }
141
142 #[must_use]
144 pub fn with(mut self, id: impl Into<String>, content: impl Into<String>, strategy: SwapStrategy) -> Self {
145 self.add(id, content, strategy);
146 self
147 }
148
149 pub fn inner_html(&mut self, id: impl Into<String>, content: impl Into<String>) -> &mut Self {
151 self.add(id, content, SwapStrategy::InnerHTML)
152 }
153
154 pub fn outer_html(&mut self, id: impl Into<String>, content: impl Into<String>) -> &mut Self {
156 self.add(id, content, SwapStrategy::OuterHTML)
157 }
158
159 pub fn append(&mut self, id: impl Into<String>, content: impl Into<String>) -> &mut Self {
161 self.add(id, content, SwapStrategy::BeforeEnd)
162 }
163
164 pub fn prepend(&mut self, id: impl Into<String>, content: impl Into<String>) -> &mut Self {
166 self.add(id, content, SwapStrategy::AfterBegin)
167 }
168
169 #[must_use]
171 pub fn is_empty(&self) -> bool {
172 self.targets.is_empty() && self.primary_content.is_none()
173 }
174
175 #[must_use]
177 pub fn len(&self) -> usize {
178 self.targets.len()
179 }
180
181 #[must_use]
183 pub fn render(&self) -> String {
184 let mut html = String::new();
185
186 if let Some(ref primary) = self.primary_content {
188 html.push_str(primary);
189 }
190
191 for target in &self.targets {
193 write!(
194 html,
195 r#"<div id="{}" hx-swap-oob="{}">{}</div>"#,
196 target.id,
197 target.strategy.oob_value(),
198 target.content
199 ).unwrap();
200 }
201
202 html
203 }
204}
205
206impl IntoResponse for HxSwapOob {
207 fn into_response(self) -> Response {
208 let html = self.render();
209 (
210 [(CONTENT_TYPE, "text/html; charset=utf-8")],
211 Html(html),
212 )
213 .into_response()
214 }
215}
216
217#[cfg(test)]
218mod tests {
219 use super::*;
220
221 #[test]
222 fn test_swap_strategy_as_str() {
223 assert_eq!(SwapStrategy::InnerHTML.as_str(), "innerHTML");
224 assert_eq!(SwapStrategy::OuterHTML.as_str(), "outerHTML");
225 assert_eq!(SwapStrategy::BeforeEnd.as_str(), "beforeend");
226 assert_eq!(SwapStrategy::Delete.as_str(), "delete");
227 }
228
229 #[test]
230 fn test_swap_strategy_oob_value() {
231 assert_eq!(SwapStrategy::InnerHTML.oob_value(), "true");
232 assert_eq!(SwapStrategy::OuterHTML.oob_value(), "outerHTML");
233 }
234
235 #[test]
236 fn test_new_empty() {
237 let oob = HxSwapOob::new();
238 assert!(oob.is_empty());
239 assert_eq!(oob.len(), 0);
240 }
241
242 #[test]
243 fn test_add_target() {
244 let mut oob = HxSwapOob::new();
245 oob.add("test-id", "<p>Content</p>", SwapStrategy::InnerHTML);
246
247 assert!(!oob.is_empty());
248 assert_eq!(oob.len(), 1);
249 }
250
251 #[test]
252 fn test_builder_pattern() {
253 let oob = HxSwapOob::new()
254 .with("id1", "content1", SwapStrategy::InnerHTML)
255 .with("id2", "content2", SwapStrategy::OuterHTML);
256
257 assert_eq!(oob.len(), 2);
258 }
259
260 #[test]
261 fn test_render_single() {
262 let mut oob = HxSwapOob::new();
263 oob.add("my-id", "<span>Test</span>", SwapStrategy::InnerHTML);
264
265 let html = oob.render();
266 assert!(html.contains(r#"id="my-id""#));
267 assert!(html.contains(r#"hx-swap-oob="true""#));
268 assert!(html.contains("<span>Test</span>"));
269 }
270
271 #[test]
272 fn test_render_multiple() {
273 let oob = HxSwapOob::new()
274 .with("first", "<p>First</p>", SwapStrategy::InnerHTML)
275 .with("second", "<p>Second</p>", SwapStrategy::OuterHTML);
276
277 let html = oob.render();
278 assert!(html.contains(r#"id="first""#));
279 assert!(html.contains(r#"id="second""#));
280 assert!(html.contains(r#"hx-swap-oob="true""#));
281 assert!(html.contains(r#"hx-swap-oob="outerHTML""#));
282 }
283
284 #[test]
285 fn test_render_with_primary() {
286 let oob = HxSwapOob::with_primary("<main>Primary</main>")
287 .with("sidebar", "<nav>Nav</nav>", SwapStrategy::InnerHTML);
288
289 let html = oob.render();
290 assert!(html.starts_with("<main>Primary</main>"));
291 assert!(html.contains(r#"id="sidebar""#));
292 }
293
294 #[test]
295 fn test_convenience_methods() {
296 let mut oob = HxSwapOob::new();
297 oob.inner_html("a", "content");
298 oob.outer_html("b", "content");
299 oob.append("c", "content");
300 oob.prepend("d", "content");
301
302 assert_eq!(oob.len(), 4);
303 }
304
305 #[test]
306 fn test_into_response() {
307 let oob = HxSwapOob::new()
308 .with("test", "<p>Test</p>", SwapStrategy::InnerHTML);
309
310 let response = oob.into_response();
311 assert_eq!(response.status(), axum::http::StatusCode::OK);
312 }
313}