diff --git a/BUILD.bazel b/BUILD.bazel
index a6a3dc5..315d7b3 100644
--- a/BUILD.bazel
+++ b/BUILD.bazel
@@ -2,6 +2,7 @@ load("@aspect_rules_js//js:defs.bzl", "js_library")
load("@aspect_rules_ts//ts:defs.bzl", "ts_config")
load("@npm//:defs.bzl", "npm_link_all_packages")
load("@npm//:tsconfig-to-swcconfig/package_json.bzl", tsconfig_to_swcconfig = "bin")
+load("@rules_kotlin//kotlin:core.bzl", "kt_kotlinc_options")
package(default_visibility = ["//visibility:public"])
@@ -21,6 +22,7 @@ exports_files([
"README.md",
"requirements.txt",
".pylintrc",
+ "detekt.yml",
])
js_library(
@@ -97,3 +99,9 @@ tsconfig_to_swcconfig.t2s(
stdout = ".swcrc",
visibility = ["//:__subpackages__"],
)
+
+kt_kotlinc_options(
+ name = "kt_opts",
+ jvm_target = "21",
+ visibility = ["//visibility:public"],
+)
diff --git a/MODULE.bazel b/MODULE.bazel
index 0527495..7caeb3c 100644
--- a/MODULE.bazel
+++ b/MODULE.bazel
@@ -66,8 +66,48 @@ pip.parse(
use_repo(pip, "pypi")
+####### Kotlin #########
+bazel_dep(name = "rules_kotlin", version = "2.2.1")
+bazel_dep(name = "rules_detekt", version = "0.8.1.13")
+bazel_dep(name = "rules_jvm_external", version = "6.9")
+
+maven = use_extension("@rules_jvm_external//:extensions.bzl", "maven")
+maven.install(
+ known_contributing_modules = ["bazel_worker_java", "player-lang"],
+ artifacts = [
+ "org.jetbrains.kotlin:kotlin-stdlib:2.3.0",
+ "org.jetbrains.kotlin:kotlin-test:2.3.0",
+ "org.jetbrains.kotlin:kotlin-test-junit5:2.3.0",
+ "org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0",
+ "io.kotest:kotest-runner-junit5:6.0.7",
+ "io.kotest:kotest-assertions-core:6.0.7",
+ "io.kotest:kotest-assertions-json:6.0.7",
+ "org.junit.jupiter:junit-jupiter-api:5.7.2",
+ "org.junit.jupiter:junit-jupiter-engine:5.7.2",
+ "org.junit.jupiter:junit-jupiter-params:5.7.2",
+ "org.junit.platform:junit-platform-commons:1.7.2",
+ "org.junit.platform:junit-platform-console:1.7.2",
+ "org.junit.platform:junit-platform-engine:1.7.2",
+ "org.junit.platform:junit-platform-launcher:1.7.2",
+ "org.junit.platform:junit-platform-suite-api:1.7.2",
+ "org.apiguardian:apiguardian-api:1.0.0",
+ "org.opentest4j:opentest4j:1.1.1",
+ "com.intuit.playerui.xlr:xlr-types:0.2.0--canary.4.100-SNAPSHOT",
+ "com.squareup:kotlinpoet-jvm:2.1.0",
+ ],
+ repositories = [
+ "https://repo1.maven.org/maven2",
+ "https://central.sonatype.com/repository/maven-snapshots",
+ ],
+)
+use_repo(maven, "maven")
+########################
+
build_constants = use_repo_rule("@rules_player//distribution:defs.bzl", "build_constants")
build_constants(
name = "build_constants",
+ constants = {
+ "GROUP": "com.intuit.playerui.lang",
+ },
version_file = "//:VERSION",
)
diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock
index 877b894..d2a004e 100644
--- a/MODULE.bazel.lock
+++ b/MODULE.bazel.lock
@@ -9,7 +9,10 @@
"https://bcr.bazel.build/modules/abseil-cpp/20230802.0/MODULE.bazel": "d253ae36a8bd9ee3c5955384096ccb6baf16a1b1e93e858370da0a3b94f77c16",
"https://bcr.bazel.build/modules/abseil-cpp/20230802.1/MODULE.bazel": "fa92e2eb41a04df73cdabeec37107316f7e5272650f81d6cc096418fe647b915",
"https://bcr.bazel.build/modules/abseil-cpp/20240116.1/MODULE.bazel": "37bcdb4440fbb61df6a1c296ae01b327f19e9bb521f9b8e26ec854b6f97309ed",
- "https://bcr.bazel.build/modules/abseil-cpp/20240116.1/source.json": "9be551b8d4e3ef76875c0d744b5d6a504a27e3ae67bc6b28f46415fd2d2957da",
+ "https://bcr.bazel.build/modules/abseil-cpp/20240116.2/MODULE.bazel": "73939767a4686cd9a520d16af5ab440071ed75cec1a876bf2fcfaf1f71987a16",
+ "https://bcr.bazel.build/modules/abseil-cpp/20250127.1/MODULE.bazel": "c4a89e7ceb9bf1e25cf84a9f830ff6b817b72874088bf5141b314726e46a57c1",
+ "https://bcr.bazel.build/modules/abseil-cpp/20250512.1/MODULE.bazel": "d209fdb6f36ffaf61c509fcc81b19e81b411a999a934a032e10cd009a0226215",
+ "https://bcr.bazel.build/modules/abseil-cpp/20250512.1/source.json": "d725d73707d01bb46ab3ca59ba408b8e9bd336642ca77a2269d4bfb8bbfd413d",
"https://bcr.bazel.build/modules/abseil-py/2.1.0/MODULE.bazel": "5ebe5bf853769c65707e5c28f216798f7a4b1042015e6a36e6d03094d94bec8a",
"https://bcr.bazel.build/modules/abseil-py/2.1.0/source.json": "0e8fc4f088ce07099c1cd6594c20c7ddbb48b4b3c0849b7d94ba94be88ff042b",
"https://bcr.bazel.build/modules/apple_support/1.11.1/MODULE.bazel": "1843d7cd8a58369a444fc6000e7304425fba600ff641592161d9f15b179fb896",
@@ -49,7 +52,8 @@
"https://bcr.bazel.build/modules/bazel_features/1.28.0/MODULE.bazel": "4b4200e6cbf8fa335b2c3f43e1d6ef3e240319c33d43d60cc0fbd4b87ece299d",
"https://bcr.bazel.build/modules/bazel_features/1.3.0/MODULE.bazel": "cdcafe83ec318cda34e02948e81d790aab8df7a929cec6f6969f13a489ccecd9",
"https://bcr.bazel.build/modules/bazel_features/1.30.0/MODULE.bazel": "a14b62d05969a293b80257e72e597c2da7f717e1e69fa8b339703ed6731bec87",
- "https://bcr.bazel.build/modules/bazel_features/1.30.0/source.json": "b07e17f067fe4f69f90b03b36ef1e08fe0d1f3cac254c1241a1818773e3423bc",
+ "https://bcr.bazel.build/modules/bazel_features/1.33.0/MODULE.bazel": "8b8dc9d2a4c88609409c3191165bccec0e4cb044cd7a72ccbe826583303459f6",
+ "https://bcr.bazel.build/modules/bazel_features/1.33.0/source.json": "13617db3930328c2cd2807a0f13d52ca870ac05f96db9668655113265147b2a6",
"https://bcr.bazel.build/modules/bazel_features/1.4.1/MODULE.bazel": "e45b6bb2350aff3e442ae1111c555e27eac1d915e77775f6fdc4b351b758b5d7",
"https://bcr.bazel.build/modules/bazel_features/1.9.0/MODULE.bazel": "885151d58d90d8d9c811eb75e3288c11f850e1d6b481a8c9f766adee4712358b",
"https://bcr.bazel.build/modules/bazel_features/1.9.1/MODULE.bazel": "8f679097876a9b609ad1f60249c49d68bfab783dd9be012faf9d82547b14815a",
@@ -69,12 +73,14 @@
"https://bcr.bazel.build/modules/bazel_skylib/1.8.1/MODULE.bazel": "88ade7293becda963e0e3ea33e7d54d3425127e0a326e0d17da085a5f1f03ff6",
"https://bcr.bazel.build/modules/bazel_skylib/1.8.1/source.json": "7ebaefba0b03efe59cac88ed5bbc67bcf59a3eff33af937345ede2a38b2d368a",
"https://bcr.bazel.build/modules/bazel_worker_api/0.0.1/MODULE.bazel": "02a13b77321773b2042e70ee5e4c5e099c8ddee4cf2da9cd420442c36938d4bd",
+ "https://bcr.bazel.build/modules/bazel_worker_api/0.0.10/MODULE.bazel": "a426df551b40c3997c351d05f00f0d1f86b618ddb646012f1e9c72efce8ed939",
+ "https://bcr.bazel.build/modules/bazel_worker_api/0.0.10/source.json": "7f220d3edfeba5d1f61535fd8400338df74f8bdeb241ebe660534c6926a5a645",
"https://bcr.bazel.build/modules/bazel_worker_api/0.0.4/MODULE.bazel": "460aa12d01231a80cce03c548287b433b321d205b0028ae596728c35e5ee442e",
"https://bcr.bazel.build/modules/bazel_worker_api/0.0.6/MODULE.bazel": "fd1f9432ca04c947e91b500df69ce7c5b6dbfe1bc45ab1820338205dae3383a6",
- "https://bcr.bazel.build/modules/bazel_worker_api/0.0.6/source.json": "5d68545f224904745a3cabd35aea6bc2b6cc5a78b7f49f3f69660eab2eeeb273",
+ "https://bcr.bazel.build/modules/bazel_worker_java/0.0.10/MODULE.bazel": "538f21f715cab81c4a43f73e1a91a0c1f8accd9b263dd2a831952805fcfc1a62",
+ "https://bcr.bazel.build/modules/bazel_worker_java/0.0.10/source.json": "1646d3aaf5a4bc0d587ecb33d81e93ea9284ff96f5ef579006a9c054945bbe7a",
"https://bcr.bazel.build/modules/bazel_worker_java/0.0.4/MODULE.bazel": "82494a01018bb7ef06d4a17ec4cd7a758721f10eb8b6c820a818e70d669500db",
"https://bcr.bazel.build/modules/bazel_worker_java/0.0.6/MODULE.bazel": "bff16868f96a646b37e7f78fc09fb0f6d6d4ae11c0ffd082825bd24701746f73",
- "https://bcr.bazel.build/modules/bazel_worker_java/0.0.6/source.json": "c4719e1f11a6f0209005a1a54aae358d47b6f62ad36ca3a31b10ef313731dd31",
"https://bcr.bazel.build/modules/buildifier_prebuilt/6.0.0.1/MODULE.bazel": "5d23708e6a5527ab4f151da7accabc22808cb5fb579c8cc4cd4a292da57a5c97",
"https://bcr.bazel.build/modules/buildifier_prebuilt/6.0.0.1/source.json": "57bc8b05bd4bb2736efe1b41f9f4bf551cdced8314e8d20420b8a0e5a0751b30",
"https://bcr.bazel.build/modules/buildozer/7.1.2/MODULE.bazel": "2e8dd40ede9c454042645fd8d8d0cd1527966aa5c919de86661e62953cd73d84",
@@ -91,18 +97,22 @@
"https://bcr.bazel.build/modules/google_benchmark/1.8.2/MODULE.bazel": "a70cf1bba851000ba93b58ae2f6d76490a9feb74192e57ab8e8ff13c34ec50cb",
"https://bcr.bazel.build/modules/googletest/1.11.0/MODULE.bazel": "3a83f095183f66345ca86aa13c58b59f9f94a2f81999c093d4eeaa2d262d12f4",
"https://bcr.bazel.build/modules/googletest/1.14.0.bcr.1/MODULE.bazel": "22c31a561553727960057361aa33bf20fb2e98584bc4fec007906e27053f80c6",
- "https://bcr.bazel.build/modules/googletest/1.14.0.bcr.1/source.json": "41e9e129f80d8c8bf103a7acc337b76e54fad1214ac0a7084bf24f4cd924b8b4",
"https://bcr.bazel.build/modules/googletest/1.14.0/MODULE.bazel": "cfbcbf3e6eac06ef9d85900f64424708cc08687d1b527f0ef65aa7517af8118f",
+ "https://bcr.bazel.build/modules/googletest/1.15.2/MODULE.bazel": "6de1edc1d26cafb0ea1a6ab3f4d4192d91a312fd2d360b63adaa213cd00b2108",
+ "https://bcr.bazel.build/modules/googletest/1.17.0/MODULE.bazel": "dbec758171594a705933a29fcf69293d2468c49ec1f2ebca65c36f504d72df46",
+ "https://bcr.bazel.build/modules/googletest/1.17.0/source.json": "38e4454b25fc30f15439c0378e57909ab1fd0a443158aa35aec685da727cd713",
"https://bcr.bazel.build/modules/jq.bzl/0.1.0/MODULE.bazel": "2ce69b1af49952cd4121a9c3055faa679e748ce774c7f1fda9657f936cae902f",
"https://bcr.bazel.build/modules/jq.bzl/0.1.0/source.json": "746bf13cac0860f091df5e4911d0c593971cd8796b5ad4e809b2f8e133eee3d5",
"https://bcr.bazel.build/modules/jsoncpp/1.9.5/MODULE.bazel": "31271aedc59e815656f5736f282bb7509a97c7ecb43e927ac1a37966e0578075",
- "https://bcr.bazel.build/modules/jsoncpp/1.9.5/source.json": "4108ee5085dd2885a341c7fab149429db457b3169b86eb081fa245eadf69169d",
+ "https://bcr.bazel.build/modules/jsoncpp/1.9.6/MODULE.bazel": "2f8d20d3b7d54143213c4dfc3d98225c42de7d666011528dc8fe91591e2e17b0",
+ "https://bcr.bazel.build/modules/jsoncpp/1.9.6/source.json": "a04756d367a2126c3541682864ecec52f92cdee80a35735a3cb249ce015ca000",
"https://bcr.bazel.build/modules/libpfm/4.11.0/MODULE.bazel": "45061ff025b301940f1e30d2c16bea596c25b176c8b6b3087e92615adbd52902",
"https://bcr.bazel.build/modules/nlohmann_json/3.12.0.bcr.1/MODULE.bazel": "a1c8bb07b5b91d971727c635f449d05623ac9608f6fe4f5f04254ea12f08e349",
"https://bcr.bazel.build/modules/nlohmann_json/3.12.0.bcr.1/source.json": "93f82a5ae985eb935c539bfee95e04767187818189241ac956f3ccadbdb8fb02",
"https://bcr.bazel.build/modules/nlohmann_json/3.6.1/MODULE.bazel": "6f7b417dcc794d9add9e556673ad25cb3ba835224290f4f848f8e2db1e1fca74",
"https://bcr.bazel.build/modules/package_metadata/0.0.2/MODULE.bazel": "fb8d25550742674d63d7b250063d4580ca530499f045d70748b1b142081ebb92",
- "https://bcr.bazel.build/modules/package_metadata/0.0.2/source.json": "e53a759a72488d2c0576f57491ef2da0cf4aab05ac0997314012495935531b73",
+ "https://bcr.bazel.build/modules/package_metadata/0.0.3/MODULE.bazel": "77890552ecea9e284b5424c9de827a58099348763a4359e975c359a83d4faa83",
+ "https://bcr.bazel.build/modules/package_metadata/0.0.3/source.json": "742075a428ad12a3fa18a69014c2f57f01af910c6d9d18646c990200853e641a",
"https://bcr.bazel.build/modules/platforms/0.0.10/MODULE.bazel": "8cb8efaf200bdeb2150d93e162c40f388529a25852b332cec879373771e48ed5",
"https://bcr.bazel.build/modules/platforms/0.0.11/MODULE.bazel": "0daefc49732e227caa8bfa834d65dc52e8cc18a2faf80df25e8caea151a9413f",
"https://bcr.bazel.build/modules/platforms/0.0.11/source.json": "f7e188b79ebedebfe75e9e1d098b8845226c7992b307e28e1496f23112e8fc29",
@@ -121,19 +131,25 @@
"https://bcr.bazel.build/modules/protobuf/29.0-rc2/MODULE.bazel": "6241d35983510143049943fc0d57937937122baf1b287862f9dc8590fc4c37df",
"https://bcr.bazel.build/modules/protobuf/29.0-rc3/MODULE.bazel": "33c2dfa286578573afc55a7acaea3cada4122b9631007c594bf0729f41c8de92",
"https://bcr.bazel.build/modules/protobuf/29.0/MODULE.bazel": "319dc8bf4c679ff87e71b1ccfb5a6e90a6dbc4693501d471f48662ac46d04e4e",
+ "https://bcr.bazel.build/modules/protobuf/29.1/MODULE.bazel": "557c3457560ff49e122ed76c0bc3397a64af9574691cb8201b4e46d4ab2ecb95",
"https://bcr.bazel.build/modules/protobuf/29.3/MODULE.bazel": "77480eea5fb5541903e49683f24dc3e09f4a79e0eea247414887bb9fc0066e94",
- "https://bcr.bazel.build/modules/protobuf/29.3/source.json": "c460e6550ddd24996232c7542ebf201f73c4e01d2183a31a041035fb50f19681",
"https://bcr.bazel.build/modules/protobuf/3.19.0/MODULE.bazel": "6b5fbb433f760a99a22b18b6850ed5784ef0e9928a72668b66e4d7ccd47db9b0",
"https://bcr.bazel.build/modules/protobuf/3.19.2/MODULE.bazel": "532ffe5f2186b69fdde039efe6df13ba726ff338c6bc82275ad433013fa10573",
"https://bcr.bazel.build/modules/protobuf/3.19.6/MODULE.bazel": "9233edc5e1f2ee276a60de3eaa47ac4132302ef9643238f23128fea53ea12858",
+ "https://bcr.bazel.build/modules/protobuf/33.1/MODULE.bazel": "982c8a0cceab4d790076f72b7677faf836b0dfadc2b66a34aab7232116c4ae39",
+ "https://bcr.bazel.build/modules/protobuf/33.1/source.json": "992c237a40899425648213bf79b05f08c6e8dcd619f96cd944b4511b0276fbd8",
"https://bcr.bazel.build/modules/pybind11_bazel/2.11.1/MODULE.bazel": "88af1c246226d87e65be78ed49ecd1e6f5e98648558c14ce99176da041dc378e",
- "https://bcr.bazel.build/modules/pybind11_bazel/2.11.1/source.json": "be4789e951dd5301282729fe3d4938995dc4c1a81c2ff150afc9f1b0504c6022",
+ "https://bcr.bazel.build/modules/pybind11_bazel/2.12.0/MODULE.bazel": "e6f4c20442eaa7c90d7190d8dc539d0ab422f95c65a57cc59562170c58ae3d34",
+ "https://bcr.bazel.build/modules/pybind11_bazel/2.12.0/source.json": "6900fdc8a9e95866b8c0d4ad4aba4d4236317b5c1cd04c502df3f0d33afed680",
"https://bcr.bazel.build/modules/re2/2023-09-01/MODULE.bazel": "cb3d511531b16cfc78a225a9e2136007a48cf8a677e4264baeab57fe78a80206",
- "https://bcr.bazel.build/modules/re2/2023-09-01/source.json": "e044ce89c2883cd957a2969a43e79f7752f9656f6b20050b62f90ede21ec6eb4",
+ "https://bcr.bazel.build/modules/re2/2024-07-02.bcr.1/MODULE.bazel": "b4963dda9b31080be1905ef085ecd7dd6cd47c05c79b9cdf83ade83ab2ab271a",
+ "https://bcr.bazel.build/modules/re2/2024-07-02.bcr.1/source.json": "2ff292be6ef3340325ce8a045ecc326e92cbfab47c7cbab4bd85d28971b97ac4",
+ "https://bcr.bazel.build/modules/re2/2024-07-02/MODULE.bazel": "0eadc4395959969297cbcf31a249ff457f2f1d456228c67719480205aa306daa",
"https://bcr.bazel.build/modules/rules_android/0.1.1/MODULE.bazel": "48809ab0091b07ad0182defb787c4c5328bd3a278938415c00a7b69b50c4d3a8",
"https://bcr.bazel.build/modules/rules_android/0.6.4/MODULE.bazel": "b4cde12d506dd65d82b2be39761f49f5797303343a3d5b4ee191c0cdf9ef387c",
"https://bcr.bazel.build/modules/rules_android/0.6.5/MODULE.bazel": "6a44151a40914578679754a4f4b1fdc80d3e1cc674626524f0e6037a9d6c775f",
"https://bcr.bazel.build/modules/rules_android/0.6.5/source.json": "c45d1b4a54d165c16089b1fd90aecadd21a2400f0eca149359a41919da6830ed",
+ "https://bcr.bazel.build/modules/rules_apple/3.16.0/MODULE.bazel": "0d1caf0b8375942ce98ea944be754a18874041e4e0459401d925577624d3a54a",
"https://bcr.bazel.build/modules/rules_apple/4.3.3/MODULE.bazel": "c5c2c4adeeac5f3f2f9b7f16abfa8be7ffefa596171d0d92bed4cae9ade0a498",
"https://bcr.bazel.build/modules/rules_apple/4.3.3/source.json": "3cb1d69c8243ffcc42ecbf84ae8b9cccd7b1e2f091b0aee5a3e9c9a45267f312",
"https://bcr.bazel.build/modules/rules_cc/0.0.1/MODULE.bazel": "cb2aa0747f84c6c3a78dad4e2049c154f08ab9d166b1273835a8174940365647",
@@ -152,9 +168,10 @@
"https://bcr.bazel.build/modules/rules_cc/0.2.14/MODULE.bazel": "353c99ed148887ee89c54a17d4100ae7e7e436593d104b668476019023b58df8",
"https://bcr.bazel.build/modules/rules_cc/0.2.14/source.json": "55d0a4587c5592fad350f6e698530f4faf0e7dd15e69d43f8d87e220c78bea54",
"https://bcr.bazel.build/modules/rules_cc/0.2.8/MODULE.bazel": "f1df20f0bf22c28192a794f29b501ee2018fa37a3862a1a2132ae2940a23a642",
+ "https://bcr.bazel.build/modules/rules_detekt/0.8.1.13/MODULE.bazel": "d1807e63bf94d3c3f8d0ab475437e4dede1e3d0c9e166e4556a94cf2a1be7f17",
+ "https://bcr.bazel.build/modules/rules_detekt/0.8.1.13/source.json": "48340024890d74f72ed0d8084d6fa5a23de259423ca4ab509dafa48dfb37191e",
"https://bcr.bazel.build/modules/rules_foreign_cc/0.9.0/MODULE.bazel": "c9e8c682bf75b0e7c704166d79b599f93b72cfca5ad7477df596947891feeef6",
"https://bcr.bazel.build/modules/rules_fuzzing/0.5.2/MODULE.bazel": "40c97d1144356f52905566c55811f13b299453a14ac7769dfba2ac38192337a8",
- "https://bcr.bazel.build/modules/rules_fuzzing/0.5.2/source.json": "c8b1e2c717646f1702290959a3302a178fb639d987ab61d548105019f11e527e",
"https://bcr.bazel.build/modules/rules_go/0.41.0/MODULE.bazel": "55861d8e8bb0e62cbd2896f60ff303f62ffcb0eddb74ecb0e5c0cbe36fc292c8",
"https://bcr.bazel.build/modules/rules_go/0.42.0/MODULE.bazel": "8cfa875b9aa8c6fce2b2e5925e73c1388173ea3c32a0db4d2b4804b453c14270",
"https://bcr.bazel.build/modules/rules_go/0.46.0/MODULE.bazel": "3477df8bdcc49e698b9d25f734c4f3a9f5931ff34ee48a2c662be168f5f2d3fd",
@@ -163,6 +180,7 @@
"https://bcr.bazel.build/modules/rules_go/0.51.0-rc2/source.json": "6b5cd0b3da2bd0e6949580851db990a04af0a285f072b9a0f059424457cd8cc9",
"https://bcr.bazel.build/modules/rules_java/4.0.0/MODULE.bazel": "5a78a7ae82cd1a33cef56dc578c7d2a46ed0dca12643ee45edbb8417899e6f74",
"https://bcr.bazel.build/modules/rules_java/5.3.5/MODULE.bazel": "a4ec4f2db570171e3e5eb753276ee4b389bae16b96207e9d3230895c99644b86",
+ "https://bcr.bazel.build/modules/rules_java/5.5.0/MODULE.bazel": "486ad1aa15cdc881af632b4b1448b0136c76025a1fe1ad1b65c5899376b83a50",
"https://bcr.bazel.build/modules/rules_java/6.0.0/MODULE.bazel": "8a43b7df601a7ec1af61d79345c17b31ea1fedc6711fd4abfd013ea612978e39",
"https://bcr.bazel.build/modules/rules_java/6.3.0/MODULE.bazel": "a97c7678c19f236a956ad260d59c86e10a463badb7eb2eda787490f4c969b963",
"https://bcr.bazel.build/modules/rules_java/6.4.0/MODULE.bazel": "e986a9fe25aeaa84ac17ca093ef13a4637f6107375f64667a15999f77db6c8f6",
@@ -176,10 +194,12 @@
"https://bcr.bazel.build/modules/rules_java/7.6.1/MODULE.bazel": "2f14b7e8a1aa2f67ae92bc69d1ec0fa8d9f827c4e17ff5e5f02e91caa3b2d0fe",
"https://bcr.bazel.build/modules/rules_java/8.13.0/MODULE.bazel": "0444ebf737d144cf2bb2ccb368e7f1cce735264285f2a3711785827c1686625e",
"https://bcr.bazel.build/modules/rules_java/8.14.0/MODULE.bazel": "717717ed40cc69994596a45aec6ea78135ea434b8402fb91b009b9151dd65615",
- "https://bcr.bazel.build/modules/rules_java/8.14.0/source.json": "8a88c4ca9e8759da53cddc88123880565c520503321e2566b4e33d0287a3d4bc",
+ "https://bcr.bazel.build/modules/rules_java/8.15.2/MODULE.bazel": "5cc6698c822b2f9ef90ca5558599851bed8c3b13f1f8eb140d9bfec638d2acb4",
+ "https://bcr.bazel.build/modules/rules_java/8.15.2/source.json": "352984cbe6d32fac3bf76449e581ed5bcd54a2da2137fca1559aaf04756b7bfa",
"https://bcr.bazel.build/modules/rules_java/8.3.2/MODULE.bazel": "7336d5511ad5af0b8615fdc7477535a2e4e723a357b6713af439fe8cf0195017",
"https://bcr.bazel.build/modules/rules_java/8.5.1/MODULE.bazel": "d8a9e38cc5228881f7055a6079f6f7821a073df3744d441978e7a43e20226939",
"https://bcr.bazel.build/modules/rules_java/8.6.0/MODULE.bazel": "9c064c434606d75a086f15ade5edb514308cccd1544c2b2a89bbac4310e41c71",
+ "https://bcr.bazel.build/modules/rules_java/8.6.1/MODULE.bazel": "f4808e2ab5b0197f094cabce9f4b006a27766beb6a9975931da07099560ca9c2",
"https://bcr.bazel.build/modules/rules_java/8.6.3/MODULE.bazel": "e90505b7a931d194245ffcfb6ff4ca8ef9d46b4e830d12e64817752e0198e2ed",
"https://bcr.bazel.build/modules/rules_java/8.9.0/MODULE.bazel": "e17c876cb53dcd817b7b7f0d2985b710610169729e8c371b2221cacdcd3dce4a",
"https://bcr.bazel.build/modules/rules_jvm_external/4.4.2/MODULE.bazel": "a56b85e418c83eb1839819f0b515c431010160383306d13ec21959ac412d2fe7",
@@ -191,12 +211,14 @@
"https://bcr.bazel.build/modules/rules_jvm_external/6.3/MODULE.bazel": "c998e060b85f71e00de5ec552019347c8bca255062c990ac02d051bb80a38df0",
"https://bcr.bazel.build/modules/rules_jvm_external/6.6/MODULE.bazel": "153042249c7060536dc95b6bb9f9bb8063b8a0b0cb7acdb381bddbc2374aed55",
"https://bcr.bazel.build/modules/rules_jvm_external/6.7/MODULE.bazel": "e717beabc4d091ecb2c803c2d341b88590e9116b8bf7947915eeb33aab4f96dd",
- "https://bcr.bazel.build/modules/rules_jvm_external/6.7/source.json": "5426f412d0a7fc6b611643376c7e4a82dec991491b9ce5cb1cfdd25fe2e92be4",
+ "https://bcr.bazel.build/modules/rules_jvm_external/6.9/MODULE.bazel": "07c5db05527db7744a54fcffd653e1550d40e0540207a7f7e6d0a4de5bef8274",
+ "https://bcr.bazel.build/modules/rules_jvm_external/6.9/source.json": "b12970214f3cc144b26610caeb101fa622d910f1ab3d98f0bae1058edbd00bd4",
"https://bcr.bazel.build/modules/rules_kotlin/1.9.0/MODULE.bazel": "ef85697305025e5a61f395d4eaede272a5393cee479ace6686dba707de804d59",
"https://bcr.bazel.build/modules/rules_kotlin/1.9.5/MODULE.bazel": "043a16a572f610558ec2030db3ff0c9938574e7dd9f58bded1bb07c0192ef025",
"https://bcr.bazel.build/modules/rules_kotlin/1.9.6/MODULE.bazel": "d269a01a18ee74d0335450b10f62c9ed81f2321d7958a2934e44272fe82dcef3",
"https://bcr.bazel.build/modules/rules_kotlin/2.1.8/MODULE.bazel": "e832456b08f4bdf23a466ae4ec424ce700d4ccaa2c9e83912d32791ade43b77d",
- "https://bcr.bazel.build/modules/rules_kotlin/2.1.8/source.json": "4ccee7dee20c21546ae4ea3d26d4f9c859fc906fa4c8e317d571b62d8048ae10",
+ "https://bcr.bazel.build/modules/rules_kotlin/2.2.1/MODULE.bazel": "e6e2dc739345a74a0946056d0bf03a3e15080da8f54e872404c944f777803e10",
+ "https://bcr.bazel.build/modules/rules_kotlin/2.2.1/source.json": "2a549f7f39f66a07d22081346bc51053b45f413ede77a34b3a21b13e1d20b320",
"https://bcr.bazel.build/modules/rules_license/0.0.3/MODULE.bazel": "627e9ab0247f7d1e05736b59dbb1b6871373de5ad31c3011880b4133cafd4bd0",
"https://bcr.bazel.build/modules/rules_license/0.0.7/MODULE.bazel": "088fbeb0b6a419005b89cf93fe62d9517c0a2b8bb56af3244af65ecfe37e7d5d",
"https://bcr.bazel.build/modules/rules_license/1.0.0/MODULE.bazel": "a7fda60eefdf3d8c827262ba499957e4df06f659330bbe6cdbdb975b768bb65c",
@@ -209,24 +231,29 @@
"https://bcr.bazel.build/modules/rules_pkg/1.0.1/MODULE.bazel": "5b1df97dbc29623bccdf2b0dcd0f5cb08e2f2c9050aab1092fd39a41e82686ff",
"https://bcr.bazel.build/modules/rules_pkg/1.1.0/MODULE.bazel": "9db8031e71b6ef32d1846106e10dd0ee2deac042bd9a2de22b4761b0c3036453",
"https://bcr.bazel.build/modules/rules_pkg/1.1.0/source.json": "fef768df13a92ce6067e1cd0cdc47560dace01354f1d921cfb1d632511f7d608",
+ "https://bcr.bazel.build/modules/rules_pmd/0.4.0/MODULE.bazel": "2ce687fc15a244e4404f497be89a230a0de1742c41012530977aaeb693003d10",
+ "https://bcr.bazel.build/modules/rules_pmd/0.4.0/source.json": "ca481c0e8862f6e5e6217ab58ec427a620ab47035bab6b1a4cc7fd0ab6cdef62",
"https://bcr.bazel.build/modules/rules_proto/4.0.0/MODULE.bazel": "a7a7b6ce9bee418c1a760b3d84f83a299ad6952f9903c67f19e4edd964894e06",
"https://bcr.bazel.build/modules/rules_proto/5.3.0-21.7/MODULE.bazel": "e8dff86b0971688790ae75528fe1813f71809b5afd57facb44dad9e8eca631b7",
"https://bcr.bazel.build/modules/rules_proto/6.0.0-rc1/MODULE.bazel": "1e5b502e2e1a9e825eef74476a5a1ee524a92297085015a052510b09a1a09483",
"https://bcr.bazel.build/modules/rules_proto/6.0.0/MODULE.bazel": "b531d7f09f58dce456cd61b4579ce8c86b38544da75184eadaf0a7cb7966453f",
"https://bcr.bazel.build/modules/rules_proto/6.0.2/MODULE.bazel": "ce916b775a62b90b61888052a416ccdda405212b6aaeb39522f7dc53431a5e73",
"https://bcr.bazel.build/modules/rules_proto/7.0.2/MODULE.bazel": "bf81793bd6d2ad89a37a40693e56c61b0ee30f7a7fdbaf3eabbf5f39de47dea2",
- "https://bcr.bazel.build/modules/rules_proto/7.0.2/source.json": "1e5e7260ae32ef4f2b52fd1d0de8d03b606a44c91b694d2f1afb1d3b28a48ce1",
+ "https://bcr.bazel.build/modules/rules_proto/7.1.0/MODULE.bazel": "002d62d9108f75bb807cd56245d45648f38275cb3a99dcd45dfb864c5d74cb96",
+ "https://bcr.bazel.build/modules/rules_proto/7.1.0/source.json": "39f89066c12c24097854e8f57ab8558929f9c8d474d34b2c00ac04630ad8940e",
"https://bcr.bazel.build/modules/rules_python/0.10.2/MODULE.bazel": "cc82bc96f2997baa545ab3ce73f196d040ffb8756fd2d66125a530031cd90e5f",
"https://bcr.bazel.build/modules/rules_python/0.23.1/MODULE.bazel": "49ffccf0511cb8414de28321f5fcf2a31312b47c40cc21577144b7447f2bf300",
"https://bcr.bazel.build/modules/rules_python/0.25.0/MODULE.bazel": "72f1506841c920a1afec76975b35312410eea3aa7b63267436bfb1dd91d2d382",
"https://bcr.bazel.build/modules/rules_python/0.28.0/MODULE.bazel": "cba2573d870babc976664a912539b320cbaa7114cd3e8f053c720171cde331ed",
"https://bcr.bazel.build/modules/rules_python/0.31.0/MODULE.bazel": "93a43dc47ee570e6ec9f5779b2e64c1476a6ce921c48cc9a1678a91dd5f8fd58",
+ "https://bcr.bazel.build/modules/rules_python/0.33.2/MODULE.bazel": "3e036c4ad8d804a4dad897d333d8dce200d943df4827cb849840055be8d2e937",
"https://bcr.bazel.build/modules/rules_python/0.37.1/MODULE.bazel": "3faeb2d9fa0a81f8980643ee33f212308f4d93eea4b9ce6f36d0b742e71e9500",
"https://bcr.bazel.build/modules/rules_python/0.37.2/MODULE.bazel": "b5ffde91410745750b6c13be1c5dc4555ef5bc50562af4a89fd77807fdde626a",
"https://bcr.bazel.build/modules/rules_python/0.4.0/MODULE.bazel": "9208ee05fd48bf09ac60ed269791cf17fb343db56c8226a720fbb1cdf467166c",
"https://bcr.bazel.build/modules/rules_python/0.40.0/MODULE.bazel": "9d1a3cd88ed7d8e39583d9ffe56ae8a244f67783ae89b60caafc9f5cf318ada7",
"https://bcr.bazel.build/modules/rules_python/1.0.0/MODULE.bazel": "898a3d999c22caa585eb062b600f88654bf92efb204fa346fb55f6f8edffca43",
"https://bcr.bazel.build/modules/rules_python/1.3.0/MODULE.bazel": "8361d57eafb67c09b75bf4bbe6be360e1b8f4f18118ab48037f2bd50aa2ccb13",
+ "https://bcr.bazel.build/modules/rules_python/1.6.0/MODULE.bazel": "7e04ad8f8d5bea40451cf80b1bd8262552aa73f841415d20db96b7241bd027d8",
"https://bcr.bazel.build/modules/rules_python/1.6.1/MODULE.bazel": "0dd0dd858e4480a7dc0cecb21d2131a476cdd520bdb42d9fae64a50965a50082",
"https://bcr.bazel.build/modules/rules_python/1.6.1/source.json": "ef9a16eb730d643123689686b00bc5fd65d33f17061e7e9ac313a946acb33dea",
"https://bcr.bazel.build/modules/rules_robolectric/4.14.1.2/MODULE.bazel": "d44fec647d0aeb67b9f3b980cf68ba634976f3ae7ccd6c07d790b59b87a4f251",
@@ -237,6 +264,7 @@
"https://bcr.bazel.build/modules/rules_shell/0.4.1/MODULE.bazel": "00e501db01bbf4e3e1dd1595959092c2fadf2087b2852d3f553b5370f5633592",
"https://bcr.bazel.build/modules/rules_shell/0.4.1/source.json": "4757bd277fe1567763991c4425b483477bb82e35e777a56fd846eb5cceda324a",
"https://bcr.bazel.build/modules/rules_swift/1.16.0/MODULE.bazel": "4a09f199545a60d09895e8281362b1ff3bb08bbde69c6fc87aff5b92fcc916ca",
+ "https://bcr.bazel.build/modules/rules_swift/2.1.1/MODULE.bazel": "494900a80f944fc7aa61500c2073d9729dff0b764f0e89b824eb746959bc1046",
"https://bcr.bazel.build/modules/rules_swift/2.4.0/MODULE.bazel": "1639617eb1ede28d774d967a738b4a68b0accb40650beadb57c21846beab5efd",
"https://bcr.bazel.build/modules/rules_swift/3.4.1/MODULE.bazel": "c53b33c3f9db4e9cfe1b41ab12e909d62af1eeb9d15e4c0bfe0f39168c80ba44",
"https://bcr.bazel.build/modules/rules_swift/3.4.1/source.json": "f9dd7a5f18662c0762452c5a3267f570339a9661fcc325e9b50e6c7bd49ff4c1",
@@ -433,90 +461,27 @@
"recordedRepoMappingEntries": []
}
},
- "@@rules_kotlin+//src/main/starlark/core/repositories:bzlmod_setup.bzl%rules_kotlin_extensions": {
+ "@@rules_detekt+//detekt:extensions.bzl%detekt": {
"general": {
- "bzlTransitiveDigest": "vfLCTchDthU74iCKvoskQ+ovk2Wu2tLykbCddrcLy7U=",
- "usagesDigest": "QPppUlwb7NSBhcaYae+JZPqTEmJKCkOXKFPXQS7aAJE=",
+ "bzlTransitiveDigest": "+OuFn2ZT61Z6ezaVBJShaz969zy9EqR9H4yrU5h4NF0=",
+ "usagesDigest": "ijL3kJiGAQ3pUg2VpUJFTr5/8GI6sV/q4KZSOufqDbw=",
"recordedFileInputs": {},
"recordedDirentsInputs": {},
"envVariables": {},
"generatedRepoSpecs": {
- "com_github_jetbrains_kotlin_git": {
- "repoRuleId": "@@rules_kotlin+//src/main/starlark/core/repositories:compiler.bzl%kotlin_compiler_git_repository",
- "attributes": {
- "urls": [
- "https://github.com/JetBrains/kotlin/releases/download/v2.1.21/kotlin-compiler-2.1.21.zip"
- ],
- "sha256": "1ba08a8b45da99339a0601134cc037b54cf85e9bc0edbe76dcbd27c2d684a977"
- }
- },
- "com_github_jetbrains_kotlin": {
- "repoRuleId": "@@rules_kotlin+//src/main/starlark/core/repositories:compiler.bzl%kotlin_capabilities_repository",
- "attributes": {
- "git_repository_name": "com_github_jetbrains_kotlin_git",
- "compiler_version": "2.1.21"
- }
- },
- "com_github_google_ksp": {
- "repoRuleId": "@@rules_kotlin+//src/main/starlark/core/repositories:ksp.bzl%ksp_compiler_plugin_repository",
- "attributes": {
- "urls": [
- "https://github.com/google/ksp/releases/download/2.1.21-2.0.1/artifacts.zip"
- ],
- "sha256": "44e965bb067b2bb5cd9184dab2c3dea6e3eab747d341c07645bb4c88f09e49c8",
- "strip_version": "2.1.21-2.0.1"
- }
- },
- "com_github_pinterest_ktlint": {
- "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_file",
- "attributes": {
- "sha256": "5ba1ac917a06b0f02daaa60d10abbedd2220d60216af670c67a45b91c74cf8bb",
- "urls": [
- "https://github.com/pinterest/ktlint/releases/download/1.6.0/ktlint"
- ],
- "executable": true
- }
- },
- "kotlinx_serialization_core_jvm": {
- "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_jar",
- "attributes": {
- "sha256": "3565b6d4d789bf70683c45566944287fc1d8dc75c23d98bd87d01059cc76f2b3",
- "urls": [
- "https://repo1.maven.org/maven2/org/jetbrains/kotlinx/kotlinx-serialization-core-jvm/1.8.1/kotlinx-serialization-core-jvm-1.8.1.jar"
- ]
- }
- },
- "kotlinx_serialization_json": {
- "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_jar",
- "attributes": {
- "sha256": "58adf3358a0f99dd8d66a550fbe19064d395e0d5f7f1e46515cd3470a56fbbb0",
- "urls": [
- "https://repo1.maven.org/maven2/org/jetbrains/kotlinx/kotlinx-serialization-json/1.8.1/kotlinx-serialization-json-1.8.1.jar"
- ]
- }
- },
- "kotlinx_serialization_json_jvm": {
- "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_jar",
- "attributes": {
- "sha256": "8769e5647557e3700919c32d508f5c5dad53c5d8234cd10846354fbcff14aa24",
- "urls": [
- "https://repo1.maven.org/maven2/org/jetbrains/kotlinx/kotlinx-serialization-json-jvm/1.8.1/kotlinx-serialization-json-jvm-1.8.1.jar"
- ]
- }
- },
- "kotlin_build_tools_impl": {
+ "detekt_cli_all": {
"repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_jar",
"attributes": {
- "sha256": "6e94896e321603e3bfe89fef02478e44d1d64a3d25d49d0694892ffc01c60acf",
+ "sha256": "2ce2ff952e150baf28a29cda70a363b0340b3e81a55f43e51ec5edffc3d066c1",
"urls": [
- "https://repo1.maven.org/maven2/org/jetbrains/kotlin/kotlin-build-tools-impl/2.1.20/kotlin-build-tools-impl-2.1.20.jar"
+ "https://github.com/detekt/detekt/releases/download/v1.23.8/detekt-cli-1.23.8-all.jar"
]
}
}
},
"recordedRepoMappingEntries": [
[
- "rules_kotlin+",
+ "rules_detekt+",
"bazel_tools",
"bazel_tools"
]
diff --git a/detekt.yml b/detekt.yml
new file mode 100644
index 0000000..f7ed2ee
--- /dev/null
+++ b/detekt.yml
@@ -0,0 +1,95 @@
+# Detekt configuration for Player Language Kotlin modules.
+# Used with build_upon_default_config = True, so only overrides are needed.
+# ktlint handles naming, comments, and formatting — those rulesets are disabled here.
+
+complexity:
+ active: true
+ CyclomaticComplexMethod:
+ active: true
+ threshold: 25
+ LongMethod:
+ active: true
+ threshold: 80
+ LongParameterList:
+ active: true
+ functionThreshold: 8
+ constructorThreshold: 10
+ TooManyFunctions:
+ active: true
+ thresholdInFiles: 40
+ thresholdInClasses: 30
+ thresholdInInterfaces: 15
+ thresholdInObjects: 20
+ NestedBlockDepth:
+ active: true
+ threshold: 6
+ NestedScopeFunctions:
+ active: true
+ threshold: 3
+
+coroutines:
+ active: true
+
+empty-blocks:
+ active: true
+
+exceptions:
+ active: true
+
+naming:
+ active: false
+
+comments:
+ active: false
+
+potential-bugs:
+ active: true
+
+style:
+ active: true
+ ForbiddenComment:
+ active: false
+ MagicNumber:
+ active: false
+ MaxLineLength:
+ active: false
+ NewLineAtEndOfFile:
+ active: false
+ NoTabs:
+ active: false
+ OptionalAbstractKeyword:
+ active: false
+ BracesOnWhenStatements:
+ active: false
+ ReturnCount:
+ active: false
+ ThrowsCount:
+ active: false
+ TrailingWhitespace:
+ active: false
+ WildcardImport:
+ active: false
+ UnusedImports:
+ active: true
+ UnusedParameter:
+ active: true
+ UnusedPrivateClass:
+ active: true
+ UnusedPrivateMember:
+ active: true
+ UnusedPrivateProperty:
+ active: true
+ UnnecessaryAbstractClass:
+ active: true
+ UnnecessaryAnnotationUseSiteTarget:
+ active: true
+ UnnecessaryApply:
+ active: true
+ UnnecessaryFilter:
+ active: true
+ UnnecessaryInheritance:
+ active: true
+ UnnecessaryLet:
+ active: true
+ UnnecessaryParentheses:
+ active: true
diff --git a/dsl/kotlin/.editorconfig b/dsl/kotlin/.editorconfig
new file mode 100644
index 0000000..9eab273
--- /dev/null
+++ b/dsl/kotlin/.editorconfig
@@ -0,0 +1,32 @@
+# EditorConfig for Kotlin code style
+# https://editorconfig.org/
+
+root = true
+
+[*]
+charset = utf-8
+end_of_line = lf
+indent_size = 4
+indent_style = space
+insert_final_newline = true
+max_line_length = 120
+trim_trailing_whitespace = true
+
+[*.kt]
+# ktlint specific rules
+ktlint_standard_no-wildcard-imports = disabled
+ktlint_standard_package-name = disabled
+
+# Allow trailing commas
+ktlint_standard_trailing-comma-on-call-site = disabled
+ktlint_standard_trailing-comma-on-declaration-site = disabled
+
+# Function signature - allow multi-line
+ktlint_standard_function-signature = disabled
+
+# Disable some strict rules for DSL-style code
+ktlint_standard_parameter-list-wrapping = disabled
+ktlint_standard_argument-list-wrapping = disabled
+
+[*.kts]
+ktlint_standard_no-wildcard-imports = disabled
diff --git a/dsl/kotlin/BUILD.bazel b/dsl/kotlin/BUILD.bazel
new file mode 100644
index 0000000..c8f534f
--- /dev/null
+++ b/dsl/kotlin/BUILD.bazel
@@ -0,0 +1,59 @@
+load("@rules_player//kotlin:defs.bzl", "kt_jvm")
+load("@rules_kotlin//kotlin:core.bzl", "kt_compiler_plugin")
+load("@rules_kotlin//kotlin:jvm.bzl", "kt_jvm_library")
+load("@rules_kotlin//kotlin:lint.bzl", "ktlint_config")
+load("@rules_detekt//detekt:defs.bzl", "detekt_test")
+load("@build_constants//:constants.bzl", "GROUP", "VERSION")
+
+package(default_visibility = ["//visibility:public"])
+
+ktlint_config(
+ name = "lint_config",
+ editorconfig = ".editorconfig",
+)
+
+kt_compiler_plugin(
+ name = "serialization_plugin",
+ compile_phase = True,
+ id = "org.jetbrains.kotlin.serialization",
+ stubs_phase = True,
+ deps = ["@rules_kotlin//kotlin/compiler:kotlin-serialization-compiler-plugin"],
+)
+
+kt_jvm_library(
+ name = "kotlin_serialization",
+ srcs = [],
+ exported_compiler_plugins = [":serialization_plugin"],
+ exports = ["@maven//:org_jetbrains_kotlinx_kotlinx_serialization_json"],
+)
+
+detekt_test(
+ name = "detekt",
+ srcs = glob(["src/main/kotlin/**/*.kt", "src/test/kotlin/**/*.kt"]),
+ cfgs = ["//:detekt.yml"],
+ build_upon_default_config = True,
+ html_report = True,
+ tags = ["manual"],
+)
+
+kt_jvm(
+ name = "kotlin-dsl",
+ group = GROUP,
+ version = VERSION,
+ lint_config = ":lint_config",
+ main_opts = "//:kt_opts",
+ main_deps = [
+ ":kotlin_serialization",
+ "@maven//:org_jetbrains_kotlin_kotlin_stdlib",
+ ],
+ test_package = "com.intuit.playerui.lang.dsl",
+ test_opts = "//:kt_opts",
+ test_deps = [
+ "@maven//:io_kotest_kotest_assertions_core",
+ "@maven//:io_kotest_kotest_assertions_json",
+ "@maven//:io_kotest_kotest_runner_junit5",
+ "@maven//:org_jetbrains_kotlin_kotlin_stdlib",
+ "@maven//:org_junit_platform_junit_platform_console",
+ ],
+ pom_template = ":pom.tpl",
+)
diff --git a/dsl/kotlin/pom.tpl b/dsl/kotlin/pom.tpl
new file mode 100644
index 0000000..96c57b4
--- /dev/null
+++ b/dsl/kotlin/pom.tpl
@@ -0,0 +1,44 @@
+
+
+ 4.0.0
+
+ Player Language - {artifactId}
+ Kotlin DSL for building Player UI content
+ https://github.com/player-ui/language
+
+
+ Apache License, Version 2.0
+ https://www.apache.org/licenses/LICENSE-2.0.txt
+
+
+
+
+ rafbcampos
+ Rafael Campos
+
+
+ sugarmanz
+ Jeremiah Zucker
+
+
+ brocollie08
+ Tony Lin
+
+
+
+ https://github.com/player-ui/language.git
+ https://github.com/player-ui/language.git
+ v{version}
+ https://github.com/player-ui/language.git
+
+
+ {groupId}
+ {artifactId}
+ {version}
+ {type}
+
+
+{dependencies}
+
+
diff --git a/dsl/kotlin/src/main/kotlin/com/intuit/playerui/lang/dsl/FluentDslMarker.kt b/dsl/kotlin/src/main/kotlin/com/intuit/playerui/lang/dsl/FluentDslMarker.kt
new file mode 100644
index 0000000..b39b8a5
--- /dev/null
+++ b/dsl/kotlin/src/main/kotlin/com/intuit/playerui/lang/dsl/FluentDslMarker.kt
@@ -0,0 +1,11 @@
+package com.intuit.playerui.lang.dsl
+
+/**
+ * DSL marker to prevent scope leakage in nested builder blocks.
+ * This ensures that methods from outer builders are not accessible
+ * in nested builder scopes, preventing accidental property assignments
+ * to the wrong builder.
+ */
+@DslMarker
+@Target(AnnotationTarget.CLASS, AnnotationTarget.TYPE)
+annotation class FluentDslMarker
diff --git a/dsl/kotlin/src/main/kotlin/com/intuit/playerui/lang/dsl/core/AssetWrapperBuilder.kt b/dsl/kotlin/src/main/kotlin/com/intuit/playerui/lang/dsl/core/AssetWrapperBuilder.kt
new file mode 100644
index 0000000..f678d38
--- /dev/null
+++ b/dsl/kotlin/src/main/kotlin/com/intuit/playerui/lang/dsl/core/AssetWrapperBuilder.kt
@@ -0,0 +1,10 @@
+package com.intuit.playerui.lang.dsl.core
+
+/**
+ * Wrapper for builders that should be wrapped in AssetWrapper format.
+ * Used by generated builders to wrap nested assets.
+ */
+@JvmInline
+value class AssetWrapperBuilder(
+ val builder: FluentBuilder<*>,
+)
diff --git a/dsl/kotlin/src/main/kotlin/com/intuit/playerui/lang/dsl/core/AuxiliaryStorage.kt b/dsl/kotlin/src/main/kotlin/com/intuit/playerui/lang/dsl/core/AuxiliaryStorage.kt
new file mode 100644
index 0000000..b244b85
--- /dev/null
+++ b/dsl/kotlin/src/main/kotlin/com/intuit/playerui/lang/dsl/core/AuxiliaryStorage.kt
@@ -0,0 +1,121 @@
+package com.intuit.playerui.lang.dsl.core
+
+/**
+ * A type-safe key for AuxiliaryStorage.
+ * The type parameter T represents the type of value stored at this key.
+ */
+class TypedKey(
+ val name: String,
+)
+
+/**
+ * A type-safe key for list values in AuxiliaryStorage.
+ * The type parameter T represents the element type of the list.
+ */
+class TypedListKey(
+ val name: String,
+)
+
+/**
+ * Storage for auxiliary metadata like templates and switches.
+ * Uses typed keys to ensure type safety at call sites.
+ */
+class AuxiliaryStorage {
+ private val data = mutableMapOf()
+
+ /**
+ * Sets a value for a typed key, replacing any existing value.
+ */
+ operator fun set(
+ key: TypedKey,
+ value: T,
+ ) {
+ data[key.name] = value
+ }
+
+ /**
+ * Gets a value by typed key.
+ */
+ @Suppress("UNCHECKED_CAST")
+ fun get(key: TypedKey): T? = data[key.name] as? T
+
+ /**
+ * Pushes an item to a list at the given typed list key.
+ * Creates a new list if one doesn't exist.
+ */
+ @Suppress("UNCHECKED_CAST")
+ fun push(
+ key: TypedListKey,
+ item: T,
+ ) {
+ val existing = data[key.name]
+ if (existing is MutableList<*>) {
+ (existing as MutableList).add(item)
+ } else {
+ data[key.name] = mutableListOf(item)
+ }
+ }
+
+ /**
+ * Gets a list by typed list key, returning empty list if not found.
+ */
+ @Suppress("UNCHECKED_CAST")
+ fun getList(key: TypedListKey): List = data[key.name] as? List ?: emptyList()
+
+ /**
+ * Checks if a typed key exists.
+ */
+ fun has(key: TypedKey): Boolean = key.name in data
+
+ /**
+ * Checks if a typed list key exists.
+ */
+ fun has(key: TypedListKey): Boolean = key.name in data
+
+ /**
+ * Removes a value by typed key.
+ */
+ fun remove(key: TypedKey): Boolean = data.remove(key.name) != null
+
+ /**
+ * Removes a value by typed list key.
+ */
+ fun remove(key: TypedListKey): Boolean = data.remove(key.name) != null
+
+ /**
+ * Clears all stored data.
+ */
+ fun clear() = data.clear()
+
+ /**
+ * Creates a shallow clone of this storage.
+ */
+ fun clone(): AuxiliaryStorage =
+ AuxiliaryStorage().also { cloned ->
+ cloned.copyFrom(this)
+ }
+
+ /**
+ * Copies all data from another AuxiliaryStorage instance.
+ * Clears existing data before copying.
+ */
+ fun copyFrom(other: AuxiliaryStorage) {
+ data.clear()
+ other.data.forEach { (key, value) ->
+ data[key] =
+ if (value is MutableList<*>) {
+ value.toMutableList()
+ } else {
+ value
+ }
+ }
+ }
+
+ companion object {
+ /** Key for storing template configuration functions */
+ internal val TEMPLATES = TypedListKey<(BuildContext) -> TemplateConfig>("__templates__")
+
+ /** Key for storing switch metadata */
+ internal val SWITCHES = TypedListKey("__switches__")
+ }
+}
diff --git a/dsl/kotlin/src/main/kotlin/com/intuit/playerui/lang/dsl/core/BuildContext.kt b/dsl/kotlin/src/main/kotlin/com/intuit/playerui/lang/dsl/core/BuildContext.kt
new file mode 100644
index 0000000..21e48e0
--- /dev/null
+++ b/dsl/kotlin/src/main/kotlin/com/intuit/playerui/lang/dsl/core/BuildContext.kt
@@ -0,0 +1,78 @@
+package com.intuit.playerui.lang.dsl.core
+
+import com.intuit.playerui.lang.dsl.id.IdRegistry
+
+/**
+ * Represents the different branch types used for hierarchical ID generation.
+ * Each branch type determines how the ID segment is constructed.
+ */
+sealed interface IdBranch {
+ /**
+ * A named slot branch (e.g., "parent-label", "parent-values").
+ * Used for named properties like label, values, actions.
+ */
+ data class Slot(
+ val name: String,
+ ) : IdBranch
+
+ /**
+ * An array item branch with index (e.g., "parent-0", "parent-1").
+ * Used for items within array properties.
+ */
+ data class ArrayItem(
+ val index: Int,
+ ) : IdBranch
+
+ /**
+ * A template branch with optional depth for nested templates.
+ * depth=0 or null produces "_index_", depth=1 produces "_index1_", etc.
+ */
+ data class Template(
+ val depth: Int = 0,
+ ) : IdBranch
+
+ /**
+ * A switch branch for conditional asset selection.
+ * Generates IDs like "parent-staticSwitch-0" or "parent-dynamicSwitch-1".
+ */
+ data class Switch(
+ val index: Int,
+ val kind: SwitchKind,
+ ) : IdBranch {
+ enum class SwitchKind { STATIC, DYNAMIC }
+ }
+}
+
+/**
+ * Metadata about an asset being built, used for smart ID naming.
+ */
+data class AssetMetadata(
+ val type: String? = null,
+ val binding: String? = null,
+ val value: String? = null,
+)
+
+/**
+ * Context passed during the build process to generate hierarchical IDs
+ * and manage nested asset relationships.
+ */
+data class BuildContext(
+ val parentId: String = "",
+ val parameterName: String? = null,
+ val index: Int? = null,
+ val branch: IdBranch? = null,
+ val assetMetadata: AssetMetadata? = null,
+ val idRegistry: IdRegistry = IdRegistry(),
+) {
+ fun withParentId(id: String): BuildContext = copy(parentId = id)
+
+ fun withBranch(branch: IdBranch): BuildContext = copy(branch = branch)
+
+ fun withIndex(index: Int): BuildContext = copy(index = index)
+
+ fun withAssetMetadata(metadata: AssetMetadata): BuildContext = copy(assetMetadata = metadata)
+
+ fun withParameterName(name: String): BuildContext = copy(parameterName = name)
+
+ fun clearBranch(): BuildContext = copy(branch = null)
+}
diff --git a/dsl/kotlin/src/main/kotlin/com/intuit/playerui/lang/dsl/core/BuildPipeline.kt b/dsl/kotlin/src/main/kotlin/com/intuit/playerui/lang/dsl/core/BuildPipeline.kt
new file mode 100644
index 0000000..26ce288
--- /dev/null
+++ b/dsl/kotlin/src/main/kotlin/com/intuit/playerui/lang/dsl/core/BuildPipeline.kt
@@ -0,0 +1,620 @@
+package com.intuit.playerui.lang.dsl.core
+
+import com.intuit.playerui.lang.dsl.id.determineSlotName
+import com.intuit.playerui.lang.dsl.id.genId
+import com.intuit.playerui.lang.dsl.id.peekId
+import com.intuit.playerui.lang.dsl.tagged.TaggedValue
+
+/**
+ * The 9-step build pipeline for resolving builder properties into final JSON.
+ * Matches the TypeScript implementation's resolution order.
+ */
+object BuildPipeline {
+ /**
+ * Type-safe helper to cast Map to mutable string-keyed map.
+ *
+ * @receiver Any value that is known to be a Map
+ * @return The value cast to MutableMap
+ */
+ @Suppress("UNCHECKED_CAST")
+ private fun Any.asMutableStringMap(): MutableMap = this as MutableMap
+
+ /**
+ * Type-safe helper to cast Map to immutable string-keyed map.
+ *
+ * @receiver Any value that is known to be a Map
+ * @return The value cast to Map
+ */
+ @Suppress("UNCHECKED_CAST")
+ private fun Any.asStringMap(): Map = this as Map
+
+ /**
+ * Executes the full build pipeline.
+ *
+ * Steps:
+ * 1. Resolve static values (TaggedValue → string)
+ * 2. Generate asset ID
+ * 3. Create nested context for child assets
+ * 4. Resolve AssetWrapper values
+ * 5. Resolve mixed arrays (static + builder values)
+ * 6. Resolve builders
+ * 7. Resolve nested AssetWrapper paths
+ * 8. Resolve switches
+ * 9. Resolve templates
+ */
+ fun execute(
+ storage: ValueStorage,
+ auxiliary: AuxiliaryStorage,
+ defaults: Map,
+ context: BuildContext?,
+ arrayProperties: Set,
+ assetWrapperProperties: Set,
+ assetWrapperPaths: List> = emptyList(),
+ ): Map {
+ val result = mutableMapOf()
+
+ // Apply defaults first
+ defaults.forEach { (k, v) -> result[k] = v }
+
+ val allEntries = storage.getAll()
+
+ // Step 1: Resolve static values (Primitive and Tagged entries)
+ resolveStaticValues(allEntries, result)
+
+ // Step 2: Generate asset ID
+ generateAssetId(result, context)
+
+ // Step 3: Create nested context
+ val nestedContext = createNestedContext(result, context)
+
+ // Step 4: Resolve AssetWrapper values
+ resolveAssetWrappers(allEntries, result, nestedContext, assetWrapperProperties)
+
+ // Step 5 & 6: Resolve arrays with builders and direct builders
+ resolveBuilderEntries(allEntries, result, nestedContext, assetWrapperProperties)
+
+ // Step 7: Resolve nested AssetWrapper paths
+ resolveNestedAssetWrappers(result, nestedContext, assetWrapperPaths)
+
+ // Step 8: Resolve switches
+ resolveSwitches(auxiliary, result, nestedContext, arrayProperties)
+
+ // Step 9: Resolve templates
+ resolveTemplates(auxiliary, result, context)
+
+ return result
+ }
+
+ /**
+ * Step 1: Resolve Primitive and Tagged StoredValues to their raw representations.
+ */
+ private fun resolveStaticValues(
+ entries: Map,
+ result: MutableMap,
+ ) {
+ entries.forEach { (key, stored) ->
+ when (stored) {
+ is StoredValue.Primitive -> {
+ result[key] = stored.value
+ }
+
+ is StoredValue.Tagged -> {
+ result[key] = if (key == "binding" || key == "data") {
+ stored.value.toValue()
+ } else {
+ stored.value.toString()
+ }
+ }
+
+ // Builder/WrappedBuilder/ObjectValue/ArrayValue handled in later steps
+ else -> {}
+ }
+ }
+ }
+
+ /**
+ * Step 2: Generate a unique ID for the asset.
+ */
+ private fun generateAssetId(
+ result: MutableMap,
+ context: BuildContext?,
+ ) {
+ // If ID is already set explicitly (non-empty), use it
+ if ((result["id"] as? String)?.isNotEmpty() == true) return
+ if (context == null) return
+
+ val type = result["type"] as? String
+ // Not an asset (no type field) — skip ID generation
+ if (type == null) return
+ val binding = result["binding"] as? String
+ val value = result["value"] as? String
+ val assetMetadata = AssetMetadata(type, binding, value)
+ val slotName = determineSlotName(type, assetMetadata)
+
+ val generatedId =
+ when {
+ // Has branch (e.g., ArrayItem or Slot from parent context):
+ // First resolve the branch, then append the asset type
+ context.branch != null -> {
+ val baseId = genId(context)
+ genId(context.copy(parentId = baseId, branch = IdBranch.Slot(slotName)))
+ }
+
+ // Has parentId but no branch: append type as a Slot
+ context.parentId.isNotEmpty() -> {
+ genId(context.copy(branch = IdBranch.Slot(slotName)))
+ }
+
+ // Fallback
+ else -> {
+ slotName
+ }
+ }
+
+ if (generatedId.isNotEmpty()) {
+ result["id"] = generatedId
+ }
+ }
+
+ /**
+ * Step 3: Create a nested context for child assets.
+ */
+ private fun createNestedContext(
+ result: Map,
+ context: BuildContext?,
+ ): BuildContext? {
+ if (context == null) return null
+
+ val parentId = result["id"] as? String ?: context.parentId
+ return context
+ .withParentId(parentId)
+ .clearBranch()
+ }
+
+ /**
+ * Step 4: Resolve AssetWrapper values.
+ * Wraps builders in { asset: ... } structure.
+ */
+ private fun resolveAssetWrappers(
+ entries: Map,
+ result: MutableMap,
+ context: BuildContext?,
+ assetWrapperProperties: Set,
+ ) {
+ entries.forEach { (key, stored) ->
+ if (key in assetWrapperProperties && stored is StoredValue.WrappedBuilder) {
+ val slotContext = createSlotContext(context, key)
+ val builtAsset = stored.builder.build(slotContext)
+ result[key] = mapOf("asset" to builtAsset)
+ }
+ }
+ }
+
+ /**
+ * Steps 5 & 6: Resolve builder entries (Builder, ArrayValue with builders, ObjectValue with builders).
+ */
+ private fun resolveBuilderEntries(
+ entries: Map,
+ result: MutableMap,
+ context: BuildContext?,
+ assetWrapperProperties: Set,
+ ) {
+ entries.forEach { (key, stored) ->
+ // Skip WrappedBuilder in assetWrapperProperties (handled in step 4)
+ if (key in assetWrapperProperties && stored is StoredValue.WrappedBuilder) return@forEach
+
+ when (stored) {
+ is StoredValue.Builder -> {
+ val slotContext = createSlotContext(context, key)
+ result[key] = stored.builder.build(slotContext)
+ }
+
+ is StoredValue.WrappedBuilder -> {
+ // Non-asset-wrapper WrappedBuilder: build it directly
+ val slotContext = createSlotContext(context, key)
+ result[key] = stored.builder.build(slotContext)
+ }
+
+ is StoredValue.ArrayValue -> {
+ val isAssetWrapperArray = key in assetWrapperProperties
+ result[key] = resolveArrayValue(stored.items, context, key, isAssetWrapperArray)
+ }
+
+ is StoredValue.ObjectValue -> {
+ result[key] = resolveObjectValue(stored.map, context, key)
+ }
+
+ // Primitive and Tagged already handled in step 1
+ is StoredValue.Primitive, is StoredValue.Tagged -> {}
+ }
+ }
+ }
+
+ /**
+ * Resolves an array of StoredValues.
+ * When [wrapInAssetWrapper] is true, each builder element is wrapped in { asset: ... } format.
+ */
+ private fun resolveArrayValue(
+ items: List,
+ context: BuildContext?,
+ key: String,
+ wrapInAssetWrapper: Boolean = false,
+ ): List =
+ items.mapIndexedNotNull { index, stored ->
+ when (stored) {
+ is StoredValue.Primitive -> {
+ stored.value
+ }
+
+ is StoredValue.Tagged -> {
+ stored.value.toString()
+ }
+
+ is StoredValue.Builder -> {
+ val arrayContext = createArrayItemContext(context, key, index)
+ val built = stored.builder.build(arrayContext)
+ if (wrapInAssetWrapper) mapOf("asset" to built) else built
+ }
+
+ is StoredValue.WrappedBuilder -> {
+ val arrayContext = createArrayItemContext(context, key, index)
+ val built = stored.builder.build(arrayContext)
+ mapOf("asset" to built)
+ }
+
+ is StoredValue.ArrayValue -> {
+ resolveArrayValue(stored.items, context, key, wrapInAssetWrapper)
+ }
+
+ is StoredValue.ObjectValue -> {
+ resolveObjectValue(stored.map, context, key)
+ }
+ }
+ }
+
+ /**
+ * Resolves an object map of StoredValues.
+ */
+ private fun resolveObjectValue(
+ map: Map,
+ context: BuildContext?,
+ key: String,
+ ): Map =
+ map.mapValues { (_, stored) ->
+ when (stored) {
+ is StoredValue.Primitive -> {
+ stored.value
+ }
+
+ is StoredValue.Tagged -> {
+ stored.value.toString()
+ }
+
+ is StoredValue.Builder -> {
+ val slotContext = createSlotContext(context, key)
+ stored.builder.build(slotContext)
+ }
+
+ is StoredValue.WrappedBuilder -> {
+ val slotContext = createSlotContext(context, key)
+ stored.builder.build(slotContext)
+ }
+
+ is StoredValue.ArrayValue -> {
+ resolveArrayValue(stored.items, context, key)
+ }
+
+ is StoredValue.ObjectValue -> {
+ resolveObjectValue(stored.map, context, key)
+ }
+ }
+ }
+
+ /**
+ * Step 7: Resolve nested AssetWrapper paths.
+ * Handles AssetWrapper properties nested within intermediate objects.
+ */
+ private fun resolveNestedAssetWrappers(
+ result: MutableMap,
+ context: BuildContext?,
+ assetWrapperPaths: List>,
+ ) {
+ if (assetWrapperPaths.isEmpty()) return
+
+ for (path in assetWrapperPaths) {
+ if (path.size < 2) continue
+ resolveNestedPath(result, path, context)
+ }
+ }
+
+ /**
+ * Resolves a specific nested path, wrapping the target value in AssetWrapper format.
+ */
+ private fun resolveNestedPath(
+ result: MutableMap,
+ path: List,
+ context: BuildContext?,
+ ) {
+ // Navigate to the parent object containing the AssetWrapper property
+ var current: Any? = result
+
+ for (i in 0 until path.size - 1) {
+ val key = path[i]
+ if (current !is Map<*, *>) return
+
+ var next: Any? = current.asStringMap()[key] ?: return
+
+ // If intermediate value is a builder, resolve it first
+ if (next is FluentBuilder<*>) {
+ val slotContext = createSlotContext(context, key)
+ next = next.build(slotContext)
+ current.asMutableStringMap()[key] = next
+ }
+
+ current = next
+ }
+
+ // Now `current` is the parent object, wrap the final property
+ val finalKey = path.last()
+ if (current !is MutableMap<*, *>) return
+
+ val parent = current.asMutableStringMap()
+ val value = parent[finalKey] ?: return
+
+ // If it's already wrapped in { asset: ... }, skip
+ if (value is Map<*, *> && value.containsKey("asset")) return
+
+ val slotName = path.joinToString("-")
+
+ // Handle arrays of values that need wrapping
+ if (value is List<*>) {
+ parent[finalKey] =
+ value
+ .filterNotNull()
+ .mapIndexed { index, item ->
+ if (item is Map<*, *> && item.containsKey("asset")) {
+ item
+ } else {
+ wrapAssetValue(item, context, "$slotName-$index")
+ }
+ }
+ return
+ }
+
+ // Handle single value that needs wrapping
+ if (value is FluentBuilder<*> || value is Map<*, *>) {
+ parent[finalKey] = wrapAssetValue(value, context, slotName)
+ }
+ }
+
+ /**
+ * Wraps a value in AssetWrapper format: { asset: { id: ..., ...value } }
+ */
+ private fun wrapAssetValue(
+ value: Any?,
+ context: BuildContext?,
+ slotName: String,
+ ): Map {
+ val resolved =
+ when (value) {
+ is FluentBuilder<*> -> {
+ val slotContext = context?.withBranch(IdBranch.Slot(slotName))
+ value.build(slotContext)
+ }
+
+ is Map<*, *> -> {
+ value.asStringMap()
+ }
+
+ else -> {
+ return mapOf("asset" to value)
+ }
+ }
+ return mapOf("asset" to resolved)
+ }
+
+ /**
+ * Step 8: Resolve switch configurations.
+ */
+ @Suppress("NestedBlockDepth")
+ private fun resolveSwitches(
+ auxiliary: AuxiliaryStorage,
+ result: MutableMap,
+ context: BuildContext?,
+ arrayProperties: Set,
+ ) {
+ val switches = auxiliary.getList(AuxiliaryStorage.SWITCHES)
+ if (switches.isEmpty()) return
+
+ var globalCaseIndex = 0
+ switches.forEach { switchMeta ->
+ val (path, args) = switchMeta
+ val switchKey = if (args.isDynamic) "dynamicSwitch" else "staticSwitch"
+
+ val propertyName = path.firstOrNull()?.toString() ?: ""
+ val switchParentId = if (context?.parentId?.isNotEmpty() == true) {
+ "${context.parentId}-$propertyName"
+ } else {
+ propertyName
+ }
+ val switchContext = context?.withParentId(switchParentId)?.clearBranch()
+
+ val resolvedCases =
+ args.cases.mapIndexed { index, case ->
+ val caseContext =
+ switchContext?.withBranch(
+ IdBranch.Switch(
+ index = globalCaseIndex + index,
+ kind =
+ if (args.isDynamic) {
+ IdBranch.Switch.SwitchKind.DYNAMIC
+ } else {
+ IdBranch.Switch.SwitchKind.STATIC
+ },
+ ),
+ )
+
+ val caseValue: Any =
+ when (val c = case.condition) {
+ is SwitchCondition.Static -> c.value
+ is SwitchCondition.Dynamic -> c.expr.toString()
+ }
+
+ val assetValue = case.asset.build(caseContext)
+
+ mapOf(
+ "case" to caseValue,
+ "asset" to assetValue,
+ )
+ }
+
+ // If this property is an array type (e.g., actions: Array>),
+ // wrap the switch result in an array to match the expected schema type.
+ // Only wrap if we're replacing the entire property (path.size == 1),
+ // not a specific element in the array (path.size > 1)
+ var switchResult: Any = mapOf(switchKey to resolvedCases)
+
+ if (propertyName in arrayProperties && path.size == 1) {
+ switchResult = listOf(switchResult)
+ }
+
+ // Inject switch at the specified path
+ injectAtPath(result, path, switchResult)
+
+ globalCaseIndex += args.cases.size
+ }
+ }
+
+ /**
+ * Step 9: Resolve template configurations.
+ */
+ private fun resolveTemplates(
+ auxiliary: AuxiliaryStorage,
+ result: MutableMap,
+ context: BuildContext?,
+ ) {
+ val templates = auxiliary.getList(AuxiliaryStorage.TEMPLATES)
+ if (templates.isEmpty()) return
+
+ val resolvedTemplates = templates.map { templateFn ->
+ val templateContext = context ?: BuildContext()
+ val config = templateFn(templateContext)
+
+ val templateDepth = extractTemplateDepth(context)
+ val newParentId = genId(templateContext)
+ val valueContext = templateContext.copy(parentId = newParentId, branch = IdBranch.Template(templateDepth))
+
+ val resolvedValue =
+ when (val v = config.value) {
+ is FluentBuilder<*> -> mapOf("asset" to v.build(valueContext))
+ else -> resolveValue(v)
+ }
+
+ buildMap {
+ put("data", config.data)
+ put("output", config.output)
+ put("value", resolvedValue)
+ if (config.dynamic) put("dynamic", true)
+ }
+ }
+ result["template"] = resolvedTemplates
+ }
+
+ /**
+ * Recursively resolves values, converting TaggedValues to strings.
+ */
+ private fun resolveValue(value: Any?): Any? =
+ when (value) {
+ null -> null
+ is TaggedValue<*> -> value.toString()
+ is Map<*, *> -> value.mapValues { (_, v) -> resolveValue(v) }
+ is List<*> -> value.map { resolveValue(it) }
+ else -> value
+ }
+
+ /**
+ * Creates a context for a slot (named property).
+ */
+ private fun createSlotContext(
+ context: BuildContext?,
+ key: String,
+ ): BuildContext? {
+ if (context == null) return null
+
+ return context
+ .withBranch(IdBranch.Slot(key))
+ .withParameterName(key)
+ }
+
+ /**
+ * Creates a context for an array item.
+ */
+ private fun createArrayItemContext(
+ context: BuildContext?,
+ key: String,
+ index: Int,
+ ): BuildContext? {
+ if (context == null) return null
+
+ // Use peekId (not genId) to avoid registering intermediate ID for each array item
+ val intermediateId = peekId(context.withBranch(IdBranch.Slot(key)))
+
+ return context
+ .withParentId(intermediateId)
+ .withBranch(IdBranch.ArrayItem(index))
+ .withParameterName(key)
+ .withIndex(index)
+ }
+
+ /**
+ * Injects a value at a nested path in the result map.
+ */
+ private fun injectAtPath(
+ result: MutableMap,
+ path: List,
+ value: Any?,
+ ) {
+ if (path.isEmpty()) return
+
+ var current: Any? = result
+ val lastIndex = path.size - 1
+
+ path.forEachIndexed { index, segment ->
+ if (index == lastIndex) {
+ when {
+ current is MutableMap<*, *> && segment is String -> {
+ val map = current.asMutableStringMap()
+ map[segment] = value
+ }
+ }
+ } else {
+ current =
+ when {
+ current is Map<*, *> && segment is String -> {
+ current.asStringMap()[segment]
+ }
+
+ current is List<*> && segment is Int -> {
+ current.getOrNull(segment)
+ }
+
+ // Intentional: when path segment type mismatches the container
+ // (e.g., string key on a list, or int key on a map), we set current to null.
+ // Subsequent segments then become no-ops. This is by-design defensive behavior
+ // for switch/template injection where the target path may not yet exist.
+ else -> {
+ null
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Extracts the current template depth from context.
+ */
+ private fun extractTemplateDepth(context: BuildContext?): Int {
+ val branch = context?.branch
+ return if (branch is IdBranch.Template) branch.depth + 1 else 0
+ }
+}
diff --git a/dsl/kotlin/src/main/kotlin/com/intuit/playerui/lang/dsl/core/FluentBuilder.kt b/dsl/kotlin/src/main/kotlin/com/intuit/playerui/lang/dsl/core/FluentBuilder.kt
new file mode 100644
index 0000000..567e274
--- /dev/null
+++ b/dsl/kotlin/src/main/kotlin/com/intuit/playerui/lang/dsl/core/FluentBuilder.kt
@@ -0,0 +1,232 @@
+package com.intuit.playerui.lang.dsl.core
+
+import com.intuit.playerui.lang.dsl.FluentDslMarker
+import com.intuit.playerui.lang.dsl.tagged.Binding
+import com.intuit.playerui.lang.dsl.tagged.Expression
+
+/**
+ * Base interface for all fluent builders.
+ * Defines the core contract for building Player-UI assets.
+ */
+interface FluentBuilder {
+ /**
+ * Builds the final asset/object from the configured properties.
+ * @param context Optional build context for ID generation and nesting
+ * @return The built object as a Map (JSON-serializable)
+ */
+ fun build(context: BuildContext? = null): Map
+
+ /**
+ * Peeks at a property value without triggering resolution.
+ * @param key The property name
+ * @return The raw value or null if not set
+ */
+ fun peek(key: String): Any?
+
+ /**
+ * Checks if a property has been set.
+ * @param key The property name
+ * @return True if the property has a value
+ */
+ fun has(key: String): Boolean
+}
+
+/**
+ * Abstract base class for fluent builders.
+ * Provides common functionality for property storage, conditional building,
+ * and the build pipeline.
+ */
+@FluentDslMarker
+abstract class FluentBuilderBase : FluentBuilder {
+ protected val storage = ValueStorage()
+ protected val auxiliary = AuxiliaryStorage()
+
+ /**
+ * Default values for properties. Subclasses should override this
+ * to provide type-specific defaults (e.g., { "type" to "text" }).
+ */
+ protected abstract val defaults: Map
+
+ /**
+ * Properties that are arrays and should be merged rather than replaced.
+ * Used by the build pipeline for proper array handling.
+ */
+ protected open val arrayProperties: Set = emptySet()
+
+ /**
+ * Properties that wrap assets (AssetWrapper). Used to auto-wrap
+ * builder values in { asset: ... } structure.
+ */
+ protected open val assetWrapperProperties: Set = emptySet()
+
+ /**
+ * Paths to AssetWrapper properties nested within intermediate objects.
+ * Each path is a list of property names (e.g., ["header", "left"]).
+ * Paths with length >= 2 are resolved by the nested AssetWrapper step.
+ */
+ protected open val assetWrapperPaths: List> = emptyList()
+
+ /**
+ * Conditionally show/hide this asset based on an expression or binding.
+ * When set, the asset will only be rendered when the expression evaluates to true.
+ */
+ var applicability: Any?
+ get() = storage.peek("applicability")
+ set(value) {
+ storage["applicability"] = value
+ }
+
+ /**
+ * Sets applicability using a boolean expression.
+ */
+ fun applicability(expr: Expression) {
+ storage["applicability"] = expr
+ }
+
+ /**
+ * Sets applicability using a boolean binding.
+ */
+ fun applicability(binding: Binding) {
+ storage["applicability"] = binding
+ }
+
+ /**
+ * Sets a property value.
+ * @param key The property name
+ * @param value The value to set
+ * @return This builder for chaining
+ */
+ protected fun set(
+ key: String,
+ value: Any?,
+ ): FluentBuilderBase {
+ storage[key] = value
+ return this
+ }
+
+ /**
+ * Conditionally sets a property if the predicate is true.
+ */
+ fun setIf(
+ predicate: () -> Boolean,
+ property: String,
+ value: Any?,
+ ): FluentBuilderBase {
+ if (predicate()) {
+ val wrapped = maybeWrapAsset(property, value)
+ set(property, wrapped)
+ }
+ return this
+ }
+
+ /**
+ * Conditionally sets a property to one of two values based on the predicate.
+ */
+ fun setIfElse(
+ predicate: () -> Boolean,
+ property: String,
+ trueValue: Any?,
+ falseValue: Any?,
+ ): FluentBuilderBase {
+ val valueToUse = if (predicate()) trueValue else falseValue
+ val wrapped = maybeWrapAsset(property, valueToUse)
+ set(property, wrapped)
+ return this
+ }
+
+ override fun has(key: String): Boolean = storage.has(key)
+
+ override fun peek(key: String): Any? = storage.peek(key)
+
+ /**
+ * Removes a property value.
+ */
+ fun unset(key: String): FluentBuilderBase {
+ storage.remove(key)
+ return this
+ }
+
+ /**
+ * Clears all property values, resetting the builder.
+ */
+ fun clear(): FluentBuilderBase {
+ storage.clear()
+ auxiliary.clear()
+ return this
+ }
+
+ /**
+ * Creates a copy of this builder with the same property values.
+ */
+ abstract fun clone(): FluentBuilderBase
+
+ /**
+ * Copies storage state to another builder (used by clone implementations).
+ */
+ protected fun cloneStorageTo(target: FluentBuilderBase) {
+ val clonedStorage = storage.clone()
+ target.storage.clear()
+ clonedStorage.getAll().forEach { (k, stored) -> target.storage[k] = stored.toRawValue() }
+ target.auxiliary.copyFrom(auxiliary)
+ }
+
+ /**
+ * Adds a template configuration for dynamic list generation.
+ */
+ fun template(templateFn: (BuildContext) -> TemplateConfig): FluentBuilderBase {
+ auxiliary.push(AuxiliaryStorage.TEMPLATES, templateFn)
+ return this
+ }
+
+ /**
+ * Adds a switch configuration for runtime conditional selection.
+ */
+ fun switch(
+ path: List,
+ args: SwitchArgs,
+ ): FluentBuilderBase {
+ auxiliary.push(AuxiliaryStorage.SWITCHES, SwitchMetadata(path, args))
+ return this
+ }
+
+ /**
+ * Wraps a builder in AssetWrapper format if the property requires it.
+ */
+ private fun maybeWrapAsset(
+ property: String,
+ value: Any?,
+ ): Any? {
+ if (value == null) return null
+ if (property !in assetWrapperProperties) return value
+
+ return when (value) {
+ is FluentBuilder<*> -> {
+ AssetWrapperBuilder(value)
+ }
+
+ is List<*> -> {
+ value.map { item ->
+ if (item is FluentBuilder<*>) AssetWrapperBuilder(item) else item
+ }
+ }
+
+ else -> {
+ value
+ }
+ }
+ }
+
+ /**
+ * Executes the build pipeline with defaults.
+ */
+ protected fun buildWithDefaults(context: BuildContext?): Map =
+ BuildPipeline.execute(
+ storage = storage,
+ auxiliary = auxiliary,
+ defaults = defaults,
+ context = context,
+ arrayProperties = arrayProperties,
+ assetWrapperProperties = assetWrapperProperties,
+ assetWrapperPaths = assetWrapperPaths,
+ )
+}
diff --git a/dsl/kotlin/src/main/kotlin/com/intuit/playerui/lang/dsl/core/StoredValue.kt b/dsl/kotlin/src/main/kotlin/com/intuit/playerui/lang/dsl/core/StoredValue.kt
new file mode 100644
index 0000000..32a494d
--- /dev/null
+++ b/dsl/kotlin/src/main/kotlin/com/intuit/playerui/lang/dsl/core/StoredValue.kt
@@ -0,0 +1,203 @@
+package com.intuit.playerui.lang.dsl.core
+
+import com.intuit.playerui.lang.dsl.tagged.TaggedValue
+
+/**
+ * Sealed class representing all possible value types that can be stored in a builder.
+ * This provides compile-time type safety and exhaustive pattern matching.
+ */
+sealed interface StoredValue {
+ /**
+ * A primitive JSON value (string, number, boolean, null).
+ */
+ data class Primitive(
+ val value: Any?,
+ ) : StoredValue
+
+ /**
+ * A tagged value (binding or expression).
+ */
+ data class Tagged(
+ val value: TaggedValue<*>,
+ ) : StoredValue
+
+ /**
+ * A nested builder instance.
+ */
+ data class Builder(
+ val builder: FluentBuilder<*>,
+ ) : StoredValue
+
+ /**
+ * A builder wrapped for AssetWrapper properties.
+ */
+ data class WrappedBuilder(
+ val builder: FluentBuilder<*>,
+ ) : StoredValue
+
+ /**
+ * A map (object) that may contain nested builders.
+ */
+ data class ObjectValue(
+ val map: Map,
+ ) : StoredValue
+
+ /**
+ * An array that may contain mixed values.
+ */
+ data class ArrayValue(
+ val items: List,
+ ) : StoredValue
+}
+
+/**
+ * Maximum recursion depth for deep copy to prevent stack overflow.
+ */
+private const val MAX_COPY_DEPTH = 100
+
+/**
+ * Creates a deep copy of this StoredValue, ensuring mutable containers are not shared.
+ * @throws IllegalStateException if maximum copy depth is exceeded
+ */
+fun StoredValue.deepCopy(): StoredValue = deepCopyImpl(0)
+
+/**
+ * Internal implementation of deep copy with depth tracking.
+ */
+private fun StoredValue.deepCopyImpl(depth: Int): StoredValue {
+ if (depth > MAX_COPY_DEPTH) {
+ error(
+ "Deep copy exceeded maximum depth of $MAX_COPY_DEPTH - " +
+ "possible circular reference or excessively deep structure",
+ )
+ }
+
+ return when (this) {
+ is StoredValue.Primitive -> StoredValue.Primitive(value)
+ is StoredValue.Tagged -> StoredValue.Tagged(value)
+ is StoredValue.Builder -> StoredValue.Builder(builder)
+ is StoredValue.WrappedBuilder -> StoredValue.WrappedBuilder(builder)
+ is StoredValue.ObjectValue -> StoredValue.ObjectValue(map.mapValues { (_, v) -> v.deepCopyImpl(depth + 1) })
+ is StoredValue.ArrayValue -> StoredValue.ArrayValue(items.map { it.deepCopyImpl(depth + 1) })
+ }
+}
+
+/**
+ * Converts a raw value to a StoredValue with proper type classification.
+ */
+fun toStoredValue(value: Any?): StoredValue =
+ when (value) {
+ null -> {
+ StoredValue.Primitive(null)
+ }
+
+ is TaggedValue<*> -> {
+ StoredValue.Tagged(value)
+ }
+
+ is FluentBuilder<*> -> {
+ StoredValue.Builder(value)
+ }
+
+ is AssetWrapperBuilder -> {
+ StoredValue.WrappedBuilder(value.builder)
+ }
+
+ is Map<*, *> -> {
+ try {
+ // Validate all keys are strings before casting
+ if (value.keys.all { it is String }) {
+ // Safe: validated all keys are strings
+ @Suppress("UNCHECKED_CAST")
+ val map = value as Map
+ if (map.values.any { containsBuilder(it) }) {
+ StoredValue.ObjectValue(map.mapValues { (_, v) -> toStoredValue(v) })
+ } else {
+ StoredValue.Primitive(value)
+ }
+ } else {
+ // Fallback: treat as primitive if keys aren't all strings
+ StoredValue.Primitive(value)
+ }
+ } catch (e: Exception) {
+ // If any error occurs during map processing, treat as primitive
+ StoredValue.Primitive(value)
+ }
+ }
+
+ is List<*> -> {
+ try {
+ if (value.any { containsBuilder(it) }) {
+ StoredValue.ArrayValue(value.map { toStoredValue(it) })
+ } else {
+ StoredValue.Primitive(value)
+ }
+ } catch (e: Exception) {
+ // If any error occurs during list processing, treat as primitive
+ StoredValue.Primitive(value)
+ }
+ }
+
+ else -> {
+ StoredValue.Primitive(value)
+ }
+ }
+
+/**
+ * Converts a StoredValue back to a raw value (for JSON serialization).
+ */
+fun StoredValue.toRawValue(): Any? =
+ when (this) {
+ is StoredValue.Primitive -> value
+
+ is StoredValue.Tagged -> value.toString()
+
+ is StoredValue.Builder -> builder
+
+ // Will be resolved during build
+ is StoredValue.WrappedBuilder -> builder
+
+ // Will be wrapped during build
+ is StoredValue.ObjectValue -> map.mapValues { (_, v) -> v.toRawValue() }
+
+ is StoredValue.ArrayValue -> items.map { it.toRawValue() }
+ }
+
+/**
+ * Checks if a StoredValue contains any builders that need resolution.
+ */
+fun StoredValue.hasBuilders(): Boolean =
+ when (this) {
+ is StoredValue.Primitive, is StoredValue.Tagged -> false
+ is StoredValue.Builder, is StoredValue.WrappedBuilder -> true
+ is StoredValue.ObjectValue -> map.values.any { it.hasBuilders() }
+ is StoredValue.ArrayValue -> items.any { it.hasBuilders() }
+ }
+
+/**
+ * Checks if a raw value contains any builders.
+ */
+private fun containsBuilder(value: Any?): Boolean =
+ when (value) {
+ null -> false
+ is FluentBuilder<*> -> true
+ is AssetWrapperBuilder -> true
+ is Map<*, *> -> value.values.any { containsBuilder(it) }
+ is List<*> -> value.any { containsBuilder(it) }
+ else -> false
+ }
+
+/**
+ * Type alias for JSON-compatible values (the output of build()).
+ */
+typealias JsonValue = Any?
+
+/**
+ * Type alias for JSON objects.
+ */
+typealias JsonObject = Map
+
+/**
+ * Type alias for JSON arrays.
+ */
+typealias JsonArray = List
diff --git a/dsl/kotlin/src/main/kotlin/com/intuit/playerui/lang/dsl/core/SwitchSupport.kt b/dsl/kotlin/src/main/kotlin/com/intuit/playerui/lang/dsl/core/SwitchSupport.kt
new file mode 100644
index 0000000..43f1d2a
--- /dev/null
+++ b/dsl/kotlin/src/main/kotlin/com/intuit/playerui/lang/dsl/core/SwitchSupport.kt
@@ -0,0 +1,83 @@
+package com.intuit.playerui.lang.dsl.core
+
+import com.intuit.playerui.lang.dsl.FluentDslMarker
+import com.intuit.playerui.lang.dsl.tagged.Expression
+
+/**
+ * Typed condition for switch cases.
+ */
+sealed interface SwitchCondition {
+ data class Static(
+ val value: Boolean,
+ ) : SwitchCondition
+
+ data class Dynamic(
+ val expr: Expression,
+ ) : SwitchCondition
+}
+
+/**
+ * Arguments for switch configuration.
+ */
+data class SwitchArgs(
+ val cases: List,
+ val isDynamic: Boolean = false,
+)
+
+/**
+ * A single case in a switch configuration.
+ */
+data class SwitchCase(
+ val condition: SwitchCondition,
+ val asset: FluentBuilder<*>,
+)
+
+/**
+ * Internal metadata for switch configurations.
+ */
+internal data class SwitchMetadata(
+ val path: List,
+ val args: SwitchArgs,
+)
+
+/**
+ * Helper builder for constructing switch cases.
+ */
+@FluentDslMarker
+class SwitchBuilder {
+ internal val cases = mutableListOf()
+
+ /**
+ * Adds a case with a boolean condition.
+ */
+ fun case(
+ condition: Boolean,
+ asset: FluentBuilder<*>,
+ ) {
+ cases.add(SwitchCase(SwitchCondition.Static(condition), asset))
+ }
+
+ /**
+ * Adds a case with an expression condition.
+ */
+ fun case(
+ condition: Expression,
+ asset: FluentBuilder<*>,
+ ) {
+ cases.add(SwitchCase(SwitchCondition.Dynamic(condition), asset))
+ }
+
+ /**
+ * Adds a default case (always true).
+ */
+ fun default(asset: FluentBuilder<*>) {
+ cases.add(SwitchCase(SwitchCondition.Static(true), asset))
+ }
+
+ /**
+ * Adds a default case using a DSL block.
+ */
+ fun default(init: () -> FluentBuilder<*>) {
+ cases.add(SwitchCase(SwitchCondition.Static(true), init()))
+ }
+}
diff --git a/dsl/kotlin/src/main/kotlin/com/intuit/playerui/lang/dsl/core/TemplateConfig.kt b/dsl/kotlin/src/main/kotlin/com/intuit/playerui/lang/dsl/core/TemplateConfig.kt
new file mode 100644
index 0000000..3946f87
--- /dev/null
+++ b/dsl/kotlin/src/main/kotlin/com/intuit/playerui/lang/dsl/core/TemplateConfig.kt
@@ -0,0 +1,11 @@
+package com.intuit.playerui.lang.dsl.core
+
+/**
+ * Configuration for a template (dynamic list generation).
+ */
+data class TemplateConfig(
+ val data: String,
+ val output: String,
+ val value: Any,
+ val dynamic: Boolean = false,
+)
diff --git a/dsl/kotlin/src/main/kotlin/com/intuit/playerui/lang/dsl/core/ValueStorage.kt b/dsl/kotlin/src/main/kotlin/com/intuit/playerui/lang/dsl/core/ValueStorage.kt
new file mode 100644
index 0000000..057b93a
--- /dev/null
+++ b/dsl/kotlin/src/main/kotlin/com/intuit/playerui/lang/dsl/core/ValueStorage.kt
@@ -0,0 +1,65 @@
+package com.intuit.playerui.lang.dsl.core
+
+/**
+ * Storage for builder property values using type-safe [StoredValue] representation.
+ * Provides intelligent routing and retrieval based on value category.
+ */
+class ValueStorage {
+ private val entries = mutableMapOf()
+
+ /**
+ * Sets a value with automatic type classification via [toStoredValue].
+ */
+ operator fun set(
+ key: String,
+ value: Any?,
+ ) {
+ if (value == null) {
+ entries[key] = StoredValue.Primitive(null)
+ } else {
+ entries[key] = toStoredValue(value)
+ }
+ }
+
+ /**
+ * Gets the raw value for a key (resolves StoredValue back to raw form).
+ */
+ operator fun get(key: String): Any? = peek(key)
+
+ /**
+ * Checks if a key has any value.
+ */
+ fun has(key: String): Boolean = key in entries
+
+ /**
+ * Peeks at a value without resolution (returns raw form).
+ */
+ fun peek(key: String): Any? = entries[key]?.toRawValue()
+
+ /**
+ * Removes a value.
+ */
+ fun remove(key: String) {
+ entries.remove(key)
+ }
+
+ /**
+ * Clears all stored values.
+ */
+ fun clear() {
+ entries.clear()
+ }
+
+ /**
+ * Returns all stored values as a map of StoredValue.
+ */
+ internal fun getAll(): Map = entries.toMap()
+
+ /**
+ * Creates a deep clone of this storage.
+ */
+ fun clone(): ValueStorage =
+ ValueStorage().also { cloned ->
+ entries.forEach { (k, v) -> cloned.entries[k] = v.deepCopy() }
+ }
+}
diff --git a/dsl/kotlin/src/main/kotlin/com/intuit/playerui/lang/dsl/flow/Flow.kt b/dsl/kotlin/src/main/kotlin/com/intuit/playerui/lang/dsl/flow/Flow.kt
new file mode 100644
index 0000000..f25846a
--- /dev/null
+++ b/dsl/kotlin/src/main/kotlin/com/intuit/playerui/lang/dsl/flow/Flow.kt
@@ -0,0 +1,134 @@
+package com.intuit.playerui.lang.dsl.flow
+
+import com.intuit.playerui.lang.dsl.FluentDslMarker
+import com.intuit.playerui.lang.dsl.core.BuildContext
+import com.intuit.playerui.lang.dsl.core.FluentBuilder
+import com.intuit.playerui.lang.dsl.core.IdBranch
+import com.intuit.playerui.lang.dsl.id.IdRegistry
+
+/**
+ * Options for creating a Player-UI Flow.
+ * This is a builder-style class for constructing flows with views, data, and navigation.
+ */
+@FluentDslMarker
+class FlowBuilder {
+ var id: String = "root"
+ var views: List = emptyList()
+ var data: Map? = null
+ var schema: Map? = null
+ var navigation: Map = emptyMap()
+
+ /**
+ * Additional properties to include in the flow output.
+ */
+ private val additionalProperties = mutableMapOf()
+
+ /**
+ * Sets an additional property on the flow.
+ * @throws IllegalArgumentException if the key is a reserved flow property
+ */
+ fun set(
+ key: String,
+ value: Any?,
+ ) {
+ require(key !in RESERVED_KEYS) { "Cannot override reserved key '$key'. Use the dedicated property instead." }
+ additionalProperties[key] = value
+ }
+
+ companion object {
+ private val RESERVED_KEYS = setOf("id", "views", "navigation", "data", "schema")
+ }
+
+ /**
+ * Builds the flow, processing all views with proper context.
+ * Creates a fresh [IdRegistry] per build for thread-safe ID generation.
+ */
+ fun build(): Map {
+ val registry = IdRegistry()
+
+ val flowId = id
+ val viewsNamespace = "$flowId-views"
+
+ val processedViews =
+ views.mapIndexed { index, viewOrBuilder ->
+ val ctx =
+ BuildContext(
+ parentId = viewsNamespace,
+ branch = IdBranch.ArrayItem(index),
+ idRegistry = registry,
+ )
+
+ when (viewOrBuilder) {
+ is FluentBuilder<*> -> viewOrBuilder.build(ctx)
+ is Map<*, *> -> viewOrBuilder
+ else -> viewOrBuilder
+ }
+ }
+
+ val result =
+ mutableMapOf(
+ "id" to flowId,
+ "views" to processedViews,
+ "navigation" to navigation,
+ )
+
+ data?.let { result["data"] = it }
+ schema?.let { result["schema"] = it }
+ result.putAll(additionalProperties)
+
+ return result
+ }
+}
+
+/**
+ * DSL function to create a Player-UI Flow.
+ *
+ * A flow combines views, data, and navigation into a complete Player-UI content structure.
+ *
+ * Example:
+ * ```kotlin
+ * val myFlow = flow {
+ * id = "welcome-flow"
+ * views = listOf(
+ * collection {
+ * label { value = "Welcome" }
+ * values(
+ * text { value = "Hello World" },
+ * input { binding("user.name") }
+ * )
+ * actions(
+ * action { value = "next"; label { value = "Continue" } }
+ * )
+ * }
+ * )
+ * data = mapOf(
+ * "user" to mapOf("name" to "")
+ * )
+ * navigation = mapOf(
+ * "BEGIN" to "FLOW_1",
+ * "FLOW_1" to mapOf(
+ * "startState" to "VIEW_welcome",
+ * "VIEW_welcome" to mapOf(
+ * "state_type" to "VIEW",
+ * "ref" to "welcome-flow-views-0",
+ * "transitions" to mapOf("next" to "END_Done")
+ * ),
+ * "END_Done" to mapOf(
+ * "state_type" to "END",
+ * "outcome" to "done"
+ * )
+ * )
+ * )
+ * }
+ * ```
+ *
+ * @param init Configuration block for the flow
+ * @return The built flow as a Map (JSON-serializable)
+ */
+fun flow(init: FlowBuilder.() -> Unit): Map = FlowBuilder().apply(init).build()
+
+/**
+ * Creates a flow builder without immediately building.
+ * Useful when you need to further configure the flow before building.
+ */
+fun flowBuilder(init: FlowBuilder.() -> Unit = {}): FlowBuilder = FlowBuilder().apply(init)
diff --git a/dsl/kotlin/src/main/kotlin/com/intuit/playerui/lang/dsl/flow/NavigationBuilder.kt b/dsl/kotlin/src/main/kotlin/com/intuit/playerui/lang/dsl/flow/NavigationBuilder.kt
new file mode 100644
index 0000000..d07aab6
--- /dev/null
+++ b/dsl/kotlin/src/main/kotlin/com/intuit/playerui/lang/dsl/flow/NavigationBuilder.kt
@@ -0,0 +1,145 @@
+package com.intuit.playerui.lang.dsl.flow
+
+import com.intuit.playerui.lang.dsl.FluentDslMarker
+import com.intuit.playerui.lang.dsl.tagged.Expression
+
+/**
+ * Type-safe builder for Player-UI navigation state machines.
+ *
+ * Example:
+ * ```kotlin
+ * navigation {
+ * begin = "FLOW_1"
+ * flow("FLOW_1") {
+ * startState = "VIEW_welcome"
+ * view("VIEW_welcome", ref = "welcome-views-0") {
+ * on("next", "END_Done")
+ * }
+ * end("END_Done", outcome = "done")
+ * }
+ * }
+ * ```
+ */
+@FluentDslMarker
+class NavigationBuilder {
+ var begin: String = "FLOW_1"
+ private val flows = mutableMapOf()
+
+ fun flow(
+ name: String,
+ init: NavigationFlowBuilder.() -> Unit,
+ ) {
+ flows[name] = NavigationFlowBuilder().apply(init)
+ }
+
+ fun build(): Map {
+ val result = mutableMapOf("BEGIN" to begin)
+ flows.forEach { (name, builder) ->
+ result[name] = builder.build()
+ }
+ return result
+ }
+}
+
+@FluentDslMarker
+class NavigationFlowBuilder {
+ var startState: String = ""
+ var onStart: Expression<*>? = null
+ var onEnd: Expression<*>? = null
+ private val states = mutableMapOf>()
+
+ fun view(
+ name: String,
+ ref: String,
+ init: ViewStateBuilder.() -> Unit = {},
+ ) {
+ states[name] = ViewStateBuilder(ref).apply(init).build()
+ }
+
+ fun end(
+ name: String,
+ outcome: String,
+ ) {
+ states[name] =
+ mapOf(
+ "state_type" to "END",
+ "outcome" to outcome,
+ )
+ }
+
+ fun action(
+ name: String,
+ exp: Expression<*>,
+ init: ActionStateBuilder.() -> Unit = {},
+ ) {
+ states[name] = ActionStateBuilder(exp).apply(init).build()
+ }
+
+ fun build(): Map {
+ val result = mutableMapOf()
+ if (startState.isNotEmpty()) result["startState"] = startState
+ onStart?.let { result["onStart"] = it.toString() }
+ onEnd?.let { result["onEnd"] = it.toString() }
+ result.putAll(states)
+ return result
+ }
+}
+
+@FluentDslMarker
+class ViewStateBuilder(
+ private val ref: String,
+) {
+ private val transitions = mutableMapOf()
+
+ fun on(
+ event: String,
+ target: String,
+ ) {
+ transitions[event] = target
+ }
+
+ fun build(): Map {
+ val result =
+ mutableMapOf(
+ "state_type" to "VIEW",
+ "ref" to ref,
+ )
+ if (transitions.isNotEmpty()) {
+ result["transitions"] = transitions.toMap()
+ }
+ return result
+ }
+}
+
+@FluentDslMarker
+class ActionStateBuilder(
+ private val exp: Expression<*>,
+) {
+ private val transitions = mutableMapOf()
+
+ fun on(
+ event: String,
+ target: String,
+ ) {
+ transitions[event] = target
+ }
+
+ fun build(): Map {
+ val result =
+ mutableMapOf(
+ "state_type" to "ACTION",
+ "exp" to exp.toString(),
+ )
+ if (transitions.isNotEmpty()) {
+ result["transitions"] = transitions.toMap()
+ }
+ return result
+ }
+}
+
+/**
+ * Extension function on FlowBuilder to add type-safe navigation.
+ */
+fun FlowBuilder.navigation(init: NavigationBuilder.() -> Unit) {
+ navigation = NavigationBuilder().apply(init).build()
+}
diff --git a/dsl/kotlin/src/main/kotlin/com/intuit/playerui/lang/dsl/id/IdGenerator.kt b/dsl/kotlin/src/main/kotlin/com/intuit/playerui/lang/dsl/id/IdGenerator.kt
new file mode 100644
index 0000000..b90dd78
--- /dev/null
+++ b/dsl/kotlin/src/main/kotlin/com/intuit/playerui/lang/dsl/id/IdGenerator.kt
@@ -0,0 +1,112 @@
+package com.intuit.playerui.lang.dsl.id
+
+import com.intuit.playerui.lang.dsl.core.AssetMetadata
+import com.intuit.playerui.lang.dsl.core.BuildContext
+import com.intuit.playerui.lang.dsl.core.IdBranch
+
+/**
+ * Generates a unique ID based on the build context and registers it
+ * in the context's [IdRegistry].
+ *
+ * @param context The build context containing parent ID, branch info, and metadata
+ * @return A unique ID string
+ * @throws IllegalArgumentException if branch validation fails
+ */
+fun genId(context: BuildContext): String {
+ val baseId = generateBaseId(context)
+ return context.idRegistry.ensureUnique(baseId)
+}
+
+/**
+ * Generates an ID without registering it. Useful for intermediate lookups
+ * or when you need to preview what an ID would be without consuming it.
+ *
+ * @param context The build context
+ * @return The ID that would be generated (without collision suffix)
+ */
+fun peekId(context: BuildContext): String = generateBaseId(context)
+
+/**
+ * Generates the base ID from context without collision detection.
+ */
+private fun generateBaseId(context: BuildContext): String {
+ val parentId = context.parentId
+
+ return when (val branch = context.branch) {
+ null -> {
+ parentId
+ }
+
+ is IdBranch.Slot -> {
+ require(branch.name.isNotEmpty()) {
+ "genId: Slot branch requires a 'name' property"
+ }
+ if (parentId.isEmpty()) branch.name else "$parentId-${branch.name}"
+ }
+
+ is IdBranch.ArrayItem -> {
+ require(branch.index >= 0) {
+ "genId: Array-item index must be non-negative"
+ }
+ "$parentId-${branch.index}"
+ }
+
+ is IdBranch.Template -> {
+ require(branch.depth >= 0) {
+ "genId: Template depth must be non-negative"
+ }
+ val suffix = if (branch.depth == 0) "" else branch.depth.toString()
+ "$parentId-_index${suffix}_"
+ }
+
+ is IdBranch.Switch -> {
+ require(branch.index >= 0) {
+ "genId: Switch index must be non-negative"
+ }
+ val kindName =
+ when (branch.kind) {
+ IdBranch.Switch.SwitchKind.STATIC -> "static"
+ IdBranch.Switch.SwitchKind.DYNAMIC -> "dynamic"
+ }
+ "$parentId-${kindName}Switch-${branch.index}"
+ }
+ }
+}
+
+/**
+ * Determines a smart slot name based on asset metadata.
+ * Used for generating meaningful IDs based on the asset's type, binding, or value.
+ *
+ * @param parameterName The default parameter name (e.g., "label", "values")
+ * @param assetMetadata Optional metadata about the asset
+ * @return A descriptive slot name
+ */
+fun determineSlotName(
+ parameterName: String,
+ assetMetadata: AssetMetadata?,
+): String {
+ if (assetMetadata == null) return parameterName
+
+ val type = assetMetadata.type
+ val binding = assetMetadata.binding
+ val value = assetMetadata.value
+
+ // Rule 1: Action with value - append the value's last segment
+ if (type == "action" && value != null) {
+ val cleanValue = value.removeSurrounding("{{", "}}")
+ val lastSegment = cleanValue.split(".").lastOrNull() ?: return type
+ return "$type-$lastSegment"
+ }
+
+ // Rule 2: Non-action with binding - append the binding's last segment
+ if (type != "action" && binding != null) {
+ val cleanBinding = binding.removeSurrounding("{{", "}}")
+ val lastSegment = cleanBinding.split(".").lastOrNull()
+ if (lastSegment != null) {
+ return "${type ?: parameterName}-$lastSegment"
+ }
+ }
+
+ // Rule 3: Use type or fall back to parameter name
+ return type ?: parameterName
+}
diff --git a/dsl/kotlin/src/main/kotlin/com/intuit/playerui/lang/dsl/id/IdRegistry.kt b/dsl/kotlin/src/main/kotlin/com/intuit/playerui/lang/dsl/id/IdRegistry.kt
new file mode 100644
index 0000000..11dd544
--- /dev/null
+++ b/dsl/kotlin/src/main/kotlin/com/intuit/playerui/lang/dsl/id/IdRegistry.kt
@@ -0,0 +1,52 @@
+package com.intuit.playerui.lang.dsl.id
+
+/**
+ * Per-build registry for tracking generated asset IDs and ensuring uniqueness.
+ * When an ID collision is detected, a numeric suffix (-1, -2, etc.) is appended.
+ *
+ * Each flow build should create a fresh [IdRegistry] instance, eliminating the need
+ * for global mutable state and enabling safe concurrent builds.
+ */
+class IdRegistry {
+ private val registered = LinkedHashSet()
+ private val suffixCounters = mutableMapOf()
+
+ /**
+ * Registers an ID and returns a unique version.
+ * If the ID already exists, appends a numeric suffix.
+ *
+ * @param baseId The desired base ID
+ * @return A unique ID (either baseId or baseId-N where N is a number)
+ * @throws IllegalStateException if unable to generate unique ID after max attempts
+ */
+ fun ensureUnique(baseId: String): String {
+ if (registered.add(baseId)) {
+ return baseId
+ }
+
+ // Safeguard against infinite loops (should never be reached in practice)
+ val maxAttempts = 10000
+ repeat(maxAttempts) {
+ val next =
+ suffixCounters.compute(baseId) { _, current ->
+ (current ?: 0) + 1
+ } ?: error("Unexpected null from compute operation")
+ val candidate = "$baseId-$next"
+ if (registered.add(candidate)) {
+ return candidate
+ }
+ }
+
+ error("Failed to generate unique ID for '$baseId' after $maxAttempts attempts")
+ }
+
+ /**
+ * Checks if an ID is already registered without registering it.
+ */
+ fun isRegistered(id: String): Boolean = id in registered
+
+ /**
+ * Returns the count of registered IDs.
+ */
+ fun size(): Int = registered.size
+}
diff --git a/dsl/kotlin/src/main/kotlin/com/intuit/playerui/lang/dsl/schema/FlowSchemaSupport.kt b/dsl/kotlin/src/main/kotlin/com/intuit/playerui/lang/dsl/schema/FlowSchemaSupport.kt
new file mode 100644
index 0000000..e7e4c10
--- /dev/null
+++ b/dsl/kotlin/src/main/kotlin/com/intuit/playerui/lang/dsl/schema/FlowSchemaSupport.kt
@@ -0,0 +1,68 @@
+package com.intuit.playerui.lang.dsl.schema
+
+import com.intuit.playerui.lang.dsl.flow.FlowBuilder
+import com.intuit.playerui.lang.dsl.types.Schema
+import com.intuit.playerui.lang.dsl.types.SchemaDataType
+
+/**
+ * Sets the schema on the FlowBuilder from a typed [Schema] definition.
+ *
+ * Converts the typed schema to the raw map format expected by the flow output.
+ *
+ * Example:
+ * ```kotlin
+ * val schema: Schema = mapOf(
+ * "ROOT" to mapOf(
+ * "name" to SchemaDataType(type = "StringType"),
+ * "age" to SchemaDataType(type = "NumberType"),
+ * ),
+ * )
+ *
+ * flow {
+ * id = "my-flow"
+ * schema(schema)
+ * navigation = mapOf("BEGIN" to "FLOW_1")
+ * }
+ * ```
+ */
+fun FlowBuilder.schema(typedSchema: Schema) {
+ schema =
+ typedSchema.mapValues { (_, node) ->
+ node.mapValues { (_, dataType) -> dataType.toMap() }
+ }
+}
+
+/**
+ * Sets the schema on the FlowBuilder and returns extracted bindings for use in the flow.
+ *
+ * Example:
+ * ```kotlin
+ * val schema: Schema = mapOf(
+ * "ROOT" to mapOf(
+ * "name" to SchemaDataType(type = "StringType"),
+ * ),
+ * )
+ *
+ * flow {
+ * id = "my-flow"
+ * val bindings = schemaWithBindings(schema)
+ * val nameBinding = bindings.binding("name")!!
+ * views = listOf(input { binding(nameBinding) })
+ * navigation = mapOf("BEGIN" to "FLOW_1")
+ * }
+ * ```
+ */
+fun FlowBuilder.schemaWithBindings(typedSchema: Schema): SchemaBindings {
+ schema(typedSchema)
+ return extractBindings(typedSchema)
+}
+
+private fun SchemaDataType.toMap(): Map =
+ buildMap {
+ put("type", type)
+ isArray?.let { put("isArray", it) }
+ isRecord?.let { put("isRecord", it) }
+ validation?.let { put("validation", it) }
+ format?.let { put("format", it) }
+ default?.let { put("default", it) }
+ }
diff --git a/dsl/kotlin/src/main/kotlin/com/intuit/playerui/lang/dsl/schema/SchemaBindings.kt b/dsl/kotlin/src/main/kotlin/com/intuit/playerui/lang/dsl/schema/SchemaBindings.kt
new file mode 100644
index 0000000..6d10ef2
--- /dev/null
+++ b/dsl/kotlin/src/main/kotlin/com/intuit/playerui/lang/dsl/schema/SchemaBindings.kt
@@ -0,0 +1,164 @@
+package com.intuit.playerui.lang.dsl.schema
+
+import com.intuit.playerui.lang.dsl.tagged.Binding
+import com.intuit.playerui.lang.dsl.types.Schema
+import com.intuit.playerui.lang.dsl.types.SchemaDataType
+import com.intuit.playerui.lang.dsl.types.SchemaNode
+
+/**
+ * A container for typed bindings extracted from a Player-UI schema.
+ * Each key maps to either a [Binding] (for primitive types) or a nested [SchemaBindings] (for complex types).
+ */
+class SchemaBindings internal constructor(
+ private val bindings: Map,
+) {
+ /** Access a binding or nested schema bindings by key. */
+ operator fun get(key: String): Any? = bindings[key]
+
+ /**
+ * Get a typed binding for the given key, or null if the key doesn't exist or isn't a binding.
+ */
+ @Suppress("UNCHECKED_CAST")
+ fun binding(key: String): Binding? = bindings[key] as? Binding
+
+ /** Get nested schema bindings for the given key, or null if the key doesn't exist or isn't nested. */
+ fun nested(key: String): SchemaBindings? = bindings[key] as? SchemaBindings
+
+ /** The set of all property keys at this level. */
+ val keys: Set get() = bindings.keys
+
+ /** Whether this container has any bindings. */
+ val isEmpty: Boolean get() = bindings.isEmpty()
+}
+
+/**
+ * Extracts typed [Binding] instances from a Player-UI [Schema] definition.
+ *
+ * Follows the same algorithm as the TypeScript `extractBindingsFromSchema`:
+ * - Starts from the ROOT node in the schema
+ * - Recursively processes each property
+ * - Creates [Binding], [Binding], or [Binding] for primitive types
+ * - Creates nested [SchemaBindings] for complex types
+ * - Handles arrays (via `_current_` path) and records
+ * - Prevents infinite recursion with a visited type set
+ *
+ * Example:
+ * ```kotlin
+ * val schema: Schema = mapOf(
+ * "ROOT" to mapOf(
+ * "name" to SchemaDataType(type = "StringType"),
+ * "age" to SchemaDataType(type = "NumberType"),
+ * "address" to SchemaDataType(type = "AddressType"),
+ * ),
+ * "AddressType" to mapOf(
+ * "street" to SchemaDataType(type = "StringType"),
+ * "city" to SchemaDataType(type = "StringType"),
+ * ),
+ * )
+ *
+ * val bindings = SchemaBindingsExtractor.extract(schema)
+ * val name: Binding = bindings.binding("name")!! // path: "name"
+ * val street: Binding = bindings.nested("address")!!.binding("street")!! // path: "address.street"
+ * ```
+ */
+object SchemaBindingsExtractor {
+ private val PRIMITIVE_TYPES = setOf("StringType", "NumberType", "BooleanType")
+
+ /**
+ * Extract typed bindings from a schema definition.
+ *
+ * @param schema The Player-UI schema with a ROOT node
+ * @return A [SchemaBindings] object providing typed access to bindings
+ */
+ fun extract(schema: Schema): SchemaBindings {
+ val root = schema["ROOT"] ?: return SchemaBindings(emptyMap())
+ return processNode(root, schema, "", mutableSetOf())
+ }
+
+ private fun processNode(
+ node: SchemaNode,
+ schema: Schema,
+ basePath: String,
+ visited: MutableSet,
+ ): SchemaBindings {
+ val result = mutableMapOf()
+ for ((key, dataType) in node) {
+ val path = if (basePath.isEmpty()) key else "$basePath.$key"
+ result[key] = processDataType(dataType, schema, path, HashSet(visited))
+ }
+ return SchemaBindings(result)
+ }
+
+ private fun processDataType(
+ dataType: SchemaDataType,
+ schema: Schema,
+ path: String,
+ visited: MutableSet,
+ ): Any {
+ val typeName = dataType.type
+
+ // Prevent infinite recursion
+ if (typeName in visited) {
+ return createBinding(typeName, path)
+ }
+
+ // Handle arrays
+ if (dataType.isArray == true) {
+ val arrayPath = if (path.isNotEmpty()) "$path._current_" else "_current_"
+
+ if (typeName in PRIMITIVE_TYPES) {
+ // Primitive arrays: StringType → { name: Binding }, others → { value: Binding }
+ val propName = if (typeName == "StringType") "name" else "value"
+ return SchemaBindings(mapOf(propName to createBinding(typeName, arrayPath)))
+ }
+
+ return resolveComplexType(typeName, schema, arrayPath, visited)
+ }
+
+ // Handle records
+ if (dataType.isRecord == true) {
+ return resolveComplexType(typeName, schema, path, visited)
+ }
+
+ // Handle primitives
+ if (typeName in PRIMITIVE_TYPES) {
+ return createBinding(typeName, path)
+ }
+
+ // Handle complex types (look up type definition in schema)
+ return resolveComplexType(typeName, schema, path, visited)
+ }
+
+ private fun resolveComplexType(
+ typeName: String,
+ schema: Schema,
+ path: String,
+ visited: MutableSet,
+ ): Any {
+ val typeNode = schema[typeName]
+ if (typeNode != null) {
+ val newVisited = HashSet(visited)
+ newVisited.add(typeName)
+ return processNode(typeNode, schema, path, newVisited)
+ }
+ return createBinding("StringType", path)
+ }
+
+ private fun createBinding(
+ typeName: String,
+ path: String,
+ ): Binding<*> =
+ when (typeName) {
+ "StringType" -> Binding(path)
+ "NumberType" -> Binding(path)
+ "BooleanType" -> Binding(path)
+ else -> Binding(path)
+ }
+}
+
+/**
+ * Top-level convenience function to extract bindings from a schema.
+ *
+ * @see SchemaBindingsExtractor.extract
+ */
+fun extractBindings(schema: Schema): SchemaBindings = SchemaBindingsExtractor.extract(schema)
diff --git a/dsl/kotlin/src/main/kotlin/com/intuit/playerui/lang/dsl/schema/SchemaGenerator.kt b/dsl/kotlin/src/main/kotlin/com/intuit/playerui/lang/dsl/schema/SchemaGenerator.kt
new file mode 100644
index 0000000..fa93224
--- /dev/null
+++ b/dsl/kotlin/src/main/kotlin/com/intuit/playerui/lang/dsl/schema/SchemaGenerator.kt
@@ -0,0 +1,141 @@
+package com.intuit.playerui.lang.dsl.schema
+
+import com.intuit.playerui.lang.dsl.types.SchemaDataType
+
+/**
+ * Generates [Schema][com.intuit.playerui.lang.dsl.types.Schema] representations from plain objects.
+ *
+ * Converts an input map to a schema structure with ROOT type and intermediate types
+ * for nested objects. Handles arrays, duplicate type name detection, and name collision
+ * resolution (appending numeric suffixes).
+ *
+ * Matches the JavaScript `SchemaGenerator` implementation semantics.
+ *
+ * Example:
+ * ```kotlin
+ * val generator = SchemaGenerator()
+ * val schema = generator.toSchema(mapOf(
+ * "name" to SchemaDataType(type = "StringType"),
+ * "address" to mapOf(
+ * "street" to SchemaDataType(type = "StringType"),
+ * "city" to SchemaDataType(type = "StringType"),
+ * ),
+ * ))
+ * // schema["ROOT"]["address"] = SchemaDataType(type = "AddressType")
+ * // schema["AddressType"]["street"] = SchemaDataType(type = "StringType")
+ * ```
+ */
+class SchemaGenerator {
+ private data class SchemaChild(
+ val name: String,
+ val child: Map,
+ )
+
+ private data class GeneratedDataType(
+ val shape: Any?,
+ var count: Int,
+ )
+
+ private val children = mutableListOf()
+ private val generatedDataTypes = mutableMapOf()
+ private val typeNameCache = mutableMapOf()
+
+ /**
+ * Converts an object to a Schema representation.
+ *
+ * @param schema Input map where values are either [SchemaDataType] (leaf types)
+ * or [Map] (nested objects that become intermediate types)
+ * or [List] (arrays of objects that become array types)
+ * @return A schema map with ROOT and intermediate type definitions
+ */
+ fun toSchema(schema: Map): Map> {
+ children.clear()
+ generatedDataTypes.clear()
+ typeNameCache.clear()
+
+ val result = mutableMapOf>()
+ val rootEntries = mutableMapOf()
+
+ for ((property, subType) in schema) {
+ rootEntries[property] = processChild(property, subType)
+ }
+
+ result["ROOT"] = rootEntries
+
+ while (children.isNotEmpty()) {
+ val (name, child) = children.removeLast()
+ val typeDef = mutableMapOf()
+ for ((property, subType) in child) {
+ typeDef[property] = processChild(property, subType)
+ }
+ result[name] = typeDef
+ }
+
+ return result
+ }
+
+ private fun processChild(property: String, subType: Any?): SchemaDataType {
+ if (subType is SchemaDataType) {
+ return subType
+ }
+
+ val intermediateType: SchemaDataType
+ val child: Map
+
+ if (subType is List<*> && subType.isNotEmpty()) {
+ @Suppress("UNCHECKED_CAST")
+ val firstElement = subType[0] as? Map ?: return SchemaDataType(type = "AnyType")
+ intermediateType = makePlaceholderArrayType(property)
+ child = firstElement
+ } else if (subType is Map<*, *>) {
+ @Suppress("UNCHECKED_CAST")
+ child = subType as Map
+ intermediateType = makePlaceholderType(property)
+ } else {
+ return SchemaDataType(type = "AnyType")
+ }
+
+ val typeName = intermediateType.type
+
+ val existing = generatedDataTypes[typeName]
+ if (existing != null) {
+ if (!deepEquals(child, existing.shape)) {
+ existing.count += 1
+ val newTypeName = "$typeName${existing.count}"
+ val newType = intermediateType.copy(type = newTypeName)
+ generatedDataTypes[newTypeName] = GeneratedDataType(shape = child, count = 1)
+ children.add(SchemaChild(newTypeName, child))
+ return newType
+ }
+ } else {
+ generatedDataTypes[typeName] = GeneratedDataType(shape = child, count = 1)
+ }
+
+ children.add(SchemaChild(intermediateType.type, child))
+ return intermediateType
+ }
+
+ private fun makePlaceholderType(typeName: String): SchemaDataType {
+ val cachedName = typeNameCache.getOrPut(typeName) { "${typeName}Type" }
+ return SchemaDataType(type = cachedName)
+ }
+
+ private fun makePlaceholderArrayType(typeName: String): SchemaDataType {
+ val cachedName = typeNameCache.getOrPut(typeName) { "${typeName}Type" }
+ return SchemaDataType(type = cachedName, isArray = true)
+ }
+
+ private fun deepEquals(a: Any?, b: Any?): Boolean {
+ if (a === b) return true
+ if (a == null || b == null) return a == b
+ if (a is Map<*, *> && b is Map<*, *>) {
+ if (a.size != b.size) return false
+ return a.all { (k, v) -> b.containsKey(k) && deepEquals(v, b[k]) }
+ }
+ if (a is List<*> && b is List<*>) {
+ if (a.size != b.size) return false
+ return a.zip(b).all { (x, y) -> deepEquals(x, y) }
+ }
+ return a == b
+ }
+}
diff --git a/dsl/kotlin/src/main/kotlin/com/intuit/playerui/lang/dsl/tagged/StandardExpressions.kt b/dsl/kotlin/src/main/kotlin/com/intuit/playerui/lang/dsl/tagged/StandardExpressions.kt
new file mode 100644
index 0000000..aae6ed6
--- /dev/null
+++ b/dsl/kotlin/src/main/kotlin/com/intuit/playerui/lang/dsl/tagged/StandardExpressions.kt
@@ -0,0 +1,284 @@
+package com.intuit.playerui.lang.dsl.tagged
+
+/*
+ * Standard library of expressions for the Player-UI DSL.
+ * Provides common logical, comparison, and arithmetic operations.
+ * Matches the TypeScript fluent library's std.ts implementation.
+ */
+
+/**
+ * Logical AND operation - returns true if all arguments are truthy.
+ */
+fun and(vararg args: Any): Expression {
+ val expressions =
+ args.map { arg ->
+ val expr = toExpressionString(arg)
+ // Wrap OR expressions in parentheses to maintain proper precedence
+ if (expr.contains(" || ") && !expr.startsWith("(")) {
+ "($expr)"
+ } else {
+ expr
+ }
+ }
+ return expression(expressions.joinToString(" && "))
+}
+
+/**
+ * Logical OR operation - returns true if any argument is truthy.
+ */
+fun or(vararg args: Any): Expression {
+ val expressions = args.map { toExpressionString(it) }
+ return expression(expressions.joinToString(" || "))
+}
+
+/**
+ * Logical NOT operation - returns true if argument is falsy.
+ */
+fun not(value: Any): Expression {
+ val expr = toExpressionString(value)
+ // Wrap complex expressions in parentheses
+ val wrappedExpr =
+ if (expr.contains(" ") && !expr.startsWith("(")) {
+ "($expr)"
+ } else {
+ expr
+ }
+ return expression("!$wrappedExpr")
+}
+
+/**
+ * Logical NOR operation - returns true if all arguments are falsy.
+ */
+fun nor(vararg args: Any): Expression = not(or(*args))
+
+/**
+ * Logical NAND operation - returns false if all arguments are truthy.
+ */
+fun nand(vararg args: Any): Expression = not(and(*args))
+
+/**
+ * Logical XOR operation - returns true if exactly one argument is truthy.
+ */
+fun xor(left: Any, right: Any): Expression {
+ val leftExpr = toExpressionString(left)
+ val rightExpr = toExpressionString(right)
+ return expression("($leftExpr && !$rightExpr) || (!$leftExpr && $rightExpr)")
+}
+
+/**
+ * Helper function to create binary comparison expressions.
+ * Reduces code duplication across all comparison operators.
+ */
+private fun createComparisonExpression(
+ left: Any,
+ right: Any?,
+ operator: String,
+): Expression {
+ val leftExpr = toExpressionString(left)
+ val rightExpr = toValueString(right)
+ return expression("$leftExpr $operator $rightExpr")
+}
+
+/**
+ * Equality comparison (loose equality ==).
+ */
+fun equal(
+ left: Any,
+ right: T,
+): Expression = createComparisonExpression(left, right, "==")
+
+/**
+ * Strict equality comparison (===).
+ */
+fun strictEqual(
+ left: Any,
+ right: T,
+): Expression = createComparisonExpression(left, right, "===")
+
+/**
+ * Inequality comparison (!=).
+ */
+fun notEqual(
+ left: Any,
+ right: T,
+): Expression = createComparisonExpression(left, right, "!=")
+
+/**
+ * Strict inequality comparison (!==).
+ */
+fun strictNotEqual(
+ left: Any,
+ right: T,
+): Expression = createComparisonExpression(left, right, "!==")
+
+/**
+ * Greater than comparison (>).
+ */
+fun greaterThan(
+ left: Any,
+ right: Any,
+): Expression = createComparisonExpression(left, right, ">")
+
+/**
+ * Greater than or equal comparison (>=).
+ */
+fun greaterThanOrEqual(
+ left: Any,
+ right: Any,
+): Expression = createComparisonExpression(left, right, ">=")
+
+/**
+ * Less than comparison (<).
+ */
+fun lessThan(
+ left: Any,
+ right: Any,
+): Expression = createComparisonExpression(left, right, "<")
+
+/**
+ * Less than or equal comparison (<=).
+ */
+fun lessThanOrEqual(
+ left: Any,
+ right: Any,
+): Expression = createComparisonExpression(left, right, "<=")
+
+/**
+ * Addition operation (+).
+ */
+fun add(vararg args: Any): Expression {
+ val expressions = args.map { toExpressionString(it) }
+ return expression(expressions.joinToString(" + "))
+}
+
+/**
+ * Subtraction operation (-).
+ */
+fun subtract(left: Any, right: Any): Expression {
+ val leftExpr = toExpressionString(left)
+ val rightExpr = toExpressionString(right)
+ return expression("$leftExpr - $rightExpr")
+}
+
+/**
+ * Multiplication operation (*).
+ */
+fun multiply(vararg args: Any): Expression {
+ val expressions = args.map { toExpressionString(it) }
+ return expression(expressions.joinToString(" * "))
+}
+
+/**
+ * Division operation (/).
+ */
+fun divide(left: Any, right: Any): Expression {
+ val leftExpr = toExpressionString(left)
+ val rightExpr = toExpressionString(right)
+ return expression("$leftExpr / $rightExpr")
+}
+
+/**
+ * Modulo operation (%).
+ */
+fun modulo(left: Any, right: Any): Expression {
+ val leftExpr = toExpressionString(left)
+ val rightExpr = toExpressionString(right)
+ return expression("$leftExpr % $rightExpr")
+}
+
+/**
+ * Conditional (ternary) operation - if-then-else logic.
+ */
+fun conditional(
+ condition: Any,
+ ifTrue: T,
+ ifFalse: T
+): Expression {
+ val conditionExpr = toExpressionString(condition)
+ val trueExpr = toValueString(ifTrue)
+ val falseExpr = toValueString(ifFalse)
+ return expression("$conditionExpr ? $trueExpr : $falseExpr")
+}
+
+/**
+ * Function call expression.
+ */
+fun call(functionName: String, vararg args: Any): Expression {
+ val argExpressions = args.map { toValueString(it) }
+ return expression("$functionName(${argExpressions.joinToString(", ")})")
+}
+
+/**
+ * Creates a literal value expression.
+ */
+fun literal(value: T): Expression = expression(toValueString(value))
+
+/**
+ * Converts a value to its expression string representation.
+ * For TaggedValues, extracts the inner expression/binding path.
+ * For primitives, returns the string representation.
+ */
+private fun toExpressionString(value: Any): String =
+ when (value) {
+ is TaggedValue<*> -> value.toValue()
+ is Boolean -> value.toString()
+ is Number -> value.toString()
+ is String -> value
+ else -> value.toString()
+ }
+
+/**
+ * Converts a value to its JSON representation for use in expressions.
+ * For TaggedValues, extracts the inner expression/binding path.
+ * For primitives, returns JSON-encoded strings.
+ */
+private fun toValueString(value: Any?): String =
+ when (value) {
+ null -> "null"
+ is TaggedValue<*> -> value.toValue()
+ is Boolean -> value.toString()
+ is Number -> value.toString()
+ is String -> "\"${value.replace("\\", "\\\\").replace("\"", "\\\"")}\""
+ is List<*> -> "[${value.joinToString(", ") { toValueString(it) }}]"
+ is Map<*, *> -> "{${value.entries.joinToString(", ") { "\"${it.key}\": ${toValueString(it.value)}" }}}"
+ else -> "\"$value\""
+ }
+
+/**
+ * Comparison aliases as functions.
+ */
+fun eq(left: Any, right: T): Expression = equal(left, right)
+
+fun strictEq(left: Any, right: T): Expression = strictEqual(left, right)
+
+fun neq(left: Any, right: T): Expression = notEqual(left, right)
+
+fun strictNeq(left: Any, right: T): Expression = strictNotEqual(left, right)
+
+fun gt(left: Any, right: Any): Expression = greaterThan(left, right)
+
+fun gte(left: Any, right: Any): Expression = greaterThanOrEqual(left, right)
+
+fun lt(left: Any, right: Any): Expression = lessThan(left, right)
+
+fun lte(left: Any, right: Any): Expression = lessThanOrEqual(left, right)
+
+/**
+ * Arithmetic aliases as functions.
+ */
+fun plus(vararg args: Any): Expression = add(*args)
+
+fun minus(left: Any, right: Any): Expression = subtract(left, right)
+
+fun times(vararg args: Any): Expression = multiply(*args)
+
+fun div(left: Any, right: Any): Expression = divide(left, right)
+
+fun mod(left: Any, right: Any): Expression = modulo(left, right)
+
+/**
+ * Control flow aliases as functions.
+ */
+fun ternary(condition: Any, ifTrue: T, ifFalse: T): Expression = conditional(condition, ifTrue, ifFalse)
+
+fun ifElse(condition: Any, ifTrue: T, ifFalse: T): Expression = conditional(condition, ifTrue, ifFalse)
diff --git a/dsl/kotlin/src/main/kotlin/com/intuit/playerui/lang/dsl/tagged/TaggedValue.kt b/dsl/kotlin/src/main/kotlin/com/intuit/playerui/lang/dsl/tagged/TaggedValue.kt
new file mode 100644
index 0000000..94f8852
--- /dev/null
+++ b/dsl/kotlin/src/main/kotlin/com/intuit/playerui/lang/dsl/tagged/TaggedValue.kt
@@ -0,0 +1,188 @@
+package com.intuit.playerui.lang.dsl.tagged
+
+/**
+ * Base interface for tagged values (bindings and expressions).
+ * These represent dynamic values that are resolved at runtime by Player-UI.
+ *
+ * @param T Phantom type parameter for type-safe usage (not used at runtime)
+ */
+sealed interface TaggedValue {
+ /**
+ * Returns the raw value without formatting.
+ */
+ fun toValue(): String
+
+ /**
+ * Returns the formatted string representation.
+ * For bindings: "{{path}}"
+ * For expressions: "@[expr]@"
+ */
+ override fun toString(): String
+}
+
+/**
+ * Represents a data binding in Player-UI.
+ * Bindings reference paths in the data model.
+ *
+ * Example: binding("user.name") produces "{{user.name}}"
+ *
+ * @param T The expected type of the bound value (phantom type)
+ * @property path The data path to bind to
+ */
+class Binding(
+ private val path: String,
+) : TaggedValue {
+ init {
+ require(path.isNotBlank()) { "Binding path cannot be empty or blank" }
+ }
+
+ override fun toValue(): String = path
+
+ override fun toString(): String = "{{$path}}"
+
+ /**
+ * Returns the path with _index_ placeholders replaced.
+ */
+ fun withIndex(
+ index: Int,
+ depth: Int = 0,
+ ): Binding {
+ val placeholder = if (depth == 0) "_index_" else "_index${depth}_"
+ return Binding(path.replace(placeholder, index.toString()))
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is Binding<*>) return false
+ return path == other.path
+ }
+
+ override fun hashCode(): Int = path.hashCode()
+}
+
+/**
+ * Represents an expression in Player-UI.
+ * Expressions are evaluated at runtime.
+ *
+ * Example: expression("user.age >= 18") produces "@[user.age >= 18]@"
+ *
+ * @param T The expected return type of the expression (phantom type)
+ * @property expr The expression string
+ */
+class Expression(
+ private val expr: String,
+) : TaggedValue {
+ init {
+ validateSyntax(expr)
+ }
+
+ override fun toValue(): String = expr
+
+ override fun toString(): String = "@[$expr]@"
+
+ /**
+ * Returns the expression with _index_ placeholders replaced.
+ */
+ fun withIndex(
+ index: Int,
+ depth: Int = 0,
+ ): Expression {
+ val placeholder = if (depth == 0) "_index_" else "_index${depth}_"
+ return Expression(expr.replace(placeholder, index.toString()))
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is Expression<*>) return false
+ return expr == other.expr
+ }
+
+ override fun hashCode(): Int = expr.hashCode()
+
+ private fun validateSyntax(expression: String) {
+ val stack = mutableListOf>() // (opening bracket, position)
+
+ expression.forEachIndexed { index, char ->
+ when (char) {
+ '(', '[', '{' -> {
+ stack.add(char to index)
+ }
+
+ ')', ']', '}' -> {
+ if (stack.isEmpty()) {
+ throw IllegalArgumentException(
+ "Unexpected $char at character $index in expression: $expression",
+ )
+ }
+ val (opening, openPos) = stack.removeLast()
+ val expected =
+ when (char) {
+ ')' -> '('
+ ']' -> '['
+ '}' -> '{'
+ else -> throw IllegalStateException()
+ }
+ require(opening == expected) {
+ "Mismatched brackets: found $char at character $index but expected ${getClosing(opening)} to match $opening at character $openPos in expression: $expression"
+ }
+ }
+ }
+ }
+
+ require(stack.isEmpty()) {
+ val (bracket, pos) = stack.last()
+ "Expected ${getClosing(bracket)} to match $bracket at character $pos in expression: $expression"
+ }
+ }
+
+ private fun getClosing(opening: Char): Char =
+ when (opening) {
+ '(' -> ')'
+ '[' -> ']'
+ '{' -> '}'
+ else -> throw IllegalArgumentException("Not an opening bracket: $opening")
+ }
+}
+
+/**
+ * Creates a binding to a data path.
+ */
+fun binding(path: String): Binding = Binding(path)
+
+/**
+ * Creates an expression.
+ */
+fun expression(expr: String): Expression = Expression(expr)
+
+/**
+ * Checks if a value is a TaggedValue.
+ */
+fun isTaggedValue(value: Any?): Boolean = value is TaggedValue<*>
+
+// Operator overloads for Expression
+
+operator fun Expression.plus(other: Any): Expression = add(this, other)
+
+operator fun Expression.minus(other: Any): Expression = subtract(this, other)
+
+operator fun Expression.times(other: Any): Expression = multiply(this, other)
+
+operator fun Expression.div(other: Any): Expression = divide(this, other)
+
+@Suppress("ktlint:standard:chain-method-continuation")
+operator fun Expression.not(): Expression = com.intuit.playerui.lang.dsl.tagged.not(this)
+
+// Infix comparison operators for TaggedValue
+
+infix fun TaggedValue.eq(other: Any): Expression = equal(this, other)
+
+infix fun TaggedValue.neq(other: Any): Expression = notEqual(this, other)
+
+// Binding path composition
+
+operator fun Binding<*>.div(segment: String): Binding = Binding("${toValue()}.$segment")
+
+fun Binding>.indexed(depth: Int = 0): Binding {
+ val placeholder = if (depth == 0) "_index_" else "_index${depth}_"
+ return Binding("${toValue()}.$placeholder")
+}
diff --git a/dsl/kotlin/src/main/kotlin/com/intuit/playerui/lang/dsl/types/PlayerTypes.kt b/dsl/kotlin/src/main/kotlin/com/intuit/playerui/lang/dsl/types/PlayerTypes.kt
new file mode 100644
index 0000000..8fdba60
--- /dev/null
+++ b/dsl/kotlin/src/main/kotlin/com/intuit/playerui/lang/dsl/types/PlayerTypes.kt
@@ -0,0 +1,169 @@
+package com.intuit.playerui.lang.dsl.types
+
+import kotlinx.serialization.Contextual
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+// Player-UI type definitions for Kotlin.
+// These types represent the core structures of Player-UI content.
+
+/**
+ * A binding describes a location in the data model.
+ * Format: "{{path.to.data}}"
+ */
+typealias BindingRef = String
+
+/**
+ * An expression reference for runtime evaluation.
+ * Format: "@[expression]@"
+ */
+typealias ExpressionRef = String
+
+/**
+ * Expression can be a single string or an array of strings.
+ * Represented as Any since it can be either String or List.
+ */
+typealias Expression = Any
+
+/**
+ * The data model is the location where all user data is stored.
+ */
+typealias DataModel = Map
+
+/**
+ * Base interface for all Player-UI assets.
+ * Each asset requires a unique id per view and a type that determines semantics.
+ */
+interface Asset {
+ val id: String
+ val type: String
+}
+
+/**
+ * A template describes a mapping from a data array to an array of objects.
+ */
+@Serializable
+data class Template(
+ val data: BindingRef,
+ val output: String,
+ val value: kotlinx.serialization.json.JsonElement,
+ val dynamic: Boolean = false,
+ val placement: TemplatePlacement? = null,
+)
+
+/**
+ * Template placement relative to existing elements.
+ */
+@Serializable
+enum class TemplatePlacement {
+ @SerialName("prepend")
+ PREPEND,
+
+ @SerialName("append")
+ APPEND,
+}
+
+/**
+ * Base for navigation state transitions.
+ */
+typealias NavigationTransitions = Map
+
+/**
+ * The complete navigation section of a flow.
+ * BEGIN specifies the starting flow, and additional keys are flow definitions.
+ */
+typealias Navigation = Map
+
+/**
+ * Validation severity levels.
+ */
+@Serializable
+enum class ValidationSeverity {
+ @SerialName("error")
+ ERROR,
+
+ @SerialName("warning")
+ WARNING,
+}
+
+/**
+ * Validation trigger timing.
+ */
+@Serializable
+enum class ValidationTrigger {
+ @SerialName("navigation")
+ NAVIGATION,
+
+ @SerialName("change")
+ CHANGE,
+
+ @SerialName("load")
+ LOAD,
+}
+
+/**
+ * Validation display target.
+ */
+@Serializable
+enum class ValidationDisplayTarget {
+ @SerialName("page")
+ PAGE,
+
+ @SerialName("section")
+ SECTION,
+
+ @SerialName("field")
+ FIELD,
+}
+
+/**
+ * A validation reference.
+ * @property blocking Can be Boolean or the string "once"
+ */
+@Serializable
+data class ValidationReference(
+ val type: String,
+ val message: String? = null,
+ val severity: ValidationSeverity? = null,
+ val trigger: ValidationTrigger? = null,
+ val displayTarget: ValidationDisplayTarget? = null,
+ @Contextual val blocking: Any? = null,
+)
+
+/**
+ * Schema data type definition.
+ */
+@Serializable
+data class SchemaDataType(
+ val type: String,
+ val validation: List? = null,
+ val format: Map? = null,
+ @Contextual val default: Any? = null,
+ val isRecord: Boolean? = null,
+ val isArray: Boolean? = null,
+)
+
+/**
+ * Schema node definition.
+ */
+typealias SchemaNode = Map
+
+/**
+ * Complete schema definition.
+ */
+typealias Schema = Map
+
+/**
+ * A single case in a switch statement.
+ */
+@Serializable
+data class SwitchCase(
+ val asset: kotlinx.serialization.json.JsonObject,
+ /** Expression or true */
+ val case: kotlinx.serialization.json.JsonElement,
+)
+
+/**
+ * A switch replaces an asset with the applicable case on first render.
+ */
+typealias Switch = List
diff --git a/dsl/kotlin/src/test/kotlin/com/intuit/playerui/lang/dsl/ApplicabilityTest.kt b/dsl/kotlin/src/test/kotlin/com/intuit/playerui/lang/dsl/ApplicabilityTest.kt
new file mode 100644
index 0000000..7e57a54
--- /dev/null
+++ b/dsl/kotlin/src/test/kotlin/com/intuit/playerui/lang/dsl/ApplicabilityTest.kt
@@ -0,0 +1,59 @@
+@file:Suppress("UNCHECKED_CAST")
+
+package com.intuit.playerui.lang.dsl
+
+import com.intuit.playerui.lang.dsl.mocks.builders.text
+import com.intuit.playerui.lang.dsl.tagged.Binding
+import com.intuit.playerui.lang.dsl.tagged.Expression
+import io.kotest.core.spec.style.DescribeSpec
+import io.kotest.matchers.shouldBe
+
+class ApplicabilityTest :
+ DescribeSpec({
+
+ describe("Applicability") {
+ it("sets applicability with an expression") {
+ val builder =
+ text {
+ value = "Conditional"
+ }
+ builder.applicability(Expression("user.isActive"))
+
+ val result = builder.build()
+ result["applicability"] shouldBe "@[user.isActive]@"
+ }
+
+ it("sets applicability with a binding") {
+ val builder =
+ text {
+ value = "Conditional"
+ }
+ builder.applicability(Binding("user.showField"))
+
+ val result = builder.build()
+ result["applicability"] shouldBe "{{user.showField}}"
+ }
+
+ it("sets applicability via property setter") {
+ val builder =
+ text {
+ value = "Conditional"
+ }
+ builder.applicability = Expression("isVisible")
+
+ val result = builder.build()
+ result["applicability"] shouldBe "@[isVisible]@"
+ }
+
+ it("reads applicability via property getter") {
+ val expr = Expression("user.isActive")
+ val builder =
+ text {
+ value = "Test"
+ }
+ builder.applicability(expr)
+
+ builder.applicability shouldBe expr
+ }
+ }
+ })
diff --git a/dsl/kotlin/src/test/kotlin/com/intuit/playerui/lang/dsl/BuildPipelineSwitchTest.kt b/dsl/kotlin/src/test/kotlin/com/intuit/playerui/lang/dsl/BuildPipelineSwitchTest.kt
new file mode 100644
index 0000000..0f469a1
--- /dev/null
+++ b/dsl/kotlin/src/test/kotlin/com/intuit/playerui/lang/dsl/BuildPipelineSwitchTest.kt
@@ -0,0 +1,147 @@
+@file:Suppress("UNCHECKED_CAST")
+
+package com.intuit.playerui.lang.dsl
+
+import com.intuit.playerui.lang.dsl.core.AuxiliaryStorage
+import com.intuit.playerui.lang.dsl.core.BuildContext
+import com.intuit.playerui.lang.dsl.core.BuildPipeline
+import com.intuit.playerui.lang.dsl.core.SwitchArgs
+import com.intuit.playerui.lang.dsl.core.SwitchCase
+import com.intuit.playerui.lang.dsl.core.SwitchCondition
+import com.intuit.playerui.lang.dsl.core.SwitchMetadata
+import com.intuit.playerui.lang.dsl.core.ValueStorage
+import com.intuit.playerui.lang.dsl.id.IdRegistry
+import com.intuit.playerui.lang.dsl.mocks.builders.*
+import com.intuit.playerui.lang.dsl.tagged.expression
+import io.kotest.core.spec.style.DescribeSpec
+import io.kotest.matchers.shouldBe
+import io.kotest.matchers.types.shouldBeInstanceOf
+
+class BuildPipelineSwitchTest :
+ DescribeSpec({
+
+ fun registry() = IdRegistry()
+
+ describe("Switch array property wrapping") {
+ it("wraps switch result in array for array properties at path depth 1") {
+ val reg = registry()
+
+ val result =
+ collection {
+ id = "test-collection"
+ switch(
+ path = listOf("values"),
+ isDynamic = false,
+ ) {
+ case(expression("showItems"), text { value = "Items" })
+ default(text { value = "No Items" })
+ }
+ }.build(BuildContext(parentId = "flow-views-0", idRegistry = reg))
+
+ // "values" is in arrayProperties for CollectionBuilder,
+ // so the switch result should be wrapped in an array
+ val values = result["values"]
+ values.shouldBeInstanceOf>()
+ (values as List<*>).size shouldBe 1
+
+ val switchWrapper = values[0] as Map
+ switchWrapper.containsKey("staticSwitch") shouldBe true
+
+ val staticSwitch = switchWrapper["staticSwitch"] as List