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