fun generateStructures()

in generator/src/main/kotlin/space/jetbrains/api/generator/GenerateStructures.kt [66:283]


fun generateStructures(model: HttpApiEntitiesById): List<FileSpec> {
    val fieldDescriptorsByDtoId = model.buildFieldsByDtoId()

    return model.dtoAndUrlParams.values.mapNotNull { root ->
        if (root.extends != null) return@mapNotNull null

        val rootClassName = root.getClassName()
        if (rootClassName == batchInfoType) return@mapNotNull null

        val rootStructureClassName = rootClassName.getStructureClassName()

        FileSpec.builder(rootStructureClassName.packageName, rootStructureClassName.simpleName).apply {
            indent(INDENT)

            addAnnotation(
                AnnotationSpec.builder(Suppress::class)
                    .addMember("%S", "ClassName")
                    .addMember("%S", "UnusedImport")
                    .addMember("%S", "REDUNDANT_ELSE_IN_WHEN")
                    .addMember("%S", "RemoveExplicitTypeArguments")
                    .addMember("%S", "KotlinRedundantDiagnosticSuppress")
                    .build()
            )

            addAnnotation(
                AnnotationSpec.builder(ClassName("kotlin", "OptIn")).also { ann ->
                    model.featureFlags.values.forEach {
                        ann.addMember("%T::class", it.annotationClassName())
                    }
                }.build()
            )

            root.subclasses(model).forEach { dto ->
                val dtoClassName = dto.getClassName()
                val dtoStructureClassName = dtoClassName.getStructureClassName()
                Log.info {
                    "Generating structure for '${dto.name}'"
                }
                addType(TypeSpec.objectBuilder(dtoStructureClassName).also { typeBuilder ->

                    typeBuilder.superclass(typeStructureType.parameterizedBy(dtoClassName))
                    typeBuilder.addSuperclassConstructorParameter(dto.record.toString())

                    val fields = fieldDescriptorsByDtoId.getValue(dto.id)

                    typeBuilder.addProperties(fields.map {
                        PropertySpec.builder(
                            name = it.dtoField.name,
                            type = propertyType.importNested().parameterizedBy(it.dtoField.field.fieldKotlinPoetType(model)),
                            modifiers = listOf(KModifier.PRIVATE),
                        ).initializer(buildCodeBlock { appendPropertyProvider(it.dtoField, model) })
                            .build()
                    })

                    typeBuilder.addFunction(FunSpec.builder("deserialize").also func@{ funcBuilder ->
                        funcBuilder.addModifiers(KModifier.PUBLIC, KModifier.OVERRIDE)
                        funcBuilder.addParameter("context", deserializationContextType)
                        funcBuilder.returns(dtoClassName)

                        if (dto.id in model.urlParams) {
                            funcBuilder.beginControlFlow(
                                "context.json?.%M()?.let·{ compactId ->",
                                asStringOrNullFunction
                            )
                            val urlParam = model.urlParams.getValue(dto.id)
                            funcBuilder.addCode("val (fieldNames, json) = compactIdToFieldNamesAndJson(compactId)\n")
                            funcBuilder.addCode("val newContext = context.copy(json = json)\n")
                            funcBuilder.beginControlFlow("return·when (fieldNames) {")
                            urlParam.options.forEach { option ->
                                when (option) {
                                    is HA_UrlParameterOption.Const -> {
                                        funcBuilder.addCode("setOf(%S", option.value)
                                    }
                                    is HA_UrlParameterOption.Var -> {
                                        funcBuilder.addCode("setOf(")
                                        option.parameters.forEachIndexed { i, param ->
                                            if (i != 0) funcBuilder.addCode(", ")
                                            funcBuilder.addCode("%S", param.name)
                                        }
                                    }
                                }
                                funcBuilder.addCode(
                                    ") -> %T.deserialize(newContext)\n",
                                    option.getClassName().getStructureClassName()
                                )
                            }
                            funcBuilder.addCode("else -> ")
                            urlParam.options.firstNotNullOfOrNull { it as? HA_UrlParameterOption.Var }?.let {
                                funcBuilder.addCode(
                                    "%T.deserialize(newContext)\n",
                                    it.getClassName().getStructureClassName()
                                )
                            } ?: funcBuilder.addCode(
                                "minorDeserializationError(\"Unsupported parameter set: '\$fieldNames'\", context.link)\n"
                            )

                            funcBuilder.endControlFlow()
                            funcBuilder.endControlFlow()
                        }

                        val codeReferences = mutableListOf<Any>()

                        val createInstance = buildString {
                            when {
                                dto.isObject -> append("%T")
                                fields.isNotEmpty() -> {
                                    append("%T(\n$INDENT")
                                    fields.forEachIndexed { i, field ->
                                        if (i != 0) append(",\n$INDENT")
                                        append("${field.dtoField.name} = this.${field.dtoField.name}.deserialize(context)")
                                    }
                                    append("\n)")
                                }
                                else -> append("%T()")
                            }
                        }

                        val toReturn = if (dto.inheritors.isEmpty() && !dto.hierarchyRole2.isAbstract) {
                            codeReferences += dtoClassName
                            createInstance
                        } else {
                            "when (val className = context.className()) {" +
                                dto.inheritors.joinToString("\n$INDENT", "\n$INDENT", "\n") {
                                    val inheritor = model.resolveDto(it)
                                    val inheritorClassName = inheritor.getClassName()
                                    codeReferences += inheritorClassName.getStructureClassName()
                                    val condition = "\"${inheritor.name}\"" + if (inheritor.inheritors.isNotEmpty()) {
                                        codeReferences += inheritorClassName.getStructureClassName()
                                        ", in %T.childClassNames"
                                    } else ""
                                    val withActualTypeCall = if (inheritor.inheritors.isEmpty()) ".withActualType(className)" else ""
                                    "$condition -> %T.deserialize(context$withActualTypeCall)"
                                } +
                                (if (!dto.hierarchyRole2.isAbstract) {
                                    codeReferences += dtoClassName
                                    "$INDENT\"${dto.name}\" -> " +
                                        createInstance.indentNonFirst() + "\n"
                                } else "") +
                                "${INDENT}else -> minorDeserializationError(\"Unsupported class name: '\$className'\", context.link)\n}"
                        }
                        if (dto.id in model.urlParams) {
                            funcBuilder.addCode("return·$toReturn", *codeReferences.toTypedArray())
                        } else {
                            funcBuilder.addCode("return $toReturn", *codeReferences.toTypedArray())
                        }
                    }.build())

                    typeBuilder.addFunction(FunSpec.builder("serialize").also func@{ func ->
                        func.addModifiers(KModifier.PUBLIC, KModifier.OVERRIDE)
                        func.addParameter("value", dtoClassName)
                        func.returns(jsonValueType)

                        if (dto.id in model.urlParams) {
                            func.addCode("return %M(value.compactId)", jsonStringFunction)
                            return@func
                        }

                        val codeReferences = mutableListOf<Any>()

                        val createJson = "%M(listOfNotNull(" + (fields.takeIf { it.isNotEmpty() }
                            ?.joinToString(",\n$INDENT", "\n$INDENT", "\n") {
                                if (it.dtoField.field.requiresAddedNullability) {
                                    "value.${it.dtoField.name}?.let·{ this.${it.dtoField.name}.serialize(it) }"
                                } else {
                                    "this.${it.dtoField.name}.serialize(value.${it.dtoField.name})"
                                }
                            } ?: "") + "))"

                        val toReturn = if (dto.inheritors.isEmpty() && !dto.hierarchyRole2.isAbstract) {
                            codeReferences += jsonObjectFunction
                            createJson
                        } else {
                            "when (value) {" +
                                dto.inheritors.joinToString("\n$INDENT", "\n$INDENT", "\n") {
                                    val inheritor = model.resolveDto(it)
                                    val inheritorClassName = inheritor.getClassName()
                                    codeReferences += inheritorClassName
                                    codeReferences += inheritorClassName.getStructureClassName()
                                    "is %T -> %T.serialize(value).withClassName(\"${inheritor.name}\")"
                                } +
                                "${INDENT}else -> " +
                                if (!dto.hierarchyRole2.isAbstract) {
                                    codeReferences += jsonObjectFunction
                                    createJson.indentNonFirst() + ".withClassName(\"${dto.name}\")"
                                } else {
                                    "error(\"Unsupported class: '\${value::class.simpleName}'\")"
                                } +
                                "\n}"
                        }

                        func.addCode("return $toReturn", *codeReferences.toTypedArray())
                    }.build())

                    if (dto.inheritors.isNotEmpty()) {
                        typeBuilder.addProperty(
                            PropertySpec.builder("childClassNames", SET.parameterizedBy(STRING), KModifier.OVERRIDE)
                                .initializer(buildCodeBlock {
                                    add("setOf(")
                                    dto.inheritors.forEachIndexed { i, it ->
                                        if (i != 0) add(", ")
                                        add("%S", model.resolveDto(it).name)
                                    }
                                    add(")")
                                    dto.inheritors.forEach {
                                        val inheritor = model.resolveDto(it)
                                        if (inheritor.inheritors.isNotEmpty()) {
                                            add("·+ %T.childClassNames", inheritor.getClassName().getStructureClassName())
                                        }
                                    }
                                })
                                .build()
                        )
                    }
                }.build())
            }
        }.build()
    }
}