Feature/examplebodies (#44)

This commit is contained in:
Ryan Brink
2021-05-04 14:19:29 -04:00
committed by GitHub
parent ad33832ed3
commit 6b87519a64
23 changed files with 582 additions and 523 deletions

View File

@ -16,6 +16,7 @@ import org.leafygreens.kompendium.models.meta.MethodInfo
import org.leafygreens.kompendium.models.meta.RequestInfo
import org.leafygreens.kompendium.models.meta.ResponseInfo
import org.leafygreens.kompendium.models.meta.SchemaMap
import org.leafygreens.kompendium.models.oas.ExampleWrapper
import org.leafygreens.kompendium.models.oas.OpenApiSpec
import org.leafygreens.kompendium.models.oas.OpenApiSpecInfo
import org.leafygreens.kompendium.models.oas.OpenApiSpecMediaType
@ -45,33 +46,37 @@ object Kompendium {
var pathCalculator: PathCalculator = CorePathCalculator()
// TODO here down is a mess, needs refactor once core functionality is in place
fun MethodInfo.parseMethodInfo(
method: HttpMethod,
fun parseMethodInfo(
info: MethodInfo<*, *>,
paramType: KType,
requestType: KType,
responseType: KType
) = OpenApiSpecPathItemOperation(
summary = this.summary,
description = this.description,
tags = this.tags,
deprecated = this.deprecated,
summary = info.summary,
description = info.description,
tags = info.tags,
deprecated = info.deprecated,
parameters = paramType.toParameterSpec(),
responses = responseType.toResponseSpec(responseInfo)?.let { mapOf(it) }.let {
responses = responseType.toResponseSpec(info.responseInfo)?.let { mapOf(it) }.let {
when (it) {
null -> {
val throwables = parseThrowables(canThrow)
val throwables = parseThrowables(info.canThrow)
when (throwables.isEmpty()) {
true -> null
false -> throwables
}
}
else -> it.plus(parseThrowables(canThrow))
else -> it.plus(parseThrowables(info.canThrow))
}
},
requestBody = if (method != HttpMethod.Get) requestType.toRequestSpec(requestInfo) else null,
security = if (this.securitySchemes.isNotEmpty()) listOf(
requestBody = when (info) {
is MethodInfo.PutInfo<*, *, *> -> requestType.toRequestSpec(info.requestInfo)
is MethodInfo.PostInfo<*, *, *> -> requestType.toRequestSpec(info.requestInfo)
else -> null
},
security = if (info.securitySchemes.isNotEmpty()) listOf(
// TODO support scopes
this.securitySchemes.associateWith { listOf() }
info.securitySchemes.associateWith { listOf() }
) else null
)
@ -79,7 +84,7 @@ object Kompendium {
errorMap[it.createType()]
}.toMap()
fun ResponseInfo.parseErrorInfo(
fun <TResp> ResponseInfo<TResp>.parseErrorInfo(
errorType: KType,
responseType: KType
) {
@ -87,37 +92,44 @@ object Kompendium {
}
// TODO These two lookin' real similar 👀 Combine?
private fun KType.toRequestSpec(requestInfo: RequestInfo?): OpenApiSpecRequest? = when (requestInfo) {
null -> null
else -> {
OpenApiSpecRequest(
description = requestInfo.description,
content = resolveContent(requestInfo.mediaTypes) ?: mapOf()
)
private fun <TReq> KType.toRequestSpec(requestInfo: RequestInfo<TReq>?): OpenApiSpecRequest<TReq>? =
when (requestInfo) {
null -> null
else -> {
OpenApiSpecRequest(
description = requestInfo.description,
content = resolveContent(requestInfo.mediaTypes, requestInfo.examples) ?: mapOf()
)
}
}
}
private fun KType.toResponseSpec(responseInfo: ResponseInfo?): Pair<Int, OpenApiSpecResponse>? = when (responseInfo) {
null -> null // TODO again probably revisit this
else -> {
val specResponse = OpenApiSpecResponse(
description = responseInfo.description,
content = resolveContent(responseInfo.mediaTypes)
)
Pair(responseInfo.status, specResponse)
private fun <TResp> KType.toResponseSpec(responseInfo: ResponseInfo<TResp>?): Pair<Int, OpenApiSpecResponse<TResp>>? =
when (responseInfo) {
null -> null // TODO again probably revisit this
else -> {
val specResponse = OpenApiSpecResponse(
description = responseInfo.description,
content = resolveContent(responseInfo.mediaTypes, responseInfo.examples)
)
Pair(responseInfo.status, specResponse)
}
}
}
private fun KType.resolveContent(mediaTypes: List<String>): Map<String, OpenApiSpecMediaType>? {
private fun <F> KType.resolveContent(
mediaTypes: List<String>,
examples: Map<String, F>
): Map<String, OpenApiSpecMediaType<F>>? {
return if (this != Helpers.UNIT_TYPE && mediaTypes.isNotEmpty()) {
mediaTypes.associateWith {
val ref = getReferenceSlug()
OpenApiSpecMediaType.Referenced(OpenApiSpecReferenceObject(ref))
OpenApiSpecMediaType(
schema = OpenApiSpecReferenceObject(ref),
examples = examples.mapValues { (_, v) -> ExampleWrapper(v) }.ifEmpty { null }
)
}
} else null
}
// TODO God these annotations make this hideous... any way to improve?
private fun KType.toParameterSpec(): List<OpenApiSpecParameter> {
val clazz = classifier as KClass<*>
@ -151,7 +163,7 @@ object Kompendium {
}
}
internal fun resetSchema() {
fun resetSchema() {
openApiSpec = OpenApiSpec(
info = OpenApiSpecInfo(),
servers = mutableListOf(),

View File

@ -15,10 +15,10 @@ import org.leafygreens.kompendium.models.oas.FormatSchema
import org.leafygreens.kompendium.models.oas.ObjectSchema
import org.leafygreens.kompendium.models.oas.ReferencedSchema
import org.leafygreens.kompendium.models.oas.SimpleSchema
import org.leafygreens.kompendium.util.Helpers
import org.leafygreens.kompendium.util.Helpers.COMPONENT_SLUG
import org.leafygreens.kompendium.util.Helpers.genericNameAdapter
import org.leafygreens.kompendium.util.Helpers.getReferenceSlug
import org.leafygreens.kompendium.util.Helpers.logged
import org.slf4j.LoggerFactory
object Kontent {
@ -45,7 +45,7 @@ object Kontent {
fun generateKTypeKontent(
type: KType,
cache: SchemaMap = emptyMap()
): SchemaMap = Helpers.logged(object {}.javaClass.enclosingMethod.name, mapOf("cache" to cache)) {
): SchemaMap = logged(object {}.javaClass.enclosingMethod.name, mapOf("cache" to cache)) {
logger.debug("Parsing Kontent of $type")
when (val clazz = type.classifier as KClass<*>) {
Unit::class -> cache

View File

@ -10,7 +10,10 @@ import io.ktor.util.pipeline.PipelineInterceptor
import org.leafygreens.kompendium.Kompendium.parseErrorInfo
import org.leafygreens.kompendium.Kompendium.parseMethodInfo
import org.leafygreens.kompendium.KompendiumPreFlight.errorNotarizationPreFlight
import org.leafygreens.kompendium.models.meta.MethodInfo
import org.leafygreens.kompendium.models.meta.MethodInfo.GetInfo
import org.leafygreens.kompendium.models.meta.MethodInfo.PostInfo
import org.leafygreens.kompendium.models.meta.MethodInfo.PutInfo
import org.leafygreens.kompendium.models.meta.MethodInfo.DeleteInfo
import org.leafygreens.kompendium.models.meta.ResponseInfo
import org.leafygreens.kompendium.models.oas.OpenApiSpecPathItem
@ -18,55 +21,52 @@ object Notarized {
@OptIn(ExperimentalStdlibApi::class)
inline fun <reified TParam : Any, reified TResp : Any> Route.notarizedGet(
info: MethodInfo,
info: GetInfo<TParam, TResp>,
noinline body: PipelineInterceptor<Unit, ApplicationCall>
): Route =
KompendiumPreFlight.methodNotarizationPreFlight<TParam, Unit, TResp>() { paramType, requestType, responseType ->
val path = Kompendium.pathCalculator.calculate(this)
Kompendium.openApiSpec.paths.getOrPut(path) { OpenApiSpecPathItem() }
Kompendium.openApiSpec.paths[path]?.get =
info.parseMethodInfo(HttpMethod.Get, paramType, requestType, responseType)
Kompendium.openApiSpec.paths[path]?.get = parseMethodInfo(info, paramType, requestType, responseType)
return method(HttpMethod.Get) { handle(body) }
}
inline fun <reified TParam : Any, reified TReq : Any, reified TResp : Any> Route.notarizedPost(
info: MethodInfo,
info: PostInfo<TParam, TReq, TResp>,
noinline body: PipelineInterceptor<Unit, ApplicationCall>
): Route =
KompendiumPreFlight.methodNotarizationPreFlight<TParam, TReq, TResp>() { paramType, requestType, responseType ->
val path = Kompendium.pathCalculator.calculate(this)
Kompendium.openApiSpec.paths.getOrPut(path) { OpenApiSpecPathItem() }
Kompendium.openApiSpec.paths[path]?.post =
info.parseMethodInfo(HttpMethod.Post, paramType, requestType, responseType)
Kompendium.openApiSpec.paths[path]?.post = parseMethodInfo(info, paramType, requestType, responseType)
return method(HttpMethod.Post) { handle(body) }
}
inline fun <reified TParam : Any, reified TReq : Any, reified TResp : Any> Route.notarizedPut(
info: MethodInfo,
info: PutInfo<TParam, TReq, TResp>,
noinline body: PipelineInterceptor<Unit, ApplicationCall>,
): Route =
KompendiumPreFlight.methodNotarizationPreFlight<TParam, TReq, TResp>() { paramType, requestType, responseType ->
val path = Kompendium.pathCalculator.calculate(this)
Kompendium.openApiSpec.paths.getOrPut(path) { OpenApiSpecPathItem() }
Kompendium.openApiSpec.paths[path]?.put =
info.parseMethodInfo(HttpMethod.Put, paramType, requestType, responseType)
parseMethodInfo(info, paramType, requestType, responseType)
return method(HttpMethod.Put) { handle(body) }
}
inline fun <reified TParam : Any, reified TResp : Any> Route.notarizedDelete(
info: MethodInfo,
info: DeleteInfo<TParam, TResp>,
noinline body: PipelineInterceptor<Unit, ApplicationCall>
): Route =
KompendiumPreFlight.methodNotarizationPreFlight<TParam, Unit, TResp> { paramType, requestType, responseType ->
val path = Kompendium.pathCalculator.calculate(this)
Kompendium.openApiSpec.paths.getOrPut(path) { OpenApiSpecPathItem() }
Kompendium.openApiSpec.paths[path]?.delete =
info.parseMethodInfo(HttpMethod.Delete, paramType, requestType, responseType)
Kompendium.openApiSpec.paths[path]?.delete = parseMethodInfo(info, paramType, requestType, responseType)
return method(HttpMethod.Delete) { handle(body) }
}
inline fun <reified TErr : Throwable, reified TResp : Any> StatusPages.Configuration.notarizedException(
info: ResponseInfo,
info: ResponseInfo<TResp>,
noinline handler: suspend PipelineContext<Unit, ApplicationCall>.(TErr) -> Unit
) = errorNotarizationPreFlight<TErr, TResp>() { errorType, responseType ->
info.parseErrorInfo(errorType, responseType)

View File

@ -3,4 +3,4 @@ package org.leafygreens.kompendium.models.meta
import kotlin.reflect.KType
import org.leafygreens.kompendium.models.oas.OpenApiSpecResponse
typealias ErrorMap = Map<KType, Pair<Int, OpenApiSpecResponse>?>
typealias ErrorMap = Map<KType, Pair<Int, OpenApiSpecResponse<*>>?>

View File

@ -2,14 +2,94 @@ package org.leafygreens.kompendium.models.meta
import kotlin.reflect.KClass
// TODO Seal and extend by method type?
data class MethodInfo(
val summary: String,
val description: String? = null,
val responseInfo: ResponseInfo? = null,
val requestInfo: RequestInfo? = null,
val tags: Set<String> = emptySet(),
val deprecated: Boolean = false,
val securitySchemes: Set<String> = emptySet(),
val canThrow: Set<KClass<*>> = emptySet()
)
sealed class MethodInfo<TParam, TResp>(
open val summary: String,
open val description: String? = null,
open val tags: Set<String> = emptySet(),
open val deprecated: Boolean = false,
open val securitySchemes: Set<String> = emptySet(),
open val canThrow: Set<KClass<*>> = emptySet(),
open val responseInfo: ResponseInfo<TResp>? = null,
open val parameterExamples: Map<String, TParam> = emptyMap(),
) {
data class GetInfo<TParam, TResp>(
override val responseInfo: ResponseInfo<TResp>? = null,
override val summary: String,
override val description: String? = null,
override val tags: Set<String> = emptySet(),
override val deprecated: Boolean = false,
override val securitySchemes: Set<String> = emptySet(),
override val canThrow: Set<KClass<*>> = emptySet(),
override val parameterExamples: Map<String, TParam> = emptyMap()
) : MethodInfo<TParam, TResp>(
summary = summary,
description = description,
tags = tags,
deprecated = deprecated,
securitySchemes = securitySchemes,
canThrow = canThrow,
responseInfo = responseInfo,
parameterExamples = parameterExamples
)
data class PostInfo<TParam, TReq, TResp>(
val requestInfo: RequestInfo<TReq>? = null,
override val responseInfo: ResponseInfo<TResp>? = null,
override val summary: String,
override val description: String? = null,
override val tags: Set<String> = emptySet(),
override val deprecated: Boolean = false,
override val securitySchemes: Set<String> = emptySet(),
override val canThrow: Set<KClass<*>> = emptySet(),
override val parameterExamples: Map<String, TParam> = emptyMap()
) : MethodInfo<TParam, TResp>(
summary = summary,
description = description,
tags = tags,
deprecated = deprecated,
securitySchemes = securitySchemes,
canThrow = canThrow,
responseInfo = responseInfo,
parameterExamples = parameterExamples
)
data class PutInfo<TParam, TReq, TResp>(
val requestInfo: RequestInfo<TReq>? = null,
override val responseInfo: ResponseInfo<TResp>? = null,
override val summary: String,
override val description: String? = null,
override val tags: Set<String> = emptySet(),
override val deprecated: Boolean = false,
override val securitySchemes: Set<String> = emptySet(),
override val canThrow: Set<KClass<*>> = emptySet(),
override val parameterExamples: Map<String, TParam> = emptyMap()
) : MethodInfo<TParam, TResp>(
summary = summary,
description = description,
tags = tags,
deprecated = deprecated,
securitySchemes = securitySchemes,
canThrow = canThrow,
parameterExamples = parameterExamples
)
data class DeleteInfo<TParam, TResp>(
override val responseInfo: ResponseInfo<TResp>? = null,
override val summary: String,
override val description: String? = null,
override val tags: Set<String> = emptySet(),
override val deprecated: Boolean = false,
override val securitySchemes: Set<String> = emptySet(),
override val canThrow: Set<KClass<*>> = emptySet(),
override val parameterExamples: Map<String, TParam> = emptyMap()
) : MethodInfo<TParam, TResp>(
summary = summary,
description = description,
tags = tags,
deprecated = deprecated,
securitySchemes = securitySchemes,
canThrow = canThrow,
parameterExamples = parameterExamples
)
}

View File

@ -1,7 +1,8 @@
package org.leafygreens.kompendium.models.meta
data class RequestInfo(
data class RequestInfo<TReq>(
val description: String,
val required: Boolean = true,
val mediaTypes: List<String> = listOf("application/json")
val mediaTypes: List<String> = listOf("application/json"),
val examples: Map<String, TReq> = emptyMap()
)

View File

@ -1,7 +1,8 @@
package org.leafygreens.kompendium.models.meta
data class ResponseInfo(
data class ResponseInfo<TResp>(
val status: Int,
val description: String,
val mediaTypes: List<String> = listOf("application/json")
val mediaTypes: List<String> = listOf("application/json"),
val examples: Map<String, TResp> = emptyMap()
)

View File

@ -1,16 +1,8 @@
package org.leafygreens.kompendium.models.oas
// TODO Oof -> https://swagger.io/specification/#media-type-object
sealed class OpenApiSpecMediaType {
data class Explicit(
val schema: OpenApiSpecSchema, // TODO sheesh -> https://swagger.io/specification/#schema-object
val example: String? = null, // TODO Enforce type? then serialize?
val examples: Map<String, String>? = null, // needs to be mutually exclusive with example
val encoding: Map<String, String>? = null // todo encoding object -> https://swagger.io/specification/#encoding-object
) : OpenApiSpecMediaType()
data class Referenced(
val schema: OpenApiSpecReferenceObject
) : OpenApiSpecMediaType()
}
data class OpenApiSpecMediaType<T>(
val schema: OpenApiSpecReferenceObject,
val examples: Map<String, ExampleWrapper<T>>? = null
)
data class ExampleWrapper<T>(val value: T)

View File

@ -4,23 +4,13 @@ sealed class OpenApiSpecReferencable
class OpenApiSpecReferenceObject(val `$ref`: String) : OpenApiSpecReferencable()
data class OpenApiSpecCallback(
val todo: String // todo fuck me -> https://swagger.io/specification/#callback-object
) : OpenApiSpecReferencable()
data class OpenApiSpecResponse(
data class OpenApiSpecResponse<T>(
val description: String? = null,
val headers: Map<String, OpenApiSpecReferencable>? = null,
val content: Map<String, OpenApiSpecMediaType>? = null,
val content: Map<String, OpenApiSpecMediaType<T>>? = null,
val links: Map<String, OpenApiSpecReferencable>? = null
) : OpenApiSpecReferencable()
data class OpenApiSpecHeader(
val name: String,
val description: String?,
val externalDocs: OpenApiSpecExternalDocumentation?
) : OpenApiSpecReferencable()
data class OpenApiSpecParameter(
val name: String,
val `in`: String, // TODO Enum? "query", "header", "path" or "cookie"
@ -33,8 +23,8 @@ data class OpenApiSpecParameter(
val explode: Boolean? = null
) : OpenApiSpecReferencable()
data class OpenApiSpecRequest(
data class OpenApiSpecRequest<T>(
val description: String?,
val content: Map<String, OpenApiSpecMediaType>,
val content: Map<String, OpenApiSpecMediaType<T>>,
val required: Boolean = false
) : OpenApiSpecReferencable()

View File

@ -26,7 +26,10 @@ import org.leafygreens.kompendium.Notarized.notarizedGet
import org.leafygreens.kompendium.Notarized.notarizedPost
import org.leafygreens.kompendium.Notarized.notarizedPut
import org.leafygreens.kompendium.annotations.QueryParam
import org.leafygreens.kompendium.models.meta.MethodInfo
import org.leafygreens.kompendium.models.meta.MethodInfo.GetInfo
import org.leafygreens.kompendium.models.meta.MethodInfo.PostInfo
import org.leafygreens.kompendium.models.meta.MethodInfo.PutInfo
import org.leafygreens.kompendium.models.meta.MethodInfo.DeleteInfo
import org.leafygreens.kompendium.models.meta.RequestInfo
import org.leafygreens.kompendium.models.meta.ResponseInfo
import org.leafygreens.kompendium.models.oas.OpenApiSpecInfo
@ -39,7 +42,8 @@ import org.leafygreens.kompendium.util.ComplexRequest
import org.leafygreens.kompendium.util.ExceptionResponse
import org.leafygreens.kompendium.util.KompendiumHttpCodes
import org.leafygreens.kompendium.util.TestCreatedResponse
import org.leafygreens.kompendium.util.TestData
import org.leafygreens.kompendium.util.TestHelpers.getFileSnapshot
import org.leafygreens.kompendium.util.TestNested
import org.leafygreens.kompendium.util.TestParams
import org.leafygreens.kompendium.util.TestRequest
import org.leafygreens.kompendium.util.TestResponse
@ -67,7 +71,7 @@ internal class KompendiumTest {
val json = handleRequest(HttpMethod.Get, "/openapi.json").response.content
// expect
val expected = TestData.getFileSnapshot("notarized_get.json").trim()
val expected = getFileSnapshot("notarized_get.json").trim()
assertEquals(expected, json, "The received json spec should match the expected content")
}
}
@ -99,7 +103,7 @@ internal class KompendiumTest {
val json = handleRequest(HttpMethod.Get, "/openapi.json").response.content
// expect
val expected = TestData.getFileSnapshot("notarized_post.json").trim()
val expected = getFileSnapshot("notarized_post.json").trim()
assertEquals(expected, json, "The received json spec should match the expected content")
}
}
@ -131,7 +135,7 @@ internal class KompendiumTest {
val json = handleRequest(HttpMethod.Get, "/openapi.json").response.content
// expect
val expected = TestData.getFileSnapshot("notarized_put.json").trim()
val expected = getFileSnapshot("notarized_put.json").trim()
assertEquals(expected, json, "The received json spec should match the expected content")
}
}
@ -164,7 +168,7 @@ internal class KompendiumTest {
val json = handleRequest(HttpMethod.Get, "/openapi.json").response.content
// expect
val expected = TestData.getFileSnapshot("notarized_delete.json").trim()
val expected = getFileSnapshot("notarized_delete.json").trim()
assertEquals(expected, json, "The received json spec should match the expected content")
}
}
@ -195,7 +199,7 @@ internal class KompendiumTest {
val json = handleRequest(HttpMethod.Get, "/openapi.json").response.content
// expect
val expected = TestData.getFileSnapshot("path_parser.json").trim()
val expected = getFileSnapshot("path_parser.json").trim()
assertEquals(expected, json, "The received json spec should match the expected content")
}
}
@ -211,7 +215,7 @@ internal class KompendiumTest {
val json = handleRequest(HttpMethod.Get, "/openapi.json").response.content
// expect
val expected = TestData.getFileSnapshot("root_route.json").trim()
val expected = getFileSnapshot("root_route.json").trim()
assertEquals(expected, json, "The received json spec should match the expected content")
}
}
@ -243,7 +247,7 @@ internal class KompendiumTest {
val json = handleRequest(HttpMethod.Get, "/openapi.json").response.content
// expect
val expected = TestData.getFileSnapshot("nested_under_root.json").trim()
val expected = getFileSnapshot("nested_under_root.json").trim()
assertEquals(expected, json, "The received json spec should match the expected content")
}
}
@ -259,7 +263,7 @@ internal class KompendiumTest {
val json = handleRequest(HttpMethod.Get, "/openapi.json").response.content
// expect
val expected = TestData.getFileSnapshot("trailing_slash.json").trim()
val expected = getFileSnapshot("trailing_slash.json").trim()
assertEquals(expected, json, "The received json spec should match the expected content")
}
}
@ -291,7 +295,7 @@ internal class KompendiumTest {
val json = handleRequest(HttpMethod.Get, "/openapi.json").response.content
// expect
val expected = TestData.getFileSnapshot("complex_type.json").trim()
val expected = getFileSnapshot("complex_type.json").trim()
assertEquals(expected, json, "The received json spec should match the expected content")
}
}
@ -307,7 +311,7 @@ internal class KompendiumTest {
val json = handleRequest(HttpMethod.Get, "/openapi.json").response.content
// expect
val expected = TestData.getFileSnapshot("notarized_primitives.json").trim()
val expected = getFileSnapshot("notarized_primitives.json").trim()
assertEquals(expected, json, "The received json spec should match the expected content")
}
}
@ -323,7 +327,7 @@ internal class KompendiumTest {
val json = handleRequest(HttpMethod.Get, "/openapi.json").response.content
// expect
val expected = TestData.getFileSnapshot("response_list.json").trim()
val expected = getFileSnapshot("response_list.json").trim()
assertEquals(expected, json, "The received json spec should match the expected content")
}
}
@ -339,7 +343,7 @@ internal class KompendiumTest {
val json = handleRequest(HttpMethod.Get, "/openapi.json").response.content
// expect
val expected = TestData.getFileSnapshot("no_request_params_and_no_response_body.json").trim()
val expected = getFileSnapshot("no_request_params_and_no_response_body.json").trim()
assertEquals(expected, json, "The received json spec should match the expected content")
}
}
@ -355,7 +359,7 @@ internal class KompendiumTest {
val json = handleRequest(HttpMethod.Get, "/openapi.json").response.content
// expect
val expected = TestData.getFileSnapshot("non_required_params.json").trim()
val expected = getFileSnapshot("non_required_params.json").trim()
assertEquals(expected, json, "The received json spec should match the expected content")
}
}
@ -367,12 +371,11 @@ internal class KompendiumTest {
docs()
returnsList()
}) {
// do
val html = handleRequest(HttpMethod.Get, "/docs").response.content
// expected
val expected = TestData.getFileSnapshot("redoc.html")
val expected = getFileSnapshot("redoc.html")
assertEquals(expected, html)
}
}
@ -389,13 +392,11 @@ internal class KompendiumTest {
val json = handleRequest(HttpMethod.Get, "/openapi.json").response.content
// expect
val expected = TestData.getFileSnapshot("notarized_get_with_exception_response.json").trim()
val expected = getFileSnapshot("notarized_get_with_exception_response.json").trim()
assertEquals(expected, json, "The received json spec should match the expected content")
}
}
@Test
fun `Generates additional responses when passed multiple throwables`() {
withTestApplication({
@ -408,28 +409,52 @@ internal class KompendiumTest {
val json = handleRequest(HttpMethod.Get, "/openapi.json").response.content
// expect
val expected = TestData.getFileSnapshot("notarized_get_with_multiple_exception_responses.json").trim()
val expected = getFileSnapshot("notarized_get_with_multiple_exception_responses.json").trim()
assertEquals(expected, json, "The received json spec should match the expected content")
}
}
@Test
fun `Can generate example response and request bodies`() {
withTestApplication({
configModule()
docs()
withExamples()
}) {
// do
val json = handleRequest(HttpMethod.Get, "/openapi.json").response.content
// expect
val expected = getFileSnapshot("example_req_and_resp.json").trim()
assertEquals(expected, json, "The received json spec should match the expected content")
}
}
private companion object {
val testGetResponse = ResponseInfo(KompendiumHttpCodes.OK, "A Successful Endeavor")
val testPostResponse = ResponseInfo(KompendiumHttpCodes.CREATED, "A Successful Endeavor")
val testGetResponse = ResponseInfo<TestResponse>(KompendiumHttpCodes.OK, "A Successful Endeavor")
val testGetListResponse = ResponseInfo<List<TestResponse>>(KompendiumHttpCodes.OK, "A Successful List-y Endeavor")
val testPostResponse = ResponseInfo<TestCreatedResponse>(KompendiumHttpCodes.CREATED, "A Successful Endeavor")
val testPostResponseAgain = ResponseInfo<Boolean>(KompendiumHttpCodes.CREATED, "A Successful Endeavor")
val testDeleteResponse =
ResponseInfo(KompendiumHttpCodes.NO_CONTENT, "A Successful Endeavor", mediaTypes = emptyList())
val testRequest = RequestInfo("A Test request")
val testGetInfo = MethodInfo("Another get test", "testing more", testGetResponse)
ResponseInfo<Unit>(KompendiumHttpCodes.NO_CONTENT, "A Successful Endeavor", mediaTypes = emptyList())
val testRequest = RequestInfo<TestRequest>("A Test request")
val testRequestAgain = RequestInfo<Int>("A Test request")
val complexRequest = RequestInfo<ComplexRequest>("A Complex request")
val testGetInfo = GetInfo<TestParams, TestResponse>(summary = "Another get test", description = "testing more", responseInfo = testGetResponse)
val testGetInfoAgain = GetInfo<TestParams, List<TestResponse>>(summary = "Another get test", description = "testing more", responseInfo = testGetListResponse)
val testGetWithException = testGetInfo.copy(
canThrow = setOf(Exception::class)
)
val testGetWithMultipleExceptions = testGetInfo.copy(
canThrow = setOf(AccessDeniedException::class, Exception::class)
)
val testPostInfo = MethodInfo("Test post endpoint", "Post your tests here!", testPostResponse, testRequest)
val testPutInfo = MethodInfo("Test put endpoint", "Put your tests here!", testPostResponse, testRequest)
val testDeleteInfo = MethodInfo("Test delete endpoint", "testing my deletes", testDeleteResponse)
val emptyTestGetInfo = MethodInfo("No request params and response body", "testing more")
val testPostInfo = PostInfo<TestParams, TestRequest, TestCreatedResponse>(summary = "Test post endpoint", description = "Post your tests here!", responseInfo = testPostResponse, requestInfo = testRequest)
val testPutInfo = PutInfo<Unit, ComplexRequest, TestCreatedResponse>(summary = "Test put endpoint", description = "Put your tests here!", responseInfo = testPostResponse, requestInfo = complexRequest)
val testPutInfoAlso = PutInfo<TestParams, TestRequest, TestCreatedResponse>(summary = "Test put endpoint", description = "Put your tests here!", responseInfo = testPostResponse, requestInfo = testRequest)
val testPutInfoAgain = PutInfo<Unit, Int, Boolean>(summary = "Test put endpoint", description = "Put your tests here!", responseInfo = testPostResponseAgain, requestInfo = testRequestAgain)
val testDeleteInfo = DeleteInfo<TestParams, Unit>(summary = "Test delete endpoint", description = "testing my deletes", responseInfo = testDeleteResponse)
val emptyTestGetInfo = GetInfo<OptionalParams, Unit>(summary = "No request params and response body", description = "testing more")
val trulyEmptyTestGetInfo = GetInfo<Unit, Unit>(summary = "No request params and response body", description = "testing more")
}
private fun Application.configModule() {
@ -463,7 +488,7 @@ internal class KompendiumTest {
private fun Application.notarizedGetWithNotarizedException() {
routing {
route("/test") {
notarizedGet<TestParams, TestResponse>(testGetWithException) {
notarizedGet(testGetWithException) {
error("something terrible has happened!")
}
}
@ -473,7 +498,7 @@ internal class KompendiumTest {
private fun Application.notarizedGetWithMultipleThrowables() {
routing {
route("/test") {
notarizedGet<TestParams, TestResponse>(testGetWithMultipleExceptions) {
notarizedGet(testGetWithMultipleExceptions) {
error("something terrible has happened!")
}
}
@ -483,7 +508,7 @@ internal class KompendiumTest {
private fun Application.notarizedGetModule() {
routing {
route("/test") {
notarizedGet<TestParams, TestResponse>(testGetInfo) {
notarizedGet(testGetInfo) {
call.respondText { "hey dude ‼️ congratz on the get request" }
}
}
@ -493,7 +518,7 @@ internal class KompendiumTest {
private fun Application.notarizedPostModule() {
routing {
route("/test") {
notarizedPost<TestParams, TestRequest, TestCreatedResponse>(testPostInfo) {
notarizedPost(testPostInfo) {
call.respondText { "hey dude ✌️ congratz on the post request" }
}
}
@ -503,7 +528,7 @@ internal class KompendiumTest {
private fun Application.notarizedDeleteModule() {
routing {
route("/test") {
notarizedDelete<TestParams, Unit>(testDeleteInfo) {
notarizedDelete(testDeleteInfo) {
call.respond(HttpStatusCode.NoContent)
}
}
@ -513,7 +538,7 @@ internal class KompendiumTest {
private fun Application.notarizedPutModule() {
routing {
route("/test") {
notarizedPut<TestParams, TestRequest, TestCreatedResponse>(testPutInfo) {
notarizedPut(testPutInfoAlso) {
call.respondText { "hey pal 🌝 whatcha doin' here?" }
}
}
@ -528,7 +553,7 @@ internal class KompendiumTest {
route("/complex") {
route("path") {
route("with/an/{id}") {
notarizedGet<TestParams, TestResponse>(testGetInfo) {
notarizedGet(testGetInfo) {
call.respondText { "Aww you followed this whole route 🥺" }
}
}
@ -543,7 +568,7 @@ internal class KompendiumTest {
private fun Application.rootModule() {
routing {
route("/") {
notarizedGet<TestParams, TestResponse>(testGetInfo) {
notarizedGet(testGetInfo) {
call.respondText { "☎️🏠🌲" }
}
}
@ -554,7 +579,7 @@ internal class KompendiumTest {
routing {
route("/") {
route("/testerino") {
notarizedGet<TestParams, TestResponse>(testGetInfo) {
notarizedGet(testGetInfo) {
call.respondText { "🤔🔥" }
}
}
@ -566,7 +591,7 @@ internal class KompendiumTest {
routing {
route("/test") {
route("/") {
notarizedGet<TestParams, TestResponse>(testGetInfo) {
notarizedGet(testGetInfo) {
call.respondText { "🙀👾" }
}
}
@ -577,7 +602,7 @@ internal class KompendiumTest {
private fun Application.returnsList() {
routing {
route("/test") {
notarizedGet<TestParams, List<TestResponse>>(testGetInfo) {
notarizedGet(testGetInfoAgain) {
call.respondText { "hey dude ur doing amazing work!" }
}
}
@ -587,7 +612,7 @@ internal class KompendiumTest {
private fun Application.complexType() {
routing {
route("/test") {
notarizedPut<Unit, ComplexRequest, TestResponse>(testPutInfo) {
notarizedPut(testPutInfo) {
call.respondText { "heya" }
}
}
@ -597,7 +622,7 @@ internal class KompendiumTest {
private fun Application.primitives() {
routing {
route("/test") {
notarizedPut<Unit, Int, Boolean>(testPutInfo) {
notarizedPut(testPutInfoAgain) {
call.respondText { "heya" }
}
}
@ -607,7 +632,32 @@ internal class KompendiumTest {
private fun Application.emptyGet() {
routing {
route("/test/empty") {
notarizedGet<Unit, Unit>(emptyTestGetInfo) {
notarizedGet(trulyEmptyTestGetInfo) {
call.respond(HttpStatusCode.OK)
}
}
}
}
private fun Application.withExamples() {
routing {
route("/test/examples") {
notarizedPost(info = PostInfo<Unit, TestRequest, TestResponse>(
summary = "Example Parameters",
description = "A test for setting parameter examples",
requestInfo = RequestInfo(
description = "Test",
examples = mapOf(
"one" to TestRequest(fieldName = TestNested(nesty = "hey"), b = 4.0, aaa = emptyList()),
"two" to TestRequest(fieldName = TestNested(nesty = "hello"), b = 3.8, aaa = listOf(31324234))
)
),
responseInfo = ResponseInfo(
status = 201,
description = "nice",
examples = mapOf("test" to TestResponse(c = "spud"))
),
)) {
call.respond(HttpStatusCode.OK)
}
}
@ -618,7 +668,7 @@ internal class KompendiumTest {
private fun Application.nonRequiredParamsGet() {
routing {
route("/test/optional") {
notarizedGet<OptionalParams, Unit>(emptyTestGetInfo) {
notarizedGet(emptyTestGetInfo) {
call.respond(HttpStatusCode.OK)
}
}

View File

@ -1,27 +0,0 @@
package org.leafygreens.kompendium.models
import com.fasterxml.jackson.annotation.JsonInclude
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import kotlin.test.Test
import kotlin.test.assertEquals
import org.leafygreens.kompendium.util.TestData
internal class OpenApiSpecTest {
private val mapper = jacksonObjectMapper()
.setSerializationInclusion(JsonInclude.Include.NON_NULL)
.writerWithDefaultPrettyPrinter()
@Test
fun `OpenApiSpec can be serialized into a valid Open API Spec`() {
// when
val spec = TestData.testSpec
// do
val json = mapper.writeValueAsString(spec)
// expect
val expected = TestData.getFileSnapshot("petstore.json").trim()
assertEquals(expected, json, "Should serialize an empty spec")
}
}

View File

@ -1,227 +0,0 @@
package org.leafygreens.kompendium.util
import java.io.File
import java.net.URI
import org.leafygreens.kompendium.models.oas.OpenApiSpec
import org.leafygreens.kompendium.models.oas.OpenApiSpecComponents
import org.leafygreens.kompendium.models.oas.OpenApiSpecExternalDocumentation
import org.leafygreens.kompendium.models.oas.OpenApiSpecInfo
import org.leafygreens.kompendium.models.oas.OpenApiSpecInfoContact
import org.leafygreens.kompendium.models.oas.OpenApiSpecInfoLicense
import org.leafygreens.kompendium.models.oas.OpenApiSpecMediaType
import org.leafygreens.kompendium.models.oas.OpenApiSpecOAuthFlow
import org.leafygreens.kompendium.models.oas.OpenApiSpecOAuthFlows
import org.leafygreens.kompendium.models.oas.OpenApiSpecParameter
import org.leafygreens.kompendium.models.oas.OpenApiSpecPathItem
import org.leafygreens.kompendium.models.oas.OpenApiSpecPathItemOperation
import org.leafygreens.kompendium.models.oas.OpenApiSpecReferenceObject
import org.leafygreens.kompendium.models.oas.OpenApiSpecRequest
import org.leafygreens.kompendium.models.oas.OpenApiSpecResponse
import org.leafygreens.kompendium.models.oas.OpenApiSpecSchemaArray
import org.leafygreens.kompendium.models.oas.OpenApiSpecSchemaRef
import org.leafygreens.kompendium.models.oas.OpenApiSpecSchemaSecurity
import org.leafygreens.kompendium.models.oas.OpenApiSpecSchemaString
import org.leafygreens.kompendium.models.oas.OpenApiSpecServer
import org.leafygreens.kompendium.models.oas.OpenApiSpecTag
object TestData {
fun getFileSnapshot(fileName: String): String {
val snapshotPath = "src/test/resources"
val file = File("$snapshotPath/$fileName")
return file.readText()
}
val testSpec = OpenApiSpec(
info = OpenApiSpecInfo(
title = "Swagger Petstore",
description = """
This is a sample server Petstore server. You can find out more about Swagger at
[http://swagger.io](http://swagger.io) or on [irc.freenode.net, #swagger](http://swagger.io/irc/).
For this sample, you can use the api key `special-key` to test the authorization filters.
""".trimIndent(),
termsOfService = URI("http://swagger.io/terms/"),
contact = OpenApiSpecInfoContact(
name = "Team Swag",
email = "apiteam@swagger.io"
),
license = OpenApiSpecInfoLicense(
name = "Apache 2.0",
url = URI("http://www.apache.org/licenses/LICENSE-2.0.html")
),
version = "1.0.0"
),
externalDocs = OpenApiSpecExternalDocumentation(
description = "Find out more about Swagger",
url = URI("http://swagger.io")
),
servers = mutableListOf(
OpenApiSpecServer(
url = URI("https://petstore.swagger.io/v2")
),
OpenApiSpecServer(
url = URI("http://petstore.swagger.io/v2")
)
),
tags = mutableListOf(
OpenApiSpecTag(
name = "pet",
description = "Everything about your Pets",
externalDocs = OpenApiSpecExternalDocumentation(
description = "Find out more",
url = URI("http://swagger.io")
)
),
OpenApiSpecTag(
name = "store",
description = "Access to Petstore orders"
),
OpenApiSpecTag(
name = "user",
description = "Operations about user",
externalDocs = OpenApiSpecExternalDocumentation(
description = "Find out more about our store",
url = URI("http://swagger.io")
)
)
),
paths = mutableMapOf(
"/pet" to OpenApiSpecPathItem(
put = OpenApiSpecPathItemOperation(
tags = setOf("pet"),
summary = "Update an existing pet",
operationId = "updatePet",
requestBody = OpenApiSpecRequest(
description = "Pet object that needs to be added to the store",
content = mapOf(
"application/json" to OpenApiSpecMediaType.Explicit(
schema = OpenApiSpecSchemaRef(`$ref` = "#/components/schemas/Pet")
),
"application/xml" to OpenApiSpecMediaType.Explicit(
schema = OpenApiSpecSchemaRef(`$ref` = "#/components/schemas/Pet")
)
),
required = true
),
responses = mapOf(
400 to OpenApiSpecResponse(
description = "Invalid ID supplied",
content = emptyMap()
),
404 to OpenApiSpecResponse(
description = "Pet not found",
content = emptyMap()
),
405 to OpenApiSpecResponse(
description = "Validation exception",
content = emptyMap()
)
),
security = listOf(
mapOf(
"petstore_auth" to listOf("write:pets", "read:pets")
)
),
`x-codegen-request-body-name` = "body"
),
post = OpenApiSpecPathItemOperation(
tags = setOf("pet"),
summary = "Add a new pet to the store",
operationId = "addPet",
requestBody = OpenApiSpecRequest(
description = "Pet object that needs to be added to the store",
content = mapOf(
"application/json" to OpenApiSpecMediaType.Referenced(
schema = OpenApiSpecReferenceObject(`$ref` = "#/components/schemas/Pet")
),
"application/xml" to OpenApiSpecMediaType.Referenced(
schema = OpenApiSpecReferenceObject(`$ref` = "#/components/schemas/Pet")
)
)
),
responses = mapOf(
405 to OpenApiSpecResponse(
description = "Invalid Input",
content = emptyMap()
)
),
security = listOf(
mapOf(
"petstore_auth" to listOf("write:pets", "read:pets")
)
),
`x-codegen-request-body-name` = "body"
)
),
"/pet/findByStatus" to OpenApiSpecPathItem(
get = OpenApiSpecPathItemOperation(
tags = setOf("pet"),
summary = "Find Pets by status",
description = "Multiple status values can be provided with comma separated strings",
operationId = "findPetsByStatus",
parameters = listOf(
OpenApiSpecParameter(
name = "status",
`in` = "query",
description = "Status values that need to be considered for filter",
required = true,
style = "form",
explode = true,
schema = OpenApiSpecSchemaArray(
items = OpenApiSpecSchemaString(
default = "available",
`enum` = setOf("available", "pending", "sold")
)
)
)
),
responses = mapOf(
200 to OpenApiSpecResponse(
description = "successful operation",
content = mapOf(
"application/xml" to OpenApiSpecMediaType.Explicit(
schema = OpenApiSpecSchemaArray(
items = OpenApiSpecSchemaRef("#/components/schemas/Pet")
)
),
"application/json" to OpenApiSpecMediaType.Explicit(
schema = OpenApiSpecSchemaArray(
items = OpenApiSpecSchemaRef("#/components/schemas/Pet")
)
)
)
),
400 to OpenApiSpecResponse(
description = "Invalid status value",
content = mapOf()
)
),
security = listOf(mapOf(
"petstore_auth" to listOf("write:pets", "read:pets")
))
)
)
),
components = OpenApiSpecComponents(
securitySchemes = mutableMapOf(
"petstore_auth" to OpenApiSpecSchemaSecurity(
type = "oauth2",
flows = OpenApiSpecOAuthFlows(
implicit = OpenApiSpecOAuthFlow(
authorizationUrl = URI("http://petstore.swagger.io/oauth/dialog"),
scopes = mapOf(
"write:pets" to "modify pets in your account",
"read:pets" to "read your pets"
)
)
)
),
"api_key" to OpenApiSpecSchemaSecurity(
type = "apiKey",
name = "api_key",
`in` = "header"
)
),
schemas = mutableMapOf()
)
)
}

View File

@ -0,0 +1,33 @@
package org.leafygreens.kompendium.util
import java.io.File
import java.net.URI
import org.leafygreens.kompendium.models.oas.OpenApiSpec
import org.leafygreens.kompendium.models.oas.OpenApiSpecComponents
import org.leafygreens.kompendium.models.oas.OpenApiSpecExternalDocumentation
import org.leafygreens.kompendium.models.oas.OpenApiSpecInfo
import org.leafygreens.kompendium.models.oas.OpenApiSpecInfoContact
import org.leafygreens.kompendium.models.oas.OpenApiSpecInfoLicense
import org.leafygreens.kompendium.models.oas.OpenApiSpecMediaType
import org.leafygreens.kompendium.models.oas.OpenApiSpecOAuthFlow
import org.leafygreens.kompendium.models.oas.OpenApiSpecOAuthFlows
import org.leafygreens.kompendium.models.oas.OpenApiSpecParameter
import org.leafygreens.kompendium.models.oas.OpenApiSpecPathItem
import org.leafygreens.kompendium.models.oas.OpenApiSpecPathItemOperation
import org.leafygreens.kompendium.models.oas.OpenApiSpecReferenceObject
import org.leafygreens.kompendium.models.oas.OpenApiSpecRequest
import org.leafygreens.kompendium.models.oas.OpenApiSpecResponse
import org.leafygreens.kompendium.models.oas.OpenApiSpecSchemaArray
import org.leafygreens.kompendium.models.oas.OpenApiSpecSchemaRef
import org.leafygreens.kompendium.models.oas.OpenApiSpecSchemaSecurity
import org.leafygreens.kompendium.models.oas.OpenApiSpecSchemaString
import org.leafygreens.kompendium.models.oas.OpenApiSpecServer
import org.leafygreens.kompendium.models.oas.OpenApiSpecTag
object TestHelpers {
fun getFileSnapshot(fileName: String): String {
val snapshotPath = "src/test/resources"
val file = File("$snapshotPath/$fileName")
return file.readText()
}
}

View File

@ -30,7 +30,7 @@
"description" : "Put your tests here!",
"parameters" : [ ],
"requestBody" : {
"description" : "A Test request",
"description" : "A Complex request",
"content" : {
"application/json" : {
"schema" : {
@ -46,7 +46,7 @@
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/TestResponse"
"$ref" : "#/components/schemas/TestCreatedResponse"
}
}
}
@ -61,10 +61,17 @@
"String" : {
"type" : "string"
},
"TestResponse" : {
"Int" : {
"format" : "int32",
"type" : "integer"
},
"TestCreatedResponse" : {
"properties" : {
"c" : {
"$ref" : "#/components/schemas/String"
},
"id" : {
"$ref" : "#/components/schemas/Int"
}
},
"type" : "object"

View File

@ -0,0 +1,140 @@
{
"openapi" : "3.0.3",
"info" : {
"title" : "Test API",
"version" : "1.33.7",
"description" : "An amazing, fully-ish \uD83D\uDE09 generated API spec",
"termsOfService" : "https://example.com",
"contact" : {
"name" : "Homer Simpson",
"url" : "https://gph.is/1NPUDiM",
"email" : "chunkylover53@aol.com"
},
"license" : {
"name" : "MIT",
"url" : "https://github.com/lg-backbone/kompendium/blob/main/LICENSE"
}
},
"servers" : [ {
"url" : "https://myawesomeapi.com",
"description" : "Production instance of my API"
}, {
"url" : "https://staging.myawesomeapi.com",
"description" : "Where the fun stuff happens"
} ],
"paths" : {
"/test/examples" : {
"post" : {
"tags" : [ ],
"summary" : "Example Parameters",
"description" : "A test for setting parameter examples",
"parameters" : [ ],
"requestBody" : {
"description" : "Test",
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/TestRequest"
},
"examples" : {
"one" : {
"value" : {
"fieldName" : {
"nesty" : "hey"
},
"b" : 4.0,
"aaa" : [ ]
}
},
"two" : {
"value" : {
"fieldName" : {
"nesty" : "hello"
},
"b" : 3.8,
"aaa" : [ 31324234 ]
}
}
}
}
},
"required" : false
},
"responses" : {
"201" : {
"description" : "nice",
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/TestResponse"
},
"examples" : {
"test" : {
"value" : {
"c" : "spud"
}
}
}
}
}
}
},
"deprecated" : false
}
}
},
"components" : {
"schemas" : {
"String" : {
"type" : "string"
},
"TestResponse" : {
"properties" : {
"c" : {
"$ref" : "#/components/schemas/String"
}
},
"type" : "object"
},
"Long" : {
"format" : "int64",
"type" : "integer"
},
"List-Long" : {
"items" : {
"$ref" : "#/components/schemas/Long"
},
"type" : "array"
},
"Double" : {
"format" : "double",
"type" : "number"
},
"TestNested" : {
"properties" : {
"nesty" : {
"$ref" : "#/components/schemas/String"
}
},
"type" : "object"
},
"TestRequest" : {
"properties" : {
"aaa" : {
"$ref" : "#/components/schemas/List-Long"
},
"b" : {
"$ref" : "#/components/schemas/Double"
},
"fieldName" : {
"$ref" : "#/components/schemas/TestNested"
}
},
"type" : "object"
}
},
"securitySchemes" : { }
},
"security" : [ ],
"tags" : [ ]
}

View File

@ -47,7 +47,7 @@
} ],
"responses" : {
"200" : {
"description" : "A Successful Endeavor",
"description" : "A Successful List-y Endeavor",
"content" : {
"application/json" : {
"schema" : {