scanner
package site.daydream.colleen
import java.lang.reflect.Method
import kotlin.reflect.KClass
import kotlin.reflect.KFunction
import kotlin.reflect.full.findAnnotation
import kotlin.reflect.full.functions
import kotlin.reflect.jvm.isAccessible
object ControllerScanner {
fun scan(obj: Any): ControllerMeta {
val klass = obj::class
val javaClass = obj.javaClass
val basePath = klass.findAnnotation<Controller>()?.value ?: "/"
val middleware = findMiddlewares(javaClass)
val routes = findRoutes(javaClass)
return ControllerMeta(basePath, middleware, routes, obj)
}
private fun findMiddlewares(javaClass: Class<*>): List<Method> {
return javaClass.declaredMethods
.filter { it.getAnnotation(Use::class.java) != null }
.map { validateMiddlewareSignature(it) }
}
private fun validateMiddlewareSignature(method: Method): Method {
val paramTypes = method.parameterTypes
require(paramTypes.size == 2) {
"@Use method '${method.name}' must have 2 parameters, got ${paramTypes.size}"
}
require(isContextType(paramTypes[0])) {
"First parameter must be Context, got ${paramTypes[0].name}"
}
require(isNextType(paramTypes[1])) {
"Second parameter must be Next, got ${paramTypes[1].name}"
}
require(method.returnType == Void.TYPE) {
"Return type must be Unit/void, got ${method.returnType.name}"
}
method.isAccessible = true
return method
}
private fun findRoutes(javaClass: Class<*>): List<RouteNode> {
return javaClass.declaredMethods.mapNotNull { method ->
val (annotation, httpMethod) = detectHttpMethod(method) ?: return@mapNotNull null
val path = extractPath(annotation)
method.isAccessible = true
RouteNode(path, httpMethod, method)
}
}
private fun detectHttpMethod(method: Method): Pair<Annotation, String>? {
return when {
method.getAnnotation(Get::class.java) != null ->
method.getAnnotation(Get::class.java)!! to "GET"
method.getAnnotation(Post::class.java) != null ->
method.getAnnotation(Post::class.java)!! to "POST"
method.getAnnotation(Put::class.java) != null ->
method.getAnnotation(Put::class.java)!! to "PUT"
method.getAnnotation(Delete::class.java) != null ->
method.getAnnotation(Delete::class.java)!! to "DELETE"
else -> null
}
}
private fun extractPath(annotation: Annotation): String {
return when (annotation) {
is Get -> annotation.value
is Post -> annotation.value
is Put -> annotation.value
is Delete -> annotation.value
else -> "/"
}
}
private fun isContextType(type: Class<*>): Boolean {
return type.simpleName == "Context" ||
type.interfaces.any { it.simpleName == "Context" } ||
type.name.endsWith("Context")
}
private fun isNextType(type: Class<*>): Boolean {
return type.simpleName == "Next" ||
type.interfaces.any { it.simpleName == "Next" } ||
type.name.endsWith("Next")
}
data class ControllerMeta(
val basePath: String,
val middlewares: List<Method>,
val routes: List<RouteNode>,
val obj: Any
)
data class RouteNode(
val path: String,
val method: String,
val handler: Method
)
}
object ControllerScannerKotlin {
fun scan(obj: Any): ControllerMeta {
val klass = obj::class
val basePath = klass.findAnnotation<Controller>()?.value ?: "/"
val middleware = findMiddleware(klass)
val routes = findRoutes(klass, basePath)
return ControllerMeta(basePath, middleware, routes)
}
private fun findMiddleware(klass: KClass<*>): KFunction<*>? {
return klass.functions
.firstOrNull { it.findAnnotation<Use>() != null }
?.apply { validateMiddlewareSignature(this) }
}
private fun validateMiddlewareSignature(method: KFunction<*>) {
val params = method.parameters.filterNot { it.kind.name == "INSTANCE" }
require(params.size == 2) {
"@Use method '${method.name}' must have 2 parameters, got ${params.size}"
}
val paramTypes = params.map { it.type.toString() }
require(paramTypes[0].contains("Context")) { "First parameter must be Context" }
require(paramTypes[1].contains("Next")) { "Second parameter must be Next" }
require(method.returnType.toString().contains("Unit")) { "Return type must be Unit" }
method.isAccessible = true
}
private fun findRoutes(klass: KClass<*>, basePath: String): List<Route> {
return klass.functions.mapNotNull { method ->
val (annotation, httpMethod) = detectHttpMethod(method) ?: return@mapNotNull null
val path = extractPath(annotation)
method.isAccessible = true
Route(normalizePath(basePath, path), httpMethod, method)
}
}
private fun detectHttpMethod(method: KFunction<*>): Pair<Any, String>? {
return when {
method.findAnnotation<Get>() != null ->
method.findAnnotation<Get>()!! to "GET"
method.findAnnotation<Post>() != null ->
method.findAnnotation<Post>()!! to "POST"
method.findAnnotation<Put>() != null ->
method.findAnnotation<Put>()!! to "PUT"
method.findAnnotation<Delete>() != null ->
method.findAnnotation<Delete>()!! to "DELETE"
else -> null
}
}
private fun extractPath(annotation: Any): String {
return when (annotation) {
is Get -> annotation.value
is Post -> annotation.value
is Put -> annotation.value
is Delete -> annotation.value
else -> "/"
}
}
private fun normalizePath(basePath: String, path: String): String {
return (basePath + path).replace(Regex("/+"), "/")
}
data class ControllerMeta(
val basePath: String,
val middleware: KFunction<*>?,
val routes: List<Route>
)
data class Route(
val path: String,
val method: String,
val handler: KFunction<*>
)
}