Last active
July 25, 2024 12:08
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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<*> | |
) | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@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