Error Responses, PR Template, Code Refactor, and Test plugin update (#42)
This commit is contained in:
30
.github/pull_request_template.md
vendored
Normal file
30
.github/pull_request_template.md
vendored
Normal 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
|
11
CHANGELOG.md
11
CHANGELOG.md
@ -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
|
||||
|
41
README.md
41
README.md
@ -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
|
||||
|
@ -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")
|
||||
|
@ -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" ]
|
||||
|
@ -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 {
|
||||
|
||||
|
@ -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?
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
@ -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>?>
|
@ -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()
|
||||
)
|
||||
|
@ -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")
|
||||
)
|
||||
|
@ -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") {
|
||||
|
@ -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)
|
||||
|
@ -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" : [ ]
|
||||
}
|
@ -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" : [ ]
|
||||
}
|
@ -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!",
|
||||
|
Reference in New Issue
Block a user