All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.jetbrains.kotlinx.dataframe.io.readOpenapi.kt Maven / Gradle / Ivy

@file:Suppress("ktlint:standard:filename")

package org.jetbrains.kotlinx.dataframe.io

import io.swagger.parser.OpenAPIParser
import io.swagger.v3.oas.models.media.ArraySchema
import io.swagger.v3.oas.models.media.Schema
import io.swagger.v3.parser.core.models.AuthorizationValue
import io.swagger.v3.parser.core.models.ParseOptions
import io.swagger.v3.parser.core.models.SwaggerParseResult
import org.jetbrains.dataframe.impl.codeGen.CodeGenerator
import org.jetbrains.dataframe.impl.codeGen.InterfaceGenerationMode
import org.jetbrains.kotlinx.dataframe.DataFrame
import org.jetbrains.kotlinx.dataframe.DataRow
import org.jetbrains.kotlinx.dataframe.annotations.DataSchema
import org.jetbrains.kotlinx.dataframe.api.JsonPath
import org.jetbrains.kotlinx.dataframe.codeGen.FieldType
import org.jetbrains.kotlinx.dataframe.codeGen.GeneratedField
import org.jetbrains.kotlinx.dataframe.codeGen.Marker
import org.jetbrains.kotlinx.dataframe.codeGen.MarkerVisibility
import org.jetbrains.kotlinx.dataframe.codeGen.ValidFieldName
import org.jetbrains.kotlinx.dataframe.codeGen.isNullable
import org.jetbrains.kotlinx.dataframe.codeGen.name
import org.jetbrains.kotlinx.dataframe.codeGen.toNotNullable
import org.jetbrains.kotlinx.dataframe.codeGen.toNullable
import org.jetbrains.kotlinx.dataframe.io.OpenApiType.Any.getType
import org.jetbrains.kotlinx.dataframe.io.OpenApiType.AnyObject.getType
import org.jetbrains.kotlinx.dataframe.io.OpenApiType.Array.getTypeAsFrame
import org.jetbrains.kotlinx.dataframe.io.OpenApiType.Array.getTypeAsFrameList
import org.jetbrains.kotlinx.dataframe.io.OpenApiType.Array.getTypeAsList
import org.jetbrains.kotlinx.dataframe.io.OpenApiType.Boolean.getType
import org.jetbrains.kotlinx.dataframe.io.OpenApiType.Integer.getType
import org.jetbrains.kotlinx.dataframe.io.OpenApiType.Number.getType
import org.jetbrains.kotlinx.dataframe.io.OpenApiType.Object.getType
import org.jetbrains.kotlinx.dataframe.io.OpenApiType.String.getType
import org.jetbrains.kotlinx.jupyter.api.Code
import kotlin.reflect.typeOf

/** Parse and read OpenApi specification to [DataSchema] interfaces. */
public fun readOpenApi(
    uri: String,
    name: String,
    auth: List? = null,
    options: ParseOptions? = null,
    extensionProperties: Boolean,
    generateHelperCompanionObject: Boolean,
    visibility: MarkerVisibility = MarkerVisibility.IMPLICIT_PUBLIC,
): Code {
    require(isOpenApi(uri)) { "Not an OpenApi specification with type schemas: $uri" }

    return readOpenApi(
        swaggerParseResult = OpenAPIParser().readLocation(uri, auth, options),
        name = name,
        extensionProperties = extensionProperties,
        visibility = visibility,
        generateHelperCompanionObject = generateHelperCompanionObject,
    )
}

/** Parse and read OpenApi specification to [DataSchema] interfaces. */
public fun readOpenApiAsString(
    openApiAsString: String,
    name: String,
    auth: List? = null,
    options: ParseOptions? = null,
    extensionProperties: Boolean,
    generateHelperCompanionObject: Boolean,
    visibility: MarkerVisibility = MarkerVisibility.IMPLICIT_PUBLIC,
): Code {
    require(isOpenApiStr(openApiAsString)) { "Not an OpenApi specification with type schemas: $openApiAsString" }

    return readOpenApi(
        swaggerParseResult = OpenAPIParser().readContents(openApiAsString, auth, options),
        name = name,
        extensionProperties = extensionProperties,
        visibility = visibility,
        generateHelperCompanionObject = generateHelperCompanionObject,
    )
}

/**
 * Converts a parsed OpenAPI specification into [Code] consisting of [DataSchema] interfaces.
 *
 * @param swaggerParseResult the result of parsing an OpenAPI specification, created using [readOpenApi] or [readOpenApiAsString].
 * @param extensionProperties whether to add extension properties to the generated interfaces. This is usually not
 *   necessary, since both the KSP- and the Gradle plugin, will add extension properties to the generated code.
 * @param visibility the visibility of the generated marker classes.
 *
 * @return a [Code] object, representing the generated code.
 */
private fun readOpenApi(
    swaggerParseResult: SwaggerParseResult,
    name: String,
    extensionProperties: Boolean,
    generateHelperCompanionObject: Boolean,
    visibility: MarkerVisibility = MarkerVisibility.IMPLICIT_PUBLIC,
): Code {
    val openApi = swaggerParseResult.openAPI
        ?: error("Failed to parse OpenAPI, ${swaggerParseResult.messages.toList()}")

    val topInterfaceName = ValidFieldName.of(name)

    // take the components.schemas from the openApi spec and convert them to a list of Markers, representing the
    // interfaces, enums, and typeAliases that need to be generated.
    val result = openApi.components?.schemas
        ?.toMap()
        ?.toMarkers(topInterfaceName)
        ?.toList()
        ?: emptyList()

    // generate the code for the markers in result
    val codeGenerator = CodeGenerator.create(useFqNames = true)

    fun toCode(marker: OpenApiMarker): Code =
        codeGenerator.generate(
            marker = marker
                .withVisibility(visibility)
                .withName(
                    name = marker.name.withoutTopInterfaceName(topInterfaceName),
                    prependTopInterfaceName = false,
                ),
            interfaceMode = when (marker) {
                is OpenApiMarker.Enum -> InterfaceGenerationMode.Enum
                is OpenApiMarker.Interface -> InterfaceGenerationMode.WithFields
                is OpenApiMarker.TypeAlias, is OpenApiMarker.MarkerAlias -> InterfaceGenerationMode.TypeAlias
            },
            extensionProperties = false,
            readDfMethod = if (marker is OpenApiMarker.Interface) DefaultReadOpenApiMethod else null,
        ).declarations

    fun Code.merge(other: Code): Code = "$this\n$other"

    fun toExtensionProperties(marker: OpenApiMarker): Code =
        if (marker !is OpenApiMarker.Interface) {
            ""
        } else {
            codeGenerator.generate(
                marker = marker.withVisibility(visibility),
                interfaceMode = InterfaceGenerationMode.None,
                extensionProperties = true,
                readDfMethod = null,
            ).declarations
        }

    val (typeAliases, markers) = result
        .partition { it is OpenApiMarker.TypeAlias || it is OpenApiMarker.MarkerAlias }

    val generatedMarkers = markers
        .map(::toCode)
        .reduceOrNull(Code::merge)
        ?: ""

    val generatedTypeAliases = typeAliases
        .map(::toCode)
        .reduceOrNull(Code::merge)
        ?: ""

    val generatedExtensionProperties =
        if (!extensionProperties) {
            ""
        } else {
            result
                .map(::toExtensionProperties)
                .reduceOrNull(Code::merge)
                ?: ""
        }

    val helperCompanionObject =
        if (!generateHelperCompanionObject) {
            ""
        } else {
            val accessors = markers
                .filterIsInstance()
                .joinToString("\n|        ") {
                    "val ${it.name.withoutTopInterfaceName(topInterfaceName)} = ${it.name}.Companion"
                }

            """
            |    companion object {
            |        $accessors
            |    }
            """
        }

    return """
             |interface ${topInterfaceName.quotedIfNeeded} {
             |    $helperCompanionObject
             |    ${generatedMarkers.replace("\n", "\n|    ")}
             |}
             |${generatedTypeAliases.replace("\n", "\n|")}
             |${generatedExtensionProperties.replace("\n", "\n|")}
        """.trimMargin()
}

/**
 * Converts named OpenApi schemas to a list of [OpenApiMarker]s.
 * Will cause an exception for circular references, however they shouldn't occur in OpenApi specs.
 *
 * Some explanation:
 * OpenApi provides schemas for all the types used. For each type, we want to generate a [Marker]
 * (Which can be an interface, enum or typealias). However, the OpenApi schema is not ordered per se,
 * so when we are reading the schema it might be that we have a reference to a (super)type
 * (which are queried using `getRefMarker`) for which we have not yet created a [Marker].
 * In that case, we "pause" that one (by returning `CannotFindRefMarker`) and try to read another type schema first.
 * Circular references cannot exist since it's encoded in JSON, so we never get stuck in an infinite loop.
 * When all markers are "retrieved" (so turned from a [RetrievableMarker] to a [MarkerResult.OpenApiMarker]),
 * we're done and have converted everything!
 * As for `produceAdditionalMarker`: In OpenAPI not all enums/objects have to be defined as a separate schema.
 * Although recommended, you can still define an object anonymously directly as a type. For this, we have
 * `produceAdditionalMarker` since during the conversion of a schema -> [Marker] we get an additional new [Marker].
 */
private fun Map>.toMarkers(topInterfaceName: ValidFieldName): List {
    // Convert the schemas to toMarker calls that can be repeated to resolve references.
    val retrievableMarkers = mapValues { (typeName, value) ->
        RetrievableMarker { getRefMarker, produceAdditionalMarker ->
            value.toMarker(
                typeName = typeName,
                getRefMarker = getRefMarker,
                produceAdditionalMarker = produceAdditionalMarker,
                topInterfaceName = topInterfaceName,
            )
        }
    }.toMutableMap()

    // Retrieved Markers will be collected here
    val markers = mutableMapOf()

    // Function to get a marker from [markers] by name, see explanation above.
    val getRefMarker = GetRefMarker { MarkerResult.fromNullable(markers[it]) }

    // convert all the retrievable markers to actual markers, resolving references as we go and if possible
    while (retrievableMarkers.isNotEmpty()) {
        try {
            retrievableMarkers.entries.first { (name, retrieveMarker) ->
                // To avoid producing additional markers twice due to a CannotFindRefMarker, save them here first
                val additionalMarkers = mutableMapOf()

                // Function to produce additional markers during conversion, see explanation above.
                val produceAdditionalMarker = ProduceAdditionalMarker { validName, marker, _ ->
                    var result = ValidFieldName.of(validName.unquoted)
                    val baseName = result
                    var attempt = 1
                    while (result.quotedIfNeeded in markers || result.quotedIfNeeded in additionalMarkers) {
                        result = ValidFieldName.of(
                            baseName.unquoted + (if (result.needsQuote) " ($attempt)" else "$attempt"),
                        )
                        attempt++
                    }

                    additionalMarkers[result.quotedIfNeeded] = marker.withName(result.quotedIfNeeded)
                    result.quotedIfNeeded
                }

                val res = retrieveMarker(
                    getRefMarker = getRefMarker,
                    produceAdditionalMarker = produceAdditionalMarker,
                )

                when (res) {
                    is MarkerResult.OpenApiMarker -> {
                        markers[name] = res.marker
                        markers += additionalMarkers
                        retrievableMarkers -= name
                        true // Marker is retrieved completely, remove it from the map
                    }

                    is MarkerResult.CannotFindRefMarker ->
                        false // Cannot find a referenced Marker for this one, so we'll try again later
                }
            }
        } catch (e: NoSuchElementException) {
            throw IllegalStateException(
                "Exception while converting OpenApi schemas to markers. ${retrievableMarkers.keys.toList()} cannot find a ref marker.",
                e,
            )
        }
    }

    return markers.values.toList()
}

/**
 * Converts a single OpenApi object type schema to an [OpenApiMarker] if successful.
 *
 * Can handle the following cases:
 * - `allOf:` combining multiple objects into one with inheritance.
 * - `enum:` creating an enum of any type.
 * - `type: object`
 *     - `properties:` (`additionalProperties` are ignored) creating an [OpenApiMarker.Interface] using the fields in the properties.
 *     - `additionalProperties:` (if `properties` is not present) creating an [OpenApiMarker.AdditionalPropertiesInterface] using the additionalProperties schema as type of `value`.
 * - `type:` if type is something else, generating a type alias for it. This can be a [OpenApiMarker.TypeAlias] or a [OpenApiMarker.MarkerAlias].
 *
 * @param typeName The name of the schema / type to convert.
 * @param getRefMarker Function to retrieve a [Marker] for a given reference name.
 * @param produceAdditionalMarker Function to produce an additional [Marker] on the fly, such as for
 *   inline enums/classes in arrays.
 * @param required Optional list of required properties for this schema.
 *
 * @return A [MarkerResult.OpenApiMarker] if successful, otherwise [MarkerResult.CannotFindRefMarker].
 */
private fun Schema<*>.toMarker(
    typeName: String,
    getRefMarker: GetRefMarker,
    produceAdditionalMarker: ProduceAdditionalMarker,
    topInterfaceName: ValidFieldName,
    required: List = emptyList(),
): MarkerResult {
    @Suppress("NAME_SHADOWING")
    val required = (this.required ?: emptyList()) + required
    val nullable = nullable ?: false
    return when {
        // If allOf is defined, multiple objects are to be composed together. This is done using inheritance.
        // https://swagger.io/docs/specification/data-models/oneof-anyof-allof-not/#allof
        allOf != null -> {
            val allOfSchemas = allOf!!.associateWith {
                it.toOpenApiType(getRefMarker = getRefMarker)
            }

            // An un-required super field might be required from a child schema.
            val requiredFields =
                (allOfSchemas.keys.flatMap { it.required ?: emptyList() } + required).distinct()

            // combine all schemas into a single schema by combining their supertypes and fields
            val superMarkers = mutableListOf()
            val fields = mutableListOf()
            val additionalPropertyPaths = mutableListOf()
            for ((schema, openApiTypeResult) in allOfSchemas) {
                when (openApiTypeResult) {
                    is OpenApiTypeResult.CannotFindRefMarker ->
                        return MarkerResult.CannotFindRefMarker

                    is OpenApiTypeResult.UsingRef -> {
                        val superMarker = openApiTypeResult.marker
                        superMarkers += superMarker

                        additionalPropertyPaths += superMarker.additionalPropertyPaths

                        // make sure required fields are overridden to be non-null
                        val allSuperFields =
                            (superMarker.fields + superMarker.allSuperMarkers.values.flatMap { it.fields })
                                .distinctBy { it.fieldName.unquoted }

                        fields += allSuperFields
                            .filter {
                                it.fieldName.unquoted in requiredFields && it.fieldType.isNullable()
                            }.map {
                                generatedFieldOf(
                                    fieldName = it.fieldName,
                                    columnName = it.columnName,
                                    fieldType = it.fieldType.toNotNullable(),
                                    overrides = true,
                                )
                            }
                    }

                    is OpenApiTypeResult.Enum -> error("allOf cannot contain enum types")

                    is OpenApiTypeResult.OpenApiType -> {
                        val (openApiType, nullable) = openApiTypeResult

                        // must be an object
                        openApiType as OpenApiType.Object

                        // create a temp marker so its fields can be merged in the allOf
                        var tempMarker: OpenApiMarker? = null

                        val fieldTypeResult = openApiType.toFieldType(
                            schema = schema,
                            schemaName = typeName,
                            nullable = nullable,
                            getRefMarker = getRefMarker,
                            produceAdditionalMarker = { name, marker, isTopLevelObject ->
                                // the top-level object must not be produced as additional marker.
                                // instead, we just need it to be the tempMarker for which we gather just the fields.
                                if (isTopLevelObject) {
                                    tempMarker = marker
                                    name.quotedIfNeeded
                                } else {
                                    produceAdditionalMarker(name, marker, false)
                                }
                            },
                            required = required,
                            topInterfaceName = topInterfaceName,
                        )

                        when (fieldTypeResult) {
                            is FieldTypeResult.CannotFindRefMarker -> {
                                return MarkerResult.CannotFindRefMarker
                            }

                            // extract the fields from tempMarker
                            is FieldTypeResult.FieldType -> {
                                fields += tempMarker!!.fields
                                additionalPropertyPaths += fieldTypeResult.additionalPropertyPaths
                            }
                        }
                    }
                }
            }

            MarkerResult.OpenApiMarker(
                OpenApiMarker.Interface(
                    nullable = nullable,
                    name = typeName,
                    fields = fields,
                    superMarkers = superMarkers,
                    additionalPropertyPaths = additionalPropertyPaths,
                    topInterfaceName = topInterfaceName,
                ),
            )
        }

        // If enum is defined, create an enum class.
        // https://swagger.io/docs/specification/data-models/enums/
        enum != null -> {
            val openApiTypeResult = toOpenApiType(
                getRefMarker = getRefMarker,
            ) as OpenApiTypeResult.Enum // must be an enum

            val enumMarker = produceNewEnum(
                name = typeName,
                topInterfaceName = topInterfaceName,
                values = openApiTypeResult.values,
                nullable = openApiTypeResult.nullable,
                produceAdditionalMarker = ProduceAdditionalMarker.NOOP, // we need it here, not as additional marker
            )

            MarkerResult.OpenApiMarker(enumMarker)
        }

        // If type == object, create a new Marker to become an interface.
        // https://swagger.io/docs/specification/data-models/data-types/#object
        type == "object" -> when {
            // Gather the given properties as fields
            properties != null -> {
                if (additionalProperties != null && additionalProperties != false) {
                    println(
                        "OpenAPI warning: type $name has both properties and additionalProperties defined, but only properties will be generated in the data schema.",
                    )
                }

                val keyValuePaths = mutableListOf()

                // build a list of fields from properties
                val fields = buildList {
                    for ((name, property) in (properties ?: emptyMap())) {
                        val isRequired = name in required

                        // find the OpenApiType of the property (or ref or enum)
                        val openApiTypeResult = property.toOpenApiType(
                            getRefMarker = getRefMarker,
                        )

                        when (openApiTypeResult) {
                            is OpenApiTypeResult.CannotFindRefMarker ->
                                return MarkerResult.CannotFindRefMarker

                            is OpenApiTypeResult.UsingRef -> {
                                keyValuePaths += openApiTypeResult.marker
                                    .additionalPropertyPaths
                                    .map { it.prepend(name) }

                                val validName = ValidFieldName.of(name.snakeToLowerCamelCase())

                                // find the field type of the marker reference
                                val fieldType = openApiTypeResult.marker.toFieldType()
                                    .let { if (!isRequired) it.toNullable() else it }

                                this += generatedFieldOf(
                                    overrides = false,
                                    fieldName = validName,
                                    columnName = name,
                                    fieldType = fieldType,
                                )
                            }

                            is OpenApiTypeResult.Enum -> {
                                // inner enum, so produce it as additional
                                val enumMarker = produceNewEnum(
                                    name = name,
                                    topInterfaceName = topInterfaceName,
                                    values = openApiTypeResult.values,
                                    produceAdditionalMarker = produceAdditionalMarker,
                                    nullable = openApiTypeResult.nullable,
                                )

                                this += generatedFieldOf(
                                    overrides = false,
                                    fieldName = ValidFieldName.of(name.snakeToLowerCamelCase()),
                                    columnName = name,
                                    fieldType = FieldType.ValueFieldType(
                                        typeFqName = enumMarker.name +
                                            if (enumMarker.nullable || !isRequired) "?" else "",
                                    ),
                                )
                            }

                            is OpenApiTypeResult.OpenApiType -> {
                                val (openApiType, nullable) = openApiTypeResult

                                val fieldTypeResult = openApiType.toFieldType(
                                    schema = property,
                                    schemaName = name,
                                    nullable = nullable,
                                    getRefMarker = getRefMarker,
                                    produceAdditionalMarker = produceAdditionalMarker,
                                    required = required,
                                    topInterfaceName = topInterfaceName,
                                )

                                when (fieldTypeResult) {
                                    is FieldTypeResult.CannotFindRefMarker ->
                                        return MarkerResult.CannotFindRefMarker

                                    is FieldTypeResult.FieldType -> {
                                        val validName = ValidFieldName.of(name.snakeToLowerCamelCase())

                                        keyValuePaths += fieldTypeResult
                                            .additionalPropertyPaths
                                            .map { it.prepend(name) }

                                        this += generatedFieldOf(
                                            overrides = false,
                                            fieldName = validName,
                                            columnName = name,
                                            fieldType = fieldTypeResult.fieldType.let {
                                                if (!isRequired) it.toNullable() else it
                                            },
                                        )
                                    }
                                }
                            }
                        }
                    }
                }

                MarkerResult.OpenApiMarker(
                    OpenApiMarker.Interface(
                        nullable = nullable,
                        name = typeName,
                        fields = fields,
                        superMarkers = emptyList(),
                        additionalPropertyPaths = keyValuePaths,
                        topInterfaceName = topInterfaceName,
                    ),
                )
            }

            // Create this object as a map-like type
            properties == null && additionalProperties != null && additionalProperties != false -> {
                val openApiTypeResult = (additionalProperties as? Schema<*>)
                    ?.toOpenApiType(getRefMarker = getRefMarker)

                val additionalPropertyPaths = mutableListOf()
                val valueType = when (openApiTypeResult) {
                    is OpenApiTypeResult.CannotFindRefMarker ->
                        return MarkerResult.CannotFindRefMarker

                    is OpenApiTypeResult.UsingRef -> {
                        val marker = openApiTypeResult.marker
                        additionalPropertyPaths += marker.additionalPropertyPaths.map {
                            it.prependWildcard()
                        }
                        marker.toFieldType()
                    }

                    is OpenApiTypeResult.OpenApiType -> {
                        val fieldTypeResult = openApiTypeResult
                            .openApiType
                            .toFieldType(
                                schema = this,
                                schemaName = typeName,
                                nullable = openApiTypeResult.nullable,
                                getRefMarker = getRefMarker,
                                produceAdditionalMarker = produceAdditionalMarker,
                                required = required,
                                topInterfaceName = topInterfaceName,
                            )

                        when (fieldTypeResult) {
                            FieldTypeResult.CannotFindRefMarker ->
                                return MarkerResult.CannotFindRefMarker

                            is FieldTypeResult.FieldType -> {
                                additionalPropertyPaths += fieldTypeResult.additionalPropertyPaths.map {
                                    it.prependWildcard()
                                }

                                fieldTypeResult.fieldType
                            }
                        }
                    }

                    is OpenApiTypeResult.Enum -> {
                        // inner enum, so produce it as additional
                        val enumMarker = produceNewEnum(
                            name = name,
                            topInterfaceName = topInterfaceName,
                            values = openApiTypeResult.values,
                            produceAdditionalMarker = produceAdditionalMarker,
                            nullable = openApiTypeResult.nullable,
                        )

                        FieldType.ValueFieldType(
                            typeFqName = enumMarker.name + if (enumMarker.nullable) "?" else "",
                        )
                    }

                    null -> FieldType.ValueFieldType(
                        typeFqName = typeOf().toString(),
                    )
                }

                MarkerResult.OpenApiMarker(
                    OpenApiMarker.AdditionalPropertyInterface(
                        nullable = nullable,
                        valueType = valueType,
                        name = ValidFieldName.of(typeName).quotedIfNeeded,
                        additionalPropertyPaths = additionalPropertyPaths,
                        topInterfaceName = topInterfaceName,
                    ),
                )
            }

            else -> MarkerResult.OpenApiMarker(
                OpenApiMarker.Interface(
                    nullable = nullable,
                    name = typeName,
                    fields = emptyList(),
                    superMarkers = emptyList(),
                    additionalPropertyPaths = emptyList(),
                    topInterfaceName = topInterfaceName,
                ),
            )
        }

        // If type is something else, produce it as type alias. Can be a reference to another OpenApi type or something else.
        else -> {
            val openApiTypeResult = toOpenApiType(
                getRefMarker = getRefMarker,
            )

            val typeAliasMarker = when (openApiTypeResult) {
                is OpenApiTypeResult.CannotFindRefMarker ->
                    return MarkerResult.CannotFindRefMarker

                is OpenApiTypeResult.UsingRef -> OpenApiMarker.MarkerAlias(
                    name = ValidFieldName.of(typeName).quotedIfNeeded,
                    superMarker = openApiTypeResult.marker,
                    topInterfaceName = topInterfaceName,
                    nullable = nullable,
                )

                is OpenApiTypeResult.OpenApiType -> {
                    val typeResult = openApiTypeResult
                        .openApiType
                        .toFieldType(
                            schema = this,
                            schemaName = typeName,
                            nullable = false,
                            getRefMarker = getRefMarker,
                            produceAdditionalMarker = produceAdditionalMarker,
                            required = required,
                            topInterfaceName = topInterfaceName,
                        )

                    val superMarkerName = when (typeResult) {
                        is FieldTypeResult.CannotFindRefMarker ->
                            return MarkerResult.CannotFindRefMarker

                        is FieldTypeResult.FieldType ->
                            when (typeResult.fieldType) {
                                is FieldType.ValueFieldType, is FieldType.GroupFieldType ->
                                    typeResult.fieldType.name

                                is FieldType.FrameFieldType ->
                                    "${DataFrame::class.qualifiedName!!}<${typeResult.fieldType.name}>"
                            }
                    }

                    OpenApiMarker.TypeAlias(
                        nullable = nullable,
                        name = ValidFieldName.of(typeName).quotedIfNeeded,
                        superMarkerName = superMarkerName,
                        additionalPropertyPaths = typeResult.additionalPropertyPaths,
                        topInterfaceName = topInterfaceName,
                    )
                }

                is OpenApiTypeResult.Enum -> error("cannot happen, since enum != null is checked earlier")
            }

            MarkerResult.OpenApiMarker(typeAliasMarker)
        }
    }
}

/**
 * Converts a single property of an OpenApi type schema to [OpenApiTypeResult] representing a single type for DataFrame.
 * It must either have `$ref`, `type`, `enum`, `oneOf`, `anyOf`, or `not` defined.
 * It can become an [OpenApiType], [OpenApiMarker] reference or unresolved reference (if `$ref:` is set), enum (if `enum:` is set).
 * `anyOf` and `oneOf` types are merged.
 *
 * These results still have to be converted to [FieldType]s to be able to generate [OpenApiMarker]s from it
 * (unless it's a [OpenApiTypeResult.UsingRef] of course).
 *
 * @receiver Single property of an OpenApi type schema to convert.
 * @param getRefMarker function to attempt to resolve a reference.
 * @return [OpenApiTypeResult]
 */
private fun Schema<*>.toOpenApiType(getRefMarker: GetRefMarker): OpenApiTypeResult {
    val nullable = nullable ?: false

    // if it's a reference, resolve it or try again later
    if (`$ref` != null) {
        val typeName = `$ref`.takeLastWhile { it != '/' }
        return when (val it = getRefMarker(typeName)) {
            is MarkerResult.CannotFindRefMarker ->
                OpenApiTypeResult.CannotFindRefMarker

            is MarkerResult.OpenApiMarker ->
                OpenApiTypeResult.UsingRef(it.marker)
        }
    }

    // if it's an enum, return the enum
    if (enum != null) {
        // nullability of an enum is given only by the enum itself
        // https://github.com/OAI/OpenAPI-Specification/blob/main/proposals/2019-10-31-Clarify-Nullable.md#if-a-schema-specifies-nullable-true-and-enum-1-2-3-does-that-schema-allow-null-values-see-1900
        @Suppress("NAME_SHADOWING")
        val nullable = enum.any { it == null }

        return OpenApiTypeResult.Enum(
            values = enum.filterNotNull().map { it.toString() },
            nullable = nullable, // enum can still become null in Kotlin if not required
        )
    }

    var openApiType = OpenApiType.fromStringOrNull(type)

    // check for anyOf/oneOf/not, https://swagger.io/docs/specification/data-models/oneof-anyof-allof-not/
    if (openApiType == null || openApiType is OpenApiType.Any) {
        val anyOf = ((anyOf ?: emptyList()) + (oneOf ?: emptyList()))

        // gather all references if there are any, try again later if unresolved
        val anyOfRefs = anyOf.mapNotNull { it.`$ref` }.map { ref ->
            val typeName = ref.takeLastWhile { it != '/' }
            when (val it = getRefMarker(typeName)) {
                is MarkerResult.CannotFindRefMarker ->
                    return OpenApiTypeResult.CannotFindRefMarker

                is MarkerResult.OpenApiMarker -> it.marker
            }
        }

        val anyOfTypes = anyOf
            .mapNotNull { it.type }
            .mapNotNull(OpenApiType.Companion::fromStringOrNull)
            .distinct()

        val allTypes = anyOfTypes + anyOfRefs

        openApiType = when {
            // only one type
            anyOfTypes.size == 1 && anyOfRefs.isEmpty() -> anyOfTypes.first()

            // just Number-like types
            anyOfTypes.size == 2 &&
                anyOfRefs.isEmpty() &&
                anyOfTypes.containsAll(listOf(OpenApiType.Number, OpenApiType.Integer)) -> OpenApiType.Number

            !anyOfTypes.any { it.isObject } && anyOfRefs.isEmpty() -> OpenApiType.Any

            // only one ref
            anyOfTypes.isEmpty() && anyOfRefs.size == 1 ->
                return OpenApiTypeResult.UsingRef(anyOfRefs.first())

            // only refs
            anyOfTypes.isEmpty() && anyOfRefs.isNotEmpty() -> {
                val commonSuperMarker = anyOfRefs
                    .map { it.allSuperMarkers.values.toSet() }
                    .reduce(Set::intersect)
                    .firstOrNull() as? OpenApiMarker?

                if (commonSuperMarker != null) {
                    return OpenApiTypeResult.UsingRef(commonSuperMarker)
                } else {
                    OpenApiType.AnyObject
                }
            }

            // more than one ref or types
            allTypes.isNotEmpty() && allTypes.all { it.isObject } -> OpenApiType.AnyObject

            // cannot assume anything about a type when there are multiple types except one
            not != null -> OpenApiType.Any

            else -> OpenApiType.Any
        }
    }

    return OpenApiTypeResult.OpenApiType(openApiType, nullable)
}

/**
 * Converts an [OpenApiType] with [schema] to a [FieldType] if successful.
 *
 * @receiver OpenApiType to convert.
 * @param schema Schema of the property that the [OpenApiType] belongs to.
 *   Used to get extra information if needed (for arrays / objects / format etc.).
 * @param schemaName Name of the schema that the property belongs to. Used in the name generation of the
 *   additionally produced [Marker]s.
 * @param nullable Whether the [FieldType] is supposed to be nullable.
 * @param getRefMarker Function to attempt to resolve a reference.
 * @param produceAdditionalMarker Function to produce additional [Marker]s if needed.
 * @param required List of required properties. Passed down into child objects.
 * @return [FieldTypeResult]
 */
private fun OpenApiType.toFieldType(
    schema: Schema<*>,
    schemaName: String,
    nullable: Boolean,
    getRefMarker: GetRefMarker,
    produceAdditionalMarker: ProduceAdditionalMarker,
    required: List,
    topInterfaceName: ValidFieldName,
): FieldTypeResult =
    when (this) {
        is OpenApiType.Any -> FieldTypeResult.FieldType(getType(nullable))

        is OpenApiType.Boolean -> FieldTypeResult.FieldType(getType(nullable))

        is OpenApiType.Integer -> FieldTypeResult.FieldType(
            getType(
                nullable = nullable,
                format = OpenApiIntegerFormat.fromStringOrNull(schema.format),
            ),
        )

        is OpenApiType.Number -> FieldTypeResult.FieldType(
            getType(
                nullable = nullable,
                format = OpenApiNumberFormat.fromStringOrNull(schema.format),
            ),
        )

        is OpenApiType.String -> FieldTypeResult.FieldType(
            getType(
                nullable = nullable,
                format = OpenApiStringFormat.fromStringOrNull(schema.format),
            ),
        )

        // Becomes a DataRow or DataRow since we don't know the type, but we do know it's an object
        is OpenApiType.AnyObject -> FieldTypeResult.FieldType(
            getType(
                nullable = nullable,
            ),
        )

        is OpenApiType.Array -> {
            schema as ArraySchema

            if (schema.items == null) {
                // should in theory not occur, but make List just in case
                FieldTypeResult.FieldType(
                    getTypeAsList(
                        nullableArray = nullable,
                        typeFqName = OpenApiType.Any.getType(nullable = true).typeFqName,
                    ),
                )
            } else {
                // resolve the type of the contents of the array
                val arrayTypeResult = schema
                    .items!!
                    .toOpenApiType(getRefMarker = getRefMarker)

                // convert the type to a FieldType
                when (arrayTypeResult) {
                    is OpenApiTypeResult.CannotFindRefMarker ->
                        FieldTypeResult.CannotFindRefMarker

                    is OpenApiTypeResult.UsingRef ->
                        when {
                            // accessed like List>
                            arrayTypeResult.marker is OpenApiMarker.AdditionalPropertyInterface ->
                                FieldTypeResult.FieldType(
                                    fieldType = getTypeAsFrameList(
                                        nullable = arrayTypeResult.marker.nullable,
                                        nullableArray = nullable,
                                        markerName = arrayTypeResult.marker.name,
                                    ),
                                    additionalPropertyPaths = arrayTypeResult.marker.additionalPropertyPaths.map {
                                        it.prependArrayWithWildcard()
                                    },
                                )

                            // accessed like DataFrame
                            arrayTypeResult.marker.isObject ->
                                FieldTypeResult.FieldType(
                                    fieldType = getTypeAsFrame(
                                        nullable = nullable || arrayTypeResult.marker.nullable,
                                        markerName = arrayTypeResult.marker.name,
                                    ),
                                    additionalPropertyPaths = arrayTypeResult.marker.additionalPropertyPaths.map {
                                        it.prependArrayWithWildcard()
                                    },
                                )

                            // accessed like List
                            else ->
                                FieldTypeResult.FieldType(
                                    fieldType = getTypeAsList(
                                        nullableArray = nullable || arrayTypeResult.marker.nullable,
                                        typeFqName = arrayTypeResult.marker.name,
                                    ),
                                    additionalPropertyPaths = arrayTypeResult.marker.additionalPropertyPaths.map {
                                        it.prependArrayWithWildcard()
                                    },
                                )
                        }

                    is OpenApiTypeResult.OpenApiType -> {
                        // Convert openApiType of array contents to FieldType.
                        // Will produce additional markers if needed.
                        val arrayTypeSchemaResult = arrayTypeResult
                            .openApiType
                            .toFieldType(
                                schema = schema.items!!,
                                schemaName = schemaName + "Content", // type name objects in the array will get
                                nullable = arrayTypeResult.nullable,
                                getRefMarker = getRefMarker,
                                produceAdditionalMarker = produceAdditionalMarker,
                                required = emptyList(),
                                topInterfaceName = topInterfaceName,
                            )

                        when (arrayTypeSchemaResult) {
                            is FieldTypeResult.CannotFindRefMarker ->
                                FieldTypeResult.CannotFindRefMarker

                            is FieldTypeResult.FieldType -> {
                                val fieldType = arrayTypeSchemaResult.fieldType
                                val additionalPropertyPaths = arrayTypeSchemaResult
                                    .additionalPropertyPaths
                                    .map { it.prependArrayWithWildcard() }

                                FieldTypeResult.FieldType(
                                    fieldType = when {
                                        // array of OpenApiType.AnyObject -> DataFrame
                                        fieldType is FieldType.GroupFieldType &&
                                            fieldType.name == typeOf>().toString() ->
                                            getTypeAsFrame(
                                                nullable = nullable,
                                                markerName = typeOf().toString(),
                                            )

                                        // array of OpenApiType.AnyObject -> DataFrame
                                        fieldType is FieldType.GroupFieldType &&
                                            fieldType.name == typeOf>().toString() ->
                                            getTypeAsFrame(
                                                nullable = nullable,
                                                markerName = typeOf().toString(),
                                            )

                                        // array of Marker -> DataFrame
                                        fieldType is FieldType.GroupFieldType ->
                                            getTypeAsFrame(
                                                nullable = nullable,
                                                markerName = fieldType.name,
                                            )

                                        // array of DataFrames -> List>
                                        fieldType is FieldType.FrameFieldType ->
                                            getTypeAsList(
                                                nullableArray = nullable,
                                                typeFqName = "${DataFrame::class.qualifiedName}<${fieldType.name}>",
                                            )

                                        // array of primitives -> List
                                        fieldType is FieldType.ValueFieldType ->
                                            getTypeAsList(
                                                nullableArray = nullable,
                                                typeFqName = fieldType.name,
                                            )

                                        else -> error("Error reading array type")
                                    },
                                    additionalPropertyPaths = additionalPropertyPaths,
                                )
                            }
                        }
                    }

                    is OpenApiTypeResult.Enum -> {
                        // enum needs to be produced as additional marker
                        val enumMarker = produceNewEnum(
                            name = schemaName,
                            topInterfaceName = topInterfaceName,
                            values = arrayTypeResult.values,
                            produceAdditionalMarker = produceAdditionalMarker,
                            nullable = arrayTypeResult.nullable,
                        )

                        FieldTypeResult.FieldType(
                            getTypeAsList(
                                nullableArray = nullable,
                                typeFqName = enumMarker.name + if (enumMarker.nullable) "?" else "",
                            ),
                        )
                    }
                }
            }
        }

        is OpenApiType.Object -> {
            // read the schema to an OpenApiMarker
            val dataFrameSchemaResult = schema.toMarker(
                typeName = schemaName.snakeToUpperCamelCase(),
                getRefMarker = getRefMarker,
                produceAdditionalMarker = { validName, marker, _ ->
                    // ensure isTopLevelObject == false, since we go a layer deeper
                    produceAdditionalMarker(validName, marker, isTopLevelObject = false)
                },
                required = required,
                topInterfaceName = topInterfaceName,
            )

            when (dataFrameSchemaResult) {
                is MarkerResult.CannotFindRefMarker ->
                    FieldTypeResult.CannotFindRefMarker

                is MarkerResult.OpenApiMarker -> {
                    // Produce the marker as additional marker
                    val newName = produceAdditionalMarker(
                        validName = ValidFieldName.of(schemaName.snakeToUpperCamelCase()),
                        marker = dataFrameSchemaResult.marker,
                        isTopLevelObject = true, // only relevant in `allOf` cases
                    )

                    when (val marker = dataFrameSchemaResult.marker.withName(newName)) {
                        // needs to be accessed like DataFrame
                        is OpenApiMarker.AdditionalPropertyInterface ->
                            FieldTypeResult.FieldType(
                                fieldType = OpenApiType.Array.getTypeAsFrame(
                                    nullable = nullable,
                                    markerName = marker.name,
                                ),
                                additionalPropertyPaths = marker.additionalPropertyPaths,
                            )

                        // accessed like Marker (or DataRow)
                        else -> FieldTypeResult.FieldType(
                            fieldType = getType(
                                nullable = nullable,
                                marker = marker,
                            ),
                            additionalPropertyPaths = marker.additionalPropertyPaths,
                        )
                    }
                }
            }
        }
    }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy