More extensible path calculation (#86)
This commit is contained in:
@ -1,5 +1,13 @@
|
|||||||
# Changelog
|
# 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
|
## [1.7.0] - August 14th, 2021
|
||||||
|
|
||||||
### Added
|
### 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
|
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.
|
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
|
## Examples
|
||||||
|
|
||||||
The full source code can be found in the `kompendium-playground` module. Here is a simple get endpoint example
|
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
|
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||||
|
|
||||||
plugins {
|
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("io.gitlab.arturbosch.detekt") version "1.17.0-RC3" apply false
|
||||||
id("com.adarshr.test-logger") version "3.0.0" 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
|
id("io.github.gradle-nexus.publish-plugin") version "1.1.0" apply true
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
# Kompendium
|
# Kompendium
|
||||||
project.version=1.7.0
|
project.version=1.8.0
|
||||||
# Kotlin
|
# Kotlin
|
||||||
kotlin.code.style=official
|
kotlin.code.style=official
|
||||||
# Gradle
|
# Gradle
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
[versions]
|
[versions]
|
||||||
kotlin = "1.4.32"
|
kotlin = "1.4.32"
|
||||||
ktor = "1.5.3"
|
ktor = "1.6.4"
|
||||||
kotlinx-serialization = "1.2.1"
|
kotlinx-serialization = "1.2.1"
|
||||||
jackson-kotlin = "2.12.0"
|
jackson-kotlin = "2.12.0"
|
||||||
slf4j = "1.7.30"
|
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.ktor.auth.jwt.JWTAuthenticationProvider
|
||||||
import io.bkbn.kompendium.Kompendium
|
import io.bkbn.kompendium.Kompendium
|
||||||
import io.bkbn.kompendium.models.oas.OpenApiSpecSchemaSecurity
|
import io.bkbn.kompendium.models.oas.OpenApiSpecSchemaSecurity
|
||||||
|
import io.ktor.auth.AuthenticationRouteSelector
|
||||||
|
|
||||||
object KompendiumAuth {
|
object KompendiumAuth {
|
||||||
|
|
||||||
init {
|
init {
|
||||||
Kompendium.pathCalculator = AuthPathCalculator()
|
Kompendium.addCustomRouteHandler(AuthenticationRouteSelector::class) { route, tail ->
|
||||||
|
calculate(route.parent, tail)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Authentication.Configuration.notarizedBasic(
|
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.OpenApiSpec
|
||||||
import io.bkbn.kompendium.models.oas.OpenApiSpecInfo
|
import io.bkbn.kompendium.models.oas.OpenApiSpecInfo
|
||||||
import io.bkbn.kompendium.models.oas.TypedSchema
|
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.bkbn.kompendium.path.PathCalculator
|
||||||
|
import io.ktor.routing.Route
|
||||||
|
import io.ktor.routing.RouteSelector
|
||||||
import kotlin.reflect.KClass
|
import kotlin.reflect.KClass
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -23,7 +25,7 @@ object Kompendium {
|
|||||||
paths = mutableMapOf()
|
paths = mutableMapOf()
|
||||||
)
|
)
|
||||||
|
|
||||||
var pathCalculator: PathCalculator = CorePathCalculator()
|
fun calculatePath(route: Route) = PathCalculator.calculate(route)
|
||||||
|
|
||||||
fun resetSchema() {
|
fun resetSchema() {
|
||||||
openApiSpec = OpenApiSpec(
|
openApiSpec = OpenApiSpec(
|
||||||
@ -37,4 +39,12 @@ object Kompendium {
|
|||||||
fun addCustomTypeSchema(clazz: KClass<*>, schema: TypedSchema) {
|
fun addCustomTypeSchema(clazz: KClass<*>, schema: TypedSchema) {
|
||||||
cache = cache.plus(clazz.simpleName!! to schema)
|
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>
|
noinline body: PipelineInterceptor<Unit, ApplicationCall>
|
||||||
): Route =
|
): Route =
|
||||||
KompendiumPreFlight.methodNotarizationPreFlight<TParam, Unit, TResp>() { paramType, requestType, responseType ->
|
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.getOrPut(path) { OpenApiSpecPathItem() }
|
||||||
Kompendium.openApiSpec.paths[path]?.get = parseMethodInfo(info, paramType, requestType, responseType)
|
Kompendium.openApiSpec.paths[path]?.get = parseMethodInfo(info, paramType, requestType, responseType)
|
||||||
return method(HttpMethod.Get) { handle(body) }
|
return method(HttpMethod.Get) { handle(body) }
|
||||||
@ -55,7 +55,7 @@ object Notarized {
|
|||||||
noinline body: PipelineInterceptor<Unit, ApplicationCall>
|
noinline body: PipelineInterceptor<Unit, ApplicationCall>
|
||||||
): Route =
|
): Route =
|
||||||
KompendiumPreFlight.methodNotarizationPreFlight<TParam, TReq, TResp>() { paramType, requestType, responseType ->
|
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.getOrPut(path) { OpenApiSpecPathItem() }
|
||||||
Kompendium.openApiSpec.paths[path]?.post = parseMethodInfo(info, paramType, requestType, responseType)
|
Kompendium.openApiSpec.paths[path]?.post = parseMethodInfo(info, paramType, requestType, responseType)
|
||||||
return method(HttpMethod.Post) { handle(body) }
|
return method(HttpMethod.Post) { handle(body) }
|
||||||
@ -74,7 +74,7 @@ object Notarized {
|
|||||||
noinline body: PipelineInterceptor<Unit, ApplicationCall>,
|
noinline body: PipelineInterceptor<Unit, ApplicationCall>,
|
||||||
): Route =
|
): Route =
|
||||||
KompendiumPreFlight.methodNotarizationPreFlight<TParam, TReq, TResp>() { paramType, requestType, responseType ->
|
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.getOrPut(path) { OpenApiSpecPathItem() }
|
||||||
Kompendium.openApiSpec.paths[path]?.put =
|
Kompendium.openApiSpec.paths[path]?.put =
|
||||||
parseMethodInfo(info, paramType, requestType, responseType)
|
parseMethodInfo(info, paramType, requestType, responseType)
|
||||||
@ -93,7 +93,7 @@ object Notarized {
|
|||||||
noinline body: PipelineInterceptor<Unit, ApplicationCall>
|
noinline body: PipelineInterceptor<Unit, ApplicationCall>
|
||||||
): Route =
|
): Route =
|
||||||
KompendiumPreFlight.methodNotarizationPreFlight<TParam, Unit, TResp> { paramType, requestType, responseType ->
|
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.getOrPut(path) { OpenApiSpecPathItem() }
|
||||||
Kompendium.openApiSpec.paths[path]?.delete = parseMethodInfo(info, paramType, requestType, responseType)
|
Kompendium.openApiSpec.paths[path]?.delete = parseMethodInfo(info, paramType, requestType, responseType)
|
||||||
return method(HttpMethod.Delete) { handle(body) }
|
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
|
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.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 {
|
||||||
|
|
||||||
/**
|
private val pathHandler: RouteHandlerMap = mutableMapOf()
|
||||||
* 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
|
|
||||||
|
|
||||||
|
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