use crate::{route_matcher, route_renderer, HandlerError, StateSelection};
use memchr::memchr2;
use route_matcher::CompiledRouteIndex;
use serde::de::{DeserializeSeed, IgnoredAny, MapAccess, SeqAccess, Visitor};
use serde::ser::SerializeMap;
use serde::Serialize;
use serde_json::{value::RawValue, Map, Value};
use std::borrow::Cow;
use std::collections::{HashMap, HashSet};
use std::fmt;
use std::sync::{Arc, OnceLock, RwLock};
use webui_protocol::{
web_ui_fragment::Fragment, ComponentAssetStylePreload, CssStrategy, StateProjectionMode,
WebUIFragmentRoute, WebUIProtocol,
};
fn partition_component_asset_style_preloads(
protocol: &WebUIProtocol,
preloads: &[ComponentAssetStylePreload],
) -> (
Vec<ComponentAssetStylePreload>,
Vec<ComponentAssetStylePreload>,
) {
let mut shadow = Vec::new();
let mut light = Vec::new();
for preload in preloads {
if protocol
.components
.get(&preload.root)
.is_some_and(|component| !component.uses_shadow_dom)
{
light.push(preload.clone());
} else {
shadow.push(preload.clone());
}
}
(shadow, light)
}
use crate::streaming::PreparedContinuationStatePlan;
pub struct Protocol {
protocol: WebUIProtocol,
style_metadata_error: Option<String>,
css_strategy: webui_protocol::CssStrategy,
render_fragments: crate::RenderFragmentIndex,
component_asset_style_manifest: std::result::Result<String, String>,
component_asset_style_links: String,
component_index: HashMap<String, u32>,
style_resource_index: HashMap<String, u32>,
style_resources_requiring_escape: HashSet<String>,
component_reachability: OnceLock<ComponentReachabilityIndex>,
fragment_ids: Vec<Arc<str>>,
fragment_slots: HashMap<Arc<str>, u32>,
route_index: CompiledRouteIndex,
boundary_declarations: OnceLock<HashMap<u32, BoundaryDeclaration>>,
continuation_state_plans: OnceLock<Box<[OnceLock<PreparedContinuationStatePlan>]>>,
template_metadata_cache: RwLock<HashMap<String, Value>>,
}
pub(crate) struct BoundaryDeclaration {
pub(crate) owner: Arc<str>,
pub(crate) name: Arc<str>,
}
impl Protocol {
pub fn from_protobuf(bytes: &[u8]) -> std::result::Result<Self, webui_protocol::ProtocolError> {
let protocol = WebUIProtocol::from_protobuf(bytes)?;
if let Some(error) = Self::validate_component_style_metadata(&protocol) {
return Err(webui_protocol::ProtocolError::Validation(error));
}
Ok(Self::new_with_style_metadata(protocol, None))
}
#[must_use]
pub fn new(protocol: WebUIProtocol) -> Self {
let style_metadata_error = Self::validate_component_style_metadata(&protocol);
Self::new_with_style_metadata(protocol, style_metadata_error)
}
fn new_with_style_metadata(
mut protocol: WebUIProtocol,
style_metadata_error: Option<String>,
) -> Self {
let css_strategy = protocol.css_strategy();
let component_asset_style_preloads =
std::mem::take(&mut protocol.component_asset_style_preloads);
let (shadow_preloads, light_preloads) =
partition_component_asset_style_preloads(&protocol, &component_asset_style_preloads);
let component_asset_style_links = if css_strategy == CssStrategy::Link {
crate::serialize_component_asset_style_links(&light_preloads)
} else {
String::new()
};
let component_asset_style_manifest = if css_strategy == CssStrategy::Link {
crate::serialize_component_asset_style_manifest(&shadow_preloads).map_err(|error| {
format!("failed to serialize component asset style metadata: {error}")
})
} else {
Ok(String::new())
};
let component_index = build_component_index(&protocol);
let style_resource_index = build_style_resource_index(&protocol);
let style_resources_requiring_escape = build_style_escape_resources(&protocol);
let route_index = CompiledRouteIndex::new(&protocol);
let mut fragment_ids: Vec<Arc<str>> = protocol
.fragments
.keys()
.map(|id| Arc::from(id.as_str()))
.collect();
fragment_ids.sort_unstable();
let mut fragment_slots = HashMap::with_capacity(fragment_ids.len());
for (slot, id) in fragment_ids.iter().enumerate() {
#[allow(clippy::cast_possible_truncation)]
fragment_slots.insert(Arc::clone(id), slot as u32);
}
let render_fragments =
crate::RenderFragmentIndex::new(&protocol, &fragment_ids, &fragment_slots);
Self {
protocol,
style_metadata_error,
css_strategy,
render_fragments,
component_asset_style_manifest,
component_asset_style_links,
component_index,
style_resource_index,
style_resources_requiring_escape,
component_reachability: OnceLock::new(),
fragment_ids,
fragment_slots,
route_index,
boundary_declarations: OnceLock::new(),
continuation_state_plans: OnceLock::new(),
template_metadata_cache: RwLock::new(HashMap::new()),
}
}
pub(crate) fn protocol(&self) -> &WebUIProtocol {
&self.protocol
}
pub(crate) fn ensure_style_metadata(&self) -> Result<(), HandlerError> {
self.style_metadata_error
.as_ref()
.map_or(Ok(()), |error| Err(HandlerError::Invariant(error.clone())))
}
pub(crate) fn css_strategy(&self) -> webui_protocol::CssStrategy {
self.css_strategy
}
pub(crate) fn render_fragments(&self) -> &crate::RenderFragmentIndex {
&self.render_fragments
}
pub(crate) fn component_asset_style_manifest(&self) -> Result<&str, HandlerError> {
match &self.component_asset_style_manifest {
Ok(manifest) => Ok(manifest),
Err(message) => Err(HandlerError::Rendering(message.clone())),
}
}
pub(crate) fn component_asset_style_links(&self) -> &str {
&self.component_asset_style_links
}
pub(crate) fn component_index(&self) -> &HashMap<String, u32> {
&self.component_index
}
pub(crate) fn style_resource_index(&self) -> &HashMap<String, u32> {
if self.style_resource_index.is_empty() {
&self.component_index
} else {
&self.style_resource_index
}
}
pub(crate) fn style_resources_requiring_escape(&self) -> &HashSet<String> {
&self.style_resources_requiring_escape
}
pub(crate) fn component_reachability(&self) -> &ComponentReachabilityIndex {
self.component_reachability
.get_or_init(|| ComponentReachabilityIndex::new(&self.protocol, &self.component_index))
}
pub(crate) fn route_index(&self) -> &CompiledRouteIndex {
&self.route_index
}
pub(crate) fn fragment_slot(&self, id: &str) -> Option<u32> {
self.fragment_slots.get(id).copied()
}
pub(crate) fn fragment_id(&self, slot: u32) -> Option<&str> {
usize::try_from(slot)
.ok()
.and_then(|slot| self.fragment_ids.get(slot))
.map(Arc::as_ref)
}
pub(crate) fn boundary_declaration(&self, declaration_id: u32) -> Option<&BoundaryDeclaration> {
self.boundary_declarations
.get_or_init(|| build_boundary_declarations(&self.protocol))
.get(&declaration_id)
}
pub(crate) fn continuation_state_plan(
&self,
entry_id: &str,
) -> Result<&PreparedContinuationStatePlan, HandlerError> {
let slot = self
.fragment_slot(entry_id)
.ok_or_else(|| HandlerError::MissingFragment(entry_id.to_string()))?;
let index = usize::try_from(slot).map_err(|_| {
HandlerError::Invariant("continuation plan slot does not fit usize".to_string())
})?;
let plans = self.continuation_state_plans.get_or_init(|| {
(0..self.fragment_ids.len())
.map(|_| OnceLock::new())
.collect()
});
let cell = plans
.get(index)
.ok_or_else(|| HandlerError::MissingFragment(entry_id.to_string()))?;
Ok(cell.get_or_init(|| PreparedContinuationStatePlan::new(&self.protocol, entry_id)))
}
#[must_use]
pub fn tokens(&self) -> &[String] {
&self.protocol.tokens
}
pub fn render_partial(
&self,
state_json: &str,
entry_id: &str,
request_path: &str,
inventory_hex: &str,
) -> Result<String, HandlerError> {
self.ensure_style_metadata()?;
let mut index = self.request_index();
let (response, state_selection) = render_partial_indexed_with_state(
self.protocol(),
entry_id,
request_path,
inventory_hex,
&mut index,
)?;
serialize_partial_response(&response, state_json, &state_selection)
}
pub fn render_component_templates(
&self,
component_tags: &[&str],
inventory_hex: &str,
) -> Result<Value, HandlerError> {
self.ensure_style_metadata()?;
let mut index = self.request_index();
render_component_templates_indexed(
self.protocol(),
component_tags,
inventory_hex,
&mut index,
)
}
fn validate_component_style_metadata(protocol: &WebUIProtocol) -> Option<String> {
if protocol.style_closures.is_empty() {
let requires_closures = protocol.components.iter().any(|(tag, component)| {
protocol.component_style_resource(tag).is_some() || component.uses_shadow_dom
});
if requires_closures {
return Some(
"component style closure metadata is required by this protocol".to_string(),
);
}
return None;
}
let mut seen_resources = HashSet::new();
for (root, closure) in &protocol.style_closures {
seen_resources.clear();
seen_resources.reserve(closure.component_tags.len());
for tag in &closure.component_tags {
if !seen_resources.insert(tag.as_str()) {
return Some(format!(
"component style closure `{root}` contains duplicate resource `{tag}`"
));
}
if protocol.component_style_resource(tag).is_none() {
return Some(format!(
"component style closure `{root}` references missing resource `{tag}`"
));
}
}
}
for (fragment_id, fragments) in &protocol.fragments {
for fragment in &fragments.fragments {
let Some(Fragment::Signal(signal)) = fragment.fragment.as_ref() else {
continue;
};
let Some(root) = crate::structural_signal_value(signal)
.and_then(|value| value.strip_prefix("shadow_styles:"))
else {
continue;
};
let Some(component) = protocol.components.get(fragment_id) else {
return Some(format!(
"Shadow style hook `{root}` references unknown component `{fragment_id}`"
));
};
if root != fragment_id.as_str() || !component.uses_shadow_dom {
return Some(format!(
"Shadow style hook `{root}` does not match component fragment `{fragment_id}`"
));
}
}
}
for (tag, component) in &protocol.components {
if !component.uses_shadow_dom {
continue;
}
let Some(fragments) = protocol.fragments.get(tag) else {
continue;
};
if !protocol.style_closures.contains_key(tag) {
return Some(format!(
"component style closure metadata is missing Shadow root `{tag}`"
));
}
let hook_count = fragments
.fragments
.iter()
.filter(|fragment| {
matches!(
fragment.fragment.as_ref(),
Some(Fragment::Signal(signal))
if crate::structural_signal_value(signal)
.and_then(|value| value.strip_prefix("shadow_styles:"))
== Some(tag.as_str())
)
})
.count();
if hook_count == 0 {
return Some(format!(
"Shadow component `{tag}` is missing its compiler style insertion hook"
));
}
if hook_count > 1 {
return Some(format!(
"Shadow component `{tag}` emitted more than one style insertion hook"
));
}
}
None
}
#[cfg(test)]
fn render_partial_metadata(
&self,
entry_id: &str,
request_path: &str,
inventory_hex: &str,
) -> Result<Value, HandlerError> {
self.ensure_style_metadata()?;
let mut index = self.request_index();
render_partial_indexed(
self.protocol(),
entry_id,
request_path,
inventory_hex,
&mut index,
)
}
fn request_index(&self) -> RequestProtocolIndex<'_> {
RequestProtocolIndex {
component_index: &self.component_index,
route_index: &self.route_index,
template_metadata_cache: TemplateMetadataCache::Shared(&self.template_metadata_cache),
}
}
}
#[cfg(test)]
struct ProtocolIndex {
pub component_index: HashMap<String, u32>,
pub route_index: CompiledRouteIndex,
template_metadata_cache: HashMap<String, Value>,
}
struct ComponentAssets {
component_styles: Value,
templates: serde_json::Map<String, Value>,
functions: serde_json::Map<String, Value>,
}
pub(crate) fn collect_component_styles<'a>(
protocol: &WebUIProtocol,
roots: impl IntoIterator<Item = &'a str>,
) -> Result<Value, HandlerError> {
collect_component_styles_inner(protocol, roots, None)
}
#[derive(Clone, Copy)]
struct StyleInventoryView<'a> {
bits: &'a [u8],
index: &'a HashMap<String, u32>,
chunks_are_indexed: bool,
}
impl StyleInventoryView<'_> {
fn contains_component(self, tag: &str) -> bool {
self.index
.get(tag)
.is_some_and(|&index| has_component(self.bits, index))
}
fn contains_chunk(self, name: &str) -> bool {
self.chunks_are_indexed && self.contains_component(name)
}
}
#[cfg(test)]
pub(crate) fn collect_component_style_delta<'a>(
protocol: &WebUIProtocol,
roots: impl IntoIterator<Item = &'a str>,
style_inventory: &[u8],
style_resource_index: &HashMap<String, u32>,
) -> Result<Value, HandlerError> {
collect_component_styles_inner(
protocol,
roots,
Some(StyleInventoryView {
bits: style_inventory,
index: style_resource_index,
chunks_are_indexed: true,
}),
)
}
pub(crate) struct BorrowedComponentStyleDelta<'a> {
strategy: webui_protocol::CssStrategy,
resources: Vec<BorrowedStyleResource<'a>>,
closures: Vec<BorrowedStyleClosure<'a>>,
ordered: Vec<&'a str>,
}
struct BorrowedStyleResource<'a> {
name: &'a str,
resource: &'a str,
members: Option<&'a [String]>,
}
struct BorrowedStyleClosure<'a> {
root: &'a str,
start: usize,
end: usize,
}
impl BorrowedComponentStyleDelta<'_> {
pub(crate) fn resource_names(&self) -> impl Iterator<Item = &str> {
self.resources.iter().map(|resource| resource.name)
}
}
impl Serialize for BorrowedComponentStyleDelta<'_> {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let mut payload = serializer.serialize_map(Some(4))?;
payload.serialize_entry(
"closures",
&BorrowedStyleClosures {
closures: &self.closures,
ordered: &self.ordered,
},
)?;
payload.serialize_entry(
"resources",
&BorrowedStyleResources {
strategy: self.strategy,
resources: &self.resources,
},
)?;
payload.serialize_entry("strategy", self.strategy.wire_name())?;
payload.serialize_entry("version", &1)?;
payload.end()
}
}
struct BorrowedStyleResources<'a> {
strategy: webui_protocol::CssStrategy,
resources: &'a [BorrowedStyleResource<'a>],
}
impl Serialize for BorrowedStyleResources<'_> {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let mut resources = serializer.serialize_map(Some(self.resources.len()))?;
for resource in self.resources {
resources.serialize_entry(
resource.name,
&BorrowedStyleResourceValue {
strategy: self.strategy,
resource,
},
)?;
}
resources.end()
}
}
struct BorrowedStyleResourceValue<'a> {
strategy: webui_protocol::CssStrategy,
resource: &'a BorrowedStyleResource<'a>,
}
impl Serialize for BorrowedStyleResourceValue<'_> {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let field_count = match (self.strategy, self.resource.members.is_some()) {
(webui_protocol::CssStrategy::Module, true) => 4,
(webui_protocol::CssStrategy::Module, false) | (_, true) => 3,
(_, false) => 2,
};
let mut resource = serializer.serialize_map(Some(field_count))?;
match self.strategy {
webui_protocol::CssStrategy::Link => {
resource.serialize_entry("href", self.resource.resource)?;
resource.serialize_entry("kind", "link")?;
if let Some(members) = self.resource.members {
resource.serialize_entry("members", members)?;
}
}
webui_protocol::CssStrategy::Style => {
resource.serialize_entry("css", self.resource.resource)?;
resource.serialize_entry("kind", "style")?;
if let Some(members) = self.resource.members {
resource.serialize_entry("members", members)?;
}
}
webui_protocol::CssStrategy::Module => {
resource.serialize_entry("css", self.resource.resource)?;
resource.serialize_entry("kind", "module")?;
if let Some(members) = self.resource.members {
resource.serialize_entry("members", members)?;
}
resource.serialize_entry("specifier", self.resource.name)?;
}
}
resource.end()
}
}
struct BorrowedStyleClosures<'a> {
closures: &'a [BorrowedStyleClosure<'a>],
ordered: &'a [&'a str],
}
impl Serialize for BorrowedStyleClosures<'_> {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let mut closures = serializer.serialize_map(Some(self.closures.len()))?;
for closure in self.closures {
closures.serialize_entry(closure.root, &self.ordered[closure.start..closure.end])?;
}
closures.end()
}
}
pub(crate) fn collect_borrowed_component_style_delta<'a>(
protocol: &'a WebUIProtocol,
roots: impl IntoIterator<Item = &'a str>,
style_inventory: &[u8],
style_resource_index: &HashMap<String, u32>,
chunk_index: &HashMap<&str, u32>,
) -> Result<BorrowedComponentStyleDelta<'a>, HandlerError> {
if protocol.style_closures.is_empty() {
if protocol
.components
.keys()
.any(|tag| protocol.component_style_resource(tag).is_some())
{
return Err(HandlerError::Invariant(
"component style closure metadata is required by this protocol".to_string(),
));
}
return Ok(BorrowedComponentStyleDelta {
strategy: protocol.css_strategy(),
resources: Vec::new(),
closures: Vec::new(),
ordered: Vec::new(),
});
}
let mut seen_roots = HashSet::new();
let roots: Vec<&str> = roots
.into_iter()
.filter(|root| seen_roots.insert(*root))
.collect();
let client_inventory = StyleInventoryView {
bits: style_inventory,
index: style_resource_index,
chunks_are_indexed: true,
};
let covered_components = covered_components(protocol, &roots, Some(client_inventory));
let unit_capacity = roots
.iter()
.filter_map(|root| protocol.style_closures.get(*root))
.map(WebUIProtocol::style_closure_unit_count)
.sum();
let mut resources = Vec::with_capacity(unit_capacity);
let mut closures = Vec::with_capacity(roots.len());
let mut ordered = Vec::with_capacity(unit_capacity);
let mut emitted = HashSet::with_capacity(unit_capacity);
let mut serialized_resources = HashSet::with_capacity(unit_capacity);
for root in roots {
let Some(closure) = protocol.style_closures.get(root) else {
if !protocol.fragments.contains_key(root) && !protocol.components.contains_key(root) {
continue;
}
return Err(HandlerError::Invariant(format!(
"component style closure metadata is missing root `{root}`"
)));
};
if closure.style_chunks.is_empty()
&& !closure.component_tags.is_empty()
&& closure
.component_tags
.iter()
.all(|tag| covered_components.contains(tag.as_str()))
{
continue;
}
let start = ordered.len();
emitted.clear();
let unit_count = WebUIProtocol::style_closure_unit_count(closure);
for position in 0..unit_count {
let Some(unit) = protocol.style_closure_unit(closure, chunk_index, position) else {
continue;
};
let name = unit.name;
let resource = unit.resource.ok_or_else(|| match unit.chunk {
Some(index) => HandlerError::Invariant(format!(
"component style closure `{root}` references missing style chunk {index}"
)),
None => HandlerError::Invariant(format!(
"component style closure `{root}` references missing resource `{name}`"
)),
})?;
if !emitted.insert(name) {
continue;
}
let (client_has_it, members) = match unit.chunk {
Some(index) => (
client_inventory.contains_chunk(name),
protocol.style_chunk_members(index),
),
None => (client_inventory.contains_component(name), None),
};
if !client_has_it && serialized_resources.insert(name) {
resources.push(BorrowedStyleResource {
name,
resource,
members: members.filter(|members| members.len() > 1),
});
}
ordered.push(name);
}
closures.push(BorrowedStyleClosure {
root,
start,
end: ordered.len(),
});
}
resources.sort_unstable_by_key(|resource| resource.name);
closures.sort_unstable_by_key(|closure| closure.root);
Ok(BorrowedComponentStyleDelta {
strategy: protocol.css_strategy(),
resources,
closures,
ordered,
})
}
fn collect_component_styles_for_inventory<'a>(
protocol: &WebUIProtocol,
roots: impl IntoIterator<Item = &'a str>,
component_inventory: &[u8],
component_index: &HashMap<String, u32>,
) -> Result<Value, HandlerError> {
collect_component_styles_inner(
protocol,
roots,
Some(StyleInventoryView {
bits: component_inventory,
index: component_index,
chunks_are_indexed: false,
}),
)
}
fn collect_component_styles_inner<'a>(
protocol: &WebUIProtocol,
roots: impl IntoIterator<Item = &'a str>,
client_inventory: Option<StyleInventoryView<'_>>,
) -> Result<Value, HandlerError> {
if protocol.style_closures.is_empty() {
if protocol
.components
.keys()
.any(|tag| protocol.component_style_resource(tag).is_some())
{
return Err(HandlerError::Invariant(
"component style closure metadata is required by this protocol".to_string(),
));
}
return Ok(component_styles_payload(
protocol.css_strategy(),
serde_json::Map::new(),
serde_json::Map::new(),
));
}
let mut resources = serde_json::Map::new();
let mut closures = serde_json::Map::new();
let mut seen_roots = HashSet::new();
let roots: Vec<&str> = roots
.into_iter()
.filter(|root| seen_roots.insert(*root))
.collect();
let covered_components = covered_components(protocol, &roots, client_inventory);
let chunk_index = protocol.style_chunk_index();
let mut emitted = HashSet::new();
for root in roots {
let Some(closure) = protocol.style_closures.get(root) else {
if !protocol.fragments.contains_key(root) && !protocol.components.contains_key(root) {
continue;
}
return Err(HandlerError::Invariant(format!(
"component style closure metadata is missing root `{root}`"
)));
};
if closure.style_chunks.is_empty()
&& !closure.component_tags.is_empty()
&& closure
.component_tags
.iter()
.all(|tag| covered_components.contains(tag.as_str()))
{
continue;
}
let unit_count = WebUIProtocol::style_closure_unit_count(closure);
let mut ordered = Vec::with_capacity(unit_count);
emitted.clear();
for position in 0..unit_count {
let Some(unit) = protocol.style_closure_unit(closure, &chunk_index, position) else {
continue;
};
let name = unit.name;
let resource = unit.resource.ok_or_else(|| match unit.chunk {
Some(index) => HandlerError::Invariant(format!(
"component style closure `{root}` references missing style chunk {index}"
)),
None => HandlerError::Invariant(format!(
"component style closure `{root}` references missing resource `{name}`"
)),
})?;
let (client_has_it, members) = match unit.chunk {
Some(index) => (
client_inventory.is_some_and(|inventory| inventory.contains_chunk(name)),
Some(protocol.style_chunk_members(index).unwrap_or_default()),
),
None => (
client_inventory.is_some_and(|inventory| inventory.contains_component(name)),
None,
),
};
if !emitted.insert(name) {
continue;
}
if !client_has_it && !resources.contains_key(name) {
let mut entry = serde_json::Map::new();
match protocol.css_strategy() {
webui_protocol::CssStrategy::Link => {
entry.insert("kind".into(), Value::String("link".into()));
entry.insert("href".into(), Value::String(resource.to_owned()));
}
webui_protocol::CssStrategy::Style => {
entry.insert("kind".into(), Value::String("style".into()));
entry.insert("css".into(), Value::String(resource.to_owned()));
}
webui_protocol::CssStrategy::Module => {
entry.insert("kind".into(), Value::String("module".into()));
entry.insert("specifier".into(), Value::String(name.to_owned()));
entry.insert("css".into(), Value::String(resource.to_owned()));
}
}
if let Some(members) = members.filter(|members| members.len() > 1) {
entry.insert(
"members".into(),
Value::Array(members.iter().cloned().map(Value::String).collect()),
);
}
resources.insert(name.to_owned(), Value::Object(entry));
}
ordered.push(Value::String(name.to_owned()));
}
closures.insert(root.to_owned(), Value::Array(ordered));
}
Ok(component_styles_payload(
protocol.css_strategy(),
resources,
closures,
))
}
fn covered_components<'a>(
protocol: &'a WebUIProtocol,
roots: &[&str],
inventory: Option<StyleInventoryView<'_>>,
) -> HashSet<&'a str> {
let mut covered = HashSet::with_capacity(protocol.components.len());
for root in roots {
let Some(closure) = protocol.style_closures.get(*root) else {
continue;
};
for index in &closure.style_chunks {
if let Some(chunk) = usize::try_from(*index)
.ok()
.and_then(|index| protocol.style_chunks.get(index))
{
covered.extend(chunk.component_tags.iter().map(String::as_str));
}
}
}
if let Some(inventory) = inventory {
for chunk in &protocol.style_chunks {
if inventory.contains_chunk(&chunk.name) {
covered.extend(chunk.component_tags.iter().map(String::as_str));
}
}
}
covered
}
fn component_styles_payload(
strategy: webui_protocol::CssStrategy,
resources: serde_json::Map<String, Value>,
closures: serde_json::Map<String, Value>,
) -> Value {
let strategy = strategy.wire_name();
let mut payload = serde_json::Map::new();
payload.insert("version".into(), Value::from(1));
payload.insert("strategy".into(), Value::String(strategy.into()));
payload.insert("resources".into(), Value::Object(resources));
payload.insert("closures".into(), Value::Object(closures));
Value::Object(payload)
}
#[cfg(test)]
impl ProtocolIndex {
#[must_use]
pub(crate) fn new(protocol: &WebUIProtocol) -> Self {
Self {
component_index: build_component_index(protocol),
route_index: CompiledRouteIndex::new(protocol),
template_metadata_cache: HashMap::new(),
}
}
fn request_index(&mut self) -> RequestProtocolIndex<'_> {
RequestProtocolIndex {
component_index: &self.component_index,
route_index: &self.route_index,
template_metadata_cache: TemplateMetadataCache::Exclusive(
&mut self.template_metadata_cache,
),
}
}
}
enum TemplateMetadataCache<'a> {
#[cfg(test)]
Exclusive(&'a mut HashMap<String, Value>),
Shared(&'a RwLock<HashMap<String, Value>>),
}
struct RequestProtocolIndex<'a> {
component_index: &'a HashMap<String, u32>,
route_index: &'a CompiledRouteIndex,
template_metadata_cache: TemplateMetadataCache<'a>,
}
fn build_boundary_declarations(protocol: &WebUIProtocol) -> HashMap<u32, BoundaryDeclaration> {
let mut declarations = HashMap::new();
for list in protocol.fragments.values() {
for fragment in &list.fragments {
let Some(Fragment::Boundary(boundary)) = fragment.fragment.as_ref() else {
continue;
};
if boundary.phase() != webui_protocol::BoundaryPhase::Start {
continue;
}
declarations.insert(
boundary.declaration_id,
BoundaryDeclaration {
owner: Arc::from(boundary.owner_fragment_id.as_str()),
name: Arc::from(boundary.name.as_str()),
},
);
}
}
declarations
}
pub(crate) fn build_component_index(protocol: &WebUIProtocol) -> HashMap<String, u32> {
let mut sorted: Vec<&String> = protocol
.fragments
.keys()
.filter(|key| key.contains('-'))
.collect();
sorted.sort_unstable();
let mut index = HashMap::with_capacity(sorted.len());
for (i, name) in sorted.into_iter().enumerate() {
#[allow(clippy::cast_possible_truncation)]
let idx = i as u32;
index.insert(name.clone(), idx);
}
index
}
fn build_style_resource_index(protocol: &WebUIProtocol) -> HashMap<String, u32> {
if protocol.style_chunks.is_empty() {
return HashMap::new();
}
let mut sorted: Vec<&str> = protocol
.components
.keys()
.filter(|tag| protocol.component_style_resource(tag).is_some())
.map(String::as_str)
.chain(
protocol
.style_chunks
.iter()
.map(|chunk| chunk.name.as_str()),
)
.collect();
sorted.sort_unstable();
sorted.dedup();
let mut index = HashMap::with_capacity(sorted.len());
for (position, name) in sorted.into_iter().enumerate() {
#[allow(clippy::cast_possible_truncation)]
let position = position as u32;
index.insert(name.to_string(), position);
}
index
}
fn build_style_escape_resources(protocol: &WebUIProtocol) -> HashSet<String> {
if protocol.css_strategy() == CssStrategy::Link {
return HashSet::new();
}
let mut resources = HashSet::new();
for (name, component) in &protocol.components {
if crate::html_encode::style_text_needs_escape(&component.css) {
resources.insert(name.clone());
}
}
for chunk in &protocol.style_chunks {
if crate::html_encode::style_text_needs_escape(&chunk.css) {
resources.insert(chunk.name.clone());
}
}
resources
}
pub(crate) struct ComponentReachabilityIndex {
names: Vec<String>,
dependencies: Vec<Box<[u32]>>,
route_dependent: Vec<bool>,
hydration_keys: Vec<Box<str>>,
hydration_key_ids: Vec<u32>,
hydration_runs: Vec<HydrationRun>,
}
#[derive(Clone, Copy)]
struct HydrationRun {
start: u32,
len: u32,
}
impl HydrationRun {
const FULL_STATE: Self = Self {
start: 0,
len: u32::MAX,
};
const fn requires_full_state(self) -> bool {
self.len == u32::MAX
}
}
impl ComponentReachabilityIndex {
fn new(protocol: &WebUIProtocol, component_index: &HashMap<String, u32>) -> Self {
let mut names: Vec<String> = component_index.keys().cloned().collect();
names.sort_unstable();
let mut dependencies = Vec::with_capacity(names.len());
let mut route_dependent = Vec::with_capacity(names.len());
for name in &names {
let (direct, has_route) =
collect_direct_component_dependencies(protocol, name, component_index);
dependencies.push(direct.into_boxed_slice());
route_dependent.push(has_route);
}
propagate_route_dependencies(&dependencies, &mut route_dependent);
let (hydration_keys, hydration_key_ids, hydration_runs) =
intern_hydration_projections(protocol, &names);
Self {
names,
dependencies,
route_dependent,
hydration_keys,
hydration_key_ids,
hydration_runs,
}
}
pub(crate) fn name(&self, index: u32) -> Option<&str> {
self.names.get(index as usize).map(String::as_str)
}
pub(crate) fn dependencies(&self, index: u32) -> Option<&[u32]> {
self.dependencies.get(index as usize).map(Box::as_ref)
}
pub(crate) fn is_route_dependent(&self, index: u32) -> Option<bool> {
self.route_dependent.get(index as usize).copied()
}
pub(crate) fn requires_expansion(&self, index: u32) -> Option<bool> {
Some(self.is_route_dependent(index)? || !self.dependencies.get(index as usize)?.is_empty())
}
pub(crate) fn hydration_key(&self, id: u32) -> Option<&str> {
self.hydration_keys.get(id as usize).map(Box::as_ref)
}
pub(crate) fn extend_hydration_keys(&self, index: u32, ids: &mut Vec<u32>) -> bool {
let Some(run) = self.hydration_runs.get(index as usize).copied() else {
return true;
};
if run.requires_full_state() {
return true;
}
let start = run.start as usize;
let end = start.saturating_add(run.len as usize);
match self.hydration_key_ids.get(start..end) {
Some(run) => ids.extend_from_slice(run),
None => return true,
}
false
}
}
fn intern_hydration_projections(
protocol: &WebUIProtocol,
names: &[String],
) -> (Vec<Box<str>>, Vec<u32>, Vec<HydrationRun>) {
let mut distinct: Vec<&str> = Vec::new();
for name in names {
if let Some(component) = protocol.components.get(name) {
distinct.extend(component.hydration_keys.iter().map(String::as_str));
}
}
distinct.sort_unstable();
distinct.dedup();
let keys: Vec<Box<str>> = distinct.iter().map(|key| Box::from(*key)).collect();
let mut key_ids = Vec::new();
let mut runs = Vec::with_capacity(names.len());
for name in names {
let Some(component) = protocol.components.get(name) else {
runs.push(HydrationRun::FULL_STATE);
continue;
};
let mode = component.hydration_mode;
let component_keys = &component.hydration_keys;
let projects_keys = mode == StateProjectionMode::Keys as i32
|| (mode == StateProjectionMode::None as i32 && !component_keys.is_empty());
if mode == StateProjectionMode::All as i32
|| (!projects_keys && mode != StateProjectionMode::None as i32)
{
runs.push(HydrationRun::FULL_STATE);
continue;
}
let start = key_ids.len();
if projects_keys {
for key in component_keys {
if let Ok(position) = distinct.binary_search(&key.as_str()) {
if let Ok(id) = u32::try_from(position) {
key_ids.push(id);
}
}
}
}
match (u32::try_from(start), u32::try_from(key_ids.len() - start)) {
(Ok(start), Ok(len)) if len != u32::MAX => runs.push(HydrationRun { start, len }),
_ => runs.push(HydrationRun::FULL_STATE),
}
}
(keys, key_ids, runs)
}
enum ComponentDependencyWork<'a> {
Fragment(&'a str),
Component(u32),
}
fn collect_direct_component_dependencies(
protocol: &WebUIProtocol,
root: &str,
component_index: &HashMap<String, u32>,
) -> (Vec<u32>, bool) {
let mut work = vec![ComponentDependencyWork::Fragment(root)];
let mut visited_fragments = HashSet::new();
let mut seen_components = HashSet::new();
let mut dependencies = Vec::new();
let mut has_route = false;
while let Some(item) = work.pop() {
match item {
ComponentDependencyWork::Component(index) => {
if seen_components.insert(index) {
dependencies.push(index);
}
}
ComponentDependencyWork::Fragment(id) => {
if !visited_fragments.insert(id) {
continue;
}
let Some(list) = protocol.fragments.get(id) else {
continue;
};
for fragment in list.fragments.iter().rev() {
match fragment.fragment.as_ref() {
Some(Fragment::Component(component)) => {
if let Some(&index) = component_index.get(&component.fragment_id) {
work.push(ComponentDependencyWork::Component(index));
}
}
Some(Fragment::ForLoop(for_loop)) => work.push(
ComponentDependencyWork::Fragment(for_loop.fragment_id.as_str()),
),
Some(Fragment::IfCond(if_cond)) => work.push(
ComponentDependencyWork::Fragment(if_cond.fragment_id.as_str()),
),
Some(Fragment::Attribute(attribute)) if !attribute.template.is_empty() => {
work.push(ComponentDependencyWork::Fragment(
attribute.template.as_str(),
));
}
Some(Fragment::Route(_)) => has_route = true,
_ => {}
}
}
}
}
}
(dependencies, has_route)
}
fn propagate_route_dependencies(dependencies: &[Box<[u32]>], route_dependent: &mut [bool]) {
let mut reverse = vec![Vec::new(); dependencies.len()];
for (parent, children) in dependencies.iter().enumerate() {
for &child in children.iter() {
if let Some(parents) = reverse.get_mut(child as usize) {
#[allow(clippy::cast_possible_truncation)]
parents.push(parent as u32);
}
}
}
let mut pending = Vec::new();
for (index, &depends_on_route) in route_dependent.iter().enumerate() {
if depends_on_route {
#[allow(clippy::cast_possible_truncation)]
pending.push(index as u32);
}
}
while let Some(index) = pending.pop() {
let Some(parents) = reverse.get(index as usize) else {
continue;
};
for &parent in parents {
let Some(depends_on_route) = route_dependent.get_mut(parent as usize) else {
continue;
};
if !*depends_on_route {
*depends_on_route = true;
pending.push(parent);
}
}
}
}
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) -> Result<Vec<u8>, HandlerError> {
if hex.is_empty() {
return Ok(Vec::new());
}
if !hex.len().is_multiple_of(2) {
return Err(HandlerError::Protocol(
webui_protocol::ProtocolError::Validation(format!(
"inventory hex has odd length: {}",
hex.len()
)),
));
}
let mut bytes = Vec::with_capacity(hex.len() / 2);
let mut chars = hex.bytes();
while let (Some(hi), Some(lo)) = (chars.next(), chars.next()) {
let h = route_matcher::hex_val(hi).ok_or_else(|| {
HandlerError::Protocol(webui_protocol::ProtocolError::Validation(format!(
"invalid hex digit in inventory: {:#04x}",
hi
)))
})?;
let l = route_matcher::hex_val(lo).ok_or_else(|| {
HandlerError::Protocol(webui_protocol::ProtocolError::Validation(format!(
"invalid hex digit in inventory: {:#04x}",
lo
)))
})?;
bytes.push((h << 4) | l);
}
Ok(bytes)
}
pub fn encode_inventory(inv: &[u8]) -> String {
const HEX: &[u8; 16] = b"0123456789abcdef";
let mut encoded = String::with_capacity(inv.len() * 2);
for byte in inv {
encoded.push(char::from(HEX[usize::from(byte >> 4)]));
encoded.push(char::from(HEX[usize::from(byte & 0x0f)]));
}
encoded
}
pub(crate) fn encode_component_inventory(
component_names: &HashSet<String>,
index: &HashMap<String, u32>,
) -> String {
let mut inventory = Vec::new();
for name in component_names {
if let Some(&idx) = index.get(name.as_str()) {
set_component(&mut inventory, idx);
}
}
encode_inventory(&inventory)
}
pub fn get_needed_components(
protocol: &WebUIProtocol,
entry_id: &str,
inventory_hex: &str,
component_index: &HashMap<String, u32>,
) -> Result<(Vec<String>, String), HandlerError> {
let route_index = CompiledRouteIndex::new(protocol);
let component_names =
collect_inventoryable_components(protocol, entry_id, None, true, &route_index);
filter_needed_components(&component_names, inventory_hex, component_index)
}
#[cfg(test)]
pub(crate) fn get_needed_components_for_request(
protocol: &WebUIProtocol,
entry_id: &str,
request_path: &str,
inventory_hex: &str,
indexes: (&HashMap<String, u32>, &CompiledRouteIndex),
) -> Result<(Vec<String>, String), HandlerError> {
let (component_index, route_index) = indexes;
let component_names = collect_inventoryable_components(
protocol,
entry_id,
Some(request_path),
false,
route_index,
);
filter_needed_components(&component_names, inventory_hex, component_index)
}
fn serialize_partial_response(
response: &Value,
state_json: &str,
state_selection: &StateSelection<'_>,
) -> Result<String, HandlerError> {
let response = response
.as_object()
.ok_or_else(partial_response_not_object)?;
let state = select_raw_state(state_json, state_selection)?;
serde_json::to_string(&PartialResponseWithState { response, state })
.map_err(|error| partial_serialize_error(&error.to_string()))
}
fn validate_json(json: &str) -> Result<(), HandlerError> {
validate_json_inner(json).map_err(|error| invalid_state_json(&error.to_string()))
}
fn validate_json_inner(json: &str) -> serde_json::Result<()> {
let mut deserializer = serde_json::Deserializer::from_str(json);
ValidJson
.deserialize(&mut deserializer)
.and_then(|_| deserializer.end())
}
struct ValidJson;
impl<'de> DeserializeSeed<'de> for ValidJson {
type Value = ();
fn deserialize<D>(self, deserializer: D) -> std::result::Result<(), D::Error>
where
D: serde::Deserializer<'de>,
{
deserializer.deserialize_any(ValidJsonVisitor)
}
}
struct ValidJsonVisitor;
impl<'de> Visitor<'de> for ValidJsonVisitor {
type Value = ();
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("valid JSON")
}
fn visit_bool<E>(self, _value: bool) -> std::result::Result<(), E> {
Ok(())
}
fn visit_i64<E>(self, _value: i64) -> std::result::Result<(), E> {
Ok(())
}
fn visit_u64<E>(self, _value: u64) -> std::result::Result<(), E> {
Ok(())
}
fn visit_f64<E>(self, value: f64) -> std::result::Result<(), E>
where
E: serde::de::Error,
{
if value.is_finite() {
Ok(())
} else {
Err(E::custom("number out of range"))
}
}
fn visit_str<E>(self, _value: &str) -> std::result::Result<(), E> {
Ok(())
}
fn visit_borrowed_str<E>(self, _value: &'de str) -> std::result::Result<(), E> {
Ok(())
}
fn visit_string<E>(self, _value: String) -> std::result::Result<(), E> {
Ok(())
}
fn visit_unit<E>(self) -> std::result::Result<(), E> {
Ok(())
}
fn visit_none<E>(self) -> std::result::Result<(), E> {
Ok(())
}
fn visit_some<D>(self, deserializer: D) -> std::result::Result<(), D::Error>
where
D: serde::Deserializer<'de>,
{
ValidJson.deserialize(deserializer)
}
fn visit_seq<A>(self, mut sequence: A) -> std::result::Result<(), A::Error>
where
A: SeqAccess<'de>,
{
while sequence.next_element_seed(ValidJson)?.is_some() {}
Ok(())
}
fn visit_map<A>(self, mut map: A) -> std::result::Result<(), A::Error>
where
A: MapAccess<'de>,
{
while map.next_key::<IgnoredAny>()?.is_some() {
map.next_value_seed(ValidJson)?;
}
Ok(())
}
}
struct PartialResponseWithState<'a, 'state> {
response: &'a Map<String, Value>,
state: SelectedRawState<'state>,
}
impl Serialize for PartialResponseWithState<'_, '_> {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let mut map = serializer.serialize_map(Some(self.response.len().saturating_add(1)))?;
for (key, value) in self.response {
map.serialize_entry(key, value)?;
}
map.serialize_entry("state", &self.state)?;
map.end()
}
}
struct ProjectedRawState<'de> {
entries: Vec<(Cow<'de, str>, &'de RawValue)>,
}
enum SelectedRawState<'de> {
Full(&'de RawValue),
Keys(ProjectedRawState<'de>),
}
impl Serialize for SelectedRawState<'_> {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
match self {
Self::Full(state) => state.serialize(serializer),
Self::Keys(state) => state.serialize(serializer),
}
}
}
impl Serialize for ProjectedRawState<'_> {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let mut map = serializer.serialize_map(Some(self.entries.len()))?;
for (key, value) in &self.entries {
map.serialize_entry(key.as_ref(), value)?;
}
map.end()
}
}
fn project_raw_state<'de>(
state_json: &'de str,
state_keys: &[&str],
) -> Result<ProjectedRawState<'de>, HandlerError> {
let is_object = state_json
.as_bytes()
.iter()
.find(|byte| !byte.is_ascii_whitespace())
.is_some_and(|byte| *byte == b'{');
if !is_object {
validate_json(state_json)?;
return Ok(ProjectedRawState {
entries: Vec::new(),
});
}
let mut deserializer = serde_json::Deserializer::from_str(state_json);
let mut state = ProjectedRawStateSeed { state_keys }
.deserialize(&mut deserializer)
.and_then(|state| {
deserializer.end()?;
Ok(state)
})
.map_err(|error| invalid_state_json(&error.to_string()))?;
state.entries.sort_unstable_by(|left, right| {
left.0.as_ref().as_bytes().cmp(right.0.as_ref().as_bytes())
});
Ok(state)
}
fn select_raw_state<'de>(
state_json: &'de str,
selection: &StateSelection<'_>,
) -> Result<SelectedRawState<'de>, HandlerError> {
let state_keys = match selection {
StateSelection::Full => {
if contains_reserved_state_key(state_json) {
return strip_reserved_raw_state(state_json).map(SelectedRawState::Keys);
}
return serde_json::from_str::<&RawValue>(state_json)
.map(SelectedRawState::Full)
.map_err(|error| invalid_state_json(&error.to_string()));
}
StateSelection::Keys(keys) => keys.as_slice(),
StateSelection::KeyIds(_) | StateSelection::FullExceptKeyIds(_) => {
return Err(unexpected_key_id_selection());
}
};
project_raw_state(state_json, state_keys).map(SelectedRawState::Keys)
}
fn contains_reserved_state_key(state_json: &str) -> bool {
let bytes = state_json.as_bytes();
let mut search_start = 0;
while let Some(relative_index) = memchr2(b'$', b'\\', &bytes[search_start..]) {
let candidate = search_start + relative_index;
if candidate > 0
&& bytes[candidate - 1] == b'"'
&& matches_reserved_state_key(bytes, candidate)
{
return contains_top_level_reserved_state_key(bytes);
}
search_start = candidate + 1;
}
false
}
fn contains_top_level_reserved_state_key(bytes: &[u8]) -> bool {
let Some(mut cursor) = bytes.iter().position(|byte| !byte.is_ascii_whitespace()) else {
return false;
};
if bytes[cursor] != b'{' {
return false;
}
cursor += 1;
let mut depth = 1;
while cursor < bytes.len() {
match bytes[cursor] {
b'"' => {
if depth == 1 && matches_reserved_state_key(bytes, cursor + 1) {
return true;
}
cursor = skip_json_string(bytes, cursor + 1);
}
b'{' | b'[' => {
depth += 1;
cursor += 1;
}
b'}' | b']' => {
depth -= 1;
if depth == 0 {
return false;
}
cursor += 1;
}
_ => cursor += 1,
}
}
false
}
fn skip_json_string(bytes: &[u8], mut cursor: usize) -> usize {
while let Some(relative_index) = memchr2(b'"', b'\\', &bytes[cursor..]) {
cursor += relative_index;
if bytes[cursor] == b'"' {
return cursor + 1;
}
cursor = (cursor + 2).min(bytes.len());
}
bytes.len()
}
fn matches_reserved_state_key(bytes: &[u8], mut cursor: usize) -> bool {
for expected in crate::STATE_INJECT_KEY.bytes() {
if bytes.get(cursor).is_some_and(|byte| *byte == expected) {
cursor += 1;
} else if matches_unicode_escape(bytes, cursor, expected) {
cursor += 6;
} else {
return false;
}
}
if bytes.get(cursor) != Some(&b'"') {
return false;
}
cursor += 1;
while bytes.get(cursor).is_some_and(u8::is_ascii_whitespace) {
cursor += 1;
}
bytes.get(cursor) == Some(&b':')
}
fn matches_unicode_escape(bytes: &[u8], cursor: usize, expected: u8) -> bool {
let Some(escape) = bytes.get(cursor..cursor.saturating_add(6)) else {
return false;
};
escape[..4] == *b"\\u00"
&& decode_hex_nibble(escape[4]).is_some_and(|high| {
decode_hex_nibble(escape[5]).is_some_and(|low| (high << 4) | low == expected)
})
}
fn decode_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,
}
}
fn strip_reserved_raw_state(state_json: &str) -> Result<ProjectedRawState<'_>, HandlerError> {
let mut deserializer = serde_json::Deserializer::from_str(state_json);
let projected = ReservedFilteredRawStateSeed
.deserialize(&mut deserializer)
.map_err(|error| invalid_state_json(&error.to_string()))?;
deserializer
.end()
.map_err(|error| invalid_state_json(&error.to_string()))?;
Ok(projected)
}
struct ReservedFilteredRawStateSeed;
impl<'de> DeserializeSeed<'de> for ReservedFilteredRawStateSeed {
type Value = ProjectedRawState<'de>;
fn deserialize<D>(self, deserializer: D) -> std::result::Result<Self::Value, D::Error>
where
D: serde::Deserializer<'de>,
{
deserializer.deserialize_map(ReservedFilteredRawStateVisitor)
}
}
struct ReservedFilteredRawStateVisitor;
impl<'de> Visitor<'de> for ReservedFilteredRawStateVisitor {
type Value = ProjectedRawState<'de>;
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("a JSON object")
}
fn visit_map<A>(self, mut map: A) -> std::result::Result<Self::Value, A::Error>
where
A: MapAccess<'de>,
{
let mut entries: Vec<(Cow<'de, str>, &'de RawValue)> = Vec::new();
while let Some(key) = map.next_key_seed(BorrowedString)? {
if key.as_ref() == crate::STATE_INJECT_KEY {
map.next_value_seed(ValidJson)?;
continue;
}
let value: &'de RawValue = map.next_value()?;
validate_json_inner(value.get()).map_err(serde::de::Error::custom)?;
entries.push((key, value));
}
Ok(ProjectedRawState { entries })
}
}
struct ProjectedRawStateSeed<'a> {
state_keys: &'a [&'a str],
}
impl<'de> DeserializeSeed<'de> for ProjectedRawStateSeed<'_> {
type Value = ProjectedRawState<'de>;
fn deserialize<D>(self, deserializer: D) -> std::result::Result<Self::Value, D::Error>
where
D: serde::Deserializer<'de>,
{
deserializer.deserialize_map(ProjectedRawStateVisitor {
state_keys: self.state_keys,
})
}
}
struct ProjectedRawStateVisitor<'a> {
state_keys: &'a [&'a str],
}
impl<'de> Visitor<'de> for ProjectedRawStateVisitor<'_> {
type Value = ProjectedRawState<'de>;
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("a JSON object")
}
fn visit_map<A>(self, mut map: A) -> std::result::Result<Self::Value, A::Error>
where
A: MapAccess<'de>,
{
let mut entries: Vec<(Cow<'de, str>, &'de RawValue)> =
Vec::with_capacity(self.state_keys.len());
while let Some(key) = map.next_key_seed(BorrowedString)? {
if key.as_ref() == crate::STATE_INJECT_KEY
|| self.state_keys.binary_search(&key.as_ref()).is_err()
{
map.next_value_seed(ValidJson)?;
continue;
}
let value: &'de RawValue = map.next_value()?;
validate_json_inner(value.get()).map_err(serde::de::Error::custom)?;
if let Some(entry) = entries
.iter_mut()
.find(|(existing, _)| existing.as_ref() == key.as_ref())
{
*entry = (key, value);
} else {
entries.push((key, value));
}
}
Ok(ProjectedRawState { entries })
}
}
struct BorrowedString;
impl<'de> DeserializeSeed<'de> for BorrowedString {
type Value = Cow<'de, str>;
fn deserialize<D>(self, deserializer: D) -> std::result::Result<Self::Value, D::Error>
where
D: serde::Deserializer<'de>,
{
deserializer.deserialize_str(BorrowedStringVisitor)
}
}
struct BorrowedStringVisitor;
impl<'de> Visitor<'de> for BorrowedStringVisitor {
type Value = Cow<'de, str>;
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("a string")
}
fn visit_borrowed_str<E>(self, value: &'de str) -> std::result::Result<Self::Value, E> {
Ok(Cow::Borrowed(value))
}
fn visit_str<E>(self, value: &str) -> std::result::Result<Self::Value, E> {
Ok(Cow::Owned(value.to_owned()))
}
fn visit_string<E>(self, value: String) -> std::result::Result<Self::Value, E> {
Ok(Cow::Owned(value))
}
}
#[cold]
#[inline(never)]
fn invalid_state_json(message: &str) -> HandlerError {
HandlerError::InvalidState(message.to_string())
}
#[cold]
#[inline(never)]
fn unexpected_key_id_selection() -> HandlerError {
HandlerError::Invariant(
"streaming hydration key-ID projection reached partial navigation".to_string(),
)
}
#[cold]
#[inline(never)]
fn partial_serialize_error(message: &str) -> HandlerError {
HandlerError::Rendering(format!("failed to serialize partial response: {message}"))
}
#[cold]
#[inline(never)]
fn partial_response_not_object() -> HandlerError {
HandlerError::Invariant("partial response must serialize as a JSON object".to_string())
}
pub(crate) fn collect_reachable_component_order_for_request(
protocol: &WebUIProtocol,
entry_id: &str,
request_path: &str,
route_index: &CompiledRouteIndex,
) -> Vec<String> {
collect_inventoryable_components(protocol, entry_id, Some(request_path), false, route_index)
}
pub fn filter_needed_components(
component_names: &[String],
inventory_hex: &str,
index: &HashMap<String, u32>,
) -> Result<(Vec<String>, String), HandlerError> {
let filtered = filter_components_with_inventory(component_names, inventory_hex, index)?;
Ok((filtered.needed, filtered.updated_inventory))
}
struct FilteredComponents {
needed: Vec<String>,
updated_inventory: String,
client_inventory: Vec<u8>,
}
fn filter_components_with_inventory(
component_names: &[String],
inventory_hex: &str,
index: &HashMap<String, u32>,
) -> Result<FilteredComponents, HandlerError> {
let client_inv = parse_inventory(inventory_hex)?;
let mut updated_inv = client_inv.clone();
let mut needed = Vec::with_capacity(component_names.len());
for name in component_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());
}
}
Ok(FilteredComponents {
needed,
updated_inventory: encode_inventory(&updated_inv),
client_inventory: client_inv,
})
}
fn has_template_payload(component: &webui_protocol::ComponentData) -> bool {
!component.template_json.is_empty() || !component.template.is_empty()
}
#[derive(Debug)]
struct QueuedFragment {
id: String,
inventoryable: bool,
route_base: String,
}
#[inline]
fn mark_fragment_visited(
visited: &mut HashSet<(String, String)>,
queued: &QueuedFragment,
route_sensitive: bool,
) -> bool {
let route_base = if route_sensitive {
queued.route_base.clone()
} else {
String::new()
};
visited.insert((queued.id.clone(), route_base))
}
struct ChildWalkCtx<'a> {
request_path: &'a str,
protocol: &'a WebUIProtocol,
route_index: &'a CompiledRouteIndex,
}
fn collect_inventoryable_components(
protocol: &WebUIProtocol,
entry_id: &str,
request_path: Option<&str>,
root_inventoryable: bool,
route_index: &CompiledRouteIndex,
) -> Vec<String> {
let stack = vec![QueuedFragment {
id: entry_id.to_string(),
inventoryable: root_inventoryable,
route_base: "/".to_string(),
}];
collect_inventoryable_components_from_stack(protocol, request_path, route_index, stack)
}
pub(crate) fn collect_reachable_components_from_roots(
protocol: &WebUIProtocol,
roots: &[(u32, Option<Box<str>>)],
reachability: &ComponentReachabilityIndex,
request_path: &str,
route_index: &CompiledRouteIndex,
) -> Vec<String> {
let mut stack = Vec::with_capacity(roots.len());
for (root, route_base) in roots.iter().rev() {
let Some(name) = reachability.name(*root) else {
continue;
};
stack.push(QueuedFragment {
id: name.to_string(),
inventoryable: true,
route_base: route_base.as_deref().unwrap_or("/").to_string(),
});
}
collect_inventoryable_components_from_stack(protocol, Some(request_path), route_index, stack)
}
fn collect_inventoryable_components_from_stack(
protocol: &WebUIProtocol,
request_path: Option<&str>,
route_index: &CompiledRouteIndex,
mut stack: Vec<QueuedFragment>,
) -> Vec<String> {
let mut visited_fragments = HashSet::new();
let route_sensitive = request_path.is_some();
let mut seen_components = HashSet::new();
let mut component_ids: Vec<String> = Vec::new();
while let Some(queued) = stack.pop() {
if queued.id.is_empty() {
continue;
}
if queued.inventoryable && seen_components.insert(queued.id.clone()) {
component_ids.push(queued.id.clone());
}
if !mark_fragment_visited(&mut visited_fragments, &queued, route_sensitive) {
continue;
}
let Some(frag_list) = protocol.fragments.get(&queued.id) else {
continue;
};
let matched_route = request_path.and_then(|path| {
route_renderer::find_best_route_match(
&frag_list.fragments,
path,
&queued.route_base,
route_index,
)
});
for frag in frag_list.fragments.iter().rev() {
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_frag.fragment_id.as_str());
if is_selected && !route_frag.content_fragment_id.is_empty() {
stack.push(QueuedFragment {
id: route_frag.content_fragment_id.clone(),
inventoryable: false,
route_base: queued.route_base.clone(),
});
}
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(has_template_payload),
route_base: child_route_base.clone(),
});
collect_route_boundary_components(
std::slice::from_ref(route_frag),
&child_route_base,
protocol,
&mut stack,
);
if !route_frag.children.is_empty() {
if let Some(path) = request_path {
walk_route_children(
&route_frag.children,
&child_route_base,
&mut stack,
&mut ChildWalkCtx {
request_path: path,
protocol,
route_index,
},
);
}
}
}
}
_ => {}
}
}
}
component_ids
}
fn select_best_child_route(
children: &[WebUIFragmentRoute],
request_path: &str,
route_base: &str,
route_index: &CompiledRouteIndex,
) -> Option<(usize, route_matcher::RouteMatch)> {
let request_segments = route_matcher::split_request_path(request_path);
let mut best: Option<(usize, route_matcher::RouteMatch)> = None;
for (idx, child) in children.iter().enumerate() {
if let Some(m) = route_matcher::match_route_indexed_with_segments(
route_index,
&child.path,
route_base,
&request_segments,
child.exact,
) {
let is_better = best
.as_ref()
.is_none_or(|(_, prev)| m.specificity > prev.specificity);
if is_better {
best = Some((idx, m));
}
}
}
best
}
fn walk_route_children(
children: &[WebUIFragmentRoute],
route_base: &str,
stack: &mut Vec<QueuedFragment>,
ctx: &mut ChildWalkCtx<'_>,
) {
let mut current = children;
let mut base = route_base.to_string();
while let Some((idx, ref rm)) =
select_best_child_route(current, ctx.request_path, &base, ctx.route_index)
{
let matched = ¤t[idx];
if matched.fragment_id.is_empty() {
break;
}
let child_base = route_matcher::compute_route_base(ctx.request_path, rm.consumed_segments);
stack.push(QueuedFragment {
id: matched.fragment_id.clone(),
inventoryable: ctx
.protocol
.components
.get(&matched.fragment_id)
.is_some_and(has_template_payload),
route_base: child_base.clone(),
});
if !matched.pending_component.is_empty() {
stack.push(QueuedFragment {
id: matched.pending_component.clone(),
inventoryable: ctx
.protocol
.components
.get(&matched.pending_component)
.is_some_and(has_template_payload),
route_base: child_base.clone(),
});
}
if !matched.error_component.is_empty() {
stack.push(QueuedFragment {
id: matched.error_component.clone(),
inventoryable: ctx
.protocol
.components
.get(&matched.error_component)
.is_some_and(has_template_payload),
route_base: child_base.clone(),
});
}
if matched.children.is_empty() {
break;
}
current = &matched.children;
base = child_base;
}
}
fn collect_route_boundary_components(
routes: &[WebUIFragmentRoute],
route_base: &str,
protocol: &WebUIProtocol,
stack: &mut Vec<QueuedFragment>,
) {
let mut remaining: Vec<&WebUIFragmentRoute> = routes.iter().collect();
while let Some(route) = remaining.pop() {
for component in [&route.pending_component, &route.error_component] {
if component.is_empty() {
continue;
}
stack.push(QueuedFragment {
id: component.clone(),
inventoryable: protocol
.components
.get(component)
.is_some_and(has_template_payload),
route_base: route_base.to_string(),
});
}
remaining.extend(route.children.iter());
}
}
pub fn collect_nested_route_params(
protocol: &Protocol,
entry_id: &str,
request_path: &str,
) -> HashMap<String, String> {
let route_index = protocol.route_index();
let protocol = protocol.protocol();
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() || !mark_fragment_visited(&mut visited_fragments, &queued, true) {
continue;
}
let Some(frag_list) = protocol.fragments.get(&queued.id) else {
continue;
};
let matched_route = route_renderer::find_best_route_match(
&frag_list.fragments,
request_path,
&queued.route_base,
route_index,
);
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::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::Route(route_frag)) => {
let is_selected = matched_route
.as_ref()
.is_some_and(|(best_key, _)| best_key == route_frag.fragment_id.as_str());
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,
route_index,
);
}
}
}
_ => {}
}
}
}
all_params
}
fn collect_params_from_children(
children: &[WebUIFragmentRoute],
request_path: &str,
route_base: &str,
all_params: &mut HashMap<String, String>,
route_index: &CompiledRouteIndex,
) {
let mut current = children;
let mut base = route_base.to_string();
while let Some((idx, rm)) = select_best_child_route(current, request_path, &base, route_index) {
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;
}
}
fn resolve_tag_templates(templates: &[String], params: &HashMap<String, String>) -> Vec<String> {
let mut resolved = Vec::with_capacity(templates.len());
for template in templates {
if !template.contains('{') {
resolved.push(template.clone());
continue;
}
let mut result = String::with_capacity(template.len());
let mut rest = template.as_str();
while let Some(open) = rest.find('{') {
result.push_str(&rest[..open]);
rest = &rest[open + 1..];
if let Some(close) = rest.find('}') {
let name = &rest[..close];
if let Some(value) = params.get(name) {
result.push_str(value);
} else {
result.push('{');
result.push_str(name);
result.push('}');
}
rest = &rest[close + 1..];
} else {
result.push('{');
}
}
result.push_str(rest);
resolved.push(result);
}
resolved
}
fn collect_inventory_and_chain(
protocol: &WebUIProtocol,
entry_id: &str,
request_path: &str,
index: &mut RequestProtocolIndex<'_>,
) -> (Vec<String>, Vec<RouteChainEntry>) {
let mut visited_fragments = HashSet::new();
let mut seen_components = HashSet::new();
let mut component_ids: Vec<String> = Vec::new();
let mut chain = Vec::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() {
continue;
}
if queued.inventoryable && seen_components.insert(queued.id.clone()) {
component_ids.push(queued.id.clone());
}
if !mark_fragment_visited(&mut visited_fragments, &queued, true) {
continue;
}
let Some(frag_list) = protocol.fragments.get(&queued.id) else {
continue;
};
let matched_route = route_renderer::find_best_route_match(
&frag_list.fragments,
request_path,
&queued.route_base,
index.route_index,
);
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_frag.fragment_id.as_str());
if is_selected && !route_frag.content_fragment_id.is_empty() {
stack.push(QueuedFragment {
id: route_frag.content_fragment_id.clone(),
inventoryable: false,
route_base: queued.route_base.clone(),
});
}
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(),
keep_alive: route_frag.keep_alive,
cache_tags: route_frag.cache_tags.clone(),
invalidates: route_frag.invalidates.clone(),
pending_component: route_frag.pending_component.clone(),
error_component: route_frag.error_component.clone(),
});
let child_route_base = route_matcher::compute_route_base(
request_path,
rm.consumed_segments,
);
let is_inventoryable = protocol
.components
.get(&route_frag.fragment_id)
.is_some_and(has_template_payload);
stack.push(QueuedFragment {
id: route_frag.fragment_id.clone(),
inventoryable: is_inventoryable,
route_base: child_route_base.clone(),
});
collect_route_boundary_components(
std::slice::from_ref(route_frag),
&child_route_base,
protocol,
&mut stack,
);
if !route_frag.children.is_empty() {
walk_children_for_inventory_and_chain(
&route_frag.children,
&child_route_base,
&mut stack,
&mut chain,
&mut ChildWalkCtx {
request_path,
protocol,
route_index: index.route_index,
},
);
}
}
}
}
_ => {}
}
}
}
(component_ids, chain)
}
fn walk_children_for_inventory_and_chain(
children: &[WebUIFragmentRoute],
route_base: &str,
stack: &mut Vec<QueuedFragment>,
chain: &mut Vec<RouteChainEntry>,
ctx: &mut ChildWalkCtx<'_>,
) {
let mut current = children;
let mut base = route_base.to_string();
while let Some((idx, ref rm)) =
select_best_child_route(current, ctx.request_path, &base, ctx.route_index)
{
let matched = ¤t[idx];
if matched.fragment_id.is_empty() {
break;
}
let child_base = route_matcher::compute_route_base(ctx.request_path, rm.consumed_segments);
chain.push(RouteChainEntry {
component: matched.fragment_id.clone(),
path: matched.path.clone(),
params: rm.params.clone(),
exact: matched.exact,
allowed_query: matched.allowed_query.clone(),
keep_alive: matched.keep_alive,
cache_tags: matched.cache_tags.clone(),
invalidates: matched.invalidates.clone(),
pending_component: matched.pending_component.clone(),
error_component: matched.error_component.clone(),
});
stack.push(QueuedFragment {
id: matched.fragment_id.clone(),
inventoryable: ctx
.protocol
.components
.get(&matched.fragment_id)
.is_some_and(has_template_payload),
route_base: child_base.clone(),
});
if matched.children.is_empty() {
break;
}
current = &matched.children;
base = child_base;
}
}
#[cfg(test)]
fn render_partial_metadata(
protocol: &WebUIProtocol,
entry_id: &str,
request_path: &str,
inventory_hex: &str,
index: &mut ProtocolIndex,
) -> Result<Value, HandlerError> {
let mut request_index = index.request_index();
render_partial_indexed(
protocol,
entry_id,
request_path,
inventory_hex,
&mut request_index,
)
}
#[cfg(test)]
fn render_partial(
protocol: &WebUIProtocol,
state: Value,
entry_id: &str,
request_path: &str,
inventory_hex: &str,
) -> Result<Value, HandlerError> {
let mut index = ProtocolIndex::new(protocol);
let mut request_index = index.request_index();
let (mut response, state_selection) = render_partial_indexed_with_state(
protocol,
entry_id,
request_path,
inventory_hex,
&mut request_index,
)?;
let response = response
.as_object_mut()
.ok_or_else(partial_response_not_object)?;
response.insert("state".into(), select_owned_state(state, &state_selection));
Ok(Value::Object(std::mem::take(response)))
}
#[cfg(test)]
fn render_partial_indexed(
protocol: &WebUIProtocol,
entry_id: &str,
request_path: &str,
inventory_hex: &str,
index: &mut RequestProtocolIndex<'_>,
) -> Result<Value, HandlerError> {
render_partial_indexed_with_state(protocol, entry_id, request_path, inventory_hex, index)
.map(|(response, _)| response)
}
fn render_partial_indexed_with_state<'a>(
protocol: &'a WebUIProtocol,
entry_id: &str,
request_path: &str,
inventory_hex: &str,
index: &mut RequestProtocolIndex<'_>,
) -> Result<(Value, StateSelection<'a>), HandlerError> {
let (component_ids, mut chain) =
collect_inventory_and_chain(protocol, entry_id, request_path, index);
let state_selection =
crate::collect_navigation_state(protocol, component_ids.iter().map(String::as_str));
let filtered =
filter_components_with_inventory(&component_ids, inventory_hex, index.component_index)?;
let mut accumulated_params: HashMap<String, String> = HashMap::new();
let mut all_resolved_tags: Vec<String> = Vec::new();
for entry in &mut chain {
for (k, v) in &entry.params {
accumulated_params.insert(k.clone(), v.clone());
}
entry.cache_tags = resolve_tag_templates(&entry.cache_tags, &accumulated_params);
entry.invalidates = resolve_tag_templates(&entry.invalidates, &accumulated_params);
all_resolved_tags.extend(entry.cache_tags.iter().cloned());
}
let tag_refs: Vec<&str> = filtered.needed.iter().map(String::as_str).collect();
let assets = collect_component_assets(protocol, &tag_refs, &filtered.client_inventory, index)?;
let chain_array = Value::Array(chain.iter().map(RouteChainEntry::to_json).collect());
let mut result = serde_json::Map::with_capacity(7);
result.insert("componentStyles".into(), assets.component_styles);
result.insert("templates".into(), Value::Object(assets.templates));
result.insert("templateFunctions".into(), Value::Object(assets.functions));
result.insert(
"inventory".into(),
Value::String(filtered.updated_inventory),
);
result.insert("path".into(), Value::String(request_path.to_string()));
result.insert("chain".into(), chain_array);
if !all_resolved_tags.is_empty() {
let mut seen = HashSet::new();
let deduped: Vec<Value> = all_resolved_tags
.into_iter()
.filter(|t| seen.insert(t.clone()))
.map(Value::String)
.collect();
result.insert("cacheTags".into(), Value::Array(deduped));
}
Ok((Value::Object(result), state_selection))
}
#[cfg(test)]
fn select_owned_state(state: Value, selection: &StateSelection<'_>) -> Value {
let state_keys = match selection {
StateSelection::Full => return state,
StateSelection::Keys(keys) => keys.as_slice(),
StateSelection::KeyIds(_) | StateSelection::FullExceptKeyIds(_) => {
return Value::Object(Map::new());
}
};
let Value::Object(mut source) = state else {
return Value::Object(Map::new());
};
let mut projected = Map::with_capacity(state_keys.len().min(source.len()));
for &key in state_keys {
if let Some(value) = source.remove(key) {
projected.insert(key.to_owned(), value);
}
}
Value::Object(projected)
}
#[cfg(test)]
fn render_action_response(
protocol: &WebUIProtocol,
state: Value,
entry_id: &str,
request_path: &str,
index: &mut ProtocolIndex,
) -> Value {
let mut request_index = index.request_index();
render_action_response_indexed(protocol, state, entry_id, request_path, &mut request_index)
}
#[cfg(test)]
fn render_action_response_indexed(
protocol: &WebUIProtocol,
state: Value,
entry_id: &str,
request_path: &str,
index: &mut RequestProtocolIndex<'_>,
) -> Value {
let chain = collect_route_chain(protocol, entry_id, request_path, index.route_index);
let mut accumulated_params: HashMap<String, String> = HashMap::new();
let mut all_invalidates: Vec<String> = Vec::new();
for entry in &chain {
for (k, v) in &entry.params {
accumulated_params.insert(k.clone(), v.clone());
}
let resolved = resolve_tag_templates(&entry.invalidates, &accumulated_params);
all_invalidates.extend(resolved);
}
let mut seen = HashSet::new();
let deduped: Vec<Value> = all_invalidates
.into_iter()
.filter(|t| seen.insert(t.clone()))
.map(Value::String)
.collect();
let mut result = serde_json::Map::with_capacity(3);
result.insert("state".into(), state);
result.insert("invalidateTags".into(), Value::Array(deduped));
result.insert("path".into(), Value::String(request_path.to_string()));
Value::Object(result)
}
#[cfg(test)]
fn render_component_templates(
protocol: &WebUIProtocol,
component_tags: &[&str],
inventory_hex: &str,
index: &mut ProtocolIndex,
) -> Result<Value, HandlerError> {
let mut request_index = index.request_index();
render_component_templates_indexed(protocol, component_tags, inventory_hex, &mut request_index)
}
fn render_component_templates_indexed(
protocol: &WebUIProtocol,
component_tags: &[&str],
inventory_hex: &str,
index: &mut RequestProtocolIndex<'_>,
) -> Result<Value, HandlerError> {
let mut seen: HashSet<&str> = HashSet::new();
let requested: Vec<String> = component_tags
.iter()
.filter(|s| seen.insert(**s))
.map(|s| (*s).to_string())
.collect();
let filtered =
filter_components_with_inventory(&requested, inventory_hex, index.component_index)?;
let tag_refs: Vec<&str> = filtered.needed.iter().map(String::as_str).collect();
let assets = collect_component_assets(protocol, &tag_refs, &filtered.client_inventory, index)?;
let mut result = serde_json::Map::with_capacity(4);
result.insert("componentStyles".into(), assets.component_styles);
result.insert("templates".into(), Value::Object(assets.templates));
result.insert("templateFunctions".into(), Value::Object(assets.functions));
result.insert(
"inventory".into(),
Value::String(filtered.updated_inventory),
);
Ok(Value::Object(result))
}
fn collect_component_assets(
protocol: &WebUIProtocol,
tags: &[&str],
client_inventory: &[u8],
index: &mut RequestProtocolIndex<'_>,
) -> Result<ComponentAssets, HandlerError> {
let component_styles = collect_component_styles_for_inventory(
protocol,
tags.iter().copied(),
client_inventory,
index.component_index,
)?;
let mut tmpl_map = serde_json::Map::new();
let mut function_map = serde_json::Map::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 !has_template_payload(component) {
continue;
}
if !component.template_json.is_empty() {
let template_value = cached_template_metadata(
&mut index.template_metadata_cache,
tag,
&component.template_json,
)?;
tmpl_map.insert(tag.to_string(), template_value);
if !component.template_functions.is_empty() {
function_map.insert(
tag.to_string(),
Value::String(component.template_functions.clone()),
);
}
} else {
tmpl_map.insert(tag.to_string(), Value::String(component.template.clone()));
}
}
Ok(ComponentAssets {
component_styles,
templates: tmpl_map,
functions: function_map,
})
}
fn cached_template_metadata(
cache: &mut TemplateMetadataCache<'_>,
tag: &str,
template_json: &str,
) -> Result<Value, HandlerError> {
match cache {
#[cfg(test)]
TemplateMetadataCache::Exclusive(cache) => {
cached_template_metadata_exclusive(cache, tag, template_json)
}
TemplateMetadataCache::Shared(cache) => {
if let Some(value) = cache
.read()
.map_err(|_| protocol_metadata_cache_poisoned())?
.get(tag)
.cloned()
{
return Ok(value);
}
let value = parse_template_metadata(tag, template_json)?;
let mut cache = cache
.write()
.map_err(|_| protocol_metadata_cache_poisoned())?;
Ok(cache.entry(tag.to_string()).or_insert(value).clone())
}
}
}
#[cfg(test)]
fn cached_template_metadata_exclusive(
cache: &mut HashMap<String, Value>,
tag: &str,
template_json: &str,
) -> Result<Value, HandlerError> {
if let Some(value) = cache.get(tag) {
return Ok(value.clone());
}
let value = parse_template_metadata(tag, template_json)?;
cache.insert(tag.to_string(), value.clone());
Ok(value)
}
fn parse_template_metadata(tag: &str, template_json: &str) -> Result<Value, HandlerError> {
serde_json::from_str(template_json).map_err(|error| {
HandlerError::Rendering(format!(
"failed to parse template metadata for {tag}: {error}"
))
})
}
#[cold]
#[inline(never)]
fn protocol_metadata_cache_poisoned() -> HandlerError {
HandlerError::Invariant(
"protocol metadata cache is unavailable after a previous panic".to_string(),
)
}
#[derive(Debug, Clone)]
pub struct RouteChainEntry {
pub component: String,
pub path: String,
pub params: HashMap<String, String>,
pub exact: bool,
pub allowed_query: String,
pub keep_alive: bool,
pub cache_tags: Vec<String>,
pub invalidates: Vec<String>,
pub pending_component: String,
pub error_component: String,
}
pub(crate) struct RouteChainPlan {
pub(crate) entries: Vec<RouteChainEntry>,
pub(crate) document_style_targets: Vec<bool>,
}
impl RouteChainEntry {
#[must_use]
pub fn to_json(&self) -> Value {
let mut obj = serde_json::Map::with_capacity(9);
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()),
);
}
if self.keep_alive {
obj.insert("keepAlive".into(), Value::Bool(true));
}
if !self.pending_component.is_empty() {
obj.insert(
"pendingComponent".into(),
Value::String(self.pending_component.clone()),
);
}
if !self.error_component.is_empty() {
obj.insert(
"errorComponent".into(),
Value::String(self.error_component.clone()),
);
}
if !self.invalidates.is_empty() {
obj.insert(
"invalidates".into(),
Value::Array(
self.invalidates
.iter()
.map(|s| Value::String(s.clone()))
.collect(),
),
);
}
Value::Object(obj)
}
}
#[cfg(test)]
pub(crate) fn collect_route_chain(
protocol: &WebUIProtocol,
entry_id: &str,
request_path: &str,
route_index: &CompiledRouteIndex,
) -> Vec<RouteChainEntry> {
collect_route_chain_plan(protocol, entry_id, request_path, route_index).entries
}
struct RouteChainWork {
id: String,
route_base: String,
targets_document: bool,
}
pub(crate) fn collect_route_chain_plan(
protocol: &WebUIProtocol,
entry_id: &str,
request_path: &str,
route_index: &CompiledRouteIndex,
) -> RouteChainPlan {
let mut chain = Vec::new();
let mut document_style_targets = Vec::new();
let mut visited_fragments = HashSet::new();
let mut stack = vec![RouteChainWork {
id: entry_id.to_string(),
route_base: "/".to_string(),
targets_document: !protocol.component_uses_shadow_dom(entry_id),
}];
while let Some(queued) = stack.pop() {
if queued.id.is_empty()
|| !visited_fragments.insert((queued.id.clone(), queued.route_base.clone()))
{
continue;
}
let Some(frag_list) = protocol.fragments.get(&queued.id) else {
continue;
};
let matched_route = route_renderer::find_best_route_match(
&frag_list.fragments,
request_path,
&queued.route_base,
route_index,
);
for frag in &frag_list.fragments {
match frag.fragment.as_ref() {
Some(Fragment::Component(component)) => {
stack.push(RouteChainWork {
id: component.fragment_id.clone(),
route_base: queued.route_base.clone(),
targets_document: queued.targets_document
&& !protocol.component_uses_shadow_dom(&component.fragment_id),
});
}
Some(Fragment::ForLoop(for_loop)) => {
stack.push(RouteChainWork {
id: for_loop.fragment_id.clone(),
route_base: queued.route_base.clone(),
targets_document: queued.targets_document,
});
}
Some(Fragment::IfCond(if_cond)) => {
stack.push(RouteChainWork {
id: if_cond.fragment_id.clone(),
route_base: queued.route_base.clone(),
targets_document: queued.targets_document,
});
}
Some(Fragment::Route(route_frag)) => {
let is_selected = matched_route
.as_ref()
.is_some_and(|(best_key, _)| best_key == route_frag.fragment_id.as_str());
if is_selected && !route_frag.fragment_id.is_empty() {
if let Some((_, ref rm)) = matched_route {
let targets_document = queued.targets_document
&& !protocol.component_uses_shadow_dom(&route_frag.fragment_id);
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(),
keep_alive: route_frag.keep_alive,
cache_tags: route_frag.cache_tags.clone(),
invalidates: route_frag.invalidates.clone(),
pending_component: route_frag.pending_component.clone(),
error_component: route_frag.error_component.clone(),
});
document_style_targets.push(targets_document);
let child_route_base = route_matcher::compute_route_base(
request_path,
rm.consumed_segments,
);
stack.push(RouteChainWork {
id: route_frag.fragment_id.clone(),
route_base: child_route_base.clone(),
targets_document,
});
let mut output = RouteChainOutput {
entries: &mut chain,
document_style_targets: &mut document_style_targets,
};
collect_chain_from_children(
&route_frag.children,
&child_route_base,
targets_document,
&mut output,
&mut ChildWalkCtx {
request_path,
protocol,
route_index,
},
);
}
}
}
_ => {}
}
}
}
RouteChainPlan {
entries: chain,
document_style_targets,
}
}
struct RouteChainOutput<'a> {
entries: &'a mut Vec<RouteChainEntry>,
document_style_targets: &'a mut Vec<bool>,
}
fn collect_chain_from_children(
children: &[WebUIFragmentRoute],
route_base: &str,
targets_document: bool,
output: &mut RouteChainOutput<'_>,
ctx: &mut ChildWalkCtx<'_>,
) {
let mut pending: Vec<(&[WebUIFragmentRoute], String, bool)> =
vec![(children, route_base.to_string(), targets_document)];
while let Some((current, base, current_targets_document)) = pending.pop() {
if let Some((idx, rm)) =
select_best_child_route(current, ctx.request_path, &base, ctx.route_index)
{
let matched = ¤t[idx];
let matched_targets_document = current_targets_document
&& !ctx.protocol.component_uses_shadow_dom(&matched.fragment_id);
output.entries.push(RouteChainEntry {
component: matched.fragment_id.clone(),
path: matched.path.clone(),
params: rm.params,
exact: matched.exact,
allowed_query: matched.allowed_query.clone(),
keep_alive: matched.keep_alive,
cache_tags: matched.cache_tags.clone(),
invalidates: matched.invalidates.clone(),
pending_component: matched.pending_component.clone(),
error_component: matched.error_component.clone(),
});
output.document_style_targets.push(matched_targets_document);
if !matched.children.is_empty() {
let child_base =
route_matcher::compute_route_base(ctx.request_path, rm.consumed_segments);
pending.push((&matched.children, child_base, matched_targets_document));
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
use std::sync::{Arc, Barrier};
use std::thread;
use webui_protocol::{FragmentList, WebUIFragment, WebUiFragmentRoute};
#[test]
fn protocol_caches_inline_style_resources_requiring_escape() {
let mut protocol = WebUIProtocol::new(HashMap::new());
protocol.set_css_strategy(CssStrategy::Style);
protocol.components.insert(
"safe-card".to_string(),
webui_protocol::ComponentData {
css: ".safe{color:green}".to_string(),
..Default::default()
},
);
protocol.components.insert(
"unsafe-card".to_string(),
webui_protocol::ComponentData {
css: ".unsafe{content:'</StYlE>'}".to_string(),
..Default::default()
},
);
protocol.style_chunks.push(webui_protocol::StyleChunk {
name: "_chunk-safe-card-2".to_string(),
css: ".safe{color:green}.other{color:blue}".to_string(),
..Default::default()
});
protocol.style_chunks.push(webui_protocol::StyleChunk {
name: "_chunk-unsafe-card-2".to_string(),
css: ".unsafe{content:'</style>'}.other{color:blue}".to_string(),
..Default::default()
});
let prepared = Protocol::new(protocol);
let resources = prepared.style_resources_requiring_escape();
assert_eq!(resources.len(), 2);
assert!(resources.contains("unsafe-card"));
assert!(resources.contains("_chunk-unsafe-card-2"));
}
#[test]
fn protocol_decodes_once_and_exposes_tokens() {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<p>Prepared</p>")],
contains_boundary: false,
},
);
let protocol = WebUIProtocol::with_tokens(fragments, vec!["colorBrand".to_string()]);
let bytes = protocol.to_protobuf().unwrap();
let prepared = Protocol::from_protobuf(&bytes).unwrap();
assert!(prepared.protocol().fragments.contains_key("index.html"));
assert_eq!(prepared.tokens(), ["colorBrand"]);
}
#[test]
fn continuation_state_plans_memoize_per_entry_without_locking() {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<p>plan</p>")],
contains_boundary: false,
},
);
let prepared = Protocol::new(WebUIProtocol::new(fragments));
let first = prepared.continuation_state_plan("index.html").unwrap();
let second = prepared.continuation_state_plan("index.html").unwrap();
assert!(
std::ptr::eq(first, second),
"a memoized plan must be borrowed, not rebuilt or cloned"
);
let cell = std::mem::size_of::<OnceLock<PreparedContinuationStatePlan>>();
assert!(
cell <= 40,
"continuation plan memo cell grew to {cell} bytes"
);
let barrier = Arc::new(Barrier::new(4));
let address = std::ptr::from_ref(first).addr();
thread::scope(|scope| {
for _ in 0..4 {
let barrier = Arc::clone(&barrier);
let prepared = &prepared;
scope.spawn(move || {
barrier.wait();
let plan = prepared.continuation_state_plan("index.html").unwrap();
assert_eq!(
std::ptr::from_ref(plan).addr(),
address,
"concurrent responses must share one initialization"
);
assert!(plan.resolve().is_ok());
});
}
});
}
#[test]
fn continuation_state_plans_replay_captured_failures() {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![WebUIFragment::component("missing-card")],
contains_boundary: false,
},
);
let prepared = Protocol::new(WebUIProtocol::new(fragments));
for _ in 0..2 {
match prepared
.continuation_state_plan("index.html")
.and_then(|plan| plan.resolve())
.err()
{
Some(HandlerError::MissingFragment(id)) => assert_eq!(id, "missing-card"),
other => panic!("expected a replayable missing-record failure, got {other:?}"),
}
}
match prepared.continuation_state_plan("absent.html").err() {
Some(HandlerError::MissingFragment(id)) => assert_eq!(id, "absent.html"),
other => panic!("expected a missing-entry diagnostic, got {other:?}"),
}
}
#[test]
fn component_reachability_interns_hydration_projections() {
let mut fragments = HashMap::new();
fragments.insert("keyed-card".to_string(), FragmentList::default());
fragments.insert("all-card".to_string(), FragmentList::default());
fragments.insert("bare-card".to_string(), FragmentList::default());
let mut protocol = WebUIProtocol::new(fragments);
protocol.components.insert(
"keyed-card".to_string(),
webui_protocol::ComponentData {
hydration_mode: StateProjectionMode::Keys as i32,
hydration_keys: vec!["zebra".to_string(), "alpha".to_string()],
..Default::default()
},
);
protocol.components.insert(
"all-card".to_string(),
webui_protocol::ComponentData {
hydration_mode: StateProjectionMode::All as i32,
..Default::default()
},
);
let prepared = Protocol::new(protocol);
let index = prepared.component_reachability();
let component = prepared.component_index();
let mut ids = Vec::new();
assert!(
!index.extend_hydration_keys(component["keyed-card"], &mut ids),
"a keyed surface projects keys"
);
ids.sort_unstable();
let keys: Vec<&str> = ids
.iter()
.filter_map(|id| index.hydration_key(*id))
.collect();
assert_eq!(
keys,
["alpha", "zebra"],
"sorted IDs must be lexicographically sorted keys"
);
let mut ids = Vec::new();
assert!(
index.extend_hydration_keys(component["all-card"], &mut ids),
"an ALL surface forces full state"
);
assert!(
index.extend_hydration_keys(component["bare-card"], &mut ids),
"a component without compiled metadata forces full state"
);
}
#[test]
fn protocol_retains_only_serialized_component_asset_styles() {
let mut protocol = WebUIProtocol::default();
protocol.component_asset_style_preloads =
vec![webui_protocol::ComponentAssetStylePreload {
root: "lazy-panel".to_string(),
style_hrefs: vec!["/lazy-panel.css".to_string()],
}];
let prepared = Protocol::new(protocol);
assert!(prepared
.protocol()
.component_asset_style_preloads
.is_empty());
assert_eq!(
prepared.component_asset_style_manifest().unwrap(),
r#"{"lazy-panel":["/lazy-panel.css"]}"#
);
assert!(prepared.component_asset_style_links().is_empty());
}
#[test]
fn protocol_precomputes_deduplicated_light_component_asset_styles() {
let mut protocol = WebUIProtocol::default();
protocol.set_css_strategy(webui_protocol::CssStrategy::Link);
for root in ["lazy-panel", "secondary-panel"] {
protocol.components.insert(
root.to_string(),
webui_protocol::ComponentData {
uses_shadow_dom: false,
..Default::default()
},
);
}
protocol.component_asset_style_preloads = vec![
webui_protocol::ComponentAssetStylePreload {
root: "lazy-panel".to_string(),
style_hrefs: vec![
"/lazy-panel.css".to_string(),
"/shared.css?theme=a&mode=\"dark\"".to_string(),
],
},
webui_protocol::ComponentAssetStylePreload {
root: "secondary-panel".to_string(),
style_hrefs: vec![
"/secondary-panel.css".to_string(),
"/shared.css?theme=a&mode=\"dark\"".to_string(),
],
},
];
let prepared = Protocol::new(protocol);
assert!(prepared
.protocol()
.component_asset_style_preloads
.is_empty());
assert!(prepared
.component_asset_style_manifest()
.unwrap()
.is_empty());
assert_eq!(
prepared.component_asset_style_links(),
concat!(
r#"<link rel="stylesheet" href="/lazy-panel.css">"#,
r#"<link rel="stylesheet" href="/shared.css?theme=a&mode="dark"">"#,
r#"<link rel="stylesheet" href="/secondary-panel.css">"#,
)
);
}
#[test]
fn protocol_partial_matches_direct_index_path() {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![WebUIFragment::route("/", "home-page")],
contains_boundary: false,
},
);
fragments.insert(
"home-page".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<p>Home</p>")],
contains_boundary: false,
},
);
let protocol = WebUIProtocol::new(fragments);
let mut direct_index = ProtocolIndex::new(&protocol);
let direct =
render_partial_metadata(&protocol, "index.html", "/", "", &mut direct_index).unwrap();
let prepared = Protocol::new(protocol);
let cached = prepared
.render_partial_metadata("index.html", "/", "")
.unwrap();
assert_eq!(cached, direct);
}
#[test]
fn protocol_partial_supports_concurrent_metadata_reads() {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![WebUIFragment::route("/", "home-page")],
contains_boundary: false,
},
);
fragments.insert(
"home-page".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<p>Home</p>")],
contains_boundary: false,
},
);
let mut protocol = WebUIProtocol::new(fragments);
protocol.components.insert(
"home-page".to_string(),
webui_protocol::ComponentData {
template_json: r#"{"h":"<p>Home</p>"}"#.to_string(),
..Default::default()
},
);
let prepared = Arc::new(Protocol::new(protocol));
let barrier = Arc::new(Barrier::new(4));
let handles: Vec<_> = (0..4)
.map(|_| {
let prepared = Arc::clone(&prepared);
let barrier = Arc::clone(&barrier);
thread::spawn(move || {
barrier.wait();
for _ in 0..50 {
let response = prepared
.render_partial_metadata("index.html", "/", "")
.unwrap();
assert_eq!(response["templates"]["home-page"]["h"], "<p>Home</p>");
}
})
})
.collect();
for handle in handles {
handle.join().unwrap();
}
}
fn prepared_partial_protocol(hydration_keys: &[&str]) -> Protocol {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![WebUIFragment::route("/", "home-page")],
contains_boundary: false,
},
);
fragments.insert(
"home-page".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<p>Home</p>")],
contains_boundary: false,
},
);
let mut protocol = WebUIProtocol::new(fragments);
protocol.components.insert(
"home-page".to_string(),
webui_protocol::ComponentData {
template_json: r#"{"h":"<p>Home</p>"}"#.to_string(),
hydration_mode: webui_protocol::StateProjectionMode::Keys as i32,
hydration_keys: hydration_keys
.iter()
.map(|key| (*key).to_string())
.collect(),
navigation_mode: Some(webui_protocol::StateProjectionMode::Keys as i32),
navigation_keys: hydration_keys
.iter()
.map(|key| (*key).to_string())
.collect(),
..Default::default()
},
);
Protocol::new(protocol)
}
fn prepared_full_state_partial_protocol() -> Protocol {
let mut prepared = prepared_partial_protocol(&[]);
let protocol = &mut prepared.protocol;
if let Some(component) = protocol.components.get_mut("home-page") {
component.navigation_mode = Some(webui_protocol::StateProjectionMode::All as i32);
component.navigation_keys.clear();
}
prepared
}
#[test]
fn partial_state_serialization_preserves_validated_raw_json() {
let prepared = prepared_partial_protocol(&["value"]);
let output = prepared
.render_partial(
r#"{"serverOnly":"drop","value":1e2}"#,
"index.html",
"/",
"",
)
.unwrap();
assert!(output.contains(r#""state":{"value":1e2}"#));
assert!(!output.contains("serverOnly"));
let parsed: Value = serde_json::from_str(&output).unwrap();
assert_eq!(parsed["state"]["value"], 100.0);
}
#[test]
fn partial_state_projection_strips_reserved_inject_key() {
let prepared = prepared_partial_protocol(&[crate::STATE_INJECT_KEY, "value"]);
let output = prepared
.render_partial(
r#"{"$webui":{"bodyEnd":"<script>secret</script>"},"serverOnly":"drop","value":1}"#,
"index.html",
"/",
"",
)
.unwrap();
let parsed: Value = serde_json::from_str(&output).unwrap();
assert_eq!(parsed["state"], serde_json::json!({"value": 1}));
assert!(!output.contains("<script>secret</script>"), "{output}");
}
#[test]
fn uncertain_partial_surface_preserves_complete_raw_state() {
let prepared = prepared_full_state_partial_protocol();
let output = prepared
.render_partial(
r#"{"serverOnly":"keep","value":1e2}"#,
"index.html",
"/",
"",
)
.unwrap();
assert!(output.contains(r#""state":{"serverOnly":"keep","value":1e2}"#));
}
#[test]
fn uncertain_partial_surface_strips_escaped_reserved_inject_key() {
let prepared = prepared_full_state_partial_protocol();
for state in [
r#"{"\u0024webui":{"bodyEnd":"<script>secret</script>"},"serverOnly":"keep","value":1}"#,
r#"{"$\u0077\u0065\u0062\u0075\u0069":{"bodyEnd":"<script>secret</script>"},"serverOnly":"keep","value":1}"#,
] {
let output = prepared
.render_partial(state, "index.html", "/", "")
.unwrap();
let parsed: Value = serde_json::from_str(&output).unwrap();
assert_eq!(
parsed["state"],
serde_json::json!({"serverOnly": "keep", "value": 1})
);
assert!(!output.contains("<script>secret</script>"), "{output}");
}
}
#[test]
fn uncertain_partial_surface_preserves_nested_reserved_key_bytes() {
let prepared = prepared_full_state_partial_protocol();
let output = prepared
.render_partial(
r#"{"outer": { "$webui": {"bodyEnd": "application data"} }, "value": 1e2}"#,
"index.html",
"/",
"",
)
.unwrap();
assert!(
output.contains(
r#""state":{"outer": { "$webui": {"bodyEnd": "application data"} }, "value": 1e2}"#
),
"{output}"
);
}
#[test]
fn partial_state_serialization_rejects_invalid_json() {
let prepared = prepared_partial_protocol(&[]);
let error = prepared
.render_partial(r#"{"broken":"#, "index.html", "/", "")
.expect_err("invalid state JSON must fail");
assert!(
matches!(error, HandlerError::InvalidState(_)),
"host bindings classify caller state errors by variant, got {error:?}"
);
assert!(error.to_string().contains("invalid state JSON"));
}
#[test]
fn partial_state_serialization_emits_empty_state_without_client_components() {
let prepared = Protocol::new(WebUIProtocol::default());
let output = prepared
.render_partial(r#"{"serverOnly":"drop"}"#, "index.html", "/", "")
.unwrap();
let parsed: Value = serde_json::from_str(&output).unwrap();
assert_eq!(parsed["state"], serde_json::json!({}));
}
#[test]
fn partial_state_serialization_validates_selected_and_skipped_numbers() {
let prepared = prepared_partial_protocol(&["value"]);
for state in [r#"{"value":1e9999}"#, r#"{"serverOnly":1e9999,"value":1}"#] {
let error = prepared
.render_partial(state, "index.html", "/", "")
.expect_err("out-of-range state numbers must fail");
assert!(
matches!(error, HandlerError::InvalidState(_)),
"host bindings classify caller state errors by variant, got {error:?}"
);
assert!(error.to_string().contains("invalid state JSON"));
}
}
#[test]
fn partial_state_serialization_uses_last_duplicate_and_decodes_keys() {
let prepared = prepared_partial_protocol(&["value"]);
let output = prepared
.render_partial(r#"{"value":1,"va\u006cue":2}"#, "index.html", "/", "")
.unwrap();
let parsed: Value = serde_json::from_str(&output).unwrap();
assert_eq!(parsed["state"], serde_json::json!({"value": 2}));
}
#[test]
fn parsed_partial_state_projects_without_cloning_server_only_values() {
let prepared = prepared_partial_protocol(&["value"]);
let response = render_partial(
prepared.protocol(),
serde_json::json!({"serverOnly": [1, 2, 3], "value": "kept"}),
"index.html",
"/",
"",
)
.unwrap();
assert_eq!(response["state"], serde_json::json!({"value": "kept"}));
}
#[test]
fn uncertain_partial_surface_preserves_complete_parsed_state() {
let prepared = prepared_full_state_partial_protocol();
let state = serde_json::json!({"serverOnly": [1, 2, 3], "value": "kept"});
let response =
render_partial(prepared.protocol(), state.clone(), "index.html", "/", "").unwrap();
assert_eq!(response["state"], state);
}
#[test]
fn partial_state_validation_matches_value_number_limits() {
for json in [
r#"{"value":1e9999}"#,
r#"{"value":-1e9999}"#,
r#"{"value":1e308}"#,
r#"{"value":18446744073709551615}"#,
r#"{"value":18446744073709551616}"#,
] {
assert_eq!(
validate_json(json).is_ok(),
serde_json::from_str::<Value>(json).is_ok(),
"validation mismatch for {json}"
);
}
}
#[test]
fn test_parse_encode_inventory_roundtrip() {
let original = vec![0xABu8, 0xCD, 0xEF, 0x01];
let hex = encode_inventory(&original);
let decoded = parse_inventory(&hex).unwrap();
assert_eq!(original, decoded);
}
#[test]
fn test_parse_inventory_empty() {
let result = parse_inventory("").unwrap();
assert!(result.is_empty());
}
#[test]
fn test_parse_inventory_rejects_odd_length() {
let result = parse_inventory("abc");
assert!(result.is_err());
}
#[test]
fn test_parse_inventory_rejects_invalid_hex() {
let result = parse_inventory("zz");
assert!(result.is_err());
}
#[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 names = vec!["email-message".to_string(), "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).unwrap();
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")],
contains_boundary: false,
},
);
fragments.insert(
"my-card".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("card")],
contains_boundary: false,
},
);
let protocol = WebUIProtocol::with_tokens(fragments, Vec::new());
let comp_index = build_component_index(&protocol);
let (needed, _inv) =
get_needed_components(&protocol, "app-shell", "", &comp_index).unwrap();
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")],
contains_boundary: false,
},
);
fragments.insert(
"my-card".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("card")],
contains_boundary: false,
},
);
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 comp_index = build_component_index(&protocol);
let (_needed, inv_hex) =
get_needed_components(&protocol, "app-shell", "", &comp_index).unwrap();
let (needed2, _) =
get_needed_components(&protocol, "app-shell", &inv_hex, &comp_index).unwrap();
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"),
],
contains_boundary: false,
},
);
fragments.insert(
"if-shell".to_string(),
FragmentList {
fragments: vec![WebUIFragment::component("mp-category-nav")],
contains_boundary: false,
},
);
fragments.insert(
"for-items".to_string(),
FragmentList {
fragments: vec![WebUIFragment::component("mp-product-card")],
contains_boundary: false,
},
);
fragments.insert(
"attr-title".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("Products")],
contains_boundary: false,
},
);
fragments.insert(
"mp-category-nav".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<nav></nav>")],
contains_boundary: false,
},
);
fragments.insert(
"mp-product-card".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<article></article>")],
contains_boundary: false,
},
);
let protocol = WebUIProtocol::with_tokens(fragments, Vec::new());
let comp_index = build_component_index(&protocol);
let needed: HashSet<String> =
get_needed_components(&protocol, "app-shell", "", &comp_index)
.unwrap()
.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")],
contains_boundary: false,
},
);
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,
keep_alive: false,
..Default::default()
}),
WebUIFragment::route_from(WebUiFragmentRoute {
path: "/product/:handle".into(),
fragment_id: "mp-product-page".into(),
exact: true,
keep_alive: false,
..Default::default()
}),
],
contains_boundary: false,
},
);
fragments.insert(
"mp-category-nav".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<nav></nav>")],
contains_boundary: false,
},
);
fragments.insert(
"mp-search-page".to_string(),
FragmentList {
fragments: vec![WebUIFragment::component("mp-product-grid")],
contains_boundary: false,
},
);
fragments.insert(
"mp-product-grid".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<div>grid</div>")],
contains_boundary: false,
},
);
fragments.insert(
"mp-product-page".to_string(),
FragmentList {
fragments: vec![WebUIFragment::component("mp-product-detail")],
contains_boundary: false,
},
);
fragments.insert(
"mp-product-detail".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<div>detail</div>")],
contains_boundary: false,
},
);
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 comp_index = build_component_index(&protocol);
let (needed, _inv) = get_needed_components_for_request(
&protocol,
"index.html",
"/search/shirts",
"",
(&comp_index, &CompiledRouteIndex::new(&protocol)),
)
.unwrap();
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")],
contains_boundary: false,
},
);
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,
keep_alive: false,
..Default::default()
})],
contains_boundary: false,
},
);
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,
keep_alive: false,
..Default::default()
}),
WebUIFragment::route_from(WebUiFragmentRoute {
path: "/account/orders/:id".into(),
fragment_id: "mp-order-page".into(),
exact: true,
keep_alive: false,
..Default::default()
}),
],
contains_boundary: false,
},
);
fragments.insert(
"mp-account-nav".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<nav></nav>")],
contains_boundary: false,
},
);
fragments.insert(
"mp-profile-page".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<profile></profile>")],
contains_boundary: false,
},
);
fragments.insert(
"mp-order-page".to_string(),
FragmentList {
fragments: vec![WebUIFragment::component("mp-order-detail")],
contains_boundary: false,
},
);
fragments.insert(
"mp-order-detail".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<detail></detail>")],
contains_boundary: false,
},
);
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 comp_index = build_component_index(&protocol);
let (needed, _inv) = get_needed_components_for_request(
&protocol,
"index.html",
"/account/orders/42",
"",
(&comp_index, &CompiledRouteIndex::new(&protocol)),
)
.unwrap();
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")],
contains_boundary: false,
},
);
fragments.insert(
"mp-app".to_string(),
FragmentList {
fragments: vec![WebUIFragment::route_from(WebUiFragmentRoute {
path: "/search".into(),
fragment_id: "mp-search-page".into(),
exact: true,
keep_alive: false,
..Default::default()
})],
contains_boundary: false,
},
);
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"),
],
contains_boundary: false,
},
);
fragments.insert(
"if-filters".to_string(),
FragmentList {
fragments: vec![WebUIFragment::component("mp-filter-panel")],
contains_boundary: false,
},
);
fragments.insert(
"item-loop".to_string(),
FragmentList {
fragments: vec![WebUIFragment::component("mp-item-card")],
contains_boundary: false,
},
);
fragments.insert(
"mp-filter-panel".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<filters></filters>")],
contains_boundary: false,
},
);
fragments.insert(
"mp-item-card".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<item></item>")],
contains_boundary: false,
},
);
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 comp_index = build_component_index(&protocol);
let (needed, _inv) = get_needed_components_for_request(
&protocol,
"index.html",
"/search",
"",
(&comp_index, &CompiledRouteIndex::new(&protocol)),
)
.unwrap();
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")],
contains_boundary: false,
},
);
fragments.insert(
"mp-app".to_string(),
FragmentList {
fragments: vec![WebUIFragment::route_from(WebUiFragmentRoute {
path: "/search".into(),
fragment_id: "mp-search-page".into(),
exact: true,
keep_alive: false,
..Default::default()
})],
contains_boundary: false,
},
);
fragments.insert(
"mp-search-page".to_string(),
FragmentList {
fragments: vec![WebUIFragment::component("mp-product-grid")],
contains_boundary: false,
},
);
fragments.insert(
"mp-product-grid".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<grid></grid>")],
contains_boundary: false,
},
);
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 comp_index = build_component_index(&protocol);
let route_index = CompiledRouteIndex::new(&protocol);
let (_needed, inventory) = get_needed_components_for_request(
&protocol,
"index.html",
"/search",
"",
(&comp_index, &route_index),
)
.unwrap();
let (needed_again, _) = get_needed_components_for_request(
&protocol,
"index.html",
"/search",
&inventory,
(&comp_index, &route_index),
)
.unwrap();
assert!(needed_again.is_empty());
}
#[test]
fn test_inventory_includes_boundary_components_for_unmatched_sibling_routes() {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![WebUIFragment::route_from(WebUiFragmentRoute {
path: "/".into(),
fragment_id: "route-shell".into(),
children: vec![
WebUiFragmentRoute {
path: "slow".into(),
fragment_id: "page-slow".into(),
exact: true,
pending_component: "loading-skeleton".into(),
..Default::default()
},
WebUiFragmentRoute {
path: "failing".into(),
fragment_id: "page-failing".into(),
exact: true,
error_component: "error-display".into(),
..Default::default()
},
],
..Default::default()
})],
contains_boundary: false,
},
);
for id in [
"route-shell",
"page-slow",
"page-failing",
"loading-skeleton",
"error-display",
] {
fragments.insert(
id.to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<x></x>")],
contains_boundary: false,
},
);
}
let mut protocol = WebUIProtocol::with_tokens(fragments, Vec::new());
for id in [
"route-shell",
"page-slow",
"page-failing",
"loading-skeleton",
"error-display",
] {
protocol
.components
.entry(id.to_string())
.or_default()
.template = format!("<f-template id=\"{id}\"></f-template>");
}
let reachable = collect_reachable_component_order_for_request(
&protocol,
"index.html",
"/",
&CompiledRouteIndex::new(&protocol),
);
assert!(
reachable.iter().any(|name| name == "loading-skeleton"),
"pending component of an unmatched sibling route must be inventoried: {reachable:?}"
);
assert!(
reachable.iter().any(|name| name == "error-display"),
"error component of an unmatched sibling route must be inventoried: {reachable:?}"
);
}
#[test]
fn component_reachability_propagates_route_dependency_to_parent() {
let protocol = WebUIProtocol::new(HashMap::from([
(
"comp-a".to_string(),
FragmentList {
fragments: vec![WebUIFragment::component("comp-b")],
contains_boundary: false,
},
),
(
"comp-b".to_string(),
FragmentList {
fragments: vec![WebUIFragment::route("/", "page-a")],
contains_boundary: false,
},
),
(
"page-a".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<p>page</p>")],
contains_boundary: false,
},
),
]));
let component_index = build_component_index(&protocol);
let reachability = ComponentReachabilityIndex::new(&protocol, &component_index);
assert_eq!(
reachability.is_route_dependent(component_index["comp-a"]),
Some(true)
);
assert_eq!(
reachability.is_route_dependent(component_index["comp-b"]),
Some(true)
);
assert_eq!(
reachability.is_route_dependent(component_index["page-a"]),
Some(false)
);
}
#[test]
fn request_reachability_visits_reused_route_component_at_each_base() {
let mut protocol = WebUIProtocol::new(HashMap::from([
(
"shared-shell".to_string(),
FragmentList {
fragments: vec![WebUIFragment::route("details", "detail-page")],
contains_boundary: false,
},
),
(
"detail-page".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<p>Detail</p>")],
contains_boundary: false,
},
),
]));
for tag in ["shared-shell", "detail-page"] {
protocol
.components
.entry(tag.to_string())
.or_default()
.template = "<template></template>".to_string();
}
let route_index = CompiledRouteIndex::new(&protocol);
let handler_protocol = Protocol::new(protocol.clone());
let reachability = handler_protocol.component_reachability();
let shell = handler_protocol.component_index()["shared-shell"];
let roots = [
(shell, Some(Box::from("/other"))),
(shell, Some(Box::from("/account"))),
];
let reachable = collect_reachable_components_from_roots(
&protocol,
&roots,
reachability,
"/account/details",
&route_index,
);
assert!(
reachable.iter().any(|tag| tag == "detail-page"),
"the second route base must not be hidden by fragment-id deduplication: {reachable:?}"
);
}
#[test]
fn component_reachability_index_is_lazy() {
let protocol = Protocol::new(WebUIProtocol::new(HashMap::from([(
"comp-a".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<p>leaf</p>")],
contains_boundary: false,
},
)])));
assert!(protocol.component_reachability.get().is_none());
assert_eq!(
protocol
.component_reachability()
.is_route_dependent(protocol.component_index["comp-a"]),
Some(false)
);
assert!(protocol.component_reachability.get().is_some());
}
#[test]
fn test_render_partial_carries_module_css_in_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();
protocol.populate_style_closures(&["index.html"]);
let mut index = ProtocolIndex::new(&protocol);
let partial =
render_partial_metadata(&protocol, "index.html", "/", "", &mut index).unwrap();
assert_eq!(partial["componentStyles"]["version"], 1);
assert_eq!(partial["componentStyles"]["strategy"], "module");
assert_eq!(
partial["componentStyles"]["resources"]["my-page"],
serde_json::json!({
"kind": "module",
"specifier": "my-page",
"css": ".page{color:red}"
})
);
assert_eq!(
partial["componentStyles"]["closures"]["my-page"],
serde_json::json!(["my-page"])
);
let templates = partial["templates"]
.as_object()
.expect("templates should be an object");
assert_eq!(templates.len(), 1);
let template = templates.get("my-page").expect("my-page template");
assert_eq!(template["h"], "<p>page</p>");
let inventory = partial["inventory"].as_str().expect("inventory");
let repeated =
render_partial_metadata(&protocol, "index.html", "/", inventory, &mut index).unwrap();
assert!(repeated["templates"].as_object().unwrap().is_empty());
assert_eq!(
repeated["componentStyles"]["resources"],
serde_json::json!({}),
"inventoried style definitions must not be retransmitted"
);
assert_eq!(
repeated["componentStyles"]["closures"],
serde_json::json!({}),
"inventoried style closures must not be retransmitted"
);
}
#[test]
fn styled_protocol_without_closures_is_rejected() {
let mut protocol = WebUIProtocol::default();
protocol.fragments = HashMap::from([
(
"index.html".to_string(),
FragmentList {
fragments: vec![WebUIFragment::component("legacy-page")],
contains_boundary: false,
},
),
(
"legacy-page".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<p>Legacy</p>")],
contains_boundary: false,
},
),
]);
protocol.components.insert(
"legacy-page".to_string(),
webui_protocol::ComponentData {
template_json: r#"{"h":"<p>Legacy</p>"}"#.to_string(),
css: ".legacy{display:block}".to_string(),
..Default::default()
},
);
protocol.set_css_strategy(webui_protocol::CssStrategy::Module);
let mut index = ProtocolIndex::new(&protocol);
let error = render_partial_metadata(&protocol, "index.html", "/", "", &mut index)
.expect_err("styled protocols require closure metadata");
assert!(error
.to_string()
.contains("component style closure metadata is required"));
}
#[test]
fn test_render_partial_link_strategy_uses_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::Link);
let component = protocol
.components
.entry("my-page".to_string())
.or_default();
component.template_json = r#"{"h":"<p>page</p>"}"#.to_string();
component.css_href = "my-page.css".to_string();
protocol.populate_style_closures(&["index.html"]);
let mut index = ProtocolIndex::new(&protocol);
let partial =
render_partial_metadata(&protocol, "index.html", "/", "", &mut index).unwrap();
let templates = partial["templates"]
.as_object()
.expect("templates should be an object");
assert_eq!(templates.len(), 1, "should include template metadata");
assert_eq!(
partial["componentStyles"]["resources"]["my-page"],
serde_json::json!({"kind": "link", "href": "my-page.css"})
);
}
#[test]
fn test_render_partial_style_strategy_uses_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::Style);
let component = protocol
.components
.entry("my-page".to_string())
.or_default();
component.template_json = r#"{"h":"<p>page</p>"}"#.to_string();
component.css = ".p{color:red}".to_string();
protocol.populate_style_closures(&["index.html"]);
let mut index = ProtocolIndex::new(&protocol);
let partial =
render_partial_metadata(&protocol, "index.html", "/", "", &mut index).unwrap();
let templates = partial["templates"]
.as_object()
.expect("templates should be an object");
assert_eq!(templates.len(), 1, "should include template metadata");
assert_eq!(
partial["componentStyles"]["resources"]["my-page"],
serde_json::json!({"kind": "style", "css": ".p{color:red}"})
);
}
#[test]
fn test_render_partial_emits_empty_component_styles_for_no_css_components() {
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 component = protocol
.components
.entry("my-page".to_string())
.or_default();
component.template_json = r#"{"h":"<p>page</p>"}"#.to_string();
let mut index = ProtocolIndex::new(&protocol);
let partial =
render_partial_metadata(&protocol, "index.html", "/", "", &mut index).unwrap();
assert_eq!(
partial["componentStyles"]["resources"],
serde_json::json!({})
);
assert_eq!(
partial["componentStyles"]["closures"],
serde_json::json!({})
);
}
#[test]
fn test_render_partial_filters_styles_with_template_inventory() {
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();
protocol.populate_style_closures(&["index.html"]);
let mut index = ProtocolIndex::new(&protocol);
let partial1 =
render_partial_metadata(&protocol, "index.html", "/", "", &mut index).unwrap();
let inv = partial1["inventory"].as_str().unwrap_or_default();
assert!(!inv.is_empty());
let partial2 =
render_partial_metadata(&protocol, "index.html", "/", inv, &mut index).unwrap();
let templates = partial2["templates"]
.as_object()
.expect("templates should be an object");
assert!(
templates.is_empty(),
"templates should be empty when inventory is full"
);
assert_eq!(
partial2["componentStyles"]["resources"],
serde_json::json!({})
);
assert_eq!(
partial2["componentStyles"]["closures"],
serde_json::json!({})
);
}
#[test]
fn component_style_delta_sends_missing_and_reuses_registered_closure_resources() {
let mut protocol = WebUIProtocol::default();
protocol.set_css_strategy(webui_protocol::CssStrategy::Style);
for tag in ["page-b", "shared-card"] {
protocol.fragments.insert(
tag.to_string(),
FragmentList {
fragments: Vec::new(),
contains_boundary: false,
},
);
protocol.components.insert(
tag.to_string(),
webui_protocol::ComponentData {
css: format!(".{tag}{{display:block}}"),
..Default::default()
},
);
}
protocol.style_closures.insert(
"page-b".to_string(),
webui_protocol::ComponentStyleClosure {
component_tags: vec!["page-b".to_string(), "shared-card".to_string()],
style_chunks: Vec::new(),
},
);
protocol.style_closures.insert(
"shared-card".to_string(),
webui_protocol::ComponentStyleClosure {
component_tags: vec!["shared-card".to_string()],
style_chunks: Vec::new(),
},
);
let mut index = ProtocolIndex::new(&protocol);
let first_page =
render_component_templates(&protocol, &["page-b"], "", &mut index).unwrap();
assert!(first_page["componentStyles"]["resources"]["page-b"].is_object());
assert!(
first_page["componentStyles"]["resources"]["shared-card"].is_object(),
"a missing closure dependency must be sent with its new root"
);
let shared =
render_component_templates(&protocol, &["shared-card"], "", &mut index).unwrap();
let shared_inventory = shared["inventory"].as_str().unwrap_or_default();
let page = render_component_templates(&protocol, &["page-b"], shared_inventory, &mut index)
.unwrap();
assert_eq!(
page["componentStyles"]["closures"]["page-b"],
serde_json::json!(["page-b", "shared-card"])
);
assert!(page["componentStyles"]["resources"]["page-b"].is_object());
assert!(
page["componentStyles"]["resources"]
.get("shared-card")
.is_none(),
"a resource already covered by inventory must not be retransmitted"
);
}
#[test]
fn bundled_style_metadata_tracks_chunks_independently_and_covers_members() {
let mut protocol = WebUIProtocol::default();
protocol.set_css_strategy(webui_protocol::CssStrategy::Style);
for tag in ["a-card", "b-card"] {
protocol
.fragments
.insert(tag.to_string(), FragmentList::default());
protocol.components.insert(
tag.to_string(),
webui_protocol::ComponentData {
css: format!(".{tag}{{display:block}}"),
..Default::default()
},
);
protocol.style_closures.insert(
tag.to_string(),
webui_protocol::ComponentStyleClosure {
component_tags: vec![tag.to_string()],
style_chunks: Vec::new(),
},
);
}
protocol
.fragments
.insert("index.html".to_string(), FragmentList::default());
protocol.style_closures.insert(
"index.html".to_string(),
webui_protocol::ComponentStyleClosure {
component_tags: vec!["a-card".to_string(), "b-card".to_string()],
style_chunks: vec![0],
},
);
protocol.style_chunks.push(webui_protocol::StyleChunk {
name: "_chunk-a-card-2".to_string(),
css: ".a-card{display:block}\n.b-card{display:block}".to_string(),
css_href: String::new(),
component_tags: vec!["a-card".to_string(), "b-card".to_string()],
});
let initial =
collect_component_styles(&protocol, ["index.html", "a-card", "b-card"]).unwrap();
assert_eq!(
initial["resources"]["_chunk-a-card-2"]["members"],
serde_json::json!(["a-card", "b-card"])
);
assert_eq!(
initial["closures"],
serde_json::json!({ "index.html": ["_chunk-a-card-2"] }),
"member closures already covered by the requested tree must not duplicate CSS"
);
assert_eq!(
initial["resources"].as_object().map(serde_json::Map::len),
Some(1)
);
let component_index = build_component_index(&protocol);
let mut component_inventory = vec![0u8; component_index.len().div_ceil(8)];
for index in component_index.values() {
set_component(&mut component_inventory, *index);
}
let from_component_inventory = collect_component_styles_for_inventory(
&protocol,
["index.html"],
&component_inventory,
&component_index,
)
.unwrap();
assert!(
from_component_inventory["resources"]["_chunk-a-card-2"].is_object(),
"component-template bits do not prove that the client registered a chunk"
);
let style_index = build_style_resource_index(&protocol);
let mut style_inventory = vec![0u8; style_index.len().div_ceil(8)];
set_component(&mut style_inventory, style_index["_chunk-a-card-2"]);
let from_style_inventory = collect_component_style_delta(
&protocol,
["index.html"],
&style_inventory,
&style_index,
)
.unwrap();
assert_eq!(
from_style_inventory["resources"],
serde_json::json!({}),
"the exact style-resource inventory should suppress a registered chunk"
);
let chunk_index = protocol.style_chunk_index();
let borrowed = collect_borrowed_component_style_delta(
&protocol,
["index.html"],
&style_inventory,
&style_index,
&chunk_index,
)
.unwrap();
assert_eq!(
serde_json::to_string(&borrowed).unwrap(),
serde_json::to_string(&from_style_inventory).unwrap(),
"the borrowed streaming serializer must preserve the canonical style payload bytes"
);
}
#[test]
fn borrowed_streaming_style_payload_matches_owned_for_every_strategy() {
for strategy in [
webui_protocol::CssStrategy::Link,
webui_protocol::CssStrategy::Style,
webui_protocol::CssStrategy::Module,
] {
let mut protocol = WebUIProtocol::default();
protocol.set_css_strategy(strategy);
protocol
.fragments
.insert("index.html".to_string(), FragmentList::default());
protocol.components.insert(
"a-card".to_string(),
webui_protocol::ComponentData {
css: ".a-card{display:block}".to_string(),
css_href: "/a-card.css".to_string(),
..Default::default()
},
);
protocol.style_closures.insert(
"index.html".to_string(),
webui_protocol::ComponentStyleClosure {
component_tags: vec!["a-card".to_string()],
style_chunks: Vec::new(),
},
);
let style_index = build_component_index(&protocol);
let style_inventory = vec![0u8; style_index.len().div_ceil(8)];
let owned = collect_component_style_delta(
&protocol,
["index.html"],
&style_inventory,
&style_index,
)
.unwrap();
let chunk_index = protocol.style_chunk_index();
let borrowed = collect_borrowed_component_style_delta(
&protocol,
["index.html"],
&style_inventory,
&style_index,
&chunk_index,
)
.unwrap();
assert_eq!(
serde_json::to_string(&borrowed).unwrap(),
serde_json::to_string(&owned).unwrap(),
"borrowed payload differs for {strategy:?}"
);
}
}
#[test]
fn member_closures_deliver_the_chunk_that_covers_them() {
let mut protocol = WebUIProtocol::default();
protocol.set_css_strategy(webui_protocol::CssStrategy::Style);
for tag in ["a-card", "b-card"] {
protocol
.fragments
.insert(tag.to_string(), FragmentList::default());
protocol.components.insert(
tag.to_string(),
webui_protocol::ComponentData {
css: format!(".{tag}{{display:block}}"),
..Default::default()
},
);
}
protocol
.fragments
.insert("lazy-panel".to_string(), FragmentList::default());
protocol.style_closures.insert(
"lazy-panel".to_string(),
webui_protocol::ComponentStyleClosure {
component_tags: vec!["a-card".to_string(), "b-card".to_string()],
style_chunks: Vec::new(),
},
);
protocol.style_chunks.push(webui_protocol::StyleChunk {
name: "_chunk-a-card-2".to_string(),
css: ".a-card{display:block}\n.b-card{display:block}".to_string(),
css_href: String::new(),
component_tags: vec!["a-card".to_string(), "b-card".to_string()],
});
let styles = collect_component_styles(&protocol, ["lazy-panel"]).unwrap();
assert_eq!(
styles["closures"],
serde_json::json!({ "lazy-panel": ["_chunk-a-card-2"] }),
"both members resolve to one chunk, named once"
);
assert_eq!(
styles["resources"].as_object().map(serde_json::Map::len),
Some(1),
"the per-member stylesheets the chunk already contains are not re-shipped"
);
assert!(
styles["resources"]["_chunk-a-card-2"].is_object(),
"the closure carries the chunk itself, not a dependency on another closure"
);
}
#[test]
fn member_closures_fall_back_when_no_chunk_covers_them() {
let mut protocol = WebUIProtocol::default();
protocol.set_css_strategy(webui_protocol::CssStrategy::Style);
for tag in ["a-card", "solo-card"] {
protocol
.fragments
.insert(tag.to_string(), FragmentList::default());
protocol.components.insert(
tag.to_string(),
webui_protocol::ComponentData {
css: format!(".{tag}{{display:block}}"),
..Default::default()
},
);
}
protocol
.fragments
.insert("lazy-panel".to_string(), FragmentList::default());
protocol.style_closures.insert(
"lazy-panel".to_string(),
webui_protocol::ComponentStyleClosure {
component_tags: vec!["a-card".to_string(), "solo-card".to_string()],
style_chunks: Vec::new(),
},
);
protocol.style_chunks.push(webui_protocol::StyleChunk {
name: "_chunk-a-card-1".to_string(),
css: ".a-card{display:block}".to_string(),
css_href: String::new(),
component_tags: vec!["a-card".to_string()],
});
let styles = collect_component_styles(&protocol, ["lazy-panel"]).unwrap();
assert_eq!(
styles["closures"],
serde_json::json!({ "lazy-panel": ["_chunk-a-card-1", "solo-card"] }),
"cascade order is preserved across the chunked and unchunked members"
);
assert!(styles["resources"]["solo-card"].is_object());
}
#[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")],
contains_boundary: false,
},
);
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,
keep_alive: false,
..Default::default()
}),
WebUIFragment::component("cart-panel"),
],
contains_boundary: false,
},
);
fragments.insert(
"my-navbar".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<nav/>")],
contains_boundary: false,
},
);
fragments.insert(
"page-about".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<h1>About</h1>")],
contains_boundary: false,
},
);
fragments.insert(
"cart-panel".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<aside>Cart</aside>")],
contains_boundary: false,
},
);
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_json = format!(r#"{{"h":"<div class=\"{name}\"></div>"}}"#);
comp.css = format!(".{name}{{display:block}}");
}
let comp_index = build_component_index(&protocol);
let (needed, _inv) = get_needed_components_for_request(
&protocol,
"index.html",
"/about",
"",
(&comp_index, &CompiledRouteIndex::new(&protocol)),
)
.unwrap();
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(),
keep_alive: false,
cache_tags: Vec::new(),
invalidates: Vec::new(),
pending_component: String::new(),
error_component: String::new(),
};
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(),
keep_alive: false,
cache_tags: Vec::new(),
invalidates: Vec::new(),
pending_component: String::new(),
error_component: 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(),
keep_alive: false,
..Default::default()
}],
keep_alive: false,
..Default::default()
})],
contains_boundary: false,
},
);
fragments.insert(
"app-shell".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<h1>App</h1>"), WebUIFragment::outlet()],
contains_boundary: false,
},
);
fragments.insert(
"compose-page".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<p>Compose</p>")],
contains_boundary: false,
},
);
let mut protocol = WebUIProtocol::new(fragments);
protocol
.components
.entry("app-shell".into())
.or_default()
.css_href = "/app-shell.css".into();
protocol
.components
.entry("compose-page".into())
.or_default()
.template_json = r#"{"h":"<p>Compose</p>"}"#.into();
let chain = collect_route_chain(
&protocol,
"index.html",
"/compose",
&CompiledRouteIndex::new(&protocol),
);
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 scriptless_partial_uses_navigation_keys_without_bootstrap_keys() {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![WebUIFragment::route_from(WebUiFragmentRoute {
path: "/items".into(),
fragment_id: "items-page".into(),
exact: true,
..Default::default()
})],
contains_boundary: false,
},
);
fragments.insert(
"items-page".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<p>Items</p>")],
contains_boundary: false,
},
);
let mut protocol = WebUIProtocol::new(fragments);
protocol.components.insert(
"items-page".to_string(),
webui_protocol::ComponentData {
template_json: r#"{"h":"<p>Items</p>","th":1}"#.into(),
navigation_mode: Some(webui_protocol::StateProjectionMode::Keys as i32),
navigation_keys: vec!["items".into(), "title".into()],
..Default::default()
},
);
let partial = render_partial(
&protocol,
serde_json::json!({
"items": [1, 2],
"serverOnly": "secret",
"title": "Catalog",
}),
"index.html",
"/items",
"",
)
.unwrap();
assert_eq!(
partial["state"],
serde_json::json!({
"items": [1, 2],
"title": "Catalog",
})
);
}
#[test]
fn partial_requires_explicit_navigation_keys() {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![WebUIFragment::component("authored-card")],
contains_boundary: false,
},
);
fragments.insert(
"authored-card".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<p>Card</p>")],
contains_boundary: false,
},
);
let mut protocol = WebUIProtocol::new(fragments);
protocol.components.insert(
"authored-card".to_string(),
webui_protocol::ComponentData {
hydration_mode: webui_protocol::StateProjectionMode::Keys as i32,
hydration_keys: vec!["title".into()],
navigation_mode: Some(webui_protocol::StateProjectionMode::None as i32),
..Default::default()
},
);
let partial = render_partial(
&protocol,
serde_json::json!({
"serverOnly": "secret",
"title": "Authored",
}),
"index.html",
"/",
"",
)
.unwrap();
assert_eq!(partial["state"], serde_json::json!({}));
}
#[test]
fn fully_static_scriptless_partial_emits_empty_state() {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![WebUIFragment::component("static-card")],
contains_boundary: false,
},
);
fragments.insert(
"static-card".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<p>Static</p>")],
contains_boundary: false,
},
);
let mut protocol = WebUIProtocol::new(fragments);
protocol.components.insert(
"static-card".to_string(),
webui_protocol::ComponentData {
template_json: r#"{"h":"<p>Static</p>","th":1}"#.into(),
navigation_mode: Some(webui_protocol::StateProjectionMode::None as i32),
..Default::default()
},
);
let partial = render_partial(
&protocol,
serde_json::json!({ "serverOnly": "secret" }),
"index.html",
"/",
"",
)
.unwrap();
assert_eq!(partial["state"], serde_json::json!({}));
}
#[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>")],
contains_boundary: false,
},
);
let mut protocol = WebUIProtocol::with_tokens(fragments, Vec::new());
protocol.set_css_strategy(webui_protocol::CssStrategy::Module);
let comp = protocol
.components
.entry("settings-dialog".to_string())
.or_default();
comp.template_json = r#"{"h":"<div>Settings</div>"}"#.to_string();
comp.css = ".dialog{position:fixed}".to_string();
protocol.populate_style_closures(&["settings-dialog"]);
let mut index = ProtocolIndex::new(&protocol);
let result =
render_component_templates(&protocol, &["settings-dialog"], "", &mut index).unwrap();
let templates = result["templates"].as_object().expect("templates object");
assert_eq!(templates.len(), 1);
assert_eq!(templates["settings-dialog"]["h"], "<div>Settings</div>");
assert_eq!(
result["componentStyles"]["resources"]["settings-dialog"],
serde_json::json!({
"kind": "module",
"specifier": "settings-dialog",
"css": ".dialog{position:fixed}"
})
);
assert!(
index
.template_metadata_cache
.contains_key("settings-dialog"),
"parsed static template metadata should be cached in ProtocolIndex"
);
}
#[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>")],
contains_boundary: false,
},
);
let mut protocol = WebUIProtocol::with_tokens(fragments, Vec::new());
protocol.set_css_strategy(webui_protocol::CssStrategy::Module);
let comp = protocol
.components
.entry("my-dialog".to_string())
.or_default();
comp.template_json = r#"{"h":"<div>Dialog</div>"}"#.to_string();
comp.css = ".d{color:red}".to_string();
protocol.populate_style_closures(&["my-dialog"]);
let mut index = ProtocolIndex::new(&protocol);
let result1 =
render_component_templates(&protocol, &["my-dialog"], "", &mut index).unwrap();
let inv = result1["inventory"].as_str().expect("inventory string");
assert_eq!(result1["templates"].as_object().unwrap().len(), 1);
let result2 =
render_component_templates(&protocol, &["my-dialog"], inv, &mut index).unwrap();
assert_eq!(result2["templates"].as_object().unwrap().len(), 0);
assert_eq!(
result2["componentStyles"]["closures"],
serde_json::json!({})
);
assert_eq!(
result2["componentStyles"]["resources"],
serde_json::json!({})
);
}
#[test]
fn test_render_component_templates_unknown_component_returns_empty() {
let fragments = HashMap::new();
let protocol = WebUIProtocol::with_tokens(fragments, Vec::new());
let mut index = ProtocolIndex::new(&protocol);
let result =
render_component_templates(&protocol, &["nonexistent-widget"], "", &mut index).unwrap();
assert_eq!(result["templates"].as_object().unwrap().len(), 0);
assert_eq!(
result["componentStyles"]["resources"],
serde_json::json!({})
);
}
#[test]
fn test_render_partial_includes_sibling_boundary_templates() {
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: "inbox".into(),
fragment_id: "inbox-page".into(),
exact: true,
pending_component: "inbox-loading".into(),
..Default::default()
},
WebUiFragmentRoute {
path: "settings".into(),
fragment_id: "settings-page".into(),
exact: true,
pending_component: "settings-loading".into(),
error_component: "settings-error".into(),
..Default::default()
},
],
..Default::default()
})],
contains_boundary: false,
},
);
for name in [
"app-shell",
"inbox-page",
"inbox-loading",
"settings-page",
"settings-loading",
"settings-error",
] {
fragments.insert(
name.to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw(format!("<p>{name}</p>"))],
contains_boundary: false,
},
);
}
let mut protocol = WebUIProtocol::new(fragments);
for name in [
"app-shell",
"inbox-page",
"inbox-loading",
"settings-page",
"settings-loading",
"settings-error",
] {
protocol
.components
.entry(name.to_string())
.or_default()
.template_json = format!(r#"{{"h":"<p>{name}</p>"}}"#);
}
let mut index = ProtocolIndex::new(&protocol);
let partial =
render_partial_metadata(&protocol, "index.html", "/inbox", "", &mut index).unwrap();
let templates = partial["templates"].as_object().unwrap();
assert!(
templates.contains_key("settings-loading"),
"sibling pending template should be preloaded for route transitions: {partial}"
);
assert!(
templates.contains_key("settings-error"),
"sibling error template should be preloaded for failed route transitions: {partial}"
);
let chain = partial["chain"].as_array().unwrap();
assert_eq!(chain.len(), 2);
assert_eq!(chain[0]["component"], "app-shell");
assert_eq!(chain[1]["component"], "inbox-page");
}
#[test]
fn test_resolve_tag_templates_no_placeholders() {
let tags = vec!["inbox".to_string(), "counts".to_string()];
let params = HashMap::new();
let resolved = resolve_tag_templates(&tags, ¶ms);
assert_eq!(resolved, vec!["inbox", "counts"]);
}
#[test]
fn test_resolve_tag_templates_with_params() {
let tags = vec!["thread:{threadId}".to_string(), "inbox".to_string()];
let mut params = HashMap::new();
params.insert("threadId".to_string(), "42".to_string());
let resolved = resolve_tag_templates(&tags, ¶ms);
assert_eq!(resolved, vec!["thread:42", "inbox"]);
}
#[test]
fn test_resolve_tag_templates_multiple_params() {
let tags = vec!["folder:{folderId}:user:{userId}".to_string()];
let mut params = HashMap::new();
params.insert("folderId".to_string(), "drafts".to_string());
params.insert("userId".to_string(), "abc".to_string());
let resolved = resolve_tag_templates(&tags, ¶ms);
assert_eq!(resolved, vec!["folder:drafts:user:abc"]);
}
#[test]
fn test_resolve_tag_templates_missing_param_left_unresolved() {
let tags = vec!["thread:{threadId}".to_string()];
let params = HashMap::new();
let resolved = resolve_tag_templates(&tags, ¶ms);
assert_eq!(resolved, vec!["thread:{threadId}"]);
}
#[test]
fn test_chain_entry_to_json_includes_new_fields() {
let entry = RouteChainEntry {
component: "mail-thread".into(),
path: "email/:threadId".into(),
params: {
let mut p = HashMap::new();
p.insert("threadId".to_string(), "42".to_string());
p
},
exact: true,
allowed_query: String::new(),
keep_alive: false,
cache_tags: vec!["thread:42".to_string()],
invalidates: vec!["inbox".to_string(), "counts".to_string()],
pending_component: "mail-skeleton".into(),
error_component: "error-page".into(),
};
let json = entry.to_json();
assert_eq!(json["pendingComponent"], "mail-skeleton");
assert_eq!(json["errorComponent"], "error-page");
let inv = json["invalidates"].as_array().unwrap();
assert_eq!(inv.len(), 2);
assert_eq!(inv[0], "inbox");
assert_eq!(inv[1], "counts");
}
#[test]
fn test_chain_entry_to_json_omits_empty_new_fields() {
let entry = RouteChainEntry {
component: "home-page".into(),
path: "/".into(),
params: HashMap::new(),
exact: true,
allowed_query: String::new(),
keep_alive: false,
cache_tags: Vec::new(),
invalidates: Vec::new(),
pending_component: String::new(),
error_component: String::new(),
};
let json = entry.to_json();
assert!(json.get("pendingComponent").is_none());
assert!(json.get("errorComponent").is_none());
assert!(json.get("invalidates").is_none());
}
#[test]
fn test_render_partial_includes_resolved_cache_tags() {
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: "email/:threadId".into(),
fragment_id: "mail-thread".into(),
exact: true,
cache_tags: vec!["thread:{threadId}".to_string(), "inbox".to_string()],
..Default::default()
}],
cache_tags: vec!["folders".to_string()],
..Default::default()
})],
contains_boundary: false,
},
);
fragments.insert(
"app-shell".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<h1>App</h1>"), WebUIFragment::outlet()],
contains_boundary: false,
},
);
fragments.insert(
"mail-thread".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<p>Thread</p>")],
contains_boundary: false,
},
);
let protocol = WebUIProtocol::new(fragments);
let mut index = ProtocolIndex::new(&protocol);
let partial =
render_partial_metadata(&protocol, "index.html", "/email/42", "", &mut index).unwrap();
let tags = partial["cacheTags"].as_array().unwrap();
let tag_strings: Vec<&str> = tags.iter().map(|v| v.as_str().unwrap()).collect();
assert!(
tag_strings.contains(&"folders"),
"should contain parent tags"
);
assert!(
tag_strings.contains(&"thread:42"),
"should resolve threadId param to 42"
);
assert!(tag_strings.contains(&"inbox"), "should contain static tags");
}
#[test]
fn test_render_action_response_returns_resolved_invalidation_tags() {
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,
invalidates: vec![
"inbox".to_string(),
"sent".to_string(),
"counts".to_string(),
],
..Default::default()
}],
..Default::default()
})],
contains_boundary: false,
},
);
fragments.insert(
"app-shell".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<h1>App</h1>"), WebUIFragment::outlet()],
contains_boundary: false,
},
);
fragments.insert(
"compose-page".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<p>Compose</p>")],
contains_boundary: false,
},
);
let protocol = WebUIProtocol::new(fragments);
let mut index = ProtocolIndex::new(&protocol);
let result = render_action_response(
&protocol,
serde_json::json!({"ok": true}),
"index.html",
"/compose",
&mut index,
);
let tags = result["invalidateTags"].as_array().unwrap();
let tag_strings: Vec<&str> = tags.iter().map(|v| v.as_str().unwrap()).collect();
assert_eq!(tag_strings, vec!["inbox", "sent", "counts"]);
assert_eq!(result["state"]["ok"], true);
}
#[test]
fn test_render_action_response_resolves_param_placeholders() {
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: "email/:threadId/reply".into(),
fragment_id: "reply-page".into(),
exact: true,
invalidates: vec!["thread:{threadId}".to_string(), "inbox".to_string()],
..Default::default()
}],
..Default::default()
})],
contains_boundary: false,
},
);
fragments.insert(
"app-shell".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<h1>App</h1>"), WebUIFragment::outlet()],
contains_boundary: false,
},
);
fragments.insert(
"reply-page".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<p>Reply</p>")],
contains_boundary: false,
},
);
let protocol = WebUIProtocol::new(fragments);
let mut index = ProtocolIndex::new(&protocol);
let result = render_action_response(
&protocol,
serde_json::json!({}),
"index.html",
"/email/42/reply",
&mut index,
);
let tags = result["invalidateTags"].as_array().unwrap();
let tag_strings: Vec<&str> = tags.iter().map(|v| v.as_str().unwrap()).collect();
assert!(tag_strings.contains(&"thread:42"));
assert!(tag_strings.contains(&"inbox"));
}
}