[KxSerialization] Cover all combinations of language and serialization features with tests for enums

Resolves https://github.com/Kotlin/kotlinx.serialization/issues/2324


Merge-request: KT-MR-13775
Merged-by: Sergei Shanshin <Sergey.Shanshin@jetbrains.com>
This commit is contained in:
Sergey.Shanshin
2024-02-14 13:15:01 +00:00
committed by Space Team
parent e39af4583e
commit 9a8ebd5c21
13 changed files with 1162 additions and 0 deletions
+254
View File
@@ -0,0 +1,254 @@
// WITH_STDLIB
//
// NOTE: THIS FILE IS AUTO-GENERATED by the TestMatrixIntegration.kt, DO NOT EDIT!
//
@file:UseSerializers(EnumWithUseSerializer::class, NestedEnumWithUseSerializer::class, )
@file:UseContextualSerialization(EnumWithUseContextual::class, Container.NestedEnumWithUseContextual::class, )
import kotlinx.serialization.*
import kotlinx.serialization.descriptors.*
import kotlinx.serialization.json.*
import kotlinx.serialization.encoding.*
import kotlinx.serialization.modules.*
import kotlin.reflect.typeOf
enum class EnumWithDef {
A,
B,
}
@Serializable
enum class Enum {
A,
B,
}
@Serializable(EnumWithCustomSerializer::class)
enum class EnumWithCustom {
A,
B,
}
@Serializable(EnumWithCustomClSerializer::class)
enum class EnumWithCustomCl {
A,
B,
}
enum class EnumWithContextual {
A,
B,
}
enum class EnumWithUseContextual {
A,
B,
}
enum class EnumWithUse {
A,
B,
}
@Serializable
@Extra("Enum1")
enum class Enum1 {
@Extra("A") A,
@Extra("B") B,
}
class Container {
enum class NestedEnumWithDef {
A,
B,
}
@Serializable
enum class NestedEnum {
A,
B,
}
@Serializable(NestedEnumWithCustomSerializer::class)
enum class NestedEnumWithCustom {
A,
B,
}
@Serializable(NestedEnumWithCustomClSerializer::class)
enum class NestedEnumWithCustomCl {
A,
B,
}
enum class NestedEnumWithContextual {
A,
B,
}
enum class NestedEnumWithUseContextual {
A,
B,
}
enum class NestedEnumWithUse {
A,
B,
}
}
class Outer {
}
object EnumWithCustomSerializer: ToDoSerializer<EnumWithCustom>("custom|EnumWithCustom")
object NestedEnumWithCustomSerializer: ToDoSerializer<Container.NestedEnumWithCustom>("custom|Container.NestedEnumWithCustom")
class EnumWithCustomClSerializer: ToDoSerializer<EnumWithCustomCl>("custom|EnumWithCustomCl")
class NestedEnumWithCustomClSerializer: ToDoSerializer<Container.NestedEnumWithCustomCl>("custom|Container.NestedEnumWithCustomCl")
object EnumWithContextualSerializer: ToDoSerializer<EnumWithContextual>("contextual|EnumWithContextual")
object NestedEnumWithContextualSerializer: ToDoSerializer<Container.NestedEnumWithContextual>("contextual|Container.NestedEnumWithContextual")
object EnumWithUseContextualSerializer: ToDoSerializer<EnumWithUseContextual>("contextual|EnumWithUseContextual")
object NestedEnumWithUseContextualSerializer: ToDoSerializer<Container.NestedEnumWithUseContextual>("contextual|Container.NestedEnumWithUseContextual")
class EnumWithUseSerializer: ToDoSerializer<EnumWithUse>("useSerializer|EnumWithUse")
class NestedEnumWithUseSerializer: ToDoSerializer<Container.NestedEnumWithUse>("useSerializer|Container.NestedEnumWithUse")
@kotlinx.serialization.SerialInfo
@Target(AnnotationTarget.CLASS, AnnotationTarget.PROPERTY)
annotation class Extra(val value: String)
fun box(): String {
val module = SerializersModule {
contextual(EnumWithContextualSerializer)
contextual(NestedEnumWithContextualSerializer)
contextual(EnumWithUseContextualSerializer)
contextual(NestedEnumWithUseContextualSerializer)
}
serializer<EnumWithDef>().checkElements("A", "B")
serializer<Container.NestedEnumWithDef>().checkElements("A", "B")
serializer<Enum>().checkElements("A", "B")
serializer<Container.NestedEnum>().checkElements("A", "B")
serializer<EnumWithCustom>().checkElements("A", "B")
serializer<Container.NestedEnumWithCustom>().checkElements("A", "B")
serializer<EnumWithCustomCl>().checkElements("A", "B")
serializer<Container.NestedEnumWithCustomCl>().checkElements("A", "B")
serializer<EnumWithContextual>().checkElements("A", "B")
serializer<Container.NestedEnumWithContextual>().checkElements("A", "B")
serializer<EnumWithUseContextual>().checkElements("A", "B")
serializer<Container.NestedEnumWithUseContextual>().checkElements("A", "B")
serializer<EnumWithUse>().checkElements("A", "B")
serializer<Container.NestedEnumWithUse>().checkElements("A", "B")
serializer<Enum1>().checkElements("A", "B")
// Call serializer factory function in companion
Enum.serializer().checkSerialName("Enum")?.let { return it }
Container.NestedEnum.serializer().checkSerialName("Container.NestedEnum")?.let { return it }
EnumWithCustom.serializer().checkSerialName("custom|EnumWithCustom")?.let { return it }
Container.NestedEnumWithCustom.serializer().checkSerialName("custom|Container.NestedEnumWithCustom")?.let { return it }
EnumWithCustomCl.serializer().checkSerialName("custom|EnumWithCustomCl")?.let { return it }
Container.NestedEnumWithCustomCl.serializer().checkSerialName("custom|Container.NestedEnumWithCustomCl")?.let { return it }
Enum1.serializer().checkSerialName("Enum1")?.let { return it }
// Serializer lookup by generic parameter
serializer<EnumWithDef>().checkSerialName("EnumWithDef")?.let { return it }
serializer<Container.NestedEnumWithDef>().checkSerialName("Container.NestedEnumWithDef")?.let { return it }
serializer<Enum>().checkSerialName("Enum")?.let { return it }
serializer<Container.NestedEnum>().checkSerialName("Container.NestedEnum")?.let { return it }
serializer<EnumWithCustom>().checkSerialName("custom|EnumWithCustom")?.let { return it }
serializer<Container.NestedEnumWithCustom>().checkSerialName("custom|Container.NestedEnumWithCustom")?.let { return it }
serializer<EnumWithCustomCl>().checkSerialName("custom|EnumWithCustomCl")?.let { return it }
serializer<Container.NestedEnumWithCustomCl>().checkSerialName("custom|Container.NestedEnumWithCustomCl")?.let { return it }
// generated serializer used in lookup for the empty module in case of specifying of @file:UseContextualSerialization or @UseSerializers
serializer<EnumWithUseContextual>().checkSerialName("EnumWithUseContextual")?.let { return it }
// generated serializer used in lookup for the empty module in case of specifying of @file:UseContextualSerialization or @UseSerializers
serializer<Container.NestedEnumWithUseContextual>().checkSerialName("Container.NestedEnumWithUseContextual")?.let { return it }
// generated serializer used in lookup for the empty module in case of specifying of @file:UseContextualSerialization or @UseSerializers
serializer<EnumWithUse>().checkSerialName("EnumWithUse")?.let { return it }
// generated serializer used in lookup for the empty module in case of specifying of @file:UseContextualSerialization or @UseSerializers
serializer<Container.NestedEnumWithUse>().checkSerialName("Container.NestedEnumWithUse")?.let { return it }
serializer<Enum1>().checkSerialName("Enum1")?.let { return it }
// Serializer lookup by typeOf function
serializer(typeOf<EnumWithDef>()).checkSerialName("EnumWithDef")?.let { return it }
serializer(typeOf<Container.NestedEnumWithDef>()).checkSerialName("Container.NestedEnumWithDef")?.let { return it }
serializer(typeOf<Enum>()).checkSerialName("Enum")?.let { return it }
serializer(typeOf<Container.NestedEnum>()).checkSerialName("Container.NestedEnum")?.let { return it }
serializer(typeOf<EnumWithCustom>()).checkSerialName("custom|EnumWithCustom")?.let { return it }
serializer(typeOf<Container.NestedEnumWithCustom>()).checkSerialName("custom|Container.NestedEnumWithCustom")?.let { return it }
serializer(typeOf<EnumWithCustomCl>()).checkSerialName("custom|EnumWithCustomCl")?.let { return it }
serializer(typeOf<Container.NestedEnumWithCustomCl>()).checkSerialName("custom|Container.NestedEnumWithCustomCl")?.let { return it }
// generated serializer used in lookup for the empty module in case of specifying of @file:UseContextualSerialization or @UseSerializers
serializer(typeOf<EnumWithUseContextual>()).checkSerialName("EnumWithUseContextual")?.let { return it }
// generated serializer used in lookup for the empty module in case of specifying of @file:UseContextualSerialization or @UseSerializers
serializer(typeOf<Container.NestedEnumWithUseContextual>()).checkSerialName("Container.NestedEnumWithUseContextual")?.let { return it }
// generated serializer used in lookup for the empty module in case of specifying of @file:UseContextualSerialization or @UseSerializers
serializer(typeOf<EnumWithUse>()).checkSerialName("EnumWithUse")?.let { return it }
// generated serializer used in lookup for the empty module in case of specifying of @file:UseContextualSerialization or @UseSerializers
serializer(typeOf<Container.NestedEnumWithUse>()).checkSerialName("Container.NestedEnumWithUse")?.let { return it }
serializer(typeOf<Enum1>()).checkSerialName("Enum1")?.let { return it }
// Serializer lookup by generic parameter in custom module
module.serializer<EnumWithDef>().checkSerialName("EnumWithDef")?.let { return it }
module.serializer<Container.NestedEnumWithDef>().checkSerialName("Container.NestedEnumWithDef")?.let { return it }
module.serializer<Enum>().checkSerialName("Enum")?.let { return it }
module.serializer<Container.NestedEnum>().checkSerialName("Container.NestedEnum")?.let { return it }
module.serializer<EnumWithCustom>().checkSerialName("custom|EnumWithCustom")?.let { return it }
module.serializer<Container.NestedEnumWithCustom>().checkSerialName("custom|Container.NestedEnumWithCustom")?.let { return it }
module.serializer<EnumWithCustomCl>().checkSerialName("custom|EnumWithCustomCl")?.let { return it }
module.serializer<Container.NestedEnumWithCustomCl>().checkSerialName("custom|Container.NestedEnumWithCustomCl")?.let { return it }
// !!! for some reason, the generated serializer is still lookup for the custom module in case of specifying of @file:UseContextualSerialization or @UseSerializers
module.serializer<EnumWithUseContextual>().checkSerialName("EnumWithUseContextual")?.let { return it }
// !!! for some reason, the generated serializer is still lookup for the custom module in case of specifying of @file:UseContextualSerialization or @UseSerializers
module.serializer<Container.NestedEnumWithUseContextual>().checkSerialName("Container.NestedEnumWithUseContextual")?.let { return it }
// !!! for some reason, the generated serializer is still lookup for the custom module in case of specifying of @file:UseContextualSerialization or @UseSerializers
module.serializer<EnumWithUse>().checkSerialName("EnumWithUse")?.let { return it }
// !!! for some reason, the generated serializer is still lookup for the custom module in case of specifying of @file:UseContextualSerialization or @UseSerializers
module.serializer<Container.NestedEnumWithUse>().checkSerialName("Container.NestedEnumWithUse")?.let { return it }
module.serializer<Enum1>().checkSerialName("Enum1")?.let { return it }
// Serializer lookup by typeOf function in custom module
module.serializer(typeOf<EnumWithDef>()).checkSerialName("EnumWithDef")?.let { return it }
module.serializer(typeOf<Container.NestedEnumWithDef>()).checkSerialName("Container.NestedEnumWithDef")?.let { return it }
module.serializer(typeOf<Enum>()).checkSerialName("Enum")?.let { return it }
module.serializer(typeOf<Container.NestedEnum>()).checkSerialName("Container.NestedEnum")?.let { return it }
module.serializer(typeOf<EnumWithCustom>()).checkSerialName("custom|EnumWithCustom")?.let { return it }
module.serializer(typeOf<Container.NestedEnumWithCustom>()).checkSerialName("custom|Container.NestedEnumWithCustom")?.let { return it }
module.serializer(typeOf<EnumWithCustomCl>()).checkSerialName("custom|EnumWithCustomCl")?.let { return it }
module.serializer(typeOf<Container.NestedEnumWithCustomCl>()).checkSerialName("custom|Container.NestedEnumWithCustomCl")?.let { return it }
// !!! for some reason, the generated serializer is still lookup for the custom module in case of specifying of @file:UseContextualSerialization or @UseSerializers
module.serializer(typeOf<EnumWithUseContextual>()).checkSerialName("EnumWithUseContextual")?.let { return it }
// !!! for some reason, the generated serializer is still lookup for the custom module in case of specifying of @file:UseContextualSerialization or @UseSerializers
module.serializer(typeOf<Container.NestedEnumWithUseContextual>()).checkSerialName("Container.NestedEnumWithUseContextual")?.let { return it }
// !!! for some reason, the generated serializer is still lookup for the custom module in case of specifying of @file:UseContextualSerialization or @UseSerializers
module.serializer(typeOf<EnumWithUse>()).checkSerialName("EnumWithUse")?.let { return it }
// !!! for some reason, the generated serializer is still lookup for the custom module in case of specifying of @file:UseContextualSerialization or @UseSerializers
module.serializer(typeOf<Container.NestedEnumWithUse>()).checkSerialName("Container.NestedEnumWithUse")?.let { return it }
module.serializer(typeOf<Enum1>()).checkSerialName("Enum1")?.let { return it }
// Annotation on type should have value same as a class name
serializer<Enum1>().checkAnnotation("Enum1")
// Annotation on enum entries should have value same as a entry names
serializer<Enum1>().checkElementAnnotations("A", "B")
return "OK"
}
fun KSerializer<*>.checkSerialName(name: String): String? = if (descriptor.serialName != name) "Wrong serial name: Expected '$name' actual '${descriptor.serialName}'" else null
fun KSerializer<*>.checkAnnotation(value: String): String? = descriptor.annotations.filterIsInstance<Extra>().single().value.let { if (it != value) it else null }
fun KSerializer<*>.checkElementAnnotations(vararg values: String): String? = (0 ..< descriptor.elementsCount).map { descriptor.getElementAnnotations(it).filterIsInstance<Extra>().single().value}.let { if (it != values.toList()) it.toString() else null }
fun KSerializer<*>.checkElements(vararg values: String): String? = (0 ..< descriptor.elementsCount).map { descriptor.getElementName(it) }.let { if (it != values.toList()) it.toString() else null }
abstract class ToDoSerializer<T: Any>(descriptorName: String): KSerializer<T> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor(descriptorName, PrimitiveKind.STRING)
override fun deserialize(decoder: Decoder): T = TODO()
override fun serialize(encoder: Encoder, value: T) = TODO()
}
@@ -0,0 +1,33 @@
/*
* Copyright 2010-2024 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlinx.serialization.matrix;
import com.intellij.testFramework.TestDataPath;
import org.jetbrains.kotlin.test.util.KtTestUtil;
import org.jetbrains.kotlin.test.TargetBackend;
import org.jetbrains.kotlin.test.TestMetadata;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import java.io.File;
import java.util.regex.Pattern;
/** This class is generated by {@link org.jetbrains.kotlinx.serialization.TestGeneratorKt}. DO NOT MODIFY MANUALLY */
@SuppressWarnings("all")
@TestMetadata("plugins/kotlinx-serialization/testData/matrix")
@TestDataPath("$PROJECT_ROOT")
public class FirTestMatrixGenerated extends AbstractFirTestMatrix {
@Test
public void testAllFilesPresentInMatrix() throws Exception {
KtTestUtil.assertAllTestsPresentByMetadataWithExcluded(this.getClass(), new File("plugins/kotlinx-serialization/testData/matrix"), Pattern.compile("^(.+)\\.kt$"), null, TargetBackend.JVM_IR, true);
}
@Test
@TestMetadata("enums.kt")
public void testEnums() throws Exception {
runTest("plugins/kotlinx-serialization/testData/matrix/enums.kt");
}
}
@@ -0,0 +1,33 @@
/*
* Copyright 2010-2024 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlinx.serialization.matrix;
import com.intellij.testFramework.TestDataPath;
import org.jetbrains.kotlin.test.util.KtTestUtil;
import org.jetbrains.kotlin.test.TargetBackend;
import org.jetbrains.kotlin.test.TestMetadata;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import java.io.File;
import java.util.regex.Pattern;
/** This class is generated by {@link org.jetbrains.kotlinx.serialization.TestGeneratorKt}. DO NOT MODIFY MANUALLY */
@SuppressWarnings("all")
@TestMetadata("plugins/kotlinx-serialization/testData/matrix")
@TestDataPath("$PROJECT_ROOT")
public class TestMatrixGenerated extends AbstractTestMatrix {
@Test
public void testAllFilesPresentInMatrix() throws Exception {
KtTestUtil.assertAllTestsPresentByMetadataWithExcluded(this.getClass(), new File("plugins/kotlinx-serialization/testData/matrix"), Pattern.compile("^(.+)\\.kt$"), null, TargetBackend.JVM_IR, true);
}
@Test
@TestMetadata("enums.kt")
public void testEnums() throws Exception {
runTest("plugins/kotlinx-serialization/testData/matrix/enums.kt");
}
}
@@ -7,6 +7,8 @@ package org.jetbrains.kotlinx.serialization
import org.jetbrains.kotlin.generators.generateTestGroupSuiteWithJUnit5
import org.jetbrains.kotlin.generators.util.TestGeneratorUtil
import org.jetbrains.kotlinx.serialization.matrix.cases.enumsTestMatrix
import org.jetbrains.kotlinx.serialization.matrix.testMatrix
import org.jetbrains.kotlinx.serialization.runners.*
fun main(args: Array<String>) {
@@ -77,6 +79,10 @@ fun main(args: Array<String>) {
testClass<AbstractSerializationFirJsBoxTest> {
model("boxIr")
}
testMatrix {
add("enums") { enumsTestMatrix() }
}
}
}
}
@@ -0,0 +1,37 @@
/*
* Copyright 2010-2024 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlinx.serialization.matrix
interface CombinationContext {
fun defineEnums(
serializers: Set<SerializerKind>,
locations: Set<TypeLocation>,
optionsConfig: EnumOptionsBuilder.() -> Unit = {},
): Set<EnumVariant>
fun function(name: String, block: FunctionContext.() -> Unit)
fun box(block: FunctionContext.() -> Unit)
fun generate(appendable: Appendable, generator: String)
val TypeVariant.named: NamedTypeVariant
}
interface FunctionContext {
fun line(code: String = "")
}
interface EnumOptionsBuilder {
fun serialInfo(vararg serialInfo: SerialInfo)
fun descriptorAccessing(vararg descriptorAccessing: DescriptorAccessing)
fun entries(vararg entries: String)
}
@@ -0,0 +1,47 @@
/*
* Copyright 2010-2024 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlinx.serialization.matrix
import org.jetbrains.kotlinx.serialization.matrix.SerializerKind.BY_DEFAULT
internal fun hasFactoryFun(type: TypeVariant): Boolean {
return type.features.serializer in setOf(
SerializerKind.GENERATED,
SerializerKind.CUSTOM_OBJECT,
SerializerKind.CUSTOM_CLASS
)
}
internal fun canBeUsedInLookup(type: TypeVariant): Boolean {
return type.features.serializer in setOf(
SerializerKind.GENERATED,
SerializerKind.CUSTOM_OBJECT,
SerializerKind.CUSTOM_CLASS,
SerializerKind.CLASS_USE_SERIALIZER,
SerializerKind.USE_CONTEXTUAL
) || (type is EnumVariant && type.features.serializer == BY_DEFAULT)
}
internal fun canBeUsedInModuleLookup(type: TypeVariant): Boolean {
return type.features.serializer in setOf(
SerializerKind.GENERATED,
SerializerKind.CUSTOM_OBJECT,
SerializerKind.CUSTOM_CLASS,
SerializerKind.CLASS_USE_SERIALIZER,
SerializerKind.USE_CONTEXTUAL
) || (type is EnumVariant && type.features.serializer == BY_DEFAULT)
}
internal fun shouldAddContextualSerializerToModule(type: TypeVariant): Boolean {
return type.features.serializer == SerializerKind.CONTEXTUAL || type.features.serializer == SerializerKind.USE_CONTEXTUAL
}
internal fun hasAnnotationOnType(type: TypeVariant): Boolean {
return type is EnumVariant && SerialInfo.ON_TYPE in type.options.serialInfo
}
internal fun hasAnnotationOnElement(type: TypeVariant): Boolean {
return type is EnumVariant && SerialInfo.ON_ELEMENTS in type.options.serialInfo
}
@@ -0,0 +1,66 @@
/*
* Copyright 2010-2024 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlinx.serialization.matrix
import org.jetbrains.kotlin.generators.TestGroup
import org.jetbrains.kotlin.incremental.deleteDirectoryContents
import org.jetbrains.kotlin.test.builders.TestConfigurationBuilder
import org.jetbrains.kotlin.test.runners.codegen.AbstractFirLightTreeBlackBoxCodegenTest
import org.jetbrains.kotlin.test.runners.codegen.AbstractIrBlackBoxCodegenTest
import org.jetbrains.kotlinx.serialization.matrix.impl.CombinationContextImpl
import org.jetbrains.kotlinx.serialization.configureForKotlinxSerialization
import java.io.File
internal fun TestGroup.testMatrix(casesBlock: TestCaseContext.() -> Unit) {
val relativeRootPath = "matrix"
val dir = File("$testDataRoot/$relativeRootPath")
dir.mkdirs()
dir.deleteDirectoryContents()
val caseContext = TestCaseContext()
caseContext.casesBlock()
caseContext.testCases.forEach { (caseName, block) ->
val combinationContext = CombinationContextImpl()
combinationContext.block()
combinationContext.generateInto(dir.resolve("$caseName.kt"))
}
testClass<AbstractTestMatrix> {
model(relativeRootPath)
}
testClass<AbstractFirTestMatrix> {
model(relativeRootPath)
}
}
internal class TestCaseContext {
val testCases: MutableMap<String, CombinationContext.() -> Unit> = mutableMapOf()
fun add(name: String, block: CombinationContext.() -> Unit) {
testCases[name] = block
}
}
internal open class AbstractTestMatrix : AbstractIrBlackBoxCodegenTest() {
override fun configure(builder: TestConfigurationBuilder) {
super.configure(builder)
builder.configureForKotlinxSerialization()
}
}
internal open class AbstractFirTestMatrix : AbstractFirLightTreeBlackBoxCodegenTest() {
override fun configure(builder: TestConfigurationBuilder) {
super.configure(builder)
builder.configureForKotlinxSerialization()
}
}
private fun CombinationContext.generateInto(file: File) {
file.writer().use {
generate(it, "TestMatrixIntegration.kt")
}
}
@@ -0,0 +1,140 @@
/*
* Copyright 2010-2024 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlinx.serialization.matrix
import org.jetbrains.kotlinx.serialization.matrix.impl.CLASS_FOR_NESTED
internal fun Set<TypeVariant>.filterEnums(predicate: (EnumVariant) -> Boolean): Set<EnumVariant> {
val filtered = filterIsInstance<EnumVariant>().filter { variant -> predicate(variant) }
return filtered.toSet()
}
/**
* A combination of mutually exclusive Kotlin language and serialization features.
*/
sealed class TypeFeatures {
abstract val serializer: SerializerKind
abstract val location: TypeLocation
}
/**
* Description of the type used to generate type definitions and their uses.
*
* Parent type.
*/
sealed class TypeVariant {
/**
* A combination of mutually exclusive Kotlin language and serialization features.
*/
abstract val features: TypeFeatures
}
/**
* Description of the enum types used to generate type definitions and their uses.
*/
data class EnumVariant(
override val features: EnumFeatures,
val options: EnumOptions,
) : TypeVariant()
/**
* A combination of mutually exclusive Kotlin language and serialization features for enum types.
*/
data class EnumFeatures(
override val serializer: SerializerKind,
override val location: TypeLocation,
) : TypeFeatures()
/**
* Optional features for the type.
*/
data class EnumOptions(
val serialInfo: Set<SerialInfo>,
val descriptorAccessing: Set<DescriptorAccessing>,
val entries: Set<String>,
)
enum class SerializerKind {
/**
* Serializable by default (Without @Serializable annotation: enum, interface, sealed interface)
*/
BY_DEFAULT,
/**
* @Serializable
*/
GENERATED,
/**
* @Serializable(CustomObjectSerializer::class)
*/
CUSTOM_OBJECT,
/**
* @Serializable(CustomSerializer::class)
*/
CUSTOM_CLASS,
/**
* Contextual by SerialModule.
*/
CONTEXTUAL,
/**
* Classes marked by file-level annotation @file:UseContextualSerialization
*/
USE_CONTEXTUAL,
/**
* Serializable by @UseSerializers
*/
CLASS_USE_SERIALIZER
}
/**
* Location of the type definition.
*/
enum class TypeLocation {
FILE_ROOT,
LOCAL,
NESTED,
}
enum class SerialInfo {
ON_TYPE,
ON_ELEMENTS
}
/**
* Kotlin and serialization feature that are not mutually exclusive and there may be several features of the same kind.
*/
interface TypeOptionalFeature
enum class DescriptorAccessing : TypeOptionalFeature {
FROM_INIT,
FROM_COMPANION_INIT,
FROM_COMPANION_PROPERTY_INIT
}
/**
* A pair in the description of the type and the name that was assigned to it during generation.
*/
class NamedTypeVariant(val name: String, val variant: TypeVariant) {
internal val classUsage: String
get() {
return when (variant.features.location) {
TypeLocation.FILE_ROOT -> name
TypeLocation.LOCAL -> name
TypeLocation.NESTED -> "$CLASS_FOR_NESTED.$name"
}
}
}
@@ -0,0 +1,126 @@
/*
* Copyright 2010-2024 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlinx.serialization.matrix.cases
import org.jetbrains.kotlinx.serialization.matrix.SerializerKind.*
import org.jetbrains.kotlinx.serialization.matrix.TypeLocation.FILE_ROOT
import org.jetbrains.kotlinx.serialization.matrix.TypeLocation.NESTED
import org.jetbrains.kotlinx.serialization.matrix.*
import org.jetbrains.kotlinx.serialization.matrix.impl.*
import org.jetbrains.kotlinx.serialization.matrix.impl.elements
import org.jetbrains.kotlinx.serialization.matrix.impl.serialName
fun CombinationContext.enumsTestMatrix() {
val enums = defineEnums(
SerializerKind.entries.toSet(),
setOf(FILE_ROOT, NESTED)
) {
entries("A", "B")
descriptorAccessing(*DescriptorAccessing.entries.toTypedArray())
}
val enumsWithAnnotations = defineEnums(
setOf(GENERATED),
setOf(FILE_ROOT)
) {
entries("A", "B")
serialInfo(*SerialInfo.entries.toTypedArray())
}
val allTypes = enums + enumsWithAnnotations
val withCompanion = allTypes.filter(::hasFactoryFun)
val lookupTypes = allTypes.filter(::canBeUsedInLookup)
val moduleLookupTypes = allTypes.filter(::canBeUsedInModuleLookup)
val annotatedTypes = allTypes.filter(::hasAnnotationOnType)
val annotatedElementsTypes = allTypes.filter(::hasAnnotationOnElement)
box() {
line("val module = SerializersModule {")
allTypes.filter(::shouldAddContextualSerializerToModule).forEach { type ->
line(" contextual(${type.named.serializerName})")
}
line("}")
line()
allTypes.forEach { type ->
line("serializer<${type.named.classUsage}>().checkElements(${
type.elements.joinToString(", ") { "\"$it\"" }
})")
}
line()
line("// Call serializer factory function in companion")
withCompanion.forEach { type ->
line("${type.named.classUsage}.serializer().checkSerialName(\"${type.named.serialName}\")?.let { return it }")
}
line()
line("// Serializer lookup by generic parameter")
lookupTypes.forEach { type ->
val serialName = if (type.features.serializer == CLASS_USE_SERIALIZER || type.features.serializer == USE_CONTEXTUAL) {
line("// generated serializer used in lookup for the empty module in case of specifying of @file:UseContextualSerialization or @UseSerializers")
type.named.classUsage
} else {
type.named.serialName
}
line("serializer<${type.named.classUsage}>().checkSerialName(\"$serialName\")?.let { return it }")
}
line()
line("// Serializer lookup by typeOf function")
lookupTypes.forEach { type ->
val serialName = if (type.features.serializer == CLASS_USE_SERIALIZER || type.features.serializer == USE_CONTEXTUAL) {
line("// generated serializer used in lookup for the empty module in case of specifying of @file:UseContextualSerialization or @UseSerializers")
type.named.classUsage
} else {
type.named.serialName
}
line("serializer(typeOf<${type.named.classUsage}>()).checkSerialName(\"$serialName\")?.let { return it }")
}
line()
line("// Serializer lookup by generic parameter in custom module")
moduleLookupTypes.forEach { type ->
val serialName = if (type.features.serializer == CLASS_USE_SERIALIZER || type.features.serializer == USE_CONTEXTUAL) {
line("// !!! for some reason, the generated serializer is still lookup for the custom module in case of specifying of @file:UseContextualSerialization or @UseSerializers")
type.named.classUsage
} else {
type.named.serialName
}
line("module.serializer<${type.named.classUsage}>().checkSerialName(\"$serialName\")?.let { return it }")
}
line()
line("// Serializer lookup by typeOf function in custom module")
moduleLookupTypes.forEach { type ->
val serialName = if (type.features.serializer == CLASS_USE_SERIALIZER || type.features.serializer == USE_CONTEXTUAL) {
line("// !!! for some reason, the generated serializer is still lookup for the custom module in case of specifying of @file:UseContextualSerialization or @UseSerializers")
type.named.classUsage
} else {
type.named.serialName
}
line("module.serializer(typeOf<${type.named.classUsage}>()).checkSerialName(\"$serialName\")?.let { return it }")
}
line()
line("// Annotation on type should have value same as a class name")
annotatedTypes.forEach { type ->
line("serializer<${type.named.classUsage}>().checkAnnotation(\"${type.named.classUsage}\")")
}
line()
line("// Annotation on enum entries should have value same as a entry names")
annotatedElementsTypes.forEach { type ->
line("serializer<${type.named.classUsage}>().checkElementAnnotations(${
type.elements.joinToString(", ") { "\"$it\"" }
})")
}
line()
}
}
@@ -0,0 +1,90 @@
/*
* Copyright 2010-2024 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlinx.serialization.matrix.impl
import org.jetbrains.kotlinx.serialization.matrix.*
internal class CombinationContextImpl : CombinationContext {
private val typesByFeatures = mutableMapOf<TypeFeatures, MutableList<TypeVariant>>()
private val types: MutableMap<TypeVariant, String> = mutableMapOf()
private val functions: MutableMap<String, FunctionContext.() -> Unit> = mutableMapOf()
override fun defineEnums(
serializers: Set<SerializerKind>,
locations: Set<TypeLocation>,
optionsConfig: EnumOptionsBuilder.() -> Unit,
): Set<EnumVariant> {
val enumTypes: MutableSet<EnumVariant> = mutableSetOf()
val options = EnumOptionsBuilderImpl().also { it.optionsConfig() }.build()
for (serializer in serializers) {
for (location in locations) {
val features = EnumFeatures(serializer, location)
enumTypes += EnumVariant(features, options)
}
}
addTypes(enumTypes)
return enumTypes
}
override fun function(name: String, block: FunctionContext.() -> Unit) {
functions[name] = block
}
override fun box(block: FunctionContext.() -> Unit) {
functions["box(): String"] = {
block()
line("return \"OK\"")
}
}
override fun generate(appendable: Appendable, generator: String) {
val namedTypes = types.map { (type, name) -> NamedTypeVariant(name, type) }
appendable.writeHeader(namedTypes, generator)
appendable.writeTypes(namedTypes)
functions.forEach { (signature, builder) ->
appendable.writeFunction(signature, builder)
}
appendable.writeUtils()
}
override val TypeVariant.named: NamedTypeVariant
get() = NamedTypeVariant(types[this] ?: throw Exception("Type variant wasn't defined properly $this"), this)
private fun addTypes(types: Iterable<TypeVariant>) {
for (type in types) {
val typesForFeatures = typesByFeatures.getOrPut(type.features) { mutableListOf() }
val className = type.className + if (typesForFeatures.size > 0) typesForFeatures.size.toString() else ""
typesForFeatures += type
this.types[type] = className
}
}
}
private class EnumOptionsBuilderImpl : EnumOptionsBuilder {
private val serialInfoP: MutableSet<SerialInfo> = mutableSetOf()
private val descriptorAccessingP: MutableSet<DescriptorAccessing> = mutableSetOf()
private val entriesP: MutableSet<String> = mutableSetOf()
override fun serialInfo(vararg serialInfo: SerialInfo) {
serialInfo.forEach { serialInfoP.add(it) }
}
override fun descriptorAccessing(vararg descriptorAccessing: DescriptorAccessing) {
descriptorAccessing.forEach { descriptorAccessingP.add(it) }
}
override fun entries(vararg entries: String) {
entries.forEach { entriesP.add(it) }
}
fun build(): EnumOptions {
return EnumOptions(serialInfoP, descriptorAccessingP, entriesP)
}
}
@@ -0,0 +1,24 @@
/*
* Copyright 2010-2024 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlinx.serialization.matrix.impl
import org.jetbrains.kotlinx.serialization.matrix.FunctionContext
internal fun Appendable.writeFunction(signature: String, builder: FunctionContext.() -> Unit) {
appendLine("fun $signature {")
val context = object : FunctionContext {
override fun line(code: String) {
append(" ")
appendLine(code)
}
}
context.builder()
appendLine("}")
appendLine()
}
@@ -0,0 +1,234 @@
/*
* Copyright 2010-2024 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlinx.serialization.matrix.impl
import org.jetbrains.kotlinx.serialization.matrix.EnumVariant
import org.jetbrains.kotlinx.serialization.matrix.NamedTypeVariant
import org.jetbrains.kotlinx.serialization.matrix.SerialInfo
import org.jetbrains.kotlinx.serialization.matrix.SerializerKind.*
import org.jetbrains.kotlinx.serialization.matrix.TypeLocation.*
/*
<file level declarations>
class Container {
<nested declarations>
}
class Outer {
<inner declarations>
}
fun <function name> () {
<local classes>
<function declaration>
}
*/
internal fun Appendable.writeTypes(types: List<NamedTypeVariant>) {
// file-level
types.filter { type -> type.variant.features.location == FILE_ROOT }.forEach { type ->
writeTypeDef(type)
}
// nested
append("class ")
append(CLASS_FOR_NESTED)
append(" {\n")
types.filter { named -> named.variant.features.location == NESTED }.forEach { type ->
writeTypeDef(type, " ")
}
append("}\n\n")
// write inner
append("class ")
append(CLASS_FOR_INNER)
append(" {\n")
types.filter { named -> named.variant.features.location == LOCAL }.forEach { type ->
writeTypeDef(type, " ")
}
append("}\n\n")
// write custom serializers
types.forEach { type ->
writeCustomSerializer(type)
}
appendLine()
// write contextual serializers
types.forEach { type ->
writeContextualSerializer(type)
}
appendLine()
// write use serializers
types.forEach { type ->
writeUseSerializer(type)
}
appendLine()
writeSerialInfo()
appendLine()
}
internal fun Appendable.writeHeader(types: List<NamedTypeVariant>, generator: String) {
appendLine("// WITH_STDLIB")
appendLine()
appendLine("//")
appendLine("// NOTE: THIS FILE IS AUTO-GENERATED by the $generator, DO NOT EDIT!")
appendLine("//")
appendLine()
writeUseSerializers(types)
writeUseContextualSerializers(types)
appendLine()
appendLine("import kotlinx.serialization.*")
appendLine("import kotlinx.serialization.descriptors.*")
appendLine("import kotlinx.serialization.json.*")
appendLine("import kotlinx.serialization.encoding.*")
appendLine("import kotlinx.serialization.modules.*")
appendLine("import kotlin.reflect.typeOf")
appendLine()
}
internal fun Appendable.writeUtils() {
append("fun KSerializer<*>.checkSerialName(name: String): String? = if (descriptor.serialName != name) \"Wrong serial name: Expected '\$name' actual '\${descriptor.serialName}'\" else null\n")
append("fun KSerializer<*>.checkAnnotation(value: String): String? = descriptor.annotations.filterIsInstance<Extra>().single().value.let { if (it != value) it else null }\n\n")
append("fun KSerializer<*>.checkElementAnnotations(vararg values: String): String? = (0 ..< descriptor.elementsCount).map { descriptor.getElementAnnotations(it).filterIsInstance<Extra>().single().value}.let { if (it != values.toList()) it.toString() else null }\n\n")
append("fun KSerializer<*>.checkElements(vararg values: String): String? = (0 ..< descriptor.elementsCount).map { descriptor.getElementName(it) }.let { if (it != values.toList()) it.toString() else null }\n\n")
// write abstract serializer
appendLine(TODO_SERIALIZER)
}
private fun Appendable.writeTypeDef(named: NamedTypeVariant, indent: String = "") {
when (named.variant) {
is EnumVariant -> writeEnumDef(named, indent)
}
}
private fun Appendable.writeEnumDef(named: NamedTypeVariant, indent: String) {
val enum = named.variant as EnumVariant
if (enum.features.location == LOCAL) {
throw IllegalArgumentException("Local enums are not allowed, can't generate test code.")
}
val classUsage = named.classUsage
if (enum.features.serializer == GENERATED) {
append(indent)
appendLine("@Serializable")
}
if (enum.features.serializer == CUSTOM_OBJECT || enum.features.serializer == CUSTOM_CLASS) {
append(indent)
appendLine("@Serializable(${named.serializerName}::class)")
}
if (SerialInfo.ON_TYPE in enum.options.serialInfo) {
append(indent)
appendLine("@$SERIAL_ANNOTATION(\"$classUsage\")")
}
append(indent)
appendLine("enum class ${named.name} {")
enum.options.entries.forEach { entry ->
append(indent)
append(" ")
if (SerialInfo.ON_ELEMENTS in enum.options.serialInfo) {
append("@$SERIAL_ANNOTATION(\"$entry\") ")
}
append(entry)
appendLine(",")
}
append(indent)
appendLine("}")
appendLine()
}
private fun Appendable.writeUseSerializers(types: List<NamedTypeVariant>) {
val serializers = types.mapNotNull { type -> type.useSerializer }
if (serializers.isEmpty()) {
return
}
append("@file:UseSerializers(")
serializers.forEach { name ->
append(name)
append("::class, ")
}
appendLine(")")
}
private fun Appendable.writeUseContextualSerializers(types: List<NamedTypeVariant>) {
val types = types.filter { type -> type.useContextualSerializer != null }
if (types.isEmpty()) {
return
}
append("@file:UseContextualSerialization(")
types.forEach { type ->
append(type.classUsage)
append("::class, ")
}
appendLine(")")
}
private fun Appendable.writeSerialInfo() {
appendLine("@kotlinx.serialization.SerialInfo")
appendLine("@Target(AnnotationTarget.CLASS, AnnotationTarget.PROPERTY)")
appendLine("annotation class $SERIAL_ANNOTATION(val value: String)")
}
private fun Appendable.writeCustomSerializer(type: NamedTypeVariant) {
when (type.variant.features.serializer) {
CUSTOM_CLASS -> {
appendLine("class ${type.serializerName}: ToDoSerializer<${type.classUsage}>(\"${type.serialName}\")")
}
CUSTOM_OBJECT -> {
appendLine("object ${type.serializerName}: ToDoSerializer<${type.classUsage}>(\"${type.serialName}\")")
}
else -> Unit // no-op
}
}
private fun Appendable.writeContextualSerializer(type: NamedTypeVariant) {
when (type.variant.features.serializer) {
CONTEXTUAL, USE_CONTEXTUAL -> {
val usage = type.classUsage
appendLine("object ${type.serializerName}: ToDoSerializer<$usage>(\"${type.serialName}\")")
}
else -> Unit // no-op
}
}
private fun Appendable.writeUseSerializer(type: NamedTypeVariant) {
when (type.variant.features.serializer) {
CLASS_USE_SERIALIZER -> {
val usage = type.classUsage
appendLine("class ${type.useSerializer}: ToDoSerializer<$usage>(\"${type.serialName}\")")
}
else -> Unit // no-op
}
}
private const val TODO_SERIALIZER_NAME = "ToDoSerializer"
private const val TODO_SERIALIZER = """
abstract class $TODO_SERIALIZER_NAME<T: Any>(descriptorName: String): KSerializer<T> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor(descriptorName, PrimitiveKind.STRING)
override fun deserialize(decoder: Decoder): T = TODO()
override fun serialize(encoder: Encoder, value: T) = TODO()
}
"""
@@ -0,0 +1,72 @@
/*
* Copyright 2010-2024 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlinx.serialization.matrix.impl
import org.jetbrains.kotlinx.serialization.matrix.*
import org.jetbrains.kotlinx.serialization.matrix.SerializerKind.*
internal const val CLASS_FOR_NESTED = "Container"
internal const val CLASS_FOR_INNER = "Outer"
internal const val SERIAL_ANNOTATION = "Extra"
internal val TypeVariant.className: String
get() = when (this) {
is EnumVariant -> features.location.namePart + "Enum" + features.serializer.namePart
}
internal val TypeVariant.elements: List<String>
get() = when (this) {
is EnumVariant -> options.entries.toList()
}
internal val NamedTypeVariant.serialName: String
get() = when (variant.features.serializer) {
CUSTOM_CLASS, CUSTOM_OBJECT -> "custom|$classUsage"
CONTEXTUAL, USE_CONTEXTUAL -> "contextual|$classUsage"
CLASS_USE_SERIALIZER -> "useSerializer|$classUsage"
else -> classUsage
}
internal val NamedTypeVariant.useSerializer: String?
get() {
if (variant.features.serializer != CLASS_USE_SERIALIZER) return null
return serializerName
}
internal val NamedTypeVariant.useContextualSerializer: String?
get() {
if (variant.features.serializer != USE_CONTEXTUAL) return null
return serializerName
}
internal val NamedTypeVariant.serializerName: String
get() = when (variant.features.serializer) {
CUSTOM_CLASS, CUSTOM_OBJECT, CLASS_USE_SERIALIZER, CONTEXTUAL, USE_CONTEXTUAL -> name + "Serializer"
BY_DEFAULT -> throw IllegalStateException("No named serializer for type serializable by default '$name'")
GENERATED -> throw IllegalStateException("No named serializer for type '$name' with automatically generated serializer")
}
private val TypeLocation.namePart: String
get() = when (this) {
TypeLocation.FILE_ROOT -> ""
TypeLocation.LOCAL -> "Local"
TypeLocation.NESTED -> "Nested"
}
private val SerializerKind.namePart: String
get() = when (this) {
BY_DEFAULT -> "WithDef"
GENERATED -> ""
CUSTOM_OBJECT -> "WithCustom"
CUSTOM_CLASS -> "WithCustomCl"
CONTEXTUAL -> "WithContextual"
USE_CONTEXTUAL -> "WithUseContextual"
CLASS_USE_SERIALIZER -> "WithUse"
}