use crate::compat::{format, String, Vec};
use crate::json::project::ProjectNode;
use crate::json::JsonProject;
use crate::widget::capability::{WidgetFactory, WIRE_RULES};
use serde_json::Value;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TargetProfile {
Default,
Stripped,
}
impl TargetProfile {
pub fn feature_hint(self) -> &'static str {
match self {
Self::Default => "desktop",
Self::Stripped => "mini",
}
}
pub fn child_capacity(self) -> usize {
match self {
Self::Default => DEFAULT_CHILD_CAPACITY,
Self::Stripped => MINI_CHILD_CAPACITY,
}
}
}
pub const MINI_CHILD_CAPACITY: usize = 64;
pub const DEFAULT_CHILD_CAPACITY: usize = 4096;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Availability {
Local,
Unavailable,
Unknown,
CrossProfileCaveat,
}
impl Availability {
pub fn permits_generation(&self) -> bool {
matches!(self, Self::Local | Self::CrossProfileCaveat)
}
pub fn needs_attention(&self) -> bool {
!matches!(self, Self::Local)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GenerationGap {
pub path: Vec<usize>,
pub widget: String,
pub reason: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct GenerationReport {
pub nodes_emitted: usize,
pub properties_emitted: usize,
pub resolved_at_generation: Vec<String>,
pub unsupported: Vec<GenerationGap>,
pub capacity_overflow: Vec<GenerationGap>,
pub cross_profile_caveats: Vec<GenerationGap>,
}
impl GenerationReport {
pub fn is_clean(&self) -> bool {
self.unsupported.is_empty()
&& self.capacity_overflow.is_empty()
&& self.cross_profile_caveats.is_empty()
}
pub fn summary(&self) -> String {
format!(
"{} nodes, {} properties, {} resolved at generation time, {} unsupported, \
{} over capacity, {} cross-profile caveats",
self.nodes_emitted,
self.properties_emitted,
self.resolved_at_generation.len(),
self.unsupported.len(),
self.capacity_overflow.len(),
self.cross_profile_caveats.len()
)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GeneratedSource {
pub source: String,
pub report: GenerationReport,
}
pub fn is_style_only_property(name: &str) -> bool {
matches!(
name,
"css_class"
| "selector"
| "transition"
| "animation"
| "hover_style"
| "active_style"
| "focus_style"
)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GenerationRequest {
pub json: String,
pub target: TargetProfile,
pub width: u32,
pub height: u32,
pub function_name: String,
}
pub fn generate(request: &GenerationRequest) -> Result<GeneratedSource, String> {
let project = JsonProject::parse(&request.json)?;
let factory = WidgetFactory::new_with_defaults();
let mut report = GenerationReport::default();
let availability = availability(&project.root_widget, request.target);
match availability {
Availability::Unavailable | Availability::Unknown => {
report.unsupported.push(GenerationGap {
path: Vec::new(),
widget: project.root_widget.clone(),
reason: availability_reason(availability),
});
return Ok(GeneratedSource { source: empty_source(request), report });
}
Availability::CrossProfileCaveat => {
report.cross_profile_caveats.push(GenerationGap {
path: Vec::new(),
widget: project.root_widget.clone(),
reason: availability_reason(availability),
});
}
Availability::Local => {}
}
check_capacity(&project, request.target, &mut report);
let geometry = plan_geometry(request, &project, &mut report);
let body = match request.target {
TargetProfile::Default => {
emit_default_mode(&project, request, &geometry, &factory, &mut report)
}
TargetProfile::Stripped => {
emit_stripped_mode(&project, request, &geometry, &factory, &mut report)
}
};
let source = assemble(request, &project, &body, &report);
Ok(GeneratedSource { source, report })
}
pub fn availability(name: &str, target: TargetProfile) -> Availability {
let factory = WidgetFactory::new_with_defaults();
if factory.create(name, crate::core::Rect::new(0, 0, 1, 1), "").is_some() {
if host_profile_matches(target) {
Availability::Local
} else {
Availability::CrossProfileCaveat
}
} else if factory.capability(name).is_some() {
Availability::Unavailable
} else {
Availability::Unknown
}
}
fn host_profile_matches(target: TargetProfile) -> bool {
let host_is_stripped = cfg!(all(not(full_widgets), not(widgets_unstripped)));
host_is_stripped == (target == TargetProfile::Stripped)
}
fn availability_reason(availability: Availability) -> String {
match availability {
Availability::Unavailable => String::from(
"the running profile cannot construct this control, so it is gated out of the target \
too (this is how `mini` gates most `create_*` functions)",
),
Availability::Unknown => String::from(
"no capability and no constructor is registered for this name: it is a typo or a \
control this version does not ship",
),
Availability::CrossProfileCaveat => String::from(
"resolved under the running profile, which differs from the target; regenerate under \
the target's features to make this conclusive",
),
Availability::Local => String::new(),
}
}
fn empty_source(request: &GenerationRequest) -> String {
format!(
"// generated for `{}` — nothing was emitted; see the generation report.\n\
pub fn {}() {{}}\n",
request.target.feature_hint(),
request.function_name
)
}
fn check_capacity(project: &JsonProject, target: TargetProfile, report: &mut GenerationReport) {
let capacity = target.child_capacity();
for node in project.walk() {
if node.children.len() <= capacity {
continue;
}
report.capacity_overflow.push(GenerationGap {
path: node.path.clone(),
widget: node.widget.clone(),
reason: format!(
"{} children exceeds the target's capacity of {capacity}; the fixed-capacity storage \
drops the excess silently, so this must be split rather than generated",
node.children.len()
),
});
}
}
fn emit_default_mode(
project: &JsonProject,
request: &GenerationRequest,
geometry: &GeometryPlan,
factory: &WidgetFactory,
report: &mut GenerationReport,
) -> String {
let mut node_code = String::new();
emit_node(project, request, &[], 1, geometry, factory, &mut node_code, report);
let mut body = String::new();
body.push_str(" let tree = ");
body.push_str(&node_code);
body.push_str(";\n\n");
body.push_str(
" // The `View` value is the generated tree, and `create_for` is generated too, so this\n\
\x20 // program links no JSON parser and no widget-name table (mode 1's weight).\n\
\x20 let mut engine = rust_widgets::view::ViewEngine::new();\n\
\x20 let report = engine.mount(&GeneratedTree { tree }, &|node| {\n\
\x20 let geometry = rust_widgets::core::Rect::new(0, 0, 0, 0);\n\
\x20 let text = node\n\
\x20 .prop_value(\"text\")\n\
\x20 .or_else(|| node.prop_value(\"title\"))\n\
\x20 .and_then(|v| v.as_str())\n\
\x20 .unwrap_or(\"\");\n\
\x20 create_for(&node.widget, geometry, text)\n\
\x20 });\n\
\x20 let _ = report;\n",
);
body
}
#[allow(clippy::too_many_arguments)]
fn emit_node(
project: &JsonProject,
request: &GenerationRequest,
path: &[usize],
depth: usize,
geometry: &GeometryPlan,
factory: &WidgetFactory,
out: &mut String,
report: &mut GenerationReport,
) {
let Some(node) = project.node(path) else {
report.unsupported.push(GenerationGap {
path: path.to_vec(),
widget: String::from("?"),
reason: String::from(
"the parsed project has no node at this path; the parser and the generator \
disagree about the document's shape",
),
});
out.push_str("Node::new(\"__missing__\")");
return;
};
let availability = availability(&node.widget, request.target);
if !availability.permits_generation() {
report.unsupported.push(GenerationGap {
path: path.to_vec(),
widget: node.widget.clone(),
reason: availability_reason(availability),
});
out.push_str("Node::new(\"__unsupported__\")");
return;
}
if availability == Availability::CrossProfileCaveat {
report.cross_profile_caveats.push(GenerationGap {
path: path.to_vec(),
widget: node.widget.clone(),
reason: availability_reason(availability),
});
}
report.nodes_emitted += 1;
let mut line = format!("Node::new({})", quote(&node.widget));
line.push_str(&format!(".key({})", quote(&node.key)));
if depth == 1 {
line.push_str(&format!(
".prop(\"width\", CapabilityValue::UInt({})).prop(\"height\", CapabilityValue::UInt({}))",
geometry.root.2, geometry.root.3
));
report.properties_emitted += 2;
}
let _ = depth;
for (name, value) in node.scalar_properties() {
if is_style_only_property(&name) {
report.resolved_at_generation.push(format!("`{name}` on {:?}", path));
continue;
}
if is_wire_key(&name) {
continue;
}
line.push_str(&format!(".prop({}, {})", quote(&name), capability_value_expr(&value)));
report.properties_emitted += 1;
}
let _ = factory;
out.push_str(&line);
let next_depth = depth + 1;
for (child_index, child) in node.children.iter().enumerate() {
let Some(child_path) = project_child_path(project, node, child_index) else {
report.unsupported.push(GenerationGap {
path: path.to_vec(),
widget: node.widget.clone(),
reason: format!(
"child entry {child} does not resolve to a node, so this subtree is incomplete"
),
});
continue;
};
out.push_str(".child(");
let mut child_code = String::new();
emit_node(
project,
request,
&child_path,
next_depth,
geometry,
factory,
&mut child_code,
report,
);
out.push_str(&child_code);
out.push(')');
}
}
fn emit_stripped_mode(
project: &JsonProject,
request: &GenerationRequest,
geometry: &GeometryPlan,
factory: &WidgetFactory,
report: &mut GenerationReport,
) -> String {
let mut nodes: Vec<(Vec<usize>, String, bool)> = Vec::new();
collect_stripped_nodes(project, request, &[], 1, geometry, factory, &mut nodes, report);
if nodes.is_empty() {
return String::from(" // nothing could be emitted; see the generation report\n");
}
let mut body = String::new();
let (root_path, root_expr, _) = &nodes[0];
debug_assert!(root_path.is_empty(), "the root must be collected first");
body.push_str(&format!(" let mut root = {root_expr};\n"));
for (path, expr, _setters) in nodes.iter().skip(1) {
body.push_str(&format!(" let {} = {expr};\n", binding_name(path)));
}
body.push('\n');
for (path, _expr, _setters) in nodes.iter().skip(1) {
let parent = if path.len() == 1 {
String::from("root")
} else {
binding_name(&path[..path.len() - 1])
};
let child = binding_name(path);
body.push_str(&format!(
" // A refused add must be *observable*: `try_add_child` records it in\n\
\x20 // `child_overflow_count`, and the assertion below turns a silent drop into a loud\n\
\x20 // failure. On `alloc_frugal` storage the extra child would otherwise vanish.\n\
\x20 let before = {parent}.base().child_overflow_count();\n\
\x20 {parent}.base_mut().try_add_child({child}.base().id());\n\
\x20 debug_assert_eq!(\n\
\x20 {parent}.base().child_overflow_count(),\n\
\x20 before,\n\
\x20 \"child capacity exceeded while adding a child; split this container\"\n\
\x20 );\n"
));
}
body.push_str("\n let _ = &root;\n");
body
}
#[allow(clippy::too_many_arguments)]
fn collect_stripped_nodes(
project: &JsonProject,
request: &GenerationRequest,
path: &[usize],
depth: usize,
geometry: &GeometryPlan,
factory: &WidgetFactory,
out: &mut Vec<(Vec<usize>, String, bool)>,
report: &mut GenerationReport,
) {
let Some(node) = project.node(path) else {
return;
};
let availability = availability(&node.widget, request.target);
if !availability.permits_generation() {
report.unsupported.push(GenerationGap {
path: path.to_vec(),
widget: node.widget.clone(),
reason: availability_reason(availability),
});
for descendant in project.descendants(path) {
report.unsupported.push(GenerationGap {
path: descendant,
widget: String::from("(under an unsupported parent)"),
reason: String::from(
"this node's parent could not be generated, so it has no control to be added to",
),
});
}
return;
}
report.nodes_emitted += 1;
let rect = geometry.rect_for(path, depth);
let type_name = constructor_path(&node.widget);
let mut expr = if let Some(text) = node.text().filter(|_| constructor_takes_text(&node.widget))
{
format!(
"{type_name}::new(String::from({}), Rect::new({}, {}, {}, {}))",
quote(&text),
rect.0,
rect.1,
rect.2,
rect.3
)
} else {
format!("{type_name}::new(Rect::new({}, {}, {}, {}))", rect.0, rect.1, rect.2, rect.3)
};
let binding = binding_name(path);
let mut setters = String::new();
for (name, value) in node.scalar_properties() {
if is_style_only_property(&name) {
report.resolved_at_generation.push(format!("`{name}` on {path:?}"));
continue;
}
if is_wire_key(&name) {
continue;
}
if let Some(call) = setter_call(&binding, &name, &value) {
setters.push_str(&format!("\n // {name}\n {call}"));
report.properties_emitted += 1;
}
}
if !setters.is_empty() {
expr =
format!("{{\n let mut {binding} = {expr};{setters}\n {binding}\n }}");
}
let _ = factory;
out.push((path.to_vec(), expr, !setters.is_empty()));
let next_depth = depth + 1;
for (child_index, child) in node.children.iter().enumerate() {
let Some(child_path) = project_child_path(project, node, child_index) else {
report.unsupported.push(GenerationGap {
path: path.to_vec(),
widget: node.widget.clone(),
reason: format!(
"child entry {child} does not resolve to a node, so this subtree is incomplete"
),
});
continue;
};
collect_stripped_nodes(
project,
request,
&child_path,
next_depth,
geometry,
factory,
out,
report,
);
}
}
fn placeholder_id(node: &ProjectNode) -> crate::core::ObjectId {
let mut id: crate::core::ObjectId = 1;
for index in &node.path {
id = id.wrapping_mul(31).wrapping_add(*index as crate::core::ObjectId + 1);
}
id
}
fn child_stretch(node: &ProjectNode) -> u32 {
node.property("stretch").and_then(|v| v.as_u64()).unwrap_or(1) as u32
}
pub fn constructor_type_name(widget: &str) -> &'static str {
match constructor_path(widget).rsplit("::").next() {
Some(name) => name,
None => "UnsupportedControl",
}
}
fn constructor_takes_text(widget: &str) -> bool {
matches!(
widget,
"window"
| "button"
| "label"
| "checkbox"
| "check_box"
| "radiobutton"
| "radio_button"
| "groupbox"
| "group_box"
| "lineedit"
| "line_edit"
| "textedit"
| "text_edit"
)
}
fn constructor_path(widget: &str) -> &'static str {
match widget {
"window" => "rust_widgets::widget::Window",
"button" => "rust_widgets::widget::Button",
"label" => "rust_widgets::widget::Label",
"checkbox" | "check_box" => "rust_widgets::widget::CheckBox",
"radiobutton" | "radio_button" => "rust_widgets::widget::RadioButton",
"slider" => "rust_widgets::widget::Slider",
"progressbar" | "progress_bar" => "rust_widgets::widget::ProgressBar",
"lineedit" | "line_edit" => "rust_widgets::widget::LineEdit",
"textedit" | "text_edit" => "rust_widgets::widget::TextEdit",
"combobox" | "combo_box" => "rust_widgets::widget::ComboBox",
"spinbox" | "spin_box" => "rust_widgets::widget::SpinBox",
"listbox" | "list_box" => "rust_widgets::widget::ListBox",
"groupbox" | "group_box" => "rust_widgets::widget::GroupBox",
"frame" => "rust_widgets::widget::Frame",
"arc" => "rust_widgets::widget::Arc",
"meter" => "rust_widgets::widget::Meter",
"stackedwidget" | "stacked_widget" => "rust_widgets::widget::StackedWidget",
"splitter" => "rust_widgets::widget::Splitter",
"scrollarea" | "scroll_area" => "rust_widgets::widget::ScrollArea",
"tabwidget" | "tab_widget" => "rust_widgets::widget::TabWidget",
_ => "rust_widgets::designer::UnsupportedControl",
}
}
fn setter_call(binding: &str, name: &str, value: &Value) -> Option<String> {
if let Some(flag) = value.as_bool() {
return match name {
"visible" => Some(format!("{binding}.set_visible({flag});")),
"enabled" => Some(format!("{binding}.set_enabled({flag});")),
"checked" => Some(format!("{binding}.set_checked({flag});")),
_ => None,
};
}
if let Some(number) = value.as_i64() {
return match name {
"value" => Some(format!("{binding}.set_value({number});")),
"minimum" | "min" => Some(format!("{binding}.set_minimum({number});")),
"maximum" | "max" => Some(format!("{binding}.set_maximum({number});")),
_ => None,
};
}
if let Some(text) = value.as_str() {
return match name {
"text" | "title" => None,
_ => {
let _ = text;
None
}
};
}
None
}
struct GeometryPlan {
rects: Vec<(Vec<usize>, (i32, i32, u32, u32))>,
root: (i32, i32, u32, u32),
}
impl GeometryPlan {
fn rect_for(&self, path: &[usize], depth: usize) -> (i32, i32, u32, u32) {
if let Some((_, rect)) = self.rects.iter().find(|(p, _)| p.as_slice() == path) {
return *rect;
}
if depth == 1 {
return self.root;
}
(0, 0, self.root.2.max(1), self.root.3.max(1))
}
}
fn plan_geometry(
request: &GenerationRequest,
project: &JsonProject,
report: &mut GenerationReport,
) -> GeometryPlan {
let root = (0i32, 0i32, request.width, request.height);
let mut rects: Vec<(Vec<usize>, (i32, i32, u32, u32))> = vec![(Vec::new(), root)];
let Some((kind, container_children)) = project.layout_declaration() else {
for node in project.walk() {
if !node.path.is_empty() {
rects.push((node.path.clone(), root));
}
}
return GeometryPlan { rects, root };
};
let mut engine = crate::json::create_layout_from_kind(&kind);
let mut solved: Vec<crate::core::Rect> = Vec::new();
let container_rect = crate::core::Rect::new(root.0, root.1, root.2, root.3);
for child in &container_children {
if let Some(node) = project.node(child) {
let placeholder = placeholder_id(node);
engine.add_widget(placeholder, child_stretch(node));
}
}
engine.update(container_rect, &mut |_id, rect| {
solved.push(rect);
});
report.resolved_at_generation.push(format!(
"the document's layout was solved into constant coordinates against {}x{}",
request.width, request.height
));
for (index, node) in container_children.iter().enumerate() {
match solved.get(index) {
Some(rect) => rects.push((node.clone(), (rect.x, rect.y, rect.width, rect.height))),
None => report.unsupported.push(GenerationGap {
path: node.clone(),
widget: String::from("?"),
reason: format!(
"the layout engine reported no rect for this node ({} placements for {} \
children), so its coordinates are not known",
solved.len(),
container_children.len()
),
}),
}
}
GeometryPlan { rects, root }
}
fn binding_name(path: &[usize]) -> String {
if path.is_empty() {
return String::from("root");
}
let mut name = String::from("n");
for index in path {
name.push('_');
name.push_str(&format!("{index}"));
}
name
}
fn quote(value: &str) -> String {
let mut out = String::with_capacity(value.len() + 2);
out.push('"');
for ch in value.chars() {
match ch {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
_ => out.push(ch),
}
}
out.push('"');
out
}
fn capability_value_expr(value: &Value) -> String {
if let Some(v) = value.as_bool() {
return format!("CapabilityValue::Bool({v})");
}
if let Some(v) = value.as_i64() {
return format!("CapabilityValue::Int({v})");
}
if let Some(v) = value.as_u64() {
return format!("CapabilityValue::UInt({v})");
}
if let Some(v) = value.as_f64() {
return format!("CapabilityValue::Float({v})");
}
if let Some(s) = value.as_str() {
return format!("CapabilityValue::String(String::from({}))", quote(s));
}
String::from("CapabilityValue::Null")
}
fn project_child_path(
project: &JsonProject,
node: &ProjectNode,
child_index: usize,
) -> Option<Vec<usize>> {
project.child_of(node, child_index).map(|child| child.path.clone())
}
pub fn is_wire_key(name: &str) -> bool {
crate::json::is_marker_key(name) || name == crate::json::EVENTS_KEY
}
pub fn shared_wire_rule_count() -> usize {
WIRE_RULES.len()
}
pub fn wire_verdict_for(
source: Option<crate::widget::capability::PropertyValueKind>,
target: crate::widget::capability::WireTarget,
) -> crate::widget::capability::WireCompatibility {
crate::widget::capability::compatibility(source, target)
}
fn assemble(
request: &GenerationRequest,
project: &JsonProject,
body: &str,
report: &GenerationReport,
) -> String {
let mut source = String::new();
source.push_str(&format!(
"// Generated by the rust_widgets designer. Do not edit by hand.\n\
//\n\
// target profile : {} (build with `cargo build --no-default-features --features {}`)\n\
// root control : {}\n\
// nodes : {}\n\
// properties : {}\n",
request.target.feature_hint(),
request.target.feature_hint(),
project.root_widget,
report.nodes_emitted,
report.properties_emitted
));
if !report.resolved_at_generation.is_empty() {
source.push_str("//\n// resolved at generation time (not calls in this file):\n");
for item in &report.resolved_at_generation {
source.push_str(&format!("// - {item}\n"));
}
}
if !report.unsupported.is_empty() {
source.push_str("//\n// REFUSED — present in the document, absent from this file:\n");
for gap in &report.unsupported {
source.push_str(&format!(
"// - path {:?} `{}`: {}\n",
gap.path, gap.widget, gap.reason
));
}
}
if !report.capacity_overflow.is_empty() {
source.push_str("//\n// OVER CAPACITY — the target would drop these children silently:\n");
for gap in &report.capacity_overflow {
source.push_str(&format!(
"// - path {:?} `{}`: {}\n",
gap.path, gap.widget, gap.reason
));
}
}
source.push_str("\nuse rust_widgets::core::Rect;\n");
match request.target {
TargetProfile::Default => {
source.push_str("use rust_widgets::view::Node;\n");
source.push_str("use rust_widgets::widget::capability::CapabilityValue;\n");
}
TargetProfile::Stripped => {
source.push_str("use rust_widgets::widget::Widget;\n");
}
}
source.push('\n');
source.push_str(&format!("pub fn {}() {{\n", request.function_name));
source.push_str(body);
source.push_str("}\n");
if request.target == TargetProfile::Default {
source.push_str(
"\n/// The generated tree, as a `View`.\n\
struct GeneratedTree {\n\
\x20 tree: Node,\n\
}\n\n\
impl rust_widgets::view::View for GeneratedTree {\n\
\x20 fn build(&self) -> Node {\n\
\x20 self.tree.clone()\n\
\x20 }\n\
}\n",
);
source.push_str(
"\n/// Builds one control for the generated tree.\n\
///\n\
/// The arms are exactly the widget types this file uses, so no name table is linked.\n\
fn create_for(widget: &str, geometry: Rect, text: &str) -> Option<rust_widgets::core::ObjectId> {\n\
\x20 let mut control: Option<Box<dyn rust_widgets::widget::Widget>> = match widget {\n",
);
let mut names: Vec<String> = Vec::new();
for node in project.walk() {
if !names.contains(&node.widget) {
names.push(node.widget.clone());
}
}
names.sort();
for name in &names {
let path = constructor_path(name);
if path.ends_with("UnsupportedControl") {
continue;
}
if constructor_takes_text(name) {
source.push_str(&format!(
"\x20 \"{name}\" => Some(Box::new({path}::new(text.to_string(), geometry))),\n"
));
} else {
source.push_str(&format!(
"\x20 \"{name}\" => Some(Box::new({path}::new(geometry))),\n"
));
}
}
source.push_str(
"\x20 _ => None,\n\
\x20 };\n\
\x20 // The registry assigns the id; a generated program has exactly one tree, so the\n\
\x20 // returned id is the one `ViewEngine` will address it by.\n\
\x20 control.take().map(|c| rust_widgets::widget::runtime::register(c)).flatten()\n\
}\n",
);
}
source
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn style_only_names_are_the_css_concepts_with_no_storage() {
assert!(is_style_only_property("css_class"));
assert!(is_style_only_property("transition"));
assert!(!is_style_only_property("width"));
assert!(!is_style_only_property("text"));
}
#[test]
fn wire_keys_are_not_properties() {
assert!(is_wire_key("events"));
assert!(is_wire_key("on_click"));
assert!(!is_wire_key("text"));
}
#[test]
fn the_generator_shares_the_runtime_wire_table() {
assert!(
shared_wire_rule_count() > 0,
"the generator must consult the same rule table the runtime uses (T-3)"
);
}
#[test]
fn a_wire_verdict_comes_from_the_shared_table() {
use crate::widget::capability::types::PropertyValueKind;
use crate::widget::capability::{WireCompatibility, WireTarget as Target};
assert!(matches!(
wire_verdict_for(
Some(PropertyValueKind::String),
Target::Property(PropertyValueKind::Int)
),
WireCompatibility::Rejected(_)
));
assert_eq!(
wire_verdict_for(
Some(PropertyValueKind::Bool),
Target::Property(PropertyValueKind::Bool)
),
WireCompatibility::Direct
);
}
#[test]
fn the_two_targets_report_different_capacities() {
assert_eq!(TargetProfile::Stripped.child_capacity(), MINI_CHILD_CAPACITY);
assert!(TargetProfile::Default.child_capacity() > MINI_CHILD_CAPACITY);
}
#[test]
fn availability_distinguishes_unknown_from_unavailable() {
assert!(matches!(
availability("definitely_not_a_control", TargetProfile::Default),
Availability::Unknown
));
}
#[test]
fn only_permitting_answers_allow_generation() {
assert!(Availability::Local.permits_generation());
assert!(Availability::CrossProfileCaveat.permits_generation());
assert!(!Availability::Unavailable.permits_generation());
assert!(!Availability::Unknown.permits_generation());
assert!(Availability::CrossProfileCaveat.needs_attention());
assert!(!Availability::Local.needs_attention());
}
#[test]
fn a_report_is_clean_only_when_nothing_needs_attention() {
let mut report = GenerationReport::default();
assert!(report.is_clean());
report.unsupported.push(GenerationGap {
path: Vec::new(),
widget: String::from("x"),
reason: String::from("nope"),
});
assert!(!report.is_clean());
}
#[test]
fn quoting_escapes_what_would_break_the_literal() {
assert_eq!(quote("plain"), "\"plain\"");
assert_eq!(quote("a\"b"), "\"a\\\"b\"");
assert_eq!(quote("a\\b"), "\"a\\\\b\"");
assert_eq!(quote("a\nb"), "\"a\\nb\"");
}
#[test]
fn scalar_values_become_capability_values() {
assert_eq!(capability_value_expr(&Value::Bool(true)), "CapabilityValue::Bool(true)");
assert_eq!(
capability_value_expr(&Value::String(String::from("hi"))),
"CapabilityValue::String(String::from(\"hi\"))"
);
assert_eq!(capability_value_expr(&Value::Null), "CapabilityValue::Null");
}
#[test]
fn a_non_scalar_does_not_become_a_misleading_string() {
assert_eq!(capability_value_expr(&Value::Array(Vec::new())), "CapabilityValue::Null");
}
#[test]
fn binding_names_are_derived_from_the_path() {
assert_eq!(binding_name(&[]), "root");
assert_eq!(binding_name(&[3]), "n_3");
assert_eq!(binding_name(&[1, 2]), "n_1_2");
}
}