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

com.ancientlightstudios.quarkus.kotlin.openapi.parser.SchemaBuilder.kt Maven / Gradle / Ivy

There is a newer version: 0.4.14
Show newest version
package com.ancientlightstudios.quarkus.kotlin.openapi.parser

import com.ancientlightstudios.quarkus.kotlin.openapi.models.transformable.DefaultSchemaUsage
import com.ancientlightstudios.quarkus.kotlin.openapi.models.transformable.SchemaModifier
import com.ancientlightstudios.quarkus.kotlin.openapi.models.transformable.SchemaTypes
import com.ancientlightstudios.quarkus.kotlin.openapi.models.transformable.TransformableSchema
import com.ancientlightstudios.quarkus.kotlin.openapi.models.transformable.components.*
import com.ancientlightstudios.quarkus.kotlin.openapi.utils.SpecIssue
import com.fasterxml.jackson.databind.node.ObjectNode
import java.util.*

class SchemaBuilder(
    private val schema: TransformableSchema,
    private val node: ObjectNode
) {

    fun ParseContext.build() {
        // schema references can be paired with all other schema components since version 3.1. This is not allowed
        // for other references (e.g. parameter or request bodies), so this code works a little bit different.
        val components = mutableListOf()

        val baseDefinitionAvailable = addBaseDefinitionComponent(components)
        if (!baseDefinitionAvailable || openApiVersion != ApiVersion.V3_0) {
            // if no $ref was found or version is at least 3.1.x we can add other components too
            addDirectionComponent(components)
            addDefaultComponent(components)
            addTypeComponent(components)
            addFormatComponent(components)
            addNullableComponent(components)
            addArrayComponent(components)
            addObjectComponent(components)
            addAllOfComponent(components)
            addAnyOfComponent(components)
            addOneOfComponent(components)
            addContainerModelNameComponent(components)
            addEnumItemNamesComponent(components)
            addCustomConstraintsValidationComponent(components)
            addArrayValidationComponent(components)
            addObjectValidationComponent(components)
            addEnumValidationComponent(components)
            addStringValidationComponent(components)
            addNumberValidationComponent(components)
        }

        schema.components = components
        schema.name = node.getTextOrNull("x-model-name") ?: contextPath.nameSuggestion() ?: ""
    }

    private fun ParseContext.addBaseDefinitionComponent(components: MutableList): Boolean {
        return node.getTextOrNull("\$ref")?.let {
            val schema = schemaCollector.registerSchema(it)
            components.add(BaseSchemaComponent(schema))
            true
        } ?: false
    }

    private fun ParseContext.addTypeComponent(components: MutableList) {
        val types = when (openApiVersion) {
            ApiVersion.V3_0 -> node.getTextOrNull("type")?.let { listOf(it) }
            ApiVersion.V3_1 -> node.getMultiValue("type")?.map { it.asText() }?.filterNot { it == "null" }
        }?.map(SchemaTypes::fromString)

        if (types.isNullOrEmpty()) {
            return
        }

        if (types.size > 1) {
            SpecIssue("Schemas with more than one type is not supported. Found in $contextPath")
        }

        components.add(TypeComponent(types.first()))
    }

    private fun ParseContext.addFormatComponent(components: MutableList) {
        val format = node.getTextOrNull("format")
        if (format != null) {
            components.add(FormatComponent(format))
        }
    }

    private fun ParseContext.addNullableComponent(components: MutableList) {
        val nullable = when (openApiVersion) {
            ApiVersion.V3_0 -> node.getBooleanOrNull("nullable")
            ApiVersion.V3_1 -> node.getMultiValue("type")?.any { it.asText() == "null" }
        }

        if (nullable != null) {
            components.add(NullableComponent(nullable))
        }
    }

    private fun ParseContext.addDirectionComponent(components: MutableList) {
        val readOnly = node.getBooleanOrNull("readOnly") ?: false
        val writeOnly = node.getBooleanOrNull("writeOnly") ?: false

        val direction = when {
            readOnly && writeOnly -> SpecIssue("Property can't be read-only and write-only at the same time. $contextPath")
            readOnly -> SchemaModifier.ReadOnly
            writeOnly -> SchemaModifier.WriteOnly
            else -> null
        }

        direction?.let { components.add(SchemaModifierComponent(direction)) }
    }

    private fun addCustomConstraintsValidationComponent(components: MutableList) {
        node.getMultiValue("x-constraints")?.map { it.asText() }
            ?.let { components.add(ValidationComponent(CustomConstraintsValidation(it))) }
    }

    private fun ParseContext.addArrayComponent(components: MutableList) {
        node.get("items")?.let {
            val schema = contextFor(it, "items").parseAsSchema()
            components.add(ArrayItemsComponent(schema))
        }
    }

    private fun addArrayValidationComponent(components: MutableList) {
        val minItems = node.getTextOrNull("minItems")?.toInt()
        val maxItems = node.getTextOrNull("maxItems")?.toInt()
        if (minItems != null || maxItems != null) {
            components.add(ValidationComponent(ArrayValidation(minItems, maxItems)))
        }
    }

    private fun addEnumValidationComponent(components: MutableList) {
        val enumValues = node.withArray("enum").map { it.asText() }
        if (enumValues.isNotEmpty()) {
            components.add(EnumValidationComponent(enumValues))
        }
    }

    private fun addStringValidationComponent(components: MutableList) {
        val minLength = node.getTextOrNull("minLength")?.toInt()
        val maxLength = node.getTextOrNull("maxLength")?.toInt()
        val pattern = node.getTextOrNull("pattern")
        if (minLength != null || maxLength != null || pattern != null) {
            components.add(ValidationComponent(StringValidation(minLength, maxLength, pattern)))
        }
    }

    private fun ParseContext.addNumberValidationComponent(components: MutableList) {
        val minimum = extractComparableNumber("minimum")
        val maximum = extractComparableNumber("maximum")
        if (minimum != null || maximum != null) {
            components.add(ValidationComponent(NumberValidation(minimum, maximum)))
        }
    }

    private fun ParseContext.extractComparableNumber(name: String): ComparableNumber? {
        val capitalizedPostfix = name.replaceFirstChar {
            if (it.isLowerCase()) it.titlecase(Locale.ENGLISH) else it.toString()
        }

        return when (openApiVersion) {
            ApiVersion.V3_0 -> node.getTextOrNull(name)?.let {
                ComparableNumber(it, node.getBooleanOrNull("exclusive$capitalizedPostfix") ?: false)
            }

            ApiVersion.V3_1 -> node.getTextOrNull(name)?.let {
                ComparableNumber(it, false)
            } ?: node.getTextOrNull("exclusive$capitalizedPostfix")?.let {
                ComparableNumber(it, true)
            }
        }
    }

    private fun addDefaultComponent(components: MutableList) {
        node.getTextOrNull("default")?.let { components.add(DefaultComponent(it)) }
    }

    private fun ParseContext.addAllOfComponent(components: MutableList) {
        val schemas = node.withArray("allOf")
            .mapIndexed { idx, it ->
                contextFor(it, "allOf", "$idx").parseAsSchema()
            }.map { DefaultSchemaUsage(it) }

        if (schemas.isNotEmpty()) {
            components.add(AllOfComponent(schemas))
        }
    }

    private fun ParseContext.addAnyOfComponent(components: MutableList) {
        val schemas = node.withArray("anyOf")
            .mapIndexed { idx, it ->
                contextFor(it, "anyOf", "$idx").parseAsSchema()
            }.map { DefaultSchemaUsage(it) }

        if (schemas.isNotEmpty()) {
            components.add(AnyOfComponent(schemas))
        }
    }

    private fun ParseContext.addOneOfComponent(components: MutableList) {
        val schemas = node.withArray("oneOf")
            .mapIndexed { idx, it ->
                contextFor(it, "oneOf", "$idx").parseAsSchema()
            }.map { DefaultSchemaUsage(it) }

        if (schemas.isNotEmpty()) {
            val discriminatorNode = node.get("discriminator")
                ?.asObjectNode { "Json object expected for discriminator at $contextPath" }
            var discriminator: OneOfDiscriminator? = null
            if (discriminatorNode != null) {
                val propertyName = discriminatorNode.get("propertyName")?.asText()
                    ?: SpecIssue("Discriminator without propertyName found")

                val mappings = discriminatorNode.with("mapping")
                    .propertiesAsList()
                    .toMap()
                    .mapValues { it.value.asText() }

                discriminator = OneOfDiscriminator(propertyName, mappings)
            }

            components.add(OneOfComponent(schemas, discriminator))
        }
    }

    private fun ParseContext.addObjectComponent(components: MutableList) {
        val properties = node.with("properties")
            .propertiesAsList()
            .map { (name, propertyNode) ->
                contextFor(propertyNode, "properties", name).parseAsSchemaProperty(name)
            }

        if (properties.isNotEmpty()) {
            components.add(ObjectComponent(properties))
        }
    }

    private fun addObjectValidationComponent(components: MutableList) {
        val required = node.withArray("required").map { it.asText() }
        if (required.isNotEmpty()) {
            components.add(ObjectValidationComponent(required))
        }
    }

    private fun addContainerModelNameComponent(components: MutableList) {
        node.getTextOrNull("x-container-model-name")?.let { components.add(ContainerModelNameComponent(it)) }
    }

    private fun addEnumItemNamesComponent(components: MutableList) {
        val names = node.with("x-enum-item-names")
            .propertiesAsList()
            .associate { (enumName, modelNameNode) -> enumName to modelNameNode.asText() }

        if (names.isNotEmpty()) {
            components.add(EnumItemNamesComponent(names))
        }
    }
    
}

fun ParseContext.parseAsSchema() =
    contextNode.asObjectNode { "Json object expected for $contextPath" }
        .let { schemaCollector.registerSchema(contextPath) }

fun ParseContext.parseAsSchemaInto(schema: TransformableSchema) {
    contextNode.asObjectNode { "Json object expected for $contextPath" }
        .let {
            SchemaBuilder(schema, it).run { [email protected]() }
        }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy