Skip to content

Commit

Permalink
GH-40 Convert annotated routing plugin into routing API component
Browse files Browse the repository at this point in the history
  • Loading branch information
dzikoysk committed Nov 11, 2023
1 parent 71b803e commit 1ce4339
Show file tree
Hide file tree
Showing 9 changed files with 269 additions and 239 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package io.javalin.community.routing.annotations

import io.javalin.Javalin
import io.javalin.community.routing.Route
import io.javalin.community.routing.dsl.DslRoute
import io.javalin.community.routing.registerRoute
Expand All @@ -9,68 +8,90 @@ import io.javalin.config.JavalinConfig
import io.javalin.http.BadRequestResponse
import io.javalin.http.Context
import io.javalin.http.Handler
import io.javalin.plugin.Plugin
import java.lang.UnsupportedOperationException
import io.javalin.router.InternalRouter
import io.javalin.router.RoutingApiInitializer
import java.util.function.Consumer

fun interface HandlerResultConsumer<T> {
fun handle(ctx: Context, value: T)
}

class AnnotatedRoutingPluginConfiguration {
class AnnotatedRoutingConfig {

var apiVersionHeader: String = "X-API-Version"
var resultHandlers: MutableMap<Class<*>, HandlerResultConsumer<*>> = mutableMapOf(
String::class.java to HandlerResultConsumer<String?> { ctx, value -> value?.also { ctx.result(it) } },
Unit::class.java to HandlerResultConsumer<Unit?> { _, _ -> },
Void::class.java to HandlerResultConsumer<Void?> { _, _ -> },
Void.TYPE to HandlerResultConsumer<Void?> { _, _ -> },
)
internal val registeredRoutes = mutableListOf<Any>()

fun <T> registerResultHandler(type: Class<T>, handler: HandlerResultConsumer<T>): AnnotatedRoutingPluginConfiguration = also {
fun <T> registerResultHandler(type: Class<T>, handler: HandlerResultConsumer<T>): AnnotatedRoutingConfig = also {
this.resultHandlers[type] = handler
}

inline fun <reified T> registerResultHandler(handler: HandlerResultConsumer<T>): AnnotatedRoutingPluginConfiguration =
inline fun <reified T> registerResultHandler(handler: HandlerResultConsumer<T>): AnnotatedRoutingConfig =
registerResultHandler(T::class.java, handler)

fun registerEndpoints(vararg endpoints: Any) {
registeredRoutes.addAll(endpoints)
}

}

class AnnotatedRoutingPlugin @JvmOverloads constructor(
private val configuration: AnnotatedRoutingPluginConfiguration = AnnotatedRoutingPluginConfiguration()
) : Plugin {
object AnnotatedRouting : RoutingApiInitializer<AnnotatedRoutingConfig> {

private val registeredRoutes = mutableListOf<AnnotatedRoute>()
private val registeredExceptionHandlers = mutableListOf<AnnotatedException>()
private val reflectiveEndpointLoader = ReflectiveEndpointLoader(configuration.resultHandlers)
@JvmField val Annotated = this

private data class RouteIdentifier(val route: Route, val path: String)

override fun apply(app: Javalin) {
override fun initialize(cfg: JavalinConfig, internalRouter: InternalRouter, setup: Consumer<AnnotatedRoutingConfig>) {
val configuration = AnnotatedRoutingConfig()
setup.accept(configuration)

val loader = ReflectiveEndpointLoader(configuration.resultHandlers)
val registeredRoutes = mutableListOf<AnnotatedRoute>()
val registeredExceptionHandlers = mutableListOf<AnnotatedException>()

configuration.registeredRoutes.forEach {
val detectedRoutes = loader.loadRoutesFromEndpoint(it)
registeredRoutes.addAll(detectedRoutes)

val detectedExceptionHandlers = loader.loadExceptionHandlers(it)
registeredExceptionHandlers.addAll(detectedExceptionHandlers)
}

registeredRoutes
.sortRoutes()
.groupBy { RouteIdentifier(it.method, it.path) }
.map { (id, routes) ->
id to when (routes.size) {
1 -> routes.first().let { Handler { ctx -> it.handler(ctx) } }
else -> createVersionedRoute(id, routes)
else -> createVersionedRoute(
apiVersionHeader = configuration.apiVersionHeader,
id = id,
routes = routes
)
}
}
.forEach { (id, handler) ->
app.registerRoute(id.route, id.path, handler)
internalRouter.registerRoute(id.route, id.path, handler)
}

registeredExceptionHandlers.forEach { annotatedException ->
app.exception(annotatedException.type.java) { exception, ctx ->
internalRouter.addHttpExceptionHandler(annotatedException.type.java) { exception, ctx ->
annotatedException.handler.invoke(ctx, exception)
}
}
}

private fun createVersionedRoute(id: RouteIdentifier, routes: List<DslRoute<Context, Unit>>): Handler {
private fun createVersionedRoute(apiVersionHeader: String, id: RouteIdentifier, routes: List<DslRoute<Context, Unit>>): Handler {
val versions = routes.map { it.version }
check(versions.size == versions.toSet().size) { "Duplicated version found for the same route: ${id.route} ${id.path} (versions: $versions)" }

return Handler { ctx ->
val version = ctx.header(configuration.apiVersionHeader)
val version = ctx.header(apiVersionHeader)

routes.firstOrNull { it.version == version }
?.handler
Expand All @@ -79,25 +100,4 @@ class AnnotatedRoutingPlugin @JvmOverloads constructor(
}
}

fun registerPrecompiledEndpoints() {
throw UnsupportedOperationException("Not implemented")
}

fun registerEndpoints(vararg endpoints: Any) {
val detectedRoutes = endpoints.flatMap { reflectiveEndpointLoader.loadRoutesFromEndpoint(it) }
registeredRoutes.addAll(detectedRoutes)

val detectedExceptionHandlers = endpoints.flatMap { reflectiveEndpointLoader.loadExceptionHandlers(it) }
registeredExceptionHandlers.addAll(detectedExceptionHandlers)
}

}

fun JavalinConfig.registerAnnotatedEndpoints(configuration: AnnotatedRoutingPluginConfiguration, vararg endpoints: Any) {
val plugin = AnnotatedRoutingPlugin(configuration)
plugin.registerEndpoints(*endpoints)
this.plugins.register(plugin)
}

fun JavalinConfig.registerAnnotatedEndpoints(vararg endpoints: Any) =
registerAnnotatedEndpoints(AnnotatedRoutingPluginConfiguration(), *endpoints)
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import io.javalin.community.routing.dsl.DefaultDslException
import io.javalin.community.routing.dsl.DefaultDslRoute
import io.javalin.http.Context
import io.javalin.http.HttpStatus
import io.javalin.validation.Validator
import io.javalin.validation.validation
import java.lang.reflect.Method
import java.lang.reflect.Parameter
import kotlin.reflect.KClass
Expand Down Expand Up @@ -141,7 +141,7 @@ internal class ReflectiveEndpointLoader(
status: Status?,
ctx: Context,
resultHandler: HandlerResultConsumer<Any?>
): Any? =
): Any =
try {
val result = method.invoke(instance, *arguments)
status
Expand Down Expand Up @@ -198,7 +198,7 @@ internal class ReflectiveEndpointLoader(
getAnnotation(Cookie::class.java)
.value
.ifEmpty { name }
.let { Validator.create(type, ctx.cookie(it), it) }
.let { ctx.validation().validator(it, type, ctx.cookie(it)) }
.get()
}
isAnnotationPresent(Body::class.java) -> { ctx, _ ->
Expand Down
Loading

0 comments on commit 1ce4339

Please sign in to comment.