diff --git a/.bazelrc b/.bazelrc
index d8fbf5b..418ea93 100644
--- a/.bazelrc
+++ b/.bazelrc
@@ -17,7 +17,11 @@ common --remote_cache_compression --remote_cache_async
# Python config
common --incompatible_default_to_explicit_init_py
-# CI Config
+# Java config - use remote JDK since CI doesn't have a local one
+common --java_runtime_version=remotejdk_21
+common --tool_java_runtime_version=remotejdk_21
+
+# CI Config
common:ci --build_metadata=ROLE=CI
common:ci --local_resources=cpu=4
common:ci --local_resources=memory=8000
diff --git a/.circleci/config.yml b/.circleci/config.yml
index d7436be..88b4655 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -37,6 +37,7 @@ commands:
- setup_auth
- run: echo "//registry.npmjs.com/:_authToken=$NPM_TOKEN" >> ~/.npmrc
- run: echo -e $GPG_KEY | gpg --import --batch
+ - run: echo -e "pinentry-mode loopback\npassphrase $DEPLOY_MAVEN_GPG_PASSPHRASE" > ~/.gnupg/gpg.conf
- run: |
source ~/.bashrc
npx auto shipit --only-graduate-with-release-label -vv
@@ -101,7 +102,7 @@ jobs:
at: ~/xlr
- run: |
- bazel coverage --config=ci -- $(bazel query "kind(js_test, //...) + kind(py_test, //...)" --output label 2>/dev/null | tr '\n' ' ')
+ bazel coverage --config=ci -- $(bazel query "kind(js_test, //...) + kind(py_test, //...) + kind(kt_jvm_test, //...) + kind(_ktlint_test, //...)" --output label 2>/dev/null | tr '\n' ' ')
- run:
when: always
diff --git a/BUILD.bazel b/BUILD.bazel
index a6a3dc5..fc9114f 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"])
@@ -97,3 +98,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 f0928ea..73752e1 100644
--- a/MODULE.bazel
+++ b/MODULE.bazel
@@ -66,8 +66,40 @@ pip.parse(
use_repo(pip, "pypi")
+####### Kotlin #########
+bazel_dep(name = "rules_kotlin", version = "2.2.1")
+bazel_dep(name = "rules_jvm_external", version = "6.9")
+
+maven = use_extension("@rules_jvm_external//:extensions.bzl", "maven")
+maven.install(
+ artifacts = [
+ "org.jetbrains.kotlin:kotlin-stdlib:2.3.0",
+ "org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0",
+ "org.jetbrains.kotlin:kotlin-test:2.3.0",
+ "org.jetbrains.kotlin:kotlin-test-junit5:2.3.0",
+ "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",
+ ],
+ repositories = [
+ "https://repo1.maven.org/maven2",
+ ],
+)
+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.xlr",
+ },
version_file = "//:VERSION",
)
diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock
index 77c5f34..2194cae 100644
--- a/MODULE.bazel.lock
+++ b/MODULE.bazel.lock
@@ -102,7 +102,8 @@
"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",
@@ -191,12 +192,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",
@@ -433,96 +436,6 @@
"recordedRepoMappingEntries": []
}
},
- "@@rules_kotlin+//src/main/starlark/core/repositories:bzlmod_setup.bzl%rules_kotlin_extensions": {
- "general": {
- "bzlTransitiveDigest": "vfLCTchDthU74iCKvoskQ+ovk2Wu2tLykbCddrcLy7U=",
- "usagesDigest": "QPppUlwb7NSBhcaYae+JZPqTEmJKCkOXKFPXQS7aAJE=",
- "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": {
- "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_jar",
- "attributes": {
- "sha256": "6e94896e321603e3bfe89fef02478e44d1d64a3d25d49d0694892ffc01c60acf",
- "urls": [
- "https://repo1.maven.org/maven2/org/jetbrains/kotlin/kotlin-build-tools-impl/2.1.20/kotlin-build-tools-impl-2.1.20.jar"
- ]
- }
- }
- },
- "recordedRepoMappingEntries": [
- [
- "rules_kotlin+",
- "bazel_tools",
- "bazel_tools"
- ]
- ]
- }
- },
"@@rules_nodejs+//nodejs:extensions.bzl%node": {
"general": {
"bzlTransitiveDigest": "NwcLXHrbh2hoorA/Ybmcpjxsn/6avQmewDglodkDrgo=",
diff --git a/scripts/release.sh b/scripts/release.sh
index 1d24acd..9a4233a 100755
--- a/scripts/release.sh
+++ b/scripts/release.sh
@@ -1,7 +1,7 @@
#!/usr/bin/env bash
# See https://github.com/bazelbuild/rules_nodejs/blob/stable/scripts/publish_release.sh
-set -u -o pipefail
+set -eu -o pipefail
readonly PKG_NPM_LABELS=`bazel query --output=label 'kind("npm_package rule", //...) - attr("tags", "\[.*do-not-publish.*\]", //...)'`
NPM_TAG=canary
@@ -29,4 +29,14 @@ readonly PKG_PYPI_LABELS=`bazel query --output=label 'kind("py_wheel rule", //..
for pkg in $PKG_PYPI_LABELS ; do
TWINE_USERNAME=$PYPI_USER TWINE_PASSWORD=$PYPI_TOKEN bazel run --config=release ${pkg}.publish --
-done
\ No newline at end of file
+done
+
+# Maven Central Publishing
+MVN_RELEASE_TYPE=snapshot
+if [ "$RELEASE_TYPE" == "release" ] && [ "$CURRENT_BRANCH" == "main" ]; then
+ MVN_RELEASE_TYPE=release
+fi
+
+echo "Publishing Maven Packages with release type: ${MVN_RELEASE_TYPE} on branch: ${CURRENT_BRANCH}"
+bazel build --config=release @rules_player//distribution:staged-maven-deploy
+bazel run --config=release @rules_player//distribution:staged-maven-deploy -- "$MVN_RELEASE_TYPE" --package-group=com.intuit.playerui.xlr --client-timeout=600 --connect-timeout=600
diff --git a/types/kotlin/.editorconfig b/types/kotlin/.editorconfig
new file mode 100644
index 0000000..9eab273
--- /dev/null
+++ b/types/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/types/kotlin/BUILD.bazel b/types/kotlin/BUILD.bazel
new file mode 100644
index 0000000..d3c071b
--- /dev/null
+++ b/types/kotlin/BUILD.bazel
@@ -0,0 +1,53 @@
+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("@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",
+ ],
+)
+
+kt_jvm(
+ name = "xlr-types",
+ 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.xlr",
+ test_opts = "//:kt_opts",
+ test_deps = [
+ "@maven//:org_jetbrains_kotlin_kotlin_test",
+ "@maven//:org_jetbrains_kotlin_kotlin_test_junit5",
+ "@maven//:org_jetbrains_kotlinx_kotlinx_serialization_json",
+ ],
+ test_resources = glob(["src/test/resources/**/*"]),
+ test_resource_strip_prefix = "types/kotlin/src/test/resources",
+ pom_template = ":pom.tpl",
+)
diff --git a/types/kotlin/pom.tpl b/types/kotlin/pom.tpl
new file mode 100644
index 0000000..f070425
--- /dev/null
+++ b/types/kotlin/pom.tpl
@@ -0,0 +1,44 @@
+
+
+ 4.0.0
+
+ XLR - {artifactId}
+ Kotlin XLR type definitions, serializer and deserializer
+ https://github.com/player-ui/xlr
+
+
+ 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/xlr.git
+ https://github.com/player-ui/xlr.git
+ v{version}
+ https://github.com/player-ui/xlr.git
+
+
+ {groupId}
+ {artifactId}
+ {version}
+ {type}
+
+
+{dependencies}
+
+
diff --git a/types/kotlin/src/main/kotlin/com/intuit/playerui/xlr/XlrDeserializer.kt b/types/kotlin/src/main/kotlin/com/intuit/playerui/xlr/XlrDeserializer.kt
new file mode 100644
index 0000000..9b2565f
--- /dev/null
+++ b/types/kotlin/src/main/kotlin/com/intuit/playerui/xlr/XlrDeserializer.kt
@@ -0,0 +1,42 @@
+package com.intuit.playerui.xlr
+
+import kotlinx.serialization.json.JsonElement
+import kotlinx.serialization.json.JsonNull
+import kotlinx.serialization.json.JsonObject
+import kotlinx.serialization.json.decodeFromJsonElement
+import kotlinx.serialization.json.jsonObject
+import kotlinx.serialization.json.jsonPrimitive
+
+object XlrDeserializer {
+ fun deserialize(jsonString: String): XlrDocument {
+ val element = xlrJson.parseToJsonElement(jsonString).jsonObject
+ return parseDocument(element)
+ }
+
+ fun parseDocument(obj: JsonObject): XlrDocument {
+ val node = xlrJson.decodeFromJsonElement(NodeType.serializer(), obj)
+ val objectType =
+ node as? ObjectType
+ ?: throw IllegalArgumentException(
+ "Document root must be an object type, got: ${obj["type"]}",
+ )
+ return XlrDocument(
+ name =
+ obj["name"]?.jsonPrimitive?.content
+ ?: throw IllegalArgumentException("Document missing 'name'"),
+ source =
+ obj["source"]?.jsonPrimitive?.content
+ ?: throw IllegalArgumentException("Document missing 'source'"),
+ objectType = objectType,
+ genericTokens =
+ obj["genericTokens"]?.takeIf { it !is JsonNull }?.let {
+ xlrJson.decodeFromJsonElement(it)
+ },
+ )
+ }
+
+ fun parseNode(element: JsonElement): NodeType {
+ if (element is JsonNull) return NullType()
+ return xlrJson.decodeFromJsonElement(NodeType.serializer(), element)
+ }
+}
diff --git a/types/kotlin/src/main/kotlin/com/intuit/playerui/xlr/XlrGuards.kt b/types/kotlin/src/main/kotlin/com/intuit/playerui/xlr/XlrGuards.kt
new file mode 100644
index 0000000..5460957
--- /dev/null
+++ b/types/kotlin/src/main/kotlin/com/intuit/playerui/xlr/XlrGuards.kt
@@ -0,0 +1,102 @@
+package com.intuit.playerui.xlr
+
+// Composite guard and utility functions for XLR node types.
+// Trivial single-type checks (e.g. `node is StringType`) are not included
+// because Kotlin's `is` operator with smart casting makes them redundant.
+
+/**
+ * Check if a node is a primitive type (string, number, boolean, null, etc.)
+ */
+fun isPrimitiveType(node: NodeType): Boolean =
+ when (node) {
+ is StringType, is NumberType, is BooleanType, is NullType,
+ is AnyType, is UnknownType, is UndefinedType, is VoidType, is NeverType,
+ -> true
+
+ else -> false
+ }
+
+/**
+ * Check if a ref node references an AssetWrapper type.
+ */
+fun isAssetWrapperRef(node: NodeType): Boolean {
+ if (node !is RefType) return false
+ return node.ref.startsWith("AssetWrapper")
+}
+
+/**
+ * Check if a ref node references an Asset type.
+ */
+fun isAssetRef(node: NodeType): Boolean {
+ if (node !is RefType) return false
+ return node.ref.startsWith("Asset<") || node.ref == "Asset"
+}
+
+/**
+ * Check if a ref node references a Binding type.
+ */
+fun isBindingRef(node: NodeType): Boolean {
+ if (node !is RefType) return false
+ return node.ref == "Binding" || node.ref.startsWith("Binding<")
+}
+
+/**
+ * Check if a ref node references an Expression type.
+ */
+fun isExpressionRef(node: NodeType): Boolean {
+ if (node !is RefType) return false
+ return node.ref == "Expression" || node.ref.startsWith("Expression<")
+}
+
+/**
+ * Extract the asset type constant from an extends clause.
+ * E.g., Asset<"action"> -> "action"
+ */
+fun extractAssetTypeConstant(extendsRef: RefType?): String? {
+ if (extendsRef == null) return null
+ if (!extendsRef.ref.startsWith("Asset<")) return null
+
+ val genericArgs = extendsRef.genericArguments ?: return null
+ if (genericArgs.isEmpty()) return null
+
+ val firstArg = genericArgs.first()
+ if (firstArg is StringType && firstArg.const != null) {
+ return firstArg.const
+ }
+
+ return null
+}
+
+/**
+ * Check if a string type has a const value (literal type).
+ */
+fun hasConstValue(node: StringType): Boolean = node.const != null
+
+/**
+ * Check if a node has any const value.
+ */
+fun hasAnyConstValue(node: NodeType): Boolean =
+ when (node) {
+ is StringType -> node.const != null
+ is NumberType -> node.const != null
+ is BooleanType -> node.const != null
+ else -> false
+ }
+
+/**
+ * Check if an OrType contains only primitives with const values (Literal type).
+ */
+fun isLiteralUnion(node: OrType): Boolean = node.orTypes.all { hasAnyConstValue(it) }
+
+/**
+ * Get all const values from a literal union.
+ */
+fun getLiteralValues(node: OrType): List =
+ node.orTypes.mapNotNull { type ->
+ when (type) {
+ is StringType -> type.const
+ is NumberType -> type.const
+ is BooleanType -> type.const
+ else -> null
+ }
+ }
diff --git a/types/kotlin/src/main/kotlin/com/intuit/playerui/xlr/XlrJson.kt b/types/kotlin/src/main/kotlin/com/intuit/playerui/xlr/XlrJson.kt
new file mode 100644
index 0000000..4ced1da
--- /dev/null
+++ b/types/kotlin/src/main/kotlin/com/intuit/playerui/xlr/XlrJson.kt
@@ -0,0 +1,10 @@
+package com.intuit.playerui.xlr
+
+import kotlinx.serialization.json.Json
+
+internal val xlrJson =
+ Json {
+ encodeDefaults = false
+ ignoreUnknownKeys = true
+ isLenient = true
+ }
diff --git a/types/kotlin/src/main/kotlin/com/intuit/playerui/xlr/XlrSerializer.kt b/types/kotlin/src/main/kotlin/com/intuit/playerui/xlr/XlrSerializer.kt
new file mode 100644
index 0000000..d3420fc
--- /dev/null
+++ b/types/kotlin/src/main/kotlin/com/intuit/playerui/xlr/XlrSerializer.kt
@@ -0,0 +1,27 @@
+package com.intuit.playerui.xlr
+
+import kotlinx.serialization.json.JsonObject
+import kotlinx.serialization.json.JsonPrimitive
+import kotlinx.serialization.json.buildJsonObject
+import kotlinx.serialization.json.encodeToJsonElement
+import kotlinx.serialization.json.jsonObject
+
+object XlrSerializer {
+ fun serialize(document: XlrDocument): String = serializeDocument(document).toString()
+
+ // Wire format requires ObjectType fields flattened at the document root level
+ // (not nested under an "objectType" key), so we manually merge the node JSON
+ // with document-level name/source/genericTokens which take precedence.
+ fun serializeDocument(document: XlrDocument): JsonObject =
+ buildJsonObject {
+ val nodeJson = xlrJson.encodeToJsonElement(NodeType.serializer(), document.objectType).jsonObject
+ nodeJson.forEach { (key, value) -> put(key, value) }
+ put("source", JsonPrimitive(document.source))
+ put("name", JsonPrimitive(document.name))
+ document.genericTokens?.let {
+ put("genericTokens", xlrJson.encodeToJsonElement(it))
+ }
+ }
+
+ fun serializeNode(node: NodeType): JsonObject = xlrJson.encodeToJsonElement(NodeType.serializer(), node).jsonObject
+}
diff --git a/types/kotlin/src/main/kotlin/com/intuit/playerui/xlr/XlrTypes.kt b/types/kotlin/src/main/kotlin/com/intuit/playerui/xlr/XlrTypes.kt
new file mode 100644
index 0000000..5c0799f
--- /dev/null
+++ b/types/kotlin/src/main/kotlin/com/intuit/playerui/xlr/XlrTypes.kt
@@ -0,0 +1,540 @@
+@file:OptIn(ExperimentalSerializationApi::class)
+@file:UseSerializers(WholeNumberDoubleSerializer::class)
+
+package com.intuit.playerui.xlr
+
+import kotlinx.serialization.EncodeDefault
+import kotlinx.serialization.ExperimentalSerializationApi
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.SerializationException
+import kotlinx.serialization.Transient
+import kotlinx.serialization.UseSerializers
+import kotlinx.serialization.descriptors.PrimitiveKind
+import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
+import kotlinx.serialization.descriptors.SerialDescriptor
+import kotlinx.serialization.encoding.Decoder
+import kotlinx.serialization.encoding.Encoder
+import kotlinx.serialization.json.JsonClassDiscriminator
+import kotlinx.serialization.json.JsonDecoder
+import kotlinx.serialization.json.JsonElement
+import kotlinx.serialization.json.JsonEncoder
+import kotlinx.serialization.json.JsonNull
+import kotlinx.serialization.json.JsonPrimitive
+import kotlinx.serialization.json.booleanOrNull
+import kotlinx.serialization.json.decodeFromJsonElement
+import kotlinx.serialization.json.encodeToJsonElement
+
+/**
+ * Interface for annotation fields on XLR types.
+ * Mirrors the JS Annotations interface with all 8 annotation properties.
+ */
+interface Annotated {
+ val name: String?
+ val title: String?
+ val description: String?
+ val examples: JsonElement?
+ val default: JsonElement?
+ val see: JsonElement?
+ val comment: String?
+ val meta: Map?
+}
+
+/**
+ * Sealed type representing `false | NodeType` for additional properties/items.
+ */
+@Serializable(with = AdditionalItemsTypeSerializer::class)
+sealed interface AdditionalItemsType {
+ data object None : AdditionalItemsType
+
+ data class Typed(
+ val node: NodeType,
+ ) : AdditionalItemsType
+}
+
+object AdditionalItemsTypeSerializer : KSerializer {
+ override val descriptor: SerialDescriptor =
+ PrimitiveSerialDescriptor("AdditionalItemsType", PrimitiveKind.STRING)
+
+ override fun serialize(encoder: Encoder, value: AdditionalItemsType) {
+ val jsonEncoder =
+ encoder as? JsonEncoder
+ ?: throw SerializationException("AdditionalItemsTypeSerializer only supports JSON format")
+ when (value) {
+ is AdditionalItemsType.None -> jsonEncoder.encodeJsonElement(JsonPrimitive(false))
+ is AdditionalItemsType.Typed ->
+ jsonEncoder.encodeJsonElement(
+ jsonEncoder.json.encodeToJsonElement(NodeType.serializer(), value.node),
+ )
+ }
+ }
+
+ override fun deserialize(decoder: Decoder): AdditionalItemsType {
+ val jsonDecoder =
+ decoder as? JsonDecoder
+ ?: throw SerializationException("AdditionalItemsTypeSerializer only supports JSON format")
+ val element = jsonDecoder.decodeJsonElement()
+ if (element is JsonNull) return AdditionalItemsType.None
+ if (element is JsonPrimitive && element.booleanOrNull == false) return AdditionalItemsType.None
+ return AdditionalItemsType.Typed(
+ jsonDecoder.json.decodeFromJsonElement(NodeType.serializer(), element),
+ )
+ }
+}
+
+/**
+ * Structured check for conditional types: `{ left: NodeType, right: NodeType }`.
+ */
+@Serializable
+data class ConditionalCheck(
+ val left: NodeType,
+ val right: NodeType,
+)
+
+/**
+ * Structured value for conditional types: `{ true: NodeType, false: NodeType }`.
+ */
+@Serializable
+data class ConditionalValue(
+ @SerialName("true") val trueValue: NodeType,
+ @SerialName("false") val falseValue: NodeType,
+)
+
+/**
+ * Base sealed interface for all XLR node types.
+ */
+@Serializable
+@JsonClassDiscriminator("type")
+sealed interface NodeType : Annotated {
+ val type: String
+ val source: String?
+ val genericTokens: List?
+}
+
+@Serializable
+@SerialName("string")
+data class StringType(
+ @Transient override val type: String = "string",
+ val const: String? = null,
+ val enum: List? = null,
+ override val name: String? = null,
+ override val title: String? = null,
+ override val description: String? = null,
+ override val examples: JsonElement? = null,
+ override val default: JsonElement? = null,
+ override val see: JsonElement? = null,
+ override val comment: String? = null,
+ override val meta: Map? = null,
+ override val source: String? = null,
+ override val genericTokens: List? = null,
+) : NodeType
+
+@Serializable
+@SerialName("number")
+data class NumberType(
+ @Transient override val type: String = "number",
+ val const: Double? = null,
+ val enum: List? = null,
+ override val name: String? = null,
+ override val title: String? = null,
+ override val description: String? = null,
+ override val examples: JsonElement? = null,
+ override val default: JsonElement? = null,
+ override val see: JsonElement? = null,
+ override val comment: String? = null,
+ override val meta: Map? = null,
+ override val source: String? = null,
+ override val genericTokens: List? = null,
+) : NodeType
+
+@Serializable
+@SerialName("boolean")
+data class BooleanType(
+ @Transient override val type: String = "boolean",
+ val const: Boolean? = null,
+ override val name: String? = null,
+ override val title: String? = null,
+ override val description: String? = null,
+ override val examples: JsonElement? = null,
+ override val default: JsonElement? = null,
+ override val see: JsonElement? = null,
+ override val comment: String? = null,
+ override val meta: Map? = null,
+ override val source: String? = null,
+ override val genericTokens: List? = null,
+) : NodeType
+
+@Serializable
+@SerialName("null")
+data class NullType(
+ @Transient override val type: String = "null",
+ override val name: String? = null,
+ override val title: String? = null,
+ override val description: String? = null,
+ override val examples: JsonElement? = null,
+ override val default: JsonElement? = null,
+ override val see: JsonElement? = null,
+ override val comment: String? = null,
+ override val meta: Map? = null,
+ override val source: String? = null,
+ override val genericTokens: List? = null,
+) : NodeType
+
+@Serializable
+@SerialName("any")
+data class AnyType(
+ @Transient override val type: String = "any",
+ override val name: String? = null,
+ override val title: String? = null,
+ override val description: String? = null,
+ override val examples: JsonElement? = null,
+ override val default: JsonElement? = null,
+ override val see: JsonElement? = null,
+ override val comment: String? = null,
+ override val meta: Map? = null,
+ override val source: String? = null,
+ override val genericTokens: List? = null,
+) : NodeType
+
+@Serializable
+@SerialName("unknown")
+data class UnknownType(
+ @Transient override val type: String = "unknown",
+ override val name: String? = null,
+ override val title: String? = null,
+ override val description: String? = null,
+ override val examples: JsonElement? = null,
+ override val default: JsonElement? = null,
+ override val see: JsonElement? = null,
+ override val comment: String? = null,
+ override val meta: Map? = null,
+ override val source: String? = null,
+ override val genericTokens: List? = null,
+) : NodeType
+
+@Serializable
+@SerialName("undefined")
+data class UndefinedType(
+ @Transient override val type: String = "undefined",
+ override val name: String? = null,
+ override val title: String? = null,
+ override val description: String? = null,
+ override val examples: JsonElement? = null,
+ override val default: JsonElement? = null,
+ override val see: JsonElement? = null,
+ override val comment: String? = null,
+ override val meta: Map? = null,
+ override val source: String? = null,
+ override val genericTokens: List? = null,
+) : NodeType
+
+@Serializable
+@SerialName("void")
+data class VoidType(
+ @Transient override val type: String = "void",
+ override val name: String? = null,
+ override val title: String? = null,
+ override val description: String? = null,
+ override val examples: JsonElement? = null,
+ override val default: JsonElement? = null,
+ override val see: JsonElement? = null,
+ override val comment: String? = null,
+ override val meta: Map? = null,
+ override val source: String? = null,
+ override val genericTokens: List? = null,
+) : NodeType
+
+@Serializable
+@SerialName("never")
+data class NeverType(
+ @Transient override val type: String = "never",
+ override val name: String? = null,
+ override val title: String? = null,
+ override val description: String? = null,
+ override val examples: JsonElement? = null,
+ override val default: JsonElement? = null,
+ override val see: JsonElement? = null,
+ override val comment: String? = null,
+ override val meta: Map? = null,
+ override val source: String? = null,
+ override val genericTokens: List? = null,
+) : NodeType
+
+@Serializable
+@SerialName("ref")
+data class RefType(
+ @Transient override val type: String = "ref",
+ val ref: String,
+ val genericArguments: List? = null,
+ val property: String? = null,
+ override val name: String? = null,
+ override val title: String? = null,
+ override val description: String? = null,
+ override val examples: JsonElement? = null,
+ override val default: JsonElement? = null,
+ override val see: JsonElement? = null,
+ override val comment: String? = null,
+ override val meta: Map? = null,
+ override val source: String? = null,
+ override val genericTokens: List? = null,
+) : NodeType
+
+@Serializable
+data class ObjectProperty(
+ val required: Boolean,
+ val node: NodeType,
+)
+
+@Serializable
+@SerialName("object")
+data class ObjectType(
+ @Transient override val type: String = "object",
+ @EncodeDefault(EncodeDefault.Mode.ALWAYS)
+ val properties: Map = emptyMap(),
+ val extends: RefType? = null,
+ @EncodeDefault(EncodeDefault.Mode.ALWAYS)
+ val additionalProperties: AdditionalItemsType = AdditionalItemsType.None,
+ override val name: String? = null,
+ override val title: String? = null,
+ override val description: String? = null,
+ override val examples: JsonElement? = null,
+ override val default: JsonElement? = null,
+ override val see: JsonElement? = null,
+ override val comment: String? = null,
+ override val meta: Map? = null,
+ override val source: String? = null,
+ override val genericTokens: List? = null,
+) : NodeType
+
+@Serializable
+@SerialName("array")
+data class ArrayType(
+ @Transient override val type: String = "array",
+ val elementType: NodeType,
+ override val name: String? = null,
+ override val title: String? = null,
+ override val description: String? = null,
+ override val examples: JsonElement? = null,
+ override val default: JsonElement? = null,
+ override val see: JsonElement? = null,
+ override val comment: String? = null,
+ override val meta: Map? = null,
+ override val source: String? = null,
+ override val genericTokens: List? = null,
+) : NodeType
+
+@Serializable
+data class TupleMember(
+ val name: String? = null,
+ val type: NodeType,
+ val optional: Boolean? = null,
+)
+
+@Serializable
+@SerialName("tuple")
+data class TupleType(
+ @Transient override val type: String = "tuple",
+ val elementTypes: List,
+ val minItems: Int,
+ @EncodeDefault(EncodeDefault.Mode.ALWAYS)
+ val additionalItems: AdditionalItemsType = AdditionalItemsType.None,
+ override val name: String? = null,
+ override val title: String? = null,
+ override val description: String? = null,
+ override val examples: JsonElement? = null,
+ override val default: JsonElement? = null,
+ override val see: JsonElement? = null,
+ override val comment: String? = null,
+ override val meta: Map? = null,
+ override val source: String? = null,
+ override val genericTokens: List? = null,
+) : NodeType
+
+@Serializable
+@SerialName("record")
+data class RecordType(
+ @Transient override val type: String = "record",
+ val keyType: NodeType,
+ val valueType: NodeType,
+ override val name: String? = null,
+ override val title: String? = null,
+ override val description: String? = null,
+ override val examples: JsonElement? = null,
+ override val default: JsonElement? = null,
+ override val see: JsonElement? = null,
+ override val comment: String? = null,
+ override val meta: Map? = null,
+ override val source: String? = null,
+ override val genericTokens: List? = null,
+) : NodeType
+
+// Class-level @SerialName("or") sets the polymorphic discriminator value ("type": "or"),
+// while property-level @SerialName("or") aliases the Kotlin field `orTypes` to JSON key "or".
+@Serializable
+@SerialName("or")
+data class OrType(
+ @Transient override val type: String = "or",
+ @SerialName("or")
+ val orTypes: List,
+ override val name: String? = null,
+ override val title: String? = null,
+ override val description: String? = null,
+ override val examples: JsonElement? = null,
+ override val default: JsonElement? = null,
+ override val see: JsonElement? = null,
+ override val comment: String? = null,
+ override val meta: Map? = null,
+ override val source: String? = null,
+ override val genericTokens: List? = null,
+) : NodeType
+
+// Class-level @SerialName("and") sets the polymorphic discriminator value ("type": "and"),
+// while property-level @SerialName("and") aliases the Kotlin field `andTypes` to JSON key "and".
+@Serializable
+@SerialName("and")
+data class AndType(
+ @Transient override val type: String = "and",
+ @SerialName("and")
+ val andTypes: List,
+ override val name: String? = null,
+ override val title: String? = null,
+ override val description: String? = null,
+ override val examples: JsonElement? = null,
+ override val default: JsonElement? = null,
+ override val see: JsonElement? = null,
+ override val comment: String? = null,
+ override val meta: Map? = null,
+ override val source: String? = null,
+ override val genericTokens: List? = null,
+) : NodeType
+
+@Serializable
+@SerialName("template")
+data class TemplateLiteralType(
+ @Transient override val type: String = "template",
+ val format: String,
+ override val name: String? = null,
+ override val title: String? = null,
+ override val description: String? = null,
+ override val examples: JsonElement? = null,
+ override val default: JsonElement? = null,
+ override val see: JsonElement? = null,
+ override val comment: String? = null,
+ override val meta: Map? = null,
+ override val source: String? = null,
+ override val genericTokens: List? = null,
+) : NodeType
+
+@Serializable
+@SerialName("conditional")
+data class ConditionalType(
+ @Transient override val type: String = "conditional",
+ val check: ConditionalCheck,
+ val value: ConditionalValue,
+ override val name: String? = null,
+ override val title: String? = null,
+ override val description: String? = null,
+ override val examples: JsonElement? = null,
+ override val default: JsonElement? = null,
+ override val see: JsonElement? = null,
+ override val comment: String? = null,
+ override val meta: Map? = null,
+ override val source: String? = null,
+ override val genericTokens: List? = null,
+) : NodeType
+
+@Serializable
+data class FunctionParameter(
+ val name: String,
+ val type: NodeType,
+ val optional: Boolean? = null,
+ val default: NodeType? = null,
+)
+
+@Serializable
+@SerialName("function")
+data class FunctionType(
+ @Transient override val type: String = "function",
+ val parameters: List,
+ val returnType: NodeType? = null,
+ override val name: String? = null,
+ override val title: String? = null,
+ override val description: String? = null,
+ override val examples: JsonElement? = null,
+ override val default: JsonElement? = null,
+ override val see: JsonElement? = null,
+ override val comment: String? = null,
+ override val meta: Map? = null,
+ override val source: String? = null,
+ override val genericTokens: List? = null,
+) : NodeType
+
+@Serializable
+data class ParamTypeNode(
+ val symbol: String,
+ val constraints: NodeType? = null,
+ val default: NodeType? = null,
+)
+
+/**
+ * Named type wrapper that adds name and source information to any node type.
+ */
+@Serializable
+data class NamedType(
+ @SerialName("name") val typeName: String,
+ val source: String,
+ val genericTokens: List? = null,
+ val node: T,
+)
+
+/**
+ * Named type wrapper with required generic tokens.
+ */
+@Serializable
+data class NamedTypeWithGenerics(
+ @SerialName("name") val typeName: String,
+ val source: String,
+ val genericTokens: List,
+ val node: T,
+)
+
+/**
+ * Node type with generic tokens attached.
+ */
+@Serializable
+data class NodeTypeWithGenerics(
+ val node: T,
+ val genericTokens: List,
+)
+
+/**
+ * Complete XLR document representing a named object type (asset definition).
+ */
+@Serializable
+data class XlrDocument(
+ val name: String,
+ val source: String,
+ val objectType: ObjectType,
+ val genericTokens: List? = null,
+) {
+ fun toObjectType(): ObjectType = objectType
+}
+
+object WholeNumberDoubleSerializer : KSerializer {
+ override val descriptor: SerialDescriptor =
+ PrimitiveSerialDescriptor("WholeNumberDouble", PrimitiveKind.DOUBLE)
+
+ override fun serialize(encoder: Encoder, value: Double) {
+ val jsonEncoder =
+ encoder as? JsonEncoder
+ ?: throw SerializationException("WholeNumberDoubleSerializer only supports JSON format")
+ if (value.isFinite() && value % 1.0 == 0.0) {
+ jsonEncoder.encodeJsonElement(JsonPrimitive(value.toLong()))
+ } else {
+ jsonEncoder.encodeJsonElement(JsonPrimitive(value))
+ }
+ }
+
+ override fun deserialize(decoder: Decoder): Double = decoder.decodeDouble()
+}
diff --git a/types/kotlin/src/main/kotlin/com/intuit/playerui/xlr/XlrUtility.kt b/types/kotlin/src/main/kotlin/com/intuit/playerui/xlr/XlrUtility.kt
new file mode 100644
index 0000000..dddf285
--- /dev/null
+++ b/types/kotlin/src/main/kotlin/com/intuit/playerui/xlr/XlrUtility.kt
@@ -0,0 +1,49 @@
+package com.intuit.playerui.xlr
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class Capability(
+ val name: String,
+ val provides: List,
+)
+
+@Serializable
+data class Manifest(
+ val pluginName: String,
+ val capabilities: Map>? = null,
+ val customPrimitives: List? = null,
+)
+
+@Serializable
+data class TSManifest(
+ val pluginName: String,
+ val capabilities: Map>>,
+)
+
+sealed interface TransformInput {
+ data class Named(
+ val namedType: NamedType,
+ ) : TransformInput
+
+ data class Anonymous(
+ val nodeType: NodeType,
+ ) : TransformInput
+}
+
+sealed interface TransformOutput {
+ data class Named(
+ val namedType: NamedType,
+ ) : TransformOutput
+
+ data class Anonymous(
+ val nodeType: NodeType,
+ ) : TransformOutput
+}
+
+fun interface TransformFunction {
+ fun transform(
+ input: TransformInput,
+ capabilityType: String,
+ ): TransformOutput
+}
diff --git a/types/kotlin/src/test/kotlin/com/intuit/playerui/xlr/TestFixtures.kt b/types/kotlin/src/test/kotlin/com/intuit/playerui/xlr/TestFixtures.kt
new file mode 100644
index 0000000..4ada117
--- /dev/null
+++ b/types/kotlin/src/test/kotlin/com/intuit/playerui/xlr/TestFixtures.kt
@@ -0,0 +1,38 @@
+package com.intuit.playerui.xlr
+
+object TestFixtures {
+ val choiceAssetJson: String by lazy {
+ TestFixtures::class.java
+ .getResourceAsStream("/test.json")!!
+ .bufferedReader()
+ .readText()
+ }
+
+ /** List of all 19 NodeType instances for cross-test reuse. */
+ val allNodeTypeInstances: List by lazy {
+ listOf(
+ StringType(const = "x"),
+ NumberType(const = 1.0),
+ BooleanType(const = true),
+ NullType(),
+ AnyType(),
+ UnknownType(),
+ UndefinedType(),
+ VoidType(),
+ NeverType(),
+ RefType(ref = "Foo"),
+ ObjectType(properties = mapOf("a" to ObjectProperty(required = true, node = StringType()))),
+ ArrayType(elementType = StringType()),
+ TupleType(elementTypes = listOf(TupleMember(type = StringType())), minItems = 1),
+ RecordType(keyType = StringType(), valueType = AnyType()),
+ OrType(orTypes = listOf(StringType(), NumberType())),
+ AndType(andTypes = listOf(StringType(), NumberType())),
+ TemplateLiteralType(format = ".*"),
+ ConditionalType(
+ check = ConditionalCheck(StringType(), NumberType()),
+ value = ConditionalValue(BooleanType(), NullType()),
+ ),
+ FunctionType(parameters = listOf(FunctionParameter(name = "x", type = StringType()))),
+ )
+ }
+}
diff --git a/types/kotlin/src/test/kotlin/com/intuit/playerui/xlr/XlrDeserializerTest.kt b/types/kotlin/src/test/kotlin/com/intuit/playerui/xlr/XlrDeserializerTest.kt
new file mode 100644
index 0000000..43c6be5
--- /dev/null
+++ b/types/kotlin/src/test/kotlin/com/intuit/playerui/xlr/XlrDeserializerTest.kt
@@ -0,0 +1,637 @@
+package com.intuit.playerui.xlr
+
+import kotlinx.serialization.json.Json
+import kotlinx.serialization.json.JsonNull
+import kotlinx.serialization.json.JsonPrimitive
+import kotlinx.serialization.json.jsonObject
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertIs
+import kotlin.test.assertNotNull
+import kotlin.test.assertNull
+import kotlin.test.assertTrue
+
+class XlrDeserializerTest {
+ private val fixtureJson: String get() = TestFixtures.choiceAssetJson
+ private val doc by lazy { XlrDeserializer.deserialize(fixtureJson) }
+
+ @Test
+ fun `deserializes document name and source`() {
+ assertEquals("ChoiceAsset", doc.name)
+ assertTrue(doc.source.contains("choice/types.ts"))
+ }
+
+ @Test
+ fun `deserializes document objectType as object`() {
+ assertEquals("object", doc.objectType.type)
+ }
+
+ @Test
+ fun `toObjectType returns equivalent ObjectType`() {
+ val objType = doc.toObjectType()
+ assertEquals(doc.objectType, objType)
+ }
+
+ @Test
+ fun `deserializes properties map with correct keys`() {
+ val props = doc.objectType.properties
+ assertTrue(props.containsKey("title"))
+ assertTrue(props.containsKey("note"))
+ assertTrue(props.containsKey("binding"))
+ assertTrue(props.containsKey("items"))
+ assertTrue(props.containsKey("metaData"))
+ assertEquals(5, props.size)
+ }
+
+ @Test
+ fun `deserializes title property as RefType`() {
+ val titleProp = doc.objectType.properties["title"]!!
+ assertEquals(false, titleProp.required)
+ val node = assertIs(titleProp.node)
+ assertEquals("AssetWrapper", node.ref)
+ assertNotNull(node.genericArguments)
+ assertEquals(1, node.genericArguments!!.size)
+ val ga = assertIs(node.genericArguments!!.first())
+ assertEquals("AnyTextAsset", ga.ref)
+ }
+
+ @Test
+ fun `deserializes binding property as RefType`() {
+ val bindingNode = assertIs(doc.objectType.properties["binding"]!!.node)
+ assertEquals("Binding", bindingNode.ref)
+ assertEquals("ChoiceAsset.binding", bindingNode.title)
+ assertEquals("The location in the data-model to store the data", bindingNode.description)
+ }
+
+ @Test
+ fun `deserializes items property as ArrayType with nested ObjectType`() {
+ val itemsNode = assertIs(doc.objectType.properties["items"]!!.node)
+ val elementType = assertIs(itemsNode.elementType)
+ assertTrue(elementType.properties.containsKey("id"))
+ assertTrue(elementType.properties.containsKey("label"))
+ assertTrue(elementType.properties.containsKey("value"))
+ }
+
+ @Test
+ fun `deserializes nested ChoiceItem id as required StringType`() {
+ val itemsNode = assertIs(doc.objectType.properties["items"]!!.node)
+ val choiceItem = assertIs(itemsNode.elementType)
+ val idProp = choiceItem.properties["id"]!!
+ assertEquals(true, idProp.required)
+ assertIs(idProp.node)
+ }
+
+ @Test
+ fun `deserializes nested ValueType as OrType with 4 members`() {
+ val itemsNode = assertIs(doc.objectType.properties["items"]!!.node)
+ val choiceItem = assertIs(itemsNode.elementType)
+ val valueNode = assertIs(choiceItem.properties["value"]!!.node)
+ assertEquals(4, valueNode.orTypes.size)
+ assertIs(valueNode.orTypes[0])
+ assertIs(valueNode.orTypes[1])
+ assertIs(valueNode.orTypes[2])
+ assertIs(valueNode.orTypes[3])
+ }
+
+ @Test
+ fun `deserializes BeaconDataType as OrType with RecordType`() {
+ val metaDataNode = assertIs(doc.objectType.properties["metaData"]!!.node)
+ val beaconNode = assertIs(metaDataNode.properties["beacon"]!!.node)
+ assertEquals(2, beaconNode.orTypes.size)
+ assertIs(beaconNode.orTypes[0])
+ val record = assertIs(beaconNode.orTypes[1])
+ assertIs(record.keyType)
+ assertIs(record.valueType)
+ }
+
+ @Test
+ fun `deserializes generic tokens on document`() {
+ assertNotNull(doc.genericTokens)
+ assertEquals(1, doc.genericTokens!!.size)
+ val token = doc.genericTokens!!.first()
+ assertEquals("AnyTextAsset", token.symbol)
+ val constraint = assertIs(token.constraints)
+ assertEquals("Asset", constraint.ref)
+ val default = assertIs(token.default)
+ assertEquals("Asset", default.ref)
+ }
+
+ @Test
+ fun `deserializes generic tokens on nested ChoiceItem`() {
+ val itemsNode = assertIs(doc.objectType.properties["items"]!!.node)
+ val choiceItem = assertIs(itemsNode.elementType)
+ assertNotNull(choiceItem.genericTokens)
+ assertEquals(1, choiceItem.genericTokens!!.size)
+ val token = choiceItem.genericTokens!!.first()
+ assertEquals("AnyTextAsset", token.symbol)
+ val constraint = assertIs(token.constraints)
+ assertEquals("Asset", constraint.ref)
+ val default = assertIs(token.default)
+ assertEquals("Asset", default.ref)
+ }
+
+ @Test
+ fun `deserializes source on nested ChoiceItem`() {
+ val itemsNode = assertIs(doc.objectType.properties["items"]!!.node)
+ val choiceItem = assertIs(itemsNode.elementType)
+ assertNotNull(choiceItem.source)
+ assertTrue(choiceItem.source!!.contains("choice/types.ts"))
+ }
+
+ @Test
+ fun `deserializes source on nested ValueType`() {
+ val itemsNode = assertIs(doc.objectType.properties["items"]!!.node)
+ val choiceItem = assertIs(itemsNode.elementType)
+ val valueNode = assertIs(choiceItem.properties["value"]!!.node)
+ assertNotNull(valueNode.source)
+ assertTrue(valueNode.source!!.contains("choice/types.ts"))
+ }
+
+ @Test
+ fun `deserializes source on nested BeaconMetaData`() {
+ val metaDataNode = assertIs(doc.objectType.properties["metaData"]!!.node)
+ assertNotNull(metaDataNode.source)
+ assertTrue(metaDataNode.source!!.contains("beacon"))
+ }
+
+ @Test
+ fun `deserializes source on nested BeaconDataType`() {
+ val metaDataNode = assertIs(doc.objectType.properties["metaData"]!!.node)
+ val beaconNode = assertIs(metaDataNode.properties["beacon"]!!.node)
+ assertNotNull(beaconNode.source)
+ assertTrue(beaconNode.source!!.contains("beacon"))
+ }
+
+ @Test
+ fun `root objectType carries source and genericTokens`() {
+ assertNotNull(doc.objectType.source)
+ assertTrue(doc.objectType.source!!.contains("choice/types.ts"))
+ assertNotNull(doc.objectType.genericTokens)
+ assertEquals(1, doc.objectType.genericTokens!!.size)
+ assertEquals(
+ "AnyTextAsset",
+ doc.objectType.genericTokens!!
+ .first()
+ .symbol,
+ )
+ }
+
+ @Test
+ fun `nodes without source or genericTokens default to null`() {
+ val bindingNode = assertIs(doc.objectType.properties["binding"]!!.node)
+ assertNull(bindingNode.source)
+ assertNull(bindingNode.genericTokens)
+ }
+
+ @Test
+ fun `deserializes extends clause with Asset choice`() {
+ val ext = doc.objectType.extends
+ assertNotNull(ext)
+ assertEquals("Asset<\"choice\">", ext.ref)
+ assertNotNull(ext.genericArguments)
+ assertEquals(1, ext.genericArguments!!.size)
+ val arg = assertIs(ext.genericArguments!!.first())
+ assertEquals("choice", arg.const)
+ }
+
+ @Test
+ fun `deserializes additionalProperties as None for false`() {
+ assertEquals(AdditionalItemsType.None, doc.objectType.additionalProperties)
+ }
+
+ @Test
+ fun `deserializes annotation title on document`() {
+ assertEquals("ChoiceAsset", doc.objectType.title)
+ }
+
+ @Test
+ fun `deserializes annotation description on document`() {
+ assertNotNull(doc.objectType.description)
+ assertTrue(doc.objectType.description!!.startsWith("A choice asset"))
+ }
+
+ @Test
+ fun `deserializes annotation title on nested nodes`() {
+ val titleNode = assertIs(doc.objectType.properties["title"]!!.node)
+ assertEquals("ChoiceAsset.title", titleNode.title)
+ }
+
+ @Test
+ fun `parseNode throws for missing type field`() {
+ val json =
+ Json
+ .parseToJsonElement("""{"ref": "foo"}""")
+ assertFailsWith {
+ XlrDeserializer.parseNode(json)
+ }
+ }
+
+ @Test
+ fun `parseNode throws for unknown type`() {
+ val json =
+ Json
+ .parseToJsonElement("""{"type": "foobar"}""")
+ assertFailsWith {
+ XlrDeserializer.parseNode(json)
+ }
+ }
+
+ @Test
+ fun `parseNode handles JsonNull as NullType`() {
+ val node = XlrDeserializer.parseNode(JsonNull)
+ assertIs(node)
+ }
+
+ @Test
+ fun `parseNode deserializes simple string type`() {
+ val json =
+ Json
+ .parseToJsonElement("""{"type": "string", "const": "hello"}""")
+ val node = assertIs(XlrDeserializer.parseNode(json))
+ assertEquals("hello", node.const)
+ }
+
+ @Test
+ fun `parseNode deserializes string type with enum`() {
+ val json =
+ Json.parseToJsonElement(
+ """{"type": "string", "enum": ["a", "b", "c"]}""",
+ )
+ val node = assertIs(XlrDeserializer.parseNode(json))
+ assertEquals(listOf("a", "b", "c"), node.enum)
+ }
+
+ @Test
+ fun `parseNode deserializes number type with enum`() {
+ val json =
+ Json
+ .parseToJsonElement("""{"type": "number", "enum": [1.0, 2.0, 3.0]}""")
+ val node = assertIs(XlrDeserializer.parseNode(json))
+ assertEquals(listOf(1.0, 2.0, 3.0), node.enum)
+ }
+
+ @Test
+ fun `parseNode deserializes boolean type with const`() {
+ val json =
+ Json
+ .parseToJsonElement("""{"type": "boolean", "const": true}""")
+ val node = assertIs(XlrDeserializer.parseNode(json))
+ assertEquals(true, node.const)
+ }
+
+ @Test
+ fun `parseNode deserializes template literal type`() {
+ val json =
+ Json.parseToJsonElement(
+ """{"type": "template", "format": "hello_\\d+"}""",
+ )
+ val node = assertIs(XlrDeserializer.parseNode(json))
+ assertEquals("hello_\\d+", node.format)
+ }
+
+ @Test
+ fun `parseNode deserializes conditional type with structured check and value`() {
+ val json =
+ Json.parseToJsonElement(
+ """
+ {
+ "type": "conditional",
+ "check": {"left": {"type": "string"}, "right": {"type": "number"}},
+ "value": {"true": {"type": "boolean"}, "false": {"type": "null"}}
+ }
+ """.trimIndent(),
+ )
+ val node = assertIs(XlrDeserializer.parseNode(json))
+ assertIs(node.check.left)
+ assertIs(node.check.right)
+ assertIs(node.value.trueValue)
+ assertIs(node.value.falseValue)
+ }
+
+ @Test
+ fun `parseNode deserializes function type`() {
+ val json =
+ Json.parseToJsonElement(
+ """
+ {
+ "type": "function",
+ "parameters": [
+ {"name": "x", "type": {"type": "string"}},
+ {"name": "y", "type": {"type": "number"}, "optional": true}
+ ],
+ "returnType": {"type": "boolean"}
+ }
+ """.trimIndent(),
+ )
+ val node = assertIs(XlrDeserializer.parseNode(json))
+ assertEquals(2, node.parameters.size)
+ assertEquals("x", node.parameters[0].name)
+ assertIs(node.parameters[0].type)
+ assertNull(node.parameters[0].optional)
+ assertEquals("y", node.parameters[1].name)
+ assertEquals(true, node.parameters[1].optional)
+ assertIs(node.returnType)
+ }
+
+ @Test
+ fun `parseNode deserializes function type with parameter default`() {
+ val json =
+ Json.parseToJsonElement(
+ """
+ {
+ "type": "function",
+ "parameters": [
+ {"name": "x", "type": {"type": "string"}, "default": {"type": "string", "const": "hi"}}
+ ]
+ }
+ """.trimIndent(),
+ )
+ val node = assertIs(XlrDeserializer.parseNode(json))
+ assertEquals(1, node.parameters.size)
+ val defaultNode = assertIs(node.parameters[0].default)
+ assertEquals("hi", defaultNode.const)
+ }
+
+ @Test
+ fun `parseNode deserializes ref type with property`() {
+ val json =
+ Json.parseToJsonElement(
+ """{"type": "ref", "ref": "Foo", "property": "bar"}""",
+ )
+ val node = assertIs(XlrDeserializer.parseNode(json))
+ assertEquals("Foo", node.ref)
+ assertEquals("bar", node.property)
+ }
+
+ @Test
+ fun `parseNode deserializes tuple type`() {
+ val json =
+ Json.parseToJsonElement(
+ """
+ {
+ "type": "tuple",
+ "elementTypes": [
+ {"name": "first", "type": {"type": "string"}},
+ {"name": "second", "type": {"type": "number"}, "optional": true}
+ ],
+ "minItems": 1,
+ "additionalItems": false
+ }
+ """.trimIndent(),
+ )
+ val node = assertIs(XlrDeserializer.parseNode(json))
+ assertEquals(2, node.elementTypes.size)
+ assertEquals("first", node.elementTypes[0].name)
+ assertIs(node.elementTypes[0].type)
+ assertEquals(true, node.elementTypes[1].optional)
+ assertEquals(1, node.minItems)
+ assertEquals(AdditionalItemsType.None, node.additionalItems)
+ }
+
+ @Test
+ fun `parseNode deserializes tuple member without name`() {
+ val json =
+ Json.parseToJsonElement(
+ """
+ {
+ "type": "tuple",
+ "elementTypes": [{"type": {"type": "string"}}],
+ "minItems": 1
+ }
+ """.trimIndent(),
+ )
+ val node = assertIs(XlrDeserializer.parseNode(json))
+ assertEquals(1, node.elementTypes.size)
+ assertNull(node.elementTypes[0].name)
+ assertIs(node.elementTypes[0].type)
+ }
+
+ @Test
+ fun `parseNode deserializes and type`() {
+ val json =
+ Json.parseToJsonElement(
+ """{"type": "and", "and": [{"type": "string"}, {"type": "number"}]}""",
+ )
+ val node = assertIs(XlrDeserializer.parseNode(json))
+ assertEquals(2, node.andTypes.size)
+ assertIs(node.andTypes[0])
+ assertIs(node.andTypes[1])
+ }
+
+ @Test
+ fun `parseNode deserializes all simple types`() {
+ val simpleTypes = listOf("any", "unknown", "undefined", "void", "never", "null")
+ for (typeName in simpleTypes) {
+ val json =
+ Json
+ .parseToJsonElement("""{"type": "$typeName"}""")
+ val node = XlrDeserializer.parseNode(json)
+ assertEquals(typeName, node.type, "Failed for type: $typeName")
+ }
+ }
+
+ @Test
+ fun `additionalProperties Typed parses NodeType`() {
+ val json =
+ Json.parseToJsonElement(
+ """
+ {
+ "type": "object",
+ "properties": {},
+ "additionalProperties": {"type": "string"}
+ }
+ """.trimIndent(),
+ )
+ val node = assertIs(XlrDeserializer.parseNode(json))
+ val ap = assertIs(node.additionalProperties)
+ assertIs(ap.node)
+ }
+
+ @Test
+ fun `additionalProperties null becomes None`() {
+ val json =
+ Json.parseToJsonElement(
+ """{"type": "object", "properties": {}}""",
+ )
+ val node = assertIs(XlrDeserializer.parseNode(json))
+ assertEquals(AdditionalItemsType.None, node.additionalProperties)
+ }
+
+ @Test
+ fun `parseNode preserves source and genericTokens on node`() {
+ val json =
+ Json.parseToJsonElement(
+ """
+ {
+ "type": "object",
+ "properties": {},
+ "source": "foo.ts",
+ "name": "Foo",
+ "genericTokens": [
+ {"symbol": "T", "constraints": {"type": "ref", "ref": "Bar"}}
+ ]
+ }
+ """.trimIndent(),
+ )
+ val node = assertIs(XlrDeserializer.parseNode(json))
+ assertEquals("foo.ts", node.source)
+ assertEquals("Foo", node.name)
+ assertNotNull(node.genericTokens)
+ assertEquals(1, node.genericTokens!!.size)
+ assertEquals("T", node.genericTokens!!.first().symbol)
+ }
+
+ @Test
+ fun `throws on RefType missing ref`() {
+ val json =
+ Json.parseToJsonElement(
+ """{"type": "ref"}""",
+ )
+ assertFailsWith {
+ XlrDeserializer.parseNode(json)
+ }
+ }
+
+ @Test
+ fun `throws on TemplateLiteralType missing format`() {
+ val json =
+ Json.parseToJsonElement(
+ """{"type": "template"}""",
+ )
+ assertFailsWith {
+ XlrDeserializer.parseNode(json)
+ }
+ }
+
+ @Test
+ fun `throws on ArrayType missing elementType`() {
+ val json =
+ Json.parseToJsonElement(
+ """{"type": "array"}""",
+ )
+ assertFailsWith {
+ XlrDeserializer.parseNode(json)
+ }
+ }
+
+ @Test
+ fun `throws on RecordType missing keyType`() {
+ val json =
+ Json.parseToJsonElement(
+ """{"type": "record", "valueType": {"type": "any"}}""",
+ )
+ assertFailsWith {
+ XlrDeserializer.parseNode(json)
+ }
+ }
+
+ @Test
+ fun `throws on ConditionalType missing check`() {
+ val json =
+ Json.parseToJsonElement(
+ """
+ {
+ "type": "conditional",
+ "value": {"true": {"type": "string"}, "false": {"type": "null"}}
+ }
+ """.trimIndent(),
+ )
+ assertFailsWith {
+ XlrDeserializer.parseNode(json)
+ }
+ }
+
+ @Test
+ fun `parseDocument throws for non-ObjectType root`() {
+ val json =
+ Json
+ .parseToJsonElement(
+ """{"type": "string", "const": "hello"}""",
+ ).jsonObject
+ assertFailsWith {
+ XlrDeserializer.parseDocument(json)
+ }
+ }
+
+ @Test
+ fun `parseDocument throws when name is missing`() {
+ val json =
+ Json
+ .parseToJsonElement(
+ """{"type": "object", "properties": {}, "additionalProperties": false, "source": "test.ts"}""",
+ ).jsonObject
+ assertFailsWith {
+ XlrDeserializer.parseDocument(json)
+ }
+ }
+
+ @Test
+ fun `parseDocument throws when source is missing`() {
+ val json =
+ Json
+ .parseToJsonElement(
+ """{"type": "object", "properties": {}, "additionalProperties": false, "name": "Test"}""",
+ ).jsonObject
+ assertFailsWith {
+ XlrDeserializer.parseDocument(json)
+ }
+ }
+
+ @Test
+ fun `deserialize throws for invalid JSON string`() {
+ assertFailsWith {
+ XlrDeserializer.deserialize("not valid json {{{")
+ }
+ }
+
+ @Test
+ fun `parseNode throws for JsonPrimitive`() {
+ assertFailsWith {
+ XlrDeserializer.parseNode(JsonPrimitive("hello"))
+ }
+ }
+
+ @Test
+ fun `parseNode deserializes OrType with source and annotations`() {
+ val json =
+ Json.parseToJsonElement(
+ """{"type": "or", "or": [{"type": "string"}], "source": "test.ts", "title": "MyUnion"}""",
+ )
+ val node = assertIs(XlrDeserializer.parseNode(json))
+ assertEquals("test.ts", node.source)
+ assertEquals("MyUnion", node.title)
+ }
+
+ @Test
+ fun `parseDocument with genericTokens as explicit JsonNull`() {
+ val json =
+ Json
+ .parseToJsonElement(
+ """
+ {
+ "type": "object", "properties": {}, "additionalProperties": false,
+ "name": "Test", "source": "test.ts", "genericTokens": null
+ }
+ """.trimIndent(),
+ ).jsonObject
+ val doc = XlrDeserializer.parseDocument(json)
+ assertNull(doc.genericTokens)
+ }
+
+ @Test
+ fun `parseDocument with no genericTokens key`() {
+ val json =
+ Json
+ .parseToJsonElement(
+ """
+ {
+ "type": "object", "properties": {}, "additionalProperties": false,
+ "name": "Test", "source": "test.ts"
+ }
+ """.trimIndent(),
+ ).jsonObject
+ val doc = XlrDeserializer.parseDocument(json)
+ assertNull(doc.genericTokens)
+ }
+}
diff --git a/types/kotlin/src/test/kotlin/com/intuit/playerui/xlr/XlrGuardsTest.kt b/types/kotlin/src/test/kotlin/com/intuit/playerui/xlr/XlrGuardsTest.kt
new file mode 100644
index 0000000..8be6746
--- /dev/null
+++ b/types/kotlin/src/test/kotlin/com/intuit/playerui/xlr/XlrGuardsTest.kt
@@ -0,0 +1,238 @@
+package com.intuit.playerui.xlr
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertNull
+import kotlin.test.assertTrue
+
+class XlrGuardsTest {
+ @Test
+ fun `isPrimitiveType returns true for all primitive types`() {
+ val primitives = TestFixtures.allNodeTypeInstances.filter { isPrimitiveType(it) }
+ for (node in primitives) {
+ assertTrue(isPrimitiveType(node), "Expected true for ${node::class.simpleName}")
+ }
+ }
+
+ @Test
+ fun `isPrimitiveType returns false for non-primitive types`() {
+ val nonPrimitives = TestFixtures.allNodeTypeInstances.filter { !isPrimitiveType(it) }
+ for (node in nonPrimitives) {
+ assertFalse(isPrimitiveType(node), "Expected false for ${node::class.simpleName}")
+ }
+ }
+
+ @Test
+ fun `isAssetWrapperRef positive`() {
+ assertTrue(isAssetWrapperRef(RefType(ref = "AssetWrapper")))
+ assertTrue(isAssetWrapperRef(RefType(ref = "AssetWrapper")))
+ }
+
+ @Test
+ fun `isAssetWrapperRef negative`() {
+ assertFalse(isAssetWrapperRef(RefType(ref = "Asset")))
+ assertFalse(isAssetWrapperRef(StringType()))
+ }
+
+ @Test
+ fun `isAssetRef positive`() {
+ assertTrue(isAssetRef(RefType(ref = "Asset<\"choice\">")))
+ assertTrue(isAssetRef(RefType(ref = "Asset")))
+ }
+
+ @Test
+ fun `isAssetRef negative`() {
+ assertFalse(isAssetRef(RefType(ref = "AssetWrapper")))
+ assertFalse(isAssetRef(StringType()))
+ }
+
+ @Test
+ fun `isBindingRef positive`() {
+ assertTrue(isBindingRef(RefType(ref = "Binding")))
+ assertTrue(isBindingRef(RefType(ref = "Binding")))
+ }
+
+ @Test
+ fun `isBindingRef negative`() {
+ assertFalse(isBindingRef(RefType(ref = "Expression")))
+ assertFalse(isBindingRef(NumberType()))
+ }
+
+ @Test
+ fun `isExpressionRef positive`() {
+ assertTrue(isExpressionRef(RefType(ref = "Expression")))
+ assertTrue(isExpressionRef(RefType(ref = "Expression")))
+ }
+
+ @Test
+ fun `isExpressionRef negative`() {
+ assertFalse(isExpressionRef(RefType(ref = "Binding")))
+ assertFalse(isExpressionRef(BooleanType()))
+ }
+
+ @Test
+ fun `extractAssetTypeConstant extracts const from generic arg`() {
+ val ref =
+ RefType(
+ ref = "Asset<\"choice\">",
+ genericArguments = listOf(StringType(const = "choice")),
+ )
+ assertEquals("choice", extractAssetTypeConstant(ref))
+ }
+
+ @Test
+ fun `extractAssetTypeConstant returns null for non-Asset ref`() {
+ val ref = RefType(ref = "Binding")
+ assertNull(extractAssetTypeConstant(ref))
+ }
+
+ @Test
+ fun `extractAssetTypeConstant returns null for null ref`() {
+ assertNull(extractAssetTypeConstant(null))
+ }
+
+ @Test
+ fun `extractAssetTypeConstant returns null for empty genericArguments`() {
+ val ref = RefType(ref = "Asset<\"choice\">", genericArguments = emptyList())
+ assertNull(extractAssetTypeConstant(ref))
+ }
+
+ @Test
+ fun `extractAssetTypeConstant returns null for non-string generic arg`() {
+ val ref =
+ RefType(
+ ref = "Asset<\"choice\">",
+ genericArguments = listOf(NumberType()),
+ )
+ assertNull(extractAssetTypeConstant(ref))
+ }
+
+ @Test
+ fun `hasConstValue returns true when const is set`() {
+ assertTrue(hasConstValue(StringType(const = "hello")))
+ }
+
+ @Test
+ fun `hasConstValue returns false when const is null`() {
+ assertFalse(hasConstValue(StringType()))
+ }
+
+ @Test
+ fun `hasAnyConstValue returns true for string with const`() {
+ assertTrue(hasAnyConstValue(StringType(const = "x")))
+ }
+
+ @Test
+ fun `hasAnyConstValue returns true for number with const`() {
+ assertTrue(hasAnyConstValue(NumberType(const = 42.0)))
+ }
+
+ @Test
+ fun `hasAnyConstValue returns true for boolean with const`() {
+ assertTrue(hasAnyConstValue(BooleanType(const = true)))
+ }
+
+ @Test
+ fun `hasAnyConstValue returns false for types without const`() {
+ assertFalse(hasAnyConstValue(StringType()))
+ assertFalse(hasAnyConstValue(ObjectType(properties = emptyMap())))
+ assertFalse(hasAnyConstValue(NullType()))
+ }
+
+ @Test
+ fun `isLiteralUnion returns true when all members have const`() {
+ val union =
+ OrType(
+ orTypes =
+ listOf(
+ StringType(const = "a"),
+ StringType(const = "b"),
+ NumberType(const = 1.0),
+ ),
+ )
+ assertTrue(isLiteralUnion(union))
+ }
+
+ @Test
+ fun `isLiteralUnion returns false when any member lacks const`() {
+ val union =
+ OrType(
+ orTypes =
+ listOf(
+ StringType(const = "a"),
+ StringType(),
+ ),
+ )
+ assertFalse(isLiteralUnion(union))
+ }
+
+ @Test
+ fun `getLiteralValues extracts all const values`() {
+ val union =
+ OrType(
+ orTypes =
+ listOf(
+ StringType(const = "a"),
+ NumberType(const = 2.0),
+ BooleanType(const = false),
+ ),
+ )
+ assertEquals(listOf("a", 2.0, false), getLiteralValues(union))
+ }
+
+ @Test
+ fun `getLiteralValues skips members without const`() {
+ val union =
+ OrType(
+ orTypes =
+ listOf(
+ StringType(const = "a"),
+ NullType(),
+ NumberType(const = 3.0),
+ ),
+ )
+ assertEquals(listOf("a", 3.0), getLiteralValues(union))
+ }
+
+ @Test
+ fun `isAssetRef returns false for similar but non-matching ref`() {
+ assertFalse(isAssetRef(RefType(ref = "Assets")))
+ }
+
+ @Test
+ fun `isBindingRef returns false for prefix-only match`() {
+ assertFalse(isBindingRef(RefType(ref = "BindingFoo")))
+ }
+
+ @Test
+ fun `isExpressionRef returns false for prefix-only match`() {
+ assertFalse(isExpressionRef(RefType(ref = "ExpressionFoo")))
+ }
+
+ @Test
+ fun `isLiteralUnion returns true for empty OrType`() {
+ assertTrue(isLiteralUnion(OrType(orTypes = emptyList())))
+ }
+
+ @Test
+ fun `getLiteralValues returns empty list for empty OrType`() {
+ assertEquals(emptyList(), getLiteralValues(OrType(orTypes = emptyList())))
+ }
+
+ @Test
+ fun `extractAssetTypeConstant returns null when genericArguments is null`() {
+ val ref = RefType(ref = "Asset<\"choice\">")
+ assertNull(extractAssetTypeConstant(ref))
+ }
+
+ @Test
+ fun `extractAssetTypeConstant returns null for StringType without const`() {
+ val ref =
+ RefType(
+ ref = "Asset<\"choice\">",
+ genericArguments = listOf(StringType()),
+ )
+ assertNull(extractAssetTypeConstant(ref))
+ }
+}
diff --git a/types/kotlin/src/test/kotlin/com/intuit/playerui/xlr/XlrSerializerTest.kt b/types/kotlin/src/test/kotlin/com/intuit/playerui/xlr/XlrSerializerTest.kt
new file mode 100644
index 0000000..97de135
--- /dev/null
+++ b/types/kotlin/src/test/kotlin/com/intuit/playerui/xlr/XlrSerializerTest.kt
@@ -0,0 +1,614 @@
+package com.intuit.playerui.xlr
+
+import kotlinx.serialization.json.Json
+import kotlinx.serialization.json.JsonObject
+import kotlinx.serialization.json.JsonPrimitive
+import kotlinx.serialization.json.jsonArray
+import kotlinx.serialization.json.jsonObject
+import kotlinx.serialization.json.jsonPrimitive
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertIs
+import kotlin.test.assertNotNull
+import kotlin.test.assertTrue
+
+class XlrSerializerTest {
+ private val fixtureJson: String get() = TestFixtures.choiceAssetJson
+ private val doc by lazy { XlrDeserializer.deserialize(fixtureJson) }
+
+ @Test
+ fun `round-trip deserialize-serialize-deserialize produces equal documents`() {
+ val serialized = XlrSerializer.serialize(doc)
+ val doc2 = XlrDeserializer.deserialize(serialized)
+ assertEquals(doc, doc2)
+ }
+
+ @Test
+ fun `serializes document name and source`() {
+ val jsonObj = XlrSerializer.serializeDocument(doc)
+ assertEquals("ChoiceAsset", jsonObj["name"]?.jsonPrimitive?.content)
+ assertTrue(
+ jsonObj["source"]
+ ?.jsonPrimitive
+ ?.content
+ ?.contains("choice/types.ts") == true,
+ )
+ }
+
+ @Test
+ fun `serializes document genericTokens`() {
+ val jsonObj = XlrSerializer.serializeDocument(doc)
+ val tokens = jsonObj["genericTokens"]
+ assertNotNull(tokens)
+ }
+
+ @Test
+ fun `serializes StringType`() {
+ val node = StringType(const = "hello", name = "MyStr")
+ val jsonObj = XlrSerializer.serializeNode(node)
+ assertEquals("string", jsonObj["type"]?.jsonPrimitive?.content)
+ assertEquals("hello", jsonObj["const"]?.jsonPrimitive?.content)
+ assertEquals("MyStr", jsonObj["name"]?.jsonPrimitive?.content)
+ }
+
+ @Test
+ fun `serializes StringType with enum`() {
+ val node = StringType(enum = listOf("a", "b"))
+ val jsonObj = XlrSerializer.serializeNode(node)
+ val arr = jsonObj["enum"]?.jsonArray
+ assertNotNull(arr)
+ assertEquals(2, arr.size)
+ assertEquals("a", arr[0].jsonPrimitive.content)
+ assertEquals("b", arr[1].jsonPrimitive.content)
+ }
+
+ @Test
+ fun `serializes NumberType with enum`() {
+ val node = NumberType(enum = listOf(1.0, 2.0))
+ val jsonObj = XlrSerializer.serializeNode(node)
+ assertEquals("number", jsonObj["type"]?.jsonPrimitive?.content)
+ assertNotNull(jsonObj["enum"])
+ }
+
+ @Test
+ fun `serializes NumberType integer const without decimal`() {
+ val node = NumberType(const = 42.0)
+ val jsonObj = XlrSerializer.serializeNode(node)
+ assertEquals("42", jsonObj["const"]?.jsonPrimitive?.content)
+ }
+
+ @Test
+ fun `serializes NumberType fractional const with decimal`() {
+ val node = NumberType(const = 3.14)
+ val jsonObj = XlrSerializer.serializeNode(node)
+ assertEquals("3.14", jsonObj["const"]?.jsonPrimitive?.content)
+ }
+
+ @Test
+ fun `serializes BooleanType with const`() {
+ val node = BooleanType(const = true)
+ val jsonObj = XlrSerializer.serializeNode(node)
+ assertEquals("boolean", jsonObj["type"]?.jsonPrimitive?.content)
+ assertEquals("true", jsonObj["const"]?.jsonPrimitive?.content)
+ }
+
+ @Test
+ fun `serializes simple types`() {
+ assertEquals(
+ "null",
+ XlrSerializer.serializeNode(NullType())["type"]?.jsonPrimitive?.content,
+ )
+ assertEquals(
+ "any",
+ XlrSerializer.serializeNode(AnyType())["type"]?.jsonPrimitive?.content,
+ )
+ assertEquals(
+ "unknown",
+ XlrSerializer.serializeNode(UnknownType())["type"]?.jsonPrimitive?.content,
+ )
+ assertEquals(
+ "undefined",
+ XlrSerializer.serializeNode(UndefinedType())["type"]?.jsonPrimitive?.content,
+ )
+ assertEquals(
+ "void",
+ XlrSerializer.serializeNode(VoidType())["type"]?.jsonPrimitive?.content,
+ )
+ assertEquals(
+ "never",
+ XlrSerializer.serializeNode(NeverType())["type"]?.jsonPrimitive?.content,
+ )
+ }
+
+ @Test
+ fun `serializes RefType with genericArguments`() {
+ val node = RefType(ref = "Foo", genericArguments = listOf(StringType()))
+ val jsonObj = XlrSerializer.serializeNode(node)
+ assertEquals("ref", jsonObj["type"]?.jsonPrimitive?.content)
+ assertEquals("Foo", jsonObj["ref"]?.jsonPrimitive?.content)
+ assertNotNull(jsonObj["genericArguments"])
+ }
+
+ @Test
+ fun `serializes RefType with property`() {
+ val node = RefType(ref = "Foo", property = "bar")
+ val jsonObj = XlrSerializer.serializeNode(node)
+ assertEquals("bar", jsonObj["property"]?.jsonPrimitive?.content)
+ }
+
+ @Test
+ fun `serializes ObjectType with properties and extends`() {
+ val node =
+ ObjectType(
+ properties = mapOf("x" to ObjectProperty(required = true, node = StringType())),
+ extends = RefType(ref = "Base"),
+ )
+ val jsonObj = XlrSerializer.serializeNode(node)
+ assertEquals("object", jsonObj["type"]?.jsonPrimitive?.content)
+ assertNotNull(jsonObj["properties"])
+ assertNotNull(jsonObj["extends"])
+ }
+
+ @Test
+ fun `serializes ObjectType with empty properties`() {
+ val node = ObjectType()
+ val jsonObj = XlrSerializer.serializeNode(node)
+ assertTrue(jsonObj.containsKey("properties"))
+ assertEquals(0, jsonObj["properties"]?.jsonObject?.size)
+ }
+
+ @Test
+ fun `serializes ObjectType with typed additionalProperties`() {
+ val node =
+ ObjectType(
+ additionalProperties = AdditionalItemsType.Typed(StringType()),
+ )
+ val jsonObj = XlrSerializer.serializeNode(node)
+ val ap = jsonObj["additionalProperties"]?.jsonObject
+ assertNotNull(ap)
+ assertEquals("string", ap["type"]?.jsonPrimitive?.content)
+ }
+
+ @Test
+ fun `serializes ArrayType`() {
+ val node = ArrayType(elementType = NumberType())
+ val jsonObj = XlrSerializer.serializeNode(node)
+ assertEquals("array", jsonObj["type"]?.jsonPrimitive?.content)
+ assertNotNull(jsonObj["elementType"])
+ }
+
+ @Test
+ fun `serializes TupleType`() {
+ val node =
+ TupleType(
+ elementTypes = listOf(TupleMember(name = "a", type = StringType())),
+ minItems = 1,
+ )
+ val jsonObj = XlrSerializer.serializeNode(node)
+ assertEquals("tuple", jsonObj["type"]?.jsonPrimitive?.content)
+ assertEquals("1", jsonObj["minItems"]?.jsonPrimitive?.content)
+ }
+
+ @Test
+ fun `serializes TupleType with typed additionalItems`() {
+ val node =
+ TupleType(
+ elementTypes = listOf(TupleMember(type = StringType())),
+ minItems = 1,
+ additionalItems = AdditionalItemsType.Typed(AnyType()),
+ )
+ val jsonObj = XlrSerializer.serializeNode(node)
+ val ai = jsonObj["additionalItems"]?.jsonObject
+ assertNotNull(ai)
+ assertEquals("any", ai["type"]?.jsonPrimitive?.content)
+ }
+
+ @Test
+ fun `serializes TupleMember without name`() {
+ val node =
+ TupleType(
+ elementTypes = listOf(TupleMember(type = StringType())),
+ minItems = 1,
+ )
+ val jsonObj = XlrSerializer.serializeNode(node)
+ val member = jsonObj["elementTypes"]?.jsonArray?.first()?.jsonObject
+ assertNotNull(member)
+ assertFalse(member.containsKey("name"))
+ }
+
+ @Test
+ fun `serializes RecordType`() {
+ val node = RecordType(keyType = StringType(), valueType = AnyType())
+ val jsonObj = XlrSerializer.serializeNode(node)
+ assertEquals("record", jsonObj["type"]?.jsonPrimitive?.content)
+ assertNotNull(jsonObj["keyType"])
+ assertNotNull(jsonObj["valueType"])
+ }
+
+ @Test
+ fun `serializes OrType`() {
+ val node = OrType(orTypes = listOf(StringType(), NumberType()))
+ val jsonObj = XlrSerializer.serializeNode(node)
+ assertEquals("or", jsonObj["type"]?.jsonPrimitive?.content)
+ assertNotNull(jsonObj["or"])
+ }
+
+ @Test
+ fun `serializes AndType`() {
+ val node = AndType(andTypes = listOf(StringType(), NumberType()))
+ val jsonObj = XlrSerializer.serializeNode(node)
+ assertEquals("and", jsonObj["type"]?.jsonPrimitive?.content)
+ assertNotNull(jsonObj["and"])
+ }
+
+ @Test
+ fun `serializes TemplateLiteralType`() {
+ val node = TemplateLiteralType(format = "hello_\\d+")
+ val jsonObj = XlrSerializer.serializeNode(node)
+ assertEquals("template", jsonObj["type"]?.jsonPrimitive?.content)
+ assertEquals("hello_\\d+", jsonObj["format"]?.jsonPrimitive?.content)
+ }
+
+ @Test
+ fun `serializes ConditionalType`() {
+ val node =
+ ConditionalType(
+ check = ConditionalCheck(StringType(), NumberType()),
+ value = ConditionalValue(BooleanType(), NullType()),
+ )
+ val jsonObj = XlrSerializer.serializeNode(node)
+ assertEquals("conditional", jsonObj["type"]?.jsonPrimitive?.content)
+ assertNotNull(jsonObj["check"])
+ assertNotNull(jsonObj["value"])
+ }
+
+ @Test
+ fun `serializes FunctionType`() {
+ val node =
+ FunctionType(
+ parameters = listOf(FunctionParameter(name = "x", type = StringType(), optional = true)),
+ returnType = BooleanType(),
+ )
+ val jsonObj = XlrSerializer.serializeNode(node)
+ assertEquals("function", jsonObj["type"]?.jsonPrimitive?.content)
+ assertNotNull(jsonObj["parameters"])
+ assertNotNull(jsonObj["returnType"])
+ }
+
+ @Test
+ fun `serializes FunctionType with parameter default`() {
+ val node =
+ FunctionType(
+ parameters =
+ listOf(
+ FunctionParameter(
+ name = "x",
+ type = StringType(),
+ default = StringType(const = "hi"),
+ ),
+ ),
+ )
+ val jsonObj = XlrSerializer.serializeNode(node)
+ val param = jsonObj["parameters"]?.jsonArray?.first()?.jsonObject
+ assertNotNull(param)
+ val defaultObj = param["default"]?.jsonObject
+ assertNotNull(defaultObj)
+ assertEquals("string", defaultObj["type"]?.jsonPrimitive?.content)
+ assertEquals("hi", defaultObj["const"]?.jsonPrimitive?.content)
+ }
+
+ @Test
+ fun `null optional fields are not emitted`() {
+ val node = StringType()
+ val jsonObj = XlrSerializer.serializeNode(node)
+ assertFalse(jsonObj.containsKey("const"))
+ assertFalse(jsonObj.containsKey("enum"))
+ assertFalse(jsonObj.containsKey("name"))
+ assertFalse(jsonObj.containsKey("title"))
+ assertFalse(jsonObj.containsKey("description"))
+ assertFalse(jsonObj.containsKey("source"))
+ assertFalse(jsonObj.containsKey("genericTokens"))
+ assertFalse(jsonObj.containsKey("examples"))
+ assertFalse(jsonObj.containsKey("comment"))
+ assertFalse(jsonObj.containsKey("meta"))
+ }
+
+ @Test
+ fun `serializes source and genericTokens on node`() {
+ val tokens = listOf(ParamTypeNode(symbol = "T", constraints = RefType(ref = "Base")))
+ val node = ObjectType(source = "foo.ts", genericTokens = tokens, name = "Foo")
+ val jsonObj = XlrSerializer.serializeNode(node)
+ assertEquals("foo.ts", jsonObj["source"]?.jsonPrimitive?.content)
+ assertNotNull(jsonObj["genericTokens"])
+ assertEquals("Foo", jsonObj["name"]?.jsonPrimitive?.content)
+ }
+
+ @Test
+ fun `serializes annotations on nodes`() {
+ val node =
+ StringType(
+ title = "MyTitle",
+ description = "A string",
+ comment = "Some comment",
+ meta = mapOf("key" to "value"),
+ )
+ val jsonObj = XlrSerializer.serializeNode(node)
+ assertEquals("MyTitle", jsonObj["title"]?.jsonPrimitive?.content)
+ assertEquals("A string", jsonObj["description"]?.jsonPrimitive?.content)
+ assertEquals("Some comment", jsonObj["comment"]?.jsonPrimitive?.content)
+ assertNotNull(jsonObj["meta"])
+ assertEquals(
+ "value",
+ jsonObj["meta"]
+ ?.jsonObject
+ ?.get("key")
+ ?.jsonPrimitive
+ ?.content,
+ )
+ }
+
+ @Test
+ fun `round-trip ObjectType with empty properties`() {
+ val original = ObjectType()
+ val json = XlrSerializer.serializeNode(original)
+ val deserialized = XlrDeserializer.parseNode(json)
+ assertEquals(original, deserialized)
+ }
+
+ @Test
+ fun `round-trip ObjectType with typed additionalProperties`() {
+ val original =
+ ObjectType(
+ additionalProperties = AdditionalItemsType.Typed(StringType()),
+ )
+ val json = XlrSerializer.serializeNode(original)
+ val deserialized = XlrDeserializer.parseNode(json)
+ assertEquals(original, deserialized)
+ }
+
+ @Test
+ fun `round-trip TupleType with typed additionalItems`() {
+ val original =
+ TupleType(
+ elementTypes =
+ listOf(
+ TupleMember(name = "a", type = StringType()),
+ TupleMember(type = NumberType(), optional = true),
+ ),
+ minItems = 1,
+ additionalItems = AdditionalItemsType.Typed(AnyType()),
+ )
+ val json = XlrSerializer.serializeNode(original)
+ val deserialized = XlrDeserializer.parseNode(json)
+ assertEquals(original, deserialized)
+ }
+
+ @Test
+ fun `round-trip nested ConditionalType`() {
+ val original =
+ ConditionalType(
+ check =
+ ConditionalCheck(
+ left = OrType(orTypes = listOf(StringType(), NumberType())),
+ right = RefType(ref = "Foo"),
+ ),
+ value =
+ ConditionalValue(
+ trueValue = ArrayType(elementType = BooleanType()),
+ falseValue = NullType(),
+ ),
+ )
+ val json = XlrSerializer.serializeNode(original)
+ val deserialized = XlrDeserializer.parseNode(json)
+ assertEquals(original, deserialized)
+ }
+
+ @Test
+ fun `round-trip FunctionType with parameter defaults`() {
+ val original =
+ FunctionType(
+ parameters =
+ listOf(
+ FunctionParameter(
+ name = "x",
+ type = StringType(),
+ default = StringType(const = "hi"),
+ ),
+ FunctionParameter(
+ name = "y",
+ type = NumberType(),
+ optional = true,
+ ),
+ ),
+ returnType = BooleanType(),
+ )
+ val json = XlrSerializer.serializeNode(original)
+ val deserialized = XlrDeserializer.parseNode(json)
+ assertEquals(original, deserialized)
+ }
+
+ @Test
+ fun `round-trip NumberType integer const preserves value`() {
+ val original = NumberType(const = 42.0)
+ val json = XlrSerializer.serializeNode(original)
+ val deserialized = assertIs(XlrDeserializer.parseNode(json))
+ assertEquals(42.0, deserialized.const)
+ }
+
+ @Test
+ fun `round-trip RefType with property`() {
+ val original = RefType(ref = "Foo", property = "bar")
+ val json = XlrSerializer.serializeNode(original)
+ val deserialized = XlrDeserializer.parseNode(json)
+ assertEquals(original, deserialized)
+ }
+
+ @Test
+ fun `round-trip StringType with enum`() {
+ val original = StringType(enum = listOf("a", "b", "c"))
+ val json = XlrSerializer.serializeNode(original)
+ val deserialized = XlrDeserializer.parseNode(json)
+ assertEquals(original, deserialized)
+ }
+
+ @Test
+ fun `AdditionalItemsType None serializes to false`() {
+ val obj = ObjectType(additionalProperties = AdditionalItemsType.None)
+ val json = XlrSerializer.serializeNode(obj)
+ assertEquals(JsonPrimitive(false), json["additionalProperties"])
+ }
+
+ @Test
+ fun `TupleType additionalItems None serializes to false`() {
+ val tuple =
+ TupleType(
+ elementTypes = listOf(TupleMember(type = StringType())),
+ minItems = 1,
+ additionalItems = AdditionalItemsType.None,
+ )
+ val json = XlrSerializer.serializeNode(tuple)
+ assertEquals(JsonPrimitive(false), json["additionalItems"])
+ }
+
+ @Test
+ fun `serialize returns parseable JSON string`() {
+ val doc = XlrDocument(name = "Test", source = "test.ts", objectType = ObjectType())
+ val jsonString = XlrSerializer.serialize(doc)
+ val parsed =
+ Json.parseToJsonElement(jsonString)
+ assertIs(parsed)
+ }
+
+ @Test
+ fun `serialize round-trip via string form`() {
+ val doc =
+ XlrDocument(
+ name = "Test",
+ source = "test.ts",
+ objectType =
+ ObjectType(
+ properties = mapOf("x" to ObjectProperty(required = true, node = StringType())),
+ ),
+ )
+ val jsonString = XlrSerializer.serialize(doc)
+ val doc2 = XlrDeserializer.deserialize(jsonString)
+ assertEquals(doc.name, doc2.name)
+ assertEquals(doc.source, doc2.source)
+ assertEquals(doc.objectType.properties, doc2.objectType.properties)
+ }
+
+ @Test
+ fun `serializeDocument omits genericTokens when null`() {
+ val doc = XlrDocument(name = "Test", source = "test.ts", objectType = ObjectType())
+ val jsonObj = XlrSerializer.serializeDocument(doc)
+ assertFalse(jsonObj.containsKey("genericTokens"))
+ }
+
+ @Test
+ fun `serializeDocument includes genericTokens when present`() {
+ val doc =
+ XlrDocument(
+ name = "Test",
+ source = "test.ts",
+ objectType = ObjectType(),
+ genericTokens = listOf(ParamTypeNode(symbol = "T")),
+ )
+ val jsonObj = XlrSerializer.serializeDocument(doc)
+ assertTrue(jsonObj.containsKey("genericTokens"))
+ }
+
+ @Test
+ fun `serializeNode preserves JsonElement annotation fields`() {
+ val examples =
+ Json.parseToJsonElement("""["a", "b"]""")
+ val defaultVal =
+ Json.parseToJsonElement(""""hello"""")
+ val see =
+ Json.parseToJsonElement("""{"ref": "OtherType"}""")
+ val node = StringType(examples = examples, default = defaultVal, see = see)
+ val jsonObj = XlrSerializer.serializeNode(node)
+ assertEquals(examples, jsonObj["examples"])
+ assertEquals(defaultVal, jsonObj["default"])
+ assertEquals(see, jsonObj["see"])
+ }
+
+ @Test
+ fun `round-trip all 19 node types`() {
+ for (original in TestFixtures.allNodeTypeInstances) {
+ val json = XlrSerializer.serializeNode(original)
+ val deserialized = XlrDeserializer.parseNode(json)
+ assertEquals(original, deserialized, "Round-trip failed for ${original::class.simpleName}")
+ }
+ }
+
+ @Test
+ fun `serializeDocument flattens ObjectType at root`() {
+ val doc =
+ XlrDocument(
+ name = "Test",
+ source = "test.ts",
+ objectType =
+ ObjectType(
+ properties = mapOf("x" to ObjectProperty(required = true, node = StringType())),
+ ),
+ )
+ val jsonObj = XlrSerializer.serializeDocument(doc)
+ assertEquals("object", jsonObj["type"]?.jsonPrimitive?.content)
+ assertFalse(jsonObj.containsKey("objectType"))
+ assertTrue(jsonObj.containsKey("properties"))
+ }
+
+ @Test
+ fun `serializeDocument name and source override ObjectType values`() {
+ val obj = ObjectType(name = "InnerName", source = "inner.ts")
+ val doc = XlrDocument(name = "OuterName", source = "outer.ts", objectType = obj)
+ val jsonObj = XlrSerializer.serializeDocument(doc)
+ assertEquals("OuterName", jsonObj["name"]?.jsonPrimitive?.content)
+ assertEquals("outer.ts", jsonObj["source"]?.jsonPrimitive?.content)
+ }
+
+ @Test
+ fun `serializeDocument preserves annotations on ObjectType root`() {
+ val doc =
+ XlrDocument(
+ name = "Test",
+ source = "test.ts",
+ objectType =
+ ObjectType(
+ title = "TestTitle",
+ description = "A test object",
+ comment = "Some comment",
+ ),
+ )
+ val jsonObj = XlrSerializer.serializeDocument(doc)
+ assertEquals("TestTitle", jsonObj["title"]?.jsonPrimitive?.content)
+ assertEquals("A test object", jsonObj["description"]?.jsonPrimitive?.content)
+ assertEquals("Some comment", jsonObj["comment"]?.jsonPrimitive?.content)
+ }
+
+ @Test
+ fun `serialize round-trip with genericTokens`() {
+ val doc =
+ XlrDocument(
+ name = "Test",
+ source = "test.ts",
+ objectType = ObjectType(),
+ genericTokens =
+ listOf(
+ ParamTypeNode(
+ symbol = "T",
+ constraints = RefType(ref = "Asset"),
+ default = RefType(ref = "Asset"),
+ ),
+ ),
+ )
+ val jsonString = XlrSerializer.serialize(doc)
+ val doc2 = XlrDeserializer.deserialize(jsonString)
+ assertEquals(doc.name, doc2.name)
+ assertEquals(doc.source, doc2.source)
+ assertNotNull(doc2.genericTokens)
+ assertEquals(1, doc2.genericTokens!!.size)
+ assertEquals("T", doc2.genericTokens!!.first().symbol)
+ }
+}
diff --git a/types/kotlin/src/test/kotlin/com/intuit/playerui/xlr/XlrTypesTest.kt b/types/kotlin/src/test/kotlin/com/intuit/playerui/xlr/XlrTypesTest.kt
new file mode 100644
index 0000000..3372bb5
--- /dev/null
+++ b/types/kotlin/src/test/kotlin/com/intuit/playerui/xlr/XlrTypesTest.kt
@@ -0,0 +1,446 @@
+package com.intuit.playerui.xlr
+
+import kotlinx.serialization.json.Json
+import kotlinx.serialization.json.JsonArray
+import kotlinx.serialization.json.JsonNull
+import kotlinx.serialization.json.JsonObject
+import kotlinx.serialization.json.JsonPrimitive
+import kotlinx.serialization.json.jsonObject
+import kotlinx.serialization.json.jsonPrimitive
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertIs
+import kotlin.test.assertNotEquals
+import kotlin.test.assertNotNull
+import kotlin.test.assertNull
+
+class XlrTypesTest {
+ private val json = Json { ignoreUnknownKeys = true }
+
+ @Test
+ fun `StringType defaults`() {
+ val s = StringType()
+ assertEquals("string", s.type)
+ assertNull(s.const)
+ assertNull(s.enum)
+ assertNull(s.title)
+ assertNull(s.description)
+ assertNull(s.name)
+ assertNull(s.examples)
+ assertNull(s.default)
+ assertNull(s.see)
+ assertNull(s.comment)
+ assertNull(s.meta)
+ assertNull(s.source)
+ assertNull(s.genericTokens)
+ }
+
+ @Test
+ fun `NumberType defaults`() {
+ val n = NumberType()
+ assertEquals("number", n.type)
+ assertNull(n.const)
+ assertNull(n.enum)
+ }
+
+ @Test
+ fun `BooleanType defaults`() {
+ val b = BooleanType()
+ assertEquals("boolean", b.type)
+ assertNull(b.const)
+ }
+
+ @Test
+ fun `all simple types have correct type field`() {
+ assertEquals("null", NullType().type)
+ assertEquals("any", AnyType().type)
+ assertEquals("unknown", UnknownType().type)
+ assertEquals("undefined", UndefinedType().type)
+ assertEquals("void", VoidType().type)
+ assertEquals("never", NeverType().type)
+ }
+
+ @Test
+ fun `RefType construction`() {
+ val ref = RefType(ref = "Foo", genericArguments = listOf(StringType()))
+ assertEquals("ref", ref.type)
+ assertEquals("Foo", ref.ref)
+ assertEquals(1, ref.genericArguments?.size)
+ }
+
+ @Test
+ fun `ObjectType defaults`() {
+ val obj = ObjectType()
+ assertEquals("object", obj.type)
+ assertEquals(emptyMap(), obj.properties)
+ assertNull(obj.extends)
+ assertEquals(AdditionalItemsType.None, obj.additionalProperties)
+ }
+
+ @Test
+ fun `ObjectType with source and genericTokens`() {
+ val tokens = listOf(ParamTypeNode(symbol = "T", constraints = RefType(ref = "Asset")))
+ val obj = ObjectType(source = "test.ts", genericTokens = tokens)
+ assertEquals("test.ts", obj.source)
+ assertNotNull(obj.genericTokens)
+ assertEquals(1, obj.genericTokens!!.size)
+ assertEquals("T", obj.genericTokens!!.first().symbol)
+ }
+
+ @Test
+ fun `AdditionalItemsType None and Typed`() {
+ val none = AdditionalItemsType.None
+ val typed = AdditionalItemsType.Typed(StringType())
+ assertIs(none)
+ assertIs(typed)
+ assertIs(typed.node)
+ }
+
+ @Test
+ fun `ConditionalCheck construction`() {
+ val check = ConditionalCheck(left = StringType(), right = NumberType())
+ assertIs(check.left)
+ assertIs(check.right)
+ }
+
+ @Test
+ fun `ConditionalValue construction`() {
+ val value = ConditionalValue(trueValue = BooleanType(), falseValue = NullType())
+ assertIs(value.trueValue)
+ assertIs(value.falseValue)
+ }
+
+ @Test
+ fun `ConditionalType construction`() {
+ val ct =
+ ConditionalType(
+ check = ConditionalCheck(StringType(), NumberType()),
+ value = ConditionalValue(BooleanType(), NullType()),
+ )
+ assertEquals("conditional", ct.type)
+ }
+
+ @Test
+ fun `NamedType construction`() {
+ val nt = NamedType(typeName = "Foo", source = "bar.ts", node = StringType(const = "x"))
+ assertEquals("Foo", nt.typeName)
+ assertEquals("bar.ts", nt.source)
+ assertNull(nt.genericTokens)
+ assertIs(nt.node)
+ assertEquals("x", (nt.node as StringType).const)
+ }
+
+ @Test
+ fun `NamedTypeWithGenerics construction`() {
+ val token = ParamTypeNode(symbol = "T", constraints = RefType(ref = "Asset"))
+ val ntg =
+ NamedTypeWithGenerics(
+ typeName = "Foo",
+ source = "bar.ts",
+ genericTokens = listOf(token),
+ node = ObjectType(),
+ )
+ assertEquals("Foo", ntg.typeName)
+ assertEquals(1, ntg.genericTokens.size)
+ assertEquals("T", ntg.genericTokens.first().symbol)
+ }
+
+ @Test
+ fun `NodeTypeWithGenerics construction`() {
+ val ntwg =
+ NodeTypeWithGenerics(
+ node = ArrayType(elementType = StringType()),
+ genericTokens = listOf(ParamTypeNode(symbol = "T")),
+ )
+ assertIs(ntwg.node)
+ assertEquals(1, ntwg.genericTokens.size)
+ }
+
+ @Test
+ fun `XlrDocument toObjectType delegates`() {
+ val objType =
+ ObjectType(
+ properties = mapOf("x" to ObjectProperty(required = true, node = StringType())),
+ title = "Test",
+ )
+ val doc = XlrDocument(name = "Test", source = "test.ts", objectType = objType)
+ assertEquals(objType, doc.toObjectType())
+ }
+
+ @Test
+ fun `TupleType construction`() {
+ val tt =
+ TupleType(
+ elementTypes =
+ listOf(
+ TupleMember(name = "a", type = StringType()),
+ TupleMember(type = NumberType(), optional = true),
+ ),
+ minItems = 1,
+ additionalItems = AdditionalItemsType.Typed(AnyType()),
+ )
+ assertEquals("tuple", tt.type)
+ assertEquals(2, tt.elementTypes.size)
+ assertEquals(1, tt.minItems)
+ assertIs(tt.additionalItems)
+ }
+
+ @Test
+ fun `FunctionType construction`() {
+ val ft =
+ FunctionType(
+ parameters =
+ listOf(
+ FunctionParameter(name = "x", type = StringType(), optional = true),
+ ),
+ returnType = BooleanType(),
+ )
+ assertEquals("function", ft.type)
+ assertEquals(1, ft.parameters.size)
+ }
+
+ @Test
+ fun `polymorphic serialization round-trip via NodeType serializer`() {
+ val original: NodeType = StringType(const = "test", title = "MyTitle")
+ val serialized = json.encodeToString(NodeType.serializer(), original)
+ val deserialized = json.decodeFromString(NodeType.serializer(), serialized)
+ assertEquals(original, deserialized)
+ }
+
+ @Test
+ fun `NodeType sealed hierarchy includes all 19 types`() {
+ val instances = TestFixtures.allNodeTypeInstances
+ assertEquals(19, instances.size)
+ for (instance in instances) {
+ assertNotNull(instance.type)
+ }
+ }
+
+ @Test
+ fun `Annotated interface is implemented by all NodeTypes`() {
+ val node: NodeType = StringType(title = "hello", comment = "world")
+ val annotated: Annotated = node
+ assertEquals("hello", annotated.title)
+ assertEquals("world", annotated.comment)
+ }
+
+ @Test
+ fun `ParamTypeNode construction`() {
+ val param = ParamTypeNode(symbol = "T", constraints = RefType(ref = "Foo"), default = StringType())
+ assertEquals("T", param.symbol)
+ assertIs(param.constraints)
+ assertIs(param.default)
+ }
+
+ @Test
+ fun `ObjectProperty construction`() {
+ val prop = ObjectProperty(required = true, node = NumberType(const = 42.0))
+ assertEquals(true, prop.required)
+ assertIs(prop.node)
+ }
+
+ @Test
+ fun `RecordType defaults`() {
+ val r = RecordType(keyType = StringType(), valueType = AnyType())
+ assertEquals("record", r.type)
+ assertNull(r.name)
+ assertNull(r.title)
+ assertNull(r.description)
+ assertNull(r.source)
+ assertNull(r.genericTokens)
+ }
+
+ @Test
+ fun `ArrayType defaults`() {
+ val a = ArrayType(elementType = StringType())
+ assertEquals("array", a.type)
+ assertNull(a.name)
+ assertNull(a.source)
+ assertNull(a.genericTokens)
+ }
+
+ @Test
+ fun `TemplateLiteralType defaults`() {
+ val t = TemplateLiteralType(format = ".*")
+ assertEquals("template", t.type)
+ assertNull(t.name)
+ assertNull(t.source)
+ assertNull(t.genericTokens)
+ }
+
+ @Test
+ fun `OrType defaults`() {
+ val o = OrType(orTypes = listOf(StringType()))
+ assertEquals("or", o.type)
+ assertNull(o.name)
+ assertNull(o.source)
+ assertNull(o.genericTokens)
+ }
+
+ @Test
+ fun `AndType defaults`() {
+ val a = AndType(andTypes = listOf(StringType()))
+ assertEquals("and", a.type)
+ assertNull(a.name)
+ assertNull(a.source)
+ assertNull(a.genericTokens)
+ }
+
+ @Test
+ fun `RefType defaults`() {
+ val r = RefType(ref = "Foo")
+ assertEquals("ref", r.type)
+ assertNull(r.genericArguments)
+ assertNull(r.property)
+ assertNull(r.name)
+ assertNull(r.source)
+ assertNull(r.genericTokens)
+ }
+
+ @Test
+ fun `FunctionType defaults`() {
+ val f = FunctionType(parameters = emptyList())
+ assertEquals("function", f.type)
+ assertNull(f.returnType)
+ assertNull(f.name)
+ assertNull(f.source)
+ assertNull(f.genericTokens)
+ }
+
+ @Test
+ fun `FunctionParameter with all fields`() {
+ val p =
+ FunctionParameter(
+ name = "x",
+ type = StringType(),
+ optional = true,
+ default = StringType(const = "hi"),
+ )
+ assertEquals("x", p.name)
+ assertIs(p.type)
+ assertEquals(true, p.optional)
+ val defaultNode = assertIs(p.default)
+ assertEquals("hi", defaultNode.const)
+ }
+
+ @Test
+ fun `TupleMember defaults`() {
+ val m = TupleMember(type = StringType())
+ assertNull(m.name)
+ assertNull(m.optional)
+ assertIs(m.type)
+ }
+
+ @Test
+ fun `XlrDocument defaults`() {
+ val doc = XlrDocument(name = "Test", source = "test.ts", objectType = ObjectType())
+ assertEquals("Test", doc.name)
+ assertEquals("test.ts", doc.source)
+ assertNull(doc.genericTokens)
+ }
+
+ @Test
+ fun `data class equality for identical instances`() {
+ val a = StringType(const = "hello", title = "Title")
+ val b = StringType(const = "hello", title = "Title")
+ assertEquals(a, b)
+ assertEquals(a.hashCode(), b.hashCode())
+ }
+
+ @Test
+ fun `data class inequality for different field values`() {
+ val a = StringType(const = "hello")
+ val b = StringType(const = "world")
+ assertNotEquals(a, b)
+ }
+
+ @Test
+ fun `data class copy with modified fields`() {
+ val original = StringType(const = "hello", title = "T")
+ val copied = original.copy(const = "world")
+ assertEquals("world", copied.const)
+ assertEquals("T", copied.title)
+ }
+
+ @Test
+ fun `StringType with JsonElement annotations`() {
+ val examples = Json.parseToJsonElement("""["a", "b"]""")
+ val defaultVal = Json.parseToJsonElement(""""hello"""")
+ val see = Json.parseToJsonElement("""{"ref": "OtherType"}""")
+ val s = StringType(examples = examples, default = defaultVal, see = see)
+ assertIs(s.examples)
+ assertIs(s.default)
+ assertIs(s.see)
+ }
+
+ @Test
+ fun `WholeNumberDoubleSerializer serializes negative whole number as integer`() {
+ val element = json.encodeToJsonElement(NodeType.serializer(), NumberType(const = -42.0))
+ assertEquals(JsonPrimitive(-42), element.jsonObject["const"])
+ }
+
+ @Test
+ fun `WholeNumberDoubleSerializer serializes zero as integer`() {
+ val element = json.encodeToJsonElement(NodeType.serializer(), NumberType(const = 0.0))
+ assertEquals(JsonPrimitive(0), element.jsonObject["const"])
+ }
+
+ @Test
+ fun `WholeNumberDoubleSerializer serializes large whole number as integer`() {
+ val element = json.encodeToJsonElement(NodeType.serializer(), NumberType(const = 1000000.0))
+ assertEquals(JsonPrimitive(1000000), element.jsonObject["const"])
+ }
+
+ @Test
+ fun `AdditionalItemsTypeSerializer encodes None to false`() {
+ val encoded = json.encodeToJsonElement(AdditionalItemsTypeSerializer, AdditionalItemsType.None)
+ assertEquals(JsonPrimitive(false), encoded)
+ }
+
+ @Test
+ fun `AdditionalItemsTypeSerializer encodes Typed to node object`() {
+ val encoded =
+ json.encodeToJsonElement(
+ AdditionalItemsTypeSerializer,
+ AdditionalItemsType.Typed(StringType()),
+ )
+ assertIs(encoded)
+ assertEquals("string", encoded.jsonObject["type"]?.jsonPrimitive?.content)
+ }
+
+ @Test
+ fun `AdditionalItemsTypeSerializer decodes false to None`() {
+ val decoded = json.decodeFromJsonElement(AdditionalItemsTypeSerializer, JsonPrimitive(false))
+ assertIs(decoded)
+ }
+
+ @Test
+ fun `AdditionalItemsTypeSerializer decodes JsonNull to None`() {
+ val decoded = json.decodeFromJsonElement(AdditionalItemsTypeSerializer, JsonNull)
+ assertIs(decoded)
+ }
+
+ @Test
+ fun `AdditionalItemsTypeSerializer decodes node object to Typed`() {
+ val nodeJson = json.parseToJsonElement("""{"type": "string"}""")
+ val decoded = json.decodeFromJsonElement(AdditionalItemsTypeSerializer, nodeJson)
+ val typed = assertIs(decoded)
+ assertIs(typed.node)
+ }
+
+ @Test
+ fun `AdditionalItemsTypeSerializer round-trips None`() {
+ val original = AdditionalItemsType.None
+ val encoded = json.encodeToJsonElement(AdditionalItemsTypeSerializer, original)
+ val decoded = json.decodeFromJsonElement(AdditionalItemsTypeSerializer, encoded)
+ assertEquals(original, decoded)
+ }
+
+ @Test
+ fun `AdditionalItemsTypeSerializer round-trips Typed`() {
+ val original = AdditionalItemsType.Typed(StringType(const = "test"))
+ val encoded = json.encodeToJsonElement(AdditionalItemsTypeSerializer, original)
+ val decoded = json.decodeFromJsonElement(AdditionalItemsTypeSerializer, encoded)
+ assertEquals(original, decoded)
+ }
+}
diff --git a/types/kotlin/src/test/kotlin/com/intuit/playerui/xlr/XlrUtilityTest.kt b/types/kotlin/src/test/kotlin/com/intuit/playerui/xlr/XlrUtilityTest.kt
new file mode 100644
index 0000000..c596e37
--- /dev/null
+++ b/types/kotlin/src/test/kotlin/com/intuit/playerui/xlr/XlrUtilityTest.kt
@@ -0,0 +1,224 @@
+package com.intuit.playerui.xlr
+
+import kotlinx.serialization.json.Json
+import kotlinx.serialization.json.jsonObject
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertIs
+import kotlin.test.assertNull
+
+class XlrUtilityTest {
+ // Intentionally omits isLenient=true (present in production xlrJson) to test with stricter parsing.
+ private val json =
+ Json {
+ encodeDefaults = false
+ ignoreUnknownKeys = true
+ }
+
+ @Test
+ fun `Capability construction`() {
+ val cap = Capability(name = "MyCapability", provides = listOf("TypeA", "TypeB"))
+ assertEquals("MyCapability", cap.name)
+ assertEquals(listOf("TypeA", "TypeB"), cap.provides)
+ }
+
+ @Test
+ fun `Capability serialization round-trip`() {
+ val original = Capability(name = "MyCap", provides = listOf("A", "B"))
+ val encoded = json.encodeToString(Capability.serializer(), original)
+ val decoded = json.decodeFromString(Capability.serializer(), encoded)
+ assertEquals(original, decoded)
+ }
+
+ @Test
+ fun `Capability with empty provides list`() {
+ val cap = Capability(name = "EmptyCap", provides = emptyList())
+ assertEquals(emptyList(), cap.provides)
+ }
+
+ @Test
+ fun `Manifest construction with all fields`() {
+ val manifest =
+ Manifest(
+ pluginName = "TestPlugin",
+ capabilities = mapOf("cap1" to listOf("TypeA")),
+ customPrimitives = listOf("CustomString"),
+ )
+ assertEquals("TestPlugin", manifest.pluginName)
+ assertEquals(mapOf("cap1" to listOf("TypeA")), manifest.capabilities)
+ assertEquals(listOf("CustomString"), manifest.customPrimitives)
+ }
+
+ @Test
+ fun `Manifest with defaults`() {
+ val manifest = Manifest(pluginName = "TestPlugin")
+ assertEquals("TestPlugin", manifest.pluginName)
+ assertNull(manifest.capabilities)
+ assertNull(manifest.customPrimitives)
+ }
+
+ @Test
+ fun `Manifest serialization round-trip with all fields`() {
+ val original =
+ Manifest(
+ pluginName = "TestPlugin",
+ capabilities = mapOf("cap1" to listOf("TypeA", "TypeB")),
+ customPrimitives = listOf("CustomString"),
+ )
+ val encoded = json.encodeToString(Manifest.serializer(), original)
+ val decoded = json.decodeFromString(Manifest.serializer(), encoded)
+ assertEquals(original, decoded)
+ }
+
+ @Test
+ fun `Manifest serialization omits null optionals`() {
+ val manifest = Manifest(pluginName = "TestPlugin")
+ val encoded = json.encodeToJsonElement(Manifest.serializer(), manifest)
+ val obj = encoded.jsonObject
+ assertEquals(setOf("pluginName"), obj.keys)
+ }
+
+ @Test
+ fun `TSManifest construction`() {
+ val namedType: NamedType =
+ NamedType(
+ typeName = "MyType",
+ source = "test.ts",
+ node = StringType(const = "value"),
+ )
+ val manifest =
+ TSManifest(
+ pluginName = "TestPlugin",
+ capabilities = mapOf("cap1" to listOf(namedType)),
+ )
+ assertEquals("TestPlugin", manifest.pluginName)
+ assertEquals(1, manifest.capabilities.size)
+ assertEquals(1, manifest.capabilities["cap1"]?.size)
+ }
+
+ @Test
+ fun `TSManifest serialization round-trip`() {
+ val namedType: NamedType =
+ NamedType(
+ typeName = "MyType",
+ source = "test.ts",
+ node = ObjectType(),
+ )
+ val original =
+ TSManifest(
+ pluginName = "TestPlugin",
+ capabilities = mapOf("cap1" to listOf(namedType)),
+ )
+ val encoded = json.encodeToString(TSManifest.serializer(), original)
+ val decoded = json.decodeFromString(TSManifest.serializer(), encoded)
+ assertEquals(original, decoded)
+ }
+
+ @Test
+ fun `TransformInput Named construction`() {
+ val namedType: NamedType = NamedType(typeName = "Foo", source = "foo.ts", node = StringType())
+ val input = TransformInput.Named(namedType)
+ assertEquals(namedType, input.namedType)
+ }
+
+ @Test
+ fun `TransformInput Anonymous construction`() {
+ val nodeType: NodeType = StringType(const = "hello")
+ val input = TransformInput.Anonymous(nodeType)
+ assertEquals(nodeType, input.nodeType)
+ }
+
+ @Test
+ fun `TransformInput sealed variants distinguished via is checks`() {
+ val named: TransformInput =
+ TransformInput.Named(
+ NamedType(typeName = "Foo", source = "foo.ts", node = StringType()),
+ )
+ val anonymous: TransformInput = TransformInput.Anonymous(StringType())
+ assertIs(named)
+ assertIs(anonymous)
+ }
+
+ @Test
+ fun `TransformInput Named data class equality`() {
+ val nt: NamedType = NamedType(typeName = "Foo", source = "foo.ts", node = StringType())
+ val a = TransformInput.Named(nt)
+ val b = TransformInput.Named(nt)
+ assertEquals(a, b)
+ assertEquals(a.hashCode(), b.hashCode())
+ }
+
+ @Test
+ fun `TransformInput Anonymous data class equality`() {
+ val a = TransformInput.Anonymous(StringType(const = "x"))
+ val b = TransformInput.Anonymous(StringType(const = "x"))
+ assertEquals(a, b)
+ assertEquals(a.hashCode(), b.hashCode())
+ }
+
+ @Test
+ fun `TransformOutput Named construction`() {
+ val namedType: NamedType = NamedType(typeName = "Bar", source = "bar.ts", node = NumberType())
+ val output = TransformOutput.Named(namedType)
+ assertEquals(namedType, output.namedType)
+ }
+
+ @Test
+ fun `TransformOutput Anonymous construction`() {
+ val nodeType: NodeType = NumberType(const = 42.0)
+ val output = TransformOutput.Anonymous(nodeType)
+ assertEquals(nodeType, output.nodeType)
+ }
+
+ @Test
+ fun `TransformOutput sealed variants distinguished via is checks`() {
+ val named: TransformOutput =
+ TransformOutput.Named(
+ NamedType(typeName = "Bar", source = "bar.ts", node = NumberType()),
+ )
+ val anonymous: TransformOutput = TransformOutput.Anonymous(NumberType())
+ assertIs(named)
+ assertIs(anonymous)
+ }
+
+ @Test
+ fun `TransformOutput data class equality`() {
+ val nt: NamedType = NamedType(typeName = "Bar", source = "bar.ts", node = NumberType())
+ val a = TransformOutput.Named(nt)
+ val b = TransformOutput.Named(nt)
+ assertEquals(a, b)
+ assertEquals(a.hashCode(), b.hashCode())
+ }
+
+ @Test
+ fun `TransformFunction invocation with Named input`() {
+ val transform =
+ TransformFunction { input, capabilityType ->
+ val named = input as TransformInput.Named
+ TransformOutput.Anonymous(
+ StringType(const = "${named.namedType.typeName}_$capabilityType"),
+ )
+ }
+ val input =
+ TransformInput.Named(
+ NamedType(typeName = "Foo", source = "foo.ts", node = ObjectType()),
+ )
+ val output = transform.transform(input, "asset")
+ val anonymous = assertIs(output)
+ val strType = assertIs(anonymous.nodeType)
+ assertEquals("Foo_asset", strType.const)
+ }
+
+ @Test
+ fun `TransformFunction invocation with Anonymous input`() {
+ val transform =
+ TransformFunction { input, _ ->
+ val anon = input as TransformInput.Anonymous
+ TransformOutput.Anonymous(anon.nodeType)
+ }
+ val input = TransformInput.Anonymous(NumberType(const = 42.0))
+ val output = transform.transform(input, "binding")
+ val anonymous = assertIs(output)
+ assertIs(anonymous.nodeType)
+ }
+}
diff --git a/types/kotlin/src/test/resources/test.json b/types/kotlin/src/test/resources/test.json
new file mode 100644
index 0000000..ff8e2bc
--- /dev/null
+++ b/types/kotlin/src/test/resources/test.json
@@ -0,0 +1,191 @@
+{
+ "source": "/home/circleci/.cache/bazel/_bazel_circleci/e8362d362e14c7d23506d1dfa3aea8b8/sandbox/processwrapper-sandbox/3907/execroot/_main/bazel-out/k8-fastbuild/bin/plugins/reference-assets/core/src/assets/choice/types.ts",
+ "name": "ChoiceAsset",
+ "type": "object",
+ "properties": {
+ "title": {
+ "required": false,
+ "node": {
+ "type": "ref",
+ "ref": "AssetWrapper",
+ "genericArguments": [
+ {
+ "type": "ref",
+ "ref": "AnyTextAsset"
+ }
+ ],
+ "title": "ChoiceAsset.title",
+ "description": "A text-like asset for the choice's label"
+ }
+ },
+ "note": {
+ "required": false,
+ "node": {
+ "type": "ref",
+ "ref": "AssetWrapper",
+ "genericArguments": [
+ {
+ "type": "ref",
+ "ref": "AnyTextAsset"
+ }
+ ],
+ "title": "ChoiceAsset.note",
+ "description": "Asset container for a note."
+ }
+ },
+ "binding": {
+ "required": false,
+ "node": {
+ "type": "ref",
+ "ref": "Binding",
+ "title": "ChoiceAsset.binding",
+ "description": "The location in the data-model to store the data"
+ }
+ },
+ "items": {
+ "required": false,
+ "node": {
+ "type": "array",
+ "elementType": {
+ "source": "/home/circleci/.cache/bazel/_bazel_circleci/e8362d362e14c7d23506d1dfa3aea8b8/sandbox/processwrapper-sandbox/3907/execroot/_main/bazel-out/k8-fastbuild/bin/plugins/reference-assets/core/src/assets/choice/types.ts",
+ "name": "ChoiceItem",
+ "type": "object",
+ "properties": {
+ "id": {
+ "required": true,
+ "node": {
+ "type": "string",
+ "title": "ChoiceItem.id",
+ "description": "The id associated with the choice item"
+ }
+ },
+ "label": {
+ "required": false,
+ "node": {
+ "type": "ref",
+ "ref": "AssetWrapper",
+ "genericArguments": [
+ {
+ "type": "ref",
+ "ref": "AnyTextAsset"
+ }
+ ],
+ "title": "ChoiceItem.label",
+ "description": "A text-like asset for the choice's label"
+ }
+ },
+ "value": {
+ "required": false,
+ "node": {
+ "source": "/home/circleci/.cache/bazel/_bazel_circleci/e8362d362e14c7d23506d1dfa3aea8b8/sandbox/processwrapper-sandbox/3907/execroot/_main/bazel-out/k8-fastbuild/bin/plugins/reference-assets/core/src/assets/choice/types.ts",
+ "name": "ValueType",
+ "type": "or",
+ "or": [
+ {
+ "type": "string",
+ "title": "ValueType"
+ },
+ {
+ "type": "number",
+ "title": "ValueType"
+ },
+ {
+ "type": "boolean",
+ "title": "ValueType"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "ChoiceItem.value",
+ "description": "The value of the input from the data-model"
+ }
+ }
+ },
+ "additionalProperties": false,
+ "title": "ChoiceItem",
+ "genericTokens": [
+ {
+ "symbol": "AnyTextAsset",
+ "constraints": {
+ "type": "ref",
+ "ref": "Asset"
+ },
+ "default": {
+ "type": "ref",
+ "ref": "Asset"
+ }
+ }
+ ]
+ },
+ "title": "ChoiceAsset.items",
+ "description": "The options to select from"
+ }
+ },
+ "metaData": {
+ "required": false,
+ "node": {
+ "source": "/home/circleci/.cache/bazel/_bazel_circleci/e8362d362e14c7d23506d1dfa3aea8b8/sandbox/processwrapper-sandbox/3907/execroot/_main/bazel-out/k8-fastbuild/bin/node_modules/.aspect_rules_js/@player-ui+beacon-plugin@0.0.0/node_modules/@player-ui/beacon-plugin/types/beacon.d.ts",
+ "name": "BeaconMetaData",
+ "type": "object",
+ "properties": {
+ "beacon": {
+ "required": false,
+ "node": {
+ "source": "/home/circleci/.cache/bazel/_bazel_circleci/e8362d362e14c7d23506d1dfa3aea8b8/sandbox/processwrapper-sandbox/3907/execroot/_main/bazel-out/k8-fastbuild/bin/node_modules/.aspect_rules_js/@player-ui+beacon-plugin@0.0.0/node_modules/@player-ui/beacon-plugin/types/beacon.d.ts",
+ "name": "BeaconDataType",
+ "type": "or",
+ "or": [
+ {
+ "type": "string",
+ "title": "BeaconDataType"
+ },
+ {
+ "type": "record",
+ "keyType": {
+ "type": "string"
+ },
+ "valueType": {
+ "type": "any"
+ },
+ "title": "BeaconDataType"
+ }
+ ],
+ "title": "BeaconMetaData.beacon",
+ "description": "Additional data to send along with beacons"
+ }
+ }
+ },
+ "additionalProperties": false,
+ "title": "ChoiceAsset.metaData",
+ "description": "Optional additional data"
+ }
+ }
+ },
+ "additionalProperties": false,
+ "title": "ChoiceAsset",
+ "description": "A choice asset represents a single selection choice, often displayed as radio buttons in a web context.\nThis will allow users to test out more complex flows than just inputs + buttons.",
+ "genericTokens": [
+ {
+ "symbol": "AnyTextAsset",
+ "constraints": {
+ "type": "ref",
+ "ref": "Asset"
+ },
+ "default": {
+ "type": "ref",
+ "ref": "Asset"
+ }
+ }
+ ],
+ "extends": {
+ "type": "ref",
+ "ref": "Asset<\"choice\">",
+ "genericArguments": [
+ {
+ "type": "string",
+ "const": "choice"
+ }
+ ]
+ }
+}
\ No newline at end of file