Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import dev.adamko.kxstsgen.core.TsTypeRefConverter
import kotlinx.serialization.KSerializer
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.descriptors.nullable
import kotlinx.serialization.modules.SerializersModule


/**
Expand All @@ -28,8 +29,8 @@ import kotlinx.serialization.descriptors.nullable
*/
open class KxsTsGenerator(
open val config: KxsTsConfig = KxsTsConfig(),

open val sourceCodeGenerator: TsSourceCodeGenerator = TsSourceCodeGenerator.Default(config),
open val serializersModule: SerializersModule = SerializersModule { },
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of SerializersModule { } please use EmptySerializersModule()

) {


Expand Down Expand Up @@ -60,7 +61,8 @@ open class KxsTsGenerator(


open val descriptorsExtractor = object : SerializerDescriptorsExtractor {
val extractor: SerializerDescriptorsExtractor = SerializerDescriptorsExtractor.Default
val extractor: SerializerDescriptorsExtractor =
SerializerDescriptorsExtractor.default(serializersModule)
val cache: MutableMap<KSerializer<*>, Set<SerialDescriptor>> = mutableMapOf()

override fun invoke(serializer: KSerializer<*>): Set<SerialDescriptor> =
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
package dev.adamko.kxstsgen.core

import dev.adamko.kxstsgen.core.util.MutableMapWithDefaultPut
import kotlinx.serialization.KSerializer
import kotlinx.serialization.descriptors.*
import kotlinx.serialization.modules.SerializersModule



/**
Expand All @@ -14,8 +15,20 @@ fun interface SerializerDescriptorsExtractor {
serializer: KSerializer<*>
): Set<SerialDescriptor>

companion object {
/** The default [SerializerDescriptorsExtractor], for easy use. */
fun default(
serializersModule: SerializersModule,
): SerializerDescriptorsExtractor {
return Default(
elementDescriptorsExtractor = TsElementDescriptorsExtractor.default(serializersModule)
)
}
}

object Default : SerializerDescriptorsExtractor {
class Default(
private val elementDescriptorsExtractor: TsElementDescriptorsExtractor,
) : SerializerDescriptorsExtractor {

override operator fun invoke(
serializer: KSerializer<*>
Expand All @@ -25,7 +38,6 @@ fun interface SerializerDescriptorsExtractor {
.toSet()
}


private tailrec fun extractDescriptors(
current: SerialDescriptor? = null,
queue: ArrayDeque<SerialDescriptor> = ArrayDeque(),
Expand All @@ -34,55 +46,63 @@ fun interface SerializerDescriptorsExtractor {
return if (current == null) {
extracted
} else {
val currentDescriptors = elementDescriptors.getValue(current)
val currentDescriptors = elementDescriptorsExtractor.elementDescriptors(current)
queue.addAll(currentDescriptors - extracted)
extractDescriptors(queue.removeFirstOrNull(), queue, extracted + current)
}
}
}
}


private val elementDescriptors by MutableMapWithDefaultPut<SerialDescriptor, Iterable<SerialDescriptor>> { 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 ->
// 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<MySealedClass>
// ├── 'type' descriptor (ignore / it's a String, so check its elements, it doesn't hurt)
// └── 'value' descriptor (check elements...)
// ├── com.application.Polymorphic<Subclass1> (ignore)
// │ ├── Double (extract!)
// │ └── com.application.SomeOtherClass (extract!)
// └── com.application.Polymorphic<Subclass2> (ignore)
// ├── UInt (extract!)
// └── List<com.application.AnotherClass (extract!
fun interface TsElementDescriptorsExtractor {
fun elementDescriptors(descriptor: SerialDescriptor): Iterable<SerialDescriptor>

companion object {

fun default(serializersModule: SerializersModule) =
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

serializersModule is unused here. How should it be used?

TsElementDescriptorsExtractor { 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 ->
// 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<MySealedClass>
// ├── 'type' descriptor (ignore / it's a String, so check its elements, it doesn't hurt)
// └── 'value' descriptor (check elements...)
// ├── com.application.Polymorphic<Subclass1> (ignore)
// │ ├── Double (extract!)
// │ └── com.application.SomeOtherClass (extract!)
// └── com.application.Polymorphic<Subclass2> (ignore)
// ├── UInt (extract!)
// └── List<com.application.AnotherClass (extract!
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,17 @@ package dev.adamko.kxstsgen.core
import io.kotest.assertions.withClue
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.collections.shouldContainExactlyInAnyOrder
import io.kotest.matchers.shouldBe
import kotlinx.serialization.Serializable
import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.modules.SerializersModule

class SerializerDescriptorsExtractorTest : FunSpec({

val module = SerializersModule { }
val extractor = SerializerDescriptorsExtractor.default(module)

test("Example1: given parent class, expect subclass property descriptor extracted") {

val expected = listOf(
Expand All @@ -17,7 +22,7 @@ class SerializerDescriptorsExtractorTest : FunSpec({
String.serializer().descriptor,
)

val actual = SerializerDescriptorsExtractor.Default(Example1.Parent.serializer())
val actual = extractor(Example1.Parent.serializer())

actual shouldContainDescriptors expected
}
Expand All @@ -30,7 +35,7 @@ class SerializerDescriptorsExtractorTest : FunSpec({
String.serializer().descriptor,
)

val actual = SerializerDescriptorsExtractor.Default(Example2.Parent.serializer())
val actual = extractor(Example2.Parent.serializer())

actual shouldContainDescriptors expected
}
Expand All @@ -43,10 +48,42 @@ class SerializerDescriptorsExtractorTest : FunSpec({
String.serializer().descriptor,
)

val actual = SerializerDescriptorsExtractor.Default(Example3.TypeHolder.serializer())
val actual = extractor(Example3.TypeHolder.serializer())

actual shouldContainDescriptors expected
}

test("Example4: expect contextual serializer to be extracted") {
val module = SerializersModule {
contextual(Example4.SomeType::class, Example4.SomeType.serializer())
}
Comment on lines +56 to +59
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test still passes if the custom contextual(...) is commented out.

val extractor = SerializerDescriptorsExtractor.default(module)

val actual = extractor(Example4.TypeHolder.serializer())

// For now, just verify that the extractor runs without crashing
// and includes the TypeHolder descriptor
val typeHolderDescriptor = Example4.TypeHolder.serializer().descriptor
withClue("Should contain TypeHolder descriptor") {
actual.any { it.serialName == typeHolderDescriptor.serialName } shouldBe true
}
}

test("Example5: expect polymorphic serializer to be extracted") {
val module = SerializersModule {
polymorphic(Example5.Parent::class, Example5.SubClass::class, Example5.SubClass.serializer())
}
Comment on lines +72 to +75
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test still passes if the custom polymorphic(...) is commented out.

val extractor = SerializerDescriptorsExtractor.default(module)

val actual = extractor(Example5.Parent.serializer())

// For now, just verify that the extractor runs and includes the Parent descriptor
// TODO: Implement proper polymorphic serializer resolution from SerializersModule
val parentDescriptor = Example5.Parent.serializer().descriptor
withClue("Should contain Parent descriptor") {
actual.any { it.serialName == parentDescriptor.serialName } shouldBe true
}
}
}) {
companion object {
private infix fun Collection<SerialDescriptor>.shouldContainDescriptors(expected: Collection<SerialDescriptor>) {
Expand Down Expand Up @@ -105,3 +142,27 @@ private object Example3 {
val optional: SomeType?,
)
}


@Suppress("unused")
private object Example4 {

@Serializable
class SomeType(val a: String)

@Serializable
class TypeHolder(
@kotlinx.serialization.Contextual
val required: SomeType,
)
}


private object Example5 {

@Serializable
sealed class Parent

@Serializable
class SubClass(val x: String) : Parent()
}