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_json = r#"{"h":"<p>page</p>"}"#.to_string();
175 component.css = ".page{color:red}".to_string();
176
177 let handler = WebUIHandler::new();
178 let request = ServeRequest {
179 path: "/",
180 accept_json: true,
181 inventory_hex: "",
182 };
183
184 let response = serve_request(&protocol, &handler, json!({}), "index.html", &request)
185 .expect("partial response should succeed");
186
187 let json = match response {
188 ServeResponse::Json(value) => value,
189 ServeResponse::Html(_) => panic!("expected JSON partial response"),
190 };
191
192 assert_eq!(
194 json["templateStyles"].as_array().map(Vec::len),
195 Some(1),
196 "serve_request should include module template styles"
197 );
198 assert!(
200 !json["templates"]["my-page"]["h"]
201 .as_str()
202 .unwrap_or_default()
203 .contains("<style"),
204 "template metadata should not contain module style tags"
205 );
206 }
207
208 #[test]
209 fn serve_request_link_strategy_returns_empty_template_styles() {
210 let mut fragments = HashMap::new();
211 fragments.insert(
212 "index.html".to_string(),
213 FragmentList {
214 fragments: vec![WebUIFragment::component("my-page")],
215 },
216 );
217 fragments.insert(
218 "my-page".to_string(),
219 FragmentList {
220 fragments: vec![WebUIFragment::raw("<p>page</p>")],
221 },
222 );
223
224 let mut protocol = WebUIProtocol::with_tokens(fragments, Vec::new());
225 let comp = protocol
226 .components
227 .entry("my-page".to_string())
228 .or_default();
229 comp.template_json = r#"{"h":"<p>page</p>"}"#.to_string();
230 comp.css_href = "my-page.css".to_string();
231 let handler = WebUIHandler::new();
234 let request = ServeRequest {
235 path: "/",
236 accept_json: true,
237 inventory_hex: "",
238 };
239
240 let response = serve_request(&protocol, &handler, json!({}), "index.html", &request)
241 .expect("partial response should succeed");
242
243 let json = match response {
244 ServeResponse::Json(value) => value,
245 ServeResponse::Html(_) => panic!("expected JSON partial response"),
246 };
247
248 assert!(
249 json["templateStyles"]
250 .as_array()
251 .is_some_and(|a| a.is_empty()),
252 "Link strategy should return empty templateStyles"
253 );
254 assert!(
255 json["templates"].as_object().is_some_and(|a| a.len() == 1),
256 "Link strategy should still return templates"
257 );
258 }
259
260 #[test]
261 fn serve_request_style_strategy_returns_empty_template_styles() {
262 let mut fragments = HashMap::new();
263 fragments.insert(
264 "index.html".to_string(),
265 FragmentList {
266 fragments: vec![WebUIFragment::component("my-page")],
267 },
268 );
269 fragments.insert(
270 "my-page".to_string(),
271 FragmentList {
272 fragments: vec![WebUIFragment::raw("<p>page</p>")],
273 },
274 );
275
276 let mut protocol = WebUIProtocol::with_tokens(fragments, Vec::new());
277 let comp = protocol
278 .components
279 .entry("my-page".to_string())
280 .or_default();
281 comp.template_json = r#"{"h":"<style>.p{color:red}</style><p/>"}"#.to_string();
283
284 let handler = WebUIHandler::new();
285 let request = ServeRequest {
286 path: "/",
287 accept_json: true,
288 inventory_hex: "",
289 };
290
291 let response = serve_request(&protocol, &handler, json!({}), "index.html", &request)
292 .expect("partial response should succeed");
293
294 let json = match response {
295 ServeResponse::Json(value) => value,
296 ServeResponse::Html(_) => panic!("expected JSON partial response"),
297 };
298
299 assert!(
300 json["templateStyles"]
301 .as_array()
302 .is_some_and(|a| a.is_empty()),
303 "Style strategy should return empty templateStyles"
304 );
305 assert!(
306 json["templates"].as_object().is_some_and(|a| a.len() == 1),
307 "Style strategy should still return templates"
308 );
309 }
310}