zuzu-rust 0.3.0

Rust implementation of ZuzuScript
Documentation
from test/more import *;

requires_capability( "gui" );
requires_capability( "fs" );

from std/eval import eval;
from std/gui import *;
from std/io import Path;

let xml := "<Window xmlns=\"https://zuzulang.org/ns/std/gui\""
	_ " title=\"Phase 4\">"
	_ "<Menu id=\"file-menu\" text=\"File\">"
	_ "<MenuItem id=\"save-item\" text=\"Save\" disabled=\"true\" />"
	_ "</Menu>"
	_ "<VBox id=\"root\" gap=\"6\" padding=\"2\">"
	_ "<Input id=\"name\" value=\"Ada\" required=\"yes\""
	_ " meta.model=\"person.name\" />"
	_ "<Slider id=\"volume\" value=\"25\" min=\"0\" max=\"100\""
	_ " step=\"5\" />"
	_ "<Progress id=\"progress\" value=\"30\" max=\"200\""
	_ " show_text=\"on\" />"
	_ "<DatePicker id=\"date\" value=\"2026-04-26\""
	_ " min=\"2020-01-01\" max=\"2030-12-31\""
	_ " first_day_of_week=\"1\" />"
	_ "<Tabs id=\"tabs\" selected=\"details\">"
	_ "<Tab id=\"summary\" title=\"Summary\" value=\"summary\" />"
	_ "<Tab id=\"details\" title=\"Details\" value=\"details\""
	_ " icon=\"details.png\" closable=\"true\" />"
	_ "</Tabs>"
	_ "<TreeView id=\"tree\" multiple=\"true\" selected_path=\"0,1\" />"
	_ "</VBox>"
	_ "</Window>";

let ui := gui_from_xml(xml);
let styled := gui_from_xml(
	"<Window><Button id=\"styled\" text=\"Styled\""
		_ " style.color=\"red\" /></Window>",
);
is( ui.title(), "Phase 4", "gui_from_xml builds Window" );
is( ui.menus().length(), 1, "XML Window preserves Menu child" );
is( ui.menus()[0].text(), "File", "XML Menu text parsed" );
is(
	ui.find_by_id("save-item").disabled(),
	true,
	"XML MenuItem disabled coerces bool",
);
is( ui.find_by_id("root").gap(), 6, "XML layout number coerces" );
is( ui.find_by_id("name").required(), true, "XML bool yes coerces" );
is(
	ui.find_by_id("name").meta("model"),
	"person.name",
	"XML meta.* maps to widget meta",
);
is(
	styled.find_by_id("styled").style("color"),
	"red",
	"XML style.* maps to widget style",
);
is( ui.find_by_id("volume").value(), 25, "XML Slider value coerces" );
is( ui.find_by_id("volume").step(), 5, "XML Slider step coerces" );
is(
	ui.find_by_id("progress").show_text(),
	true,
	"XML Progress show_text coerces bool",
);
is(
	ui.find_by_id("date").first_day_of_week(),
	1,
	"XML DatePicker first_day_of_week coerces",
);
is(
	ui.find_by_id("date").min(),
	"2020-01-01",
	"DatePicker min stays string",
);
is(
	ui.find_by_id("tabs").selected(),
	"details",
	"XML Tabs selected parsed",
);
is(
	ui.find_by_id("details").icon(),
	"details.png",
	"XML Tab icon parsed",
);
is( ui.find_by_id("details").closable(), true, "XML Tab closable parsed" );
is( ui.find_by_id("tree").multiple(), true, "XML TreeView multiple parsed" );
is(
	ui.find_by_id("tree").selected_path()[1],
	1,
	"XML TreeView selected_path coerces to int list",
);

let out := gui_to_xml(ui);
ok(
	out ~ /xmlns=\"https:\/\/zuzulang\.org\/ns\/std\/gui\"/,
	"gui_to_xml writes namespace",
);
ok( out ~ /meta\.model=\"person\.name\"/, "gui_to_xml writes meta attrs" );
ok(
	gui_to_xml(styled) ~ /style\.color=\"red\"/,
	"gui_to_xml writes style attrs",
);
ok( out ~ /show_text=\"true\"/, "gui_to_xml writes coerced bool attrs" );
ok( out ~ /selected_path=\"0,1\"/, "gui_to_xml writes selected paths" );

let roundtrip := gui_from_xml(out);
is(
	roundtrip.find_by_id("name").meta("model"),
	"person.name",
	"gui_to_xml output parses again",
);
is(
	roundtrip.find_by_id("details").icon(),
	"details.png",
	"round-trip preserves Tab icon",
);
is(
	roundtrip.find_by_id("tree").selected_path()[1],
	1,
	"round-trip preserves selected_path",
);

let prefixed := "<gui:Window"
	_ " xmlns:gui=\"https://zuzulang.org/ns/std/gui\""
	_ " title=\"Prefixed\"><gui:Button id=\"ok\" text=\"OK\" />"
	_ "</gui:Window>";
is(
	gui_from_xml(prefixed).find_by_id("ok").text(),
	"OK",
	"gui_from_xml accepts prefixed GUI namespace",
);

let file := Path.tempfile();
file.spew_utf8(
	"<Window title=\"From file\"><Button id=\"ok\" text=\"OK\" /></Window>",
);
let from_file := gui_from_xml_file(file);
is( from_file.title(), "From file", "gui_from_xml_file loads Path input" );
is(
	gui_from_xml_file(file.to_String()).find_by_id("ok").text(),
	"OK",
	"gui_from_xml_file loads string paths",
);

let denied := exception( function () {
	eval(
		"from std/gui import *; gui_from_xml_file(\"missing.xml\");",
		deny_fs: true,
	);
} );
ok( denied instanceof Exception, "gui_from_xml_file denied fs throws" );
ok(
	denied{message} ~ /XML\.load is denied by runtime policy/,
	"denied fs error is deterministic",
);

let unknown_attr := exception( function () {
	gui_from_xml("<Window surprise=\"x\" />");
} );
ok( unknown_attr instanceof Exception, "unknown XML attr throws" );
ok(
	unknown_attr{message} ~ /GUI_XML_ATTR_UNKNOWN/,
	"unknown XML attr error includes code",
);

let bad_bool := exception( function () {
	gui_from_xml("<Input required=\"maybe\" />");
} );
ok( bad_bool instanceof Exception, "bad XML bool throws" );
ok(
	bad_bool{message} ~ /GUI_XML_ATTR_TYPE/,
	"bad XML bool error includes type code",
);

let bad_number := exception( function () {
	gui_from_xml("<Slider value=\"loud\" />");
} );
ok( bad_number instanceof Exception, "bad XML number throws" );
ok(
	bad_number{message} ~ /GUI_XML_ATTR_TYPE/,
	"bad XML number error includes type code",
);

let bad_namespace := exception( function () {
	gui_from_xml("<Window xmlns=\"urn:not-gui\" />");
} );
ok( bad_namespace instanceof Exception, "bad XML namespace throws" );
ok(
	bad_namespace{message} ~ /GUI_XML_STRUCTURE/,
	"bad XML namespace error includes structure code",
);

done_testing();