From 28e0fff2e705cbce7c1d79200503532f2747b9bc Mon Sep 17 00:00:00 2001 From: "Sergey.Shanshin" Date: Mon, 14 Dec 2020 11:58:15 +0300 Subject: [PATCH] Generator for .proto files based on serializable Kotlin classes (Resolves #34) --- ...tlinx-serialization-protobuf-generator.api | 4 + formats/protobuf-generator/build.gradle.kts | 18 + .../Generation.kt | 383 ++++++++++++++++++ .../jvmTest/resources/AbstractHolder.proto | 15 + .../jvmTest/resources/ContextualHolder.proto | 9 + .../jvmTest/resources/FieldNumberClass.proto | 10 + .../jvmTest/resources/ListClass.proto | 23 ++ .../jvmTest/resources/MapClass.proto | 22 + .../jvmTest/resources/OptionalClass.proto | 13 + .../jvmTest/resources/OptionsClass.proto | 10 + .../jvmTest/resources/ScalarHolder.proto | 21 + .../jvmTest/resources/SealedHolder.proto | 27 ++ .../jvmTest/resources/SerialNameClass.proto | 16 + .../protobuf/generator/GenerationTest.kt | 196 +++++++++ settings.gradle | 3 + 15 files changed, 770 insertions(+) create mode 100644 formats/protobuf-generator/api/kotlinx-serialization-protobuf-generator.api create mode 100644 formats/protobuf-generator/build.gradle.kts create mode 100644 formats/protobuf-generator/commonMain/src/kotlinx.serialization.protobuf.generator/Generation.kt create mode 100644 formats/protobuf-generator/jvmTest/resources/AbstractHolder.proto create mode 100644 formats/protobuf-generator/jvmTest/resources/ContextualHolder.proto create mode 100644 formats/protobuf-generator/jvmTest/resources/FieldNumberClass.proto create mode 100644 formats/protobuf-generator/jvmTest/resources/ListClass.proto create mode 100644 formats/protobuf-generator/jvmTest/resources/MapClass.proto create mode 100644 formats/protobuf-generator/jvmTest/resources/OptionalClass.proto create mode 100644 formats/protobuf-generator/jvmTest/resources/OptionsClass.proto create mode 100644 formats/protobuf-generator/jvmTest/resources/ScalarHolder.proto create mode 100644 formats/protobuf-generator/jvmTest/resources/SealedHolder.proto create mode 100644 formats/protobuf-generator/jvmTest/resources/SerialNameClass.proto create mode 100644 formats/protobuf-generator/jvmTest/src/kotlinx/serialization/protobuf/generator/GenerationTest.kt diff --git a/formats/protobuf-generator/api/kotlinx-serialization-protobuf-generator.api b/formats/protobuf-generator/api/kotlinx-serialization-protobuf-generator.api new file mode 100644 index 0000000000..55e7a57109 --- /dev/null +++ b/formats/protobuf-generator/api/kotlinx-serialization-protobuf-generator.api @@ -0,0 +1,4 @@ +public final class kotlinx/serialization/protobuf/generator/GenerationKt { + public static final fun generateProto (Ljava/util/List;Ljava/lang/String;Ljava/util/Map;)Ljava/lang/String; + public static synthetic fun generateProto$default (Ljava/util/List;Ljava/lang/String;Ljava/util/Map;ILjava/lang/Object;)Ljava/lang/String; +} diff --git a/formats/protobuf-generator/build.gradle.kts b/formats/protobuf-generator/build.gradle.kts new file mode 100644 index 0000000000..bea1af3187 --- /dev/null +++ b/formats/protobuf-generator/build.gradle.kts @@ -0,0 +1,18 @@ +plugins { + kotlin("multiplatform") + id("kotlinx-serialization") +} + +apply(from = rootProject.file("gradle/native-targets.gradle")) +apply(from = rootProject.file("gradle/configure-source-sets.gradle")) + +kotlin { + sourceSets { + commonMain { + dependencies { + api(project(":kotlinx-serialization-core")) + api(project(":kotlinx-serialization-protobuf")) + } + } + } +} diff --git a/formats/protobuf-generator/commonMain/src/kotlinx.serialization.protobuf.generator/Generation.kt b/formats/protobuf-generator/commonMain/src/kotlinx.serialization.protobuf.generator/Generation.kt new file mode 100644 index 0000000000..39d7c21724 --- /dev/null +++ b/formats/protobuf-generator/commonMain/src/kotlinx.serialization.protobuf.generator/Generation.kt @@ -0,0 +1,383 @@ +package kotlinx.serialization.protobuf.generator + +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.descriptors.* +import kotlinx.serialization.protobuf.ProtoIntegerType +import kotlinx.serialization.protobuf.ProtoNumber +import kotlinx.serialization.protobuf.ProtoType + +@ExperimentalSerializationApi +public fun generateProto( + descriptors: List, + packageName: String? = null, + options: Map = emptyMap() +): String { + packageName?.let { if (!it.isProtobufFullIdent) throw IllegalArgumentException("Incorrect protobuf package name '$it'") } + + val customTypes = findCustomTypes(descriptors) + return generateSchemeString(customTypes, packageName, options) +} + +private data class CustomTypeDeclaration(val name: String, val descriptor: SerialDescriptor) + +private fun findCustomTypes(descriptors: List): List { + val customTypeDescriptorsBySerialName = linkedMapOf() + descriptors.forEach { addCustomTypeWithElements(it, customTypeDescriptorsBySerialName) } + + val customTypesByName = linkedMapOf() + customTypeDescriptorsBySerialName.values.forEach { + val serialNameAsIdent = makeProtobufIdent(it.serialName.substringAfterLast('.', it.serialName)) + var nameVariantNumber = 1 + var name = serialNameAsIdent + while (customTypesByName.containsKey(name)) { + nameVariantNumber++ + name = "${serialNameAsIdent}_$nameVariantNumber" + } + customTypesByName[name] = CustomTypeDeclaration(name, it) + } + + return customTypesByName.values.toList() +} + +private fun generateSchemeString( + customTypes: List, + packageName: String?, + options: Map +): String { + val builder = StringBuilder() + builder.appendLine("""syntax = "proto2";""") + .appendLine() + + packageName?.let { + builder.append("package ").append(it).appendLine(';') + } + for ((optionName, optionValue) in options) { + builder.append("option ").append(optionName).append(" = \"").append(optionValue).appendLine("\";") + } + + for (type in customTypes) { + builder.appendLine() + when { + type.descriptor.isProtobufEnum -> generateEnum(type, builder) + type.descriptor.isProtobufMessage -> generateMessage(type, builder) + else -> throw IllegalStateException( + "Custom type can be enum or message but found kind '${type.descriptor.kind}'" + ) + } + } + + return builder.toString() +} + +private fun addCustomTypeWithElements(descriptor: SerialDescriptor, all: MutableMap) { + when { + descriptor.isProtobufScalar -> return + descriptor.isProtobufStaticMessage -> + if (!all.containsKey(descriptor.serialName)) { + all[descriptor.serialName] = descriptor + for (childDescriptor in descriptor.elementDescriptors) { + addCustomTypeWithElements(childDescriptor, all) + } + } + descriptor.isProtobufEnum -> { + if (!all.containsKey(descriptor.serialName)) { + all[descriptor.serialName] = descriptor + } + } + descriptor.isProtobufRepeated -> addCustomTypeWithElements(descriptor.getElementDescriptor(0), all) + descriptor.isProtobufMap -> addCustomTypeWithElements(descriptor.getElementDescriptor(1), all) + descriptor.isProtobufSealedMessage -> { + if (!all.containsKey(descriptor.serialName)) { + all[descriptor.serialName] = descriptor + } + val contextualDescriptor = descriptor.getElementDescriptor(1) + for (childDescriptor in contextualDescriptor.elementDescriptors) { + addCustomTypeWithElements(childDescriptor, all) + } + } + descriptor.isProtobufOpenMessage -> { + if (!all.containsKey(descriptor.serialName)) { + all[descriptor.serialName] = descriptor + } + } + descriptor.isProtobufContextualMessage -> return + else -> throw IllegalStateException( + "Unrecognized custom type with serial name " + + "'${descriptor.serialName}' and kind '${descriptor.kind}'" + ) + } +} + +private fun generateMessage( + message: CustomTypeDeclaration, + builder: StringBuilder +) { + val serialDescriptor = message.descriptor + + builder.append("// serial name '").append(removeLineBreaks(serialDescriptor.serialName)).appendLine('\'') + + builder.append("message ").append(message.name).appendLine(" {") + for (index in 0 until serialDescriptor.elementsCount) { + val childDescriptor = serialDescriptor.getElementDescriptor(index) + val annotations = serialDescriptor.getElementAnnotations(index) + + val originFieldName = serialDescriptor.getElementName(index) + val fieldName = makeProtobufIdent(originFieldName) + + if (originFieldName != fieldName) builder.append(" // original field name '") + .append(removeLineBreaks(originFieldName)) + .appendLine('\'') + + if (serialDescriptor.isElementOptional(index)) { + builder.appendLine(" // WARNING: field has a default value that is not present in the scheme") + println( + """WARNING: field '$fieldName' in serializable class '${serialDescriptor.serialName}' """ + + "has a default value that is not present in the scheme!" + ) + } + + try { + when { + childDescriptor.isProtobufNamedType -> generateNamedType( + serialDescriptor, + childDescriptor, + index, + builder + ) + childDescriptor.isProtobufMap -> generateMapType(childDescriptor, builder) + childDescriptor.isProtobufRepeated -> generateListType(childDescriptor, builder) + else -> throw IllegalStateException( + "Unprocessed message field type with serial name " + + "'${childDescriptor.serialName}' and kind '${childDescriptor.kind}'" + ) + } + } catch (e: IllegalArgumentException) { + throw IllegalArgumentException( + "An error occurred during value generation for field $fieldName of message ${serialDescriptor.protobufCustomTypeName} " + + "(serial name ${serialDescriptor.serialName}): ${e.message}", e + ) + } catch (e: Exception) { + throw IllegalStateException( + "Unexpected error occurred during value generation for field $fieldName of message ${serialDescriptor.protobufCustomTypeName} " + + "(serial name ${serialDescriptor.serialName}): ${e.message}", e + ) + } + + builder.append(' ') + builder.append(fieldName) + builder.append(" = ") + val number = annotations.filterIsInstance().singleOrNull()?.number ?: index + 1 + builder.append(number) + builder.appendLine(';') + } + builder.appendLine('}') +} + +private fun generateEnum( + enum: CustomTypeDeclaration, + builder: StringBuilder +) { + builder.append("// serial name '").append(enum.descriptor.serialName).appendLine('\'') + builder.append("enum ").append(enum.name).appendLine(" {") + + enum.descriptor.elementDescriptors.forEachIndexed { number, element -> + builder.append(" ").append(element.protobufEnumElementName).append(" = ").append(number).appendLine(';') + } + builder.appendLine('}') +} + +private fun generateNamedType( + messageDescriptor: SerialDescriptor, + fieldDescriptor: SerialDescriptor, + index: Int, + builder: StringBuilder +) { + if (fieldDescriptor.isProtobufContextualMessage) { + if (messageDescriptor.isProtobufSealedMessage) { + builder.appendLine(" // decoded as message with one of these types:") + fieldDescriptor.elementDescriptors.forEachIndexed { _, childDescriptor -> + builder.append(" // message ").append(childDescriptor.protobufCustomTypeName) + .append(", serial name '").append(removeLineBreaks(childDescriptor.serialName)).appendLine('\'') + } + } else { + builder.appendLine(" // contextual message type") + } + } + + builder.append(" ") + .append(if (messageDescriptor.isElementOptional(index)) "optional " else "required ") + .append(namedTypeName(fieldDescriptor, messageDescriptor.getElementAnnotations(index))) +} + +private fun generateMapType(descriptor: SerialDescriptor, builder: StringBuilder) { + builder.append(" map<") + builder.append(protobufMapKeyType(descriptor.getElementDescriptor(0))) + builder.append(", ") + builder.append(protobufMapValueType(descriptor.getElementDescriptor(1))) + builder.append(">") +} + +private fun generateListType(descriptor: SerialDescriptor, builder: StringBuilder) { + builder.append(" repeated ") + builder.append(protobufRepeatedType(descriptor.getElementDescriptor(0))) +} + +private val SerialDescriptor.isProtobufNamedType: Boolean + get() { + return isProtobufScalar || isProtobufCustomType + } + +private val SerialDescriptor.isProtobufCustomType: Boolean + get() { + return isProtobufMessage || isProtobufEnum + } + +private val SerialDescriptor.isProtobufMessage: Boolean + get() { + return isProtobufStaticMessage || isProtobufOpenMessage || isProtobufSealedMessage || isProtobufContextualMessage + } + +private val SerialDescriptor.isProtobufScalar: Boolean + get() { + return (kind is PrimitiveKind) + || (kind is StructureKind.LIST && getElementDescriptor(0).kind === PrimitiveKind.BYTE) + } + +private val SerialDescriptor.isProtobufStaticMessage: Boolean + get() { + return kind == StructureKind.CLASS || kind == StructureKind.OBJECT + } + +private val SerialDescriptor.isProtobufOpenMessage: Boolean + get() { + return kind == PolymorphicKind.OPEN + } + +private val SerialDescriptor.isProtobufSealedMessage: Boolean + get() { + return kind == PolymorphicKind.SEALED + } + +private val SerialDescriptor.isProtobufContextualMessage: Boolean + get() { + return kind == SerialKind.CONTEXTUAL + } + +private val SerialDescriptor.isProtobufRepeated: Boolean + get() { + return kind == StructureKind.LIST && getElementDescriptor(0).kind != PrimitiveKind.BYTE + } + +private val SerialDescriptor.isProtobufMap: Boolean + get() { + return kind == StructureKind.MAP + } + +private val SerialDescriptor.isProtobufEnum: Boolean + get() { + return this.kind == SerialKind.ENUM + } + +private val SerialDescriptor.protobufCustomTypeName: String + get() { + return makeProtobufIdent(serialName.substringAfterLast('.', serialName)) + } + +private val SerialDescriptor.protobufEnumElementName: String + get() { + return makeProtobufIdent(serialName.substringAfterLast('.', serialName)) + } + +private fun scalarTypeName(descriptor: SerialDescriptor, annotations: List = emptyList()): String { + val integerType = annotations.filterIsInstance().firstOrNull()?.type ?: ProtoIntegerType.DEFAULT + + if (descriptor.kind is StructureKind.LIST && descriptor.getElementDescriptor(0).kind == PrimitiveKind.BYTE) { + return "bytes" + } + + return when (descriptor.kind as PrimitiveKind) { + PrimitiveKind.BOOLEAN -> "bool" + PrimitiveKind.BYTE, PrimitiveKind.CHAR, PrimitiveKind.SHORT, PrimitiveKind.INT -> + when (integerType) { + ProtoIntegerType.DEFAULT -> "int32" + ProtoIntegerType.SIGNED -> "sint32" + ProtoIntegerType.FIXED -> "fixed32" + } + PrimitiveKind.LONG -> + when (integerType) { + ProtoIntegerType.DEFAULT -> "int64" + ProtoIntegerType.SIGNED -> "sint64" + ProtoIntegerType.FIXED -> "fixed64" + } + PrimitiveKind.FLOAT -> "float" + PrimitiveKind.DOUBLE -> "double" + PrimitiveKind.STRING -> "string" + } +} + +private fun namedTypeName(descriptor: SerialDescriptor, annotations: List): String { + return when { + descriptor.isProtobufScalar -> scalarTypeName(descriptor, annotations) + descriptor.isProtobufContextualMessage -> "bytes" + descriptor.isProtobufCustomType -> descriptor.protobufCustomTypeName + else -> throw IllegalStateException( + "Descriptor with serial name '${descriptor.serialName}' and kind " + + "'${descriptor.kind}' isn't named protobuf type" + ) + } +} + +private fun protobufMapKeyType(descriptor: SerialDescriptor): String { + if (!descriptor.isProtobufScalar || descriptor.kind === PrimitiveKind.DOUBLE || descriptor.kind === PrimitiveKind.FLOAT) { + throw IllegalArgumentException( + "Illegal type for map key: serial name '${descriptor.serialName}' and kind '${descriptor.kind}'." + + "As map key type in protobuf allowed only scalar type except for floating point types and bytes." + ) + } + return scalarTypeName(descriptor) +} + +private fun protobufMapValueType(descriptor: SerialDescriptor): String { + if (descriptor.isProtobufRepeated) { + throw IllegalArgumentException("List is not allowed as a map value type in protobuf") + } + if (descriptor.isProtobufMap) { + throw IllegalArgumentException("Map is not allowed as a map value type in protobuf") + } + return namedTypeName(descriptor, emptyList()) +} + +private fun protobufRepeatedType(descriptor: SerialDescriptor): String { + if (descriptor.isProtobufRepeated) { + throw IllegalArgumentException("List is not allowed as a list element") + } + if (descriptor.isProtobufMap) { + throw IllegalArgumentException("Map is not allowed as a list element") + } + return namedTypeName(descriptor, emptyList()) +} + +private fun removeLineBreaks(text: String): String { + return text.replace('\n', ' ').replace('\r', ' ') +} + +private val INCORRECT_IDENT_CHAR_REGEX = Regex("[^A-Za-z0-9_]") +private val IDENT_REGEX = Regex("[A-Za-z][A-Za-z0-9_]*") + +private fun makeProtobufIdent(serialName: String): String { + val replaced = serialName.replace(INCORRECT_IDENT_CHAR_REGEX, "_") + return if (!replaced.matches(IDENT_REGEX)) { + "a$replaced" + } else { + replaced + } +} + +private val String.isProtobufFullIdent: Boolean + get() { + split('.').forEach { + if (!it.matches(IDENT_REGEX)) return false + } + return true + } + diff --git a/formats/protobuf-generator/jvmTest/resources/AbstractHolder.proto b/formats/protobuf-generator/jvmTest/resources/AbstractHolder.proto new file mode 100644 index 0000000000..922b27690f --- /dev/null +++ b/formats/protobuf-generator/jvmTest/resources/AbstractHolder.proto @@ -0,0 +1,15 @@ +syntax = "proto2"; + +package kotlinx.serialization.protobuf.generator.scheme; + +// serial name 'kotlinx.serialization.protobuf.generator.GenerationTest.AbstractHolder' +message AbstractHolder { + required Polymorphic_AbstractClass_ abs = 1; +} + +// serial name 'kotlinx.serialization.Polymorphic' +message Polymorphic_AbstractClass_ { + required string type = 1; + // contextual message type + required bytes value = 2; +} diff --git a/formats/protobuf-generator/jvmTest/resources/ContextualHolder.proto b/formats/protobuf-generator/jvmTest/resources/ContextualHolder.proto new file mode 100644 index 0000000000..e73adf93c3 --- /dev/null +++ b/formats/protobuf-generator/jvmTest/resources/ContextualHolder.proto @@ -0,0 +1,9 @@ +syntax = "proto2"; + +package kotlinx.serialization.protobuf.generator.scheme; + +// serial name 'kotlinx.serialization.protobuf.generator.GenerationTest.ContextualHolder' +message ContextualHolder { + // contextual message type + required bytes value = 1; +} diff --git a/formats/protobuf-generator/jvmTest/resources/FieldNumberClass.proto b/formats/protobuf-generator/jvmTest/resources/FieldNumberClass.proto new file mode 100644 index 0000000000..b9fdc8dbba --- /dev/null +++ b/formats/protobuf-generator/jvmTest/resources/FieldNumberClass.proto @@ -0,0 +1,10 @@ +syntax = "proto2"; + +package kotlinx.serialization.protobuf.generator.scheme; + +// serial name 'kotlinx.serialization.protobuf.generator.GenerationTest.FieldNumberClass' +message FieldNumberClass { + required int32 a = 1; + required int32 b = 5; + required int32 c = 3; +} diff --git a/formats/protobuf-generator/jvmTest/resources/ListClass.proto b/formats/protobuf-generator/jvmTest/resources/ListClass.proto new file mode 100644 index 0000000000..0376692250 --- /dev/null +++ b/formats/protobuf-generator/jvmTest/resources/ListClass.proto @@ -0,0 +1,23 @@ +syntax = "proto2"; + +package kotlinx.serialization.protobuf.generator.scheme; + +// serial name 'kotlinx.serialization.protobuf.generator.GenerationTest.ListClass' +message ListClass { + repeated int32 intList = 1; + repeated int32 intArray = 2; + repeated int32 boxedIntArray = 3; + repeated OptionsClass messageList = 4; + repeated SerialNameEnum enumList = 5; +} + +// serial name 'kotlinx.serialization.protobuf.generator.GenerationTest.OptionsClass' +message OptionsClass { + required int32 i = 1; +} + +// serial name 'kotlinx.serialization.protobuf.generator.GenerationTest.SerialNameEnum' +enum SerialNameEnum { + FIRST = 0; + overridden_name_of_enum_ = 1; +} diff --git a/formats/protobuf-generator/jvmTest/resources/MapClass.proto b/formats/protobuf-generator/jvmTest/resources/MapClass.proto new file mode 100644 index 0000000000..590d4c36a7 --- /dev/null +++ b/formats/protobuf-generator/jvmTest/resources/MapClass.proto @@ -0,0 +1,22 @@ +syntax = "proto2"; + +package kotlinx.serialization.protobuf.generator.scheme; + +// serial name 'kotlinx.serialization.protobuf.generator.GenerationTest.MapClass' +message MapClass { + map scalarMap = 1; + map bytesMap = 2; + map messageMap = 3; + map enumMap = 4; +} + +// serial name 'kotlinx.serialization.protobuf.generator.GenerationTest.OptionsClass' +message OptionsClass { + required int32 i = 1; +} + +// serial name 'kotlinx.serialization.protobuf.generator.GenerationTest.SerialNameEnum' +enum SerialNameEnum { + FIRST = 0; + overridden_name_of_enum_ = 1; +} diff --git a/formats/protobuf-generator/jvmTest/resources/OptionalClass.proto b/formats/protobuf-generator/jvmTest/resources/OptionalClass.proto new file mode 100644 index 0000000000..98941e16c9 --- /dev/null +++ b/formats/protobuf-generator/jvmTest/resources/OptionalClass.proto @@ -0,0 +1,13 @@ +syntax = "proto2"; + +package kotlinx.serialization.protobuf.generator.scheme; + +// serial name 'kotlinx.serialization.protobuf.generator.GenerationTest.OptionalClass' +message OptionalClass { + required int32 requiredInt = 1; + // WARNING: field has a default value that is not present in the scheme + optional int32 optionalInt = 2; + required int32 nullableInt = 3; + // WARNING: field has a default value that is not present in the scheme + optional int32 nullableOptionalInt = 4; +} diff --git a/formats/protobuf-generator/jvmTest/resources/OptionsClass.proto b/formats/protobuf-generator/jvmTest/resources/OptionsClass.proto new file mode 100644 index 0000000000..d41c6ebf13 --- /dev/null +++ b/formats/protobuf-generator/jvmTest/resources/OptionsClass.proto @@ -0,0 +1,10 @@ +syntax = "proto2"; + +package kotlinx.serialization.protobuf.generator.scheme; +option java_package = "api.proto"; +option java_outer_classname = "Outer"; + +// serial name 'kotlinx.serialization.protobuf.generator.GenerationTest.OptionsClass' +message OptionsClass { + required int32 i = 1; +} diff --git a/formats/protobuf-generator/jvmTest/resources/ScalarHolder.proto b/formats/protobuf-generator/jvmTest/resources/ScalarHolder.proto new file mode 100644 index 0000000000..14b4c5b5d4 --- /dev/null +++ b/formats/protobuf-generator/jvmTest/resources/ScalarHolder.proto @@ -0,0 +1,21 @@ +syntax = "proto2"; + +package kotlinx.serialization.protobuf.generator.scheme; + +// serial name 'kotlinx.serialization.protobuf.generator.GenerationTest.ScalarHolder' +message ScalarHolder { + required int32 int = 1; + required sint32 intSigned = 2; + required fixed32 intFixed = 3; + required int32 intDefault = 4; + required int64 long = 5; + required sint64 longSigned = 6; + required fixed64 longFixed = 7; + required int32 longDefault = 8; + required bool flag = 9; + required bytes byteArray = 10; + required bytes boxedByteArray = 11; + required string text = 12; + required float float = 13; + required double double = 14; +} diff --git a/formats/protobuf-generator/jvmTest/resources/SealedHolder.proto b/formats/protobuf-generator/jvmTest/resources/SealedHolder.proto new file mode 100644 index 0000000000..7e6e57f46d --- /dev/null +++ b/formats/protobuf-generator/jvmTest/resources/SealedHolder.proto @@ -0,0 +1,27 @@ +syntax = "proto2"; + +package kotlinx.serialization.protobuf.generator.scheme; + +// serial name 'kotlinx.serialization.protobuf.generator.GenerationTest.SealedHolder' +message SealedHolder { + required SealedClass sealed = 1; +} + +// serial name 'kotlinx.serialization.protobuf.generator.GenerationTest.SealedClass' +message SealedClass { + required string type = 1; + // decoded as message with one of these types: + // message Impl1, serial name 'kotlinx.serialization.protobuf.generator.GenerationTest.SealedClass.Impl1' + // message Impl2, serial name 'kotlinx.serialization.protobuf.generator.GenerationTest.SealedClass.Impl2' + required bytes value = 2; +} + +// serial name 'kotlinx.serialization.protobuf.generator.GenerationTest.SealedClass.Impl1' +message Impl1 { + required int32 int = 1; +} + +// serial name 'kotlinx.serialization.protobuf.generator.GenerationTest.SealedClass.Impl2' +message Impl2 { + required int64 long = 1; +} diff --git a/formats/protobuf-generator/jvmTest/resources/SerialNameClass.proto b/formats/protobuf-generator/jvmTest/resources/SerialNameClass.proto new file mode 100644 index 0000000000..d40ffa712c --- /dev/null +++ b/formats/protobuf-generator/jvmTest/resources/SerialNameClass.proto @@ -0,0 +1,16 @@ +syntax = "proto2"; + +package kotlinx.serialization.protobuf.generator.scheme; + +// serial name 'my serial name' +message my_serial_name { + required int32 original = 1; + // original field name 'enum field' + required SerialNameEnum enum_field = 2; +} + +// serial name 'kotlinx.serialization.protobuf.generator.GenerationTest.SerialNameEnum' +enum SerialNameEnum { + FIRST = 0; + overridden_name_of_enum_ = 1; +} diff --git a/formats/protobuf-generator/jvmTest/src/kotlinx/serialization/protobuf/generator/GenerationTest.kt b/formats/protobuf-generator/jvmTest/src/kotlinx/serialization/protobuf/generator/GenerationTest.kt new file mode 100644 index 0000000000..d41bfe5716 --- /dev/null +++ b/formats/protobuf-generator/jvmTest/src/kotlinx/serialization/protobuf/generator/GenerationTest.kt @@ -0,0 +1,196 @@ +package kotlinx.serialization.protobuf.generator + +import kotlinx.serialization.* +import kotlinx.serialization.protobuf.ProtoIntegerType +import kotlinx.serialization.protobuf.ProtoNumber +import kotlinx.serialization.protobuf.ProtoType +import java.io.File +import java.nio.charset.StandardCharsets +import kotlin.reflect.KClass +import kotlin.reflect.typeOf +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith + +private const val TARGET_PACKAGE = "kotlinx.serialization.protobuf.generator.scheme" +private const val SCHEMES_DIRECTORY_PATH = "formats/protobuf-generator/jvmTest/resources" + +class GenerationTest { + + @Serializable + class ScalarHolder( + val int: Int, + @ProtoType(ProtoIntegerType.SIGNED) + val intSigned: Int, + @ProtoType(ProtoIntegerType.FIXED) + val intFixed: Int, + @ProtoType(ProtoIntegerType.DEFAULT) + val intDefault: Int, + + val long: Long, + @ProtoType(ProtoIntegerType.SIGNED) + val longSigned: Long, + @ProtoType(ProtoIntegerType.FIXED) + val longFixed: Long, + @ProtoType(ProtoIntegerType.DEFAULT) + val longDefault: Int, + + val flag: Boolean, + val byteArray: ByteArray, + val boxedByteArray: Array, + val text: String, + val float: Float, + val double: Double + ) + + @Serializable + class FieldNumberClass( + val a: Int, + @ProtoNumber(5) + val b: Int, + @ProtoNumber(3) + val c: Int + ) + + @Serializable + @SerialName("my serial name") + class SerialNameClass( + val original: Int, + @SerialName("enum field") + val b: SerialNameEnum + + ) + + @Serializable + enum class SerialNameEnum { + FIRST, + + @SerialName("overridden-name-of-enum!") + SECOND + } + + @Serializable + data class OptionsClass(val i: Int) + + @Serializable + class ListClass( + val intList: List, + val intArray: IntArray, + val boxedIntArray: Array, + val messageList: List, + val enumList: List + ) + + @Serializable + class MapClass( + val scalarMap: Map, + val bytesMap: Map>, + val messageMap: Map, + val enumMap: Map + ) + + @Serializable + data class OptionalClass( + val requiredInt: Int, + val optionalInt: Int = 5, + val nullableInt: Int?, + val nullableOptionalInt: Int? = 10 + ) + + @Serializable + data class ContextualHolder( + @Contextual val value: Int + ) + + @Serializable + abstract class AbstractClass(val int: Int) + + @Serializable + data class AbstractHolder(@Polymorphic val abs: AbstractClass) + + @Serializable + sealed class SealedClass { + @Serializable + data class Impl1(val int: Int) : SealedClass() + + @Serializable + data class Impl2(val long: Long) : SealedClass() + } + + @Serializable + data class SealedHolder(val sealed: SealedClass) + + @Test + fun testIllegalPackageNames() { + val descriptors = listOf(OptionsClass.serializer().descriptor) + assertFailsWith(IllegalArgumentException::class) { generateProto(descriptors, "", emptyMap()) } + assertFailsWith(IllegalArgumentException::class) { generateProto(descriptors, ".", emptyMap()) } + assertFailsWith(IllegalArgumentException::class) { generateProto(descriptors, ".first.dot", emptyMap()) } + assertFailsWith(IllegalArgumentException::class) { generateProto(descriptors, "ended.with.dot.", emptyMap()) } + assertFailsWith(IllegalArgumentException::class) { generateProto(descriptors, "first._underscore", emptyMap()) } + assertFailsWith(IllegalArgumentException::class) { generateProto(descriptors, "first.1digit", emptyMap()) } + assertFailsWith(IllegalArgumentException::class) { generateProto(descriptors, "illegal.sym+bol", emptyMap()) } + } + + @Test + fun testValidPackageNames() { + val descriptors = listOf(OptionsClass.serializer().descriptor) + generateProto(descriptors, "singleIdent", emptyMap()) + generateProto(descriptors, "double.ident", emptyMap()) + generateProto(descriptors, "with.digits0123", emptyMap()) + generateProto(descriptors, "with.underscore_", emptyMap()) + } + + @Test + fun test() { + assertProtoForType(ScalarHolder::class) + assertProtoForType(FieldNumberClass::class) + assertProtoForType(SerialNameClass::class) + assertProtoForType(OptionsClass::class, mapOf("java_package" to "api.proto", "java_outer_classname" to "Outer")) + assertProtoForType(ListClass::class) + assertProtoForType(MapClass::class) + assertProtoForType(OptionalClass::class) + assertProtoForType(ContextualHolder::class) + assertProtoForType(AbstractHolder::class) + assertProtoForType(SealedHolder::class) + } + + private inline fun assertProtoForType( + clazz: KClass, + options: Map = emptyMap() + ) { + val scheme = clazz.java.getResourceAsStream("/${clazz.simpleName}.proto").readBytes().toString(Charsets.UTF_8) + assertEquals(scheme, generateProto(listOf(serializer(typeOf()).descriptor), TARGET_PACKAGE, options)) + } + +} + +/* +// Regenerate all proto file for tests. +private fun main() { + regenerateAllProtoFiles() +} +*/ + +private fun regenerateAllProtoFiles() { + generateProtoFile(GenerationTest.ScalarHolder::class) + generateProtoFile(GenerationTest.FieldNumberClass::class) + generateProtoFile(GenerationTest.SerialNameClass::class) + generateProtoFile(GenerationTest.OptionsClass::class, mapOf("java_package" to "api.proto", "java_outer_classname" to "Outer")) + generateProtoFile(GenerationTest.ListClass::class) + generateProtoFile(GenerationTest.MapClass::class) + generateProtoFile(GenerationTest.OptionalClass::class) + generateProtoFile(GenerationTest.ContextualHolder::class) + generateProtoFile(GenerationTest.AbstractHolder::class) + generateProtoFile(GenerationTest.SealedHolder::class) +} + +private inline fun generateProtoFile( + clazz: KClass, + options: Map = emptyMap() +) { + val filePath = "$SCHEMES_DIRECTORY_PATH/${clazz.simpleName}.proto" + val file = File(filePath) + val scheme = generateProto(listOf(serializer(typeOf()).descriptor), TARGET_PACKAGE, options) + file.writeText(scheme, StandardCharsets.UTF_8) +} diff --git a/settings.gradle b/settings.gradle index 88bcb9427e..ff8c5da0e1 100644 --- a/settings.gradle +++ b/settings.gradle @@ -14,6 +14,9 @@ project(':kotlinx-serialization-json').projectDir = file('./formats/json') include ':kotlinx-serialization-protobuf' project(':kotlinx-serialization-protobuf').projectDir = file('./formats/protobuf') +include ':kotlinx-serialization-protobuf-generator' +project(':kotlinx-serialization-protobuf-generator').projectDir = file('./formats/protobuf-generator') + include ':kotlinx-serialization-cbor' project(':kotlinx-serialization-cbor').projectDir = file('./formats/cbor')