Skip to content

GH-54 Endpoint metadata #55

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package io.javalin.community.routing.annotations

import io.javalin.community.routing.dsl.DslRouteMetadata
import java.util.*

class AnnotatedEndpointMetadata(
val annotations: Set<Annotation>
) : DslRouteMetadata {

fun getAnnotation(annotationClass: Class<out Annotation>): Optional<Annotation> =
annotations
.firstOrNull { it.annotationClass.java == annotationClass }
.let { Optional.ofNullable(it) }

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package io.javalin.community.routing.annotations

import io.javalin.community.routing.Route
import io.javalin.community.routing.dsl.DslRoute
import io.javalin.community.routing.dsl.DslRouteMetadataFactory
import io.javalin.community.routing.invokeAsSamWithReceiver
import io.javalin.community.routing.registerRoute
import io.javalin.community.routing.sortRoutes
Expand Down Expand Up @@ -93,17 +94,25 @@ object AnnotatedRouting : RoutingApiInitializer<AnnotatedRoutingConfig> {
.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(
apiVersionHeader = configuration.apiVersionHeader,
id = id,
routes = routes
)
when (routes.size) {
1 -> Triple(id, routes.first().let { Handler { ctx -> it.handler(ctx) } }, routes.first().metadataFactory)
else ->
createVersionedRoute(
apiVersionHeader = configuration.apiVersionHeader,
id = id,
routes = routes
).let {
Triple(id, it.first, it.second)
}
}
}
.forEach { (id, handler) ->
internalRouter.registerRoute(id.route, id.path, handler)
.forEach { (id, handler, metadataFactory) ->
internalRouter.registerRoute(
route = id.route,
path = id.path,
handler = handler,
metadata = metadataFactory?.let { arrayOf(it) } ?: emptyArray(),
)
}

registeredExceptionHandlers.forEach { annotatedException ->
Expand All @@ -113,18 +122,28 @@ object AnnotatedRouting : RoutingApiInitializer<AnnotatedRoutingConfig> {
}
}

private fun createVersionedRoute(apiVersionHeader: String, id: RouteIdentifier, routes: List<DslRoute<Context, Unit>>): Handler {
private fun createVersionedRoute(apiVersionHeader: String, id: RouteIdentifier, routes: List<DslRoute<Context, Unit>>): Pair<Handler, DslRouteMetadataFactory> {
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 metadataFactory = DslRouteMetadataFactory { ctx ->
val version = ctx.header(apiVersionHeader)

routes.firstOrNull { it.version == version }
?.let { it.metadataFactory?.create(ctx) }
?: throw BadRequestResponse("This endpoint does not support the requested API version ($version).")
}

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

routes.firstOrNull { it.version == version }
?.handler
?.invoke(ctx)
?: throw BadRequestResponse("This endpoint does not support the requested API version ($version).")
}

return handler to metadataFactory
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import io.javalin.community.routing.Route
import io.javalin.community.routing.Route.BEFORE_MATCHED
import io.javalin.community.routing.dsl.DefaultDslException
import io.javalin.community.routing.dsl.DefaultDslRoute
import io.javalin.community.routing.dsl.DslRouteMetadataFactory
import io.javalin.event.JavalinLifecycleEvent
import io.javalin.http.Context
import io.javalin.http.HttpStatus
Expand Down Expand Up @@ -60,7 +61,9 @@ internal class ReflectiveEndpointLoader(
"Unable to access method $method in class $endpointClass"
}

val annotations = method.annotations.toSet()
val types = method.genericParameterTypes

val argumentSuppliers = method.parameters.mapIndexed { idx, parameter ->
createArgumentSupplier<Unit>(parameter, types[idx]) ?: throw IllegalArgumentException("Unsupported parameter type: $parameter")
}
Expand All @@ -80,6 +83,7 @@ internal class ReflectiveEndpointLoader(
AnnotatedRoute(
method = httpMethod,
path = pathToAdd,
metadataFactory = { _ -> AnnotatedEndpointMetadata(annotations = annotations) },
version = method.getAnnotation<Version>()?.value,
handler = {
val arguments = argumentSuppliers
Expand Down Expand Up @@ -231,6 +235,14 @@ internal class ReflectiveEndpointLoader(
.firstOrNull()
?.endpoint
}
expectedTypeAsClass.isAssignableFrom(AnnotatedEndpointMetadata::class.java) -> { ctx, _ ->
internalRouter
.findHttpHandlerEntries(ctx.method(), ctx.path().removePrefix(ctx.contextPath()))
.firstOrNull()
?.endpoint
?.metadata(DslRouteMetadataFactory::class.java)
?.create(ctx)
}
isAnnotationPresent<Param>() -> { ctx, _ ->
getAnnotationOrThrow<Param>()
.value
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import io.javalin.community.routing.annotations.AnnotatedRouting.Annotated
import io.javalin.event.JavalinLifecycleEvent.SERVER_STARTED
import io.javalin.http.Context
import io.javalin.http.HandlerType
import io.javalin.http.Header.AUTHORIZATION
import io.javalin.http.HttpStatus
import io.javalin.router.Endpoint
import io.javalin.testtools.HttpClient
Expand Down Expand Up @@ -542,4 +543,43 @@ class AnnotatedRoutingTest {

}

@Retention(AnnotationRetention.RUNTIME)
annotation class Protected

@Nested
inner class Metadata {

@Test
fun `should expose custom annotations in metadata`() =
JavalinTest.test(
Javalin.create { cfg ->
cfg.router.mount(Annotated) {
it.registerEndpoints(
object {
@BeforeMatched
fun protect(ctx: Context, metadata: AnnotatedEndpointMetadata) {
val isProtected = metadata.getAnnotation(Protected::class.java).isPresent
val hasValidToken = ctx.header(AUTHORIZATION) == "Bearer token"

if (isProtected && !hasValidToken) {
ctx.status(HttpStatus.UNAUTHORIZED).skipRemainingHandlers()
}
}

@Protected
@Get("/protected")
fun endpoint(ctx: Context) {
ctx.result("success")
}
}
)
}
}
) { _, client ->
assertThat(Unirest.get("${client.origin}/protected").asString().status).isEqualTo(HttpStatus.UNAUTHORIZED.code)
assertThat(Unirest.get("${client.origin}/protected").header(AUTHORIZATION, "Bearer token").asString().body).isEqualTo("success")
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,8 @@ import io.javalin.Javalin
import io.javalin.config.RouterConfig
import io.javalin.http.Handler
import io.javalin.http.HandlerType
import io.javalin.router.Endpoint
import io.javalin.router.InternalRouter
import io.javalin.router.JavalinDefaultRouting
import io.javalin.router.RoutingApiInitializer
import io.javalin.router.RoutingSetupScope
import io.javalin.router.*
import io.javalin.security.Roles
import io.javalin.security.RouteRole
import java.util.function.Consumer

Expand Down Expand Up @@ -43,24 +40,33 @@ data class HandlerEntry @JvmOverloads constructor(
) : Routed

fun InternalRouter.registerRoute(handlerEntry: HandlerEntry) =
registerRoute(handlerEntry.route, handlerEntry.path, handlerEntry.handler, *handlerEntry.roles.toTypedArray())
registerRoute(
route = handlerEntry.route,
path = handlerEntry.path,
handler = handlerEntry.handler,
metadata = arrayOf(Roles(handlerEntry.roles.toSet())),
)

fun InternalRouter.registerRoute(route: Route, path: String, handler: Handler, vararg roles: RouteRole) {
@Suppress("DEPRECATION")
fun InternalRouter.registerRoute(route: Route, path: String, handler: Handler, vararg metadata: EndpointMetadata) {
when (route) {
Route.HEAD -> addHttpEndpoint(Endpoint(method = HandlerType.HEAD, path = path, handler = handler, roles = roles))
Route.PATCH -> addHttpEndpoint(Endpoint(method = HandlerType.PATCH, path = path, handler = handler, roles = roles))
Route.OPTIONS -> addHttpEndpoint(Endpoint(method = HandlerType.OPTIONS, path = path, handler = handler, roles = roles))
Route.GET -> addHttpEndpoint(Endpoint(method = HandlerType.GET, path = path, handler = handler, roles = roles))
Route.PUT -> addHttpEndpoint(Endpoint(method = HandlerType.PUT, path = path, handler = handler, roles = roles))
Route.POST -> addHttpEndpoint(Endpoint(method = HandlerType.POST, path = path, handler = handler, roles = roles))
Route.DELETE -> addHttpEndpoint(Endpoint(method = HandlerType.DELETE, path = path, handler = handler, roles = roles))
Route.BEFORE -> addHttpEndpoint(Endpoint(method = HandlerType.BEFORE, path = path, handler = handler))
Route.BEFORE_MATCHED -> addHttpEndpoint(Endpoint(method = HandlerType.BEFORE_MATCHED, path = path, handler = handler))
Route.AFTER -> addHttpEndpoint(Endpoint(method = HandlerType.AFTER, path = path, handler = handler))
Route.AFTER_MATCHED -> addHttpEndpoint(Endpoint(method = HandlerType.AFTER_MATCHED, path = path, handler = handler))
Route.HEAD -> addHttpEndpoint(Endpoint.create(HandlerType.HEAD, path).metadata(*metadata).handler(handler))
Route.PATCH -> addHttpEndpoint(Endpoint.create(HandlerType.PATCH, path).metadata(*metadata).handler(handler))
Route.OPTIONS -> addHttpEndpoint(Endpoint.create(HandlerType.OPTIONS, path).metadata(*metadata).handler(handler))
Route.GET -> addHttpEndpoint(Endpoint.create(HandlerType.GET, path).metadata(*metadata).handler(handler))
Route.PUT -> addHttpEndpoint(Endpoint.create(HandlerType.PUT, path).metadata(*metadata).handler(handler))
Route.POST -> addHttpEndpoint(Endpoint.create(HandlerType.POST, path).metadata(*metadata).handler(handler))
Route.DELETE -> addHttpEndpoint(Endpoint.create(HandlerType.DELETE, path).metadata(*metadata).handler(handler))
Route.BEFORE -> addHttpEndpoint(Endpoint.create(HandlerType.BEFORE, path).metadata(*metadata).handler(handler))
Route.BEFORE_MATCHED -> addHttpEndpoint(Endpoint.create(HandlerType.BEFORE_MATCHED, path).metadata(*metadata).handler(handler))
Route.AFTER -> addHttpEndpoint(Endpoint.create(HandlerType.AFTER, path).metadata(*metadata).handler(handler))
Route.AFTER_MATCHED -> addHttpEndpoint(Endpoint.create(HandlerType.AFTER_MATCHED, path).metadata(*metadata).handler(handler))
}
}

fun Endpoint.Companion.EndpointBuilder.metadata(vararg metadata: EndpointMetadata): Endpoint.Companion.EndpointBuilder =
metadata.fold(this) { acc, value -> acc.addMetadata(value) }

fun RouterConfig.mount(setup: RoutingSetupScope<JavalinDefaultRouting>): RouterConfig = also {
mount(Consumer {
setup.invokeAsSamWithReceiver(it)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,30 @@ package io.javalin.community.routing.dsl

import io.javalin.community.routing.Route
import io.javalin.community.routing.Routed
import io.javalin.http.Context
import io.javalin.router.EndpointMetadata

/* Regular routes */

typealias DslHandler<CONTEXT, RESPONSE> = CONTEXT.() -> RESPONSE

interface DslRouteMetadataFactory : EndpointMetadata {
fun create(ctx: Context): DslRouteMetadata
}

interface DslRouteMetadata

interface DslRoute<CONTEXT, RESPONSE : Any> : Routed {
val method: Route
val version: String?
val metadataFactory: DslRouteMetadataFactory?
val handler: DslHandler<CONTEXT, RESPONSE>
}

open class DefaultDslRoute<CONTEXT, RESPONSE : Any>(
override val method: Route,
override val path: String,
override val metadataFactory: DslRouteMetadataFactory? = null,
override val version: String? = null,
override val handler: CONTEXT.() -> RESPONSE
) : DslRoute<CONTEXT, RESPONSE>
Loading