use axum::{
http::header::CONTENT_TYPE,
response::{Html, IntoResponse, Response},
};
use std::fmt::Write;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum SwapStrategy {
#[default]
InnerHTML,
OuterHTML,
BeforeEnd,
AfterBegin,
BeforeBegin,
AfterEnd,
Delete,
None,
}
impl SwapStrategy {
#[must_use]
pub const fn as_str(&self) -> &'static str {
match self {
Self::InnerHTML => "innerHTML",
Self::OuterHTML => "outerHTML",
Self::BeforeEnd => "beforeend",
Self::AfterBegin => "afterbegin",
Self::BeforeBegin => "beforebegin",
Self::AfterEnd => "afterend",
Self::Delete => "delete",
Self::None => "none",
}
}
#[must_use]
pub const fn oob_value(&self) -> &'static str {
match self {
Self::InnerHTML => "true", _ => self.as_str(),
}
}
}
#[derive(Debug, Default, Clone)]
pub struct HxSwapOob {
targets: Vec<OobTarget>,
primary_content: Option<String>,
}
#[derive(Debug, Clone)]
struct OobTarget {
id: String,
content: String,
strategy: SwapStrategy,
}
impl HxSwapOob {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_primary(content: impl Into<String>) -> Self {
Self {
targets: Vec::new(),
primary_content: Some(content.into()),
}
}
pub fn set_primary(&mut self, content: impl Into<String>) -> &mut Self {
self.primary_content = Some(content.into());
self
}
pub fn add(&mut self, id: impl Into<String>, content: impl Into<String>, strategy: SwapStrategy) -> &mut Self {
self.targets.push(OobTarget {
id: id.into(),
content: content.into(),
strategy,
});
self
}
#[must_use]
pub fn with(mut self, id: impl Into<String>, content: impl Into<String>, strategy: SwapStrategy) -> Self {
self.add(id, content, strategy);
self
}
pub fn inner_html(&mut self, id: impl Into<String>, content: impl Into<String>) -> &mut Self {
self.add(id, content, SwapStrategy::InnerHTML)
}
pub fn outer_html(&mut self, id: impl Into<String>, content: impl Into<String>) -> &mut Self {
self.add(id, content, SwapStrategy::OuterHTML)
}
pub fn append(&mut self, id: impl Into<String>, content: impl Into<String>) -> &mut Self {
self.add(id, content, SwapStrategy::BeforeEnd)
}
pub fn prepend(&mut self, id: impl Into<String>, content: impl Into<String>) -> &mut Self {
self.add(id, content, SwapStrategy::AfterBegin)
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.targets.is_empty() && self.primary_content.is_none()
}
#[must_use]
pub fn len(&self) -> usize {
self.targets.len()
}
#[must_use]
pub fn render(&self) -> String {
let mut html = String::new();
if let Some(ref primary) = self.primary_content {
html.push_str(primary);
}
for target in &self.targets {
write!(
html,
r#"<div id="{}" hx-swap-oob="{}">{}</div>"#,
target.id,
target.strategy.oob_value(),
target.content
).unwrap();
}
html
}
}
impl IntoResponse for HxSwapOob {
fn into_response(self) -> Response {
let html = self.render();
(
[(CONTENT_TYPE, "text/html; charset=utf-8")],
Html(html),
)
.into_response()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_swap_strategy_as_str() {
assert_eq!(SwapStrategy::InnerHTML.as_str(), "innerHTML");
assert_eq!(SwapStrategy::OuterHTML.as_str(), "outerHTML");
assert_eq!(SwapStrategy::BeforeEnd.as_str(), "beforeend");
assert_eq!(SwapStrategy::Delete.as_str(), "delete");
}
#[test]
fn test_swap_strategy_oob_value() {
assert_eq!(SwapStrategy::InnerHTML.oob_value(), "true");
assert_eq!(SwapStrategy::OuterHTML.oob_value(), "outerHTML");
}
#[test]
fn test_new_empty() {
let oob = HxSwapOob::new();
assert!(oob.is_empty());
assert_eq!(oob.len(), 0);
}
#[test]
fn test_add_target() {
let mut oob = HxSwapOob::new();
oob.add("test-id", "<p>Content</p>", SwapStrategy::InnerHTML);
assert!(!oob.is_empty());
assert_eq!(oob.len(), 1);
}
#[test]
fn test_builder_pattern() {
let oob = HxSwapOob::new()
.with("id1", "content1", SwapStrategy::InnerHTML)
.with("id2", "content2", SwapStrategy::OuterHTML);
assert_eq!(oob.len(), 2);
}
#[test]
fn test_render_single() {
let mut oob = HxSwapOob::new();
oob.add("my-id", "<span>Test</span>", SwapStrategy::InnerHTML);
let html = oob.render();
assert!(html.contains(r#"id="my-id""#));
assert!(html.contains(r#"hx-swap-oob="true""#));
assert!(html.contains("<span>Test</span>"));
}
#[test]
fn test_render_multiple() {
let oob = HxSwapOob::new()
.with("first", "<p>First</p>", SwapStrategy::InnerHTML)
.with("second", "<p>Second</p>", SwapStrategy::OuterHTML);
let html = oob.render();
assert!(html.contains(r#"id="first""#));
assert!(html.contains(r#"id="second""#));
assert!(html.contains(r#"hx-swap-oob="true""#));
assert!(html.contains(r#"hx-swap-oob="outerHTML""#));
}
#[test]
fn test_render_with_primary() {
let oob = HxSwapOob::with_primary("<main>Primary</main>")
.with("sidebar", "<nav>Nav</nav>", SwapStrategy::InnerHTML);
let html = oob.render();
assert!(html.starts_with("<main>Primary</main>"));
assert!(html.contains(r#"id="sidebar""#));
}
#[test]
fn test_convenience_methods() {
let mut oob = HxSwapOob::new();
oob.inner_html("a", "content");
oob.outer_html("b", "content");
oob.append("c", "content");
oob.prepend("d", "content");
assert_eq!(oob.len(), 4);
}
#[test]
fn test_into_response() {
let oob = HxSwapOob::new()
.with("test", "<p>Test</p>", SwapStrategy::InnerHTML);
let response = oob.into_response();
assert_eq!(response.status(), axum::http::StatusCode::OK);
}
}