diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index 0dab274f..8a072be6 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -17,7 +17,7 @@ object Versions { const val kotlinxSerialization = "1.3.2" const val ksp = "1.6.10-1.0.4" - const val kotest = "5.1.0" + const val kotest = "5.2.3" } diff --git a/docs/code/example/example-tuple-01.kt b/docs/code/example/example-tuple-01.kt index cba8b0c0..250f0bfe 100644 --- a/docs/code/example/example-tuple-01.kt +++ b/docs/code/example/example-tuple-01.kt @@ -27,14 +27,14 @@ data class SimpleTypes( element(SimpleTypes::privateMember) } ) { - override fun tupleConstructor(elements: List<*>): SimpleTypes { + override fun tupleConstructor(elements: Iterator<*>): SimpleTypes { // When deserializing, the elements will be available as a list, in the order defined return SimpleTypes( - elements[0] as String, - elements[1] as Int, - elements[2] as Double, - elements[3] as Boolean, - elements[4] as String, + elements.next() as String, + elements.next() as Int, + elements.next() as Double, + elements.next() as Boolean, + elements.next() as String, ) } } diff --git a/docs/code/example/example-tuple-02.kt b/docs/code/example/example-tuple-02.kt index 6416fd3d..8dac896d 100644 --- a/docs/code/example/example-tuple-02.kt +++ b/docs/code/example/example-tuple-02.kt @@ -22,7 +22,7 @@ data class OptionalFields( element(OptionalFields::nullableOptionalString) } ) { - override fun tupleConstructor(elements: List<*>): OptionalFields { + override fun tupleConstructor(elements: Iterator<*>): OptionalFields { val iter = elements.iterator() return OptionalFields( iter.next() as String, diff --git a/docs/code/example/example-tuple-03.kt b/docs/code/example/example-tuple-03.kt index b81d7ae4..6838945d 100644 --- a/docs/code/example/example-tuple-03.kt +++ b/docs/code/example/example-tuple-03.kt @@ -20,11 +20,11 @@ data class Coordinates( element(Coordinates::z) } ) { - override fun tupleConstructor(elements: List<*>): Coordinates { + override fun tupleConstructor(elements: Iterator<*>): Coordinates { return Coordinates( - elements[0] as Int, - elements[1] as Int, - elements[2] as Int, + elements.next() as Int, + elements.next() as Int, + elements.next() as Int, ) } } diff --git a/docs/code/test/PolymorphismTest.kt b/docs/code/test/PolymorphismTest.kt index 2e2e6bd9..c55bc229 100644 --- a/docs/code/test/PolymorphismTest.kt +++ b/docs/code/test/PolymorphismTest.kt @@ -75,7 +75,9 @@ class PolymorphismTest { .shouldBe( // language=TypeScript """ - |export type Project = Project.DeprecatedProject | Project.OProj; + |export type Project = + | | Project.DeprecatedProject + | | Project.OProj; | |export namespace Project { | export enum Type { @@ -108,7 +110,10 @@ class PolymorphismTest { .shouldBe( // language=TypeScript """ - |export type Dog = Dog.Golden | Dog.Mutt | Dog.NovaScotia; + |export type Dog = + | | Dog.Golden + | | Dog.Mutt + | | Dog.NovaScotia; | |export namespace Dog { | export enum Type { @@ -185,7 +190,9 @@ class PolymorphismTest { .shouldBe( // language=TypeScript """ - |export type Response = Response.EmptyResponse | Response.TextResponse; + |export type Response = + | | Response.EmptyResponse + | | Response.TextResponse; | |export namespace Response { | export enum Type { diff --git a/docs/polymorphism.md b/docs/polymorphism.md index f14cf0a6..c1c47e8d 100644 --- a/docs/polymorphism.md +++ b/docs/polymorphism.md @@ -163,7 +163,9 @@ fun main() { > You can get the full code [here](./code/example/example-polymorphic-sealed-class-01.kt). ```typescript -export type Project = Project.DeprecatedProject | Project.OProj; +export type Project = + | Project.DeprecatedProject + | Project.OProj; export namespace Project { export enum Type { @@ -231,7 +233,10 @@ fun main() { > 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 type Dog = + | Dog.Golden + | Dog.Mutt + | Dog.NovaScotia; export namespace Dog { export enum Type { @@ -322,7 +327,9 @@ fun main() { > You can get the full code [here](./code/example/example-polymorphic-objects-01.kt). ```typescript -export type Response = Response.EmptyResponse | Response.TextResponse; +export type Response = + | Response.EmptyResponse + | Response.TextResponse; export namespace Response { export enum Type { diff --git a/docs/tuples.md b/docs/tuples.md index 81042d7b..306ccba2 100644 --- a/docs/tuples.md +++ b/docs/tuples.md @@ -58,14 +58,14 @@ data class SimpleTypes( element(SimpleTypes::privateMember) } ) { - override fun tupleConstructor(elements: List<*>): SimpleTypes { + override fun tupleConstructor(elements: Iterator<*>): SimpleTypes { // When deserializing, the elements will be available as a list, in the order defined return SimpleTypes( - elements[0] as String, - elements[1] as Int, - elements[2] as Double, - elements[3] as Boolean, - elements[4] as String, + elements.next() as String, + elements.next() as Int, + elements.next() as Double, + elements.next() as Boolean, + elements.next() as String, ) } } @@ -111,7 +111,7 @@ data class OptionalFields( element(OptionalFields::nullableOptionalString) } ) { - override fun tupleConstructor(elements: List<*>): OptionalFields { + override fun tupleConstructor(elements: Iterator<*>): OptionalFields { val iter = elements.iterator() return OptionalFields( iter.next() as String, @@ -154,11 +154,11 @@ data class Coordinates( element(Coordinates::z) } ) { - override fun tupleConstructor(elements: List<*>): Coordinates { + override fun tupleConstructor(elements: Iterator<*>): Coordinates { return Coordinates( - elements[0] as Int, - elements[1] as Int, - elements[2] as Int, + elements.next() as Int, + elements.next() as Int, + elements.next() as Int, ) } } diff --git a/modules/kxs-ts-gen-core/build.gradle.kts b/modules/kxs-ts-gen-core/build.gradle.kts index d74a7f5d..cac37a91 100644 --- a/modules/kxs-ts-gen-core/build.gradle.kts +++ b/modules/kxs-ts-gen-core/build.gradle.kts @@ -6,9 +6,11 @@ plugins { buildsrc.convention.`maven-publish` kotlin("plugin.serialization") // id("org.jetbrains.reflekt") + id("io.kotest.multiplatform") } val kotlinxSerializationVersion = "1.3.2" +val kotestVersion = "5.2.2" kotlin { @@ -70,6 +72,12 @@ kotlin { val commonTest by getting { dependencies { implementation(kotlin("test")) + + implementation("io.kotest:kotest-assertions-core:$kotestVersion") + implementation("io.kotest:kotest-assertions-json:$kotestVersion") + implementation("io.kotest:kotest-property:$kotestVersion") + implementation("io.kotest:kotest-framework-engine:$kotestVersion") + implementation("io.kotest:kotest-framework-datatest:$kotestVersion") } } // val nativeMain by getting @@ -81,6 +89,11 @@ kotlin { implementation(kotlin("reflect")) } } -// val jvmTest by getting + + val jvmTest by getting { + dependencies { + implementation("io.kotest:kotest-runner-junit5-jvm:$kotestVersion") + } + } } } 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 12320fa4..14d09102 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 @@ -68,6 +68,7 @@ open class KxsTsGenerator( open fun generate(vararg serializers: KSerializer<*>): String { return serializers + .toSet() // 1. get all SerialDescriptors from a KSerializer .flatMap { serializer -> 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 45801db1..9a57a52a 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 @@ -17,7 +17,8 @@ abstract class KxsTsSourceCodeGenerator( is TsDeclaration.TsEnum -> generateEnum(element) is TsDeclaration.TsInterface -> generateInterface(element) is TsDeclaration.TsNamespace -> generateNamespace(element) - is TsDeclaration.TsTypeAlias -> generateType(element) + is TsDeclaration.TsTypeUnion -> generateTypeUnion(element) + is TsDeclaration.TsTypeAlias -> generateTypeAlias(element) is TsDeclaration.TsTuple -> generateTuple(element) } } @@ -25,7 +26,8 @@ abstract class KxsTsSourceCodeGenerator( 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 generateTypeAlias(element: TsDeclaration.TsTypeAlias): String + abstract fun generateTypeUnion(element: TsDeclaration.TsTypeUnion): String abstract fun generateTuple(tuple: TsDeclaration.TsTuple): String abstract fun generateMapTypeReference(tsMap: TsLiteral.TsMap): String @@ -117,17 +119,13 @@ abstract class KxsTsSourceCodeGenerator( } - override fun generateType(element: TsDeclaration.TsTypeAlias): String { - val aliases = - element.typeRefs - .map { generateTypeReference(it) } - .sorted() - .joinToString(" | ") + override fun generateTypeAlias(element: TsDeclaration.TsTypeAlias): String { + val aliasedRef = generateTypeReference(element.typeRef) return when (config.typeAliasTyping) { KxsTsConfig.TypeAliasTypingConfig.None -> """ - |export type ${element.id.name} = ${aliases}; + |export type ${element.id.name} = ${aliasedRef}; """.trimMargin() KxsTsConfig.TypeAliasTypingConfig.BrandTyping -> { @@ -141,12 +139,31 @@ abstract class KxsTsSourceCodeGenerator( }.joinToString("") """ - |export type ${element.id.name} = $aliases & { __${brandType}__: void }; + |export type ${element.id.name} = $aliasedRef & { __${brandType}__: void }; """.trimMargin() } } } + override fun generateTypeUnion(element: TsDeclaration.TsTypeUnion): String { + return if (element.typeRefs.isEmpty()) { + """ + |export type ${element.id.name} = ${generatePrimitive(TsLiteral.Primitive.TsUnknown)}; + """.trimMargin() + } else { + val aliases = element.typeRefs + .map { "${config.indent}| ${generateTypeReference(it)}" } + .sorted() + .joinToString("\n") + + """ + ¦export type ${element.id.name} = + ¦$aliases; + """.trimMargin("¦") + } + } + + override fun generateTuple(tuple: TsDeclaration.TsTuple): String { val types = tuple.elements.joinToString(separator = ", ") { diff --git a/modules/kxs-ts-gen-core/src/commonMain/kotlin/dev/adamko/kxstsgen/core/SerializerDescriptorsExtractor.kt b/modules/kxs-ts-gen-core/src/commonMain/kotlin/dev/adamko/kxstsgen/core/SerializerDescriptorsExtractor.kt index 41f6b0dd..d5b01c6b 100644 --- a/modules/kxs-ts-gen-core/src/commonMain/kotlin/dev/adamko/kxstsgen/core/SerializerDescriptorsExtractor.kt +++ b/modules/kxs-ts-gen-core/src/commonMain/kotlin/dev/adamko/kxstsgen/core/SerializerDescriptorsExtractor.kt @@ -66,10 +66,25 @@ fun interface SerializerDescriptorsExtractor { StructureKind.OBJECT -> descriptor.elementDescriptors PolymorphicKind.SEALED, - PolymorphicKind.OPEN -> descriptor - .elementDescriptors - .filter { it.kind is PolymorphicKind } - .flatMap { it.elementDescriptors } + PolymorphicKind.OPEN -> + // Polymorphic descriptors have 2 elements, the 'type' and 'value' - we don't need either + // for generation, they're metadata that will be used later. + // The elements of 'value' are similarly unneeded, but their elements might contain new + // descriptors - so extract them + descriptor.elementDescriptors + .flatMap { it.elementDescriptors } + .flatMap { it.elementDescriptors } + + // Example: + // com.application.Polymorphic + // ├── 'type' descriptor (ignore / it's a String, so check its elements, it doesn't hurt) + // └── 'value' descriptor (check elements...) + // ├── com.application.Polymorphic (ignore) + // │ ├── Double (extract!) + // │ └── com.application.SomeOtherClass (extract!) + // └── com.application.Polymorphic (ignore) + // ├── UInt (extract!) + // └── List( } private val indexedTupleElements = tupleElements.associateBy { it.index } - abstract fun tupleConstructor(elements: List<*>): T + abstract fun tupleConstructor(elements: Iterator<*>): T override val descriptor: SerialDescriptor = buildSerialDescriptor( serialName = serialName, @@ -123,7 +123,7 @@ abstract class TupleSerializer( generateSequence { val index = decodeElementIndex(descriptor) indexedTupleElements[index]?.decodeElement(this) - }.toList() + }.iterator() } return tupleConstructor(elements) } diff --git a/modules/kxs-ts-gen-core/src/commonMain/kotlin/dev/adamko/kxstsgen/core/tsElements.kt b/modules/kxs-ts-gen-core/src/commonMain/kotlin/dev/adamko/kxstsgen/core/tsElements.kt index 92b2e0e8..8961ded8 100644 --- a/modules/kxs-ts-gen-core/src/commonMain/kotlin/dev/adamko/kxstsgen/core/tsElements.kt +++ b/modules/kxs-ts-gen-core/src/commonMain/kotlin/dev/adamko/kxstsgen/core/tsElements.kt @@ -38,14 +38,18 @@ sealed interface TsDeclaration : TsElement { val id: TsElementId - /** A named reference to one or more other types. */ + /** A named reference to another type. */ data class TsTypeAlias( + override val id: TsElementId, + val typeRef: TsTypeRef, + ) : TsDeclaration + + + /** A named reference to one or more other types. */ + data class TsTypeUnion( override val id: TsElementId, val typeRefs: Set, - ) : TsDeclaration { - constructor(id: TsElementId, typeRef: TsTypeRef, vararg typeRefs: TsTypeRef) : - this(id, typeRefs.toSet() + typeRef) - } + ) : TsDeclaration /** A [tuple type](https://www.typescriptlang.org/docs/handbook/2/objects.html#tuple-types). */ diff --git a/modules/kxs-ts-gen-core/src/commonTest/kotlin/dev/adamko/kxstsgen/core/SerializerDescriptorsExtractorTest.kt b/modules/kxs-ts-gen-core/src/commonTest/kotlin/dev/adamko/kxstsgen/core/SerializerDescriptorsExtractorTest.kt new file mode 100644 index 00000000..4c1dd3b5 --- /dev/null +++ b/modules/kxs-ts-gen-core/src/commonTest/kotlin/dev/adamko/kxstsgen/core/SerializerDescriptorsExtractorTest.kt @@ -0,0 +1,80 @@ +package dev.adamko.kxstsgen.core + +import io.kotest.assertions.withClue +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.collections.shouldContainExactlyInAnyOrder +import kotlinx.serialization.Serializable +import kotlinx.serialization.builtins.serializer + +class SerializerDescriptorsExtractorTest : FunSpec({ + + test("Example1: given parent class, expect subclass property descriptor extracted") { + + val expected = listOf( + Example1.Parent.serializer().descriptor, + Example1.Nested.serializer().descriptor, + String.serializer().descriptor, + ) + + val actual = SerializerDescriptorsExtractor.Default(Example1.Parent.serializer()) + + withClue( + """ + expected: ${expected.map { it.serialName }.sorted().joinToString()} + actual: ${actual.map { it.serialName }.sorted().joinToString()} + """.trimIndent() + ) { + actual shouldContainExactlyInAnyOrder expected + } + } + + test("Example2: given parent class, expect subclass property descriptor extracted") { + + val expected = listOf( + Example2.Parent.serializer().descriptor, + Example2.Nested.serializer().descriptor, + String.serializer().descriptor, + ) + + val actual = SerializerDescriptorsExtractor.Default(Example2.Parent.serializer()) + + withClue( + """ + expected: ${expected.map { it.serialName }.sorted().joinToString()} + actual: ${actual.map { it.serialName }.sorted().joinToString()} + """.trimIndent() + ) { + actual shouldContainExactlyInAnyOrder expected + } + } + +}) + + +@Suppress("unused") +private object Example1 { + @Serializable + class Nested(val x: String) + + @Serializable + sealed class Parent + + @Serializable + class SubClass(val n: Nested) : Parent() +} + + +@Suppress("unused") +private object Example2 { + @Serializable + class Nested(val x: String) + + @Serializable + sealed class Parent + + @Serializable + sealed class SealedSub : Parent() + + @Serializable + class SubClass1(val n: Nested) : SealedSub() +}