Skip to content

Instantly share code, notes, and snippets.

@SubSide
Last active May 28, 2024 20:22
Show Gist options
  • Save SubSide/76c829a2fa7032372b6b168b273ac654 to your computer and use it in GitHub Desktop.
Save SubSide/76c829a2fa7032372b6b168b273ac654 to your computer and use it in GitHub Desktop.
An easy way to register controller methods in Ktor. This helps clean up some code.
import io.ktor.application.ApplicationCall
import io.ktor.application.call
import io.ktor.http.HttpMethod
import io.ktor.routing.Route
import io.ktor.routing.Routing
import io.ktor.routing.route
import kotlin.reflect.KFunction
import kotlin.reflect.full.callSuspendBy
import kotlin.reflect.full.declaredMemberFunctions
import kotlin.reflect.jvm.javaType
/**
* The RouteProcessor is an easy way to process methods that are marked with one of the methods annotations.
* Use this in your Application class in the routing part as processRoutes(objWithAnnotatedMethods).
*/
object RouteProcessor {
/**
* Processes all methods in the object.
* This is a convenience method for #process(Routing,Any) so it can easily be used in routing.
*
* @param obj The object to process
*/
fun Routing.processRoutes(obj: Any) {
process(this, obj)
}
/**
* Processes all methods in the object.
* All methods that are annotated with any (or multiple) of the annotations will be registered.
*
* If your give the annotation a route like /profile/{name} it will fill whatever is filled in {name} in the
* parameter "name". You can give a parameter the type HttpMethod to get which HTTP method was used. And give a
* parameter the type ApplicationCall to receive the application call. This can be mandatory to respond to the
* request.
*
* @param routing The routing object
* @param obj The object whose methods should be processed
*/
fun process(routing: Routing, obj: Any) {
getRoutePaths(obj).forEach { route ->
// A route path consists of an array of paths, so we need to handle them one by one
route.paths.forEach { path ->
if (route.method == null) {
// If the method is null, we want to globally register it instead of registering it to
// a certain method.
routing.route(path, createRoute(obj, route.function))
} else {
// Otherwise we route it to a single method
routing.route(path, route.method, createRoute(obj, route.function))
}
}
}
}
/**
* A method to clean up #process. Does not much special accept the boilerplate code to register the route.
*
* @param obj The object to create the route for
* @param function The function that should be called for this route
* @return The function that will be executed in the Routes' context
*/
private fun createRoute(obj: Any, function: KFunction<*>): Route.() -> Unit = {
handle {
handleRoute(obj, function, call)
}
}
/**
* This function handles the route request. It maps the call parameters to the function parameters. After that
* it calls the function with those parameters in a suspended way.
*
* @param obj The object that the function should be called on
* @param function The function that should be called
* @param call The requests' ApplicationCall
*/
private suspend fun handleRoute(
obj: Any,
function: KFunction<*>,
call: ApplicationCall
) {
val parameters = function.parameters.mapNotNull {
when {
it.type.javaType == obj.javaClass -> it to obj
it.type.javaType == ApplicationCall::class.java -> it to call
it.type.javaType == HttpMethod::class.java -> it to call.request.local.method
call.parameters.contains(it.name ?: "") -> it to call.parameters[it.name ?: ""]
else -> null
}
}.toMap()
function.callSuspendBy(parameters)
}
/**
* Get RoutePath objects from the "obj" that contains the different routes and to which HTTP method it should map to
*
* @param obj The object it should get the RoutePaths from
* @return A list of RoutePaths
*/
private fun getRoutePaths(obj: Any): List<RoutePaths> {
return obj::class.declaredMemberFunctions.map { method ->
method.annotations.mapNotNull {
when (it) {
is GET -> RoutePaths(it.routes, HttpMethod.Get, method)
is POST -> RoutePaths(it.routes, HttpMethod.Post, method)
is PUT -> RoutePaths(it.routes, HttpMethod.Put, method)
is PATCH -> RoutePaths(it.routes, HttpMethod.Patch, method)
is DELETE -> RoutePaths(it.routes, HttpMethod.Delete, method)
is HEAD -> RoutePaths(it.routes, HttpMethod.Head, method)
is OPTIONS -> RoutePaths(it.routes, HttpMethod.Options, method)
is ANY -> RoutePaths(it.routes, null, method)
else -> null
}
}
}.flatten()
}
/**
* Contains the paths, method and function of a certain route.
* Will be used to register all the different paths.
*
* @param paths An array of paths that will route to the #function
* @param method The HTTP method it should listen to
* @param function The function that will be called when this route is visited
*/
class RoutePaths(
val paths: Array<out String>,
val method: HttpMethod?,
val function: KFunction<*>
)
}
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FUNCTION)
annotation class GET(vararg val routes: String)
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FUNCTION)
annotation class POST(vararg val routes: String)
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FUNCTION)
annotation class PUT(vararg val routes: String)
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FUNCTION)
annotation class PATCH(vararg val routes: String)
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FUNCTION)
annotation class DELETE(vararg val routes: String)
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FUNCTION)
annotation class HEAD(vararg val routes: String)
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FUNCTION)
annotation class OPTIONS(vararg val routes: String)
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FUNCTION)
annotation class ANY(vararg val routes: String)
@kotlin.jvm.JvmOverloads
fun Application.module(testing: Boolean = false) {
routing {
processRoutes(HomeController())
}
}
// Somewhere else, like in a controller package
import io.ktor.application.ApplicationCall
import io.ktor.response.respondText
class ProfileController {
@GET("profile/{profile}")
suspend fun showProfile(call: ApplicationCall, profile: String, test: String = " optional!") {
call.respondText("Profile: $profile, test: $test")
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment