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

org.http4k.contract.Swagger.kt Maven / Gradle / Ivy

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

import org.http4k.core.HttpMessage
import org.http4k.core.Response
import org.http4k.core.Status.Companion.OK
import org.http4k.format.Json
import org.http4k.format.JsonErrorResponseRenderer
import org.http4k.lens.Failure
import org.http4k.lens.Meta
import util.JsonSchema
import util.JsonToJsonSchema

data class ApiInfo(val title: String, val version: String, val description: String? = null)

private typealias ReasonToResponse = Pair

class Swagger(private val apiInfo: ApiInfo, private val json: Json) : ContractRenderer {

    private val schemaGenerator = JsonToJsonSchema(json)
    private val errors = JsonErrorResponseRenderer(json)

    override fun badRequest(failures: List) = errors.badRequest(failures)

    override fun notFound() = errors.notFound()

    override fun description(contractRoot: PathSegments, security: Security, routes: List) =
        Response(OK).body(json.pretty(json.obj(
            "swagger" to json.string("2.0"),
            "info" to apiInfo.asJson(),
            "basePath" to json.string("/"),
            "tags" to json.array(renderTags(routes)),
            "paths" to json.obj(renderPaths(routes, contractRoot, security).fields),
            "securityDefinitions" to security.asJson(),
            "definitions" to json.obj(renderPaths(routes, contractRoot, security).definitions)
        )))

    private fun renderPaths(routes: List, contractRoot: PathSegments, security: Security): FieldsAndDefinitions {
        return routes
            .groupBy { it.describeFor(contractRoot) }.entries
            .fold(FieldsAndDefinitions(), {
                memo, (path, routes) ->
                val routeFieldsAndDefinitions = routes.fold(FieldsAndDefinitions(), {
                    memoFields, route ->
                    memoFields.add(render(contractRoot, security, route))
                })
                memo.add(path to json.obj(routeFieldsAndDefinitions.fields), routeFieldsAndDefinitions.definitions)
            })
    }

    private fun renderMeta(it: Meta, schema: JsonSchema? = null): ROOT = json.obj(
        "in" to json.string(it.location),
        "name" to json.string(it.name),
        "description" to (it.description?.let(json::string) ?: json.nullNode()),
        "required" to json.boolean(it.required),
        schema?.let { "schema" to it.node } ?: "type" to json.string(it.paramMeta.value)
    )

    private fun renderTags(routes: List) = routes.flatMap(ContractRoute::tags).toSet().sortedBy { it.name }.map { it.asJson() }

    private fun render(pathSegments: PathSegments, security: Security, route: ContractRoute): FieldAndDefinitions {
        val (responses, responseDefinitions) = render(route.meta.responses.values.toList())

        val schema = route.jsonRequest?.asSchema()

        val bodyParamNodes = route.spec.body?.metas?.map { renderMeta(it, schema) } ?: emptyList()

        val nonBodyParamNodes = route.nonBodyParams.flatMap { it.asList() }.map { renderMeta(it) }

        val routeTags = if (route.tags.isEmpty()) listOf(json.string(pathSegments.toString())) else route.tagsAsJson()
        val consumes = route.meta.consumes.plus(route.spec.body?.let { listOf(it.contentType) } ?: emptyList())

        val pathJson = json.obj(
            "tags" to json.array(routeTags),
            "summary" to json.string(route.meta.summary),
            "description" to (route.meta.description?.let(json::string) ?: json.nullNode()),
            "produces" to json.array(route.meta.produces.map { json.string(it.value) }),
            "consumes" to json.array(consumes.map { json.string(it.value) }),
            "parameters" to json.array(nonBodyParamNodes.plus(bodyParamNodes)),
            "responses" to json.obj(responses),
            "supportedContentTypes" to json.array(route.meta.produces.map { json.string(it.value) }),
            "security" to json.array(when (security) {
                is ApiKey<*> -> listOf(json.obj("api_key" to json.array(emptyList())))
                else -> emptyList()
            })
        )

        val definitions = route.meta.request.asList().flatMap { it.asSchema().definitions }.plus(responseDefinitions).distinct()

        return FieldAndDefinitions(route.method.toString().toLowerCase() to pathJson, definitions)
    }

    private fun render(responses: List>) =
        responses.fold(FieldsAndDefinitions(),
            {
                memo, (reason, response) ->
                val (node, definitions) = response.asSchema()
                val newField = response.status.code.toString() to json.obj(
                    "description" to json.string(reason),
                    "schema" to node)
                memo.add(newField, definitions)
            })

    private fun Security.asJson() = when (this) {
        is ApiKey<*> -> json.obj(
            "api_key" to json.obj(
                "type" to json.string("apiKey"),
                "in" to json.string(param.meta.location),
                "name" to json.string(param.meta.name)
            ))
        else -> json.obj(listOf())
    }

    private fun HttpMessage.asSchema(): JsonSchema = try {
        schemaGenerator.toSchema(json.parse(bodyString()))
    } catch (e: Exception) {
        JsonSchema(json.nullNode(), emptyList())
    }

    private fun ContractRoute.tagsAsJson() = tags.map(Tag::name).map(json::string)

    private fun ApiInfo.asJson() = json.obj("title" to json.string(title), "version" to json.string(version), "description" to json.string(description ?: ""))

    private fun Tag.asJson() = json.obj(listOf("name" to json.string(name)).plus(description?.let { "description" to json.string(it) }.asList()))
}

private data class FieldsAndDefinitions(val fields: List> = emptyList(), val definitions: List> = emptyList()) {
    fun add(newField: Pair, newDefinitions: List>) = FieldsAndDefinitions(fields.plus(newField), newDefinitions.plus(definitions))

    fun add(fieldAndDefinitions: FieldAndDefinitions) = FieldsAndDefinitions(fields.plus(fieldAndDefinitions.field),
        fieldAndDefinitions.definitions.plus(definitions))
}

private data class FieldAndDefinitions(val field: Pair, val definitions: List>)

private fun  T?.asList() = this?.let { listOf(it) } ?: listOf()




© 2015 - 2024 Weber Informatics LLC | Privacy Policy