use crate::route_matcher;
use serde_json::Value;
use std::collections::{HashMap, HashSet};
use webui_protocol::{web_ui_fragment::Fragment, WebUIFragment, WebUIFragmentRoute, WebUIProtocol};
pub fn build_component_index(protocol: &WebUIProtocol) -> HashMap<String, u32> {
let mut names: HashSet<&String> = HashSet::new();
for key in protocol.fragments.keys() {
if key.contains('-') {
names.insert(key);
}
}
let mut sorted: Vec<&String> = names.into_iter().collect();
sorted.sort_unstable();
sorted
.iter()
.enumerate()
.map(|(i, n)| ((*n).clone(), i as u32))
.collect()
}
fn has_component(inventory: &[u8], index: u32) -> bool {
let byte_idx = (index / 8) as usize;
let bit_idx = index % 8;
byte_idx < inventory.len() && inventory[byte_idx] & (1 << bit_idx) != 0
}
fn set_component(inventory: &mut Vec<u8>, index: u32) {
let byte_idx = (index / 8) as usize;
let bit_idx = index % 8;
if byte_idx >= inventory.len() {
inventory.resize(byte_idx + 1, 0);
}
inventory[byte_idx] |= 1 << bit_idx;
}
pub fn parse_inventory(hex: &str) -> Vec<u8> {
let mut bytes = Vec::with_capacity(hex.len() / 2);
let mut chars = hex.bytes();
while let (Some(hi), Some(lo)) = (chars.next(), chars.next()) {
if let (Some(h), Some(l)) = (hex_nibble(hi), hex_nibble(lo)) {
bytes.push((h << 4) | l);
}
}
bytes
}
fn hex_nibble(byte: u8) -> Option<u8> {
match byte {
b'0'..=b'9' => Some(byte - b'0'),
b'a'..=b'f' => Some(byte - b'a' + 10),
b'A'..=b'F' => Some(byte - b'A' + 10),
_ => None,
}
}
pub fn encode_inventory(inv: &[u8]) -> String {
inv.iter()
.fold(String::with_capacity(inv.len() * 2), |mut acc, b| {
use std::fmt::Write;
let _ = write!(acc, "{b:02x}");
acc
})
}
pub fn get_needed_components(
protocol: &WebUIProtocol,
entry_id: &str,
inventory_hex: &str,
) -> (Vec<String>, String) {
let component_names = collect_inventoryable_components(protocol, entry_id, None, true);
let index = build_component_index(protocol);
filter_needed_components(&component_names, inventory_hex, &index)
}
pub fn get_needed_components_for_request(
protocol: &WebUIProtocol,
entry_id: &str,
request_path: &str,
inventory_hex: &str,
) -> (Vec<String>, String) {
let component_names =
collect_inventoryable_components(protocol, entry_id, Some(request_path), false);
let index = build_component_index(protocol);
filter_needed_components(&component_names, inventory_hex, &index)
}
#[must_use]
pub fn filter_needed_components(
component_names: &HashSet<String>,
inventory_hex: &str,
index: &HashMap<String, u32>,
) -> (Vec<String>, String) {
let client_inv = parse_inventory(inventory_hex);
let mut updated_inv = client_inv.clone();
let mut ordered_names: Vec<&String> = component_names.iter().collect();
ordered_names.sort_unstable();
let mut needed = Vec::with_capacity(ordered_names.len());
for name in ordered_names {
if let Some(&idx) = index.get(name.as_str()) {
if !has_component(&client_inv, idx) {
needed.push(name.clone());
}
set_component(&mut updated_inv, idx);
} else {
needed.push(name.clone());
}
}
(needed, encode_inventory(&updated_inv))
}
#[derive(Debug)]
struct QueuedFragment {
id: String,
inventoryable: bool,
route_base: String,
}
fn collect_inventoryable_components(
protocol: &WebUIProtocol,
entry_id: &str,
request_path: Option<&str>,
root_inventoryable: bool,
) -> HashSet<String> {
let mut visited_fragments = HashSet::new();
let mut component_ids = HashSet::new();
let mut stack = vec![QueuedFragment {
id: entry_id.to_string(),
inventoryable: root_inventoryable,
route_base: "/".to_string(),
}];
while let Some(queued) = stack.pop() {
if queued.id.is_empty() {
continue;
}
if queued.inventoryable {
component_ids.insert(queued.id.clone());
}
if !visited_fragments.insert(queued.id.clone()) {
continue;
}
let Some(frag_list) = protocol.fragments.get(&queued.id) else {
continue;
};
let matched_route = request_path
.and_then(|path| find_best_route_match(&frag_list.fragments, path, &queued.route_base));
for frag in &frag_list.fragments {
match frag.fragment.as_ref() {
Some(Fragment::Component(component)) => {
stack.push(QueuedFragment {
id: component.fragment_id.clone(),
inventoryable: true,
route_base: queued.route_base.clone(),
});
}
Some(Fragment::ForLoop(for_loop)) => {
stack.push(QueuedFragment {
id: for_loop.fragment_id.clone(),
inventoryable: false,
route_base: queued.route_base.clone(),
});
}
Some(Fragment::IfCond(if_cond)) => {
stack.push(QueuedFragment {
id: if_cond.fragment_id.clone(),
inventoryable: false,
route_base: queued.route_base.clone(),
});
}
Some(Fragment::Attribute(attr)) if !attr.template.is_empty() => {
stack.push(QueuedFragment {
id: attr.template.clone(),
inventoryable: false,
route_base: queued.route_base.clone(),
});
}
Some(Fragment::Route(route_frag)) => {
let is_selected = matched_route
.as_ref()
.is_some_and(|(best_key, _)| best_key == route_fragment_key(route_frag));
if is_selected && !route_frag.fragment_id.is_empty() {
let child_route_base = if let Some((_, ref rm)) = matched_route {
if let Some(path) = request_path {
route_matcher::compute_route_base(path, rm.consumed_segments)
} else {
queued.route_base.clone()
}
} else {
queued.route_base.clone()
};
stack.push(QueuedFragment {
id: route_frag.fragment_id.clone(),
inventoryable: protocol
.components
.get(&route_frag.fragment_id)
.is_some_and(|c| !c.template.is_empty()),
route_base: child_route_base.clone(),
});
if !route_frag.children.is_empty() {
if let Some(path) = request_path {
walk_route_children(
&route_frag.children,
path,
&child_route_base,
protocol,
&mut stack,
);
}
}
}
}
_ => {}
}
}
}
component_ids
}
fn walk_route_children(
children: &[WebUIFragmentRoute],
request_path: &str,
route_base: &str,
protocol: &WebUIProtocol,
stack: &mut Vec<QueuedFragment>,
) {
let mut current = children;
let mut base = route_base.to_string();
loop {
let mut best: Option<(usize, route_matcher::RouteMatch)> = None;
for (idx, child) in current.iter().enumerate() {
let resolved = route_matcher::resolve_route_path(&child.path, &base);
if let Some(m) = route_matcher::match_single_route(&resolved, request_path, child.exact)
{
let is_better = best
.as_ref()
.is_none_or(|(_, prev)| m.specificity > prev.specificity);
if is_better {
best = Some((idx, m));
}
}
}
let Some((idx, ref rm)) = best else { break };
let matched = ¤t[idx];
if matched.fragment_id.is_empty() {
break;
}
let child_base = route_matcher::compute_route_base(request_path, rm.consumed_segments);
stack.push(QueuedFragment {
id: matched.fragment_id.clone(),
inventoryable: protocol
.components
.get(&matched.fragment_id)
.is_some_and(|c| !c.template.is_empty()),
route_base: child_base.clone(),
});
if matched.children.is_empty() {
break;
}
current = &matched.children;
base = child_base;
}
}
fn find_best_route_match(
fragments: &[WebUIFragment],
request_path: &str,
route_base: &str,
) -> Option<(String, route_matcher::RouteMatch)> {
let mut best: Option<(String, route_matcher::RouteMatch)> = None;
for item in fragments {
if let Some(Fragment::Route(route_frag)) = item.fragment.as_ref() {
let resolved_path = route_matcher::resolve_route_path(&route_frag.path, route_base);
if let Some(m) =
route_matcher::match_single_route(&resolved_path, request_path, route_frag.exact)
{
let is_better = best
.as_ref()
.is_none_or(|(_, prev)| m.specificity > prev.specificity);
if is_better {
best = Some((route_fragment_key(route_frag).to_string(), m));
}
}
}
}
best
}
fn route_fragment_key(route_frag: &WebUIFragmentRoute) -> &str {
route_frag.fragment_id.as_str()
}
pub fn collect_nested_route_params(
protocol: &WebUIProtocol,
entry_id: &str,
request_path: &str,
) -> HashMap<String, String> {
let mut all_params = HashMap::new();
let mut visited_fragments = HashSet::new();
let mut stack = vec![QueuedFragment {
id: entry_id.to_string(),
inventoryable: false,
route_base: "/".to_string(),
}];
while let Some(queued) = stack.pop() {
if queued.id.is_empty() || !visited_fragments.insert(queued.id.clone()) {
continue;
}
let Some(frag_list) = protocol.fragments.get(&queued.id) else {
continue;
};
let matched_route =
find_best_route_match(&frag_list.fragments, request_path, &queued.route_base);
for frag in &frag_list.fragments {
match frag.fragment.as_ref() {
Some(Fragment::Component(component)) => {
stack.push(QueuedFragment {
id: component.fragment_id.clone(),
inventoryable: false,
route_base: queued.route_base.clone(),
});
}
Some(Fragment::Route(route_frag)) => {
let is_selected = matched_route
.as_ref()
.is_some_and(|(best_key, _)| best_key == route_fragment_key(route_frag));
if is_selected && !route_frag.fragment_id.is_empty() {
if let Some((_, ref rm)) = matched_route {
all_params.extend(rm.params.clone());
let child_route_base = route_matcher::compute_route_base(
request_path,
rm.consumed_segments,
);
stack.push(QueuedFragment {
id: route_frag.fragment_id.clone(),
inventoryable: false,
route_base: child_route_base.clone(),
});
collect_params_from_children(
&route_frag.children,
request_path,
&child_route_base,
&mut all_params,
);
}
}
}
_ => {}
}
}
}
all_params
}
fn collect_params_from_children(
children: &[WebUIFragmentRoute],
request_path: &str,
route_base: &str,
all_params: &mut HashMap<String, String>,
) {
let mut current = children;
let mut base = route_base.to_string();
loop {
let mut best: Option<(usize, route_matcher::RouteMatch)> = None;
for (idx, child) in current.iter().enumerate() {
let resolved = route_matcher::resolve_route_path(&child.path, &base);
if let Some(m) = route_matcher::match_single_route(&resolved, request_path, child.exact)
{
let is_better = best
.as_ref()
.is_none_or(|(_, prev)| m.specificity > prev.specificity);
if is_better {
best = Some((idx, m));
}
}
}
let Some((idx, rm)) = best else { break };
all_params.extend(rm.params);
let matched = ¤t[idx];
if matched.children.is_empty() {
break;
}
let child_base = route_matcher::compute_route_base(request_path, rm.consumed_segments);
current = &matched.children;
base = child_base;
}
}
#[must_use]
pub fn render_partial(
protocol: &WebUIProtocol,
state: Value,
entry_id: &str,
request_path: &str,
inventory_hex: &str,
) -> Value {
let (needed_names, updated_inv) =
get_needed_components_for_request(protocol, entry_id, request_path, inventory_hex);
let chain = collect_route_chain(protocol, entry_id, request_path);
let tag_refs: Vec<&str> = needed_names.iter().map(|s| s.as_str()).collect();
let (style_array, tmpl_array) = collect_component_assets(protocol, &tag_refs);
let chain_array = Value::Array(chain.iter().map(RouteChainEntry::to_json).collect());
let mut result = serde_json::Map::with_capacity(6);
result.insert("state".into(), state);
result.insert("templateStyles".into(), Value::Array(style_array));
result.insert("templates".into(), Value::Array(tmpl_array));
result.insert("inventory".into(), Value::String(updated_inv));
result.insert("path".into(), Value::String(request_path.to_string()));
result.insert("chain".into(), chain_array);
Value::Object(result)
}
#[must_use]
pub fn render_component_templates(
protocol: &WebUIProtocol,
component_tags: &[&str],
inventory_hex: &str,
) -> Value {
let requested: HashSet<String> = component_tags.iter().map(|s| s.to_string()).collect();
let index = build_component_index(protocol);
let (needed, updated_inv) = filter_needed_components(&requested, inventory_hex, &index);
let tag_refs: Vec<&str> = needed.iter().map(|s| s.as_str()).collect();
let (style_array, tmpl_array) = collect_component_assets(protocol, &tag_refs);
let mut result = serde_json::Map::with_capacity(3);
result.insert("templateStyles".into(), Value::Array(style_array));
result.insert("templates".into(), Value::Array(tmpl_array));
result.insert("inventory".into(), Value::String(updated_inv));
Value::Object(result)
}
fn collect_component_assets(protocol: &WebUIProtocol, tags: &[&str]) -> (Vec<Value>, Vec<Value>) {
let mut style_array = Vec::new();
let mut tmpl_array = Vec::new();
let mut sorted_tags: Vec<&str> = tags.to_vec();
sorted_tags.sort_unstable();
for tag in sorted_tags {
let Some(component) = protocol.components.get(tag) else {
continue;
};
if component.template.is_empty() {
continue;
}
if !component.css.is_empty() {
let mut s = String::with_capacity(40 + tag.len() + component.css.len());
s.push_str("<style type=\"module\" specifier=\"");
s.push_str(tag);
s.push_str("\">");
s.push_str(&component.css);
s.push_str("</style>");
style_array.push(Value::String(s));
}
tmpl_array.push(Value::String(component.template.clone()));
}
(style_array, tmpl_array)
}
#[derive(Debug, Clone)]
pub struct RouteChainEntry {
pub component: String,
pub path: String,
pub params: HashMap<String, String>,
pub exact: bool,
pub allowed_query: String,
}
impl RouteChainEntry {
#[must_use]
pub fn to_json(&self) -> Value {
let mut obj = serde_json::Map::with_capacity(5);
obj.insert("component".into(), Value::String(self.component.clone()));
obj.insert("path".into(), Value::String(self.path.clone()));
if !self.params.is_empty() {
let params_obj: serde_json::Map<String, Value> = self
.params
.iter()
.map(|(k, v)| (k.clone(), Value::String(v.clone())))
.collect();
obj.insert("params".into(), Value::Object(params_obj));
}
if self.exact {
obj.insert("exact".into(), Value::Bool(true));
}
if !self.allowed_query.is_empty() {
obj.insert(
"allowedQuery".into(),
Value::String(self.allowed_query.clone()),
);
}
Value::Object(obj)
}
}
#[must_use]
pub fn collect_route_chain_json(
protocol: &WebUIProtocol,
entry_id: &str,
request_path: &str,
) -> Value {
let chain = collect_route_chain(protocol, entry_id, request_path);
Value::Array(chain.iter().map(RouteChainEntry::to_json).collect())
}
pub fn collect_route_chain(
protocol: &WebUIProtocol,
entry_id: &str,
request_path: &str,
) -> Vec<RouteChainEntry> {
let mut chain = Vec::new();
let mut visited_fragments = HashSet::new();
let mut stack = vec![QueuedFragment {
id: entry_id.to_string(),
inventoryable: false,
route_base: "/".to_string(),
}];
while let Some(queued) = stack.pop() {
if queued.id.is_empty() || !visited_fragments.insert(queued.id.clone()) {
continue;
}
let Some(frag_list) = protocol.fragments.get(&queued.id) else {
continue;
};
let matched_route =
find_best_route_match(&frag_list.fragments, request_path, &queued.route_base);
for frag in &frag_list.fragments {
match frag.fragment.as_ref() {
Some(Fragment::Component(component)) => {
stack.push(QueuedFragment {
id: component.fragment_id.clone(),
inventoryable: false,
route_base: queued.route_base.clone(),
});
}
Some(Fragment::Route(route_frag)) => {
let is_selected = matched_route
.as_ref()
.is_some_and(|(best_key, _)| best_key == route_fragment_key(route_frag));
if is_selected && !route_frag.fragment_id.is_empty() {
if let Some((_, ref rm)) = matched_route {
chain.push(RouteChainEntry {
component: route_frag.fragment_id.clone(),
path: route_frag.path.clone(),
params: rm.params.clone(),
exact: route_frag.exact,
allowed_query: route_frag.allowed_query.clone(),
});
let child_route_base = route_matcher::compute_route_base(
request_path,
rm.consumed_segments,
);
stack.push(QueuedFragment {
id: route_frag.fragment_id.clone(),
inventoryable: false,
route_base: child_route_base.clone(),
});
collect_chain_from_children(
&route_frag.children,
request_path,
&child_route_base,
&mut chain,
);
}
}
}
_ => {}
}
}
}
chain
}
fn collect_chain_from_children(
children: &[WebUIFragmentRoute],
request_path: &str,
route_base: &str,
chain: &mut Vec<RouteChainEntry>,
) {
let mut pending: Vec<(&[WebUIFragmentRoute], String)> =
vec![(children, route_base.to_string())];
while let Some((current, base)) = pending.pop() {
let mut best: Option<(usize, route_matcher::RouteMatch)> = None;
for (idx, child) in current.iter().enumerate() {
let resolved = route_matcher::resolve_route_path(&child.path, &base);
if let Some(m) = route_matcher::match_single_route(&resolved, request_path, child.exact)
{
let is_better = best
.as_ref()
.is_none_or(|(_, prev)| m.specificity > prev.specificity);
if is_better {
best = Some((idx, m));
}
}
}
if let Some((idx, rm)) = best {
let matched = ¤t[idx];
chain.push(RouteChainEntry {
component: matched.fragment_id.clone(),
path: matched.path.clone(),
params: rm.params,
exact: matched.exact,
allowed_query: matched.allowed_query.clone(),
});
if !matched.children.is_empty() {
let child_base =
route_matcher::compute_route_base(request_path, rm.consumed_segments);
pending.push((&matched.children, child_base));
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
use webui_protocol::{FragmentList, WebUIFragment, WebUiFragmentRoute};
#[test]
fn test_parse_encode_inventory_roundtrip() {
let original = vec![0xABu8, 0xCD, 0xEF, 0x01];
let hex = encode_inventory(&original);
let decoded = parse_inventory(&hex);
assert_eq!(original, decoded);
}
#[test]
fn test_bitfield_set_and_check() {
let mut inv = vec![0u8; 4];
set_component(&mut inv, 0);
set_component(&mut inv, 7);
set_component(&mut inv, 8);
set_component(&mut inv, 15);
assert!(has_component(&inv, 0));
assert!(has_component(&inv, 7));
assert!(has_component(&inv, 8));
assert!(has_component(&inv, 15));
assert!(!has_component(&inv, 1));
assert!(!has_component(&inv, 9));
}
#[test]
fn test_filter_needed_components_no_false_positives() {
let mut index = HashMap::new();
index.insert("email-message".to_string(), 0);
index.insert("o-button".to_string(), 1);
index.insert("o-avatar".to_string(), 2);
let mut names = HashSet::new();
names.insert("email-message".to_string());
names.insert("o-button".to_string());
let mut inv = vec![0u8; 1];
set_component(&mut inv, 1); let inv_hex = encode_inventory(&inv);
let (needed, _) = filter_needed_components(&names, &inv_hex, &index);
assert_eq!(needed.len(), 1);
assert!(
needed.contains(&"email-message".to_string()),
"email-message must be needed: {needed:?}"
);
}
#[test]
fn test_get_needed_components_empty_inventory() {
let mut fragments = HashMap::new();
fragments.insert(
"app-shell".to_string(),
FragmentList {
fragments: vec![WebUIFragment::component("my-card")],
},
);
fragments.insert(
"my-card".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("card")],
},
);
let protocol = WebUIProtocol::with_tokens(fragments, Vec::new());
let (needed, _inv) = get_needed_components(&protocol, "app-shell", "");
assert!(needed.contains(&"app-shell".to_string()));
assert!(needed.contains(&"my-card".to_string()));
}
#[test]
fn test_get_needed_components_skips_inventory() {
let mut fragments = HashMap::new();
fragments.insert(
"app-shell".to_string(),
FragmentList {
fragments: vec![WebUIFragment::component("my-card")],
},
);
fragments.insert(
"my-card".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("card")],
},
);
let mut protocol = WebUIProtocol::with_tokens(fragments, Vec::new());
protocol
.components
.entry("app-shell".to_string())
.or_default()
.template = "<t></t>".to_string();
protocol
.components
.entry("my-card".to_string())
.or_default()
.template = "<t></t>".to_string();
let (_needed, inv_hex) = get_needed_components(&protocol, "app-shell", "");
let (needed2, _) = get_needed_components(&protocol, "app-shell", &inv_hex);
assert!(needed2.is_empty());
}
#[test]
fn test_get_needed_components_skips_control_fragments() {
let mut fragments = HashMap::new();
fragments.insert(
"app-shell".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::if_cond(
webui_protocol::ConditionExpr::identifier("showNav"),
"if-shell",
),
WebUIFragment::for_loop("item", "items", "for-items"),
WebUIFragment::attribute_template("title", "attr-title"),
],
},
);
fragments.insert(
"if-shell".to_string(),
FragmentList {
fragments: vec![WebUIFragment::component("mp-category-nav")],
},
);
fragments.insert(
"for-items".to_string(),
FragmentList {
fragments: vec![WebUIFragment::component("mp-product-card")],
},
);
fragments.insert(
"attr-title".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("Products")],
},
);
fragments.insert(
"mp-category-nav".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<nav></nav>")],
},
);
fragments.insert(
"mp-product-card".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<article></article>")],
},
);
let protocol = WebUIProtocol::with_tokens(fragments, Vec::new());
let needed: HashSet<String> = get_needed_components(&protocol, "app-shell", "")
.0
.into_iter()
.collect();
assert!(needed.contains("app-shell"));
assert!(needed.contains("mp-category-nav"));
assert!(needed.contains("mp-product-card"));
assert!(!needed.contains("if-shell"));
assert!(!needed.contains("for-items"));
assert!(!needed.contains("attr-title"));
}
#[test]
fn test_get_needed_components_for_request_includes_shell_and_matched_route_chain() {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![WebUIFragment::component("mp-app")],
},
);
fragments.insert(
"mp-app".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::component("mp-category-nav"),
WebUIFragment::route_from(WebUiFragmentRoute {
path: "/search/:category".into(),
fragment_id: "mp-search-page".into(),
exact: true,
..Default::default()
}),
WebUIFragment::route_from(WebUiFragmentRoute {
path: "/product/:handle".into(),
fragment_id: "mp-product-page".into(),
exact: true,
..Default::default()
}),
],
},
);
fragments.insert(
"mp-category-nav".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<nav></nav>")],
},
);
fragments.insert(
"mp-search-page".to_string(),
FragmentList {
fragments: vec![WebUIFragment::component("mp-product-grid")],
},
);
fragments.insert(
"mp-product-grid".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<div>grid</div>")],
},
);
fragments.insert(
"mp-product-page".to_string(),
FragmentList {
fragments: vec![WebUIFragment::component("mp-product-detail")],
},
);
fragments.insert(
"mp-product-detail".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<div>detail</div>")],
},
);
let mut protocol = WebUIProtocol::with_tokens(fragments, Vec::new());
protocol
.components
.entry("mp-search-page".to_string())
.or_default()
.template = "<mp-search-page></mp-search-page>".to_string();
protocol
.components
.entry("mp-product-page".to_string())
.or_default()
.template = "<mp-product-page></mp-product-page>".to_string();
let (needed, _inv) =
get_needed_components_for_request(&protocol, "index.html", "/search/shirts", "");
let needed: HashSet<String> = needed.into_iter().collect();
assert!(needed.contains("mp-app"));
assert!(needed.contains("mp-category-nav"));
assert!(needed.contains("mp-search-page"));
assert!(needed.contains("mp-product-grid"));
assert!(!needed.contains("mp-product-page"));
assert!(!needed.contains("mp-product-detail"));
}
#[test]
fn test_get_needed_components_for_request_follows_nested_route_chain() {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![WebUIFragment::component("mp-app")],
},
);
fragments.insert(
"mp-app".to_string(),
FragmentList {
fragments: vec![WebUIFragment::route_from(WebUiFragmentRoute {
path: "/account/*rest".into(),
fragment_id: "mp-account-shell".into(),
exact: false,
..Default::default()
})],
},
);
fragments.insert(
"mp-account-shell".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::component("mp-account-nav"),
WebUIFragment::route_from(WebUiFragmentRoute {
path: "/account/profile".into(),
fragment_id: "mp-profile-page".into(),
exact: true,
..Default::default()
}),
WebUIFragment::route_from(WebUiFragmentRoute {
path: "/account/orders/:id".into(),
fragment_id: "mp-order-page".into(),
exact: true,
..Default::default()
}),
],
},
);
fragments.insert(
"mp-account-nav".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<nav></nav>")],
},
);
fragments.insert(
"mp-profile-page".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<profile></profile>")],
},
);
fragments.insert(
"mp-order-page".to_string(),
FragmentList {
fragments: vec![WebUIFragment::component("mp-order-detail")],
},
);
fragments.insert(
"mp-order-detail".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<detail></detail>")],
},
);
let mut protocol = WebUIProtocol::with_tokens(fragments, Vec::new());
protocol
.components
.entry("mp-account-shell".to_string())
.or_default()
.template = "<mp-account-shell></mp-account-shell>".to_string();
protocol
.components
.entry("mp-profile-page".to_string())
.or_default()
.template = "<mp-profile-page></mp-profile-page>".to_string();
protocol
.components
.entry("mp-order-page".to_string())
.or_default()
.template = "<mp-order-page></mp-order-page>".to_string();
let (needed, _inv) =
get_needed_components_for_request(&protocol, "index.html", "/account/orders/42", "");
let needed: HashSet<String> = needed.into_iter().collect();
assert!(needed.contains("mp-app"));
assert!(needed.contains("mp-account-shell"));
assert!(needed.contains("mp-account-nav"));
assert!(needed.contains("mp-order-page"));
assert!(needed.contains("mp-order-detail"));
assert!(!needed.contains("mp-profile-page"));
}
#[test]
fn test_get_needed_components_for_request_over_ships_if_and_loop_within_matched_route() {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![WebUIFragment::component("mp-app")],
},
);
fragments.insert(
"mp-app".to_string(),
FragmentList {
fragments: vec![WebUIFragment::route_from(WebUiFragmentRoute {
path: "/search".into(),
fragment_id: "mp-search-page".into(),
exact: true,
..Default::default()
})],
},
);
fragments.insert(
"mp-search-page".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::if_cond(
webui_protocol::ConditionExpr::identifier("showFilters"),
"if-filters",
),
WebUIFragment::for_loop("item", "items", "item-loop"),
],
},
);
fragments.insert(
"if-filters".to_string(),
FragmentList {
fragments: vec![WebUIFragment::component("mp-filter-panel")],
},
);
fragments.insert(
"item-loop".to_string(),
FragmentList {
fragments: vec![WebUIFragment::component("mp-item-card")],
},
);
fragments.insert(
"mp-filter-panel".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<filters></filters>")],
},
);
fragments.insert(
"mp-item-card".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<item></item>")],
},
);
let mut protocol = WebUIProtocol::with_tokens(fragments, Vec::new());
protocol
.components
.entry("mp-search-page".to_string())
.or_default()
.template = "<mp-search-page></mp-search-page>".to_string();
let (needed, _inv) =
get_needed_components_for_request(&protocol, "index.html", "/search", "");
let needed: HashSet<String> = needed.into_iter().collect();
assert!(needed.contains("mp-app"));
assert!(needed.contains("mp-search-page"));
assert!(needed.contains("mp-filter-panel"));
assert!(needed.contains("mp-item-card"));
}
#[test]
fn test_get_needed_components_for_request_respects_inventory() {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![WebUIFragment::component("mp-app")],
},
);
fragments.insert(
"mp-app".to_string(),
FragmentList {
fragments: vec![WebUIFragment::route_from(WebUiFragmentRoute {
path: "/search".into(),
fragment_id: "mp-search-page".into(),
exact: true,
..Default::default()
})],
},
);
fragments.insert(
"mp-search-page".to_string(),
FragmentList {
fragments: vec![WebUIFragment::component("mp-product-grid")],
},
);
fragments.insert(
"mp-product-grid".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<grid></grid>")],
},
);
let mut protocol = WebUIProtocol::with_tokens(fragments, Vec::new());
for name in ["mp-app", "mp-search-page", "mp-product-grid"] {
protocol
.components
.entry(name.to_string())
.or_default()
.template = format!("<f-template id=\"{name}\"></f-template>");
}
let (_needed, inventory) =
get_needed_components_for_request(&protocol, "index.html", "/search", "");
let (needed_again, _) =
get_needed_components_for_request(&protocol, "index.html", "/search", &inventory);
assert!(needed_again.is_empty());
}
#[test]
fn test_render_partial_separates_module_styles_from_templates() {
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 =
"(function(){window.__webui_templates['my-page']={h:'<p>page</p>'};})();".to_string();
component.css = ".page{color:red}".to_string();
let partial = render_partial(&protocol, serde_json::json!({}), "index.html", "/", "");
let styles = partial["templateStyles"]
.as_array()
.expect("templateStyles should be an array");
assert_eq!(styles.len(), 1);
assert!(
styles[0]
.as_str()
.unwrap_or_default()
.contains("specifier=\"my-page\""),
"module style should carry the component specifier"
);
assert!(
styles[0]
.as_str()
.unwrap_or_default()
.contains(".page{color:red}"),
"module style should contain the CSS content"
);
let templates = partial["templates"]
.as_array()
.expect("templates should be an array");
assert_eq!(templates.len(), 1);
assert!(
!templates[0].as_str().unwrap_or_default().contains("<style"),
"template should not contain any style tags"
);
assert!(
templates[0]
.as_str()
.unwrap_or_default()
.starts_with("(function()"),
"template should be a raw JS IIFE"
);
}
#[test]
fn test_render_partial_link_strategy_has_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 component = protocol
.components
.entry("my-page".to_string())
.or_default();
component.template = "(function(){})();".to_string();
component.css_href = "/my-page.css".to_string();
let partial = render_partial(&protocol, serde_json::json!({}), "index.html", "/", "");
let styles = partial["templateStyles"]
.as_array()
.expect("templateStyles should be an array");
let templates = partial["templates"]
.as_array()
.expect("templates should be an array");
assert!(
styles.is_empty(),
"Link strategy should produce empty templateStyles: {styles:?}"
);
assert_eq!(templates.len(), 1, "should include the template IIFE");
}
#[test]
fn test_render_partial_style_strategy_has_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 component = protocol
.components
.entry("my-page".to_string())
.or_default();
component.template =
"(function(){var w=window.__webui_templates;w['my-page']={h:'<style>.p{color:red}</style><p>page</p>'};})();"
.to_string();
let partial = render_partial(&protocol, serde_json::json!({}), "index.html", "/", "");
let styles = partial["templateStyles"]
.as_array()
.expect("templateStyles should be an array");
let templates = partial["templates"]
.as_array()
.expect("templates should be an array");
assert!(
styles.is_empty(),
"Style strategy should produce empty templateStyles: {styles:?}"
);
assert_eq!(templates.len(), 1, "should include the template IIFE");
assert!(
templates[0]
.as_str()
.unwrap_or_default()
.contains("<style>"),
"Style strategy template should contain inline <style> tag"
);
}
#[test]
fn test_render_partial_empty_styles_for_no_css_components() {
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 = "(function(){})();".to_string();
let partial = render_partial(&protocol, serde_json::json!({}), "index.html", "/", "");
let styles = partial["templateStyles"]
.as_array()
.expect("templateStyles should be an array");
assert!(
styles.is_empty(),
"templateStyles should be empty when components have no CSS"
);
}
#[test]
fn test_render_partial_sends_styles_even_when_templates_filtered_by_inventory() {
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 = "(function(){})();".to_string();
component.css = ".page{color:red}".to_string();
let partial1 = render_partial(&protocol, serde_json::json!({}), "index.html", "/", "");
let inv = partial1["inventory"].as_str().unwrap_or_default();
assert!(!inv.is_empty());
let partial2 = render_partial(&protocol, serde_json::json!({}), "index.html", "/", inv);
let styles = partial2["templateStyles"]
.as_array()
.expect("templateStyles should be an array");
let templates = partial2["templates"]
.as_array()
.expect("templates should be an array");
assert!(
templates.is_empty(),
"templates should be empty when inventory is full"
);
assert!(
styles.is_empty(),
"module styles should be empty when inventory is full — SSR already placed them"
);
}
#[test]
fn test_non_route_siblings_included_in_needed_components() {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![WebUIFragment::component("app-shell")],
},
);
fragments.insert(
"app-shell".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::component("my-navbar"),
WebUIFragment::route_from(WebUiFragmentRoute {
path: "/about".into(),
fragment_id: "page-about".into(),
exact: true,
..Default::default()
}),
WebUIFragment::component("cart-panel"),
],
},
);
fragments.insert(
"my-navbar".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<nav/>")],
},
);
fragments.insert(
"page-about".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<h1>About</h1>")],
},
);
fragments.insert(
"cart-panel".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<aside>Cart</aside>")],
},
);
let mut protocol = WebUIProtocol::with_tokens(fragments, Vec::new());
for name in ["app-shell", "my-navbar", "page-about", "cart-panel"] {
let comp = protocol.components.entry(name.to_string()).or_default();
comp.template = format!("(function(){{/* {name} */}})();");
comp.css = format!(".{name}{{display:block}}");
}
let (needed, _inv) =
get_needed_components_for_request(&protocol, "index.html", "/about", "");
assert!(
needed.contains(&"app-shell".to_string()),
"app-shell should be needed: {needed:?}"
);
assert!(
needed.contains(&"my-navbar".to_string()),
"my-navbar (non-route sibling) should be needed: {needed:?}"
);
assert!(
needed.contains(&"page-about".to_string()),
"page-about (active route) should be needed: {needed:?}"
);
assert!(
needed.contains(&"cart-panel".to_string()),
"cart-panel (non-route sibling) should be needed: {needed:?}"
);
}
#[test]
fn test_chain_entry_to_json_includes_allowed_query() {
let entry = RouteChainEntry {
component: "compose-page".into(),
path: "compose".into(),
params: HashMap::new(),
exact: true,
allowed_query: "action,to,subject".into(),
};
let json = entry.to_json();
assert_eq!(json["allowedQuery"], "action,to,subject");
}
#[test]
fn test_chain_entry_to_json_omits_empty_allowed_query() {
let entry = RouteChainEntry {
component: "home-page".into(),
path: "/".into(),
params: HashMap::new(),
exact: true,
allowed_query: String::new(),
};
let json = entry.to_json();
assert!(
json.get("allowedQuery").is_none(),
"empty allowed_query should not appear in JSON"
);
}
#[test]
fn test_collect_route_chain_carries_allowed_query() {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![WebUIFragment::route_from(WebUiFragmentRoute {
path: "/".into(),
fragment_id: "app-shell".into(),
exact: false,
children: vec![WebUiFragmentRoute {
path: "compose".into(),
fragment_id: "compose-page".into(),
exact: true,
allowed_query: "action,to".into(),
..Default::default()
}],
..Default::default()
})],
},
);
fragments.insert(
"app-shell".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<h1>App</h1>"), WebUIFragment::outlet()],
},
);
fragments.insert(
"compose-page".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<p>Compose</p>")],
},
);
let protocol = WebUIProtocol::new(fragments);
let chain = collect_route_chain(&protocol, "index.html", "/compose");
assert_eq!(chain.len(), 2);
assert_eq!(chain[0].component, "app-shell");
assert!(chain[0].allowed_query.is_empty());
assert_eq!(chain[1].component, "compose-page");
assert_eq!(chain[1].allowed_query, "action,to");
}
#[test]
fn test_render_component_templates_returns_template_and_css() {
let mut fragments = HashMap::new();
fragments.insert(
"settings-dialog".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<div class='dialog'>Settings</div>")],
},
);
let mut protocol = WebUIProtocol::with_tokens(fragments, Vec::new());
let comp = protocol
.components
.entry("settings-dialog".to_string())
.or_default();
comp.template = "(function(){window.__webui_templates['settings-dialog']={h:'<div>Settings</div>'};})();".to_string();
comp.css = ".dialog{position:fixed}".to_string();
let result = render_component_templates(&protocol, &["settings-dialog"], "");
let templates = result["templates"].as_array().expect("templates array");
let styles = result["templateStyles"].as_array().expect("styles array");
assert_eq!(templates.len(), 1);
assert!(templates[0].as_str().unwrap().contains("settings-dialog"));
assert_eq!(styles.len(), 1);
assert!(styles[0]
.as_str()
.unwrap()
.contains(".dialog{position:fixed}"));
}
#[test]
fn test_render_component_templates_respects_inventory() {
let mut fragments = HashMap::new();
fragments.insert(
"my-dialog".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<div>Dialog</div>")],
},
);
let mut protocol = WebUIProtocol::with_tokens(fragments, Vec::new());
let comp = protocol
.components
.entry("my-dialog".to_string())
.or_default();
comp.template = "(function(){})();".to_string();
comp.css = ".d{color:red}".to_string();
let result1 = render_component_templates(&protocol, &["my-dialog"], "");
let inv = result1["inventory"].as_str().expect("inventory string");
assert_eq!(result1["templates"].as_array().unwrap().len(), 1);
let result2 = render_component_templates(&protocol, &["my-dialog"], inv);
assert_eq!(result2["templates"].as_array().unwrap().len(), 0);
assert_eq!(result2["templateStyles"].as_array().unwrap().len(), 0);
}
#[test]
fn test_render_component_templates_unknown_component_returns_empty() {
let fragments = HashMap::new();
let protocol = WebUIProtocol::with_tokens(fragments, Vec::new());
let result = render_component_templates(&protocol, &["nonexistent-widget"], "");
assert_eq!(result["templates"].as_array().unwrap().len(), 0);
assert_eq!(result["templateStyles"].as_array().unwrap().len(), 0);
}
}