1use crate::{Protocol, ResponseWriter, WebUIHandler};
31use webui_handler::route_handler;
32use webui_handler::RenderOptions;
33
34pub struct ServeRequest<'a> {
36 pub path: &'a str,
38 pub accept_json: bool,
41 pub inventory_hex: &'a str,
44}
45
46pub enum ServeResponse {
48 Html(String),
50 Json(String),
52}
53
54pub fn serve_request(
75 protocol: &Protocol,
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 state_json =
93 serde_json::to_string(&data).map_err(|e| format!("state serialization failed: {e}"))?;
94 let partial = protocol
95 .render_partial(&state_json, entry, request.path, request.inventory_hex)
96 .map_err(|e| format!("render_partial failed: {e}"))?;
97 Ok(ServeResponse::Json(partial))
98 } else {
99 let mut writer = MemWriter::with_capacity(131_072);
102 let opts = RenderOptions::new(entry, request.path);
103 handler
104 .render(protocol, &data, &opts, &mut writer)
105 .map_err(|e| format!("render failed: {e}"))?;
106
107 Ok(ServeResponse::Html(writer.buf))
108 }
109}
110
111struct MemWriter {
112 buf: String,
113}
114
115impl MemWriter {
116 fn with_capacity(cap: usize) -> Self {
117 Self {
118 buf: String::with_capacity(cap),
119 }
120 }
121}
122
123impl ResponseWriter for MemWriter {
124 fn write(&mut self, content: &str) -> webui_handler::Result<()> {
125 self.buf.push_str(content);
126 Ok(())
127 }
128
129 fn end(&mut self) -> webui_handler::Result<()> {
130 Ok(())
131 }
132}
133
134#[cfg(test)]
135mod tests {
136 use super::*;
137 use serde_json::json;
138 use std::collections::HashMap;
139 use webui_protocol::{FragmentList, WebUIFragment, WebUIProtocol};
140
141 fn response_json(response: ServeResponse) -> serde_json::Value {
142 match response {
143 ServeResponse::Json(json) => {
144 serde_json::from_str(&json).expect("partial response should be valid JSON")
145 }
146 ServeResponse::Html(_) => panic!("expected JSON partial response"),
147 }
148 }
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_json = r#"{"h":"<p>page</p>"}"#.to_string();
172 component.css = ".page{color:red}".to_string();
173 let protocol = Protocol::new(protocol);
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 = response_json(response);
186
187 assert_eq!(
189 json["templateStyles"].as_array().map(Vec::len),
190 Some(1),
191 "serve_request should include module template styles"
192 );
193 assert!(
195 !json["templates"]["my-page"]["h"]
196 .as_str()
197 .unwrap_or_default()
198 .contains("<style"),
199 "template metadata should not contain module style tags"
200 );
201 }
202
203 #[test]
204 fn serve_request_link_strategy_returns_empty_template_styles() {
205 let mut fragments = HashMap::new();
206 fragments.insert(
207 "index.html".to_string(),
208 FragmentList {
209 fragments: vec![WebUIFragment::component("my-page")],
210 },
211 );
212 fragments.insert(
213 "my-page".to_string(),
214 FragmentList {
215 fragments: vec![WebUIFragment::raw("<p>page</p>")],
216 },
217 );
218
219 let mut protocol = WebUIProtocol::with_tokens(fragments, Vec::new());
220 let comp = protocol
221 .components
222 .entry("my-page".to_string())
223 .or_default();
224 comp.template_json = r#"{"h":"<p>page</p>"}"#.to_string();
225 comp.css_href = "my-page.css".to_string();
226 let protocol = Protocol::new(protocol);
228
229 let handler = WebUIHandler::new();
230 let request = ServeRequest {
231 path: "/",
232 accept_json: true,
233 inventory_hex: "",
234 };
235
236 let response = serve_request(&protocol, &handler, json!({}), "index.html", &request)
237 .expect("partial response should succeed");
238
239 let json = response_json(response);
240
241 assert!(
242 json["templateStyles"]
243 .as_array()
244 .is_some_and(|a| a.is_empty()),
245 "Link strategy should return empty templateStyles"
246 );
247 assert!(
248 json["templates"].as_object().is_some_and(|a| a.len() == 1),
249 "Link strategy should still return templates"
250 );
251 }
252
253 #[test]
254 fn serve_request_style_strategy_returns_empty_template_styles() {
255 let mut fragments = HashMap::new();
256 fragments.insert(
257 "index.html".to_string(),
258 FragmentList {
259 fragments: vec![WebUIFragment::component("my-page")],
260 },
261 );
262 fragments.insert(
263 "my-page".to_string(),
264 FragmentList {
265 fragments: vec![WebUIFragment::raw("<p>page</p>")],
266 },
267 );
268
269 let mut protocol = WebUIProtocol::with_tokens(fragments, Vec::new());
270 let comp = protocol
271 .components
272 .entry("my-page".to_string())
273 .or_default();
274 comp.template_json = r#"{"h":"<style>.p{color:red}</style><p/>"}"#.to_string();
276 let protocol = Protocol::new(protocol);
277
278 let handler = WebUIHandler::new();
279 let request = ServeRequest {
280 path: "/",
281 accept_json: true,
282 inventory_hex: "",
283 };
284
285 let response = serve_request(&protocol, &handler, json!({}), "index.html", &request)
286 .expect("partial response should succeed");
287
288 let json = response_json(response);
289
290 assert!(
291 json["templateStyles"]
292 .as_array()
293 .is_some_and(|a| a.is_empty()),
294 "Style strategy should return empty templateStyles"
295 );
296 assert!(
297 json["templates"].as_object().is_some_and(|a| a.len() == 1),
298 "Style strategy should still return templates"
299 );
300 }
301}