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
|
# 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
|
## [0.6.2] - April 23rd, 2021
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
41
README.md
41
README.md
@ -40,12 +40,13 @@ dependencies {
|
|||||||
### Warning 🚨
|
### Warning 🚨
|
||||||
Kompendium is still under active development ⚠️ There are a number of yet-to-be-implemented features, including
|
Kompendium is still under active development ⚠️ There are a number of yet-to-be-implemented features, including
|
||||||
|
|
||||||
- Multiple Responses 📜
|
|
||||||
- Sealed Class / Polymorphic Support 😬
|
- Sealed Class / Polymorphic Support 😬
|
||||||
- Validation / Enforcement (❓👀❓)
|
- Validation / Enforcement (❓👀❓)
|
||||||
|
|
||||||
If you have a feature that is not listed here, please open an issue!
|
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
|
### Notarized Routes
|
||||||
|
|
||||||
Kompendium introduces the concept of `notarized` HTTP methods. That is, for all your `GET`, `POST`, `PUT`, and `DELETE`
|
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.
|
`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,
|
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.
|
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)
|
setSerializationInclusion(JsonInclude.Include.NON_NULL)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
install(StatusPages) {
|
||||||
|
notarizedException<Exception, ExceptionResponse>(exceptionResponseInfo) {
|
||||||
|
call.respond(HttpStatusCode.BadRequest, ExceptionResponse("Why you do dis?"))
|
||||||
|
}
|
||||||
|
}
|
||||||
routing {
|
routing {
|
||||||
openApi()
|
openApi()
|
||||||
redoc()
|
redoc()
|
||||||
@ -125,9 +139,19 @@ fun Application.mainModule() {
|
|||||||
call.respondText { "heya" }
|
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`
|
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
|
### 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.
|
This needs to be added to your project as dependency.
|
||||||
|
|
||||||
At the moment, the basic and jwt authentication is only supported.
|
At the moment, the basic and jwt authentication is only supported.
|
||||||
@ -188,11 +212,22 @@ Minimal Example:
|
|||||||
```kotlin
|
```kotlin
|
||||||
install(Webjars)
|
install(Webjars)
|
||||||
routing {
|
routing {
|
||||||
openApi()
|
openApi(oas)
|
||||||
swaggerUI()
|
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
|
## Limitations
|
||||||
|
|
||||||
### Kompendium as a singleton
|
### Kompendium as a singleton
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id("org.jetbrains.kotlin.jvm") version "1.4.32" apply false
|
id("org.jetbrains.kotlin.jvm") version "1.4.32" apply false
|
||||||
id("io.gitlab.arturbosch.detekt") version "1.16.0-RC2" 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 {
|
allprojects {
|
||||||
@ -21,6 +22,7 @@ allprojects {
|
|||||||
|
|
||||||
apply(plugin = "org.jetbrains.kotlin.jvm")
|
apply(plugin = "org.jetbrains.kotlin.jvm")
|
||||||
apply(plugin = "io.gitlab.arturbosch.detekt")
|
apply(plugin = "io.gitlab.arturbosch.detekt")
|
||||||
|
apply(plugin = "com.adarshr.test-logger")
|
||||||
apply(plugin = "idea")
|
apply(plugin = "idea")
|
||||||
|
|
||||||
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
|
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> {
|
configure<io.gitlab.arturbosch.detekt.extensions.DetektExtension> {
|
||||||
toolVersion = "1.16.0-RC2"
|
toolVersion = "1.16.0-RC2"
|
||||||
config = files("${rootProject.projectDir}/detekt.yml")
|
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" }
|
logback-core = { group = "ch.qos.logback", name = "logback-core", version.ref = "logback" }
|
||||||
|
|
||||||
# webjars
|
# 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]
|
[bundles]
|
||||||
ktor = [ "ktor-server-core", "ktor-server-netty", "ktor-jackson", "ktor-html-builder" ]
|
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 io.ktor.server.testing.*
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.leafygreens.kompendium.Kompendium
|
import org.leafygreens.kompendium.Kompendium
|
||||||
import org.leafygreens.kompendium.Kompendium.notarizedGet
|
|
||||||
import org.leafygreens.kompendium.auth.KompendiumAuth.notarizedBasic
|
import org.leafygreens.kompendium.auth.KompendiumAuth.notarizedBasic
|
||||||
import org.leafygreens.kompendium.auth.KompendiumAuth.notarizedJwt
|
import org.leafygreens.kompendium.auth.KompendiumAuth.notarizedJwt
|
||||||
import org.leafygreens.kompendium.auth.util.TestData
|
import org.leafygreens.kompendium.auth.util.TestData
|
||||||
@ -27,6 +26,7 @@ import org.leafygreens.kompendium.routes.redoc
|
|||||||
import org.leafygreens.kompendium.util.KompendiumHttpCodes
|
import org.leafygreens.kompendium.util.KompendiumHttpCodes
|
||||||
import kotlin.test.AfterTest
|
import kotlin.test.AfterTest
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
|
import org.leafygreens.kompendium.Notarized.notarizedGet
|
||||||
|
|
||||||
internal class KompendiumAuthTest {
|
internal class KompendiumAuthTest {
|
||||||
|
|
||||||
|
@ -1,22 +1,17 @@
|
|||||||
package org.leafygreens.kompendium
|
package org.leafygreens.kompendium
|
||||||
|
|
||||||
import io.ktor.application.ApplicationCall
|
|
||||||
import io.ktor.http.HttpMethod
|
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.KClass
|
||||||
import kotlin.reflect.KType
|
import kotlin.reflect.KType
|
||||||
|
import kotlin.reflect.full.createType
|
||||||
import kotlin.reflect.full.findAnnotation
|
import kotlin.reflect.full.findAnnotation
|
||||||
import kotlin.reflect.full.memberProperties
|
import kotlin.reflect.full.memberProperties
|
||||||
import kotlin.reflect.jvm.javaField
|
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.CookieParam
|
||||||
import org.leafygreens.kompendium.annotations.HeaderParam
|
import org.leafygreens.kompendium.annotations.HeaderParam
|
||||||
import org.leafygreens.kompendium.annotations.PathParam
|
import org.leafygreens.kompendium.annotations.PathParam
|
||||||
import org.leafygreens.kompendium.annotations.QueryParam
|
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.MethodInfo
|
||||||
import org.leafygreens.kompendium.models.meta.RequestInfo
|
import org.leafygreens.kompendium.models.meta.RequestInfo
|
||||||
import org.leafygreens.kompendium.models.meta.ResponseInfo
|
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.OpenApiSpecInfo
|
||||||
import org.leafygreens.kompendium.models.oas.OpenApiSpecMediaType
|
import org.leafygreens.kompendium.models.oas.OpenApiSpecMediaType
|
||||||
import org.leafygreens.kompendium.models.oas.OpenApiSpecParameter
|
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.OpenApiSpecPathItemOperation
|
||||||
|
import org.leafygreens.kompendium.models.oas.OpenApiSpecReferencable
|
||||||
import org.leafygreens.kompendium.models.oas.OpenApiSpecReferenceObject
|
import org.leafygreens.kompendium.models.oas.OpenApiSpecReferenceObject
|
||||||
import org.leafygreens.kompendium.models.oas.OpenApiSpecRequest
|
import org.leafygreens.kompendium.models.oas.OpenApiSpecRequest
|
||||||
import org.leafygreens.kompendium.models.oas.OpenApiSpecResponse
|
import org.leafygreens.kompendium.models.oas.OpenApiSpecResponse
|
||||||
@ -38,6 +33,7 @@ import org.leafygreens.kompendium.util.Helpers.getReferenceSlug
|
|||||||
|
|
||||||
object Kompendium {
|
object Kompendium {
|
||||||
|
|
||||||
|
var errorMap: ErrorMap = emptyMap()
|
||||||
var cache: SchemaMap = emptyMap()
|
var cache: SchemaMap = emptyMap()
|
||||||
|
|
||||||
var openApiSpec = OpenApiSpec(
|
var openApiSpec = OpenApiSpec(
|
||||||
@ -48,47 +44,6 @@ object Kompendium {
|
|||||||
|
|
||||||
var pathCalculator: PathCalculator = CorePathCalculator()
|
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
|
// TODO here down is a mess, needs refactor once core functionality is in place
|
||||||
fun MethodInfo.parseMethodInfo(
|
fun MethodInfo.parseMethodInfo(
|
||||||
method: HttpMethod,
|
method: HttpMethod,
|
||||||
@ -101,7 +56,18 @@ object Kompendium {
|
|||||||
tags = this.tags,
|
tags = this.tags,
|
||||||
deprecated = this.deprecated,
|
deprecated = this.deprecated,
|
||||||
parameters = paramType.toParameterSpec(),
|
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,
|
requestBody = if (method != HttpMethod.Get) requestType.toRequestSpec(requestInfo) else null,
|
||||||
security = if (this.securitySchemes.isNotEmpty()) listOf(
|
security = if (this.securitySchemes.isNotEmpty()) listOf(
|
||||||
// TODO support scopes
|
// TODO support scopes
|
||||||
@ -109,18 +75,15 @@ object Kompendium {
|
|||||||
) else null
|
) else null
|
||||||
)
|
)
|
||||||
|
|
||||||
@OptIn(ExperimentalStdlibApi::class)
|
private fun parseThrowables(throwables: Set<KClass<*>>): Map<Int, OpenApiSpecReferencable> = throwables.mapNotNull {
|
||||||
inline fun <reified TParam : Any, reified TReq : Any, reified TResp : Any> notarizationPreFlight(
|
errorMap[it.createType()]
|
||||||
block: (KType, KType, KType) -> Route
|
}.toMap()
|
||||||
): Route {
|
|
||||||
cache = generateKontent<TResp>(cache)
|
fun ResponseInfo.parseErrorInfo(
|
||||||
cache = generateKontent<TReq>(cache)
|
errorType: KType,
|
||||||
cache = generateParameterKontent<TParam>(cache)
|
responseType: KType
|
||||||
openApiSpec.components.schemas.putAll(cache)
|
) {
|
||||||
val requestType = typeOf<TReq>()
|
errorMap = errorMap.plus(errorType to responseType.toResponseSpec(this))
|
||||||
val responseType = typeOf<TResp>()
|
|
||||||
val paramType = typeOf<TParam>()
|
|
||||||
return block.invoke(paramType, requestType, responseType)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO These two lookin' real similar 👀 Combine?
|
// 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
|
package org.leafygreens.kompendium.models.meta
|
||||||
|
|
||||||
|
import kotlin.reflect.KClass
|
||||||
|
|
||||||
// TODO Seal and extend by method type?
|
// TODO Seal and extend by method type?
|
||||||
data class MethodInfo(
|
data class MethodInfo(
|
||||||
val summary: String,
|
val summary: String,
|
||||||
@ -8,5 +10,6 @@ data class MethodInfo(
|
|||||||
val requestInfo: RequestInfo? = null,
|
val requestInfo: RequestInfo? = null,
|
||||||
val tags: Set<String> = emptySet(),
|
val tags: Set<String> = emptySet(),
|
||||||
val deprecated: Boolean = false,
|
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
|
package org.leafygreens.kompendium.models.meta
|
||||||
|
|
||||||
data class ResponseInfo(
|
data class ResponseInfo(
|
||||||
val status: Int, // TODO How to handle error codes?
|
val status: Int,
|
||||||
val description: String,
|
val description: String,
|
||||||
val mediaTypes: List<String> = listOf("application/json")
|
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.call
|
||||||
import io.ktor.application.install
|
import io.ktor.application.install
|
||||||
import io.ktor.features.ContentNegotiation
|
import io.ktor.features.ContentNegotiation
|
||||||
|
import io.ktor.features.StatusPages
|
||||||
import io.ktor.http.HttpMethod
|
import io.ktor.http.HttpMethod
|
||||||
import io.ktor.http.HttpStatusCode
|
import io.ktor.http.HttpStatusCode
|
||||||
import io.ktor.jackson.jackson
|
import io.ktor.jackson.jackson
|
||||||
@ -19,10 +20,11 @@ import java.net.URI
|
|||||||
import kotlin.test.AfterTest
|
import kotlin.test.AfterTest
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
import org.leafygreens.kompendium.Kompendium.notarizedDelete
|
import org.leafygreens.kompendium.Notarized.notarizedDelete
|
||||||
import org.leafygreens.kompendium.Kompendium.notarizedGet
|
import org.leafygreens.kompendium.Notarized.notarizedException
|
||||||
import org.leafygreens.kompendium.Kompendium.notarizedPost
|
import org.leafygreens.kompendium.Notarized.notarizedGet
|
||||||
import org.leafygreens.kompendium.Kompendium.notarizedPut
|
import org.leafygreens.kompendium.Notarized.notarizedPost
|
||||||
|
import org.leafygreens.kompendium.Notarized.notarizedPut
|
||||||
import org.leafygreens.kompendium.annotations.QueryParam
|
import org.leafygreens.kompendium.annotations.QueryParam
|
||||||
import org.leafygreens.kompendium.models.meta.MethodInfo
|
import org.leafygreens.kompendium.models.meta.MethodInfo
|
||||||
import org.leafygreens.kompendium.models.meta.RequestInfo
|
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.openApi
|
||||||
import org.leafygreens.kompendium.routes.redoc
|
import org.leafygreens.kompendium.routes.redoc
|
||||||
import org.leafygreens.kompendium.util.ComplexRequest
|
import org.leafygreens.kompendium.util.ComplexRequest
|
||||||
|
import org.leafygreens.kompendium.util.ExceptionResponse
|
||||||
import org.leafygreens.kompendium.util.KompendiumHttpCodes
|
import org.leafygreens.kompendium.util.KompendiumHttpCodes
|
||||||
import org.leafygreens.kompendium.util.TestCreatedResponse
|
import org.leafygreens.kompendium.util.TestCreatedResponse
|
||||||
import org.leafygreens.kompendium.util.TestData
|
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 {
|
private companion object {
|
||||||
val testGetResponse = ResponseInfo(KompendiumHttpCodes.OK, "A Successful Endeavor")
|
val testGetResponse = ResponseInfo(KompendiumHttpCodes.OK, "A Successful Endeavor")
|
||||||
val testPostResponse = ResponseInfo(KompendiumHttpCodes.CREATED, "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())
|
ResponseInfo(KompendiumHttpCodes.NO_CONTENT, "A Successful Endeavor", mediaTypes = emptyList())
|
||||||
val testRequest = RequestInfo("A Test request")
|
val testRequest = RequestInfo("A Test request")
|
||||||
val testGetInfo = MethodInfo("Another get test", "testing more", testGetResponse)
|
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 testPostInfo = MethodInfo("Test post endpoint", "Post your tests here!", testPostResponse, testRequest)
|
||||||
val testPutInfo = MethodInfo("Test put endpoint", "Put 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 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() {
|
private fun Application.notarizedGetModule() {
|
||||||
routing {
|
routing {
|
||||||
route("/test") {
|
route("/test") {
|
||||||
|
@ -71,3 +71,5 @@ sealed class TestSealedClass(open val a: String)
|
|||||||
data class SimpleTSC(val b: Int) : TestSealedClass("hey")
|
data class SimpleTSC(val b: Int) : TestSealedClass("hey")
|
||||||
open class MediumTSC(override val a: String, val b: Int) : TestSealedClass(a)
|
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 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.call
|
||||||
import io.ktor.application.install
|
import io.ktor.application.install
|
||||||
import io.ktor.auth.Authentication
|
import io.ktor.auth.Authentication
|
||||||
import io.ktor.auth.authenticate
|
|
||||||
import io.ktor.auth.UserIdPrincipal
|
import io.ktor.auth.UserIdPrincipal
|
||||||
|
import io.ktor.auth.authenticate
|
||||||
import io.ktor.features.ContentNegotiation
|
import io.ktor.features.ContentNegotiation
|
||||||
|
import io.ktor.features.StatusPages
|
||||||
import io.ktor.http.HttpStatusCode
|
import io.ktor.http.HttpStatusCode
|
||||||
import io.ktor.jackson.jackson
|
import io.ktor.jackson.jackson
|
||||||
import io.ktor.response.respond
|
import io.ktor.response.respond
|
||||||
@ -20,14 +21,15 @@ import io.ktor.server.netty.Netty
|
|||||||
import io.ktor.webjars.Webjars
|
import io.ktor.webjars.Webjars
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
import org.leafygreens.kompendium.Kompendium
|
import org.leafygreens.kompendium.Kompendium
|
||||||
import org.leafygreens.kompendium.Kompendium.notarizedDelete
|
import org.leafygreens.kompendium.Notarized.notarizedDelete
|
||||||
import org.leafygreens.kompendium.Kompendium.notarizedGet
|
import org.leafygreens.kompendium.Notarized.notarizedException
|
||||||
import org.leafygreens.kompendium.Kompendium.notarizedPost
|
import org.leafygreens.kompendium.Notarized.notarizedGet
|
||||||
import org.leafygreens.kompendium.Kompendium.notarizedPut
|
import org.leafygreens.kompendium.Notarized.notarizedPost
|
||||||
import org.leafygreens.kompendium.auth.KompendiumAuth.notarizedBasic
|
import org.leafygreens.kompendium.Notarized.notarizedPut
|
||||||
import org.leafygreens.kompendium.annotations.KompendiumField
|
import org.leafygreens.kompendium.annotations.KompendiumField
|
||||||
import org.leafygreens.kompendium.annotations.PathParam
|
import org.leafygreens.kompendium.annotations.PathParam
|
||||||
import org.leafygreens.kompendium.annotations.QueryParam
|
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.MethodInfo
|
||||||
import org.leafygreens.kompendium.models.meta.RequestInfo
|
import org.leafygreens.kompendium.models.meta.RequestInfo
|
||||||
import org.leafygreens.kompendium.models.meta.ResponseInfo
|
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.testIdGetInfo
|
||||||
import org.leafygreens.kompendium.playground.KompendiumTOC.testSingleDeleteInfo
|
import org.leafygreens.kompendium.playground.KompendiumTOC.testSingleDeleteInfo
|
||||||
import org.leafygreens.kompendium.playground.KompendiumTOC.testSingleGetInfo
|
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.testSinglePostInfo
|
||||||
import org.leafygreens.kompendium.playground.KompendiumTOC.testSinglePutInfo
|
import org.leafygreens.kompendium.playground.KompendiumTOC.testSinglePutInfo
|
||||||
import org.leafygreens.kompendium.routes.openApi
|
import org.leafygreens.kompendium.routes.openApi
|
||||||
@ -105,6 +108,16 @@ fun Application.mainModule() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
install(Webjars)
|
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
|
featuresInstalled = true
|
||||||
}
|
}
|
||||||
routing {
|
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 ExampleResponse(val c: String)
|
||||||
|
|
||||||
|
data class ExceptionResponse(val message: String)
|
||||||
|
|
||||||
data class ExampleCreatedResponse(val id: Int, val c: String)
|
data class ExampleCreatedResponse(val id: Int, val c: String)
|
||||||
|
|
||||||
object KompendiumTOC {
|
object KompendiumTOC {
|
||||||
@ -184,6 +204,10 @@ object KompendiumTOC {
|
|||||||
description = "Returns a different sample"
|
description = "Returns a different sample"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
val testSingleGetInfoWithThrowable = testSingleGetInfo.copy(
|
||||||
|
summary = "Show me the error baby 🙏",
|
||||||
|
canThrow = setOf(Exception::class)
|
||||||
|
)
|
||||||
val testSinglePostInfo = MethodInfo(
|
val testSinglePostInfo = MethodInfo(
|
||||||
summary = "Test post endpoint",
|
summary = "Test post endpoint",
|
||||||
description = "Post your tests here!",
|
description = "Post your tests here!",
|
||||||
|
Reference in New Issue
Block a user