diff --git a/.editorconfig b/.editorconfig index 0f178672..bcb8a3c2 100644 --- a/.editorconfig +++ b/.editorconfig @@ -7,3 +7,4 @@ indent_size = 2 indent_style = space insert_final_newline = true trim_trailing_whitespace = true +max_line_length = 100 diff --git a/.gitattributes b/.gitattributes index 77711366..c0b49b27 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,8 +1,60 @@ -* text eol=lf +# Common settings that generally should always be used with your language specific settings -*.java text diff=java -*.kt text diff=java -*.kts text diff=java +# Auto detect text files and perform LF normalization +* text eol=lf +# The above will handle all files NOT found below + +*.md text +*.tex text diff=tex +*.adoc text +*.textile text +*.mustache text +*.csv text +*.tab text +*.tsv text +*.txt text +*.sql text + +# Graphics +*.png binary +*.jpg binary +*.jpeg binary +*.gif binary +*.tif binary +*.tiff binary +*.ico binary +*.eps binary +# SVG treated as an asset (binary) by default. +*.svg text + +# Properties +*.json text +*.toml text +*.xml text +*.yaml text +*.yml text + +# Scripts +*.bash text eol=lf +*.fish text eol=lf +*.sh text eol=lf # These are explicitly windows files and should use crlf -*.bat text eol=crlf +*.bat text eol=crlf +*.cmd text eol=crlf +*.ps1 text eol=crlf + + +# JVM sources +*.java text diff=java +*.gradle text diff=java +*.kt text diff=java +*.kts text diff=java + +# Text files where line endings should be preserved +*.patch -text + +# +# Exclude files from exporting +.gitattributes export-ignore +.gitignore export-ignore diff --git a/README.md b/README.md index 8f94b241..ff5d09a6 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ +[![](https://jitpack.io/v/adamko-dev/kotlinx-serialization-typescript-generator.svg)](https://jitpack.io/#adamko-dev/kotlinx-serialization-typescript-generator) + # Kotlinx Serialization TypeScript Generator -Create TypeScript interfaces from Kotlin classes +Create TypeScript interfaces from Kotlinx Serialization classes. ```kotlin @Serializable @@ -10,7 +12,7 @@ data class PlayerDetails( ) println( - KxsTsGenerator().generate(Color.serializer().descriptor) + KxsTsGenerator().generate(Color.serializer()) ) ``` @@ -21,4 +23,30 @@ interface PlayerDetails { } ``` +The aim is to create TypeScript interfaces that can accurately produce Kotlinx Serialization +compatible JSON. + +The Kotlinx Serialization API should be used to generate TypeScript. The +[`SerialDescriptor`s](https://kotlin.github.io/kotlinx.serialization/kotlinx-serialization-core/kotlinx.serialization.descriptors/-serial-descriptor/index.html) +are flexible and comprehensive enough to allow for accurate TypeScript code, without any deviation. + See [the docs](./docs) for working examples. + +## Status + +This is a proof-of-concept. + +| | Status | Notes | +|---------------------------------------|----------------------------------------------------------|:-------------------------------------------------------------------------------------------------| +| Kotlin multiplatform | ❓ | The codebase is multiplatform, but only JVM has been tested | +| `@SerialName` | ✅/⚠ | The serial name is directly converted and might produce invalid TypeScript | +| Basic classes | ✅ [example](./docs/basic-classes.md) | | +| Nullable and default-value properties | ✅ [example](./docs/default-values.md) | | +| Value classes | ✅ [example](./docs/value-classes.md) | | +| Enums | ✅ [example](./docs/enums.md) | | +| Lists | ✅ [example](./docs/lists.md) | | +| Maps | ✅/⚠ [example](./docs/maps.md) | Maps with complex keys are converted to an ES6 Map, [see](./docs/maps.md#maps-with-complex-keys) | +| Polymorphism - Sealed classes | ✅/⚠ [example](./docs/polymorphism.md#sealed-classes) | Nested sealed classes are ignored, [see](./docs/polymorphism.md#nested-sealed-classes) | +| Polymorphism - Open classes | ❌ [example](./docs/abstract-classes.md) | Not implemented. Converted to `type MyClass = any` | +| `@JsonClassDiscriminator` | ❌ | Not implemented | +| Edge cases - circular dependencies | ✅ [example](./docs/edgecases.md) | | diff --git a/build.gradle.kts b/build.gradle.kts index e5fa5cc9..8f3ce8ab 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -2,15 +2,13 @@ import buildsrc.config.excludeGeneratedGradleDsl plugins { base - id("me.qoomon.git-versioning") version "5.1.2" idea - buildsrc.convention.`kotlin-jvm` - kotlin("plugin.serialization") - id("org.jetbrains.kotlinx.knit") + id("me.qoomon.git-versioning") version "5.1.5" + id("org.jetbrains.kotlinx.kover") } -project.group = "dev.adamko" +project.group = "dev.adamko.kxtsgen" project.version = "0.0.0-SNAPSHOT" gitVersioning.apply { refs { @@ -24,7 +22,7 @@ gitVersioning.apply { tasks.wrapper { - gradleVersion = "7.4" + gradleVersion = "7.4.2" distributionType = Wrapper.DistributionType.ALL } @@ -40,39 +38,3 @@ idea { ) } } - - -val kotlinxSerializationVersion = "1.3.2" - -dependencies { - implementation(projects.modules.kxsTsGenCore) - - implementation(platform("org.jetbrains.kotlinx:kotlinx-serialization-bom:${kotlinxSerializationVersion}")) - implementation("org.jetbrains.kotlinx:kotlinx-serialization-core") - implementation("org.jetbrains.kotlinx:kotlinx-serialization-json") - - testImplementation(kotlin("test")) - - testImplementation("org.jetbrains.kotlinx:kotlinx-knit-test:0.3.0") -} - -tasks.withType { - kotlinOptions.freeCompilerArgs += listOf( - "-opt-in=kotlinx.serialization.ExperimentalSerializationApi", - ) -} - -sourceSets.test { - java.srcDirs("docs/example", "docs/test") -} - -//knit { -// rootDir = layout.projectDirectory.asFile -// files = rootProject.fileTree("docs") -//} - -tasks.test { - dependsOn(tasks.knit) -} - -tasks.compileKotlin { mustRunAfter(tasks.knit) } diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index ccde2aac..0dab274f 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -10,11 +10,12 @@ plugins { object Versions { const val jvmTarget = "11" - const val kotlinTarget = "1.6" const val kotlin = "1.6.10" - const val ksp = "1.6.10-1.0.2" - const val kotlinxSerializationVersion = "1.3.2" + const val kotlinTarget = "1.6" const val kotlinxKnit = "0.3.0" + const val kotlinxKover = "0.5.0" + const val kotlinxSerialization = "1.3.2" + const val ksp = "1.6.10-1.0.4" const val kotest = "5.1.0" } @@ -28,11 +29,16 @@ dependencies { implementation("com.google.devtools.ksp:com.google.devtools.ksp.gradle.plugin:${Versions.ksp}") - implementation(platform("org.jetbrains.kotlinx:kotlinx-serialization-bom:${Versions.kotlinxSerializationVersion}")) + implementation(platform("org.jetbrains.kotlinx:kotlinx-serialization-bom:${Versions.kotlinxSerialization}")) implementation("io.kotest:kotest-framework-multiplatform-plugin-gradle:${Versions.kotest}") implementation("org.jetbrains.kotlinx:kotlinx-knit:${Versions.kotlinxKnit}") + implementation("org.jetbrains.kotlinx:kover:${Versions.kotlinxKover}") + +// implementation("org.jetbrains.reflekt:gradle-plugin:1.6.10-1-SNAPSHOT") { +// isChanging = true +// } } diff --git a/buildSrc/repositories.settings.gradle.kts b/buildSrc/repositories.settings.gradle.kts index bca6f2f5..060eeff6 100644 --- a/buildSrc/repositories.settings.gradle.kts +++ b/buildSrc/repositories.settings.gradle.kts @@ -2,6 +2,7 @@ dependencyResolutionManagement { repositories { + myMavenLocal() mavenCentral() jitpack() gradlePluginPortal() @@ -9,6 +10,7 @@ dependencyResolutionManagement { pluginManagement { repositories { + myMavenLocal() jitpack() gradlePluginPortal() mavenCentral() @@ -16,6 +18,19 @@ dependencyResolutionManagement { } } + fun RepositoryHandler.jitpack() { maven("https://jitpack.io") } + + +fun RepositoryHandler.myMavenLocal(enabled: Boolean = false) { + if (enabled) { + logger.lifecycle("Maven local is enabled") + mavenLocal { + content { +// includeGroup("org.jetbrains.reflekt") + } + } + } +} diff --git a/buildSrc/src/main/kotlin/buildsrc/config/gradle.kt b/buildSrc/src/main/kotlin/buildsrc/config/gradle.kt index ff6aaefe..d897a509 100644 --- a/buildSrc/src/main/kotlin/buildsrc/config/gradle.kt +++ b/buildSrc/src/main/kotlin/buildsrc/config/gradle.kt @@ -1,7 +1,14 @@ package buildsrc.config +import org.gradle.api.GradleException +import org.gradle.api.Project import org.gradle.api.file.ProjectLayout +import org.gradle.kotlin.dsl.findByType import org.gradle.plugins.ide.idea.model.IdeaModule +import org.jetbrains.kotlin.gradle.dsl.KotlinTargetContainerWithPresetFunctions +import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTargetWithHostTests +import org.jetbrains.kotlin.gradle.targets.js.yarn.YarnRootExtension + /** exclude generated Gradle code, so it doesn't clog up search results */ fun IdeaModule.excludeGeneratedGradleDsl(layout: ProjectLayout) { diff --git a/buildSrc/src/main/kotlin/buildsrc/config/kmm.kt b/buildSrc/src/main/kotlin/buildsrc/config/kmm.kt new file mode 100644 index 00000000..1ff3d9ef --- /dev/null +++ b/buildSrc/src/main/kotlin/buildsrc/config/kmm.kt @@ -0,0 +1,47 @@ +package buildsrc.config + +import org.gradle.api.GradleException +import org.gradle.api.Project +import org.gradle.kotlin.dsl.findByType +import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension +import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTargetWithHostTests +import org.jetbrains.kotlin.gradle.targets.js.yarn.YarnRootExtension + + +/** + * `kotlin-js` adds a directory in the root-dir for the Yarn lock. + * That's a bit annoying. It's a little neater if it's in the + * gradle dir, next to the version-catalog. + */ +fun Project.relocateKotlinJsStore() { + + afterEvaluate { + rootProject.extensions.findByType()?.apply { + lockFileDirectory = project.rootDir.resolve("gradle/kotlin-js-store") + } + } + +} + + +fun KotlinMultiplatformExtension.currentHostTarget( + targetName: String = "native", + configure: KotlinNativeTargetWithHostTests.() -> Unit, +): KotlinNativeTargetWithHostTests { + val hostOs = System.getProperty("os.name") + val hostTarget = when { + hostOs == "Mac OS X" -> macosX64(targetName) + hostOs == "Linux" -> linuxX64(targetName) + hostOs.startsWith("Windows") -> mingwX64(targetName) + else -> throw GradleException("Preset for host OS '$hostOs' is undefined") + } + + println("Current host target ${hostTarget.targetName}/${hostTarget.preset?.name}") + hostTarget.configure() + return hostTarget +} + + +fun KotlinMultiplatformExtension.publicationsFromMainHost(): List { + return listOf(jvm(), js()).map { it.name } + "kotlinMultiplatform" +} diff --git a/buildSrc/src/main/kotlin/buildsrc/convention/jacoco-aggregation.gradle.kts b/buildSrc/src/main/kotlin/buildsrc/convention/jacoco-aggregation.gradle.kts deleted file mode 100644 index 0ee26fa6..00000000 --- a/buildSrc/src/main/kotlin/buildsrc/convention/jacoco-aggregation.gradle.kts +++ /dev/null @@ -1,28 +0,0 @@ -package buildsrc.convention - -import org.gradle.kotlin.dsl.`jacoco-report-aggregation` -import org.gradle.kotlin.dsl.base -import org.gradle.kotlin.dsl.dependencies - -plugins { - base - `jacoco-report-aggregation` -} - - -dependencies { - subprojects - .filter { it.buildFile.exists() } - .forEach { jacocoAggregation(it) } -} - - -@Suppress("UnstableApiUsage") // jacoco-report-aggregation is incubating -val testCodeCoverageReport by reporting.reports.creating(JacocoCoverageReport::class) { - testType.set(TestSuiteType.UNIT_TEST) -} - - -tasks.check { - dependsOn(testCodeCoverageReport.reportTask) -} diff --git a/buildSrc/src/main/kotlin/buildsrc/convention/kotlin-jvm.gradle.kts b/buildSrc/src/main/kotlin/buildsrc/convention/kotlin-jvm.gradle.kts index 48a547c9..b3580edd 100644 --- a/buildSrc/src/main/kotlin/buildsrc/convention/kotlin-jvm.gradle.kts +++ b/buildSrc/src/main/kotlin/buildsrc/convention/kotlin-jvm.gradle.kts @@ -2,7 +2,6 @@ package buildsrc.convention import org.gradle.kotlin.dsl.`java-library` import org.gradle.kotlin.dsl.dependencies -import org.gradle.kotlin.dsl.jacoco import org.gradle.kotlin.dsl.kotlin import org.gradle.kotlin.dsl.withType import org.jetbrains.kotlin.gradle.tasks.KotlinCompile @@ -11,11 +10,10 @@ plugins { id("buildsrc.convention.subproject") kotlin("jvm") `java-library` - jacoco } dependencies { - testImplementation(platform("io.kotest:kotest-bom:5.1.0")) + testImplementation(platform("io.kotest:kotest-bom:5.2.1")) testImplementation("io.kotest:kotest-runner-junit5") testImplementation("io.kotest:kotest-assertions-core") testImplementation("io.kotest:kotest-property") @@ -33,7 +31,7 @@ java { withSourcesJar() } -tasks.withType().configureEach { +tasks.withType { kotlinOptions { jvmTarget = "11" apiVersion = "1.6" @@ -53,5 +51,4 @@ tasks.compileTestKotlin { tasks.test { useJUnitPlatform() - finalizedBy(tasks.jacocoTestReport) } diff --git a/buildSrc/src/main/kotlin/buildsrc/convention/kotlin-multiplatform.gradle.kts b/buildSrc/src/main/kotlin/buildsrc/convention/kotlin-multiplatform.gradle.kts new file mode 100644 index 00000000..aa900d74 --- /dev/null +++ b/buildSrc/src/main/kotlin/buildsrc/convention/kotlin-multiplatform.gradle.kts @@ -0,0 +1,25 @@ +package buildsrc.convention + +import buildsrc.config.relocateKotlinJsStore + + +plugins { + id("buildsrc.convention.subproject") + kotlin("multiplatform") + `java-library` +} + + +relocateKotlinJsStore() + + +kotlin { + targets.all { + compilations.all { + kotlinOptions { + languageVersion = "1.6" + apiVersion = "1.6" + } + } + } +} diff --git a/buildSrc/src/main/kotlin/buildsrc/convention/maven-publish.gradle.kts b/buildSrc/src/main/kotlin/buildsrc/convention/maven-publish.gradle.kts new file mode 100644 index 00000000..678212bc --- /dev/null +++ b/buildSrc/src/main/kotlin/buildsrc/convention/maven-publish.gradle.kts @@ -0,0 +1,26 @@ +package buildsrc.convention + +plugins { + `maven-publish` +} + +//plugins.withType(JavaPlugin::class.java) { +// publishing { +// publications { +// create("mavenJava") { +// from(components["java"]) +// } +// } +// } +//} + +tasks + .matching { + it.name.startsWith(PublishingPlugin.PUBLISH_LIFECYCLE_TASK_NAME) + && it.group == PublishingPlugin.PUBLISH_TASK_GROUP + } + .configureEach { + doLast { + logger.lifecycle("[${this.name}] ${project.group}:${project.name}:${project.version}") + } + } diff --git a/docs/abstract-classes.md b/docs/abstract-classes.md index 18f8ba50..fa41dc49 100644 --- a/docs/abstract-classes.md +++ b/docs/abstract-classes.md @@ -1,28 +1,43 @@ -### Abstract class with a single field +**Table of contents** + + + +* [Introduction](#introduction) + * [Abstract class with a single field](#abstract-class-with-a-single-field) + * [Abstract class with primitive fields](#abstract-class-with-primitive-fields) + * [Abstract class, abstract value](#abstract-class-abstract-value) + + + +## Introduction + +### Abstract class with a single field + ```kotlin @Serializable abstract class Color(val rgb: Int) fun main() { val tsGenerator = KxsTsGenerator() - println(tsGenerator.generate(Color.serializer().descriptor)) + println(tsGenerator.generate(Color.serializer())) } ``` -> You can get the full code [here](./knit/example/example-abstract-class-single-field-01.kt). +> You can get the full code [here](./code/example/example-abstract-class-single-field-01.kt). ```typescript -interface Color { - rgb: number; -} +export type Color = any; +// interface Color { +// rgb: number; +// } ``` @@ -41,20 +56,49 @@ abstract class SimpleTypes( fun main() { val tsGenerator = KxsTsGenerator() - println(tsGenerator.generate(SimpleTypes.serializer().descriptor)) + println(tsGenerator.generate(SimpleTypes.serializer())) } ``` -> You can get the full code [here](./knit/example/example-abstract-class-primitive-fields-01.kt). +> You can get the full code [here](./code/example/example-abstract-class-primitive-fields-01.kt). ```typescript -interface SimpleTypes { - aString: string; - anInt: number; - aDouble: number; - bool: boolean; - privateMember: string; +export type SimpleTypes = any; +// export interface SimpleTypes { +// aString: string; +// anInt: number; +// aDouble: number; +// bool: boolean; +// privateMember: string; +// } +``` + + + +### Abstract class, abstract value + +```kotlin +@Serializable +abstract class AbstractSimpleTypes { + abstract val aString: String + abstract var anInt: Int + abstract val aDouble: Double + abstract val bool: Boolean +} + +fun main() { + val tsGenerator = KxsTsGenerator() + println(tsGenerator.generate(AbstractSimpleTypes.serializer())) } ``` +> You can get the full code [here](./code/example/example-abstract-class-abstract-field-01.kt). + +```typescript +export type AbstractSimpleTypes = any; +// export interface AbstractSimpleTypes { +// rgb: number; +// } +``` + diff --git a/docs/basic-classes.md b/docs/basic-classes.md index d0e0425b..4153ba42 100644 --- a/docs/basic-classes.md +++ b/docs/basic-classes.md @@ -8,6 +8,7 @@ * [Plain class with a single field](#plain-class-with-a-single-field) * [Plain class with primitive fields](#plain-class-with-primitive-fields) * [Data class with primitive fields](#data-class-with-primitive-fields) + * [Ignoring fields with `@Transitive`](#ignoring-fields-with-@transitive) @@ -28,14 +29,14 @@ class Color(val rgb: Int) fun main() { val tsGenerator = KxsTsGenerator() - println(tsGenerator.generate(Color.serializer().descriptor)) + println(tsGenerator.generate(Color.serializer())) } ``` -> You can get the full code [here](./knit/example/example-plain-class-single-field-01.kt). +> You can get the full code [here](./code/example/example-plain-class-single-field-01.kt). ```typescript -interface Color { +export interface Color { rgb: number; } ``` @@ -56,14 +57,14 @@ class SimpleTypes( fun main() { val tsGenerator = KxsTsGenerator() - println(tsGenerator.generate(SimpleTypes.serializer().descriptor)) + println(tsGenerator.generate(SimpleTypes.serializer())) } ``` -> You can get the full code [here](./knit/example/example-plain-class-primitive-fields-01.kt). +> You can get the full code [here](./code/example/example-plain-class-primitive-fields-01.kt). ```typescript -interface SimpleTypes { +export interface SimpleTypes { aString: string; anInt: number; aDouble: number; @@ -88,14 +89,14 @@ data class SomeDataClass( fun main() { val tsGenerator = KxsTsGenerator() - println(tsGenerator.generate(SomeDataClass.serializer().descriptor)) + println(tsGenerator.generate(SomeDataClass.serializer())) } ``` -> You can get the full code [here](./knit/example/example-plain-data-class-01.kt). +> You can get the full code [here](./code/example/example-plain-data-class-01.kt). ```typescript -interface SomeDataClass { +export interface SomeDataClass { aString: string; anInt: number; aDouble: number; @@ -105,3 +106,39 @@ interface SomeDataClass { ``` + +### Ignoring fields with `@Transitive` + +Just like in Kotlinx.Serialization, +[fields can be ignored](https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/basic-serialization.md#transient-properties) + +> A property can be excluded from serialization by marking it with the +> [`@Transient`](https://kotlin.github.io/kotlinx.serialization/kotlinx-serialization-core/kotlinx.serialization/-transient/index.html) +> annotation +> (don't confuse it with +> [`kotlin.jvm.Transient`](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.jvm/-transient/)). +> Transient properties must have a default value. + +```kotlin +import kotlinx.serialization.Transient + +@Serializable +class SimpleTypes( + @Transient + val aString: String = "default-value" +) + +fun main() { + val tsGenerator = KxsTsGenerator() + println(tsGenerator.generate(SimpleTypes.serializer())) +} +``` + +> You can get the full code [here](./code/example/example-plain-class-primitive-fields-02.kt). + +```typescript +export interface SimpleTypes { +} +``` + + diff --git a/docs/knit/build.gradle.kts b/docs/code/build.gradle.kts similarity index 68% rename from docs/knit/build.gradle.kts rename to docs/code/build.gradle.kts index 7fe8e7ba..751b5b6a 100644 --- a/docs/knit/build.gradle.kts +++ b/docs/code/build.gradle.kts @@ -1,5 +1,3 @@ -import buildsrc.config.excludeGeneratedGradleDsl - plugins { buildsrc.convention.`kotlin-jvm` kotlin("plugin.serialization") @@ -7,7 +5,6 @@ plugins { } - val kotlinxSerializationVersion = "1.3.2" dependencies { @@ -17,6 +14,11 @@ dependencies { implementation("org.jetbrains.kotlinx:kotlinx-serialization-core") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json") + implementation("org.jetbrains.kotlinx:kotlinx-knit:0.3.0") + + implementation(kotlin("reflect")) + + testImplementation(kotlin("test")) testImplementation("org.jetbrains.kotlinx:kotlinx-knit-test:0.3.0") @@ -29,16 +31,28 @@ tasks.withType { } sourceSets.test { - java.srcDirs("example", "test") + java.srcDirs( + "example", + "test", + "util", + ) } -//knit { -// rootDir = layout.projectDirectory.asFile -// files = rootProject.fileTree("docs") -//} +knit { + val docsDir = rootProject.layout.projectDirectory.dir("docs") + rootDir = docsDir.asFile + files = project.fileTree(docsDir) { + include("*.md") + } +} tasks.test { dependsOn(tasks.knit) +// finalizedBy(tasks.knitCheck) } tasks.compileKotlin { mustRunAfter(tasks.knit) } + +//tasks.knitCheck { +// dependsOn(tasks.test) +//} diff --git a/docs/code/example/example-abstract-class-abstract-field-01.kt b/docs/code/example/example-abstract-class-abstract-field-01.kt new file mode 100644 index 00000000..7f734fa3 --- /dev/null +++ b/docs/code/example/example-abstract-class-abstract-field-01.kt @@ -0,0 +1,19 @@ +// This file was automatically generated from abstract-classes.md by Knit tool. Do not edit. +@file:Suppress("PackageDirectoryMismatch", "unused") +package dev.adamko.kxstsgen.example.exampleAbstractClassAbstractField01 + +import kotlinx.serialization.* +import dev.adamko.kxstsgen.* + +@Serializable +abstract class AbstractSimpleTypes { + abstract val aString: String + abstract var anInt: Int + abstract val aDouble: Double + abstract val bool: Boolean +} + +fun main() { + val tsGenerator = KxsTsGenerator() + println(tsGenerator.generate(AbstractSimpleTypes.serializer())) +} diff --git a/docs/code/example/example-abstract-class-primitive-fields-01.kt b/docs/code/example/example-abstract-class-primitive-fields-01.kt new file mode 100644 index 00000000..f9f812a6 --- /dev/null +++ b/docs/code/example/example-abstract-class-primitive-fields-01.kt @@ -0,0 +1,20 @@ +// This file was automatically generated from abstract-classes.md by Knit tool. Do not edit. +@file:Suppress("PackageDirectoryMismatch", "unused") +package dev.adamko.kxstsgen.example.exampleAbstractClassPrimitiveFields01 + +import kotlinx.serialization.* +import dev.adamko.kxstsgen.* + +@Serializable +abstract class SimpleTypes( + val aString: String, + var anInt: Int, + val aDouble: Double, + val bool: Boolean, + private val privateMember: String, +) + +fun main() { + val tsGenerator = KxsTsGenerator() + println(tsGenerator.generate(SimpleTypes.serializer())) +} diff --git a/docs/knit/example/example-abstract-class-single-field-01.kt b/docs/code/example/example-abstract-class-single-field-01.kt similarity index 71% rename from docs/knit/example/example-abstract-class-single-field-01.kt rename to docs/code/example/example-abstract-class-single-field-01.kt index 98155d80..1322e995 100644 --- a/docs/knit/example/example-abstract-class-single-field-01.kt +++ b/docs/code/example/example-abstract-class-single-field-01.kt @@ -1,6 +1,6 @@ // This file was automatically generated from abstract-classes.md by Knit tool. Do not edit. @file:Suppress("PackageDirectoryMismatch", "unused") -package example.exampleAbstractClassSingleField01 +package dev.adamko.kxstsgen.example.exampleAbstractClassSingleField01 import kotlinx.serialization.* import dev.adamko.kxstsgen.* @@ -10,5 +10,5 @@ abstract class Color(val rgb: Int) fun main() { val tsGenerator = KxsTsGenerator() - println(tsGenerator.generate(Color.serializer().descriptor)) + println(tsGenerator.generate(Color.serializer())) } diff --git a/docs/code/example/example-default-values-primitive-fields-01.kt b/docs/code/example/example-default-values-primitive-fields-01.kt new file mode 100644 index 00000000..420b877c --- /dev/null +++ b/docs/code/example/example-default-values-primitive-fields-01.kt @@ -0,0 +1,23 @@ +// This file was automatically generated from default-values.md by Knit tool. Do not edit. +@file:Suppress("PackageDirectoryMismatch", "unused") +package dev.adamko.kxstsgen.example.exampleDefaultValuesPrimitiveFields01 + +import kotlinx.serialization.* +import dev.adamko.kxstsgen.* + +@Serializable +data class ContactDetails( + // nullable: ❌, optional: ❌ + val name: String, + // nullable: ✅, optional: ❌ + val email: String?, + // nullable: ❌, optional: ✅ + val active: Boolean = true, + // nullable: ✅, optional: ✅ + val phoneNumber: String? = null, +) + +fun main() { + val tsGenerator = KxsTsGenerator() + println(tsGenerator.generate(ContactDetails.serializer())) +} diff --git a/docs/knit/example/example-default-values-single-field-01.kt b/docs/code/example/example-default-values-single-field-01.kt similarity index 63% rename from docs/knit/example/example-default-values-single-field-01.kt rename to docs/code/example/example-default-values-single-field-01.kt index 02a3897b..a269e75f 100644 --- a/docs/knit/example/example-default-values-single-field-01.kt +++ b/docs/code/example/example-default-values-single-field-01.kt @@ -1,14 +1,14 @@ // This file was automatically generated from default-values.md by Knit tool. Do not edit. @file:Suppress("PackageDirectoryMismatch", "unused") -package example.exampleDefaultValuesSingleField01 +package dev.adamko.kxstsgen.example.exampleDefaultValuesSingleField01 import kotlinx.serialization.* import dev.adamko.kxstsgen.* @Serializable -class Color(val rgb: Int = 12345) +class Colour(val rgb: Int = 12345) fun main() { val tsGenerator = KxsTsGenerator() - println(tsGenerator.generate(Color.serializer().descriptor)) + println(tsGenerator.generate(Colour.serializer())) } diff --git a/docs/knit/example/example-default-values-primitive-fields-01.kt b/docs/code/example/example-default-values-single-field-02.kt similarity index 52% rename from docs/knit/example/example-default-values-primitive-fields-01.kt rename to docs/code/example/example-default-values-single-field-02.kt index 9c1f8300..e530f3cd 100644 --- a/docs/knit/example/example-default-values-primitive-fields-01.kt +++ b/docs/code/example/example-default-values-single-field-02.kt @@ -1,18 +1,14 @@ // This file was automatically generated from default-values.md by Knit tool. Do not edit. @file:Suppress("PackageDirectoryMismatch", "unused") -package example.exampleDefaultValuesPrimitiveFields01 +package dev.adamko.kxstsgen.example.exampleDefaultValuesSingleField02 import kotlinx.serialization.* import dev.adamko.kxstsgen.* @Serializable -data class ContactDetails( - val email: String?, - val phoneNumber: String? = null, - val active: Boolean? = true, -) +class Colour(val rgb: Int?) // 'rgb' is required, but the value can be null fun main() { val tsGenerator = KxsTsGenerator() - println(tsGenerator.generate(ContactDetails.serializer().descriptor)) + println(tsGenerator.generate(Colour.serializer())) } diff --git a/docs/code/example/example-edgecase-recursive-references-01.kt b/docs/code/example/example-edgecase-recursive-references-01.kt new file mode 100644 index 00000000..b6b814ab --- /dev/null +++ b/docs/code/example/example-edgecase-recursive-references-01.kt @@ -0,0 +1,17 @@ +// This file was automatically generated from edgecases.md by Knit tool. Do not edit. +@file:Suppress("PackageDirectoryMismatch", "unused") +package dev.adamko.kxstsgen.example.exampleEdgecaseRecursiveReferences01 + +import kotlinx.serialization.* +import dev.adamko.kxstsgen.* + +@Serializable +class A(val b: B) + +@Serializable +class B(val a: A) + +fun main() { + val tsGenerator = KxsTsGenerator() + println(tsGenerator.generate(A.serializer(), B.serializer())) +} diff --git a/docs/code/example/example-edgecase-recursive-references-02.kt b/docs/code/example/example-edgecase-recursive-references-02.kt new file mode 100644 index 00000000..1d3a6e49 --- /dev/null +++ b/docs/code/example/example-edgecase-recursive-references-02.kt @@ -0,0 +1,21 @@ +// This file was automatically generated from edgecases.md by Knit tool. Do not edit. +@file:Suppress("PackageDirectoryMismatch", "unused") +package dev.adamko.kxstsgen.example.exampleEdgecaseRecursiveReferences02 + +import kotlinx.serialization.* +import dev.adamko.kxstsgen.* + +@Serializable +class A( + val list: List +) + +@Serializable +class B( + val list: List +) + +fun main() { + val tsGenerator = KxsTsGenerator() + println(tsGenerator.generate(A.serializer(), B.serializer())) +} diff --git a/docs/code/example/example-edgecase-recursive-references-03.kt b/docs/code/example/example-edgecase-recursive-references-03.kt new file mode 100644 index 00000000..38774986 --- /dev/null +++ b/docs/code/example/example-edgecase-recursive-references-03.kt @@ -0,0 +1,21 @@ +// This file was automatically generated from edgecases.md by Knit tool. Do not edit. +@file:Suppress("PackageDirectoryMismatch", "unused") +package dev.adamko.kxstsgen.example.exampleEdgecaseRecursiveReferences03 + +import kotlinx.serialization.* +import dev.adamko.kxstsgen.* + +@Serializable +class A( + val map: Map +) + +@Serializable +class B( + val map: Map +) + +fun main() { + val tsGenerator = KxsTsGenerator() + println(tsGenerator.generate(A.serializer(), B.serializer())) +} diff --git a/docs/knit/example/example-enum-class-01.kt b/docs/code/example/example-enum-class-01.kt similarity index 64% rename from docs/knit/example/example-enum-class-01.kt rename to docs/code/example/example-enum-class-01.kt index d6e96ed9..f21a7eab 100644 --- a/docs/knit/example/example-enum-class-01.kt +++ b/docs/code/example/example-enum-class-01.kt @@ -1,7 +1,9 @@ // This file was automatically generated from enums.md by Knit tool. Do not edit. @file:Suppress("PackageDirectoryMismatch", "unused") -package example.exampleEnumClass01 +package dev.adamko.kxstsgen.example.exampleEnumClass01 +import kotlinx.serialization.* +import dev.adamko.kxstsgen.* import kotlinx.serialization.* import dev.adamko.kxstsgen.* @@ -14,5 +16,5 @@ enum class SomeType { fun main() { val tsGenerator = KxsTsGenerator() - println(tsGenerator.generate(SomeType.serializer().descriptor)) + println(tsGenerator.generate(SomeType.serializer())) } diff --git a/docs/knit/example/example-enum-class-02.kt b/docs/code/example/example-enum-class-02.kt similarity index 70% rename from docs/knit/example/example-enum-class-02.kt rename to docs/code/example/example-enum-class-02.kt index fdb9ed8f..26b662a6 100644 --- a/docs/knit/example/example-enum-class-02.kt +++ b/docs/code/example/example-enum-class-02.kt @@ -1,7 +1,9 @@ // This file was automatically generated from enums.md by Knit tool. Do not edit. @file:Suppress("PackageDirectoryMismatch", "unused") -package example.exampleEnumClass02 +package dev.adamko.kxstsgen.example.exampleEnumClass02 +import kotlinx.serialization.* +import dev.adamko.kxstsgen.* import kotlinx.serialization.* import dev.adamko.kxstsgen.* @@ -16,5 +18,5 @@ enum class SomeType2(val coolName: String) { fun main() { val tsGenerator = KxsTsGenerator() - println(tsGenerator.generate(SomeType2.serializer().descriptor)) + println(tsGenerator.generate(SomeType2.serializer())) } diff --git a/docs/knit/example/example-polymorphism-sealed-01.kt b/docs/code/example/example-generics-01.kt similarity index 52% rename from docs/knit/example/example-polymorphism-sealed-01.kt rename to docs/code/example/example-generics-01.kt index 0179a781..648137fa 100644 --- a/docs/knit/example/example-polymorphism-sealed-01.kt +++ b/docs/code/example/example-generics-01.kt @@ -1,27 +1,23 @@ // This file was automatically generated from polymorphism.md by Knit tool. Do not edit. @file:Suppress("PackageDirectoryMismatch", "unused") -package example.examplePolymorphismSealed01 +package dev.adamko.kxstsgen.example.exampleGenerics01 import kotlinx.serialization.* import dev.adamko.kxstsgen.* -@Serializable -sealed class Project { - abstract val name: String -} +import kotlinx.serialization.builtins.serializer @Serializable -data class OwnedProject( - override val name: String, - val owner: String, -) : Project() +class Box( + val value: T, +) fun main() { val tsGenerator = KxsTsGenerator() + println( tsGenerator.generate( - Project.serializer().descriptor, - OwnedProject.serializer().descriptor, + Box.serializer(Double.serializer()), ) ) } diff --git a/docs/knit/example/example-list-primitive-01.kt b/docs/code/example/example-list-primitive-01.kt similarity index 55% rename from docs/knit/example/example-list-primitive-01.kt rename to docs/code/example/example-list-primitive-01.kt index fa8c6e24..2ec7c29d 100644 --- a/docs/knit/example/example-list-primitive-01.kt +++ b/docs/code/example/example-list-primitive-01.kt @@ -1,16 +1,18 @@ // This file was automatically generated from lists.md by Knit tool. Do not edit. @file:Suppress("PackageDirectoryMismatch", "unused") -package example.exampleListPrimitive01 +package dev.adamko.kxstsgen.example.exampleListPrimitive01 import kotlinx.serialization.* import dev.adamko.kxstsgen.* @Serializable -data class CalendarEvent( - val attendeeNames: List +data class MyLists( + val strings: List, + val ints: List, + val longs: List, ) fun main() { val tsGenerator = KxsTsGenerator() - println(tsGenerator.generate(CalendarEvent.serializer().descriptor)) + println(tsGenerator.generate(MyLists.serializer())) } diff --git a/docs/code/example/example-map-complex-01.kt b/docs/code/example/example-map-complex-01.kt new file mode 100644 index 00000000..a0752498 --- /dev/null +++ b/docs/code/example/example-map-complex-01.kt @@ -0,0 +1,24 @@ +// This file was automatically generated from maps.md by Knit tool. Do not edit. +@file:Suppress("PackageDirectoryMismatch", "unused") +package dev.adamko.kxstsgen.example.exampleMapComplex01 + +import kotlinx.serialization.* +import dev.adamko.kxstsgen.* + +@Serializable +data class Colour( + val r: UByte, + val g: UByte, + val b: UByte, + val a: UByte, +) + +@Serializable +data class CanvasProperties( + val colourNames: Map +) + +fun main() { + val tsGenerator = KxsTsGenerator() + println(tsGenerator.generate(CanvasProperties.serializer())) +} diff --git a/docs/code/example/example-map-complex-02.kt b/docs/code/example/example-map-complex-02.kt new file mode 100644 index 00000000..089257a6 --- /dev/null +++ b/docs/code/example/example-map-complex-02.kt @@ -0,0 +1,49 @@ +// This file was automatically generated from maps.md by Knit tool. Do not edit. +@file:Suppress("PackageDirectoryMismatch", "unused") +package dev.adamko.kxstsgen.example.exampleMapComplex02 + +import kotlinx.serialization.* +import dev.adamko.kxstsgen.* + +@Serializable +data class Colour( + val r: UByte, + val g: UByte, + val b: UByte, + val a: UByte, +) + +/** + * Encode a [Colour] as an 8-character string + * + * Red, green, blue, and alpha are encoded as base-16 strings. + */ +@Serializable +@JvmInline +value class ColourMapKey(private val rgba: String) { + constructor(colour: Colour) : this( + listOf( + colour.r, + colour.g, + colour.b, + colour.a, + ).joinToString("") { + it.toString(16).padStart(2, '0') + } + ) + + fun toColour(): Colour { + val (r, g, b, a) = rgba.chunked(2).map { it.toUByte(16) } + return Colour(r, g, b, a) + } +} + +@Serializable +data class CanvasProperties( + val colourNames: Map +) + +fun main() { + val tsGenerator = KxsTsGenerator() + println(tsGenerator.generate(CanvasProperties.serializer())) +} diff --git a/docs/code/example/example-map-complex-03.kt b/docs/code/example/example-map-complex-03.kt new file mode 100644 index 00000000..bbbe26ae --- /dev/null +++ b/docs/code/example/example-map-complex-03.kt @@ -0,0 +1,56 @@ +// This file was automatically generated from maps.md by Knit tool. Do not edit. +@file:Suppress("PackageDirectoryMismatch", "unused") +package dev.adamko.kxstsgen.example.exampleMapComplex03 + +import kotlinx.serialization.* +import dev.adamko.kxstsgen.* + +import kotlinx.serialization.descriptors.* +import kotlinx.serialization.encoding.* + +@Serializable(with = ColourAsStringSerializer::class) +data class Colour( + val r: UByte, + val g: UByte, + val b: UByte, + val a: UByte, +) + +/** + * Encode a [Colour] as an 8-character string + * + * Red, green, blue, and alpha are encoded as base-16 strings. + */ +object ColourAsStringSerializer : KSerializer { + override val descriptor: SerialDescriptor = + PrimitiveSerialDescriptor("Colour", PrimitiveKind.STRING) + + override fun serialize(encoder: Encoder, value: Colour) { + encoder.encodeString( + listOf( + value.r, + value.g, + value.b, + value.a, + ).joinToString("") { + it.toString(16).padStart(2, '0') + } + ) + } + + override fun deserialize(decoder: Decoder): Colour { + val string = decoder.decodeString() + val (r, g, b, a) = string.chunked(2).map { it.toUByte(16) } + return Colour(r, g, b, a) + } +} + +@Serializable +data class CanvasProperties( + val colourNames: Map +) + +fun main() { + val tsGenerator = KxsTsGenerator() + println(tsGenerator.generate(CanvasProperties.serializer())) +} diff --git a/docs/knit/example/example-map-primitive-01.kt b/docs/code/example/example-map-primitive-01.kt similarity index 60% rename from docs/knit/example/example-map-primitive-01.kt rename to docs/code/example/example-map-primitive-01.kt index a588f0ad..94b78a35 100644 --- a/docs/knit/example/example-map-primitive-01.kt +++ b/docs/code/example/example-map-primitive-01.kt @@ -1,16 +1,16 @@ // This file was automatically generated from maps.md by Knit tool. Do not edit. @file:Suppress("PackageDirectoryMismatch", "unused") -package example.exampleMapPrimitive01 +package dev.adamko.kxstsgen.example.exampleMapPrimitive01 import kotlinx.serialization.* import dev.adamko.kxstsgen.* @Serializable -class Config { - val properties: Map = mapOf() -} +data class Config( + val properties: Map +) fun main() { val tsGenerator = KxsTsGenerator() - println(tsGenerator.generate(Config.serializer().descriptor)) + println(tsGenerator.generate(Config.serializer())) } diff --git a/docs/code/example/example-map-primitive-02.kt b/docs/code/example/example-map-primitive-02.kt new file mode 100644 index 00000000..e5179b31 --- /dev/null +++ b/docs/code/example/example-map-primitive-02.kt @@ -0,0 +1,22 @@ +// This file was automatically generated from maps.md by Knit tool. Do not edit. +@file:Suppress("PackageDirectoryMismatch", "unused") +package dev.adamko.kxstsgen.example.exampleMapPrimitive02 + +import kotlinx.serialization.* +import dev.adamko.kxstsgen.* + +@Serializable +class Application( + val settings: Map +) + +@Serializable +enum class SettingKeys { + SCREEN_SIZE, + MAX_MEMORY, +} + +fun main() { + val tsGenerator = KxsTsGenerator() + println(tsGenerator.generate(Application.serializer())) +} diff --git a/docs/code/example/example-map-primitive-03.kt b/docs/code/example/example-map-primitive-03.kt new file mode 100644 index 00000000..1fff4023 --- /dev/null +++ b/docs/code/example/example-map-primitive-03.kt @@ -0,0 +1,16 @@ +// This file was automatically generated from maps.md by Knit tool. Do not edit. +@file:Suppress("PackageDirectoryMismatch", "unused") +package dev.adamko.kxstsgen.example.exampleMapPrimitive03 + +import kotlinx.serialization.* +import dev.adamko.kxstsgen.* + +@Serializable +data class Config( + val properties: Map +) + +fun main() { + val tsGenerator = KxsTsGenerator() + println(tsGenerator.generate(Config.serializer())) +} diff --git a/docs/knit/example/example-plain-class-primitive-fields-01.kt b/docs/code/example/example-plain-class-primitive-fields-01.kt similarity index 76% rename from docs/knit/example/example-plain-class-primitive-fields-01.kt rename to docs/code/example/example-plain-class-primitive-fields-01.kt index 21f562a5..1531eb16 100644 --- a/docs/knit/example/example-plain-class-primitive-fields-01.kt +++ b/docs/code/example/example-plain-class-primitive-fields-01.kt @@ -1,6 +1,6 @@ // This file was automatically generated from basic-classes.md by Knit tool. Do not edit. @file:Suppress("PackageDirectoryMismatch", "unused") -package example.examplePlainClassPrimitiveFields01 +package dev.adamko.kxstsgen.example.examplePlainClassPrimitiveFields01 import kotlinx.serialization.* import dev.adamko.kxstsgen.* @@ -16,5 +16,5 @@ class SimpleTypes( fun main() { val tsGenerator = KxsTsGenerator() - println(tsGenerator.generate(SimpleTypes.serializer().descriptor)) + println(tsGenerator.generate(SimpleTypes.serializer())) } diff --git a/docs/code/example/example-plain-class-primitive-fields-02.kt b/docs/code/example/example-plain-class-primitive-fields-02.kt new file mode 100644 index 00000000..950a573e --- /dev/null +++ b/docs/code/example/example-plain-class-primitive-fields-02.kt @@ -0,0 +1,19 @@ +// This file was automatically generated from basic-classes.md by Knit tool. Do not edit. +@file:Suppress("PackageDirectoryMismatch", "unused") +package dev.adamko.kxstsgen.example.examplePlainClassPrimitiveFields02 + +import kotlinx.serialization.* +import dev.adamko.kxstsgen.* + +import kotlinx.serialization.Transient + +@Serializable +class SimpleTypes( + @Transient + val aString: String = "default-value" +) + +fun main() { + val tsGenerator = KxsTsGenerator() + println(tsGenerator.generate(SimpleTypes.serializer())) +} diff --git a/docs/knit/example/example-plain-class-single-field-01.kt b/docs/code/example/example-plain-class-single-field-01.kt similarity index 71% rename from docs/knit/example/example-plain-class-single-field-01.kt rename to docs/code/example/example-plain-class-single-field-01.kt index 1fd466a2..18fbb258 100644 --- a/docs/knit/example/example-plain-class-single-field-01.kt +++ b/docs/code/example/example-plain-class-single-field-01.kt @@ -1,6 +1,6 @@ // This file was automatically generated from basic-classes.md by Knit tool. Do not edit. @file:Suppress("PackageDirectoryMismatch", "unused") -package example.examplePlainClassSingleField01 +package dev.adamko.kxstsgen.example.examplePlainClassSingleField01 import kotlinx.serialization.* import dev.adamko.kxstsgen.* @@ -10,5 +10,5 @@ class Color(val rgb: Int) fun main() { val tsGenerator = KxsTsGenerator() - println(tsGenerator.generate(Color.serializer().descriptor)) + println(tsGenerator.generate(Color.serializer())) } diff --git a/docs/knit/example/example-plain-data-class-01.kt b/docs/code/example/example-plain-data-class-01.kt similarity index 77% rename from docs/knit/example/example-plain-data-class-01.kt rename to docs/code/example/example-plain-data-class-01.kt index 6130a228..ab235280 100644 --- a/docs/knit/example/example-plain-data-class-01.kt +++ b/docs/code/example/example-plain-data-class-01.kt @@ -1,6 +1,6 @@ // This file was automatically generated from basic-classes.md by Knit tool. Do not edit. @file:Suppress("PackageDirectoryMismatch", "unused") -package example.examplePlainDataClass01 +package dev.adamko.kxstsgen.example.examplePlainDataClass01 import kotlinx.serialization.* import dev.adamko.kxstsgen.* @@ -16,5 +16,5 @@ data class SomeDataClass( fun main() { val tsGenerator = KxsTsGenerator() - println(tsGenerator.generate(SomeDataClass.serializer().descriptor)) + println(tsGenerator.generate(SomeDataClass.serializer())) } diff --git a/docs/knit/example/example-abstract-class-primitive-fields-01.kt b/docs/code/example/example-polymorphic-abstract-class-primitive-fields-01.kt similarity index 74% rename from docs/knit/example/example-abstract-class-primitive-fields-01.kt rename to docs/code/example/example-polymorphic-abstract-class-primitive-fields-01.kt index 2e481d93..056ee370 100644 --- a/docs/knit/example/example-abstract-class-primitive-fields-01.kt +++ b/docs/code/example/example-polymorphic-abstract-class-primitive-fields-01.kt @@ -1,6 +1,6 @@ // This file was automatically generated from polymorphism.md by Knit tool. Do not edit. @file:Suppress("PackageDirectoryMismatch", "unused") -package example.exampleAbstractClassPrimitiveFields01 +package dev.adamko.kxstsgen.example.examplePolymorphicAbstractClassPrimitiveFields01 import kotlinx.serialization.* import dev.adamko.kxstsgen.* @@ -16,5 +16,5 @@ abstract class SimpleTypes( fun main() { val tsGenerator = KxsTsGenerator() - println(tsGenerator.generate(SimpleTypes.serializer().descriptor)) + println(tsGenerator.generate(SimpleTypes.serializer())) } diff --git a/docs/code/example/example-polymorphic-objects-01.kt b/docs/code/example/example-polymorphic-objects-01.kt new file mode 100644 index 00000000..30fce999 --- /dev/null +++ b/docs/code/example/example-polymorphic-objects-01.kt @@ -0,0 +1,22 @@ +// This file was automatically generated from polymorphism.md by Knit tool. Do not edit. +@file:Suppress("PackageDirectoryMismatch", "unused") +package dev.adamko.kxstsgen.example.examplePolymorphicObjects01 + +import kotlinx.serialization.* +import dev.adamko.kxstsgen.* + +@Serializable +sealed class Response + +@Serializable +object EmptyResponse : Response() + +@Serializable +class TextResponse(val text: String) : Response() + +fun main() { + val tsGenerator = KxsTsGenerator() + println( + tsGenerator.generate(Response.serializer()) + ) +} diff --git a/docs/code/example/example-polymorphic-sealed-class-01.kt b/docs/code/example/example-polymorphic-sealed-class-01.kt new file mode 100644 index 00000000..43236c77 --- /dev/null +++ b/docs/code/example/example-polymorphic-sealed-class-01.kt @@ -0,0 +1,23 @@ +// This file was automatically generated from polymorphism.md by Knit tool. Do not edit. +@file:Suppress("PackageDirectoryMismatch", "unused") +package dev.adamko.kxstsgen.example.examplePolymorphicSealedClass01 + +import kotlinx.serialization.* +import dev.adamko.kxstsgen.* + +@Serializable +sealed class Project { + abstract val name: String +} + +@Serializable +@SerialName("OProj") +class OwnedProject(override val name: String, val owner: String) : Project() + +@Serializable +class DeprecatedProject(override val name: String, val reason: String) : Project() + +fun main() { + val tsGenerator = KxsTsGenerator() + println(tsGenerator.generate(Project.serializer())) +} diff --git a/docs/code/example/example-polymorphic-sealed-class-02.kt b/docs/code/example/example-polymorphic-sealed-class-02.kt new file mode 100644 index 00000000..dd93fbcc --- /dev/null +++ b/docs/code/example/example-polymorphic-sealed-class-02.kt @@ -0,0 +1,38 @@ +// This file was automatically generated from polymorphism.md by Knit tool. Do not edit. +@file:Suppress("PackageDirectoryMismatch", "unused") +package dev.adamko.kxstsgen.example.examplePolymorphicSealedClass02 + +import kotlinx.serialization.* +import dev.adamko.kxstsgen.* + +@Serializable +sealed class Dog { + abstract val name: String + + @Serializable + class Mutt(override val name: String, val loveable: Boolean = true) : Dog() + + @Serializable + sealed class Retriever : Dog() { + abstract val colour: String + + @Serializable + data class Golden( + override val name: String, + override val colour: String, + val cute: Boolean = true, + ) : Retriever() + + @Serializable + data class NovaScotia( + override val name: String, + override val colour: String, + val adorable: Boolean = true, + ) : Retriever() + } +} + +fun main() { + val tsGenerator = KxsTsGenerator() + println(tsGenerator.generate(Dog.serializer())) +} diff --git a/docs/code/example/example-polymorphic-static-types-01.kt b/docs/code/example/example-polymorphic-static-types-01.kt new file mode 100644 index 00000000..bf9c10c5 --- /dev/null +++ b/docs/code/example/example-polymorphic-static-types-01.kt @@ -0,0 +1,16 @@ +// This file was automatically generated from polymorphism.md by Knit tool. Do not edit. +@file:Suppress("PackageDirectoryMismatch", "unused") +package dev.adamko.kxstsgen.example.examplePolymorphicStaticTypes01 + +import kotlinx.serialization.* +import dev.adamko.kxstsgen.* + +@Serializable +open class Project(val name: String) + +class OwnedProject(name: String, val owner: String) : Project(name) + +fun main() { + val tsGenerator = KxsTsGenerator() + println(tsGenerator.generate(Project.serializer())) +} diff --git a/docs/code/example/example-polymorphic-static-types-02.kt b/docs/code/example/example-polymorphic-static-types-02.kt new file mode 100644 index 00000000..88858347 --- /dev/null +++ b/docs/code/example/example-polymorphic-static-types-02.kt @@ -0,0 +1,30 @@ +// This file was automatically generated from polymorphism.md by Knit tool. Do not edit. +@file:Suppress("PackageDirectoryMismatch", "unused") +package dev.adamko.kxstsgen.example.examplePolymorphicStaticTypes02 + +import kotlinx.serialization.* +import dev.adamko.kxstsgen.* + +import kotlinx.serialization.modules.* + +@Serializable +abstract class Project { + abstract val name: String +} + +@Serializable +class OwnedProject(override val name: String, val owner: String) : Project() + +val module = SerializersModule { + polymorphic(Project::class) { + subclass(OwnedProject::class) + } +} + +fun main() { + val config = KxsTsConfig(serializersModule = module) + + val tsGenerator = KxsTsGenerator(config) + + println(tsGenerator.generate(Project.serializer())) +} diff --git a/docs/knit/example/example-value-classes-01.kt b/docs/code/example/example-value-classes-01.kt similarity index 74% rename from docs/knit/example/example-value-classes-01.kt rename to docs/code/example/example-value-classes-01.kt index bedab750..d4c21bae 100644 --- a/docs/knit/example/example-value-classes-01.kt +++ b/docs/code/example/example-value-classes-01.kt @@ -1,6 +1,6 @@ // This file was automatically generated from value-classes.md by Knit tool. Do not edit. @file:Suppress("PackageDirectoryMismatch", "unused") -package example.exampleValueClasses01 +package dev.adamko.kxstsgen.example.exampleValueClasses01 import kotlinx.serialization.* import dev.adamko.kxstsgen.* @@ -11,5 +11,5 @@ value class AuthToken(private val token: String) fun main() { val tsGenerator = KxsTsGenerator() - println(tsGenerator.generate(AuthToken.serializer().descriptor)) + println(tsGenerator.generate(AuthToken.serializer())) } diff --git a/docs/knit/example/example-value-classes-02.kt b/docs/code/example/example-value-classes-02.kt similarity index 65% rename from docs/knit/example/example-value-classes-02.kt rename to docs/code/example/example-value-classes-02.kt index cf215d62..44a052b6 100644 --- a/docs/knit/example/example-value-classes-02.kt +++ b/docs/code/example/example-value-classes-02.kt @@ -1,6 +1,6 @@ // This file was automatically generated from value-classes.md by Knit tool. Do not edit. @file:Suppress("PackageDirectoryMismatch", "unused") -package example.exampleValueClasses02 +package dev.adamko.kxstsgen.example.exampleValueClasses02 import kotlinx.serialization.* import dev.adamko.kxstsgen.* @@ -11,10 +11,10 @@ fun main() { val tsGenerator = KxsTsGenerator() println( tsGenerator.generate( - UByte.serializer().descriptor, - UShort.serializer().descriptor, - UInt.serializer().descriptor, - ULong.serializer().descriptor, + UByte.serializer(), + UShort.serializer(), + UInt.serializer(), + ULong.serializer(), ) ) } diff --git a/docs/code/example/example-value-classes-03.kt b/docs/code/example/example-value-classes-03.kt new file mode 100644 index 00000000..90b33c8d --- /dev/null +++ b/docs/code/example/example-value-classes-03.kt @@ -0,0 +1,22 @@ +// This file was automatically generated from value-classes.md by Knit tool. Do not edit. +@file:Suppress("PackageDirectoryMismatch", "unused") +package dev.adamko.kxstsgen.example.exampleValueClasses03 + +import kotlinx.serialization.* +import dev.adamko.kxstsgen.* + +import kotlinx.serialization.builtins.serializer +import dev.adamko.kxstsgen.KxsTsConfig.TypeAliasTypingConfig.BrandTyping + + +fun main() { + + val tsConfig = KxsTsConfig(typeAliasTyping = BrandTyping) + + val tsGenerator = KxsTsGenerator(config = tsConfig) + println( + tsGenerator.generate( + ULong.serializer(), + ) + ) +} diff --git a/docs/knit/example/example-value-classes-03.kt b/docs/code/example/example-value-classes-04.kt similarity index 74% rename from docs/knit/example/example-value-classes-03.kt rename to docs/code/example/example-value-classes-04.kt index 95c5d22f..6793a720 100644 --- a/docs/knit/example/example-value-classes-03.kt +++ b/docs/code/example/example-value-classes-04.kt @@ -1,6 +1,6 @@ // This file was automatically generated from value-classes.md by Knit tool. Do not edit. @file:Suppress("PackageDirectoryMismatch", "unused") -package example.exampleValueClasses03 +package dev.adamko.kxstsgen.example.exampleValueClasses04 import kotlinx.serialization.* import dev.adamko.kxstsgen.* @@ -11,5 +11,5 @@ value class UserCount(private val count: UInt) fun main() { val tsGenerator = KxsTsGenerator() - println(tsGenerator.generate(UserCount.serializer().descriptor)) + println(tsGenerator.generate(UserCount.serializer())) } diff --git a/docs/knit/knit-include.ftl b/docs/code/knit-include.ftl similarity index 100% rename from docs/knit/knit-include.ftl rename to docs/code/knit-include.ftl diff --git a/docs/knit/knit-test.ftl b/docs/code/knit-test.ftl similarity index 61% rename from docs/knit/knit-test.ftl rename to docs/code/knit-test.ftl index e4ae1290..78bac9ff 100644 --- a/docs/knit/knit-test.ftl +++ b/docs/code/knit-test.ftl @@ -1,11 +1,13 @@ -<#-- @ftlvariable name="test.name" type="String" --> -<#-- @ftlvariable name="test.package" type="String" --> -<#-- @ftlvariable name="file.name" type="String" --> +<#-- @ftlvariable name="test.name" type="java.lang.String" --> +<#-- @ftlvariable name="test.package" type="java.lang.String" --> // This file was automatically generated from ${file.name} by Knit tool. Do not edit. +@file:Suppress("JSUnusedLocalSymbols") package ${test.package} -import org.junit.jupiter.api.Test +import io.kotest.matchers.* import kotlinx.knit.test.* +import org.junit.jupiter.api.Test +import dev.adamko.kxstsgen.util.* class ${test.name} { <#list cases as case><#assign method = test["mode.${case.param}"]!"custom"> @@ -14,10 +16,14 @@ class ${test.name} { captureOutput("${case.name}") { ${case.knit.package}.${case.knit.name}.main() }<#if method != "custom">.${method}( + // language=TypeScript + """ <#list case.lines as line> - "${line?j_string}"<#sep>, + |${line} - ) + """.trimMargin() + .normalize() + ) <#else>.also { lines -> check(${case.param}) } diff --git a/docs/code/test/AbstractClassesTest.kt b/docs/code/test/AbstractClassesTest.kt new file mode 100644 index 00000000..3c71a4aa --- /dev/null +++ b/docs/code/test/AbstractClassesTest.kt @@ -0,0 +1,65 @@ +// This file was automatically generated from abstract-classes.md by Knit tool. Do not edit. +@file:Suppress("JSUnusedLocalSymbols") +package dev.adamko.kxstsgen.example.test + +import io.kotest.matchers.* +import kotlinx.knit.test.* +import org.junit.jupiter.api.Test +import dev.adamko.kxstsgen.util.* + +class AbstractClassesTest { + @Test + fun testExampleAbstractClassSingleField01() { + captureOutput("ExampleAbstractClassSingleField01") { + dev.adamko.kxstsgen.example.exampleAbstractClassSingleField01.main() + }.normalizeJoin() + .shouldBe( + // language=TypeScript + """ + |export type Color = any; + |// interface Color { + |// rgb: number; + |// } + """.trimMargin() + .normalize() + ) + } + + @Test + fun testExampleAbstractClassPrimitiveFields01() { + captureOutput("ExampleAbstractClassPrimitiveFields01") { + dev.adamko.kxstsgen.example.exampleAbstractClassPrimitiveFields01.main() + }.normalizeJoin() + .shouldBe( + // language=TypeScript + """ + |export type SimpleTypes = any; + |// export interface SimpleTypes { + |// aString: string; + |// anInt: number; + |// aDouble: number; + |// bool: boolean; + |// privateMember: string; + |// } + """.trimMargin() + .normalize() + ) + } + + @Test + fun testExampleAbstractClassAbstractField01() { + captureOutput("ExampleAbstractClassAbstractField01") { + dev.adamko.kxstsgen.example.exampleAbstractClassAbstractField01.main() + }.normalizeJoin() + .shouldBe( + // language=TypeScript + """ + |export type AbstractSimpleTypes = any; + |// export interface AbstractSimpleTypes { + |// rgb: number; + |// } + """.trimMargin() + .normalize() + ) + } +} diff --git a/docs/code/test/BasicClassesTest.kt b/docs/code/test/BasicClassesTest.kt new file mode 100644 index 00000000..9daaa133 --- /dev/null +++ b/docs/code/test/BasicClassesTest.kt @@ -0,0 +1,81 @@ +// This file was automatically generated from basic-classes.md by Knit tool. Do not edit. +@file:Suppress("JSUnusedLocalSymbols") +package dev.adamko.kxstsgen.example.test + +import io.kotest.matchers.* +import kotlinx.knit.test.* +import org.junit.jupiter.api.Test +import dev.adamko.kxstsgen.util.* + +class BasicClassesTest { + @Test + fun testExamplePlainClassSingleField01() { + captureOutput("ExamplePlainClassSingleField01") { + dev.adamko.kxstsgen.example.examplePlainClassSingleField01.main() + }.normalizeJoin() + .shouldBe( + // language=TypeScript + """ + |export interface Color { + | rgb: number; + |} + """.trimMargin() + .normalize() + ) + } + + @Test + fun testExamplePlainClassPrimitiveFields01() { + captureOutput("ExamplePlainClassPrimitiveFields01") { + dev.adamko.kxstsgen.example.examplePlainClassPrimitiveFields01.main() + }.normalizeJoin() + .shouldBe( + // language=TypeScript + """ + |export interface SimpleTypes { + | aString: string; + | anInt: number; + | aDouble: number; + | bool: boolean; + | privateMember: string; + |} + """.trimMargin() + .normalize() + ) + } + + @Test + fun testExamplePlainDataClass01() { + captureOutput("ExamplePlainDataClass01") { + dev.adamko.kxstsgen.example.examplePlainDataClass01.main() + }.normalizeJoin() + .shouldBe( + // language=TypeScript + """ + |export interface SomeDataClass { + | aString: string; + | anInt: number; + | aDouble: number; + | bool: boolean; + | privateMember: string; + |} + """.trimMargin() + .normalize() + ) + } + + @Test + fun testExamplePlainClassPrimitiveFields02() { + captureOutput("ExamplePlainClassPrimitiveFields02") { + dev.adamko.kxstsgen.example.examplePlainClassPrimitiveFields02.main() + }.normalizeJoin() + .shouldBe( + // language=TypeScript + """ + |export interface SimpleTypes { + |} + """.trimMargin() + .normalize() + ) + } +} diff --git a/docs/code/test/DefaultValuesTest.kt b/docs/code/test/DefaultValuesTest.kt new file mode 100644 index 00000000..9ccdcf08 --- /dev/null +++ b/docs/code/test/DefaultValuesTest.kt @@ -0,0 +1,61 @@ +// This file was automatically generated from default-values.md by Knit tool. Do not edit. +@file:Suppress("JSUnusedLocalSymbols") +package dev.adamko.kxstsgen.example.test + +import io.kotest.matchers.* +import kotlinx.knit.test.* +import org.junit.jupiter.api.Test +import dev.adamko.kxstsgen.util.* + +class DefaultValuesTest { + @Test + fun testExampleDefaultValuesSingleField01() { + captureOutput("ExampleDefaultValuesSingleField01") { + dev.adamko.kxstsgen.example.exampleDefaultValuesSingleField01.main() + }.normalizeJoin() + .shouldBe( + // language=TypeScript + """ + |export interface Colour { + | rgb?: number; + |} + """.trimMargin() + .normalize() + ) + } + + @Test + fun testExampleDefaultValuesSingleField02() { + captureOutput("ExampleDefaultValuesSingleField02") { + dev.adamko.kxstsgen.example.exampleDefaultValuesSingleField02.main() + }.normalizeJoin() + .shouldBe( + // language=TypeScript + """ + |export interface Colour { + | rgb: number | null; + |} + """.trimMargin() + .normalize() + ) + } + + @Test + fun testExampleDefaultValuesPrimitiveFields01() { + captureOutput("ExampleDefaultValuesPrimitiveFields01") { + dev.adamko.kxstsgen.example.exampleDefaultValuesPrimitiveFields01.main() + }.normalizeJoin() + .shouldBe( + // language=TypeScript + """ + |export interface ContactDetails { + | name: string; + | email: string | null; + | active?: boolean; + | phoneNumber?: string | null; + |} + """.trimMargin() + .normalize() + ) + } +} diff --git a/docs/code/test/EdgeCasesTest.kt b/docs/code/test/EdgeCasesTest.kt new file mode 100644 index 00000000..09d30de0 --- /dev/null +++ b/docs/code/test/EdgeCasesTest.kt @@ -0,0 +1,70 @@ +// This file was automatically generated from edgecases.md by Knit tool. Do not edit. +@file:Suppress("JSUnusedLocalSymbols") +package dev.adamko.kxstsgen.example.test + +import io.kotest.matchers.* +import kotlinx.knit.test.* +import org.junit.jupiter.api.Test +import dev.adamko.kxstsgen.util.* + +class EdgeCasesTest { + @Test + fun testExampleEdgecaseRecursiveReferences01() { + captureOutput("ExampleEdgecaseRecursiveReferences01") { + dev.adamko.kxstsgen.example.exampleEdgecaseRecursiveReferences01.main() + }.normalizeJoin() + .shouldBe( + // language=TypeScript + """ + |export interface A { + | b: B; + |} + | + |export interface B { + | a: A; + |} + """.trimMargin() + .normalize() + ) + } + + @Test + fun testExampleEdgecaseRecursiveReferences02() { + captureOutput("ExampleEdgecaseRecursiveReferences02") { + dev.adamko.kxstsgen.example.exampleEdgecaseRecursiveReferences02.main() + }.normalizeJoin() + .shouldBe( + // language=TypeScript + """ + |export interface A { + | list: B[]; + |} + | + |export interface B { + | list: A[]; + |} + """.trimMargin() + .normalize() + ) + } + + @Test + fun testExampleEdgecaseRecursiveReferences03() { + captureOutput("ExampleEdgecaseRecursiveReferences03") { + dev.adamko.kxstsgen.example.exampleEdgecaseRecursiveReferences03.main() + }.normalizeJoin() + .shouldBe( + // language=TypeScript + """ + |export interface A { + | map: { [key: string]: B }; + |} + | + |export interface B { + | map: { [key: string]: A }; + |} + """.trimMargin() + .normalize() + ) + } +} diff --git a/docs/code/test/EnumClassTest.kt b/docs/code/test/EnumClassTest.kt new file mode 100644 index 00000000..0d1f82ba --- /dev/null +++ b/docs/code/test/EnumClassTest.kt @@ -0,0 +1,46 @@ +// This file was automatically generated from enums.md by Knit tool. Do not edit. +@file:Suppress("JSUnusedLocalSymbols") +package dev.adamko.kxstsgen.example.test + +import io.kotest.matchers.* +import kotlinx.knit.test.* +import org.junit.jupiter.api.Test +import dev.adamko.kxstsgen.util.* + +class EnumClassTest { + @Test + fun testExampleEnumClass01() { + captureOutput("ExampleEnumClass01") { + dev.adamko.kxstsgen.example.exampleEnumClass01.main() + }.normalizeJoin() + .shouldBe( + // language=TypeScript + """ + |export enum SomeType { + | Alpha = "Alpha", + | Beta = "Beta", + | Gamma = "Gamma", + |} + """.trimMargin() + .normalize() + ) + } + + @Test + fun testExampleEnumClass02() { + captureOutput("ExampleEnumClass02") { + dev.adamko.kxstsgen.example.exampleEnumClass02.main() + }.normalizeJoin() + .shouldBe( + // language=TypeScript + """ + |export enum SomeType2 { + | Alpha = "Alpha", + | Beta = "Beta", + | Gamma = "Gamma", + |} + """.trimMargin() + .normalize() + ) + } +} diff --git a/docs/code/test/ListsTests.kt b/docs/code/test/ListsTests.kt new file mode 100644 index 00000000..8ab6deb0 --- /dev/null +++ b/docs/code/test/ListsTests.kt @@ -0,0 +1,28 @@ +// This file was automatically generated from lists.md by Knit tool. Do not edit. +@file:Suppress("JSUnusedLocalSymbols") +package dev.adamko.kxstsgen.example.test + +import io.kotest.matchers.* +import kotlinx.knit.test.* +import org.junit.jupiter.api.Test +import dev.adamko.kxstsgen.util.* + +class ListsTests { + @Test + fun testExampleListPrimitive01() { + captureOutput("ExampleListPrimitive01") { + dev.adamko.kxstsgen.example.exampleListPrimitive01.main() + }.normalizeJoin() + .shouldBe( + // language=TypeScript + """ + |export interface MyLists { + | strings: string[]; + | ints: number[]; + | longs: number[]; + |} + """.trimMargin() + .normalize() + ) + } +} diff --git a/docs/code/test/MapsTests.kt b/docs/code/test/MapsTests.kt new file mode 100644 index 00000000..28dfb15f --- /dev/null +++ b/docs/code/test/MapsTests.kt @@ -0,0 +1,122 @@ +// This file was automatically generated from maps.md by Knit tool. Do not edit. +@file:Suppress("JSUnusedLocalSymbols") +package dev.adamko.kxstsgen.example.test + +import io.kotest.matchers.* +import kotlinx.knit.test.* +import org.junit.jupiter.api.Test +import dev.adamko.kxstsgen.util.* + +class MapsTests { + @Test + fun testExampleMapPrimitive01() { + captureOutput("ExampleMapPrimitive01") { + dev.adamko.kxstsgen.example.exampleMapPrimitive01.main() + }.normalizeJoin() + .shouldBe( + // language=TypeScript + """ + |export interface Config { + | properties: { [key: string]: string }; + |} + """.trimMargin() + .normalize() + ) + } + + @Test + fun testExampleMapPrimitive02() { + captureOutput("ExampleMapPrimitive02") { + dev.adamko.kxstsgen.example.exampleMapPrimitive02.main() + }.normalizeJoin() + .shouldBe( + // language=TypeScript + """ + |export interface Application { + | settings: { [key in SettingKeys]: string }; + |} + | + |export enum SettingKeys { + | SCREEN_SIZE = "SCREEN_SIZE", + | MAX_MEMORY = "MAX_MEMORY", + |} + """.trimMargin() + .normalize() + ) + } + + @Test + fun testExampleMapPrimitive03() { + captureOutput("ExampleMapPrimitive03") { + dev.adamko.kxstsgen.example.exampleMapPrimitive03.main() + }.normalizeJoin() + .shouldBe( + // language=TypeScript + """ + |export interface Config { + | properties: { [key: string | null]: string | null }; + |} + """.trimMargin() + .normalize() + ) + } + + @Test + fun testExampleMapComplex01() { + captureOutput("ExampleMapComplex01") { + dev.adamko.kxstsgen.example.exampleMapComplex01.main() + }.normalizeJoin() + .shouldBe( + // language=TypeScript + """ + |export interface CanvasProperties { + | colourNames: Map; + |} + | + |export interface Colour { + | r: UByte; + | g: UByte; + | b: UByte; + | a: UByte; + |} + | + |export type UByte = number; + """.trimMargin() + .normalize() + ) + } + + @Test + fun testExampleMapComplex02() { + captureOutput("ExampleMapComplex02") { + dev.adamko.kxstsgen.example.exampleMapComplex02.main() + }.normalizeJoin() + .shouldBe( + // language=TypeScript + """ + |export interface CanvasProperties { + | colourNames: Map; + |} + | + |export type ColourMapKey = string; + """.trimMargin() + .normalize() + ) + } + + @Test + fun testExampleMapComplex03() { + captureOutput("ExampleMapComplex03") { + dev.adamko.kxstsgen.example.exampleMapComplex03.main() + }.normalizeJoin() + .shouldBe( + // language=TypeScript + """ + |export interface CanvasProperties { + | colourNames: { [key: string]: string }; + |} + """.trimMargin() + .normalize() + ) + } +} diff --git a/docs/code/test/PolymorphismTest.kt b/docs/code/test/PolymorphismTest.kt new file mode 100644 index 00000000..271f41ec --- /dev/null +++ b/docs/code/test/PolymorphismTest.kt @@ -0,0 +1,209 @@ +// This file was automatically generated from polymorphism.md by Knit tool. Do not edit. +@file:Suppress("JSUnusedLocalSymbols") +package dev.adamko.kxstsgen.example.test + +import io.kotest.matchers.* +import kotlinx.knit.test.* +import org.junit.jupiter.api.Test +import dev.adamko.kxstsgen.util.* + +class PolymorphismTest { + @Test + fun testExamplePolymorphicAbstractClassPrimitiveFields01() { + captureOutput("ExamplePolymorphicAbstractClassPrimitiveFields01") { + dev.adamko.kxstsgen.example.examplePolymorphicAbstractClassPrimitiveFields01.main() + }.normalizeJoin() + .shouldBe( + // language=TypeScript + """ + |export type SimpleTypes = any; + |// export interface SimpleTypes { + |// aString: string; + |// anInt: number; + |// aDouble: number; + |// bool: boolean; + |// privateMember: string; + |// } + """.trimMargin() + .normalize() + ) + } + + @Test + fun testExamplePolymorphicStaticTypes01() { + captureOutput("ExamplePolymorphicStaticTypes01") { + dev.adamko.kxstsgen.example.examplePolymorphicStaticTypes01.main() + }.normalizeJoin() + .shouldBe( + // language=TypeScript + """ + |export interface Project { + | name: string; + |} + """.trimMargin() + .normalize() + ) + } + + @Test + fun testExamplePolymorphicStaticTypes02() { + captureOutput("ExamplePolymorphicStaticTypes02") { + dev.adamko.kxstsgen.example.examplePolymorphicStaticTypes02.main() + }.normalizeJoin() + .shouldBe( + // language=TypeScript + """ + |export type Project = any; + |// export interface Project { + |// name: string; + |// } + |// + |// export interface OwnedProject extends Project { + |// name: string; + |// owner: string; + |// } + """.trimMargin() + .normalize() + ) + } + + @Test + fun testExamplePolymorphicSealedClass01() { + captureOutput("ExamplePolymorphicSealedClass01") { + dev.adamko.kxstsgen.example.examplePolymorphicSealedClass01.main() + }.normalizeJoin() + .shouldBe( + // language=TypeScript + """ + |export type Project = Project.DeprecatedProject | Project.OProj; + | + |export namespace Project { + | export enum Type { + | OProj = "OProj", + | DeprecatedProject = "DeprecatedProject", + | } + | + | export interface OProj { + | type: Project.Type.OProj; + | name: string; + | owner: string; + | } + | + | export interface DeprecatedProject { + | type: Project.Type.DeprecatedProject; + | name: string; + | reason: string; + | } + |} + """.trimMargin() + .normalize() + ) + } + + @Test + fun testExamplePolymorphicSealedClass02() { + captureOutput("ExamplePolymorphicSealedClass02") { + dev.adamko.kxstsgen.example.examplePolymorphicSealedClass02.main() + }.normalizeJoin() + .shouldBe( + // language=TypeScript + """ + |export type Dog = Dog.Golden | Dog.Mutt | Dog.NovaScotia; + | + |export namespace Dog { + | export enum Type { + | Mutt = "Mutt", + | Golden = "Golden", + | NovaScotia = "NovaScotia", + | } + | + | export interface Mutt { + | type: Dog.Type.Mutt; + | name: string; + | loveable?: boolean; + | } + | + | export interface Golden { + | type: Dog.Type.Golden; + | name: string; + | colour: string; + | cute?: boolean; + | } + | + | export interface NovaScotia { + | type: Dog.Type.NovaScotia; + | name: string; + | colour: string; + | adorable?: boolean; + | } + |} + |// Nested sealed classes don't work at the moment :( + |// export type Dog = Dog.Mutt | Dog.Retriever + |// + |// export namespace Dog { + |// export enum Type { + |// Mutt = "Mutt", + |// } + |// + |// export interface Mutt { + |// type: Type.Mutt; + |// name: string; + |// loveable?: boolean; + |// } + |// + |// export type Retriever = Retriever.Golden | Retriever.NovaScotia + |// + |// export namespace Retriever { + |// export enum Type { + |// Golden = "Golden", + |// NovaScotia = "NovaScotia", + |// } + |// + |// export interface Golden { + |// type: Type.Golden; + |// name: string; + |// cute?: boolean; + |// } + |// + |// export interface NovaScotia { + |// type: Type.NovaScotia; + |// name: string; + |// adorable?: boolean; + |// } + |// } + |// } + """.trimMargin() + .normalize() + ) + } + + @Test + fun testExamplePolymorphicObjects01() { + captureOutput("ExamplePolymorphicObjects01") { + dev.adamko.kxstsgen.example.examplePolymorphicObjects01.main() + }.normalizeJoin() + .shouldBe( + // language=TypeScript + """ + |export type Response = Response.EmptyResponse | Response.TextResponse; + | + |export namespace Response { + | export enum Type { + | EmptyResponse = "EmptyResponse", + | TextResponse = "TextResponse", + | } + | + | export interface EmptyResponse { + | type: Response.Type.EmptyResponse; + | } + | + | export interface TextResponse { + | type: Response.Type.TextResponse; + | text: string; + | } + |} + """.trimMargin() + .normalize() + ) + } +} diff --git a/docs/code/test/ValueClassesTest.kt b/docs/code/test/ValueClassesTest.kt new file mode 100644 index 00000000..845c6b6f --- /dev/null +++ b/docs/code/test/ValueClassesTest.kt @@ -0,0 +1,74 @@ +// This file was automatically generated from value-classes.md by Knit tool. Do not edit. +@file:Suppress("JSUnusedLocalSymbols") +package dev.adamko.kxstsgen.example.test + +import io.kotest.matchers.* +import kotlinx.knit.test.* +import org.junit.jupiter.api.Test +import dev.adamko.kxstsgen.util.* + +class ValueClassesTest { + @Test + fun testExampleValueClasses01() { + captureOutput("ExampleValueClasses01") { + dev.adamko.kxstsgen.example.exampleValueClasses01.main() + }.normalizeJoin() + .shouldBe( + // language=TypeScript + """ + |export type AuthToken = string; + """.trimMargin() + .normalize() + ) + } + + @Test + fun testExampleValueClasses02() { + captureOutput("ExampleValueClasses02") { + dev.adamko.kxstsgen.example.exampleValueClasses02.main() + }.normalizeJoin() + .shouldBe( + // language=TypeScript + """ + |export type UByte = number; + | + |export type UShort = number; + | + |export type UInt = number; + | + |export type ULong = number; + """.trimMargin() + .normalize() + ) + } + + @Test + fun testExampleValueClasses03() { + captureOutput("ExampleValueClasses03") { + dev.adamko.kxstsgen.example.exampleValueClasses03.main() + }.normalizeJoin() + .shouldBe( + // language=TypeScript + """ + |export type ULong = number & { __ULong__: void }; + """.trimMargin() + .normalize() + ) + } + + @Test + fun testExampleValueClasses04() { + captureOutput("ExampleValueClasses04") { + dev.adamko.kxstsgen.example.exampleValueClasses04.main() + }.normalizeJoin() + .shouldBe( + // language=TypeScript + """ + |export type UserCount = UInt; + | + |export type UInt = number; + """.trimMargin() + .normalize() + ) + } +} diff --git a/docs/code/util/strings.kt b/docs/code/util/strings.kt new file mode 100644 index 00000000..08a8fa76 --- /dev/null +++ b/docs/code/util/strings.kt @@ -0,0 +1,20 @@ +package dev.adamko.kxstsgen.util + +/** + * * filter out lines that are `//` comments + * * convert whitespace-only lines to an empty string + * * remove `//` comments + * * remove trailing whitespace + */ +fun String.normalize(): String = + lines() + .filterNot { it.trimStart().startsWith("//") } + .joinToString("\n") { + it.substringBeforeLast("//") + .trimEnd() + .ifBlank { "" } + } + +/** [normalize] each String, then [join][joinToString] them */ +fun Iterable.normalizeJoin(): String = + joinToString("\n") { it.normalize() } diff --git a/docs/default-values.md b/docs/default-values.md index 335c8a2d..8cc78c84 100644 --- a/docs/default-values.md +++ b/docs/default-values.md @@ -1,10 +1,26 @@ +**Table of contents** + + + +* [Introduction](#introduction) + * [Default values](#default-values) + * [Nullable values](#nullable-values) + * [Default and nullable](#default-and-nullable) + + + + +## Introduction + +Some properties of a class are optional, or nullable, or both. + ### Default values If a value has a default value, then it is not required for creating an encoded message. Therefore, @@ -12,51 +28,80 @@ it will be marked as optional using the `?:` notation. ```kotlin @Serializable -class Color(val rgb: Int = 12345) +class Colour(val rgb: Int = 12345) fun main() { val tsGenerator = KxsTsGenerator() - println(tsGenerator.generate(Color.serializer().descriptor)) + println(tsGenerator.generate(Colour.serializer())) } ``` -> You can get the full code [here](./knit/example/example-default-values-single-field-01.kt). +> You can get the full code [here](./code/example/example-default-values-single-field-01.kt). ```typescript -interface Color { +export interface Colour { rgb?: number; } ``` +### Nullable values + +Properties might be required, but the value can be nullable. In TypeScript that is represented with +a type union that includes `null`. + +```kotlin +@Serializable +class Colour(val rgb: Int?) // 'rgb' is required, but the value can be null + +fun main() { + val tsGenerator = KxsTsGenerator() + println(tsGenerator.generate(Colour.serializer())) +} +``` + +> You can get the full code [here](./code/example/example-default-values-single-field-02.kt). + +```typescript +export interface Colour { + rgb: number | null; +} +``` + + + ### Default and nullable +A property can be both nullable and optional, which gives four possible options. + ```kotlin @Serializable data class ContactDetails( + // nullable: ❌, optional: ❌ + val name: String, + // nullable: ✅, optional: ❌ val email: String?, + // nullable: ❌, optional: ✅ + val active: Boolean = true, + // nullable: ✅, optional: ✅ val phoneNumber: String? = null, - val active: Boolean? = true, ) fun main() { val tsGenerator = KxsTsGenerator() - println(tsGenerator.generate(ContactDetails.serializer().descriptor)) + println(tsGenerator.generate(ContactDetails.serializer())) } ``` -> You can get the full code [here](./knit/example/example-default-values-primitive-fields-01.kt). - -Email has no default, so it is not marked as optional. - -Phone number and is nullable, and has a default, so i +> You can get the full code [here](./code/example/example-default-values-primitive-fields-01.kt). ```typescript -interface ContactDetails { +export interface ContactDetails { + name: string; email: string | null; + active?: boolean; phoneNumber?: string | null; - active?: boolean | null; } ``` diff --git a/docs/edgecases.md b/docs/edgecases.md new file mode 100644 index 00000000..5f59b960 --- /dev/null +++ b/docs/edgecases.md @@ -0,0 +1,121 @@ + + +**Table of contents** + + + +* [Introduction](#introduction) + * [Recursive references](#recursive-references) + * [Classes](#classes) + * [Lists](#lists) + * [Map](#map) + + + + + +## Introduction + +Lorem ipsum... + +### Recursive references + +A references B which references A which references B... should be handled properly + +#### Classes + +```kotlin +@Serializable +class A(val b: B) + +@Serializable +class B(val a: A) + +fun main() { + val tsGenerator = KxsTsGenerator() + println(tsGenerator.generate(A.serializer(), B.serializer())) +} +``` + +> You can get the full code [here](./code/example/example-edgecase-recursive-references-01.kt). + +```typescript +export interface A { + b: B; +} + +export interface B { + a: A; +} +``` + + + +#### Lists + +```kotlin +@Serializable +class A( + val list: List +) + +@Serializable +class B( + val list: List +) + +fun main() { + val tsGenerator = KxsTsGenerator() + println(tsGenerator.generate(A.serializer(), B.serializer())) +} +``` + +> You can get the full code [here](./code/example/example-edgecase-recursive-references-02.kt). + +```typescript +export interface A { + list: B[]; +} + +export interface B { + list: A[]; +} +``` + + + +#### Map + +```kotlin +@Serializable +class A( + val map: Map +) + +@Serializable +class B( + val map: Map +) + +fun main() { + val tsGenerator = KxsTsGenerator() + println(tsGenerator.generate(A.serializer(), B.serializer())) +} +``` + +> You can get the full code [here](./code/example/example-edgecase-recursive-references-03.kt). + +```typescript +export interface A { + map: { [key: string]: B }; +} + +export interface B { + map: { [key: string]: A }; +} +``` + + diff --git a/docs/enums.md b/docs/enums.md index 4bd54d30..68d9a152 100644 --- a/docs/enums.md +++ b/docs/enums.md @@ -1,10 +1,25 @@ -## Introduction -Lorem ipsum... +**Table of contents** + + + +* [Introduction](#introduction) + * [Simple enum](#simple-enum) + * [Enum with properties](#enum-with-properties) + + -### Plain class with a single field + + + +## Introduction + +### Simple enum -### Plain class with primitive fields +### Enum with properties + +Because enums are static, fields aren't converted. ```kotlin @Serializable @@ -51,14 +68,14 @@ enum class SomeType2(val coolName: String) { fun main() { val tsGenerator = KxsTsGenerator() - println(tsGenerator.generate(SomeType2.serializer().descriptor)) + println(tsGenerator.generate(SomeType2.serializer())) } ``` -> You can get the full code [here](./knit/example/example-enum-class-02.kt). +> You can get the full code [here](./code/example/example-enum-class-02.kt). ```typescript -enum SomeType2 { +export enum SomeType2 { Alpha = "Alpha", Beta = "Beta", Gamma = "Gamma", diff --git a/docs/knit.properties b/docs/knit.properties index 1a4114ba..5b8bc032 100644 --- a/docs/knit.properties +++ b/docs/knit.properties @@ -1,8 +1,9 @@ -knit.dir=./knit/example/ -test.dir=./knit/test/ -knit.package=example -test.package=example.test +knit.dir=./code/example/ +test.dir=./code/test/ +knit.package=dev.adamko.kxstsgen.example +test.package=dev.adamko.kxstsgen.example.test # -test.template=./knit/knit-test.ftl +test.template=./code/knit-test.ftl test.language=typescript -knit.include=./knit/knit-include.ftl +knit.include=./code/knit-include.ftl +test.mode.=normalizeJoin()\n .shouldBe diff --git a/docs/knit/test/AbstractClassesTest.kt b/docs/knit/test/AbstractClassesTest.kt deleted file mode 100644 index 03406432..00000000 --- a/docs/knit/test/AbstractClassesTest.kt +++ /dev/null @@ -1,33 +0,0 @@ -// This file was automatically generated from abstract-classes.md by Knit tool. Do not edit. -package example.test - -import org.junit.jupiter.api.Test -import kotlinx.knit.test.* - -class AbstractClassesTest { - @Test - fun testExampleAbstractClassSingleField01() { - captureOutput("ExampleAbstractClassSingleField01") { - example.exampleAbstractClassSingleField01.main() - }.verifyOutputLines( - "interface Color {", - " rgb: number;", - "}" - ) - } - - @Test - fun testExampleAbstractClassPrimitiveFields01() { - captureOutput("ExampleAbstractClassPrimitiveFields01") { - example.exampleAbstractClassPrimitiveFields01.main() - }.verifyOutputLines( - "interface SimpleTypes {", - " aString: string;", - " anInt: number;", - " aDouble: number;", - " bool: boolean;", - " privateMember: string;", - "}" - ) - } -} diff --git a/docs/knit/test/BasicClassesTest.kt b/docs/knit/test/BasicClassesTest.kt deleted file mode 100644 index 2b347bee..00000000 --- a/docs/knit/test/BasicClassesTest.kt +++ /dev/null @@ -1,48 +0,0 @@ -// This file was automatically generated from basic-classes.md by Knit tool. Do not edit. -package example.test - -import org.junit.jupiter.api.Test -import kotlinx.knit.test.* - -class BasicClassesTest { - @Test - fun testExamplePlainClassSingleField01() { - captureOutput("ExamplePlainClassSingleField01") { - example.examplePlainClassSingleField01.main() - }.verifyOutputLines( - "interface Color {", - " rgb: number;", - "}" - ) - } - - @Test - fun testExamplePlainClassPrimitiveFields01() { - captureOutput("ExamplePlainClassPrimitiveFields01") { - example.examplePlainClassPrimitiveFields01.main() - }.verifyOutputLines( - "interface SimpleTypes {", - " aString: string;", - " anInt: number;", - " aDouble: number;", - " bool: boolean;", - " privateMember: string;", - "}" - ) - } - - @Test - fun testExamplePlainDataClass01() { - captureOutput("ExamplePlainDataClass01") { - example.examplePlainDataClass01.main() - }.verifyOutputLines( - "interface SomeDataClass {", - " aString: string;", - " anInt: number;", - " aDouble: number;", - " bool: boolean;", - " privateMember: string;", - "}" - ) - } -} diff --git a/docs/knit/test/DefaultValuesTest.kt b/docs/knit/test/DefaultValuesTest.kt deleted file mode 100644 index 257fc830..00000000 --- a/docs/knit/test/DefaultValuesTest.kt +++ /dev/null @@ -1,31 +0,0 @@ -// This file was automatically generated from default-values.md by Knit tool. Do not edit. -package example.test - -import org.junit.jupiter.api.Test -import kotlinx.knit.test.* - -class DefaultValuesTest { - @Test - fun testExampleDefaultValuesSingleField01() { - captureOutput("ExampleDefaultValuesSingleField01") { - example.exampleDefaultValuesSingleField01.main() - }.verifyOutputLines( - "interface Color {", - " rgb?: number;", - "}" - ) - } - - @Test - fun testExampleDefaultValuesPrimitiveFields01() { - captureOutput("ExampleDefaultValuesPrimitiveFields01") { - example.exampleDefaultValuesPrimitiveFields01.main() - }.verifyOutputLines( - "interface ContactDetails {", - " email: string | null;", - " phoneNumber?: string | null;", - " active?: boolean | null;", - "}" - ) - } -} diff --git a/docs/knit/test/EnumClassTest.kt b/docs/knit/test/EnumClassTest.kt deleted file mode 100644 index 88fa3b19..00000000 --- a/docs/knit/test/EnumClassTest.kt +++ /dev/null @@ -1,33 +0,0 @@ -// This file was automatically generated from enums.md by Knit tool. Do not edit. -package example.test - -import org.junit.jupiter.api.Test -import kotlinx.knit.test.* - -class EnumClassTest { - @Test - fun testExampleEnumClass01() { - captureOutput("ExampleEnumClass01") { - example.exampleEnumClass01.main() - }.verifyOutputLines( - "enum SomeType {", - " Alpha = \"Alpha\",", - " Beta = \"Beta\",", - " Gamma = \"Gamma\",", - "}" - ) - } - - @Test - fun testExampleEnumClass02() { - captureOutput("ExampleEnumClass02") { - example.exampleEnumClass02.main() - }.verifyOutputLines( - "enum SomeType2 {", - " Alpha = \"Alpha\",", - " Beta = \"Beta\",", - " Gamma = \"Gamma\",", - "}" - ) - } -} diff --git a/docs/knit/test/ListsTests.kt b/docs/knit/test/ListsTests.kt deleted file mode 100644 index 8b4cc83c..00000000 --- a/docs/knit/test/ListsTests.kt +++ /dev/null @@ -1,18 +0,0 @@ -// This file was automatically generated from lists.md by Knit tool. Do not edit. -package example.test - -import org.junit.jupiter.api.Test -import kotlinx.knit.test.* - -class ListsTests { - @Test - fun testExampleListPrimitive01() { - captureOutput("ExampleListPrimitive01") { - example.exampleListPrimitive01.main() - }.verifyOutputLines( - "interface CalendarEvent {", - " attendeeNames: string[]", - "}" - ) - } -} diff --git a/docs/knit/test/MapsTests.kt b/docs/knit/test/MapsTests.kt deleted file mode 100644 index 86b270ce..00000000 --- a/docs/knit/test/MapsTests.kt +++ /dev/null @@ -1,18 +0,0 @@ -// This file was automatically generated from maps.md by Knit tool. Do not edit. -package example.test - -import org.junit.jupiter.api.Test -import kotlinx.knit.test.* - -class MapsTests { - @Test - fun testExampleMapPrimitive01() { - captureOutput("ExampleMapPrimitive01") { - example.exampleMapPrimitive01.main() - }.verifyOutputLines( - "interface Config {", - " properties: { [key: string]: string };", - "}" - ) - } -} diff --git a/docs/knit/test/PolymorphismTest.kt b/docs/knit/test/PolymorphismTest.kt deleted file mode 100644 index 73dd1594..00000000 --- a/docs/knit/test/PolymorphismTest.kt +++ /dev/null @@ -1,33 +0,0 @@ -// This file was automatically generated from polymorphism.md by Knit tool. Do not edit. -package example.test - -import org.junit.jupiter.api.Test -import kotlinx.knit.test.* - -class PolymorphismTest { - @Test - fun testExamplePolymorphismSealed01() { - captureOutput("ExamplePolymorphismSealed01") { - example.examplePolymorphismSealed01.main() - }.verifyOutputLines( - "interface Color {", - " rgb: number;", - "}" - ) - } - - @Test - fun testExampleAbstractClassPrimitiveFields01() { - captureOutput("ExampleAbstractClassPrimitiveFields01") { - example.exampleAbstractClassPrimitiveFields01.main() - }.verifyOutputLines( - "interface SimpleTypes {", - " aString: string;", - " anInt: number;", - " aDouble: number;", - " bool: boolean;", - " privateMember: string;", - "}" - ) - } -} diff --git a/docs/knit/test/ValueClassesTest.kt b/docs/knit/test/ValueClassesTest.kt deleted file mode 100644 index b5d5604e..00000000 --- a/docs/knit/test/ValueClassesTest.kt +++ /dev/null @@ -1,42 +0,0 @@ -// This file was automatically generated from value-classes.md by Knit tool. Do not edit. -package example.test - -import org.junit.jupiter.api.Test -import kotlinx.knit.test.* - -class ValueClassesTest { - @Test - fun testExampleValueClasses01() { - captureOutput("ExampleValueClasses01") { - example.exampleValueClasses01.main() - }.verifyOutputLines( - "type AuthToken = string;" - ) - } - - @Test - fun testExampleValueClasses02() { - captureOutput("ExampleValueClasses02") { - example.exampleValueClasses02.main() - }.verifyOutputLines( - "type UByte = number;", - "", - "type UShort = number;", - "", - "type UInt = number;", - "", - "type ULong = number;" - ) - } - - @Test - fun testExampleValueClasses03() { - captureOutput("ExampleValueClasses03") { - example.exampleValueClasses03.main() - }.verifyOutputLines( - "type UInt = number;", - "", - "type UserCount = UInt;" - ) - } -} diff --git a/docs/lists.md b/docs/lists.md index 1a4dd69d..b7992737 100644 --- a/docs/lists.md +++ b/docs/lists.md @@ -1,29 +1,45 @@ -### Primitive lists +**Table of contents** + + + +* [Introduction](#introduction) + * [Primitive lists](#primitive-lists) + + + +## Introduction + +### Primitive lists + ```kotlin @Serializable -data class CalendarEvent( - val attendeeNames: List +data class MyLists( + val strings: List, + val ints: List, + val longs: List, ) fun main() { val tsGenerator = KxsTsGenerator() - println(tsGenerator.generate(CalendarEvent.serializer().descriptor)) + println(tsGenerator.generate(MyLists.serializer())) } ``` -> You can get the full code [here](./knit/example/example-list-primitive-01.kt). +> You can get the full code [here](./code/example/example-list-primitive-01.kt). ```typescript -interface CalendarEvent { - attendeeNames: string[] +export interface MyLists { + strings: string[]; + ints: number[]; + longs: number[]; } ``` diff --git a/docs/maps.md b/docs/maps.md index f01c85ee..d77a3534 100644 --- a/docs/maps.md +++ b/docs/maps.md @@ -1,30 +1,304 @@ -### Primitive lists +**Table of contents** + + + +* [Introduction](#introduction) + * [Primitive maps](#primitive-maps) + * [Enum keys](#enum-keys) + * [Nullable keys and values](#nullable-keys-and-values) + * [Maps with complex keys](#maps-with-complex-keys) + * [ES6 Map](#es6-map) + * [Maps with complex keys - Map Key class](#maps-with-complex-keys---map-key-class) + * [Maps with complex keys - custom serializer workaround](#maps-with-complex-keys---custom-serializer-workaround) + + + +## Introduction + +### Primitive maps + ```kotlin @Serializable -class Config { - val properties: Map = mapOf() -} +data class Config( + val properties: Map +) fun main() { val tsGenerator = KxsTsGenerator() - println(tsGenerator.generate(Config.serializer().descriptor)) + println(tsGenerator.generate(Config.serializer())) } ``` -> You can get the full code [here](./knit/example/example-map-primitive-01.kt). +> You can get the full code [here](./code/example/example-map-primitive-01.kt). ```typescript -interface Config { +export interface Config { properties: { [key: string]: string }; } ``` + +### Enum keys + +```kotlin +@Serializable +class Application( + val settings: Map +) + +@Serializable +enum class SettingKeys { + SCREEN_SIZE, + MAX_MEMORY, +} + +fun main() { + val tsGenerator = KxsTsGenerator() + println(tsGenerator.generate(Application.serializer())) +} +``` + +> You can get the full code [here](./code/example/example-map-primitive-02.kt). + +```typescript +export interface Application { + settings: { [key in SettingKeys]: string }; +} + +export enum SettingKeys { + SCREEN_SIZE = "SCREEN_SIZE", + MAX_MEMORY = "MAX_MEMORY", +} +``` + + + +### Nullable keys and values + +```kotlin +@Serializable +data class Config( + val properties: Map +) + +fun main() { + val tsGenerator = KxsTsGenerator() + println(tsGenerator.generate(Config.serializer())) +} +``` + +> You can get the full code [here](./code/example/example-map-primitive-03.kt). + +```typescript +export interface Config { + properties: { [key: string | null]: string | null }; +} +``` + + + +### Maps with complex keys + +JSON maps **must** have keys that are either strings, positive integers, or enums. + +See [the Kotlinx Serialization docs](https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/json.md#allowing-structured-map-keys) +. + +As a workaround, maps with structured keys are generated as [ES6 maps](#es6-map). + +To produce correct JSON, +either [write a custom serializer](#maps-with-complex-keys---custom-serializer-workaround) +or [use an explicit map-key class](#maps-with-complex-keys---map-key-class). + +#### ES6 Map + +This is the default behaviour of KxsTsGen when it encounters complex map keys. + +KxsTsGen produces valid TypeScript, but the TypeScript might not produce correct JSON. + +```kotlin +@Serializable +data class Colour( + val r: UByte, + val g: UByte, + val b: UByte, + val a: UByte, +) + +@Serializable +data class CanvasProperties( + val colourNames: Map +) + +fun main() { + val tsGenerator = KxsTsGenerator() + println(tsGenerator.generate(CanvasProperties.serializer())) +} +``` + +> You can get the full code [here](./code/example/example-map-complex-01.kt). + +```typescript +export interface CanvasProperties { + colourNames: Map; +} + +export interface Colour { + r: UByte; + g: UByte; + b: UByte; + a: UByte; +} + +export type UByte = number; +``` + + + +#### Maps with complex keys - Map Key class + +This approach is less optimised, but more declarative and easier to understand than writing custom +serializers. + +Because the value class `ColourMapKey` has a single string value, the descriptor is a +`PrimitiveKind.STRING`. + +KxsTsGen will generate a JSON-safe mapped-type property. + +```kotlin +@Serializable +data class Colour( + val r: UByte, + val g: UByte, + val b: UByte, + val a: UByte, +) + +/** + * Encode a [Colour] as an 8-character string + * + * Red, green, blue, and alpha are encoded as base-16 strings. + */ +@Serializable +@JvmInline +value class ColourMapKey(private val rgba: String) { + constructor(colour: Colour) : this( + listOf( + colour.r, + colour.g, + colour.b, + colour.a, + ).joinToString("") { + it.toString(16).padStart(2, '0') + } + ) + + fun toColour(): Colour { + val (r, g, b, a) = rgba.chunked(2).map { it.toUByte(16) } + return Colour(r, g, b, a) + } +} + +@Serializable +data class CanvasProperties( + val colourNames: Map +) + +fun main() { + val tsGenerator = KxsTsGenerator() + println(tsGenerator.generate(CanvasProperties.serializer())) +} +``` + +> You can get the full code [here](./code/example/example-map-complex-02.kt). + +```typescript +export interface CanvasProperties { + colourNames: Map; +} + +export type ColourMapKey = string; +``` + + + +#### Maps with complex keys - custom serializer workaround + +Define a custom serializer for `Colour` that will encode and decode to/from a string. + +When encoding or decoding values with Kotlinx Serialization, under the hood it will create suitable +map keys. + +Because the custom serializer is a `PrimitiveKind.STRING`, KxsTsGen will generate a JSON-safe +mapped-type property. + +```kotlin +import kotlinx.serialization.descriptors.* +import kotlinx.serialization.encoding.* + +@Serializable(with = ColourAsStringSerializer::class) +data class Colour( + val r: UByte, + val g: UByte, + val b: UByte, + val a: UByte, +) + +/** + * Encode a [Colour] as an 8-character string + * + * Red, green, blue, and alpha are encoded as base-16 strings. + */ +object ColourAsStringSerializer : KSerializer { + override val descriptor: SerialDescriptor = + PrimitiveSerialDescriptor("Colour", PrimitiveKind.STRING) + + override fun serialize(encoder: Encoder, value: Colour) { + encoder.encodeString( + listOf( + value.r, + value.g, + value.b, + value.a, + ).joinToString("") { + it.toString(16).padStart(2, '0') + } + ) + } + + override fun deserialize(decoder: Decoder): Colour { + val string = decoder.decodeString() + val (r, g, b, a) = string.chunked(2).map { it.toUByte(16) } + return Colour(r, g, b, a) + } +} + +@Serializable +data class CanvasProperties( + val colourNames: Map +) + +fun main() { + val tsGenerator = KxsTsGenerator() + println(tsGenerator.generate(CanvasProperties.serializer())) +} +``` + +> You can get the full code [here](./code/example/example-map-complex-03.kt). + +```typescript +export interface CanvasProperties { + colourNames: { [key: string]: string }; +} +``` + + diff --git a/docs/polymorphism.md b/docs/polymorphism.md index 39ac4dad..d39828db 100644 --- a/docs/polymorphism.md +++ b/docs/polymorphism.md @@ -1,12 +1,145 @@ -### Sealed classes +**Table of contents** + + + +* [Introduction](#introduction) + * [Abstract class with primitive fields](#abstract-class-with-primitive-fields) +* [Closed Polymorphism](#closed-polymorphism) + * [Static types](#static-types) + * [Sealed classes](#sealed-classes) + * [Nested sealed classes](#nested-sealed-classes) + * [Objects](#objects) +* [Open Polymorphism](#open-polymorphism) + * [Generics](#generics) + + + +## Introduction + +### Abstract class with primitive fields + +```kotlin +@Serializable +abstract class SimpleTypes( + val aString: String, + var anInt: Int, + val aDouble: Double, + val bool: Boolean, + private val privateMember: String, +) + +fun main() { + val tsGenerator = KxsTsGenerator() + println(tsGenerator.generate(SimpleTypes.serializer())) +} +``` + +> You can get the full code [here](./code/example/example-polymorphic-abstract-class-primitive-fields-01.kt). + +```typescript +export type SimpleTypes = any; +// export interface SimpleTypes { +// aString: string; +// anInt: number; +// aDouble: number; +// bool: boolean; +// privateMember: string; +// } +``` + + + +## Closed Polymorphism + +https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/polymorphism.md#closed-polymorphism + +### Static types + +```kotlin +@Serializable +open class Project(val name: String) + +class OwnedProject(name: String, val owner: String) : Project(name) + +fun main() { + val tsGenerator = KxsTsGenerator() + println(tsGenerator.generate(Project.serializer())) +} +``` + +> You can get the full code [here](./code/example/example-polymorphic-static-types-01.kt). + +Only the Project class properties are generated. + +```typescript +export interface Project { + name: string; +} +``` + + + +```kotlin +import kotlinx.serialization.modules.* + +@Serializable +abstract class Project { + abstract val name: String +} + +@Serializable +class OwnedProject(override val name: String, val owner: String) : Project() + +val module = SerializersModule { + polymorphic(Project::class) { + subclass(OwnedProject::class) + } +} + +fun main() { + val config = KxsTsConfig(serializersModule = module) + + val tsGenerator = KxsTsGenerator(config) + + println(tsGenerator.generate(Project.serializer())) +} +``` + +> You can get the full code [here](./code/example/example-polymorphic-static-types-02.kt). + +```typescript +export type Project = any; +// export interface Project { +// name: string; +// } +// +// export interface OwnedProject extends Project { +// name: string; +// owner: string; +// } +``` + + + +### Sealed classes + +Sealed classes are the best way to generate TypeScript interface so far, because all subclasses are +defined in the `SerialDescriptor`. + +A sealed class will be converted as a +[union enum, with enum member types](https://www.typescriptlang.org/docs/handbook/enums.html#union-enums-and-enum-member-types) +. + +This has many benefits that closely match how sealed classes work in Kotlin. + ```kotlin @Serializable sealed class Project { @@ -14,60 +147,232 @@ sealed class Project { } @Serializable -data class OwnedProject( - override val name: String, - val owner: String, -) : Project() +@SerialName("OProj") +class OwnedProject(override val name: String, val owner: String) : Project() + +@Serializable +class DeprecatedProject(override val name: String, val reason: String) : Project() + +fun main() { + val tsGenerator = KxsTsGenerator() + println(tsGenerator.generate(Project.serializer())) +} +``` + +> You can get the full code [here](./code/example/example-polymorphic-sealed-class-01.kt). + +```typescript +export type Project = Project.DeprecatedProject | Project.OProj; + +export namespace Project { + export enum Type { + OProj = "OProj", + DeprecatedProject = "DeprecatedProject", + } + + export interface OProj { + type: Project.Type.OProj; + name: string; + owner: string; + } + + export interface DeprecatedProject { + type: Project.Type.DeprecatedProject; + name: string; + reason: string; + } +} +``` + + + +### Nested sealed classes + +Nested sealed classes are 'invisible' to Kotlinx Serialization. In this +example, `sealed class Retriever` is ignored. + +For now, it's recommended to avoid nested sealed classes. + +```kotlin +@Serializable +sealed class Dog { + abstract val name: String + + @Serializable + class Mutt(override val name: String, val loveable: Boolean = true) : Dog() + + @Serializable + sealed class Retriever : Dog() { + abstract val colour: String + + @Serializable + data class Golden( + override val name: String, + override val colour: String, + val cute: Boolean = true, + ) : Retriever() + + @Serializable + data class NovaScotia( + override val name: String, + override val colour: String, + val adorable: Boolean = true, + ) : Retriever() + } +} + +fun main() { + val tsGenerator = KxsTsGenerator() + println(tsGenerator.generate(Dog.serializer())) +} +``` + +> You can get the full code [here](./code/example/example-polymorphic-sealed-class-02.kt). + +```typescript +export type Dog = Dog.Golden | Dog.Mutt | Dog.NovaScotia; + +export namespace Dog { + export enum Type { + Mutt = "Mutt", + Golden = "Golden", + NovaScotia = "NovaScotia", + } + + export interface Mutt { + type: Dog.Type.Mutt; + name: string; + loveable?: boolean; + } + + export interface Golden { + type: Dog.Type.Golden; + name: string; + colour: string; + cute?: boolean; + } + + export interface NovaScotia { + type: Dog.Type.NovaScotia; + name: string; + colour: string; + adorable?: boolean; + } +} +// Nested sealed classes don't work at the moment :( +// export type Dog = Dog.Mutt | Dog.Retriever +// +// export namespace Dog { +// export enum Type { +// Mutt = "Mutt", +// } +// +// export interface Mutt { +// type: Type.Mutt; +// name: string; +// loveable?: boolean; +// } +// +// export type Retriever = Retriever.Golden | Retriever.NovaScotia +// +// export namespace Retriever { +// export enum Type { +// Golden = "Golden", +// NovaScotia = "NovaScotia", +// } +// +// export interface Golden { +// type: Type.Golden; +// name: string; +// cute?: boolean; +// } +// +// export interface NovaScotia { +// type: Type.NovaScotia; +// name: string; +// adorable?: boolean; +// } +// } +// } +``` + + + +### Objects + +```kotlin +@Serializable +sealed class Response + +@Serializable +object EmptyResponse : Response() + +@Serializable +class TextResponse(val text: String) : Response() fun main() { val tsGenerator = KxsTsGenerator() println( - tsGenerator.generate( - Project.serializer().descriptor, - OwnedProject.serializer().descriptor, - ) + tsGenerator.generate(Response.serializer()) ) } ``` -> You can get the full code [here](./knit/example/example-polymorphism-sealed-01.kt). +> You can get the full code [here](./code/example/example-polymorphic-objects-01.kt). ```typescript -interface Color { - rgb: number; +export type Response = Response.EmptyResponse | Response.TextResponse; + +export namespace Response { + export enum Type { + EmptyResponse = "EmptyResponse", + TextResponse = "TextResponse", + } + + export interface EmptyResponse { + type: Response.Type.EmptyResponse; + } + + export interface TextResponse { + type: Response.Type.TextResponse; + text: string; + } } ``` -### Abstract class with primitive fields +## Open Polymorphism + +### Generics + + ```kotlin @Serializable -abstract class SimpleTypes( - val aString: String, - var anInt: Int, - val aDouble: Double, - val bool: Boolean, - private val privateMember: String, +class Box( + val value: T, ) fun main() { val tsGenerator = KxsTsGenerator() - println(tsGenerator.generate(SimpleTypes.serializer().descriptor)) + + println( + tsGenerator.generate( + Box.serializer(Double.serializer()), + ) + ) } ``` -> You can get the full code [here](./knit/example/example-abstract-class-primitive-fields-01.kt). +> You can get the full code [here](./code/example/example-generics-01.kt). ```typescript -interface SimpleTypes { - aString: string; - anInt: number; - aDouble: number; - bool: boolean; - privateMember: string; +export type Double = number & { __kotlin_Double__: void } + +export interface Box { + value: Double } ``` - - diff --git a/docs/value-classes.md b/docs/value-classes.md index f59a9071..37ab8898 100644 --- a/docs/value-classes.md +++ b/docs/value-classes.md @@ -1,10 +1,26 @@ + +**Table of contents** + + + +* [Introduction](#introduction) + * [Inline value classes](#inline-value-classes) + * [Brand typing](#brand-typing) + * [Nested value classes](#nested-value-classes) + + + + +## Introduction + + ### Inline value classes Value classes are transformed to type aliases. The type of the value class is used. @@ -16,14 +32,14 @@ value class AuthToken(private val token: String) fun main() { val tsGenerator = KxsTsGenerator() - println(tsGenerator.generate(AuthToken.serializer().descriptor)) + println(tsGenerator.generate(AuthToken.serializer())) } ``` -> You can get the full code [here](./knit/example/example-value-classes-01.kt). +> You can get the full code [here](./code/example/example-value-classes-01.kt). ```typescript -type AuthToken = string; +export type AuthToken = string; ``` @@ -39,10 +55,10 @@ fun main() { val tsGenerator = KxsTsGenerator() println( tsGenerator.generate( - UByte.serializer().descriptor, - UShort.serializer().descriptor, - UInt.serializer().descriptor, - ULong.serializer().descriptor, + UByte.serializer(), + UShort.serializer(), + UInt.serializer(), + ULong.serializer(), ) ) } @@ -50,23 +66,59 @@ fun main() { -> You can get the full code [here](./knit/example/example-value-classes-02.kt). +> You can get the full code [here](./code/example/example-value-classes-02.kt). ```typescript -type UByte = number; +export type UByte = number; -type UShort = number; +export type UShort = number; -type UInt = number; +export type UInt = number; -type ULong = number; +export type ULong = number; ``` -(At present this is not very useful as Typescript will make no distinction between any of these -numbers, even though they are distinct in Kotlin. 'Brand typing' might be introduced in the future.) + + + +### Brand typing + +To make value classes a little more strict, we can use brand typing + + +```kotlin +import kotlinx.serialization.builtins.serializer +import dev.adamko.kxstsgen.KxsTsConfig.TypeAliasTypingConfig.BrandTyping +``` + + + +```kotlin + +fun main() { + + val tsConfig = KxsTsConfig(typeAliasTyping = BrandTyping) + + val tsGenerator = KxsTsGenerator(config = tsConfig) + println( + tsGenerator.generate( + ULong.serializer(), + ) + ) +} +``` + + + +> You can get the full code [here](./code/example/example-value-classes-03.kt). + +```typescript +export type ULong = number & { __ULong__: void }; +``` + ### Nested value classes If the value class contains another value class, then the outer class will be aliased to other value @@ -79,16 +131,16 @@ value class UserCount(private val count: UInt) fun main() { val tsGenerator = KxsTsGenerator() - println(tsGenerator.generate(UserCount.serializer().descriptor)) + println(tsGenerator.generate(UserCount.serializer())) } ``` -> You can get the full code [here](./knit/example/example-value-classes-03.kt). +> You can get the full code [here](./code/example/example-value-classes-04.kt). ```typescript -type UInt = number; +export type UserCount = UInt; -type UserCount = UInt; +export type UInt = number; ``` diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 7454180f..41d9927a 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index b1159fc5..92f06b50 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index c53aefaa..1b6c7873 100644 --- a/gradlew +++ b/gradlew @@ -1,7 +1,7 @@ #!/bin/sh # -# Copyright 2015-2021 the original authors. +# Copyright © 2015-2021 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -32,10 +32,10 @@ # Busybox and similar reduced shells will NOT work, because this script # requires all of these POSIX shell features: # * functions; -# * expansions $var, ${var}, ${var:-default}, ${var+SET}, -# ${var#prefix}, ${var%suffix}, and $( cmd ); -# * compound commands having a testable exit status, especially case; -# * various built-in commands including command, set, and ulimit. +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». # # Important for patching: # diff --git a/jitpack.yml b/jitpack.yml new file mode 100644 index 00000000..a5396c12 --- /dev/null +++ b/jitpack.yml @@ -0,0 +1,5 @@ +# https://jitpack.io/docs/BUILDING/ + +# https://jitpack.io/docs/BUILDING/#java-version +jdk: + - openjdk11 diff --git a/modules/kxs-ts-gen-core/build.gradle.kts b/modules/kxs-ts-gen-core/build.gradle.kts index c5553719..d74a7f5d 100644 --- a/modules/kxs-ts-gen-core/build.gradle.kts +++ b/modules/kxs-ts-gen-core/build.gradle.kts @@ -1,39 +1,49 @@ +import buildsrc.config.publicationsFromMainHost + + plugins { - kotlin("multiplatform") - `java-library` + buildsrc.convention.`kotlin-multiplatform` + buildsrc.convention.`maven-publish` kotlin("plugin.serialization") +// id("org.jetbrains.reflekt") } val kotlinxSerializationVersion = "1.3.2" kotlin { - val hostOs = System.getProperty("os.name") - val isMingwX64 = hostOs.startsWith("Windows") - val nativeTarget = when { - hostOs == "Mac OS X" -> macosX64("native") - hostOs == "Linux" -> linuxX64("native") - isMingwX64 -> mingwX64("native") - else -> throw GradleException("Host OS is not supported in Kotlin/Native.") - } +// js(IR) { +// binaries.executable() +// browser { +// commonWebpackConfig { +// cssSupport.enabled = true +// } +// } +// } - js(IR) { - binaries.executable() - browser { - commonWebpackConfig { - cssSupport.enabled = true - } - } - } jvm { compilations.all { - kotlinOptions.jvmTarget = "11" + kotlinOptions { + jvmTarget = "11" + } } withJava() testRuns["test"].executionTask.configure { useJUnitPlatform() } } + +// publishing { +// publications { +// matching { it.name in publicationsFromMainHost() }.all { +// val targetPublication = this@all +// tasks.withType() +// .matching { it.publication == targetPublication } +// .configureEach { onlyIf { findProperty("isMainHost") == "true" } } +// } +// } +// } + sourceSets { all { @@ -54,6 +64,7 @@ kotlin { ) implementation("org.jetbrains.kotlinx:kotlinx-serialization-core") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json") + implementation(kotlin("reflect")) } } val commonTest by getting { @@ -61,11 +72,15 @@ kotlin { implementation(kotlin("test")) } } - val nativeMain by getting - val nativeTest by getting - val jsMain by getting - val jsTest by getting - val jvmMain by getting - val jvmTest by getting +// val nativeMain by getting +// val nativeTest by getting +// val jsMain by getting +// val jsTest by getting + val jvmMain by getting { + dependencies { + implementation(kotlin("reflect")) + } + } +// val jvmTest by getting } } diff --git a/modules/kxs-ts-gen-core/src/commonMain/kotlin/dev.adamko.kxstsgen/KxsTsConfig.kt b/modules/kxs-ts-gen-core/src/commonMain/kotlin/dev.adamko.kxstsgen/KxsTsConfig.kt index ad3c7d64..3cd9af13 100644 --- a/modules/kxs-ts-gen-core/src/commonMain/kotlin/dev.adamko.kxstsgen/KxsTsConfig.kt +++ b/modules/kxs-ts-gen-core/src/commonMain/kotlin/dev.adamko.kxstsgen/KxsTsConfig.kt @@ -1,20 +1,93 @@ package dev.adamko.kxstsgen +import dev.adamko.kxstsgen.util.MutableMapWithDefaultPut import kotlin.jvm.JvmInline +import kotlin.reflect.KClass +import kotlinx.serialization.DeserializationStrategy +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.KSerializer +import kotlinx.serialization.SerializationStrategy import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.modules.EmptySerializersModule +import kotlinx.serialization.modules.SerializersModule +import kotlinx.serialization.modules.SerializersModuleCollector + +/** + * @param[indent] Define the indentation that is used when generating source code + * @param[declarationSeparator] The string that is used when joining [TsDeclaration]s + * @param[namespaceConfig] (UNIMPLEMENTED) How elements are grouped into [TsDeclaration.TsNamespace]s. + * @param[typeAliasTyping] (UNIMPLEMENTED) Control if type aliases are simple, or 'branded'. + * @param[serializersModule] Used to obtain contextual and polymorphic information. + */ data class KxsTsConfig( val indent: String = " ", - val namespaceConfig: NamespaceConfig = NamespaceConfig.None, + val declarationSeparator: String = "\n\n", + @UnimplementedKxTsGenApi + val namespaceConfig: NamespaceConfig = NamespaceConfig.Disabled, + @UnimplementedKxTsGenApi + val typeAliasTyping: TypeAliasTypingConfig = TypeAliasTypingConfig.None, + val serializersModule: SerializersModule = EmptySerializersModule, ) { sealed interface NamespaceConfig { /** Use the prefix of the [SerialDescriptor] */ - object UseDescriptorNamePrefix : NamespaceConfig + object DescriptorNamePrefix : NamespaceConfig /** don't generate a namespace */ - object None : NamespaceConfig + object Disabled : NamespaceConfig @JvmInline - value class Hardcoded(val namespace: String) : NamespaceConfig + value class Static(val namespace: String) : NamespaceConfig + } + + sealed interface TypeAliasTypingConfig { + object None : TypeAliasTypingConfig + object BrandTyping : TypeAliasTypingConfig + } + + private val contextualClasses: MutableSet> = mutableSetOf() + + private val _polymorphicDescriptors + by MutableMapWithDefaultPut, MutableSet> { mutableSetOf() } + + val polymorphicDescriptors: Map, Set> + get() = _polymorphicDescriptors.mapValues { it.value.toSet() }.toMap().withDefault { setOf() } + + init { + serializersModule.dumpTo(Collector()) } + + /** Collects the contents of a [SerializersModule], so kxs-ts-gen can view registered classes. */ + private inner class Collector : SerializersModuleCollector { + + override fun contextual( + kClass: KClass, + provider: (typeArgumentsSerializers: List>) -> KSerializer<*> + ) { + contextualClasses + kClass + } + + override fun polymorphic( + baseClass: KClass, + actualClass: KClass, + actualSerializer: KSerializer, + ) { + _polymorphicDescriptors.getValue(baseClass).add(actualSerializer.descriptor) + } + + @ExperimentalSerializationApi + override fun polymorphicDefaultDeserializer( + baseClass: KClass, + defaultDeserializerProvider: (className: String?) -> DeserializationStrategy? + ) { + } + + @ExperimentalSerializationApi + override fun polymorphicDefaultSerializer( + baseClass: KClass, + defaultSerializerProvider: (value: Base) -> SerializationStrategy? + ) { + } + + } } diff --git a/modules/kxs-ts-gen-core/src/commonMain/kotlin/dev.adamko.kxstsgen/KxsTsGenerator.kt b/modules/kxs-ts-gen-core/src/commonMain/kotlin/dev.adamko.kxstsgen/KxsTsGenerator.kt index 452b1654..8b332c29 100644 --- a/modules/kxs-ts-gen-core/src/commonMain/kotlin/dev.adamko.kxstsgen/KxsTsGenerator.kt +++ b/modules/kxs-ts-gen-core/src/commonMain/kotlin/dev.adamko.kxstsgen/KxsTsGenerator.kt @@ -1,183 +1,47 @@ package dev.adamko.kxstsgen -import kotlinx.serialization.descriptors.PolymorphicKind -import kotlinx.serialization.descriptors.PrimitiveKind -import kotlinx.serialization.descriptors.SerialDescriptor -import kotlinx.serialization.descriptors.SerialKind -import kotlinx.serialization.descriptors.StructureKind -import kotlinx.serialization.descriptors.elementDescriptors -import kotlinx.serialization.descriptors.elementNames -import kotlinx.serialization.modules.EmptySerializersModule -import kotlinx.serialization.modules.SerializersModule - -class KxsTsGenerator( - private val config: KxsTsConfig = KxsTsConfig(), - private val serializersModule: SerializersModule = EmptySerializersModule, -) { - - private val codeGenerator: KxsTsSourceCodeGenerator = KxsTsSourceCodeGenerator(config) - - - fun generate(vararg descriptors: SerialDescriptor): String { -// val allDescriptors = getAllDescriptors(descriptors, setOf()) +import kotlinx.serialization.KSerializer -// serializersModule.getContextualDescriptor() -// serializersModule.getPolymorphicDescriptors() +open class KxsTsGenerator( + val config: KxsTsConfig = KxsTsConfig(), - val elements = descriptors - .fold(mapOf()) { acc, descriptor -> - TsConverter(descriptor, acc).result - } - .values - .toSet() + val descriptorsExtractor: SerializerDescriptorsExtractor = SerializerDescriptorsExtractor.Default, - return codeGenerator.joinElementsToString(elements) - } + val elementIdConverter: TsElementIdConverter = TsElementIdConverter.Default, + val mapTypeConverter: TsMapTypeConverter = TsMapTypeConverter.Default, -// private fun convertSealedPolymorphic( -// descriptor: SerialDescriptor, -// discriminator: TsStructure.Enum, -// ): TsElement { -// } + val typeRefConverter: TsTypeRefConverter = + TsTypeRefConverter.Default(elementIdConverter, mapTypeConverter), -} + val elementConverter: TsElementConverter = + TsElementConverter.Default( + elementIdConverter, + mapTypeConverter, + typeRefConverter, + ), -class TsConverter( - target: SerialDescriptor, - existingElements: Map, - private val serializersModule: SerializersModule = EmptySerializersModule, + val sourceCodeGenerator: KxsTsSourceCodeGenerator = KxsTsSourceCodeGenerator.Default(config) ) { - private val existingElements = existingElements.toMutableMap() - val result: Map - get() = existingElements - - private val _dependencies = mutableMapOf>() - private val dependencies: Map> - get() = _dependencies - - init { - convertToTsElement(null, target) - } - - private fun convertToTsElement(requestor: TsElementId?, target: SerialDescriptor): TsElementId { - - val targetId = TsElementId(target.serialName.removeSuffix("?")) - - return existingElements.getOrPut(targetId) { - - val result: TsElement = when (target.kind) { - - PrimitiveKind.BOOLEAN -> TsPrimitive.TsBoolean - - PrimitiveKind.BYTE, - PrimitiveKind.SHORT, - PrimitiveKind.INT, - PrimitiveKind.LONG, - PrimitiveKind.FLOAT, - PrimitiveKind.DOUBLE -> TsPrimitive.TsNumber - - PrimitiveKind.CHAR, - PrimitiveKind.STRING -> TsPrimitive.TsString - - StructureKind.LIST -> convertList(targetId, target) - StructureKind.MAP -> convertMap(targetId, target) + fun generate(vararg serializers: KSerializer<*>): String { - StructureKind.CLASS, - StructureKind.OBJECT -> convertStructure(targetId, target) + val descriptors = serializers.flatMap { descriptorsExtractor(it) }.toSet() - SerialKind.ENUM -> convertEnum(targetId, target) + val elements = descriptors.map { elementConverter(it) } - PolymorphicKind.SEALED -> { - // TODO PolymorphicKind.SEALED - convertStructure(targetId, target) - } - PolymorphicKind.OPEN -> { - // TODO PolymorphicKind.SEALED - val openTargetId = TsElementId( - targetId.namespace + "." + targetId.name.substringAfter("<").substringBeforeLast(">") - ) - convertStructure(openTargetId, target) - } - SerialKind.CONTEXTUAL -> { - // TODO SerialKind.CONTEXTUAL - TsPrimitive.TsUnknown - } + return elements + .groupBy { element -> sourceCodeGenerator.groupElementsBy(element) } + .mapValues { (_, elements) -> + elements + .filterIsInstance() + .map { element -> sourceCodeGenerator.generateDeclaration(element) } + .filter { it.isNotBlank() } + .joinToString(config.declarationSeparator) } - - if (requestor != null) { - dependencies.getOrElse(requestor) { mutableSetOf() }.plus(result) - } - - result - }.id - } - - - private fun convertStructure( - targetId: TsElementId, - structDescriptor: SerialDescriptor, - ): TsElement { - - if (structDescriptor.isInline) { - val fieldDescriptor = structDescriptor.elementDescriptors.first() - val typeId = convertToTsElement(targetId, fieldDescriptor) - val typeReference = TsTypeReference(typeId, fieldDescriptor.isNullable) - return TsTypeAlias(targetId, setOf(typeReference)) - } else { - - val properties = structDescriptor.elementDescriptors.mapIndexed { index, field -> - val name = structDescriptor.getElementName(index) - val fieldTypeId = convertToTsElement(targetId, field) - val fieldTypeReference = TsTypeReference(fieldTypeId, field.isNullable) - when { - structDescriptor.isElementOptional(index) -> TsProperty.Optional(name, fieldTypeReference) - else -> TsProperty.Required(name, fieldTypeReference) - } - } - - return TsStructure.TsInterface(targetId, properties) - } + .values// TODO create namespaces + .joinToString(config.declarationSeparator) } - private fun convertEnum( - targetId: TsElementId, - enumDescriptor: SerialDescriptor, - ): TsElement { -// if (descriptor.elementsCount > 0) { -// convertStructure(descriptor) -// } - - return TsStructure.TsEnum( - targetId, - enumDescriptor.elementNames.toSet(), - ) - } - - private fun convertList( - targetId: TsElementId, - listDescriptor: SerialDescriptor, - ): TsStructure.TsList { - - val typeDescriptor = listDescriptor.elementDescriptors.first() - val typeId = - TsTypeReference(convertToTsElement(targetId, typeDescriptor), typeDescriptor.isNullable) - - return TsStructure.TsList(targetId, typeId) - } - - private fun convertMap( - targetId: TsElementId, - mapDescriptor: SerialDescriptor, - ): TsStructure.TsMap { - - val (keyDescriptor, valueDescriptor) = mapDescriptor.elementDescriptors.toList() - val keyType = - TsTypeReference(convertToTsElement(targetId, keyDescriptor), keyDescriptor.isNullable) - val valueType = - TsTypeReference(convertToTsElement(targetId, valueDescriptor), valueDescriptor.isNullable) - return TsStructure.TsMap(targetId, keyType, valueType) - } } diff --git a/modules/kxs-ts-gen-core/src/commonMain/kotlin/dev.adamko.kxstsgen/KxsTsSourceCodeGenerator.kt b/modules/kxs-ts-gen-core/src/commonMain/kotlin/dev.adamko.kxstsgen/KxsTsSourceCodeGenerator.kt index c387f014..81f03478 100644 --- a/modules/kxs-ts-gen-core/src/commonMain/kotlin/dev.adamko.kxstsgen/KxsTsSourceCodeGenerator.kt +++ b/modules/kxs-ts-gen-core/src/commonMain/kotlin/dev.adamko.kxstsgen/KxsTsSourceCodeGenerator.kt @@ -1,125 +1,317 @@ package dev.adamko.kxstsgen -class KxsTsSourceCodeGenerator( - private val config: KxsTsConfig +/** + * Writes [TsElement]s as TypeScript source code. + */ +abstract class KxsTsSourceCodeGenerator( + val config: KxsTsConfig = KxsTsConfig(), ) { - private val indent by config::indent + abstract fun groupElementsBy(element: TsElement): String? - fun joinElementsToString(elements: Set): String { + open fun generateDeclaration(element: TsDeclaration): String { + return when (element) { + is TsDeclaration.TsEnum -> generateEnum(element) + is TsDeclaration.TsInterface -> generateInterface(element) + is TsDeclaration.TsNamespace -> generateNamespace(element) + is TsDeclaration.TsTypeAlias -> generateType(element) + } + } + + abstract fun generateEnum(enum: TsDeclaration.TsEnum): String + abstract fun generateInterface(element: TsDeclaration.TsInterface): String + abstract fun generateNamespace(namespace: TsDeclaration.TsNamespace): String + abstract fun generateType(element: TsDeclaration.TsTypeAlias): String + + abstract fun generateMapTypeReference(tsMap: TsLiteral.TsMap): String + + abstract fun generatePrimitive(primitive: TsLiteral.Primitive): String + abstract fun generateTypeReference(typeRef: TsTypeRef): String + + open class Default( + config: KxsTsConfig, + ) : KxsTsSourceCodeGenerator(config) { + + + override fun groupElementsBy(element: TsElement): String { + return when (config.namespaceConfig) { + is KxsTsConfig.NamespaceConfig.Static -> config.namespaceConfig.namespace + KxsTsConfig.NamespaceConfig.Disabled -> "" + KxsTsConfig.NamespaceConfig.DescriptorNamePrefix -> + when (element) { + is TsLiteral -> "" + is TsDeclaration -> element.id.namespace + } + } + } + + + override fun generateNamespace(namespace: TsDeclaration.TsNamespace): String { + val namespaceContent = + namespace + .members + .joinToString(config.declarationSeparator) { declaration -> + generateDeclaration(declaration) + } - return elements - .groupBy { - when (config.namespaceConfig) { - is KxsTsConfig.NamespaceConfig.Hardcoded -> config.namespaceConfig.namespace - KxsTsConfig.NamespaceConfig.None -> "" - KxsTsConfig.NamespaceConfig.UseDescriptorNamePrefix -> it.id.namespace + return buildString { + appendLine("export namespace ${namespace.id.name} {") + if (namespaceContent.isNotBlank()) { + appendLine(namespaceContent.prependIndent(config.indent)) } + append("}") } - .mapValues { (_, elements) -> + } - elements.mapNotNull { element -> - when (element) { - is TsPrimitive -> null - is TsStructure.TsEnum -> generateEnum(element) - is TsStructure.TsInterface -> generateInterface(element) - is TsStructure.TsMap -> generateMap(element) - is TsStructure.TsList -> generateList(element) - is TsTypeAlias -> generateTypeAlias(element) + + override fun generateEnum(enum: TsDeclaration.TsEnum): String { + + val enumMembers = enum.members.joinToString("\n") { member -> + """ + |${config.indent}$member = "$member", + """.trimMargin() + } + + return """ + |export enum ${enum.id.name} { + |${enumMembers} + |} + """.trimMargin() + } + + override fun generateInterface(element: TsDeclaration.TsInterface): String { + + return when (element.polymorphism) { + is TsPolymorphism.Sealed -> generatePolyClosed(element, element.polymorphism) + is TsPolymorphism.Open -> generatePolyOpen(element, element.polymorphism) + null -> { + val properties = element + .properties + .joinToString(separator = "\n") { property -> + val separator = when (property) { + is TsProperty.Optional -> "?: " + is TsProperty.Required -> ": " + } + val propertyType = generateTypeReference(property.typeRef) + // generate ` name: Type;` + // or ` name:? Type;` + "${property.name}${separator}${propertyType};" + } + + buildString { + appendLine("export interface ${element.id.name} {") + if (properties.isNotBlank()) { + appendLine(properties.prependIndent(config.indent)) + } + append("}") } } - .joinToString("\n\n") + } + } + + + // note: this isn't used because at present poly-open descriptors are converted to 'any' + private fun generatePolyOpen( + element: TsDeclaration.TsInterface, + polymorphism: TsPolymorphism.Open, + ): String { + val namespaceId = element.id + val namespaceRef = TsTypeRef.Declaration(namespaceId, null, false) + + val subInterfaceRefs = polymorphism.subclasses.map { + TsTypeRef.Declaration(it.id, namespaceRef, false) + }.toSet() + + val discriminatorProperty = TsProperty.Required( + polymorphism.discriminatorName, + TsTypeRef.Literal(TsLiteral.Primitive.TsString, false), + ) + + val subInterfaces = polymorphism + .subclasses + .map { it.copy(properties = it.properties + discriminatorProperty) } + .toSet() + + val namespace = TsDeclaration.TsNamespace( + element.id, + subInterfaces, + ) + + val subInterfaceTypeUnion = TsDeclaration.TsTypeAlias( + element.id, + subInterfaceRefs + ) + + return listOf(subInterfaceTypeUnion, namespace).joinToString("\n\n") { + generateDeclaration(it) + } + } + + /** + * Generate a 'sealed class' equivalent. + * + * * type union of all subclasses + * * a namespace with contains + * * a 'Type' enum + * * subclasses, each with an additional 'Type' property that has a literal 'Type' enum value + */ + private fun generatePolyClosed( + element: TsDeclaration.TsInterface, + polymorphism: TsPolymorphism.Sealed, + ): String { + val namespaceId = element.id + val namespaceRef = TsTypeRef.Declaration(namespaceId, null, false) + + val subInterfaceRefs: Map = + polymorphism.subclasses.associateBy { subclass -> + val subclassId = TsElementId(namespaceId.toString() + "." + subclass.id.name) + TsTypeRef.Declaration(subclassId, namespaceRef, false) + } + + val discriminatorEnum = TsDeclaration.TsEnum( + TsElementId("${element.id.namespace}.${polymorphism.discriminatorName.replaceFirstChar { it.uppercaseChar() }}"), + subInterfaceRefs.keys.map { it.id.name }.toSet(), + ) - }.entries - .filter { it.value.isNotBlank() } - .joinToString("\n\n") { (namespace, namespaceContent) -> + val discriminatorEnumRef = TsTypeRef.Declaration(discriminatorEnum.id, namespaceRef, false) - if (namespace.isBlank()) { - namespaceContent - } else { + val subInterfacesWithTypeProp = subInterfaceRefs.map { (subInterfaceRef, subclass) -> + val typePropId = TsElementId( """ - |export namespace $namespace { - |${namespaceContent.prependIndent(config.indent)} - |} + |${discriminatorEnum.id.name}.${subInterfaceRef.id.name} """.trimMargin() - } + ) + + val typeProp = TsProperty.Required( + polymorphism.discriminatorName, + TsTypeRef.Declaration(typePropId, discriminatorEnumRef, false), + ) + + subclass.copy(properties = setOf(typeProp) + subclass.properties) } - } - private fun generateEnum(enum: TsStructure.TsEnum): String { + val subInterfaceTypeUnion = TsDeclaration.TsTypeAlias( + element.id, + subInterfaceRefs.keys + ) - val enumMembers = enum.members.joinToString("\n") { member -> - """ - |${indent}$member = "$member", - """.trimMargin() + val namespace = TsDeclaration.TsNamespace( + namespaceId, + buildSet { + add(discriminatorEnum) + addAll(subInterfacesWithTypeProp) + } + ) + + return listOf(subInterfaceTypeUnion, namespace).joinToString("\n\n") { + generateDeclaration(it) + } } - return """ - |enum ${enum.id.name} { - |${enumMembers} - |} - """.trimMargin() - } - private fun generateInterface(element: TsStructure.TsInterface): String { + override fun generateType(element: TsDeclaration.TsTypeAlias): String { + val aliases = + element.typeRefs + .map { generateTypeReference(it) } + .sorted() + .joinToString(" | ") + + return when (config.typeAliasTyping) { + KxsTsConfig.TypeAliasTypingConfig.None -> + """ + |export type ${element.id.name} = ${aliases}; + """.trimMargin() + KxsTsConfig.TypeAliasTypingConfig.BrandTyping -> { + + val brandType = element.id.name + .mapNotNull { c -> + when { + c == '.' -> '_' + !c.isLetter() -> null + else -> c + } + }.joinToString("") - val properties = element.properties.joinToString("\n") { property -> - val separator = when (property) { - is TsProperty.Optional -> "?: " - is TsProperty.Required -> ": " + """ + |export type ${element.id.name} = $aliases & { __${brandType}__: void }; + """.trimMargin() + } } - val propertyType = generateTypeReference(property.typeReference) - // generate ` name: Type;` - // or ` name:? Type;` - "${indent}${property.name}${separator}${propertyType};" } - return """ - |interface ${element.id.name} { - |${properties} - |} - """.trimMargin() - } - private fun generateTypeAlias(element: TsTypeAlias): String { - val aliases = generateTypeReference(element.types) - return """ - |type ${element.id.name} = ${aliases}; - """.trimMargin() - } + /** + * A type-reference, be it for the field of an interface, a type alias, or a generic type + * constraint. + */ + override fun generateTypeReference( +// rootId: TsElementId?, + typeRef: TsTypeRef, + ): String { + val plainType: String = when (typeRef) { + is TsTypeRef.Literal -> when (typeRef.element) { + is TsLiteral.Primitive -> generatePrimitive(typeRef.element) - private fun generateTypeReference(typeRefs: Collection) = - generateTypeReference(*typeRefs.toTypedArray()) + is TsLiteral.TsList -> { + val valueTypeRef = generateTypeReference(typeRef.element.valueTypeRef) + "$valueTypeRef[]" + } - /** - * A type-reference, be it for the field of an interface, a type alias, or a generic type - * constraint. - */ - private fun generateTypeReference(vararg typeRefs: TsTypeReference): String { + is TsLiteral.TsMap -> generateMapTypeReference(typeRef.element) + } - val includeNull = typeRefs.any { it.nullable } + is TsTypeRef.Declaration -> { - return typeRefs - .map { it.id.name } - .sorted() - .plus(if (includeNull) "null" else null) - .filterNotNull() - .joinToString(separator = " | ") - } + if (typeRef.parent != null) { + val parentRef = generateTypeReference(typeRef.parent) + "${parentRef}.${typeRef.id.name}" + } else { + typeRef.id.name + } + } + } - private fun generateList(element: TsStructure.TsList): String { - val elementsTypeRef = generateTypeReference(element.elementsTsType) - return """ - |${elementsTypeRef}[] - """.trimMargin() - } + return buildString { + append(plainType) + if (typeRef.nullable) { + append(" | ") + append(generatePrimitive(TsLiteral.Primitive.TsNull)) + } + } + } + + + override fun generatePrimitive(primitive: TsLiteral.Primitive): String { + return when (primitive) { + TsLiteral.Primitive.TsString -> "string" + TsLiteral.Primitive.TsNumber -> "number" + TsLiteral.Primitive.TsBoolean -> "boolean" + TsLiteral.Primitive.TsObject -> "object" + TsLiteral.Primitive.TsAny -> "any" + TsLiteral.Primitive.TsNever -> "never" + TsLiteral.Primitive.TsNull -> "null" + TsLiteral.Primitive.TsUndefined -> "undefined" + TsLiteral.Primitive.TsUnknown -> "unknown" + TsLiteral.Primitive.TsVoid -> "void" + } + } + + + override fun generateMapTypeReference( +// rootId: TsElementId?, + tsMap: TsLiteral.TsMap + ): String { + val keyTypeRef = generateTypeReference(tsMap.keyTypeRef) + val valueTypeRef = generateTypeReference(tsMap.valueTypeRef) + + return when (tsMap.type) { + TsLiteral.TsMap.Type.INDEX_SIGNATURE -> "{ [key: $keyTypeRef]: $valueTypeRef }" + TsLiteral.TsMap.Type.MAPPED_OBJECT -> "{ [key in $keyTypeRef]: $valueTypeRef }" + TsLiteral.TsMap.Type.MAP -> "Map<$keyTypeRef, $valueTypeRef>" + } + } - private fun generateMap(element: TsStructure.TsMap): String { - val keyTypeRef = generateTypeReference(element.keyTsType) - val valueTypeRef = generateTypeReference(element.keyTsType) - return """ - |{ [key: $keyTypeRef]: $valueTypeRef } - """.trimMargin() } + } diff --git a/modules/kxs-ts-gen-core/src/commonMain/kotlin/dev.adamko.kxstsgen/SerializerDescriptorsExtractor.kt b/modules/kxs-ts-gen-core/src/commonMain/kotlin/dev.adamko.kxstsgen/SerializerDescriptorsExtractor.kt new file mode 100644 index 00000000..5780fe2b --- /dev/null +++ b/modules/kxs-ts-gen-core/src/commonMain/kotlin/dev.adamko.kxstsgen/SerializerDescriptorsExtractor.kt @@ -0,0 +1,76 @@ +package dev.adamko.kxstsgen + +import dev.adamko.kxstsgen.util.MutableMapWithDefaultPut +import kotlinx.serialization.KSerializer +import kotlinx.serialization.descriptors.PolymorphicKind +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.SerialKind +import kotlinx.serialization.descriptors.StructureKind +import kotlinx.serialization.descriptors.elementDescriptors + + +/** + * Recursively extract all descriptors from a serializer and its elements. + */ +fun interface SerializerDescriptorsExtractor { + + operator fun invoke( + serializer: KSerializer<*> + ): Set + + + object Default : SerializerDescriptorsExtractor { + + override operator fun invoke( + serializer: KSerializer<*> + ): Set { + return extractDescriptors(serializer.descriptor) + } + + + private tailrec fun extractDescriptors( + current: SerialDescriptor? = null, + queue: ArrayDeque = ArrayDeque(), + extracted: Set = emptySet(), + ): Set { + return if (current == null) { + extracted + } else { + val currentDescriptors = elementDescriptors.getValue(current) + queue.addAll(currentDescriptors - extracted) + extractDescriptors(queue.removeFirstOrNull(), queue, extracted + current) + } + } + + + private val elementDescriptors by MutableMapWithDefaultPut> { descriptor -> + when (descriptor.kind) { + SerialKind.ENUM -> emptyList() + + SerialKind.CONTEXTUAL -> emptyList() + + PrimitiveKind.BOOLEAN, + PrimitiveKind.BYTE, + PrimitiveKind.CHAR, + PrimitiveKind.SHORT, + PrimitiveKind.INT, + PrimitiveKind.LONG, + PrimitiveKind.FLOAT, + PrimitiveKind.DOUBLE, + PrimitiveKind.STRING -> emptyList() + + StructureKind.CLASS, + StructureKind.LIST, + StructureKind.MAP, + StructureKind.OBJECT -> descriptor.elementDescriptors + + PolymorphicKind.SEALED, + PolymorphicKind.OPEN -> descriptor + .elementDescriptors + .filter { it.kind is PolymorphicKind } + .flatMap { it.elementDescriptors } + } + } + } +} diff --git a/modules/kxs-ts-gen-core/src/commonMain/kotlin/dev.adamko.kxstsgen/TsElementConverter.kt b/modules/kxs-ts-gen-core/src/commonMain/kotlin/dev.adamko.kxstsgen/TsElementConverter.kt new file mode 100644 index 00000000..1e15742f --- /dev/null +++ b/modules/kxs-ts-gen-core/src/commonMain/kotlin/dev.adamko.kxstsgen/TsElementConverter.kt @@ -0,0 +1,154 @@ +package dev.adamko.kxstsgen + +import kotlinx.serialization.descriptors.PolymorphicKind +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.SerialKind +import kotlinx.serialization.descriptors.StructureKind +import kotlinx.serialization.descriptors.elementDescriptors +import kotlinx.serialization.descriptors.elementNames + + +fun interface TsElementConverter { + + operator fun invoke( + descriptor: SerialDescriptor, + ): TsElement + + + open class Default( + val elementIdConverter: TsElementIdConverter, + val mapTypeConverter: TsMapTypeConverter, + val typeRefConverter: TsTypeRefConverter, + ) : TsElementConverter { + + override operator fun invoke( + descriptor: SerialDescriptor, + ): TsElement { + return when (descriptor.kind) { + SerialKind.ENUM -> convertEnum(descriptor) + + PrimitiveKind.BOOLEAN -> TsLiteral.Primitive.TsBoolean + + PrimitiveKind.CHAR, + PrimitiveKind.STRING -> TsLiteral.Primitive.TsString + + PrimitiveKind.BYTE, + PrimitiveKind.SHORT, + PrimitiveKind.INT, + PrimitiveKind.LONG, + PrimitiveKind.FLOAT, + PrimitiveKind.DOUBLE -> TsLiteral.Primitive.TsNumber + + StructureKind.LIST -> convertList(descriptor) + StructureKind.MAP -> convertMap(descriptor) + + StructureKind.CLASS, + StructureKind.OBJECT -> when { + descriptor.isInline -> convertTypeAlias(descriptor) + else -> convertInterface(descriptor, null) + } + + PolymorphicKind.SEALED -> convertPolymorphic(descriptor) + + // TODO handle contextual + // TODO handle polymorphic open + SerialKind.CONTEXTUAL, + PolymorphicKind.OPEN -> { + val resultId = elementIdConverter(descriptor) + val fieldTypeRef = TsTypeRef.Literal(TsLiteral.Primitive.TsAny, false) + TsDeclaration.TsTypeAlias(resultId, fieldTypeRef) + } + } + } + + + fun convertPolymorphic( + descriptor: SerialDescriptor, + ): TsDeclaration { + + val discriminatorIndex = descriptor.elementDescriptors + .indexOfFirst { it.kind == PrimitiveKind.STRING } + val discriminatorName = descriptor.getElementName(discriminatorIndex) + + val subclasses = descriptor + .elementDescriptors + .first { it.kind == SerialKind.CONTEXTUAL } + .elementDescriptors + + val subclassInterfaces = subclasses + .map { this(it) } + .filterIsInstance() + .map { it.copy(id = TsElementId("${descriptor.serialName}.${it.id.name}")) } + .toSet() + + val polymorphism = when (descriptor.kind) { + PolymorphicKind.SEALED -> TsPolymorphism.Sealed(discriminatorName, subclassInterfaces) + PolymorphicKind.OPEN -> TsPolymorphism.Open(discriminatorName, subclassInterfaces) + else -> error("Can't convert non-polymorphic SerialKind ${descriptor.kind} to polymorphic interface") + } + + return convertInterface(descriptor, polymorphism) + } + + + fun convertTypeAlias( + structDescriptor: SerialDescriptor, + ): TsDeclaration { + val resultId = elementIdConverter(structDescriptor) + val fieldDescriptor = structDescriptor.elementDescriptors.first() + val fieldTypeRef = typeRefConverter(fieldDescriptor) + return TsDeclaration.TsTypeAlias(resultId, fieldTypeRef) + } + + + fun convertInterface( + descriptor: SerialDescriptor, + polymorphism: TsPolymorphism?, + ): TsDeclaration { + val resultId = elementIdConverter(descriptor) + + val properties = descriptor.elementDescriptors.mapIndexed { index, fieldDescriptor -> + val name = descriptor.getElementName(index) + val fieldTypeRef = typeRefConverter(fieldDescriptor) + when { + descriptor.isElementOptional(index) -> TsProperty.Optional(name, fieldTypeRef) + else -> TsProperty.Required(name, fieldTypeRef) + } + }.toSet() + return TsDeclaration.TsInterface(resultId, properties, polymorphism) + } + + + fun convertEnum( + enumDescriptor: SerialDescriptor, + ): TsDeclaration.TsEnum { + val resultId = elementIdConverter(enumDescriptor) + return TsDeclaration.TsEnum(resultId, enumDescriptor.elementNames.toSet()) + } + + + fun convertList( + listDescriptor: SerialDescriptor, + ): TsLiteral.TsList { + val elementDescriptor = listDescriptor.elementDescriptors.first() + val elementTypeRef = typeRefConverter(elementDescriptor) + return TsLiteral.TsList(elementTypeRef) + } + + + fun convertMap( + mapDescriptor: SerialDescriptor, + ): TsLiteral.TsMap { + + val (keyDescriptor, valueDescriptor) = mapDescriptor.elementDescriptors.toList() + + val keyTypeRef = typeRefConverter(keyDescriptor) + val valueTypeRef = typeRefConverter(valueDescriptor) + + val type = mapTypeConverter(keyDescriptor) + + return TsLiteral.TsMap(keyTypeRef, valueTypeRef, type) + } + } +} diff --git a/modules/kxs-ts-gen-core/src/commonMain/kotlin/dev.adamko.kxstsgen/TsElementIdConverter.kt b/modules/kxs-ts-gen-core/src/commonMain/kotlin/dev.adamko.kxstsgen/TsElementIdConverter.kt new file mode 100644 index 00000000..27ab0206 --- /dev/null +++ b/modules/kxs-ts-gen-core/src/commonMain/kotlin/dev.adamko.kxstsgen/TsElementIdConverter.kt @@ -0,0 +1,42 @@ +package dev.adamko.kxstsgen + +import kotlinx.serialization.descriptors.PolymorphicKind +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.SerialKind +import kotlinx.serialization.descriptors.StructureKind + + +fun interface TsElementIdConverter { + + operator fun invoke(descriptor: SerialDescriptor): TsElementId + + object Default : TsElementIdConverter { + override operator fun invoke(descriptor: SerialDescriptor): TsElementId { + val targetId = TsElementId(descriptor.serialName.removeSuffix("?")) + + return when (descriptor.kind) { + PolymorphicKind.OPEN -> TsElementId( + targetId.namespace + "." + targetId.name.substringAfter("<").substringBeforeLast(">") + ) + PolymorphicKind.SEALED, + PrimitiveKind.BOOLEAN, + PrimitiveKind.BYTE, + PrimitiveKind.CHAR, + PrimitiveKind.DOUBLE, + PrimitiveKind.FLOAT, + PrimitiveKind.INT, + PrimitiveKind.LONG, + PrimitiveKind.SHORT, + PrimitiveKind.STRING, + SerialKind.CONTEXTUAL, + SerialKind.ENUM, + StructureKind.CLASS, + StructureKind.LIST, + StructureKind.MAP, + StructureKind.OBJECT -> targetId + } + } + + } +} diff --git a/modules/kxs-ts-gen-core/src/commonMain/kotlin/dev.adamko.kxstsgen/TsMapTypeConverter.kt b/modules/kxs-ts-gen-core/src/commonMain/kotlin/dev.adamko.kxstsgen/TsMapTypeConverter.kt new file mode 100644 index 00000000..3bdf82de --- /dev/null +++ b/modules/kxs-ts-gen-core/src/commonMain/kotlin/dev.adamko.kxstsgen/TsMapTypeConverter.kt @@ -0,0 +1,40 @@ +package dev.adamko.kxstsgen + +import kotlinx.serialization.descriptors.PolymorphicKind +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.SerialKind +import kotlinx.serialization.descriptors.StructureKind + + +fun interface TsMapTypeConverter { + + operator fun invoke(descriptor: SerialDescriptor): TsLiteral.TsMap.Type + + object Default : TsMapTypeConverter { + + override operator fun invoke(descriptor: SerialDescriptor): TsLiteral.TsMap.Type { + return when (descriptor.kind) { + SerialKind.ENUM -> TsLiteral.TsMap.Type.MAPPED_OBJECT + + PrimitiveKind.STRING -> TsLiteral.TsMap.Type.INDEX_SIGNATURE + + SerialKind.CONTEXTUAL, + PrimitiveKind.BOOLEAN, + PrimitiveKind.BYTE, + PrimitiveKind.CHAR, + PrimitiveKind.SHORT, + PrimitiveKind.INT, + PrimitiveKind.LONG, + PrimitiveKind.FLOAT, + PrimitiveKind.DOUBLE, + StructureKind.CLASS, + StructureKind.LIST, + StructureKind.MAP, + StructureKind.OBJECT, + PolymorphicKind.SEALED, + PolymorphicKind.OPEN -> TsLiteral.TsMap.Type.MAP + } + } + } +} diff --git a/modules/kxs-ts-gen-core/src/commonMain/kotlin/dev.adamko.kxstsgen/TsTypeRefConverter.kt b/modules/kxs-ts-gen-core/src/commonMain/kotlin/dev.adamko.kxstsgen/TsTypeRefConverter.kt new file mode 100644 index 00000000..9803e4b0 --- /dev/null +++ b/modules/kxs-ts-gen-core/src/commonMain/kotlin/dev.adamko.kxstsgen/TsTypeRefConverter.kt @@ -0,0 +1,86 @@ +package dev.adamko.kxstsgen + +import kotlinx.serialization.descriptors.PolymorphicKind +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.SerialKind +import kotlinx.serialization.descriptors.StructureKind +import kotlinx.serialization.descriptors.elementDescriptors + + +fun interface TsTypeRefConverter { + + operator fun invoke(descriptor: SerialDescriptor): TsTypeRef + + + open class Default( + val elementIdConverter: TsElementIdConverter = TsElementIdConverter.Default, + val mapTypeConverter: TsMapTypeConverter = TsMapTypeConverter.Default, + ) : TsTypeRefConverter { + + override operator fun invoke( + descriptor: SerialDescriptor, + ): TsTypeRef { + return when (val descriptorKind = descriptor.kind) { + is PrimitiveKind -> primitiveTypeRef(descriptor, descriptorKind) + + StructureKind.LIST -> listTypeRef(descriptor) + StructureKind.MAP -> mapTypeRef(descriptor) + + SerialKind.CONTEXTUAL, + PolymorphicKind.SEALED, + PolymorphicKind.OPEN, + SerialKind.ENUM, + StructureKind.CLASS, + StructureKind.OBJECT -> declarationTypeRef(descriptor) + } + } + + fun primitiveTypeRef( + descriptor: SerialDescriptor, + kind: PrimitiveKind, + ): TsTypeRef.Literal { + val tsPrimitive = when (kind) { + PrimitiveKind.BOOLEAN -> TsLiteral.Primitive.TsBoolean + + PrimitiveKind.BYTE, + PrimitiveKind.SHORT, + PrimitiveKind.INT, + PrimitiveKind.LONG, + PrimitiveKind.FLOAT, + PrimitiveKind.DOUBLE -> TsLiteral.Primitive.TsNumber + + PrimitiveKind.CHAR, + PrimitiveKind.STRING -> TsLiteral.Primitive.TsString + } + return TsTypeRef.Literal(tsPrimitive, descriptor.isNullable) + } + + + fun mapTypeRef(descriptor: SerialDescriptor): TsTypeRef.Literal { + val (keyDescriptor, valueDescriptor) = descriptor.elementDescriptors.toList() + val keyTypeRef = this(keyDescriptor) + val valueTypeRef = this(valueDescriptor) + val type = mapTypeConverter(keyDescriptor) + val map = TsLiteral.TsMap(keyTypeRef, valueTypeRef, type) + return TsTypeRef.Literal(map, descriptor.isNullable) + } + + + fun listTypeRef(descriptor: SerialDescriptor): TsTypeRef.Literal { + val elementDescriptor = descriptor.elementDescriptors.first() + val elementTypeRef = this(elementDescriptor) + val listRef = TsLiteral.TsList(elementTypeRef) + return TsTypeRef.Literal(listRef, descriptor.isNullable) + } + + + fun declarationTypeRef( + descriptor: SerialDescriptor + ): TsTypeRef.Declaration { + val id = elementIdConverter(descriptor) + return TsTypeRef.Declaration(id, null, descriptor.isNullable) + } + } + +} diff --git a/modules/kxs-ts-gen-core/src/commonMain/kotlin/dev.adamko.kxstsgen/_annotations.kt b/modules/kxs-ts-gen-core/src/commonMain/kotlin/dev.adamko.kxstsgen/_annotations.kt new file mode 100644 index 00000000..c739e0ab --- /dev/null +++ b/modules/kxs-ts-gen-core/src/commonMain/kotlin/dev.adamko.kxstsgen/_annotations.kt @@ -0,0 +1,12 @@ +package dev.adamko.kxstsgen + + +@Target( + AnnotationTarget.CLASS, + AnnotationTarget.PROPERTY, + AnnotationTarget.FUNCTION, + AnnotationTarget.TYPEALIAS +) +@RequiresOptIn(level = RequiresOptIn.Level.WARNING) +@MustBeDocumented +annotation class UnimplementedKxTsGenApi diff --git a/modules/kxs-ts-gen-core/src/commonMain/kotlin/dev.adamko.kxstsgen/experiments/serializerExtractors.kt b/modules/kxs-ts-gen-core/src/commonMain/kotlin/dev.adamko.kxstsgen/experiments/serializerExtractors.kt new file mode 100644 index 00000000..7fe8b147 --- /dev/null +++ b/modules/kxs-ts-gen-core/src/commonMain/kotlin/dev.adamko.kxstsgen/experiments/serializerExtractors.kt @@ -0,0 +1,56 @@ +@file:OptIn(InternalSerializationApi::class) + +package dev.adamko.kxstsgen.experiments + +import dev.adamko.kxstsgen.KxsTsConfig +import kotlinx.serialization.ContextualSerializer +import kotlinx.serialization.DeserializationStrategy +import kotlinx.serialization.InternalSerializationApi +import kotlinx.serialization.KSerializer +import kotlinx.serialization.SealedClassSerializer +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.AbstractDecoder +import kotlinx.serialization.modules.SerializersModule + + +// https://github.com/Kotlin/kotlinx.serialization/issues/1865 +expect fun extractSealedSubclassSerializers( + serializer: SealedClassSerializer +): Collection> + + +/** Hacky exploit to capture the [KSerializer] of a [ContextualSerializer]. */ +fun extractContextualSerializer( + serializer: ContextualSerializer<*>, + kxsTsConfig: KxsTsConfig, +): KSerializer<*>? { + return try { + val decoder = ContextualSerializerCaptorDecoder(kxsTsConfig.serializersModule) + serializer.deserialize(decoder) + null // this should never be hit, decoder should always throw an exception + } catch (e: SerializerCaptorException) { + e.serializer + } catch (e: Throwable) { + null + } +} + +private class ContextualSerializerCaptorDecoder( + override val serializersModule: SerializersModule +) : AbstractDecoder() { + + override fun decodeElementIndex(descriptor: SerialDescriptor): Nothing = + error("intentionally unimplemented, I don't expect ContextualSerializer to call this method") + + override fun decodeSerializableValue(deserializer: DeserializationStrategy): Nothing = + throw SerializerCaptorException(deserializer as KSerializer) +} + + +private class SerializerCaptorException(val serializer: KSerializer<*>) : Exception() + + +expect fun extractContextualDescriptor( + serializer: ContextualSerializer, + serializersModule: SerializersModule, +): KSerializer diff --git a/modules/kxs-ts-gen-core/src/commonMain/kotlin/dev.adamko.kxstsgen/tsElements.kt b/modules/kxs-ts-gen-core/src/commonMain/kotlin/dev.adamko.kxstsgen/tsElements.kt index 973a3dee..8096be89 100644 --- a/modules/kxs-ts-gen-core/src/commonMain/kotlin/dev.adamko.kxstsgen/tsElements.kt +++ b/modules/kxs-ts-gen-core/src/commonMain/kotlin/dev.adamko.kxstsgen/tsElements.kt @@ -1,116 +1,189 @@ package dev.adamko.kxstsgen +import dev.adamko.kxstsgen.TsProperty.Optional +import dev.adamko.kxstsgen.TsProperty.Required import kotlin.jvm.JvmInline +import kotlinx.serialization.descriptors.SerialDescriptor +/** + * A unique identifier for a [TsElement]. + * + * This is usually generated from [SerialDescriptor.serialName]. + */ +// Note: A value class probably isn't the best choice here. The manual String manipulation is +// restrictive and clunky, and makes nested references very difficult to decipher. @JvmInline value class TsElementId(private val id: String) { - val namespace: String get() = id.substringBeforeLast(".") val name: String get() = id.substringAfterLast(".") - val filename: String - get() = namespace.lowercase().replace('.', '-') + ".d.ts" + + override fun toString(): String = id } -sealed interface TsElement { +/** + * Some TypeScript source code element. Either a [TsLiteral] or a [TsDeclaration]. + */ +sealed interface TsElement + + +/** + * Declarations are named elements that developers create in TypeScript source code. + * + * For example, an [interface][TsDeclaration.TsInterface] is declared. In contrast, the interface + * may have a [string][TsLiteral.Primitive.TsString] property, which is represented as a + * [TsLiteral]. + */ +sealed interface TsDeclaration : TsElement { val id: TsElementId -// val namespace: String + + + /** A named reference to one or more other types. */ + data class TsTypeAlias( + override val id: TsElementId, + val typeRefs: Set, + ) : TsDeclaration { + constructor(id: TsElementId, typeRef: TsTypeRef, vararg typeRefs: TsTypeRef) : + this(id, typeRefs.toSet() + typeRef) + } + + + data class TsInterface( + override val id: TsElementId, + val properties: Set, + val polymorphism: TsPolymorphism?, + ) : TsDeclaration + + + data class TsEnum( + override val id: TsElementId, + val members: Set, + ) : TsDeclaration + + + data class TsNamespace( + override val id: TsElementId, + val members: Set, + ) : TsDeclaration + } -sealed class TsPrimitive( - type: String -) : TsElement { - final override val id: TsElementId = TsElementId(type) +/** Literal built-in TypeScript elements. */ +sealed interface TsLiteral : TsElement { + + sealed interface Primitive : TsLiteral { + + object TsString : Primitive + object TsNumber : Primitive + object TsBoolean : Primitive + + object TsObject : Primitive - object TsString : TsPrimitive("string") - object TsNumber : TsPrimitive("number") - object TsBoolean : TsPrimitive("boolean") + object TsAny : Primitive - object TsObject : TsPrimitive("object") + // the remaining primitives are defined, but unused + object TsNever : Primitive + object TsNull : Primitive + object TsUndefined : Primitive + object TsUnknown : Primitive + object TsVoid : Primitive + } + + + /** A list with elements of type [valueTypeRef]. */ + data class TsList( + val valueTypeRef: TsTypeRef, + ) : TsLiteral + + + /** A key-value map. */ + data class TsMap( + val keyTypeRef: TsTypeRef, + val valueTypeRef: TsTypeRef, + val type: Type, + ) : TsLiteral { + enum class Type { + INDEX_SIGNATURE, + MAPPED_OBJECT, + MAP, + } + } - object TsAny : TsPrimitive("any") - object TsNever : TsPrimitive("never") - object TsNull : TsPrimitive("null") - object TsUndefined : TsPrimitive("undefined") - object TsUnknown : TsPrimitive("unknown") - object TsVoid : TsPrimitive("void") } -data class TsTypeAlias( - override val id: TsElementId, - val types: Set, -) : TsElement -//{ -// constructor(id: TsElementId, vararg types: TsElementId) -// : this(id, types.map { it.id }.toSet()) -//} +/** + * A reference to some [TsElement]. The reference may be [nullable]. + * + * A reference does not require the target to be generated. + * This helps prevent circular dependencies causing a lock. + */ +sealed interface TsTypeRef { + val nullable: Boolean -data class TsTypeReference( - val id: TsElementId, - val nullable: Boolean, -) + data class Literal( + val element: TsLiteral, + override val nullable: Boolean, + ) : TsTypeRef + data class Declaration( + val id: TsElementId, + val parent: Declaration?, + override val nullable: Boolean, + ) : TsTypeRef + +} + + +/** + * A property within an [interface][TsDeclaration.TsInterface] + * + * In property may be [Required] or [Optional]. See the TypeScript docs: + * ['Optional Properties'](https://www.typescriptlang.org/docs/handbook/2/objects.html#optional-properties) + */ sealed interface TsProperty { val name: String - val typeReference: TsTypeReference -// val optional: Boolean + val typeRef: TsTypeRef + data class Required( override val name: String, - override val typeReference: TsTypeReference, + override val typeRef: TsTypeRef, ) : TsProperty -// { -// override val optional: Boolean = false -// } + data class Optional( override val name: String, - override val typeReference: TsTypeReference, + override val typeRef: TsTypeRef, ) : TsProperty -// { -// override val optional: Boolean = true -// } } -sealed interface TsStructure : TsElement { +/** + * Meta-data about the polymorphism of a [TsDeclaration.TsInterface]. + */ +sealed interface TsPolymorphism { - data class TsInterface( - override val id: TsElementId, - val properties: List, - ) : TsStructure + /** The name of the field used to discriminate between [subclasses]. */ + val discriminatorName: String + val subclasses: Set - data class TsEnum( - override val id: TsElementId, - val members: Set - ) : TsStructure - data class TsList( - override val id: TsElementId, - val elementsTsType: TsTypeReference, - ) : TsStructure - - data class TsMap( - override val id: TsElementId, - val keyTsType: TsTypeReference, - val valueTsType: TsTypeReference, - ) : TsStructure -} + data class Sealed( + override val discriminatorName: String, + override val subclasses: Set, + ) : TsPolymorphism -//@JvmInline -//value class TsProperties( -// private val properties: List, -//) { -// override fun toString(): String = properties.joinToString("\n") { it.output + ";" } -//} -sealed interface TsPolymorphicDiscriminator { - sealed interface Closed - sealed interface Open + /** Note: [Open] is not implemented correctly */ + @UnimplementedKxTsGenApi + data class Open( + override val discriminatorName: String, + override val subclasses: Set, + ) : TsPolymorphism } diff --git a/modules/kxs-ts-gen-core/src/commonMain/kotlin/dev.adamko.kxstsgen/util/mapWithDefaultDelegate.kt b/modules/kxs-ts-gen-core/src/commonMain/kotlin/dev.adamko.kxstsgen/util/mapWithDefaultDelegate.kt new file mode 100644 index 00000000..1abe6a83 --- /dev/null +++ b/modules/kxs-ts-gen-core/src/commonMain/kotlin/dev.adamko.kxstsgen/util/mapWithDefaultDelegate.kt @@ -0,0 +1,51 @@ +package dev.adamko.kxstsgen.util + +import kotlin.jvm.JvmName +import kotlin.properties.ReadWriteProperty +import kotlin.reflect.KProperty + + +class MutableMapWithDefaultPut( + initial: Map = emptyMap(), + private val defaultValue: (key: K) -> V, +) : ReadWriteProperty> { + + private var map: MutableMap = initial.toMutableMap().withDefaultPut(defaultValue) + + override fun getValue(thisRef: Any?, property: KProperty<*>): MutableMap = map + + override fun setValue(thisRef: Any?, property: KProperty<*>, value: MutableMap) { + this.map = value.withDefaultPut(defaultValue) + } +} + + +class MapWithDefaultPut( + initial: Map = emptyMap(), + private val defaultValue: (key: K) -> V, +) : ReadWriteProperty> { + + private var map: Map = with(initial.toMutableMap()) { + withDefault { key -> getOrPut(key) { defaultValue(key) } } + } + + override fun getValue(thisRef: Any?, property: KProperty<*>): Map = map + + override fun setValue(thisRef: Any?, property: KProperty<*>, value: Map) { + this.map = value.toMutableMap().withDefaultPut(defaultValue) + } +} + + +@JvmName("mapWithDefaultPut") +fun Map.withDefaultPut(defaultValue: (key: K) -> V): Map = + with(this.toMutableMap()) { + withDefault { key -> getOrPut(key) { defaultValue(key) } } + } + + +@JvmName("mutableMapWithDefaultPut") +fun MutableMap.withDefaultPut(defaultValue: (key: K) -> V): MutableMap = + with(this) { + withDefault { key -> getOrPut(key) { defaultValue(key) } } + } diff --git a/modules/kxs-ts-gen-core/src/jvmMain/kotlin/dev/adamko/kxstsgen/experiments/serializerExtractorsJvm.kt b/modules/kxs-ts-gen-core/src/jvmMain/kotlin/dev/adamko/kxstsgen/experiments/serializerExtractorsJvm.kt new file mode 100644 index 00000000..3f8cf326 --- /dev/null +++ b/modules/kxs-ts-gen-core/src/jvmMain/kotlin/dev/adamko/kxstsgen/experiments/serializerExtractorsJvm.kt @@ -0,0 +1,55 @@ +@file:OptIn(InternalSerializationApi::class) // TODO make GitHub issue +package dev.adamko.kxstsgen.experiments + +import kotlin.reflect.KClass +import kotlin.reflect.* +import kotlin.reflect.full.declaredMemberFunctions +import kotlin.reflect.full.declaredMemberProperties +import kotlin.reflect.full.functions +import kotlin.reflect.jvm.isAccessible +import kotlinx.serialization.ContextualSerializer +import kotlinx.serialization.InternalSerializationApi +import kotlinx.serialization.KSerializer +import kotlinx.serialization.SealedClassSerializer +import kotlinx.serialization.modules.SerializersModule + + +actual fun extractSealedSubclassSerializers( + serializer: SealedClassSerializer +): Collection> { + @Suppress("UNCHECKED_CAST") + val class2Serializer = class2SerializerAccessor + .get(serializer) as Map, KSerializer> + return class2Serializer.values +} + + +/** Access the private `class2Serializer` field in [SealedClassSerializer] */ +private val class2SerializerAccessor: KProperty1, *> = + SealedClassSerializer::class + .declaredMemberProperties + .firstOrNull { it.name == "class2Serializer" } + ?.apply { isAccessible = true } + ?: error("Can't access ContextualSerializer.serializer()") + + +@Suppress("UNCHECKED_CAST") +actual fun extractContextualDescriptor( + serializer: ContextualSerializer, + serializersModule: SerializersModule, +): KSerializer { + return contextualSerializerAccessor(serializersModule) as KSerializer +} + + +/** Access the private `.serializer()` function in [ContextualSerializer] */ +@Suppress("UNCHECKED_CAST") +private val contextualSerializerAccessor: KFunction1> = + (ContextualSerializer::class + .declaredMemberFunctions + .firstOrNull { it.name == "serializer" } + ?.apply { isAccessible = true } + as KFunction1> + ) + ?: error("Can't access ContextualSerializer.serializer()") +// as (SerializersModule) -> KSerializer<*> diff --git a/modules/kxs-ts-gen-processor/build.gradle.kts b/modules/kxs-ts-gen-processor/build.gradle.kts new file mode 100644 index 00000000..81c344f3 --- /dev/null +++ b/modules/kxs-ts-gen-processor/build.gradle.kts @@ -0,0 +1,35 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + buildsrc.convention.`kotlin-jvm` +// kotlin("plugin.serialization") + +// id("org.jetbrains.reflekt") +} + +description = "Experimental alternative to Kotlinx Serialization. Currently unused." + +val kspVersion = "1.6.10-1.0.4" +val kotlinCompileTestingVersion = "1.4.7" +val kotlinxSerializationVersion = "1.3.2" // TODO put dependencies in libs.version.toml + +dependencies { + implementation("com.google.devtools.ksp:symbol-processing-api:$kspVersion") + + testImplementation("com.github.tschuchortdev:kotlin-compile-testing:$kotlinCompileTestingVersion") + testImplementation("com.github.tschuchortdev:kotlin-compile-testing-ksp:$kotlinCompileTestingVersion") + + implementation(projects.modules.kxsTsGenCore) + + implementation(platform("org.jetbrains.kotlinx:kotlinx-serialization-bom:${kotlinxSerializationVersion}")) + implementation("org.jetbrains.kotlinx:kotlinx-serialization-core") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json") + + testImplementation(kotlin("test")) +} + +tasks.withType { + kotlinOptions.freeCompilerArgs += listOf( + "-opt-in=kotlinx.serialization.ExperimentalSerializationApi", + ) +} diff --git a/modules/kxs-ts-gen-processor/src/main/kotlin/dev/adamko/kxstsgen/kxsTsGenProcessor.kt b/modules/kxs-ts-gen-processor/src/main/kotlin/dev/adamko/kxstsgen/kxsTsGenProcessor.kt new file mode 100644 index 00000000..4377e726 --- /dev/null +++ b/modules/kxs-ts-gen-processor/src/main/kotlin/dev/adamko/kxstsgen/kxsTsGenProcessor.kt @@ -0,0 +1,141 @@ +package dev.adamko.kxstsgen + +import com.google.devtools.ksp.processing.CodeGenerator +import com.google.devtools.ksp.processing.Dependencies +import com.google.devtools.ksp.processing.KSPLogger +import com.google.devtools.ksp.processing.Resolver +import com.google.devtools.ksp.processing.SymbolProcessor +import com.google.devtools.ksp.processing.SymbolProcessorEnvironment +import com.google.devtools.ksp.processing.SymbolProcessorProvider +import com.google.devtools.ksp.symbol.ClassKind +import com.google.devtools.ksp.symbol.KSAnnotated +import com.google.devtools.ksp.symbol.KSClassDeclaration +import com.google.devtools.ksp.symbol.KSNode +import com.google.devtools.ksp.visitor.KSTopDownVisitor +import java.io.OutputStreamWriter +import kotlinx.serialization.SerialInfo + +class KxsTsGenProcessorProvider : SymbolProcessorProvider { + override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor { + return KxsTsGenProcessor(environment.codeGenerator, environment.logger) + } +} + + +class KxsTsGenProcessor( + private val codeGenerator: CodeGenerator, + private val logger: KSPLogger, +) : SymbolProcessor { + private var invoked = false + + override fun process(resolver: Resolver): List { + + val allFiles = resolver.getAllFiles().map { it.fileName } + logger.warn(allFiles.toList().joinToString()) + if (invoked) { + logger.info("Already invoked") + return emptyList() + } + invoked = true + + codeGenerator + .createNewFile(Dependencies(false), "", "Foo", "d.ts") + .use { output -> + + OutputStreamWriter(output).use { writer -> + + val visitor = ClassVisitor(codeGenerator, resolver) + resolver + .getSymbolsWithAnnotation(TsExport::class.qualifiedName!!) + .filterIsInstance() + .filter { + when (it.classKind) { + ClassKind.CLASS, + ClassKind.ENUM_CLASS, + ClassKind.OBJECT, + ClassKind.INTERFACE -> true + ClassKind.ANNOTATION_CLASS, + ClassKind.ENUM_ENTRY -> false + } + } + .forEach { + logger.info("Visiting $it") + it.accept(visitor, writer) + } + + } + } + return emptyList() + } +} + +class ClassVisitor( + private val codeGenerator: CodeGenerator, + private val resolver: Resolver +) : KSTopDownVisitor() { + + override fun defaultHandler(node: KSNode, data: OutputStreamWriter) { + } + + override fun visitClassDeclaration( + classDeclaration: KSClassDeclaration, + data: OutputStreamWriter + ) { + super.visitClassDeclaration(classDeclaration, data) + + classDeclaration.classKind + + val containingFile = classDeclaration.containingFile!! + + + codeGenerator.createNewFile( + Dependencies(true, containingFile), + containingFile.packageName.toString(), + "asd", + ) + + + val symbolName = classDeclaration.simpleName.asString() + +// val constructor = classDeclaration.primaryConstructor ?: return +// +// data.write( +// buildString { +// append("interface ") +// append(symbolName) +// appendLine(" {") +// +// constructor.parameters +// .filter { it.name != null } +// .forEach { +// append(" ") +// append(it.name?.getShortName()) +// append(if (it.hasDefault) "?:" else ":") +// append(" ") +// append(tsPrimitive(it.type.resolve())) +// appendLine(";") +// } +// +// appendLine("}") +// } +// ) + } + +} +// +// +//data class KxsIntrospectionInfo( +// val ksType: KSType, +// val descriptorHashCode: KxsDescriptorHashCode, +// val sealedParent: SerialDescriptor? = null, +// val sealedSubclasses: Set? = null, +//) + + +/** + * Mark [Serializable] classes that will be converted to TypeScript. + */ +@Target(AnnotationTarget.CLASS) +@MustBeDocumented +@SerialInfo +annotation class TsExport diff --git a/settings.gradle.kts b/settings.gradle.kts index 6e218056..796f828d 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,11 +1,16 @@ -rootProject.name = "kxs-typescript-generator" +rootProject.name = "kotlinx-serialization-typescript-generator" apply(from = "./buildSrc/repositories.settings.gradle.kts") include( ":modules:kxs-ts-gen-core", - ":modules:kxs-ts-gen-plugin", - ":docs:knit", + ":modules:kxs-ts-gen-processor", + ":docs:code", ) enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") + +dependencyResolutionManagement { + @Suppress("UnstableApiUsage") // Central declaration of repositories is an incubating feature + repositoriesMode.set(RepositoriesMode.PREFER_SETTINGS) +}