autumn_plugin_react/
update.rs1use std::convert::Infallible;
4
5use autumn_web::reexports::axum::response::{IntoResponseParts, ResponseParts};
6use autumn_web::reexports::http::{HeaderMap, HeaderName, HeaderValue};
7use serde::Serialize;
8
9use crate::island::{PropsError, props_json};
10
11pub const PROPS_EVENT: &str = "autumn:react:props";
13
14const HX_TRIGGER: &str = "hx-trigger";
15const HX_TRIGGER_AFTER_SETTLE: &str = "hx-trigger-after-settle";
16
17#[derive(Debug, Clone, Default)]
46#[must_use]
47pub struct PropsUpdate {
48 entries: Vec<(String, String)>,
50 after_settle: bool,
51}
52
53impl PropsUpdate {
54 pub const fn new() -> Self {
56 Self {
57 entries: Vec::new(),
58 after_settle: false,
59 }
60 }
61
62 pub fn set<T: Serialize + ?Sized>(
68 mut self,
69 selector: impl Into<String>,
70 props: &T,
71 ) -> Result<Self, PropsError> {
72 self.entries.push((selector.into(), props_json(props)?));
73 Ok(self)
74 }
75
76 pub const fn after_settle(mut self) -> Self {
79 self.after_settle = true;
80 self
81 }
82
83 #[must_use]
86 pub const fn header_name(&self) -> &'static str {
87 if self.after_settle {
88 HX_TRIGGER_AFTER_SETTLE
89 } else {
90 HX_TRIGGER
91 }
92 }
93
94 #[must_use]
96 pub const fn len(&self) -> usize {
97 self.entries.len()
98 }
99
100 #[must_use]
102 pub const fn is_empty(&self) -> bool {
103 self.entries.is_empty()
104 }
105
106 #[must_use]
112 pub fn to_header_value(&self) -> String {
113 self.merged_value(&serde_json::Map::new(), &[])
114 }
115
116 pub fn apply_to(&self, headers: &mut HeaderMap) {
120 if self.is_empty() {
121 return;
122 }
123 let name = HeaderName::from_static(self.header_name());
124 let (events, earlier) = headers
125 .get(&name)
126 .and_then(|value| value.to_str().ok())
127 .map(existing_events)
128 .unwrap_or_default();
129 if let Ok(value) = HeaderValue::from_str(&self.merged_value(&events, &earlier)) {
131 headers.insert(name, value);
132 }
133 }
134
135 fn merged_value(
137 &self,
138 events: &serde_json::Map<String, serde_json::Value>,
139 earlier: &[serde_json::Value],
140 ) -> String {
141 let mut json = String::from("{");
142 for (event, detail) in events {
143 json.push_str(&serde_json::Value::String(event.clone()).to_string());
144 json.push(':');
145 json.push_str(&detail.to_string());
146 json.push(',');
147 }
148 json.push('"');
149 json.push_str(PROPS_EVENT);
150 json.push_str("\":[");
151 let mut first = true;
152 for value in earlier {
153 if !first {
154 json.push(',');
155 }
156 first = false;
157 json.push_str(&value.to_string());
158 }
159 for (selector, props) in &self.entries {
160 if !first {
161 json.push(',');
162 }
163 first = false;
164 json.push_str("{\"target\":");
165 json.push_str(&serde_json::Value::String(selector.clone()).to_string());
166 json.push_str(",\"props\":");
167 json.push_str(props);
168 json.push('}');
169 }
170 json.push_str("]}");
171 ascii_json(&json)
172 }
173}
174
175fn existing_events(
178 value: &str,
179) -> (
180 serde_json::Map<String, serde_json::Value>,
181 Vec<serde_json::Value>,
182) {
183 let value = value.trim();
184 let mut events = if value.starts_with('{') {
185 match serde_json::from_str(value) {
186 Ok(serde_json::Value::Object(map)) => map,
187 _ => serde_json::Map::new(),
188 }
189 } else {
190 value
191 .split(',')
192 .map(str::trim)
193 .filter(|event| !event.is_empty())
194 .map(|event| (event.to_owned(), serde_json::Value::Null))
195 .collect()
196 };
197 let earlier = match events.remove(PROPS_EVENT) {
198 Some(serde_json::Value::Array(list)) => list,
199 _ => Vec::new(),
200 };
201 (events, earlier)
202}
203
204const HEX: [char; 16] = [
205 '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f',
206];
207
208fn push_escape(out: &mut String, c: char) {
209 for unit in c.encode_utf16(&mut [0; 2]) {
210 out.push_str("\\u");
211 for shift in [12, 8, 4, 0] {
212 out.push(HEX[usize::from((*unit >> shift) & 0xf)]);
213 }
214 }
215}
216
217fn ascii_json(json: &str) -> String {
223 let mut out = String::with_capacity(json.len());
224 let mut in_string = false;
225 let mut escaped = false;
226 for c in json.chars() {
227 if in_string {
228 if escaped {
229 escaped = false;
230 } else if c == '\\' {
231 escaped = true;
232 } else if c == '"' {
233 in_string = false;
234 }
235 if (' '..='~').contains(&c) {
236 out.push(c);
237 } else {
238 push_escape(&mut out, c);
239 }
240 } else if c == '"' {
241 in_string = true;
242 out.push(c);
243 } else if !c.is_ascii_whitespace() {
244 out.push(c);
245 }
246 }
247 out
248}
249
250impl IntoResponseParts for PropsUpdate {
251 type Error = Infallible;
252
253 fn into_response_parts(self, mut res: ResponseParts) -> Result<ResponseParts, Self::Error> {
254 self.apply_to(res.headers_mut());
255 Ok(res)
256 }
257}