use crate::{Protocol, WebUIHandler};
use webui_handler::route_handler;
use webui_handler::RenderOptions;
webui_handler::define_string_response_writer!(MemWriter, buf);
pub struct ServeRequest<'a> {
render_options: RenderOptions<'a>,
accept_json: bool,
inventory_hex: &'a str,
}
impl<'a> ServeRequest<'a> {
#[must_use]
#[inline]
pub fn new(
render_options: RenderOptions<'a>,
accept_json: bool,
inventory_hex: &'a str,
) -> Self {
Self {
render_options,
accept_json,
inventory_hex,
}
}
}
pub enum ServeResponse {
Html(String),
Json(String),
}
pub fn serve_request(
protocol: &Protocol,
handler: &WebUIHandler,
state: serde_json::Value,
request: &ServeRequest<'_>,
) -> Result<ServeResponse, String> {
let options = &request.render_options;
let params = route_handler::collect_nested_route_params(
protocol,
options.entry_id,
options.request_path,
);
let mut data = state;
if let Some(map) = data.as_object_mut() {
for (key, value) in params {
map.insert(key, serde_json::Value::String(value));
}
}
if request.accept_json {
let partial = protocol
.render_partial(
data,
options.entry_id,
options.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);
handler
.render(protocol, &data, options, &mut writer)
.map_err(|e| format!("render failed: {e}"))?;
Ok(ServeResponse::Html(writer.buf))
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
use std::collections::HashMap;
use webui_handler::plugin::webui::WebUIHydrationPlugin;
use webui_protocol::{
ComponentData, ComponentStyleClosure, CssStrategy, FragmentList, WebUIFragment,
WebUIProtocol,
};
fn structural_fragment(value: &str) -> WebUIFragment {
let mut signal = String::with_capacity("}}}webui:".len() + value.len());
signal.push_str("}}}webui:");
signal.push_str(value);
WebUIFragment::signal(signal, true)
}
fn add_page_style_closures(protocol: &mut WebUIProtocol) {
let closure = ComponentStyleClosure {
component_tags: vec!["my-page".to_string()],
style_chunks: Vec::new(),
};
protocol
.style_closures
.insert("index.html".to_string(), closure.clone());
protocol
.style_closures
.insert("my-page".to_string(), closure);
}
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"),
}
}
fn full_document_protocol() -> Protocol {
let fragments = HashMap::from([
(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<!doctype html><html><head>"),
structural_fragment("head_end"),
WebUIFragment::raw("</head><body>"),
WebUIFragment::component("my-page"),
structural_fragment("body_end"),
WebUIFragment::raw("</body></html>"),
],
contains_boundary: false,
},
),
(
"my-page".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<main>ready</main>")],
contains_boundary: false,
},
),
]);
let mut protocol = WebUIProtocol::new(fragments);
protocol.components.insert(
"my-page".to_string(),
ComponentData {
template_json: r#"{"h":"<main>ready</main>","th":1}"#.to_string(),
template_functions: "[function(){return true}]".to_string(),
..Default::default()
},
);
Protocol::new(protocol)
}
#[test]
fn serve_request_html_forwards_complete_render_options() {
let protocol = full_document_protocol();
let handler = WebUIHandler::with_plugin(|| Box::new(WebUIHydrationPlugin::new()));
let request = ServeRequest::new(
RenderOptions::new("index.html", "/")
.with_nonce("request-nonce")
.with_head_inject("<meta name=\"host-head\">")
.with_body_inject("<script src=\"host.js\"></script>"),
false,
"",
);
let response = serve_request(&protocol, &handler, json!({}), &request)
.expect("full-document response should succeed");
let html = match response {
ServeResponse::Html(html) => html,
ServeResponse::Json(_) => panic!("expected full-document response"),
};
let mut direct = MemWriter::with_capacity(131_072);
handler
.render(&protocol, &json!({}), &request.render_options, &mut direct)
.expect("direct render should succeed");
assert_eq!(html, direct.buf);
assert!(html.contains(r#"<meta name="webui-nonce" content="request-nonce">"#));
assert!(html.contains(r#"<script nonce="request-nonce">(function(){var w=window.__webui"#));
assert!(html.contains(r#"<meta name="host-head">"#));
assert!(html.contains(r#"<script src="host.js"></script>"#));
}
#[test]
fn serve_request_json_partial_carries_module_component_styles() {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![WebUIFragment::component("my-page")],
contains_boundary: false,
},
);
fragments.insert(
"my-page".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<p>page</p>")],
contains_boundary: false,
},
);
let mut protocol = WebUIProtocol::with_tokens(fragments, Vec::new());
protocol.set_css_strategy(webui_protocol::CssStrategy::Module);
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();
add_page_style_closures(&mut protocol);
let protocol = Protocol::new(protocol);
let handler = WebUIHandler::new();
let request = ServeRequest::new(RenderOptions::new("index.html", "/"), true, "");
let response = serve_request(&protocol, &handler, json!({}), &request)
.expect("partial response should succeed");
let json = response_json(response);
assert_eq!(
json["componentStyles"]["resources"]["my-page"],
json!({
"kind": "module",
"specifier": "my-page",
"css": ".page{color:red}"
})
);
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_component_styles() {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![WebUIFragment::component("my-page")],
contains_boundary: false,
},
);
fragments.insert(
"my-page".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<p>page</p>")],
contains_boundary: false,
},
);
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();
add_page_style_closures(&mut protocol);
let protocol = Protocol::new(protocol);
let handler = WebUIHandler::new();
let request = ServeRequest::new(RenderOptions::new("index.html", "/"), true, "");
let response = serve_request(&protocol, &handler, json!({}), &request)
.expect("partial response should succeed");
let json = response_json(response);
assert_eq!(
json["componentStyles"]["resources"]["my-page"],
json!({"kind": "link", "href": "my-page.css"})
);
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_component_styles() {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![WebUIFragment::component("my-page")],
contains_boundary: false,
},
);
fragments.insert(
"my-page".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<p>page</p>")],
contains_boundary: false,
},
);
let mut protocol = WebUIProtocol::with_tokens(fragments, Vec::new());
protocol.set_css_strategy(CssStrategy::Style);
let comp = protocol
.components
.entry("my-page".to_string())
.or_default();
comp.template_json = r#"{"h":"<p/>"}"#.to_string();
comp.css = ".p{color:red}".to_string();
add_page_style_closures(&mut protocol);
let protocol = Protocol::new(protocol);
let handler = WebUIHandler::new();
let request = ServeRequest::new(RenderOptions::new("index.html", "/"), true, "");
let response = serve_request(&protocol, &handler, json!({}), &request)
.expect("partial response should succeed");
let json = response_json(response);
assert_eq!(
json["componentStyles"]["resources"]["my-page"],
json!({"kind": "style", "css": ".p{color:red}"})
);
assert!(
json["templates"].as_object().is_some_and(|a| a.len() == 1),
"Style strategy should still return templates"
);
}
}