VerifySpecCallback.kt
package com.hexagonkt.rest.tools.openapi
import com.atlassian.oai.validator.OpenApiInteractionValidator
import com.atlassian.oai.validator.OpenApiInteractionValidator.createForInlineApiSpecification
import com.atlassian.oai.validator.model.Request
import com.atlassian.oai.validator.model.Request.Method
import com.atlassian.oai.validator.model.Response
import com.atlassian.oai.validator.model.SimpleRequest
import com.atlassian.oai.validator.model.SimpleResponse
import com.atlassian.oai.validator.report.ValidationReport
import com.atlassian.oai.validator.report.ValidationReport.Message
import com.hexagonkt.http.handlers.HttpCallback
import com.hexagonkt.http.handlers.HttpContext
import com.hexagonkt.http.model.ContentType
import com.hexagonkt.http.model.HttpMethod
import com.hexagonkt.http.model.HttpMethod.*
import java.net.URL
import kotlin.jvm.optionals.getOrNull
/**
* Callback that verifies server calls comply with a given OpenAPI spec.
*
* @param spec Location of the spec used to validate HTTP calls.
*/
class VerifySpecCallback(spec: URL) : HttpCallback {
private val messagePrefix: String = "\n- "
private val specText: String = spec.readText()
private val validator: OpenApiInteractionValidator =
createForInlineApiSpecification(specText).build()
override fun invoke(context: HttpContext): HttpContext {
val requestReport = validator.validateRequest(request(context))
val result = context.next()
val resultMethod = method(result.method)
val responseReport = validator.validateResponse(result.path, resultMethod, response(result))
val callReport = responseReport.merge(requestReport)
return if (callReport.hasErrors()) result.badRequest(message(callReport))
else result
}
private fun message(report: ValidationReport): String {
val messages = report.messages.map(::messageToText).distinct()
return messages.joinToString(messagePrefix, "Invalid call:$messagePrefix")
}
private fun messageToText(it: Message): String {
val level = it.level
val key = it.key
val context = it.context
.map { c ->
val op = c.apiOperation
.getOrNull()
?.let { ao ->
val method = ao.method
val apiPath = ao.apiPath
"$method ${apiPath.normalised()}"
}
?: ""
val loc = c.location.getOrNull()?.name ?: ""
"$op $loc"
}
.orElse("")
val message = it.message
val additionalInfo = it.additionalInfo
val nestedMessages = it.nestedMessages
return "$level: $key [$context] $message $additionalInfo $nestedMessages"
}
private fun request(context: HttpContext): Request {
val request = context.request
val builder = SimpleRequest.Builder(method(context.method), context.path, true)
if (request.bodyString().isNotEmpty())
builder.withBody(request.bodyString())
request.contentType?.text?.let(builder::withContentType)
request.headers.httpFields.values.forEach { builder.withHeader(it.name, it.strings()) }
request.accept.map(ContentType::text).forEach(builder::withAccept)
request.authorization?.text?.let(builder::withAuthorization)
request.queryParameters.httpFields.values.forEach {
builder.withQueryParam(it.name, it.strings())
}
return builder.build()
}
private fun response(context: HttpContext): Response {
val response = context.response
val builder = SimpleResponse.Builder(context.status.code)
builder.withBody(response.bodyString())
response.contentType?.text?.let(builder::withContentType)
response.headers.httpFields.values.forEach { builder.withHeader(it.name, it.strings()) }
return builder.build()
}
private fun method(method: HttpMethod): Method =
when (method) {
GET -> Method.GET
HEAD -> Method.HEAD
POST -> Method.POST
PUT -> Method.PUT
DELETE -> Method.DELETE
TRACE -> Method.TRACE
OPTIONS -> Method.OPTIONS
PATCH -> Method.PATCH
}
}