diff --git a/CHANGELOG.md b/CHANGELOG.md index 34394a741..792983086 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index 20329c3ee..2bcc72e1b 100644 --- a/README.md +++ b/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 addCustomRouteHandler( + selector: KClass, + 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 diff --git a/build.gradle.kts b/build.gradle.kts index d4fb5e6e9..fbc80dfe9 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -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 diff --git a/gradle.properties b/gradle.properties index f62ad89b3..74c3144eb 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ # Kompendium -project.version=1.7.0 +project.version=1.8.0 # Kotlin kotlin.code.style=official # Gradle diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 155eaca54..1516589a9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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" diff --git a/kompendium-auth/src/main/kotlin/io/bkbn/kompendium/auth/AuthPathCalculator.kt b/kompendium-auth/src/main/kotlin/io/bkbn/kompendium/auth/AuthPathCalculator.kt deleted file mode 100644 index 16a149ce9..000000000 --- a/kompendium-auth/src/main/kotlin/io/bkbn/kompendium/auth/AuthPathCalculator.kt +++ /dev/null @@ -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) - } -} diff --git a/kompendium-auth/src/main/kotlin/io/bkbn/kompendium/auth/KompendiumAuth.kt b/kompendium-auth/src/main/kotlin/io/bkbn/kompendium/auth/KompendiumAuth.kt index 6779a4c0d..1b041384c 100644 --- a/kompendium-auth/src/main/kotlin/io/bkbn/kompendium/auth/KompendiumAuth.kt +++ b/kompendium-auth/src/main/kotlin/io/bkbn/kompendium/auth/KompendiumAuth.kt @@ -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( diff --git a/kompendium-core/src/main/kotlin/io/bkbn/kompendium/Kompendium.kt b/kompendium-core/src/main/kotlin/io/bkbn/kompendium/Kompendium.kt index ab7e8fb90..101585a1c 100644 --- a/kompendium-core/src/main/kotlin/io/bkbn/kompendium/Kompendium.kt +++ b/kompendium-core/src/main/kotlin/io/bkbn/kompendium/Kompendium.kt @@ -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 addCustomRouteHandler( + selector: KClass, + handler: IPathCalculator.(Route, String) -> String + ) { + PathCalculator.addCustomRouteHandler(selector, handler) + } + } diff --git a/kompendium-core/src/main/kotlin/io/bkbn/kompendium/Notarized.kt b/kompendium-core/src/main/kotlin/io/bkbn/kompendium/Notarized.kt index e0c1fdd65..724ba21a6 100644 --- a/kompendium-core/src/main/kotlin/io/bkbn/kompendium/Notarized.kt +++ b/kompendium-core/src/main/kotlin/io/bkbn/kompendium/Notarized.kt @@ -36,7 +36,7 @@ object Notarized { noinline body: PipelineInterceptor ): Route = KompendiumPreFlight.methodNotarizationPreFlight() { 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 ): Route = KompendiumPreFlight.methodNotarizationPreFlight() { 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, ): Route = KompendiumPreFlight.methodNotarizationPreFlight() { 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 ): Route = KompendiumPreFlight.methodNotarizationPreFlight { 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) } diff --git a/kompendium-core/src/main/kotlin/io/bkbn/kompendium/path/CorePathCalculator.kt b/kompendium-core/src/main/kotlin/io/bkbn/kompendium/path/CorePathCalculator.kt deleted file mode 100644 index 02b56f46b..000000000 --- a/kompendium-core/src/main/kotlin/io/bkbn/kompendium/path/CorePathCalculator.kt +++ /dev/null @@ -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}") - -} diff --git a/kompendium-core/src/main/kotlin/io/bkbn/kompendium/path/IPathCalculator.kt b/kompendium-core/src/main/kotlin/io/bkbn/kompendium/path/IPathCalculator.kt new file mode 100644 index 000000000..8c4c4478c --- /dev/null +++ b/kompendium-core/src/main/kotlin/io/bkbn/kompendium/path/IPathCalculator.kt @@ -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 addCustomRouteHandler( + selector: KClass, + handler: IPathCalculator.(Route, String) -> String + ) +} diff --git a/kompendium-core/src/main/kotlin/io/bkbn/kompendium/path/PathCalculator.kt b/kompendium-core/src/main/kotlin/io/bkbn/kompendium/path/PathCalculator.kt index bb235c129..a7aef148d 100644 --- a/kompendium-core/src/main/kotlin/io/bkbn/kompendium/path/PathCalculator.kt +++ b/kompendium-core/src/main/kotlin/io/bkbn/kompendium/path/PathCalculator.kt @@ -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 + private val pathHandler: RouteHandlerMap = mutableMapOf() - /** - * 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 addCustomRouteHandler( + selector: KClass, + handler: IPathCalculator.(Route, String) -> String + ) { + pathHandler[selector] = handler + } } diff --git a/kompendium-core/src/main/kotlin/io/bkbn/kompendium/path/RouteHandlerMap.kt b/kompendium-core/src/main/kotlin/io/bkbn/kompendium/path/RouteHandlerMap.kt new file mode 100644 index 000000000..16bce03f2 --- /dev/null +++ b/kompendium-core/src/main/kotlin/io/bkbn/kompendium/path/RouteHandlerMap.kt @@ -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, IPathCalculator.(Route, String) -> String>