From 611f64220fd805add1a8f2c647f0df529394c43a Mon Sep 17 00:00:00 2001 From: aSemy <897017+aSemy@users.noreply.github.com> Date: Wed, 13 Apr 2022 21:16:36 +0200 Subject: [PATCH] Element overriding (#30) * allow for overriding of elements (partial support for #23 and #22), and update knit to allow for TS Compile to be disabled * make TsMapTypeConverter more clear, split inline/non-inline * code tidy * fix bug where overrides weren't consistently found and applied if the target was nullable --- .../example/example-customising-output-01.kt | 31 ++++ .../example/example-customising-output-02.kt | 32 ++++ .../example/example-customising-output-03.kt | 39 ++++ docs/code/knit-test.ftl | 10 + docs/code/test/CustomisingOutputTest.kt | 97 ++++++++++ docs/customising-output.md | 175 ++++++++++++++++++ .../dev/adamko/kxstsgen/KxsTsGenerator.kt | 125 +++++++++---- .../kxstsgen/core/KxsTsSourceCodeGenerator.kt | 5 +- .../kxstsgen/core/TsElementIdConverter.kt | 2 +- .../kxstsgen/core/TsMapTypeConverter.kt | 36 ++-- 10 files changed, 496 insertions(+), 56 deletions(-) create mode 100644 docs/code/example/example-customising-output-01.kt create mode 100644 docs/code/example/example-customising-output-02.kt create mode 100644 docs/code/example/example-customising-output-03.kt create mode 100644 docs/code/test/CustomisingOutputTest.kt create mode 100644 docs/customising-output.md diff --git a/docs/code/example/example-customising-output-01.kt b/docs/code/example/example-customising-output-01.kt new file mode 100644 index 00000000..6b89589c --- /dev/null +++ b/docs/code/example/example-customising-output-01.kt @@ -0,0 +1,31 @@ +// This file was automatically generated from customising-output.md by Knit tool. Do not edit. +@file:Suppress("PackageDirectoryMismatch", "unused") +package dev.adamko.kxstsgen.example.exampleCustomisingOutput01 + +import kotlinx.serialization.* +import dev.adamko.kxstsgen.* + +import kotlinx.serialization.builtins.serializer +import dev.adamko.kxstsgen.core.* + +@Serializable +data class Item( + val price: Double, + val count: Int, +) + +fun main() { + val tsGenerator = KxsTsGenerator() + + tsGenerator.descriptorOverrides += + Double.serializer().descriptor to TsDeclaration.TsTypeAlias( + id = TsElementId("Double"), + typeRef = TsTypeRef.Declaration( + id = TsElementId("double"), + parent = null, + nullable = false, + ) + ) + + println(tsGenerator.generate(Item.serializer())) +} diff --git a/docs/code/example/example-customising-output-02.kt b/docs/code/example/example-customising-output-02.kt new file mode 100644 index 00000000..28b63047 --- /dev/null +++ b/docs/code/example/example-customising-output-02.kt @@ -0,0 +1,32 @@ +// This file was automatically generated from customising-output.md by Knit tool. Do not edit. +@file:Suppress("PackageDirectoryMismatch", "unused") +package dev.adamko.kxstsgen.example.exampleCustomisingOutput02 + +import kotlinx.serialization.* +import dev.adamko.kxstsgen.* + +import kotlinx.serialization.builtins.serializer +import dev.adamko.kxstsgen.core.* + +@Serializable +data class ItemHolder( + val item: Item, +) + +@Serializable +data class Item( + val count: UInt? = 0u, +) + +fun main() { + val tsGenerator = KxsTsGenerator() + + tsGenerator.descriptorOverrides += + UInt.serializer().descriptor to TsDeclaration.TsTypeAlias( + id = TsElementId("kotlin.UInt"), + typeRef = TsTypeRef.Declaration(id = TsElementId("uint"), parent = null, nullable = false) + ) + + println(tsGenerator.generate(ItemHolder.serializer())) +} + diff --git a/docs/code/example/example-customising-output-03.kt b/docs/code/example/example-customising-output-03.kt new file mode 100644 index 00000000..b87d2d16 --- /dev/null +++ b/docs/code/example/example-customising-output-03.kt @@ -0,0 +1,39 @@ +// This file was automatically generated from customising-output.md by Knit tool. Do not edit. +@file:Suppress("PackageDirectoryMismatch", "unused") +package dev.adamko.kxstsgen.example.exampleCustomisingOutput03 + +import kotlinx.serialization.* +import dev.adamko.kxstsgen.* + +import kotlinx.serialization.builtins.serializer +import dev.adamko.kxstsgen.core.* + + +@Serializable +@JvmInline +value class Tick(val value: UInt) + +@Serializable +data class ItemHolder( + val item: Item, + val tick: Tick?, +) + +@Serializable +data class Item( + val count: UInt? = 0u, +) + +fun main() { + val tsGenerator = KxsTsGenerator() + + tsGenerator.descriptorOverrides += + UInt.serializer().descriptor to TsDeclaration.TsTypeAlias( + id = TsElementId("kotlin.UInt"), + typeRef = TsTypeRef.Declaration(id = TsElementId("uint"), parent = null, nullable = false) + ) + + println(tsGenerator.generate(ItemHolder.serializer())) +} + + diff --git a/docs/code/knit-test.ftl b/docs/code/knit-test.ftl index f20f4e61..564f5876 100644 --- a/docs/code/knit-test.ftl +++ b/docs/code/knit-test.ftl @@ -1,3 +1,4 @@ +<#--@formatter:off--> <#-- @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. @@ -24,7 +25,9 @@ class ${test.name} : FunSpec({ test("expect actual matches TypeScript") { actual.shouldBe( + <#if case.param != "TS_COMPILE_OFF"> // language=TypeScript + """ <#list case.lines as line> |${line} @@ -34,9 +37,16 @@ class ${test.name} : FunSpec({ ) } + <#if case.param == "TS_COMPILE_OFF"> + // TS_COMPILE_OFF + // test("expect actual compiles").config(tags = tsCompile) { + // actual.shouldTypeScriptCompile() + // } + <#else> test("expect actual compiles").config(tags = tsCompile) { actual.shouldTypeScriptCompile() } + } <#sep> diff --git a/docs/code/test/CustomisingOutputTest.kt b/docs/code/test/CustomisingOutputTest.kt new file mode 100644 index 00000000..bc792157 --- /dev/null +++ b/docs/code/test/CustomisingOutputTest.kt @@ -0,0 +1,97 @@ +// This file was automatically generated from customising-output.md by Knit tool. Do not edit. +@file:Suppress("JSUnusedLocalSymbols") +package dev.adamko.kxstsgen.example.test + +import dev.adamko.kxstsgen.util.* +import io.kotest.core.spec.style.* +import io.kotest.matchers.* +import kotlinx.knit.test.* + +class CustomisingOutputTest : FunSpec({ + + tags(Knit) + + context("ExampleCustomisingOutput01") { + val actual = captureOutput("ExampleCustomisingOutput01") { + dev.adamko.kxstsgen.example.exampleCustomisingOutput01.main() + }.normalizeJoin() + + test("expect actual matches TypeScript") { + actual.shouldBe( + """ + |export interface Item { + | price: Double; + | count: number; + |} + | + |export type Double = double; // assume that 'double' will be provided by another library + """.trimMargin() + .normalize() + ) + } + + // TS_COMPILE_OFF + // test("expect actual compiles").config(tags = tsCompile) { + // actual.shouldTypeScriptCompile() + // } + } + + context("ExampleCustomisingOutput02") { + val actual = captureOutput("ExampleCustomisingOutput02") { + dev.adamko.kxstsgen.example.exampleCustomisingOutput02.main() + }.normalizeJoin() + + test("expect actual matches TypeScript") { + actual.shouldBe( + """ + |export interface ItemHolder { + | item: Item; + |} + | + |export interface Item { + | count?: UInt | null; + |} + | + |export type UInt = uint; + """.trimMargin() + .normalize() + ) + } + + // TS_COMPILE_OFF + // test("expect actual compiles").config(tags = tsCompile) { + // actual.shouldTypeScriptCompile() + // } + } + + context("ExampleCustomisingOutput03") { + val actual = captureOutput("ExampleCustomisingOutput03") { + dev.adamko.kxstsgen.example.exampleCustomisingOutput03.main() + }.normalizeJoin() + + test("expect actual matches TypeScript") { + actual.shouldBe( + """ + |export interface ItemHolder { + | item: Item; + | tick: Tick | null; + |} + | + |export interface Item { + | count?: UInt | null; + |} + | + |export type Tick = UInt; + | + |export type UInt = uint; + """.trimMargin() + .normalize() + ) + } + + // TS_COMPILE_OFF + // test("expect actual compiles").config(tags = tsCompile) { + // actual.shouldTypeScriptCompile() + // } + } +}) diff --git a/docs/customising-output.md b/docs/customising-output.md new file mode 100644 index 00000000..63771015 --- /dev/null +++ b/docs/customising-output.md @@ -0,0 +1,175 @@ + + + +**Table of contents** + + + +* [Introduction](#introduction) + * [Overriding output](#overriding-output) + * [Override nullable elements](#override-nullable-elements) + * [Override both nullable and non-nullable descriptors](#override-both-nullable-and-non-nullable-descriptors) + + + + + + +## Introduction + +### Overriding output + +If you want to override what KxsTsGen produces, then you can provide overrides. + +By default, `Double` is transformed to `number`, but now we want to alias `Double` to `double`. + +```kotlin +import kotlinx.serialization.builtins.serializer +import dev.adamko.kxstsgen.core.* + +@Serializable +data class Item( + val price: Double, + val count: Int, +) + +fun main() { + val tsGenerator = KxsTsGenerator() + + tsGenerator.descriptorOverrides += + Double.serializer().descriptor to TsDeclaration.TsTypeAlias( + id = TsElementId("Double"), + typeRef = TsTypeRef.Declaration( + id = TsElementId("double"), + parent = null, + nullable = false, + ) + ) + + println(tsGenerator.generate(Item.serializer())) +} +``` + +> You can get the full code [here](./code/example/example-customising-output-01.kt). + +```typescript +export interface Item { + price: Double; + count: number; +} + +export type Double = double; // assume that 'double' will be provided by another library +``` + + + +### Override nullable elements + +Even though UInt is nullable, it should be overridden by the UInt defined in `descriptorOverrides`. + +```kotlin +import kotlinx.serialization.builtins.serializer +import dev.adamko.kxstsgen.core.* + +@Serializable +data class ItemHolder( + val item: Item, +) + +@Serializable +data class Item( + val count: UInt? = 0u, +) + +fun main() { + val tsGenerator = KxsTsGenerator() + + tsGenerator.descriptorOverrides += + UInt.serializer().descriptor to TsDeclaration.TsTypeAlias( + id = TsElementId("kotlin.UInt"), + typeRef = TsTypeRef.Declaration(id = TsElementId("uint"), parent = null, nullable = false) + ) + + println(tsGenerator.generate(ItemHolder.serializer())) +} + +``` + +> You can get the full code [here](./code/example/example-customising-output-02.kt). + +```typescript +export interface ItemHolder { + item: Item; +} + +export interface Item { + count?: UInt | null; +} + +export type UInt = uint; +``` + + + +### Override both nullable and non-nullable descriptors + +`Tick` has a non-nullable UInt, while `Item` has a nullable UInt. Also, in `ItemHolder`, `Tick` is +nullable. Even though a non-nullable override for UInt is supplied, the output shouldn't have +conflicting overrides. + +```kotlin +import kotlinx.serialization.builtins.serializer +import dev.adamko.kxstsgen.core.* + + +@Serializable +@JvmInline +value class Tick(val value: UInt) + +@Serializable +data class ItemHolder( + val item: Item, + val tick: Tick?, +) + +@Serializable +data class Item( + val count: UInt? = 0u, +) + +fun main() { + val tsGenerator = KxsTsGenerator() + + tsGenerator.descriptorOverrides += + UInt.serializer().descriptor to TsDeclaration.TsTypeAlias( + id = TsElementId("kotlin.UInt"), + typeRef = TsTypeRef.Declaration(id = TsElementId("uint"), parent = null, nullable = false) + ) + + println(tsGenerator.generate(ItemHolder.serializer())) +} + + +``` + +> You can get the full code [here](./code/example/example-customising-output-03.kt). + +```typescript +export interface ItemHolder { + item: Item; + tick: Tick | null; +} + +export interface Item { + count?: UInt | null; +} + +export type Tick = UInt; + +export type UInt = uint; +``` + + 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 14d09102..cb284a2b 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 @@ -7,82 +7,133 @@ import dev.adamko.kxstsgen.core.TsElement import dev.adamko.kxstsgen.core.TsElementConverter import dev.adamko.kxstsgen.core.TsElementId import dev.adamko.kxstsgen.core.TsElementIdConverter +import dev.adamko.kxstsgen.core.TsLiteral import dev.adamko.kxstsgen.core.TsMapTypeConverter import dev.adamko.kxstsgen.core.TsTypeRef import dev.adamko.kxstsgen.core.TsTypeRefConverter import kotlinx.serialization.KSerializer import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.nullable /** * Generate TypeScript from [`@Serializable`][Serializable] Kotlin. * * The output can be controlled by the settings in [config], - * or by setting hardcoded values in [serializerDescriptors] or [descriptorElements], + * or by setting hardcoded values in [serializerDescriptorOverrides] or [descriptorOverrides], * or changed by overriding any converter. * * @param[config] General settings that affect how KxTsGen works - * @param[descriptorsExtractor] Given a [KSerializer], extract all [SerialDescriptor]s - * @param[elementIdConverter] Create an [TsElementId] from a [SerialDescriptor] - * @param[mapTypeConverter] Decides how [Map]s should be converted - * @param[typeRefConverter] Creates [TsTypeRef]s - * @param[elementConverter] Converts [SerialDescriptor]s to [TsElement]s * @param[sourceCodeGenerator] Convert [TsElement]s to TypeScript source code */ open class KxsTsGenerator( open val config: KxsTsConfig = KxsTsConfig(), - open val descriptorsExtractor: SerializerDescriptorsExtractor = SerializerDescriptorsExtractor.Default, + open val sourceCodeGenerator: KxsTsSourceCodeGenerator = KxsTsSourceCodeGenerator.Default(config), +) { + - open val elementIdConverter: TsElementIdConverter = TsElementIdConverter.Default, + val serializerDescriptorOverrides: MutableMap, Set> = + mutableMapOf() - open val mapTypeConverter: TsMapTypeConverter = TsMapTypeConverter.Default, + val descriptorOverrides: MutableMap = mutableMapOf() + + private fun findOverride(descriptor: SerialDescriptor): TsElement? { + return descriptorOverrides.entries.run { + firstOrNull { it.key == descriptor } ?: firstOrNull { it.key.nullable == descriptor.nullable } + }?.value + } - open val typeRefConverter: TsTypeRefConverter = - TsTypeRefConverter.Default(elementIdConverter, mapTypeConverter), - open val elementConverter: TsElementConverter = - TsElementConverter.Default( + open val descriptorsExtractor = object : SerializerDescriptorsExtractor { + val extractor: SerializerDescriptorsExtractor = SerializerDescriptorsExtractor.Default + val cache: MutableMap, Set> = mutableMapOf() + + override fun invoke(serializer: KSerializer<*>): Set = + cache.getOrPut(serializer) { + serializerDescriptorOverrides[serializer] ?: extractor(serializer) + } + } + + + val elementIdConverter: TsElementIdConverter = object : TsElementIdConverter { + private val converter: TsElementIdConverter = TsElementIdConverter.Default + private val cache: MutableMap = mutableMapOf() + + override fun invoke(descriptor: SerialDescriptor): TsElementId = + cache.getOrPut(descriptor) { + when (val override = findOverride(descriptor)) { + is TsDeclaration -> override.id + else -> converter(descriptor) + } + } + } + + + val mapTypeConverter: TsMapTypeConverter = object : TsMapTypeConverter { + private val converter = TsMapTypeConverter.Default + private val cache: MutableMap, TsLiteral.TsMap.Type> = + mutableMapOf() + + override fun invoke( + keyDescriptor: SerialDescriptor, + valDescriptor: SerialDescriptor, + ): TsLiteral.TsMap.Type = + cache.getOrPut(keyDescriptor to valDescriptor) { + converter(keyDescriptor, valDescriptor) + } + } + + + val typeRefConverter: TsTypeRefConverter = object : TsTypeRefConverter { + private val converter = TsTypeRefConverter.Default(elementIdConverter, mapTypeConverter) + val cache: MutableMap = mutableMapOf() + + override fun invoke(descriptor: SerialDescriptor): TsTypeRef = + cache.getOrPut(descriptor) { + when (val override = findOverride(descriptor)) { + null -> converter(descriptor) + is TsLiteral -> TsTypeRef.Literal(override, descriptor.isNullable) + is TsDeclaration -> TsTypeRef.Declaration(override.id, null, descriptor.isNullable) + } + } + } + + + val elementConverter: TsElementConverter = object : TsElementConverter { + private val converter = TsElementConverter.Default( elementIdConverter, mapTypeConverter, typeRefConverter, - ), - - open val sourceCodeGenerator: KxsTsSourceCodeGenerator = KxsTsSourceCodeGenerator.Default(config), -) { + ) + val cache: MutableMap> = mutableMapOf() + + override fun invoke(descriptor: SerialDescriptor): Set = + cache.getOrPut(descriptor) { + when (val override = findOverride(descriptor)) { + null -> converter(descriptor) + else -> setOf(override) + } + } + } - /** - * Stateful cache of all [descriptors][SerialDescriptor] extracted from a - * [serializer][KSerializer]. - * - * To customise the descriptors that a serializer produces, set value into this map. - */ - open val serializerDescriptors: MutableMap, Set> = mutableMapOf() - - /** - * Cache of all [elements][TsElement] that are created from any [descriptor][SerialDescriptor]. - * - * To customise the elements that a descriptor produces, set value into this map. - */ - open val descriptorElements: MutableMap> = mutableMapOf() open fun generate(vararg serializers: KSerializer<*>): String { return serializers .toSet() // 1. get all SerialDescriptors from a KSerializer - .flatMap { serializer -> - serializerDescriptors.getOrPut(serializer) { descriptorsExtractor(serializer) } - } + .flatMap { serializer -> descriptorsExtractor(serializer) } .toSet() // 2. convert each SerialDescriptor to some TsElements - .flatMap { descriptor -> - descriptorElements.getOrPut(descriptor) { elementConverter(descriptor) } - } + .flatMap { descriptor -> elementConverter(descriptor) } .toSet() + // 3. group by namespaces .groupBy { element -> sourceCodeGenerator.groupElementsBy(element) } + + // 4. convert to source code .mapValues { (_, elements) -> elements .filterIsInstance() diff --git a/modules/kxs-ts-gen-core/src/commonMain/kotlin/dev/adamko/kxstsgen/core/KxsTsSourceCodeGenerator.kt b/modules/kxs-ts-gen-core/src/commonMain/kotlin/dev/adamko/kxstsgen/core/KxsTsSourceCodeGenerator.kt index cbcc2b89..041bcd82 100644 --- a/modules/kxs-ts-gen-core/src/commonMain/kotlin/dev/adamko/kxstsgen/core/KxsTsSourceCodeGenerator.kt +++ b/modules/kxs-ts-gen-core/src/commonMain/kotlin/dev/adamko/kxstsgen/core/KxsTsSourceCodeGenerator.kt @@ -86,6 +86,7 @@ abstract class KxsTsSourceCodeGenerator( """.trimMargin() } + override fun generateInterface(element: TsDeclaration.TsInterface): String { val properties = element.properties @@ -100,6 +101,7 @@ abstract class KxsTsSourceCodeGenerator( } } + /** * Generate * ```typescript @@ -145,6 +147,7 @@ abstract class KxsTsSourceCodeGenerator( } } + override fun generateTypeUnion(element: TsDeclaration.TsTypeUnion): String { return if (element.typeRefs.isEmpty()) { """ @@ -248,7 +251,5 @@ abstract class KxsTsSourceCodeGenerator( TsLiteral.TsMap.Type.MAP -> "Map<$keyTypeRef, $valueTypeRef>" } } - } - } diff --git a/modules/kxs-ts-gen-core/src/commonMain/kotlin/dev/adamko/kxstsgen/core/TsElementIdConverter.kt b/modules/kxs-ts-gen-core/src/commonMain/kotlin/dev/adamko/kxstsgen/core/TsElementIdConverter.kt index 4e623544..db29efb0 100644 --- a/modules/kxs-ts-gen-core/src/commonMain/kotlin/dev/adamko/kxstsgen/core/TsElementIdConverter.kt +++ b/modules/kxs-ts-gen-core/src/commonMain/kotlin/dev/adamko/kxstsgen/core/TsElementIdConverter.kt @@ -20,7 +20,7 @@ fun interface TsElementIdConverter { .substringBeforeLast(">") return when { - namespace.isBlank() -> TsElementId("$id") + namespace.isBlank() -> TsElementId(id) else -> TsElementId("$namespace.$id") } } diff --git a/modules/kxs-ts-gen-core/src/commonMain/kotlin/dev/adamko/kxstsgen/core/TsMapTypeConverter.kt b/modules/kxs-ts-gen-core/src/commonMain/kotlin/dev/adamko/kxstsgen/core/TsMapTypeConverter.kt index f71daa5c..cbb99f8d 100644 --- a/modules/kxs-ts-gen-core/src/commonMain/kotlin/dev/adamko/kxstsgen/core/TsMapTypeConverter.kt +++ b/modules/kxs-ts-gen-core/src/commonMain/kotlin/dev/adamko/kxstsgen/core/TsMapTypeConverter.kt @@ -12,21 +12,27 @@ fun interface TsMapTypeConverter { operator fun invoke( keyDescriptor: SerialDescriptor, - valDescriptor: SerialDescriptor?, + valDescriptor: SerialDescriptor, ): TsLiteral.TsMap.Type object Default : TsMapTypeConverter { override operator fun invoke( keyDescriptor: SerialDescriptor, - valDescriptor: SerialDescriptor?, + valDescriptor: SerialDescriptor, ): TsLiteral.TsMap.Type { + return when { + keyDescriptor.isNullable -> TsLiteral.TsMap.Type.MAP + keyDescriptor.isInline -> extractInlineType(keyDescriptor) + else -> serialKindMapType(keyDescriptor.kind) + } + } - if (keyDescriptor.isNullable) return TsLiteral.TsMap.Type.MAP - - if (keyDescriptor.isInline) return extractInlineType(keyDescriptor, valDescriptor) - - return when (keyDescriptor.kind) { + /** Determine a map type based on [kind] */ + fun serialKindMapType( + kind: SerialKind, + ): TsLiteral.TsMap.Type { + return when (kind) { SerialKind.ENUM -> TsLiteral.TsMap.Type.MAPPED_OBJECT PrimitiveKind.STRING -> TsLiteral.TsMap.Type.INDEX_SIGNATURE @@ -49,16 +55,14 @@ fun interface TsMapTypeConverter { } } - tailrec fun extractInlineType( - keyDescriptor: SerialDescriptor?, - valDescriptor: SerialDescriptor?, - ): TsLiteral.TsMap.Type { + + tailrec fun extractInlineType(keyDescriptor: SerialDescriptor): TsLiteral.TsMap.Type { return when { - keyDescriptor == null -> TsLiteral.TsMap.Type.MAP - !keyDescriptor.isInline -> this(keyDescriptor, valDescriptor) - else -> { - val inlineKeyDescriptor = keyDescriptor.elementDescriptors.firstOrNull() - extractInlineType(inlineKeyDescriptor, valDescriptor) + !keyDescriptor.isInline + || keyDescriptor.elementsCount == 0 -> serialKindMapType(keyDescriptor.kind) + else -> { + val inlineKeyDescriptor = keyDescriptor.elementDescriptors.first() + extractInlineType(inlineKeyDescriptor) } } }