use std::convert::Infallible;
use autumn_web::reexports::axum::response::{IntoResponseParts, ResponseParts};
use autumn_web::reexports::http::{HeaderMap, HeaderName, HeaderValue};
use serde::Serialize;
use crate::island::{PropsError, props_json};
pub const PROPS_EVENT: &str = "autumn:react:props";
const HX_TRIGGER: &str = "hx-trigger";
const HX_TRIGGER_AFTER_SETTLE: &str = "hx-trigger-after-settle";
#[derive(Debug, Clone, Default)]
#[must_use]
pub struct PropsUpdate {
entries: Vec<(String, String)>,
after_settle: bool,
}
impl PropsUpdate {
pub const fn new() -> Self {
Self {
entries: Vec::new(),
after_settle: false,
}
}
pub fn set<T: Serialize + ?Sized>(
mut self,
selector: impl Into<String>,
props: &T,
) -> Result<Self, PropsError> {
self.entries.push((selector.into(), props_json(props)?));
Ok(self)
}
pub const fn after_settle(mut self) -> Self {
self.after_settle = true;
self
}
#[must_use]
pub const fn header_name(&self) -> &'static str {
if self.after_settle {
HX_TRIGGER_AFTER_SETTLE
} else {
HX_TRIGGER
}
}
#[must_use]
pub const fn len(&self) -> usize {
self.entries.len()
}
#[must_use]
pub const fn is_empty(&self) -> bool {
self.entries.is_empty()
}
#[must_use]
pub fn to_header_value(&self) -> String {
self.merged_value(&serde_json::Map::new(), &[])
}
pub fn apply_to(&self, headers: &mut HeaderMap) {
if self.is_empty() {
return;
}
let name = HeaderName::from_static(self.header_name());
let (events, earlier) = headers
.get(&name)
.and_then(|value| value.to_str().ok())
.map(existing_events)
.unwrap_or_default();
if let Ok(value) = HeaderValue::from_str(&self.merged_value(&events, &earlier)) {
headers.insert(name, value);
}
}
fn merged_value(
&self,
events: &serde_json::Map<String, serde_json::Value>,
earlier: &[serde_json::Value],
) -> String {
let mut json = String::from("{");
for (event, detail) in events {
json.push_str(&serde_json::Value::String(event.clone()).to_string());
json.push(':');
json.push_str(&detail.to_string());
json.push(',');
}
json.push('"');
json.push_str(PROPS_EVENT);
json.push_str("\":[");
let mut first = true;
for value in earlier {
if !first {
json.push(',');
}
first = false;
json.push_str(&value.to_string());
}
for (selector, props) in &self.entries {
if !first {
json.push(',');
}
first = false;
json.push_str("{\"target\":");
json.push_str(&serde_json::Value::String(selector.clone()).to_string());
json.push_str(",\"props\":");
json.push_str(props);
json.push('}');
}
json.push_str("]}");
ascii_json(&json)
}
}
fn existing_events(
value: &str,
) -> (
serde_json::Map<String, serde_json::Value>,
Vec<serde_json::Value>,
) {
let value = value.trim();
let mut events = if value.starts_with('{') {
match serde_json::from_str(value) {
Ok(serde_json::Value::Object(map)) => map,
_ => serde_json::Map::new(),
}
} else {
value
.split(',')
.map(str::trim)
.filter(|event| !event.is_empty())
.map(|event| (event.to_owned(), serde_json::Value::Null))
.collect()
};
let earlier = match events.remove(PROPS_EVENT) {
Some(serde_json::Value::Array(list)) => list,
_ => Vec::new(),
};
(events, earlier)
}
const HEX: [char; 16] = [
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f',
];
fn push_escape(out: &mut String, c: char) {
for unit in c.encode_utf16(&mut [0; 2]) {
out.push_str("\\u");
for shift in [12, 8, 4, 0] {
out.push(HEX[usize::from((*unit >> shift) & 0xf)]);
}
}
}
fn ascii_json(json: &str) -> String {
let mut out = String::with_capacity(json.len());
let mut in_string = false;
let mut escaped = false;
for c in json.chars() {
if in_string {
if escaped {
escaped = false;
} else if c == '\\' {
escaped = true;
} else if c == '"' {
in_string = false;
}
if (' '..='~').contains(&c) {
out.push(c);
} else {
push_escape(&mut out, c);
}
} else if c == '"' {
in_string = true;
out.push(c);
} else if !c.is_ascii_whitespace() {
out.push(c);
}
}
out
}
impl IntoResponseParts for PropsUpdate {
type Error = Infallible;
fn into_response_parts(self, mut res: ResponseParts) -> Result<ResponseParts, Self::Error> {
self.apply_to(res.headers_mut());
Ok(res)
}
}