org.http4k.contract.openapi.v3.OpenApi3ApiRenderer.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of http4k-contract Show documentation
Show all versions of http4k-contract Show documentation
http4k typesafe HTTP contracts and OpenApi support
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)
)
}
}