use crate::{Protocol, ResponseWriter, WebUIHandler};
use webui_handler::route_handler;
use webui_handler::RenderOptions;
pub struct ServeRequest<'a> {
pub path: &'a str,
pub accept_json: bool,
pub inventory_hex: &'a str,
}
pub enum ServeResponse {
Html(String),
Json(String),
}
pub fn serve_request(
protocol: &Protocol,
handler: &WebUIHandler,
state: serde_json::Value,
entry: &str,
request: &ServeRequest<'_>,
) -> Result<ServeResponse, String> {
let params = route_handler::collect_nested_route_params(protocol, entry, request.path);
let mut data = state;
if let Some(map) = data.as_object_mut() {
for (k, v) in ¶ms {
map.insert(k.clone(), serde_json::Value::String(v.clone()));
}
}
if request.accept_json {
let state_json =
serde_json::to_string(&data).map_err(|e| format!("state serialization failed: {e}"))?;
let partial = protocol
.render_partial(&state_json, entry, request.path, request.inventory_hex)
.map_err(|e| format!("render_partial failed: {e}"))?;
Ok(ServeResponse::Json(partial))
} else {
let mut writer = MemWriter::with_capacity(131_072);
let opts = RenderOptions::new(entry, request.path);
handler
.render(protocol, &data, &opts, &mut writer)
.map_err(|e| format!("render failed: {e}"))?;
Ok(ServeResponse::Html(writer.buf))
}
}
struct MemWriter {
buf: String,
}
impl MemWriter {
fn with_capacity(cap: usize) -> Self {
Self {
buf: String::with_capacity(cap),
}
}
}
impl ResponseWriter for MemWriter {
fn write(&mut self, content: &str) -> webui_handler::Result<()> {
self.buf.push_str(content);
Ok(())
}
fn end(&mut self) -> webui_handler::Result<()> {
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
use std::collections::HashMap;
use webui_protocol::{FragmentList, WebUIFragment, WebUIProtocol};
fn response_json(response: ServeResponse) -> serde_json::Value {
match response {
ServeResponse::Json(json) => {
serde_json::from_str(&json).expect("partial response should be valid JSON")
}
ServeResponse::Html(_) => panic!("expected JSON partial response"),
}
}
#[test]
fn serve_request_json_partial_preserves_template_styles() {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![WebUIFragment::component("my-page")],
},
);
fragments.insert(
"my-page".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<p>page</p>")],
},
);
let mut protocol = WebUIProtocol::with_tokens(fragments, Vec::new());
let component = protocol
.components
.entry("my-page".to_string())
.or_default();
component.template_json = r#"{"h":"<p>page</p>"}"#.to_string();
component.css = ".page{color:red}".to_string();
let protocol = Protocol::new(protocol);
let handler = WebUIHandler::new();
let request = ServeRequest {
path: "/",
accept_json: true,
inventory_hex: "",
};
let response = serve_request(&protocol, &handler, json!({}), "index.html", &request)
.expect("partial response should succeed");
let json = response_json(response);
assert_eq!(
json["templateStyles"].as_array().map(Vec::len),
Some(1),
"serve_request should include module template styles"
);
assert!(
!json["templates"]["my-page"]["h"]
.as_str()
.unwrap_or_default()
.contains("<style"),
"template metadata should not contain module style tags"
);
}
#[test]
fn serve_request_link_strategy_returns_empty_template_styles() {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![WebUIFragment::component("my-page")],
},
);
fragments.insert(
"my-page".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<p>page</p>")],
},
);
let mut protocol = WebUIProtocol::with_tokens(fragments, Vec::new());
let comp = protocol
.components
.entry("my-page".to_string())
.or_default();
comp.template_json = r#"{"h":"<p>page</p>"}"#.to_string();
comp.css_href = "my-page.css".to_string();
let protocol = Protocol::new(protocol);
let handler = WebUIHandler::new();
let request = ServeRequest {
path: "/",
accept_json: true,
inventory_hex: "",
};
let response = serve_request(&protocol, &handler, json!({}), "index.html", &request)
.expect("partial response should succeed");
let json = response_json(response);
assert!(
json["templateStyles"]
.as_array()
.is_some_and(|a| a.is_empty()),
"Link strategy should return empty templateStyles"
);
assert!(
json["templates"].as_object().is_some_and(|a| a.len() == 1),
"Link strategy should still return templates"
);
}
#[test]
fn serve_request_style_strategy_returns_empty_template_styles() {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![WebUIFragment::component("my-page")],
},
);
fragments.insert(
"my-page".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<p>page</p>")],
},
);
let mut protocol = WebUIProtocol::with_tokens(fragments, Vec::new());
let comp = protocol
.components
.entry("my-page".to_string())
.or_default();
comp.template_json = r#"{"h":"<style>.p{color:red}</style><p/>"}"#.to_string();
let protocol = Protocol::new(protocol);
let handler = WebUIHandler::new();
let request = ServeRequest {
path: "/",
accept_json: true,
inventory_hex: "",
};
let response = serve_request(&protocol, &handler, json!({}), "index.html", &request)
.expect("partial response should succeed");
let json = response_json(response);
assert!(
json["templateStyles"]
.as_array()
.is_some_and(|a| a.is_empty()),
"Style strategy should return empty templateStyles"
);
assert!(
json["templates"].as_object().is_some_and(|a| a.len() == 1),
"Style strategy should still return templates"
);
}
}