• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download

<lambda>null1 @file:OptIn(ExperimentalSerializationApi::class)
2 
3 package kotlinx.serialization.protobuf.schema
4 
5 import kotlinx.serialization.*
6 import kotlinx.serialization.builtins.*
7 import kotlinx.serialization.descriptors.*
8 import kotlinx.serialization.protobuf.*
9 import kotlinx.serialization.protobuf.internal.*
10 
11 /**
12  * Experimental generator of ProtoBuf schema that is compatible with [serializable][Serializable] Kotlin classes
13  * and data encoded and decoded by [ProtoBuf] format.
14  *
15  * The schema is generated based on provided [SerialDescriptor] and is compatible with proto2 schema definition.
16  * An arbitrary Kotlin class represent much wider object domain than the ProtoBuf specification, thus schema generator
17  * has the following list of restrictions:
18  *
19  *  * Serial name of the class and all its fields should be a valid Proto [identifier](https://developers.google.com/protocol-buffers/docs/reference/proto2-spec)
20  *  * Nullable values are allowed only for Kotlin [nullable][SerialDescriptor.isNullable] types, but not [optional][SerialDescriptor.isElementOptional]
21  *    in order to properly distinguish "default" and "absent" values.
22  *  * The name of the type without the package directive uniquely identifies the proto message type and two or more fields with the same serial name
23  *    are considered to have the same type. Schema generator allows to specify a separate package directive for the pack of classes in order
24  *    to mitigate this limitation.
25  *  * Nested collections, e.g. `List<List<Int>>` are represented using the artificial wrapper message in order to distinguish
26  *    repeated fields boundaries.
27  *  * Default Kotlin values are not representable in proto schema. A special commentary is generated for properties with default values.
28  *  * Empty nullable collections are not supported by the generated schema and will be prohibited in [ProtoBuf] as well
29  *    due to their ambiguous nature.
30  *
31  * Temporary restrictions:
32  *  * [Contextual] data is represented as as `bytes` type
33  *  * [Polymorphic] data is represented as a artificial `KotlinxSerializationPolymorphic` message.
34  *
35  * Other types are mapped according to their specification: primitives as primitives, lists as 'repeated' fields and
36  * maps as 'repeated' map entries.
37  *
38  * The name of messages and enums is extracted from [SerialDescriptor.serialName] in [SerialDescriptor] without the package directive,
39  * as substring after the last dot character, the `'?'` character is also removed if it is present at the end of the string.
40  */
41 @ExperimentalSerializationApi
42 public object ProtoBufSchemaGenerator {
43 
44     /**
45      * Generate text of protocol buffers schema version 2 for the given [rootDescriptor].
46      * The resulting schema will contain all types referred by [rootDescriptor].
47      *
48      * [packageName] define common protobuf package for all messages and enum in the schema, it may contain `'a'`..`'z'`
49      * letters in upper and lower case, decimal digits, `'.'` or `'_'` chars, but must be started only by a letter and
50      * not finished by a dot.
51      *
52      * [options] define values for protobuf options. Option value (map value) is an any string, option name (map key)
53      * should be the same format as [packageName].
54      *
55      * The method throws [IllegalArgumentException] if any of the restrictions imposed by [ProtoBufSchemaGenerator] is violated.
56      */
57     @ExperimentalSerializationApi
58     public fun generateSchemaText(
59         rootDescriptor: SerialDescriptor,
60         packageName: String? = null,
61         options: Map<String, String> = emptyMap()
62     ): String = generateSchemaText(listOf(rootDescriptor), packageName, options)
63 
64     /**
65      * Generate text of protocol buffers schema version 2 for the given serializable [descriptors].
66      * [packageName] define common protobuf package for all messages and enum in the schema, it may contain `'a'`..`'z'`
67      * letters in upper and lower case, decimal digits, `'.'` or `'_'` chars, but started only from a letter and
68      * not finished by dot.
69      *
70      * [options] define values for protobuf options. Option value (map value) is an any string, option name (map key)
71      * should be the same format as [packageName].
72      *
73      * The method throws [IllegalArgumentException] if any of the restrictions imposed by [ProtoBufSchemaGenerator] is violated.
74      */
75     @ExperimentalSerializationApi
76     public fun generateSchemaText(
77         descriptors: List<SerialDescriptor>,
78         packageName: String? = null,
79         options: Map<String, String> = emptyMap()
80     ): String {
81         packageName?.let { p -> p.checkIsValidFullIdentifier { "Incorrect protobuf package name '$it'" } }
82         checkDoubles(descriptors)
83         val builder = StringBuilder()
84         builder.generateProto2SchemaText(descriptors, packageName, options)
85         return builder.toString()
86     }
87 
88     private fun checkDoubles(descriptors: List<SerialDescriptor>) {
89         val rootTypesNames = mutableSetOf<String>()
90         val duplicates = mutableListOf<String>()
91 
92         descriptors.map { it.messageOrEnumName }.forEach {
93             if (!rootTypesNames.add(it)) {
94                 duplicates += it
95             }
96         }
97         if (duplicates.isNotEmpty()) {
98             throw IllegalArgumentException("Serial names of the following types are duplicated: $duplicates")
99         }
100     }
101 
102     private fun StringBuilder.generateProto2SchemaText(
103         descriptors: List<SerialDescriptor>,
104         packageName: String?,
105         options: Map<String, String>
106     ) {
107         appendLine("""syntax = "proto2";""").appendLine()
108 
109         packageName?.let { append("package ").append(it).appendLine(';') }
110 
111         for ((optionName, optionValue) in options) {
112             val safeOptionName = removeLineBreaks(optionName)
113             val safeOptionValue = removeLineBreaks(optionValue)
114             safeOptionName.checkIsValidFullIdentifier { "Invalid option name '$it'" }
115             append("option ").append(safeOptionName).append(" = \"").append(safeOptionValue).appendLine("\";")
116         }
117 
118         val generatedTypes = mutableSetOf<String>()
119         val queue = ArrayDeque<TypeDefinition>()
120         descriptors.map { TypeDefinition(it) }.forEach { queue.add(it) }
121 
122         while (queue.isNotEmpty()) {
123             val type = queue.removeFirst()
124             val descriptor = type.descriptor
125             val name = descriptor.messageOrEnumName
126             if (!generatedTypes.add(name)) {
127                 continue
128             }
129 
130             appendLine()
131             when {
132                 descriptor.isProtobufMessage -> queue.addAll(generateMessage(type))
133                 descriptor.isProtobufEnum -> generateEnum(type)
134                 else -> throw IllegalStateException(
135                     "Unrecognized custom type with serial name "
136                             + "'${descriptor.serialName}' and kind '${descriptor.kind}'"
137                 )
138             }
139         }
140     }
141 
142     private fun StringBuilder.generateMessage(messageType: TypeDefinition): List<TypeDefinition> {
143         val messageDescriptor = messageType.descriptor
144         val messageName: String
145         if (messageType.isSynthetic) {
146             append("// This message was generated to support ").append(messageType.ability)
147                 .appendLine(" and does not present in Kotlin.")
148 
149             messageName = messageDescriptor.serialName
150             if (messageType.containingMessageName != null) {
151                 append("// Containing message '").append(messageType.containingMessageName).append("', field '")
152                     .append(messageType.fieldName).appendLine('\'')
153             }
154         } else {
155             messageName = messageDescriptor.messageOrEnumName
156             messageName.checkIsValidIdentifier {
157                 "Invalid name for the message in protobuf schema '$messageName'. " +
158                         "Serial name of the class '${messageDescriptor.serialName}'"
159             }
160             val safeSerialName = removeLineBreaks(messageDescriptor.serialName)
161             if (safeSerialName != messageName) {
162                 append("// serial name '").append(safeSerialName).appendLine('\'')
163             }
164         }
165 
166         append("message ").append(messageName).appendLine(" {")
167 
168         val usedNumbers: MutableSet<Int> = mutableSetOf()
169         val nestedTypes = mutableListOf<TypeDefinition>()
170         generateMessageField(messageName, messageType, nestedTypes, usedNumbers)
171         appendLine('}')
172 
173         return nestedTypes
174     }
175 
176     private fun StringBuilder.generateMessageField(
177         messageName: String,
178         parentType: TypeDefinition,
179         nestedTypes: MutableList<TypeDefinition>,
180         usedNumbers: MutableSet<Int>,
181         counts: Int = parentType.descriptor.elementsCount,
182         getAnnotations: (Int) -> List<Annotation> = { parentType.descriptor.getElementAnnotations(it) },
183         getChildType: (Int) -> TypeDefinition = { parentType.descriptor.getElementDescriptor(it).let(::TypeDefinition) },
184         getChildNumber: (Int) -> Int = { parentType.descriptor.getElementAnnotations(it).filterIsInstance<ProtoNumber>().singleOrNull()?.number ?: (it + 1) },
185         getChildName: (Int) -> String = { parentType.descriptor.getElementName(it) },
186         inOneOfStruct: Boolean = false,
187     ) {
188         val messageDescriptor = parentType.descriptor
189         for (index in 0 until counts) {
190             val fieldName = getChildName(index)
191             fieldName.checkIsValidIdentifier {
192                 "Invalid name of the field '$fieldName' in ${if (inOneOfStruct) "oneof" else ""} message '$messageName' for class with serial " +
193                     "name '${messageDescriptor.serialName}'"
194             }
195 
196             val fieldType = getChildType(index)
197             val fieldDescriptor = fieldType.descriptor
198 
199             val number = getChildNumber(index)
200             if (messageDescriptor.isChildOneOfMessage(index)) {
201                 require(!inOneOfStruct) {
202                     "Cannot have nested oneof in oneof struct: ${messageName}.$fieldName"
203                 }
204                 val subDescriptor = fieldDescriptor.getElementDescriptor(1).elementDescriptors.toList()
205                 append("  ").append("oneof").append(' ').append(fieldName).appendLine(" {")
206                 subDescriptor.forEach { desc ->
207                     require(desc.elementsCount == 1) {
208                         "Implementation of oneOf type ${desc.serialName} should contain only 1 element, but get ${desc.elementsCount}"
209                     }
210                     generateMessageField(
211                         messageName = messageName,
212                         parentType = TypeDefinition(desc),
213                         nestedTypes = nestedTypes,
214                         usedNumbers = usedNumbers,
215                         counts = desc.elementsCount,
216                         getAnnotations = { desc.annotations },
217                         getChildType = { desc.elementDescriptors.single().let(::TypeDefinition) },
218                         getChildNumber = { desc.getElementAnnotations(0).filterIsInstance<ProtoNumber>().singleOrNull()?.number ?: (it + 1) },
219                         getChildName = { desc.getElementName(0) },
220                         inOneOfStruct = true,
221                     )
222                 }
223                 appendLine("  }")
224             } else {
225                 val annotations = getAnnotations(index)
226 
227                 val isList = fieldDescriptor.isProtobufRepeated
228 
229                 nestedTypes += when {
230                     fieldDescriptor.isProtobufNamedType -> generateNamedType(
231                         fieldDescriptor = messageDescriptor.getElementDescriptor(index),
232                         annotations = messageDescriptor.getElementAnnotations(index),
233                         isSealedPolymorphic = messageDescriptor.isSealedPolymorphic && index == 1,
234                         isOptional = messageDescriptor.isElementOptional(index),
235                         inOneOfStruct = inOneOfStruct,
236                         indent = if (inOneOfStruct) 2 else 1,
237                     )
238                     isList -> generateListType(parentType, index)
239                     fieldDescriptor.isProtobufMap -> generateMapType(parentType, index)
240                     else -> throw IllegalStateException(
241                         "Unprocessed message field type with serial name " +
242                             "'${fieldDescriptor.serialName}' and kind '${fieldDescriptor.kind}'"
243                     )
244                 }
245                 if (!usedNumbers.add(number)) {
246                     throw IllegalArgumentException("Field number $number is repeated in the class with serial name ${messageDescriptor.serialName}")
247                 }
248 
249                 append(' ').append(fieldName).append(" = ").append(number)
250 
251                 val isPackRequested = annotations.filterIsInstance<ProtoPacked>().singleOrNull() != null
252 
253                 when {
254                     !isPackRequested ||
255                         !isList || // ignore as packed only meaningful on repeated types
256                         !fieldDescriptor.getElementDescriptor(0).isPackable // Ignore if the type is not allowed to be packed
257                     -> appendLine(';')
258 
259                     else -> appendLine(" [packed=true];")
260                 }
261             }
262         }
263     }
264 
265     private fun StringBuilder.generateNamedType(
266         fieldDescriptor: SerialDescriptor,
267         annotations: List<Annotation>,
268         isSealedPolymorphic: Boolean,
269         isOptional: Boolean,
270         inOneOfStruct: Boolean = false,
271         indent: Int = 1,
272     ): List<TypeDefinition> {
273         var unwrappedFieldDescriptor = fieldDescriptor
274         while (unwrappedFieldDescriptor.isInline) {
275             unwrappedFieldDescriptor = unwrappedFieldDescriptor.getElementDescriptor(0)
276         }
277 
278         val nestedTypes: List<TypeDefinition>
279         val typeName: String = when {
280             isSealedPolymorphic -> {
281                 append(" ".repeat(indent * 2)).appendLine("// decoded as message with one of these types:")
282                 nestedTypes = unwrappedFieldDescriptor.elementDescriptors.map { TypeDefinition(it) }.toList()
283                 nestedTypes.forEachIndexed { _, childType ->
284                     append(" ".repeat(indent * 2)).append("//   message ").append(childType.descriptor.messageOrEnumName).append(", serial name '")
285                         .append(removeLineBreaks(childType.descriptor.serialName)).appendLine('\'')
286                 }
287                 unwrappedFieldDescriptor.scalarTypeName()
288             }
289             unwrappedFieldDescriptor.isProtobufScalar -> {
290                 nestedTypes = emptyList()
291                 unwrappedFieldDescriptor.scalarTypeName(annotations)
292             }
293             unwrappedFieldDescriptor.isOpenPolymorphic -> {
294                 nestedTypes = listOf(SyntheticPolymorphicType)
295                 SyntheticPolymorphicType.descriptor.serialName
296             }
297             else -> {
298                 // enum or regular message
299                 nestedTypes = listOf(TypeDefinition(unwrappedFieldDescriptor))
300                 unwrappedFieldDescriptor.messageOrEnumName
301             }
302         }
303 
304         if (isOptional) {
305             append(" ".repeat(indent * 2)).appendLine("// WARNING: a default value decoded when value is missing")
306         }
307         val optional = fieldDescriptor.isNullable || isOptional
308 
309         append(" ".repeat(indent * 2)).append(
310             when {
311                 inOneOfStruct -> ""
312                 optional -> "optional "
313                 else -> "required "
314             }
315         ).append(typeName)
316 
317         return nestedTypes
318     }
319 
320     private fun StringBuilder.generateMapType(messageType: TypeDefinition, index: Int): List<TypeDefinition> {
321         val messageDescriptor = messageType.descriptor
322         val mapDescriptor = messageDescriptor.getElementDescriptor(index)
323         val originalMapValueDescriptor = mapDescriptor.getElementDescriptor(1)
324         val valueType = if (originalMapValueDescriptor.isProtobufCollection) {
325             createNestedCollectionType(messageType, index, originalMapValueDescriptor, "nested collection in map value")
326         } else {
327             TypeDefinition(originalMapValueDescriptor)
328         }
329         val valueDescriptor = valueType.descriptor
330 
331         if (originalMapValueDescriptor.isNullable) {
332             appendLine("  // WARNING: nullable map values can not be represented in protobuf")
333         }
334         generateCollectionAbsenceComment(messageDescriptor, mapDescriptor, index)
335 
336         val keyTypeName = mapDescriptor.getElementDescriptor(0).scalarTypeName(mapDescriptor.getElementAnnotations(0))
337         val valueTypeName = valueDescriptor.protobufTypeName(mapDescriptor.getElementAnnotations(1))
338         append("  map<").append(keyTypeName).append(", ").append(valueTypeName).append(">")
339 
340         return if (valueDescriptor.isProtobufMessageOrEnum) {
341             listOf(valueType)
342         } else {
343             emptyList()
344         }
345     }
346 
347     private fun StringBuilder.generateListType(messageType: TypeDefinition, index: Int): List<TypeDefinition> {
348         val messageDescriptor = messageType.descriptor
349         val collectionDescriptor = messageDescriptor.getElementDescriptor(index)
350         val originalElementDescriptor = collectionDescriptor.getElementDescriptor(0)
351         val elementType = if (collectionDescriptor.kind == StructureKind.LIST) {
352             if (originalElementDescriptor.isProtobufCollection) {
353                 createNestedCollectionType(messageType, index, originalElementDescriptor, "nested collection in list")
354             } else {
355                 TypeDefinition(originalElementDescriptor)
356             }
357         } else {
358             createLegacyMapType(messageType, index, "legacy map")
359         }
360 
361         val elementDescriptor = elementType.descriptor
362 
363         if (elementDescriptor.isNullable) {
364             appendLine("  // WARNING: nullable elements of collections can not be represented in protobuf")
365         }
366         generateCollectionAbsenceComment(messageDescriptor, collectionDescriptor, index)
367 
368         val typeName = elementDescriptor.protobufTypeName(messageDescriptor.getElementAnnotations(index))
369         append("  repeated ").append(typeName)
370 
371         return if (elementDescriptor.isProtobufMessageOrEnum) {
372             listOf(elementType)
373         } else {
374             emptyList()
375         }
376     }
377 
378     private fun StringBuilder.generateEnum(enumType: TypeDefinition) {
379         val enumDescriptor = enumType.descriptor
380         val enumName = enumDescriptor.messageOrEnumName
381         enumName.checkIsValidIdentifier {
382             "Invalid name for the enum in protobuf schema '$enumName'. Serial name of the enum " +
383                     "class '${enumDescriptor.serialName}'"
384         }
385         val safeSerialName = removeLineBreaks(enumDescriptor.serialName)
386         if (safeSerialName != enumName) {
387             append("// serial name '").append(safeSerialName).appendLine('\'')
388         }
389 
390         append("enum ").append(enumName).appendLine(" {")
391 
392         val usedNumbers: MutableSet<Int> = mutableSetOf()
393         val duplicatedNumbers: MutableSet<Int> = mutableSetOf()
394         enumDescriptor.elementDescriptors.forEachIndexed { index, element ->
395             val elementName = element.protobufEnumElementName
396             elementName.checkIsValidIdentifier {
397                 "The enum element name '$elementName' is invalid in the " +
398                         "protobuf schema. Serial name of the enum class '${enumDescriptor.serialName}'"
399             }
400 
401             val annotations = enumDescriptor.getElementAnnotations(index)
402             val number = annotations.filterIsInstance<ProtoNumber>().singleOrNull()?.number ?: index
403             if (!usedNumbers.add(number)) {
404                 duplicatedNumbers.add(number)
405             }
406 
407             append("  ").append(elementName).append(" = ").append(number).appendLine(';')
408         }
409         if (duplicatedNumbers.isNotEmpty()) {
410             throw IllegalArgumentException(
411                 "The class with serial name ${enumDescriptor.serialName} has duplicate " +
412                     "elements with numbers $duplicatedNumbers"
413             )
414         }
415 
416         appendLine('}')
417     }
418 
419     private val SerialDescriptor.isOpenPolymorphic: Boolean
420         get() = kind == PolymorphicKind.OPEN
421 
422     private val SerialDescriptor.isSealedPolymorphic: Boolean
423         get() = kind == PolymorphicKind.SEALED
424 
425     private val SerialDescriptor.isProtobufNamedType: Boolean
426         get() = isProtobufMessageOrEnum || isProtobufScalar
427 
428     private val SerialDescriptor.isProtobufScalar: Boolean
429         get() = (kind is PrimitiveKind)
430                 || (kind is StructureKind.LIST && getElementDescriptor(0).kind === PrimitiveKind.BYTE)
431                 || kind == SerialKind.CONTEXTUAL
432 
433     private val SerialDescriptor.isProtobufMessageOrEnum: Boolean
434         get() = isProtobufMessage || isProtobufEnum
435 
436     private val SerialDescriptor.isProtobufMessage: Boolean
437         get() = kind == StructureKind.CLASS || kind == StructureKind.OBJECT || kind == PolymorphicKind.SEALED || kind == PolymorphicKind.OPEN
438 
439     private val SerialDescriptor.isProtobufCollection: Boolean
440         get() = isProtobufRepeated || isProtobufMap
441 
442     private val SerialDescriptor.isProtobufRepeated: Boolean
443         get() = (kind == StructureKind.LIST && getElementDescriptor(0).kind != PrimitiveKind.BYTE)
444                 || (kind == StructureKind.MAP && !getElementDescriptor(0).isValidMapKey)
445 
446     private val SerialDescriptor.isProtobufMap: Boolean
447         get() = kind == StructureKind.MAP && getElementDescriptor(0).isValidMapKey
448 
449     private val SerialDescriptor.isProtobufEnum: Boolean
450         get() = kind == SerialKind.ENUM
451 
452     private val SerialDescriptor.isValidMapKey: Boolean
453         get() = kind == PrimitiveKind.INT || kind == PrimitiveKind.LONG || kind == PrimitiveKind.BOOLEAN || kind == PrimitiveKind.STRING
454 
455 
456     private val SerialDescriptor.messageOrEnumName: String
457         get() = (serialName.substringAfterLast('.', serialName)).removeSuffix("?")
458 
459     private fun SerialDescriptor.isChildOneOfMessage(index: Int): Boolean =
460         this.getElementDescriptor(index).isSealedPolymorphic && this.getElementAnnotations(index).any { it is ProtoOneOf }
461 
462     private fun SerialDescriptor.protobufTypeName(annotations: List<Annotation> = emptyList()): String {
463         return if (isProtobufScalar) {
464             scalarTypeName(annotations)
465         } else {
466             messageOrEnumName
467         }
468     }
469 
470     private val SerialDescriptor.protobufEnumElementName: String
471         get() = serialName.substringAfterLast('.', serialName)
472 
473     private fun SerialDescriptor.scalarTypeName(annotations: List<Annotation> = emptyList()): String {
474         val integerType = annotations.filterIsInstance<ProtoType>().firstOrNull()?.type ?: ProtoIntegerType.DEFAULT
475 
476         if (kind == SerialKind.CONTEXTUAL) {
477             return "bytes"
478         }
479 
480         if (kind is StructureKind.LIST && getElementDescriptor(0).kind == PrimitiveKind.BYTE) {
481             return "bytes"
482         }
483 
484         return when (kind as PrimitiveKind) {
485             PrimitiveKind.BOOLEAN -> "bool"
486             PrimitiveKind.BYTE, PrimitiveKind.CHAR, PrimitiveKind.SHORT, PrimitiveKind.INT ->
487                 when (integerType) {
488                     ProtoIntegerType.DEFAULT -> "int32"
489                     ProtoIntegerType.SIGNED -> "sint32"
490                     ProtoIntegerType.FIXED -> "fixed32"
491                 }
492             PrimitiveKind.LONG ->
493                 when (integerType) {
494                     ProtoIntegerType.DEFAULT -> "int64"
495                     ProtoIntegerType.SIGNED -> "sint64"
496                     ProtoIntegerType.FIXED -> "fixed64"
497                 }
498             PrimitiveKind.FLOAT -> "float"
499             PrimitiveKind.DOUBLE -> "double"
500             PrimitiveKind.STRING -> "string"
501         }
502     }
503 
504     @SuppressAnimalSniffer // Boolean.hashCode(boolean) in compiler-generated hashCode implementation
505     private data class TypeDefinition(
506         val descriptor: SerialDescriptor,
507         val isSynthetic: Boolean = false,
508         val ability: String? = null,
509         val containingMessageName: String? = null,
510         val fieldName: String? = null
511     )
512 
513     private val SyntheticPolymorphicType = TypeDefinition(
514         buildClassSerialDescriptor("KotlinxSerializationPolymorphic") {
515             element("type", PrimitiveSerialDescriptor("typeDescriptor", PrimitiveKind.STRING))
516             element("value", buildSerialDescriptor("valueDescriptor", StructureKind.LIST) {
517                 element("0", Byte.serializer().descriptor)
518             })
519         },
520         true,
521         "polymorphic types"
522     )
523 
524     private class NotNullSerialDescriptor(val original: SerialDescriptor) : SerialDescriptor by original {
525         override val isNullable = false
526     }
527 
528     private val SerialDescriptor.notNull get() = NotNullSerialDescriptor(this)
529 
530     private fun StringBuilder.generateCollectionAbsenceComment(
531         messageDescriptor: SerialDescriptor,
532         collectionDescriptor: SerialDescriptor,
533         index: Int
534     ) {
535         if (!collectionDescriptor.isNullable && messageDescriptor.isElementOptional(index)) {
536             appendLine("  // WARNING: a default value decoded when value is missing")
537         } else if (collectionDescriptor.isNullable && !messageDescriptor.isElementOptional(index)) {
538             appendLine("  // WARNING: an empty collection decoded when a value is missing")
539         } else if (collectionDescriptor.isNullable && messageDescriptor.isElementOptional(index)) {
540             appendLine("  // WARNING: a default value decoded when value is missing")
541         }
542     }
543 
544     private fun createLegacyMapType(
545         messageType: TypeDefinition,
546         index: Int,
547         description: String
548     ): TypeDefinition {
549         val messageDescriptor = messageType.descriptor
550         val fieldDescriptor = messageDescriptor.getElementDescriptor(index)
551         val fieldName = messageDescriptor.getElementName(index)
552         val messageName = messageDescriptor.messageOrEnumName
553 
554         val wrapperName = "${messageName}_${fieldName}"
555         val wrapperDescriptor = buildClassSerialDescriptor(wrapperName) {
556             element("key", fieldDescriptor.getElementDescriptor(0).notNull)
557             element("value", fieldDescriptor.getElementDescriptor(1).notNull)
558         }
559 
560         return TypeDefinition(
561             wrapperDescriptor,
562             true,
563             description,
564             messageType.containingMessageName ?: messageName,
565             messageType.fieldName ?: fieldName
566         )
567     }
568 
569     private fun createNestedCollectionType(
570         messageType: TypeDefinition,
571         index: Int,
572         elementDescriptor: SerialDescriptor,
573         description: String
574     ): TypeDefinition {
575         val messageDescriptor = messageType.descriptor
576         val fieldName = messageDescriptor.getElementName(index)
577         val messageName = messageDescriptor.messageOrEnumName
578 
579         val wrapperName = "${messageName}_${fieldName}"
580         val wrapperDescriptor = buildClassSerialDescriptor(wrapperName) {
581             element("value", elementDescriptor.notNull)
582         }
583 
584         return TypeDefinition(
585             wrapperDescriptor,
586             true,
587             description,
588             messageType.containingMessageName ?: messageName,
589             messageType.fieldName ?: fieldName
590         )
591     }
592 
593     private fun removeLineBreaks(text: String): String {
594         return text.replace('\n', ' ').replace('\r', ' ')
595     }
596 
597     private val IDENTIFIER_REGEX = Regex("[A-Za-z][A-Za-z0-9_]*")
598 
599     private fun String.checkIsValidFullIdentifier(messageSupplier: (String) -> String) {
600         if (split('.').any { !it.matches(IDENTIFIER_REGEX) }) {
601             throw IllegalArgumentException(messageSupplier.invoke(this))
602         }
603     }
604 
605     private fun String.checkIsValidIdentifier(messageSupplier: () -> String) {
606         if (!matches(IDENTIFIER_REGEX)) {
607             throw IllegalArgumentException(messageSupplier.invoke())
608         }
609     }
610 }
611