extractor
package site.daydream.colleen
import java.io.InputStream
import kotlin.reflect.KFunction
import kotlin.reflect.KParameter
import kotlin.reflect.KType
import kotlin.reflect.full.findAnnotation
// ===== Exception Definitions =====
sealed class ExtractionError(message: String, cause: Throwable? = null) : RuntimeException(message, cause) {
class MissingParameter(name: String, type: String) :
ExtractionError("Required parameter '$name' of type $type not found")
class InvalidType(type: String, reason: String) :
ExtractionError("Invalid type $type: $reason")
class ConversionFailed(value: String, from: String, to: String, cause: Throwable? = null) :
ExtractionError("Cannot convert '$value' from $from to $to", cause)
}
// ===== Parameter Extractor Interface =====
interface ParamExtractor<T> {
val value: T
}
// ===== Parameter Wrapper Types =====
data class Path<T>(override val value: T) : ParamExtractor<T>
data class Header(override val value: String) : ParamExtractor<String>
data class Cookie(override val value: String) : ParamExtractor<String>
data class Query<T>(override val value: T) : ParamExtractor<T>
data class Text(override val value: String) : ParamExtractor<String>
data class Json<T>(override val value: T) : ParamExtractor<T>
data class Form<T>(override val value: T) : ParamExtractor<T>
data class Stream(override val value: InputStream) : ParamExtractor<InputStream>
data class UploadedFile(override val value: Request.UploadedFile) : ParamExtractor<Request.UploadedFile>
// ===== Annotations =====
@Target(AnnotationTarget.VALUE_PARAMETER)
@Retention(AnnotationRetention.RUNTIME)
annotation class Param(val value: String = "")
// ===== cx Function Implementation =====
fun cx(fn: KFunction<*>): Handler {
val valueParams = fn.parameters.filter { it.kind == KParameter.Kind.VALUE }
val extractors = valueParams.map { param -> buildExtractor(param, fn.name) }
return Handler { ctx ->
val args = extractors.map { it(ctx) }
fn.callBy(valueParams.zip(args).toMap())
}
}
// ===== Core Extractor Builder =====
private fun buildExtractor(param: KParameter, handlerName: String): (Context) -> Any? {
val kType = param.type
val wrapperClass = kType.classifier as? kotlin.reflect.KClass<*>
?: throw ExtractionError.InvalidType(kType.toString(), "Cannot resolve classifier")
// Context 直接传递
if (wrapperClass == Context::class) {
return { it }
}
val paramName = param.findAnnotation<Param>()?.value?.takeIf { it.isNotEmpty() }
?: param.name
?: ""
return when (wrapperClass) {
Path::class -> buildPathExtractor(paramName, kType, handlerName)
Header::class -> buildHeaderExtractor(paramName)
Cookie::class -> buildCookieExtractor(paramName)
Query::class -> buildQueryExtractor(paramName, kType, handlerName)
Form::class -> buildFormExtractor(paramName, kType, handlerName)
Json::class -> buildJsonExtractor(kType, handlerName)
Stream::class -> buildStreamExtractor()
UploadedFile::class -> buildFileExtractor(paramName)
Text::class -> { ctx -> Text(ctx.text() ?: "") }
else -> { ctx -> ctx.getService(wrapperClass.java) }
}
}
// ===== 泛型类型解析 =====
/**
* 解析 Wrapper<T> 中的 T 类型
* 例如:Query<Int> -> GenericType(Int, nullable=false)
*/
private fun KType.unwrapGeneric(): GenericType {
val typeArg = arguments.firstOrNull()?.type
?: throw ExtractionError.InvalidType(toString(), "Missing generic type parameter")
val javaClass = (typeArg.classifier as? kotlin.reflect.KClass<*>)?.java
?: throw ExtractionError.InvalidType(typeArg.toString(), "Cannot resolve class")
return GenericType(javaClass, typeArg.isMarkedNullable)
}
/**
* 解析 Wrapper<List<T>> 中的元素类型 T
* 例如:Query<List<Int>> -> Int
*/
private fun KType.unwrapListElement(): Class<*> {
val listType = arguments.firstOrNull()?.type
?: throw ExtractionError.InvalidType(toString(), "Missing generic type")
val elementType = listType.arguments.firstOrNull()?.type
?: throw ExtractionError.InvalidType(listType.toString(), "List missing element type")
return (elementType.classifier as? kotlin.reflect.KClass<*>)?.java
?: throw ExtractionError.InvalidType(elementType.toString(), "Cannot resolve element class")
}
private data class GenericType(val javaClass: Class<*>, val isNullable: Boolean)
// ===== 类型判断扩展 =====
private fun Class<*>.isSimple() = this in SIMPLE_TYPES
private fun Class<*>.isMap() = Map::class.java.isAssignableFrom(this)
private fun Class<*>.isList() = List::class.java.isAssignableFrom(this)
private val SIMPLE_TYPES = setOf(
String::class.java,
Int::class.java, Integer::class.java,
Long::class.java, java.lang.Long::class.java,
Double::class.java, java.lang.Double::class.java,
Float::class.java, java.lang.Float::class.java,
Boolean::class.java, java.lang.Boolean::class.java
)
// ===== 各类型提取器 =====
private fun buildPathExtractor(name: String, kType: KType, handler: String): (Context) -> Path<*> {
require(name.isNotEmpty()) { "Path parameter requires @Param annotation in $handler" }
val generic = kType.unwrapGeneric()
require(!generic.isNullable) { "Path parameter cannot be nullable in $handler" }
return { ctx ->
val value = ctx.param(name)
?: throw ExtractionError.MissingParameter(name, "Path")
Path(value.convertTo(generic.javaClass, name))
}
}
private fun buildHeaderExtractor(name: String): (Context) -> Header {
require(name.isNotEmpty()) { "Header parameter requires @Param annotation" }
return { ctx -> Header(ctx.header(name) ?: "") }
}
private fun buildCookieExtractor(name: String): (Context) -> Cookie {
require(name.isNotEmpty()) { "Cookie parameter requires @Param annotation" }
return { ctx -> Cookie(ctx.cookie(name) ?: "") }
}
private fun buildQueryExtractor(name: String, kType: KType, handler: String): (Context) -> Query<*> {
val generic = kType.unwrapGeneric()
val targetClass = generic.javaClass
// Query<Map<String, List<String>>>
if (targetClass.isMap()) {
return { ctx -> Query(ctx.queries()) }
}
// Query<List<T>>
if (targetClass.isList()) {
val elementClass = kType.unwrapListElement()
return { ctx ->
val values = ctx.request.queryAll(name)
Query(values.map { it.convertTo(elementClass, name) })
}
}
// Query<Int> 或 Query<Int?>
if (targetClass.isSimple()) {
return { ctx ->
val value = ctx.query(name)
when {
value != null -> Query(value.convertTo(targetClass, name))
generic.isNullable -> Query(null)
else -> throw ExtractionError.MissingParameter(name, "Query")
}
}
}
// Query<CustomDto>
return { ctx ->
try {
Query(ctx.queriesAs(targetClass)!!)
} catch (e: Exception) {
throw ExtractionError.ConversionFailed("query data", "Query", targetClass.simpleName, e)
}
}
}
private fun buildFormExtractor(name: String, kType: KType, handler: String): (Context) -> Form<*> {
val generic = kType.unwrapGeneric()
val targetClass = generic.javaClass
// Form<Map<String, List<String>>>
if (targetClass.isMap()) {
return { ctx -> Form(ctx.forms()) }
}
// Form<List<T>>
if (targetClass.isList()) {
val elementClass = kType.unwrapListElement()
return { ctx ->
val values = ctx.request.formAll(name)
Form(values.map { it.convertTo(elementClass, name) })
}
}
// Form<Int> 或 Form<Int?>
if (targetClass.isSimple()) {
return { ctx ->
val value = ctx.form(name)
when {
value != null -> Form(value.convertTo(targetClass, name))
generic.isNullable -> Form(null)
else -> throw ExtractionError.MissingParameter(name, "Form")
}
}
}
// Form<CustomDto>
return { ctx ->
try {
Form(ctx.formsAs(targetClass)!!)
} catch (e: Exception) {
throw ExtractionError.ConversionFailed("form data", "Form", targetClass.simpleName, e)
}
}
}
private fun buildJsonExtractor(kType: KType, handler: String): (Context) -> Json<*> {
val targetClass = kType.unwrapGeneric().javaClass
return { ctx ->
try {
Json(ctx.jsonAs(targetClass))
} catch (e: Exception) {
throw ExtractionError.ConversionFailed("request body", "JSON", targetClass.simpleName, e)
}
}
}
private fun buildStreamExtractor(): (Context) -> Stream {
return { ctx ->
Stream(ctx.request.stream ?: throw ExtractionError.MissingParameter("stream", "InputStream"))
}
}
private fun buildFileExtractor(name: String): (Context) -> UploadedFile {
// require(name.isNotEmpty()) { "File parameter requires @Param annotation" }
return { ctx ->
val file = ctx.file(name.ifEmpty { "file" })
?: throw ExtractionError.MissingParameter(name, "UploadedFile")
UploadedFile(file)
}
}
// ===== 类型转换 =====
private fun String.convertTo(targetClass: Class<*>, paramName: String): Any {
return try {
when (targetClass) {
String::class.java -> this
Int::class.java, Integer::class.java ->
toIntOrNull() ?: throw ExtractionError.ConversionFailed(this, "String", "Int")
Long::class.java, java.lang.Long::class.java ->
toLongOrNull() ?: throw ExtractionError.ConversionFailed(this, "String", "Long")
Double::class.java, java.lang.Double::class.java ->
toDoubleOrNull() ?: throw ExtractionError.ConversionFailed(this, "String", "Double")
Float::class.java, java.lang.Float::class.java ->
toFloatOrNull() ?: throw ExtractionError.ConversionFailed(this, "String", "Float")
Boolean::class.java, java.lang.Boolean::class.java ->
toLenientBoolean()
else ->
throw ExtractionError.InvalidType(targetClass.simpleName, "Unsupported conversion type")
}
} catch (e: ExtractionError) {
throw e
} catch (e: Exception) {
throw ExtractionError.ConversionFailed(this, "String", targetClass.simpleName, e)
}
}
private fun String.toLenientBoolean(): Boolean {
return lowercase() in setOf("true", "1", "yes", "y")
}