zuzu-rust 0.2.0

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

requires_capability("fs");

from std/config import Config;
from std/io import Path;

let cfg := Config.from_data(
	{
		app: {
			name: "zuzu",
			port: 7000,
		},
		database: {
			host: "db.example.test",
			pool: 5,
		},
		features: {
			metrics: false,
		},
	},
	{
		source: "inline",
		format: "json",
	},
);

is( cfg.source(), "inline", "Config.from_data stores source metadata" );
is( cfg.format(), "json", "Config.from_data stores format metadata" );
is( cfg.layers().length(), 0, "Config.from_data starts with no file layers" );

is( cfg.get( "/app/name", "x" ), "zuzu", "get returns first matching value" );
is( cfg @ "/database/host", "db.example.test", "@ reads through Config object" );
ok( cfg @? "/database/pool", "@? reports existing path on Config object" );
let app_values := ( cfg @@ "/app/*" ).to_Set();
ok( app_values.contains("zuzu"), "@@ over Config includes string child value" );
ok( app_values.contains(7000), "@@ over Config includes numeric child value" );

is( cfg.require("/database/pool"), 5, "require returns present value" );

cfg.merge(
	{
		app: {
			port: 7100,
			mode: "dev",
		},
		database: {
			ssl: true,
		},
	},
);

is( cfg @ "/app/port", 7100, "merge overlays nested scalar values" );
is( cfg @ "/app/mode", "dev", "merge adds nested dictionary keys" );
is( cfg @ "/database/ssl", true, "merge adds sibling keys without dropping existing ones" );
is( cfg @ "/database/host", "db.example.test", "merge keeps untouched keys" );

cfg.merge(
	{
		servers: [ "a", "b" ],
	},
);
cfg.merge(
	{
		servers: [ "c" ],
	},
	{
		array_merge: "append",
	},
);
is( cfg @ "/servers/#0", "a", "array append merge keeps earlier array entries" );
is( cfg @ "/servers/#2", "c", "array append merge appends later array entries" );

cfg.merge_flat(
	{
		"APP__database__pool": "9",
		"APP__features__metrics": "true",
		"APP__limits__burst": "12.5",
		"APP__labels": "[\"a\",\"b\"]",
	},
	{
		prefix: "APP__",
		separator: "__",
		coerce: true,
	},
);

is( cfg @ "/database/pool", 9, "merge_flat coerces integers" );
is( cfg @ "/features/metrics", true, "merge_flat coerces booleans" );
is( cfg @ "/limits/burst", 12.5, "merge_flat coerces decimal numbers" );
is( cfg @ "/labels/#1", "b", "merge_flat can coerce JSON arrays" );

is( cfg.set( "/service/name", "worker" ), "worker", "set returns assigned value" );
is( cfg @ "/service/name", "worker", "set creates simple nested paths" );
is( cfg.set_default( "/service/name", "api" ), "worker", "set_default keeps existing non-null values" );
is( cfg.set_default( "/service/retries", 3 ), 3, "set_default populates missing values" );
is( cfg @ "/service/retries", 3, "set_default stores missing value" );

is( cfg.assign_first( "/service/retries", 2, "+=" ), 5, "assign_first supports compound operators" );
is( cfg @ "/service/retries", 5, "assign_first mutates through compiled path" );
let service_values := ( cfg @@ "/service/*" ).to_Set();
ok( service_values.contains("worker"), "Config query still includes string child after assignment" );
ok( service_values.contains(5), "Config query still includes numeric child after assignment" );

let cfg_ref := cfg.ref_first("/service/name");
is( cfg_ref(), "worker", "ref_first returns getter/setter ref" );
cfg_ref("jobs");
is( cfg @ "/service/name", "jobs", "ref_first setter mutates Config data" );

let parsed := Config.parse(
	"{\"debug\":true,\"nested\":{\"x\":1}}",
	"json",
	{
		source: "inline.json",
	},
);
is( parsed.source(), "inline.json", "parse stores source metadata" );
is( parsed @ "/debug", true, "parse decodes inline JSON" );
is( parsed @ "/nested/x", 1, "parse decodes nested values" );

let dir := Path.tempdir();
let base := dir.child("base.toml");
let local := dir.child("local.json");
let out := dir.child("out.json");

base.spew_utf8(
	"title = \"base\"\n"
	_ "[database]\n"
	_ "host = \"db.local\"\n"
	_ "pool = 3\n",
);
local.spew_utf8(
	"{\"database\":{\"pool\":7},\"feature\":{\"dark\":true}}",
);

let file_cfg := Config.load( [ base, local ] );
is( file_cfg.layers().length(), 2, "load records one layer per file" );
is( file_cfg @ "/title", "base", "load auto-detects TOML format" );
is( file_cfg @ "/database/host", "db.local", "later overlays keep base-only values" );
is( file_cfg @ "/database/pool", 7, "later overlays replace earlier scalar values" );
is( file_cfg @ "/feature/dark", true, "load auto-detects JSON format" );

file_cfg.save(out);
let roundtrip := Config.load(out);
is( roundtrip @ "/database/pool", 7, "save writes JSON that can be loaded again" );
is( roundtrip @ "/feature/dark", true, "roundtrip preserves boolean values" );

ok(
	try {
		cfg.require("/missing/value");
		false;
	}
	catch {
		true;
	},
	"require throws for missing config path",
);

done_testing();