More extensible path calculation (#86)
This commit is contained in:
@ -1,5 +1,13 @@
|
||||
# Changelog
|
||||
|
||||
## [1.8.0] - October 4th, 2021
|
||||
|
||||
### Changed
|
||||
|
||||
- Path calculation revamped to allow for simpler selector injection
|
||||
- Kotlin version bumped to 1.5.31
|
||||
- Ktor version bumped to 1.6.4
|
||||
|
||||
## [1.7.0] - August 14th, 2021
|
||||
|
||||
### Added
|
||||
|
27
README.md
27
README.md
@ -119,6 +119,33 @@ suggestions on better implementations are welcome 🤠
|
||||
Under the hood, Kompendium uses Jackson to serialize the final api spec. However, this implementation detail
|
||||
does not leak to the actual API, meaning that users are free to choose the serialization library of their choice.
|
||||
|
||||
### Route Handling
|
||||
|
||||
> ⚠️ Warning: Custom route handling is almost definitely an indication that a new Kompendium module needs to be added. If you have encountered a route type that is not handled, please consider opening an [issue](https://github.com/bkbnio/kompendium/issues/new)
|
||||
|
||||
Kompendium does its best to handle all Ktor routes out of the gate. However, in keeping with the modular approach of
|
||||
Ktor and Kompendium, this is not always possible.
|
||||
|
||||
Should you need to, custom route handlers can be registered via the
|
||||
`Kompendium.addCustomRouteHandler` function.
|
||||
|
||||
The method declaration is a bit gross, so lets dig in to what is happening.
|
||||
|
||||
```kotlin
|
||||
fun <T : RouteSelector> addCustomRouteHandler(
|
||||
selector: KClass<T>,
|
||||
handler: PathCalculator.(Route, String) -> String
|
||||
)
|
||||
```
|
||||
|
||||
This function takes a selector, which _must_ be an implementation of the Ktor `RouteSelector`. The handler is a function
|
||||
that extends the Kompendium `PathCalculator`. This is necessary because it gives you access to `PathCalculator.calculate`,
|
||||
which you are going to want in order to calculate the rest of the route :)
|
||||
|
||||
Its parameters are the `Route` itself, along with the "tail" of the Path (the path that has been calculated thus far).
|
||||
|
||||
Working examples `init` blocks of the `PathCalculator` and `KompendiumAuth` object.
|
||||
|
||||
## Examples
|
||||
|
||||
The full source code can be found in the `kompendium-playground` module. Here is a simple get endpoint example
|
||||
|
@ -4,7 +4,7 @@ import io.gitlab.arturbosch.detekt.extensions.DetektExtension
|
||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||
|
||||
plugins {
|
||||
id("org.jetbrains.kotlin.jvm") version "1.5.0" apply false
|
||||
id("org.jetbrains.kotlin.jvm") version "1.5.31" apply false
|
||||
id("io.gitlab.arturbosch.detekt") version "1.17.0-RC3" apply false
|
||||
id("com.adarshr.test-logger") version "3.0.0" apply false
|
||||
id("io.github.gradle-nexus.publish-plugin") version "1.1.0" apply true
|
||||
|
@ -1,5 +1,5 @@
|
||||
# Kompendium
|
||||
project.version=1.7.0
|
||||
project.version=1.8.0
|
||||
# Kotlin
|
||||
kotlin.code.style=official
|
||||
# Gradle
|
||||
|
@ -1,6 +1,6 @@
|
||||
[versions]
|
||||
kotlin = "1.4.32"
|
||||
ktor = "1.5.3"
|
||||
ktor = "1.6.4"
|
||||
kotlinx-serialization = "1.2.1"
|
||||
jackson-kotlin = "2.12.0"
|
||||
slf4j = "1.7.30"
|
||||
|
@ -1,19 +0,0 @@
|
||||
package io.bkbn.kompendium.auth
|
||||
|
||||
import io.ktor.auth.AuthenticationRouteSelector
|
||||
import io.ktor.routing.Route
|
||||
import io.bkbn.kompendium.path.CorePathCalculator
|
||||
import org.slf4j.LoggerFactory
|
||||
|
||||
class AuthPathCalculator : CorePathCalculator() {
|
||||
|
||||
private val logger = LoggerFactory.getLogger(javaClass)
|
||||
|
||||
override fun handleCustomSelectors(route: Route, tail: String): String = when (route.selector) {
|
||||
is AuthenticationRouteSelector -> {
|
||||
logger.debug("Found authentication route selector ${route.selector}")
|
||||
super.calculate(route.parent, tail)
|
||||
}
|
||||
else -> super.handleCustomSelectors(route, tail)
|
||||
}
|
||||
}
|
@ -7,11 +7,14 @@ import io.ktor.auth.jwt.jwt
|
||||
import io.ktor.auth.jwt.JWTAuthenticationProvider
|
||||
import io.bkbn.kompendium.Kompendium
|
||||
import io.bkbn.kompendium.models.oas.OpenApiSpecSchemaSecurity
|
||||
import io.ktor.auth.AuthenticationRouteSelector
|
||||
|
||||
object KompendiumAuth {
|
||||
|
||||
init {
|
||||
Kompendium.pathCalculator = AuthPathCalculator()
|
||||
Kompendium.addCustomRouteHandler(AuthenticationRouteSelector::class) { route, tail ->
|
||||
calculate(route.parent, tail)
|
||||
}
|
||||
}
|
||||
|
||||
fun Authentication.Configuration.notarizedBasic(
|
||||
|
@ -5,8 +5,10 @@ import io.bkbn.kompendium.models.meta.SchemaMap
|
||||
import io.bkbn.kompendium.models.oas.OpenApiSpec
|
||||
import io.bkbn.kompendium.models.oas.OpenApiSpecInfo
|
||||
import io.bkbn.kompendium.models.oas.TypedSchema
|
||||
import io.bkbn.kompendium.path.CorePathCalculator
|
||||
import io.bkbn.kompendium.path.IPathCalculator
|
||||
import io.bkbn.kompendium.path.PathCalculator
|
||||
import io.ktor.routing.Route
|
||||
import io.ktor.routing.RouteSelector
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
/**
|
||||
@ -23,7 +25,7 @@ object Kompendium {
|
||||
paths = mutableMapOf()
|
||||
)
|
||||
|
||||
var pathCalculator: PathCalculator = CorePathCalculator()
|
||||
fun calculatePath(route: Route) = PathCalculator.calculate(route)
|
||||
|
||||
fun resetSchema() {
|
||||
openApiSpec = OpenApiSpec(
|
||||
@ -37,4 +39,12 @@ object Kompendium {
|
||||
fun addCustomTypeSchema(clazz: KClass<*>, schema: TypedSchema) {
|
||||
cache = cache.plus(clazz.simpleName!! to schema)
|
||||
}
|
||||
|
||||
fun <T : RouteSelector> addCustomRouteHandler(
|
||||
selector: KClass<T>,
|
||||
handler: IPathCalculator.(Route, String) -> String
|
||||
) {
|
||||
PathCalculator.addCustomRouteHandler(selector, handler)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -36,7 +36,7 @@ object Notarized {
|
||||
noinline body: PipelineInterceptor<Unit, ApplicationCall>
|
||||
): Route =
|
||||
KompendiumPreFlight.methodNotarizationPreFlight<TParam, Unit, TResp>() { paramType, requestType, responseType ->
|
||||
val path = Kompendium.pathCalculator.calculate(this)
|
||||
val path = Kompendium.calculatePath(this)
|
||||
Kompendium.openApiSpec.paths.getOrPut(path) { OpenApiSpecPathItem() }
|
||||
Kompendium.openApiSpec.paths[path]?.get = parseMethodInfo(info, paramType, requestType, responseType)
|
||||
return method(HttpMethod.Get) { handle(body) }
|
||||
@ -55,7 +55,7 @@ object Notarized {
|
||||
noinline body: PipelineInterceptor<Unit, ApplicationCall>
|
||||
): Route =
|
||||
KompendiumPreFlight.methodNotarizationPreFlight<TParam, TReq, TResp>() { paramType, requestType, responseType ->
|
||||
val path = Kompendium.pathCalculator.calculate(this)
|
||||
val path = Kompendium.calculatePath(this)
|
||||
Kompendium.openApiSpec.paths.getOrPut(path) { OpenApiSpecPathItem() }
|
||||
Kompendium.openApiSpec.paths[path]?.post = parseMethodInfo(info, paramType, requestType, responseType)
|
||||
return method(HttpMethod.Post) { handle(body) }
|
||||
@ -74,7 +74,7 @@ object Notarized {
|
||||
noinline body: PipelineInterceptor<Unit, ApplicationCall>,
|
||||
): Route =
|
||||
KompendiumPreFlight.methodNotarizationPreFlight<TParam, TReq, TResp>() { paramType, requestType, responseType ->
|
||||
val path = Kompendium.pathCalculator.calculate(this)
|
||||
val path = Kompendium.calculatePath(this)
|
||||
Kompendium.openApiSpec.paths.getOrPut(path) { OpenApiSpecPathItem() }
|
||||
Kompendium.openApiSpec.paths[path]?.put =
|
||||
parseMethodInfo(info, paramType, requestType, responseType)
|
||||
@ -93,7 +93,7 @@ object Notarized {
|
||||
noinline body: PipelineInterceptor<Unit, ApplicationCall>
|
||||
): Route =
|
||||
KompendiumPreFlight.methodNotarizationPreFlight<TParam, Unit, TResp> { paramType, requestType, responseType ->
|
||||
val path = Kompendium.pathCalculator.calculate(this)
|
||||
val path = Kompendium.calculatePath(this)
|
||||
Kompendium.openApiSpec.paths.getOrPut(path) { OpenApiSpecPathItem() }
|
||||
Kompendium.openApiSpec.paths[path]?.delete = parseMethodInfo(info, paramType, requestType, responseType)
|
||||
return method(HttpMethod.Delete) { handle(body) }
|
||||
|
@ -1,51 +0,0 @@
|
||||
package io.bkbn.kompendium.path
|
||||
|
||||
import io.ktor.routing.PathSegmentConstantRouteSelector
|
||||
import io.ktor.routing.PathSegmentParameterRouteSelector
|
||||
import io.ktor.routing.RootRouteSelector
|
||||
import io.ktor.routing.Route
|
||||
import io.ktor.util.InternalAPI
|
||||
import org.slf4j.LoggerFactory
|
||||
|
||||
/**
|
||||
* Default [PathCalculator] meant to be overridden as necessary
|
||||
*/
|
||||
open class CorePathCalculator : PathCalculator {
|
||||
|
||||
private val logger = LoggerFactory.getLogger(javaClass)
|
||||
|
||||
@OptIn(InternalAPI::class)
|
||||
override fun calculate(
|
||||
route: Route?,
|
||||
tail: String
|
||||
): String = when (route) {
|
||||
null -> tail
|
||||
else -> when (route.selector) {
|
||||
is RootRouteSelector -> {
|
||||
logger.debug("Root route detected, returning path: $tail")
|
||||
tail
|
||||
}
|
||||
is PathSegmentParameterRouteSelector -> {
|
||||
logger.debug("Found segment parameter ${route.selector}, continuing to parent")
|
||||
val newTail = "/${route.selector}$tail"
|
||||
calculate(route.parent, newTail)
|
||||
}
|
||||
is PathSegmentConstantRouteSelector -> {
|
||||
logger.debug("Found segment constant ${route.selector}, continuing to parent")
|
||||
val newTail = "/${route.selector}$tail"
|
||||
calculate(route.parent, newTail)
|
||||
}
|
||||
else -> when (route.selector.javaClass.simpleName) {
|
||||
"TrailingSlashRouteSelector" -> {
|
||||
logger.debug("Found trailing slash route selector")
|
||||
val newTail = tail.ifBlank { "/" }
|
||||
calculate(route.parent, newTail)
|
||||
}
|
||||
else -> handleCustomSelectors(route, tail)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun handleCustomSelectors(route: Route, tail: String): String = error("Unknown selector ${route.selector}")
|
||||
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
package io.bkbn.kompendium.path
|
||||
|
||||
import io.ktor.routing.Route
|
||||
import io.ktor.routing.RouteSelector
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
interface IPathCalculator {
|
||||
fun calculate(route: Route?, tail: String = ""): String
|
||||
fun <T : RouteSelector> addCustomRouteHandler(
|
||||
selector: KClass<T>,
|
||||
handler: IPathCalculator.(Route, String) -> String
|
||||
)
|
||||
}
|
@ -1,20 +1,54 @@
|
||||
package io.bkbn.kompendium.path
|
||||
|
||||
import io.ktor.routing.PathSegmentConstantRouteSelector
|
||||
import io.ktor.routing.PathSegmentParameterRouteSelector
|
||||
import io.ktor.routing.RootRouteSelector
|
||||
import io.ktor.routing.Route
|
||||
import io.ktor.routing.RouteSelector
|
||||
import io.ktor.routing.TrailingSlashRouteSelector
|
||||
import io.ktor.util.InternalAPI
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
/**
|
||||
* Extensible interface for calculating Ktor paths
|
||||
* Responsible for calculating a url path from a provided [Route]
|
||||
*/
|
||||
interface PathCalculator {
|
||||
@OptIn(InternalAPI::class)
|
||||
internal object PathCalculator: IPathCalculator {
|
||||
|
||||
/**
|
||||
* Core route calculation method
|
||||
*/
|
||||
fun calculate(route: Route?, tail: String = ""): String
|
||||
|
||||
/**
|
||||
* Used to handle any custom selectors that may be missed by the base route calculation
|
||||
*/
|
||||
fun handleCustomSelectors(route: Route, tail: String): String
|
||||
private val pathHandler: RouteHandlerMap = mutableMapOf()
|
||||
|
||||
init {
|
||||
addCustomRouteHandler(RootRouteSelector::class) { _, tail -> tail }
|
||||
addCustomRouteHandler(PathSegmentParameterRouteSelector::class) { route, tail ->
|
||||
val newTail = "/${route.selector}$tail"
|
||||
calculate(route.parent, newTail)
|
||||
}
|
||||
addCustomRouteHandler(PathSegmentConstantRouteSelector::class) { route, tail ->
|
||||
val newTail = "/${route.selector}$tail"
|
||||
calculate(route.parent, newTail)
|
||||
}
|
||||
addCustomRouteHandler(TrailingSlashRouteSelector::class) { route, tail ->
|
||||
val newTail = tail.ifBlank { "/" }
|
||||
calculate(route.parent, newTail)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(InternalAPI::class)
|
||||
override fun calculate(
|
||||
route: Route?,
|
||||
tail: String
|
||||
): String = when (route) {
|
||||
null -> tail
|
||||
else -> when (pathHandler.containsKey(route.selector::class)) {
|
||||
true -> pathHandler[route.selector::class]!!.invoke(this, route, tail)
|
||||
else -> error("No handler has been registered for ${route.selector}")
|
||||
}
|
||||
}
|
||||
|
||||
override fun <T : RouteSelector> addCustomRouteHandler(
|
||||
selector: KClass<T>,
|
||||
handler: IPathCalculator.(Route, String) -> String
|
||||
) {
|
||||
pathHandler[selector] = handler
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,7 @@
|
||||
package io.bkbn.kompendium.path
|
||||
|
||||
import io.ktor.routing.Route
|
||||
import io.ktor.routing.RouteSelector
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
typealias RouteHandlerMap = MutableMap<KClass<out RouteSelector>, IPathCalculator.(Route, String) -> String>
|
Reference in New Issue
Block a user