1use crate::{ResponseWriter, WebUIHandler};
31use webui_handler::route_handler;
32use webui_handler::RenderOptions;
33use webui_protocol::WebUIProtocol;
34
35pub struct ServeRequest<'a> {
37 pub path: &'a str,
39 pub accept_json: bool,
42 pub inventory_hex: &'a str,
45}
46
47pub enum ServeResponse {
49 Html(String),
51 Json(serde_json::Value),
53}
54
55pub fn serve_request(
75 protocol: &WebUIProtocol,
76 handler: &WebUIHandler,
77 state: serde_json::Value,
78 entry: &str,
79 request: &ServeRequest<'_>,
80) -> Result<ServeResponse, String> {
81 let params = route_handler::collect_nested_route_params(protocol, entry, request.path);
83 let mut data = state;
84 if let Some(map) = data.as_object_mut() {
85 for (k, v) in ¶ms {
86 map.insert(k.clone(), serde_json::Value::String(v.clone()));
87 }
88 }
89
90 if request.accept_json {
91 let partial = route_handler::render_partial(
95 protocol,
96 data,
97 entry,
98 request.path,
99 request.inventory_hex,
100 );
101 Ok(ServeResponse::Json(partial))
102 } else {
103 let mut writer = MemWriter::with_capacity(131_072);
105 let opts = RenderOptions::new(entry, request.path);
106 handler
107 .handle(protocol, &data, &opts, &mut writer)
108 .map_err(|e| format!("render failed: {e}"))?;
109
110 let state_json =
111 serde_json::to_string(&data).map_err(|e| format!("serialize failed: {e}"))?;
112 let safe_json = state_json.replace("</", "<\\/");
113 let script = format!("<script>window.__webui_state={safe_json}</script>");
114 let html = writer.buf.replace("</body>", &format!("{script}</body>"));
115
116 Ok(ServeResponse::Html(html))
117 }
118}
119
120struct MemWriter {
121 buf: String,
122}
123
124impl MemWriter {
125 fn with_capacity(cap: usize) -> Self {
126 Self {
127 buf: String::with_capacity(cap),
128 }
129 }
130}
131
132impl ResponseWriter for MemWriter {
133 fn write(&mut self, content: &str) -> webui_handler::Result<()> {
134 self.buf.push_str(content);
135 Ok(())
136 }
137
138 fn end(&mut self) -> webui_handler::Result<()> {
139 Ok(())
140 }
141}
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146 use serde_json::json;
147 use std::collections::HashMap;
148 use webui_protocol::{FragmentList, WebUIFragment};
149
150 #[test]
151 fn serve_request_json_partial_preserves_template_styles() {
152 let mut fragments = HashMap::new();
153 fragments.insert(
154 "index.html".to_string(),
155 FragmentList {
156 fragments: vec![WebUIFragment::component("my-page")],
157 },
158 );
159 fragments.insert(
160 "my-page".to_string(),
161 FragmentList {
162 fragments: vec![WebUIFragment::raw("<p>page</p>")],
163 },
164 );
165
166 let mut protocol = WebUIProtocol::with_tokens(fragments, Vec::new());
167 let component = protocol
168 .components
169 .entry("my-page".to_string())
170 .or_default();
171 component.template =
172 "(function(){window.__webui_templates['my-page']={h:'<p>page</p>'};})();".to_string();
173 component.css = ".page{color:red}".to_string();
174
175 let handler = WebUIHandler::new();
176 let request = ServeRequest {
177 path: "/",
178 accept_json: true,
179 inventory_hex: "",
180 };
181
182 let response = serve_request(&protocol, &handler, json!({}), "index.html", &request)
183 .expect("partial response should succeed");
184
185 let json = match response {
186 ServeResponse::Json(value) => value,
187 ServeResponse::Html(_) => panic!("expected JSON partial response"),
188 };
189
190 assert_eq!(
192 json["templateStyles"].as_array().map(Vec::len),
193 Some(1),
194 "serve_request should include module template styles"
195 );
196 assert!(
198 !json["templates"][0]
199 .as_str()
200 .unwrap_or_default()
201 .contains("<style"),
202 "template scripts should not contain module style tags"
203 );
204 }
205
206 #[test]
207 fn serve_request_link_strategy_returns_empty_template_styles() {
208 let mut fragments = HashMap::new();
209 fragments.insert(
210 "index.html".to_string(),
211 FragmentList {
212 fragments: vec![WebUIFragment::component("my-page")],
213 },
214 );
215 fragments.insert(
216 "my-page".to_string(),
217 FragmentList {
218 fragments: vec![WebUIFragment::raw("<p>page</p>")],
219 },
220 );
221
222 let mut protocol = WebUIProtocol::with_tokens(fragments, Vec::new());
223 let comp = protocol
224 .components
225 .entry("my-page".to_string())
226 .or_default();
227 comp.template = "(function(){})();".to_string();
228 comp.css_href = "/my-page.css".to_string();
229 let handler = WebUIHandler::new();
232 let request = ServeRequest {
233 path: "/",
234 accept_json: true,
235 inventory_hex: "",
236 };
237
238 let response = serve_request(&protocol, &handler, json!({}), "index.html", &request)
239 .expect("partial response should succeed");
240
241 let json = match response {
242 ServeResponse::Json(value) => value,
243 ServeResponse::Html(_) => panic!("expected JSON partial response"),
244 };
245
246 assert!(
247 json["templateStyles"]
248 .as_array()
249 .is_some_and(|a| a.is_empty()),
250 "Link strategy should return empty templateStyles"
251 );
252 assert!(
253 json["templates"].as_array().is_some_and(|a| a.len() == 1),
254 "Link strategy should still return templates"
255 );
256 }
257
258 #[test]
259 fn serve_request_style_strategy_returns_empty_template_styles() {
260 let mut fragments = HashMap::new();
261 fragments.insert(
262 "index.html".to_string(),
263 FragmentList {
264 fragments: vec![WebUIFragment::component("my-page")],
265 },
266 );
267 fragments.insert(
268 "my-page".to_string(),
269 FragmentList {
270 fragments: vec![WebUIFragment::raw("<p>page</p>")],
271 },
272 );
273
274 let mut protocol = WebUIProtocol::with_tokens(fragments, Vec::new());
275 let comp = protocol
276 .components
277 .entry("my-page".to_string())
278 .or_default();
279 comp.template = "(function(){var w=window.__webui_templates;w['my-page']={h:'<style>.p{color:red}</style><p/>'};})();".to_string();
281
282 let handler = WebUIHandler::new();
283 let request = ServeRequest {
284 path: "/",
285 accept_json: true,
286 inventory_hex: "",
287 };
288
289 let response = serve_request(&protocol, &handler, json!({}), "index.html", &request)
290 .expect("partial response should succeed");
291
292 let json = match response {
293 ServeResponse::Json(value) => value,
294 ServeResponse::Html(_) => panic!("expected JSON partial response"),
295 };
296
297 assert!(
298 json["templateStyles"]
299 .as_array()
300 .is_some_and(|a| a.is_empty()),
301 "Style strategy should return empty templateStyles"
302 );
303 assert!(
304 json["templates"].as_array().is_some_and(|a| a.len() == 1),
305 "Style strategy should still return templates"
306 );
307 }
308}