Error Responses, PR Template, Code Refactor, and Test plugin update (#42)

This commit is contained in:
Ryan Brink
2021-04-29 19:08:45 -04:00
committed by GitHub
parent 571dc11c29
commit 26ada3daad
17 changed files with 578 additions and 79 deletions

30
.github/pull_request_template.md vendored Normal file
View File

@ -0,0 +1,30 @@
# Description
Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change.
Closes # (issue)
## Type of change
Please delete options that are not relevant.
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] This change requires a documentation update
# How Has This Been Tested?
Please describe the tests that you ran to verify your changes.
# Checklist:
- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my own code
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [ ] I have updated the CHANGELOG and bumped the version
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream modules

View File

@ -1,5 +1,16 @@
# Changelog
## [0.7.0] - April 29th, 2021
### Added
- `notarizedException` for notarizing `StatusPage` handlers 🎉
- `com.adarshr.test-logger` Gradle plugin for improved test output clarity and insight
### Changed
- Refactored `kompendium-core` to break up the `Kompendium` object into slightly more manageable chunks
## [0.6.2] - April 23rd, 2021
### Added

View File

@ -40,12 +40,13 @@ dependencies {
### Warning 🚨
Kompendium is still under active development ⚠️ There are a number of yet-to-be-implemented features, including
- Multiple Responses 📜
- Sealed Class / Polymorphic Support 😬
- Validation / Enforcement (❓👀❓)
If you have a feature that is not listed here, please open an issue!
In addition, if you find any 🐞😱 please open an issue as well!
### Notarized Routes
Kompendium introduces the concept of `notarized` HTTP methods. That is, for all your `GET`, `POST`, `PUT`, and `DELETE`
@ -59,6 +60,14 @@ will consume are
`GET` and `DELETE` take `TParam` and `TResp` while `PUT` and `POST` take all three.
In addition to standard HTTP Methods, Kompendium also introduced the concept of `notarizedExceptions`. Using the `StatusPage`
extension, users can notarize all handled exceptions, along with their respective HTTP codes and response types.
Exceptions that have been `notarized` require two types as supplemental information
- `TErr`: Used to notarize the exception being handled by this use case. Used for matching responses at the route level.
- `TResp`: Same as above, this dictates the expected return type of the error response.
In keeping with minimal invasion, these extension methods all consume the same code block as a standard Ktor route method,
meaning that swapping in a default Ktor route and a Kompendium `notarized` route is as simple as a single method change.
@ -102,6 +111,11 @@ fun Application.mainModule() {
setSerializationInclusion(JsonInclude.Include.NON_NULL)
}
}
install(StatusPages) {
notarizedException<Exception, ExceptionResponse>(exceptionResponseInfo) {
call.respond(HttpStatusCode.BadRequest, ExceptionResponse("Why you do dis?"))
}
}
routing {
openApi()
redoc()
@ -125,9 +139,19 @@ fun Application.mainModule() {
call.respondText { "heya" }
}
}
route("/error") {
notarizedGet<Unit, ExampleResponse>(testSingleGetInfoWithThrowable) {
error("bad things just happened")
}
}
}
}
}
val testSingleGetInfoWithThrowable = testSingleGetInfo.copy(
summary = "Show me the error baby 🙏",
canThrow = setOf(Exception::class) // Must match an exception that has been notarized in the `StatusPages`
)
```
When run in the playground, this would output the following at `/openapi.json`
@ -136,7 +160,7 @@ https://gist.github.com/rgbrizzlehizzle/b9544922f2e99a2815177f8bdbf80668
### Kompendium Auth and security schemes
There is a seperate library to handle security schemes: `kompendium-auth`.
There is a separate library to handle security schemes: `kompendium-auth`.
This needs to be added to your project as dependency.
At the moment, the basic and jwt authentication is only supported.
@ -188,11 +212,22 @@ Minimal Example:
```kotlin
install(Webjars)
routing {
openApi()
openApi(oas)
swaggerUI()
}
```
### Enabling ReDoc
Unlike swagger, redoc is provided (perhaps confusingly, in the `core` module). This means out of the box with `kompendium-core`, you can add
[ReDoc](https://github.com/Redocly/redoc) as follows
```kotlin
routing {
openApi(oas)
redoc(oas)
}
```
## Limitations
### Kompendium as a singleton

View File

@ -1,6 +1,7 @@
plugins {
id("org.jetbrains.kotlin.jvm") version "1.4.32" apply false
id("io.gitlab.arturbosch.detekt") version "1.16.0-RC2" apply false
id("com.adarshr.test-logger") version "3.0.0" apply false
}
allprojects {
@ -21,6 +22,7 @@ allprojects {
apply(plugin = "org.jetbrains.kotlin.jvm")
apply(plugin = "io.gitlab.arturbosch.detekt")
apply(plugin = "com.adarshr.test-logger")
apply(plugin = "idea")
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
@ -29,6 +31,25 @@ allprojects {
}
}
configure<com.adarshr.gradle.testlogger.TestLoggerExtension> {
setTheme("standard")
setLogLevel("lifecycle")
showExceptions = true
showStackTraces = true
showFullStackTraces = false
showCauses = true
slowThreshold = 2000
showSummary = true
showSimpleNames = false
showPassed = true
showSkipped = true
showFailed = true
showStandardStreams = false
showPassedStandardStreams = true
showSkippedStandardStreams = true
showFailedStandardStreams = true
}
configure<io.gitlab.arturbosch.detekt.extensions.DetektExtension> {
toolVersion = "1.16.0-RC2"
config = files("${rootProject.projectDir}/detekt.yml")

View File

@ -21,7 +21,7 @@ logback-classic = { group = "ch.qos.logback", name = "logback-classic", version.
logback-core = { group = "ch.qos.logback", name = "logback-core", version.ref = "logback" }
# webjars
webjars-swagger-ui = { group "org.webjars", name = "swagger-ui", version.ref = "swagger-ui" }
webjars-swagger-ui = { group = "org.webjars", name = "swagger-ui", version.ref = "swagger-ui" }
[bundles]
ktor = [ "ktor-server-core", "ktor-server-netty", "ktor-jackson", "ktor-html-builder" ]

View File

@ -12,7 +12,6 @@ import io.ktor.routing.*
import io.ktor.server.testing.*
import org.junit.Test
import org.leafygreens.kompendium.Kompendium
import org.leafygreens.kompendium.Kompendium.notarizedGet
import org.leafygreens.kompendium.auth.KompendiumAuth.notarizedBasic
import org.leafygreens.kompendium.auth.KompendiumAuth.notarizedJwt
import org.leafygreens.kompendium.auth.util.TestData
@ -27,6 +26,7 @@ import org.leafygreens.kompendium.routes.redoc
import org.leafygreens.kompendium.util.KompendiumHttpCodes
import kotlin.test.AfterTest
import kotlin.test.assertEquals
import org.leafygreens.kompendium.Notarized.notarizedGet
internal class KompendiumAuthTest {

View File

@ -1,22 +1,17 @@
package org.leafygreens.kompendium
import io.ktor.application.ApplicationCall
import io.ktor.http.HttpMethod
import io.ktor.routing.Route
import io.ktor.routing.method
import io.ktor.util.pipeline.PipelineInterceptor
import kotlin.reflect.KClass
import kotlin.reflect.KType
import kotlin.reflect.full.createType
import kotlin.reflect.full.findAnnotation
import kotlin.reflect.full.memberProperties
import kotlin.reflect.jvm.javaField
import kotlin.reflect.typeOf
import org.leafygreens.kompendium.Kontent.generateKontent
import org.leafygreens.kompendium.Kontent.generateParameterKontent
import org.leafygreens.kompendium.annotations.CookieParam
import org.leafygreens.kompendium.annotations.HeaderParam
import org.leafygreens.kompendium.annotations.PathParam
import org.leafygreens.kompendium.annotations.QueryParam
import org.leafygreens.kompendium.models.meta.ErrorMap
import org.leafygreens.kompendium.models.meta.MethodInfo
import org.leafygreens.kompendium.models.meta.RequestInfo
import org.leafygreens.kompendium.models.meta.ResponseInfo
@ -25,8 +20,8 @@ import org.leafygreens.kompendium.models.oas.OpenApiSpec
import org.leafygreens.kompendium.models.oas.OpenApiSpecInfo
import org.leafygreens.kompendium.models.oas.OpenApiSpecMediaType
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.OpenApiSpecReferencable
import org.leafygreens.kompendium.models.oas.OpenApiSpecReferenceObject
import org.leafygreens.kompendium.models.oas.OpenApiSpecRequest
import org.leafygreens.kompendium.models.oas.OpenApiSpecResponse
@ -38,6 +33,7 @@ import org.leafygreens.kompendium.util.Helpers.getReferenceSlug
object Kompendium {
var errorMap: ErrorMap = emptyMap()
var cache: SchemaMap = emptyMap()
var openApiSpec = OpenApiSpec(
@ -48,47 +44,6 @@ object Kompendium {
var pathCalculator: PathCalculator = CorePathCalculator()
@OptIn(ExperimentalStdlibApi::class)
inline fun <reified TParam : Any, reified TResp : Any> Route.notarizedGet(
info: MethodInfo,
noinline body: PipelineInterceptor<Unit, ApplicationCall>
): Route = notarizationPreFlight<TParam, Unit, TResp>() { paramType, requestType, responseType ->
val path = pathCalculator.calculate(this)
openApiSpec.paths.getOrPut(path) { OpenApiSpecPathItem() }
openApiSpec.paths[path]?.get = info.parseMethodInfo(HttpMethod.Get, paramType, requestType, responseType)
return method(HttpMethod.Get) { handle(body) }
}
inline fun <reified TParam : Any, reified TReq : Any, reified TResp : Any> Route.notarizedPost(
info: MethodInfo,
noinline body: PipelineInterceptor<Unit, ApplicationCall>
): Route = notarizationPreFlight<TParam, TReq, TResp>() { paramType, requestType, responseType ->
val path = pathCalculator.calculate(this)
openApiSpec.paths.getOrPut(path) { OpenApiSpecPathItem() }
openApiSpec.paths[path]?.post = info.parseMethodInfo(HttpMethod.Post, paramType, requestType, responseType)
return method(HttpMethod.Post) { handle(body) }
}
inline fun <reified TParam : Any, reified TReq : Any, reified TResp : Any> Route.notarizedPut(
info: MethodInfo,
noinline body: PipelineInterceptor<Unit, ApplicationCall>,
): Route = notarizationPreFlight<TParam, TReq, TResp>() { paramType, requestType, responseType ->
val path = pathCalculator.calculate(this)
openApiSpec.paths.getOrPut(path) { OpenApiSpecPathItem() }
openApiSpec.paths[path]?.put = info.parseMethodInfo(HttpMethod.Put, paramType, requestType, responseType)
return method(HttpMethod.Put) { handle(body) }
}
inline fun <reified TParam : Any, reified TResp : Any> Route.notarizedDelete(
info: MethodInfo,
noinline body: PipelineInterceptor<Unit, ApplicationCall>
): Route = notarizationPreFlight<TParam, Unit, TResp> { paramType, requestType, responseType ->
val path = pathCalculator.calculate(this)
openApiSpec.paths.getOrPut(path) { OpenApiSpecPathItem() }
openApiSpec.paths[path]?.delete = info.parseMethodInfo(HttpMethod.Delete, paramType, requestType, responseType)
return method(HttpMethod.Delete) { handle(body) }
}
// TODO here down is a mess, needs refactor once core functionality is in place
fun MethodInfo.parseMethodInfo(
method: HttpMethod,
@ -101,7 +56,18 @@ object Kompendium {
tags = this.tags,
deprecated = this.deprecated,
parameters = paramType.toParameterSpec(),
responses = responseType.toResponseSpec(responseInfo)?.let { mapOf(it) },
responses = responseType.toResponseSpec(responseInfo)?.let { mapOf(it) }.let {
when (it) {
null -> {
val throwables = parseThrowables(canThrow)
when (throwables.isEmpty()) {
true -> null
false -> throwables
}
}
else -> it.plus(parseThrowables(canThrow))
}
},
requestBody = if (method != HttpMethod.Get) requestType.toRequestSpec(requestInfo) else null,
security = if (this.securitySchemes.isNotEmpty()) listOf(
// TODO support scopes
@ -109,18 +75,15 @@ object Kompendium {
) else null
)
@OptIn(ExperimentalStdlibApi::class)
inline fun <reified TParam : Any, reified TReq : Any, reified TResp : Any> notarizationPreFlight(
block: (KType, KType, KType) -> Route
): Route {
cache = generateKontent<TResp>(cache)
cache = generateKontent<TReq>(cache)
cache = generateParameterKontent<TParam>(cache)
openApiSpec.components.schemas.putAll(cache)
val requestType = typeOf<TReq>()
val responseType = typeOf<TResp>()
val paramType = typeOf<TParam>()
return block.invoke(paramType, requestType, responseType)
private fun parseThrowables(throwables: Set<KClass<*>>): Map<Int, OpenApiSpecReferencable> = throwables.mapNotNull {
errorMap[it.createType()]
}.toMap()
fun ResponseInfo.parseErrorInfo(
errorType: KType,
responseType: KType
) {
errorMap = errorMap.plus(errorType to responseType.toResponseSpec(this))
}
// TODO These two lookin' real similar 👀 Combine?

View File

@ -0,0 +1,33 @@
package org.leafygreens.kompendium
import io.ktor.routing.Route
import kotlin.reflect.KType
import kotlin.reflect.typeOf
object KompendiumPreFlight {
@OptIn(ExperimentalStdlibApi::class)
inline fun <reified TParam : Any, reified TReq : Any, reified TResp : Any> methodNotarizationPreFlight(
block: (KType, KType, KType) -> Route
): Route {
Kompendium.cache = Kontent.generateKontent<TResp>(Kompendium.cache)
Kompendium.cache = Kontent.generateKontent<TReq>(Kompendium.cache)
Kompendium.cache = Kontent.generateParameterKontent<TParam>(Kompendium.cache)
Kompendium.openApiSpec.components.schemas.putAll(Kompendium.cache)
val requestType = typeOf<TReq>()
val responseType = typeOf<TResp>()
val paramType = typeOf<TParam>()
return block.invoke(paramType, requestType, responseType)
}
@OptIn(ExperimentalStdlibApi::class)
inline fun <reified TErr: Throwable, reified TResp : Any> errorNotarizationPreFlight(
block: (KType, KType) -> Unit
) {
Kompendium.cache = Kontent.generateKontent<TResp>(Kompendium.cache)
Kompendium.openApiSpec.components.schemas.putAll(Kompendium.cache)
val errorType = typeOf<TErr>()
val responseType = typeOf<TResp>()
return block.invoke(errorType, responseType)
}
}

View File

@ -0,0 +1,76 @@
package org.leafygreens.kompendium
import io.ktor.application.ApplicationCall
import io.ktor.features.StatusPages
import io.ktor.http.HttpMethod
import io.ktor.routing.Route
import io.ktor.routing.method
import io.ktor.util.pipeline.PipelineContext
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.ResponseInfo
import org.leafygreens.kompendium.models.oas.OpenApiSpecPathItem
object Notarized {
@OptIn(ExperimentalStdlibApi::class)
inline fun <reified TParam : Any, reified TResp : Any> Route.notarizedGet(
info: MethodInfo,
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)
return method(HttpMethod.Get) { handle(body) }
}
inline fun <reified TParam : Any, reified TReq : Any, reified TResp : Any> Route.notarizedPost(
info: MethodInfo,
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)
return method(HttpMethod.Post) { handle(body) }
}
inline fun <reified TParam : Any, reified TReq : Any, reified TResp : Any> Route.notarizedPut(
info: MethodInfo,
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)
return method(HttpMethod.Put) { handle(body) }
}
inline fun <reified TParam : Any, reified TResp : Any> Route.notarizedDelete(
info: MethodInfo,
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)
return method(HttpMethod.Delete) { handle(body) }
}
inline fun <reified TErr : Throwable, reified TResp : Any> StatusPages.Configuration.notarizedException(
info: ResponseInfo,
noinline handler: suspend PipelineContext<Unit, ApplicationCall>.(TErr) -> Unit
) = errorNotarizationPreFlight<TErr, TResp>() { errorType, responseType ->
info.parseErrorInfo(errorType, responseType)
exception(handler)
}
}

View File

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

View File

@ -1,5 +1,7 @@
package org.leafygreens.kompendium.models.meta
import kotlin.reflect.KClass
// TODO Seal and extend by method type?
data class MethodInfo(
val summary: String,
@ -8,5 +10,6 @@ data class MethodInfo(
val requestInfo: RequestInfo? = null,
val tags: Set<String> = emptySet(),
val deprecated: Boolean = false,
val securitySchemes: Set<String> = emptySet()
val securitySchemes: Set<String> = emptySet(),
val canThrow: Set<KClass<*>> = emptySet()
)

View File

@ -1,7 +1,7 @@
package org.leafygreens.kompendium.models.meta
data class ResponseInfo(
val status: Int, // TODO How to handle error codes?
val status: Int,
val description: String,
val mediaTypes: List<String> = listOf("application/json")
)

View File

@ -6,6 +6,7 @@ import io.ktor.application.Application
import io.ktor.application.call
import io.ktor.application.install
import io.ktor.features.ContentNegotiation
import io.ktor.features.StatusPages
import io.ktor.http.HttpMethod
import io.ktor.http.HttpStatusCode
import io.ktor.jackson.jackson
@ -19,10 +20,11 @@ import java.net.URI
import kotlin.test.AfterTest
import kotlin.test.Test
import kotlin.test.assertEquals
import org.leafygreens.kompendium.Kompendium.notarizedDelete
import org.leafygreens.kompendium.Kompendium.notarizedGet
import org.leafygreens.kompendium.Kompendium.notarizedPost
import org.leafygreens.kompendium.Kompendium.notarizedPut
import org.leafygreens.kompendium.Notarized.notarizedDelete
import org.leafygreens.kompendium.Notarized.notarizedException
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.RequestInfo
@ -34,6 +36,7 @@ import org.leafygreens.kompendium.models.oas.OpenApiSpecServer
import org.leafygreens.kompendium.routes.openApi
import org.leafygreens.kompendium.routes.redoc
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
@ -374,6 +377,42 @@ internal class KompendiumTest {
}
}
@Test
fun `Generates additional responses when passed a throwable`() {
withTestApplication({
statusPageModule()
configModule()
docs()
notarizedGetWithNotarizedException()
}) {
// do
val json = handleRequest(HttpMethod.Get, "/openapi.json").response.content
// expect
val expected = TestData.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({
statusPageMultiExceptions()
configModule()
docs()
notarizedGetWithMultipleThrowables()
}) {
// do
val json = handleRequest(HttpMethod.Get, "/openapi.json").response.content
// expect
val expected = TestData.getFileSnapshot("notarized_get_with_multiple_exception_responses.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")
@ -381,6 +420,12 @@ internal class KompendiumTest {
ResponseInfo(KompendiumHttpCodes.NO_CONTENT, "A Successful Endeavor", mediaTypes = emptyList())
val testRequest = RequestInfo("A Test request")
val testGetInfo = MethodInfo("Another get test", "testing more", testGetResponse)
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)
@ -396,6 +441,45 @@ internal class KompendiumTest {
}
}
private fun Application.statusPageModule() {
install(StatusPages) {
notarizedException<Exception, ExceptionResponse>(info = ResponseInfo(400, "Bad Things Happened")) {
call.respond(HttpStatusCode.BadRequest, ExceptionResponse("Why you do dis?"))
}
}
}
private fun Application.statusPageMultiExceptions() {
install(StatusPages) {
notarizedException<AccessDeniedException, Unit>(info = ResponseInfo(403, "New API who dis?")) {
call.respond(HttpStatusCode.Forbidden)
}
notarizedException<Exception, ExceptionResponse>(info = ResponseInfo(400, "Bad Things Happened")) {
call.respond(HttpStatusCode.BadRequest, ExceptionResponse("Why you do dis?"))
}
}
}
private fun Application.notarizedGetWithNotarizedException() {
routing {
route("/test") {
notarizedGet<TestParams, TestResponse>(testGetWithException) {
error("something terrible has happened!")
}
}
}
}
private fun Application.notarizedGetWithMultipleThrowables() {
routing {
route("/test") {
notarizedGet<TestParams, TestResponse>(testGetWithMultipleExceptions) {
error("something terrible has happened!")
}
}
}
}
private fun Application.notarizedGetModule() {
routing {
route("/test") {

View File

@ -71,3 +71,5 @@ sealed class TestSealedClass(open val a: String)
data class SimpleTSC(val b: Int) : TestSealedClass("hey")
open class MediumTSC(override val a: String, val b: Int) : TestSealedClass(a)
data class WildTSC(val c: Boolean, val d: String, val e: Int) : MediumTSC(d, e)
data class ExceptionResponse(val message: String)

View File

@ -0,0 +1,104 @@
{
"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" : {
"get" : {
"tags" : [ ],
"summary" : "Another get test",
"description" : "testing more",
"parameters" : [ {
"name" : "a",
"in" : "path",
"schema" : {
"$ref" : "#/components/schemas/String"
},
"required" : true,
"deprecated" : false
}, {
"name" : "aa",
"in" : "query",
"schema" : {
"$ref" : "#/components/schemas/Int"
},
"required" : true,
"deprecated" : false
} ],
"responses" : {
"200" : {
"description" : "A Successful Endeavor",
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/TestResponse"
}
}
}
},
"400" : {
"description" : "Bad Things Happened",
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/ExceptionResponse"
}
}
}
}
},
"deprecated" : false
}
}
},
"components" : {
"schemas" : {
"String" : {
"type" : "string"
},
"ExceptionResponse" : {
"properties" : {
"message" : {
"$ref" : "#/components/schemas/String"
}
},
"type" : "object"
},
"TestResponse" : {
"properties" : {
"c" : {
"$ref" : "#/components/schemas/String"
}
},
"type" : "object"
},
"Int" : {
"format" : "int32",
"type" : "integer"
}
},
"securitySchemes" : { }
},
"security" : [ ],
"tags" : [ ]
}

View File

@ -0,0 +1,107 @@
{
"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" : {
"get" : {
"tags" : [ ],
"summary" : "Another get test",
"description" : "testing more",
"parameters" : [ {
"name" : "a",
"in" : "path",
"schema" : {
"$ref" : "#/components/schemas/String"
},
"required" : true,
"deprecated" : false
}, {
"name" : "aa",
"in" : "query",
"schema" : {
"$ref" : "#/components/schemas/Int"
},
"required" : true,
"deprecated" : false
} ],
"responses" : {
"200" : {
"description" : "A Successful Endeavor",
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/TestResponse"
}
}
}
},
"403" : {
"description" : "New API who dis?"
},
"400" : {
"description" : "Bad Things Happened",
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/ExceptionResponse"
}
}
}
}
},
"deprecated" : false
}
}
},
"components" : {
"schemas" : {
"String" : {
"type" : "string"
},
"ExceptionResponse" : {
"properties" : {
"message" : {
"$ref" : "#/components/schemas/String"
}
},
"type" : "object"
},
"TestResponse" : {
"properties" : {
"c" : {
"$ref" : "#/components/schemas/String"
}
},
"type" : "object"
},
"Int" : {
"format" : "int32",
"type" : "integer"
}
},
"securitySchemes" : { }
},
"security" : [ ],
"tags" : [ ]
}

View File

@ -6,9 +6,10 @@ import io.ktor.application.Application
import io.ktor.application.call
import io.ktor.application.install
import io.ktor.auth.Authentication
import io.ktor.auth.authenticate
import io.ktor.auth.UserIdPrincipal
import io.ktor.auth.authenticate
import io.ktor.features.ContentNegotiation
import io.ktor.features.StatusPages
import io.ktor.http.HttpStatusCode
import io.ktor.jackson.jackson
import io.ktor.response.respond
@ -20,14 +21,15 @@ import io.ktor.server.netty.Netty
import io.ktor.webjars.Webjars
import java.net.URI
import org.leafygreens.kompendium.Kompendium
import org.leafygreens.kompendium.Kompendium.notarizedDelete
import org.leafygreens.kompendium.Kompendium.notarizedGet
import org.leafygreens.kompendium.Kompendium.notarizedPost
import org.leafygreens.kompendium.Kompendium.notarizedPut
import org.leafygreens.kompendium.auth.KompendiumAuth.notarizedBasic
import org.leafygreens.kompendium.Notarized.notarizedDelete
import org.leafygreens.kompendium.Notarized.notarizedException
import org.leafygreens.kompendium.Notarized.notarizedGet
import org.leafygreens.kompendium.Notarized.notarizedPost
import org.leafygreens.kompendium.Notarized.notarizedPut
import org.leafygreens.kompendium.annotations.KompendiumField
import org.leafygreens.kompendium.annotations.PathParam
import org.leafygreens.kompendium.annotations.QueryParam
import org.leafygreens.kompendium.auth.KompendiumAuth.notarizedBasic
import org.leafygreens.kompendium.models.meta.MethodInfo
import org.leafygreens.kompendium.models.meta.RequestInfo
import org.leafygreens.kompendium.models.meta.ResponseInfo
@ -39,6 +41,7 @@ import org.leafygreens.kompendium.playground.KompendiumTOC.testAuthenticatedSing
import org.leafygreens.kompendium.playground.KompendiumTOC.testIdGetInfo
import org.leafygreens.kompendium.playground.KompendiumTOC.testSingleDeleteInfo
import org.leafygreens.kompendium.playground.KompendiumTOC.testSingleGetInfo
import org.leafygreens.kompendium.playground.KompendiumTOC.testSingleGetInfoWithThrowable
import org.leafygreens.kompendium.playground.KompendiumTOC.testSinglePostInfo
import org.leafygreens.kompendium.playground.KompendiumTOC.testSinglePutInfo
import org.leafygreens.kompendium.routes.openApi
@ -105,6 +108,16 @@ fun Application.mainModule() {
}
}
install(Webjars)
install(StatusPages) {
notarizedException<Exception, ExceptionResponse>(
info = ResponseInfo(
KompendiumHttpCodes.BAD_REQUEST,
"Bad Things Happened"
)
) {
call.respond(HttpStatusCode.BadRequest, ExceptionResponse("Why you do dis?"))
}
}
featuresInstalled = true
}
routing {
@ -139,6 +152,11 @@ fun Application.mainModule() {
}
}
}
route("/error") {
notarizedGet<Unit, ExampleResponse>(testSingleGetInfoWithThrowable) {
error("bad things just happened")
}
}
}
}
@ -163,6 +181,8 @@ data class ExampleRequest(
data class ExampleResponse(val c: String)
data class ExceptionResponse(val message: String)
data class ExampleCreatedResponse(val id: Int, val c: String)
object KompendiumTOC {
@ -184,6 +204,10 @@ object KompendiumTOC {
description = "Returns a different sample"
)
)
val testSingleGetInfoWithThrowable = testSingleGetInfo.copy(
summary = "Show me the error baby 🙏",
canThrow = setOf(Exception::class)
)
val testSinglePostInfo = MethodInfo(
summary = "Test post endpoint",
description = "Post your tests here!",