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

org.http4k.contract.openapi.v3.OpenApi3ApiRenderer.kt Maven / Gradle / Ivy

There is a newer version: 5.31.0.0
Show newest version
package org.http4k.contract.openapi.v3

import org.http4k.contract.Tag
import org.http4k.contract.jsonschema.JsonSchema
import org.http4k.contract.jsonschema.JsonSchemaCreator
import org.http4k.contract.jsonschema.v3.JsonToJsonSchema
import org.http4k.contract.openapi.ApiInfo
import org.http4k.contract.openapi.ApiRenderer
import org.http4k.contract.openapi.v3.BodyContent.FormContent
import org.http4k.contract.openapi.v3.BodyContent.NoSchema
import org.http4k.contract.openapi.v3.BodyContent.OneOfSchemaContent
import org.http4k.contract.openapi.v3.BodyContent.SchemaContent
import org.http4k.contract.openapi.v3.RequestParameter.PrimitiveParameter
import org.http4k.contract.openapi.v3.RequestParameter.SchemaParameter
import org.http4k.format.Json

/**
 * Converts a API to OpenApi3 format JSON, using non-reflective JSON marshalling - this is the limited version
 *
 * If you are using Jackson, you probably want to use ApiRenderer.Auto()!
 */
class OpenApi3ApiRenderer(
    private val json: Json,
    private val refLocationPrefix: String = "components/schemas",
    private val jsonToJsonSchema: JsonSchemaCreator = JsonToJsonSchema(json, refLocationPrefix),
) : ApiRenderer, NODE> {

    override fun api(api: Api): NODE =
        with(api) {
            json {
                obj(
                    listOfNotNull(
                        "openapi" to string(openapi),
                        "info" to info.asJson(),
                        "tags" to array(tags.map { it.asJson() }),
                        "paths" to paths.asJson(),
                        webhooks?.let {
                            "webhooks" to obj(
                                it.map { (path, methodsToPaths) ->
                                    path to obj(
                                        methodsToPaths.map { it.key to it.value.toJson() }
                                    )
                                }
                            )
                        },
                        "components" to components.asJson(),
                        "servers" to array(servers.map { it.asJson() })
                    )
                )
            }
        }

    private fun ApiServer.asJson() = json {
        obj(
            "url" to string(url.toString()),
            "description" to string(description.orEmpty())
        )
    }

    private fun Tag.asJson(): NODE =
        json {
            obj(
                listOf(
                    "name" to string(name),
                    "description" to description.asJson()
                )
            )
        }

    private fun Components.asJson() = json {
        obj(
            "schemas" to schemas,
            "securitySchemes" to securitySchemes
        )
    }

    private fun Map>>.asJson(): NODE =
        json {
            obj(
                map {
                    it.key to obj(
                        it.value
                            .map { it.key to it.value.toJson() }.sortedBy { it.first }
                    )
                }.sortedBy { it.first }
            )
        }

    private fun ApiPath.toJson(): NODE = json {
        obj(
            listOfNotNull(
                "summary" to summary.asJson(),
                "description" to description.asJson(),
                tags?.takeIf { it.isNotEmpty() }?.let { "tags" to array(it.map(::string)) },
                "parameters" to parameters.asJson(),
                if (this@toJson is ApiPath.WithBody) [email protected]() else null,
                "responses" to responses.asJson(),
                security?.let { "security" to it },
                operationId?.let { "operationId" to it.asJson() },
                deprecated?.let { "deprecated" to boolean(it) },
                callbacks?.let { cb ->
                    "callbacks" to obj(
                        cb.map {
                            it.key to obj(
                                it.value.map { (path, methodsToPaths) ->
                                    path.toString() to obj(
                                        methodsToPaths.map { it.key to it.value.toJson() }
                                    )
                                }
                            )
                        }
                    )
                }
            )
        )
    }

    private fun RequestContents.asJson() = json {
        content?.let {
            "requestBody" to obj(
                listOfNotNull(
                    "content" to it.asJson(),
                    "required" to boolean(content.isNotEmpty())
                )
            )
        }
    }

    @JvmName("contentAsJson")
    private fun Map.asJson(): NODE = json {
        obj(
            map {
                it.key to (
                    listOf(it.value).filterIsInstance>().map { it.toJson() } +
                        listOf(it.value).filterIsInstance>().map { it.toJson() } +
                        listOf(it.value).filterIsInstance>().map { it.toJson() } +
                        listOf(it.value).filterIsInstance().map { it.toJson() }
                    ).firstOrNull().orNullNode()
            }
        )
    }

    private fun NoSchema.toJson(): NODE = json {
        obj("schema" to schema)
    }

    private fun OneOfSchemaContent.toJson(): NODE = json {
        obj("schema" to obj("oneOf" to array(schema.oneOf)))
    }

    private fun SchemaContent.toJson(): NODE = json {
        obj(
            listOfNotNull(
                example?.let { "example" to it },
                schema?.let { "schema" to it }
            )
        )
    }

    private fun FormContent.toJson(): NODE = json {
        obj("schema" to
            obj(
                listOfNotNull(
                    "type" to string("object"),
                    "properties" to obj(
                        schema.properties.map {
                            it.key to obj(it.value.map { (key, value) ->
                                key to
                                    when (value) {
                                        is String -> value.asJson()
                                        is Map<*, *> -> value.mapAsJson()
                                        else -> error("")
                                    }
                            })
                        }
                    ),
                    schema.required.takeIf { it.isNotEmpty() }?.let { "required" to array(it.map { it.asJson() }) }
                )
            )
        )
    }

    @JvmName("responseAsJson")
    private fun Map>.asJson(): NODE = json {
        obj(map {
            it.key to
                obj(
                    "description" to it.value.description.asJson(),
                    "content" to it.value.content.asJson()
                )
        })
    }

    private fun List>.asJson(): NODE = json {
        array(
            filterIsInstance>().map { it.asJson() }
                + filterIsInstance>().map { it.asJson() }
        )
    }

    private fun SchemaParameter.asJson(): NODE = json {
        obj(
            listOfNotNull(
                schema?.let { "schema" to it },
                "in" to string(`in`),
                "name" to string(name),
                "required" to boolean(required),
                "description" to description.asJson()
            )
        )
    }

    private fun PrimitiveParameter.asJson(): NODE = json {
        obj(
            "schema" to schema,
            "in" to string(`in`),
            "name" to string(name),
            "required" to boolean(required),
            "description" to description.asJson()
        )
    }

    private fun ApiInfo.asJson() = json {
        obj("title" to string(title), "version" to string(version), "description" to string(description.orEmpty()))
    }

    private fun String?.asJson() = this?.let { json.string(it) } ?: json.nullNode()

    private fun Map<*, *>.mapAsJson() = json {
        obj(map { it.key.toString() to string(it.value.toString()) }.toList())
    }

    private fun NODE?.orNullNode() = this ?: json.nullNode()

    @Suppress("UNCHECKED_CAST")
    override fun toSchema(obj: Any, overrideDefinitionId: String?, refModelNamePrefix: String?): JsonSchema =
        try {
            jsonToJsonSchema.toSchema(obj as NODE, overrideDefinitionId, refModelNamePrefix)
        } catch (e: ClassCastException) {
            when (obj) {
                is Enum<*> -> toEnumSchema(obj, refModelNamePrefix, overrideDefinitionId)
                else -> jsonToJsonSchema.toSchema(json.obj(), overrideDefinitionId, refModelNamePrefix)
            }
        }

    private fun toEnumSchema(
        obj: Enum<*>,
        refModelNamePrefix: String?,
        overrideDefinitionId: String?,
    ): JsonSchema {
        val newDefinition = json.obj(
            "example" to json.string(obj.name),
            "type" to json.string("string"),
            "enum" to json.array(obj.javaClass.enumConstants.map { json.string(it.name) })
        )
        val definitionId =
            (refModelNamePrefix.orEmpty()) + (overrideDefinitionId ?: ("object" + newDefinition.hashCode()))
        return JsonSchema(
            json { obj("\$ref" to string("#/$refLocationPrefix/$definitionId")) },
            setOf(definitionId to newDefinition)
        )
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy