diff --git a/ksp-annotations/common/src/dev/programadorthi/routing/annotation/Body.kt b/ksp-annotations/common/src/dev/programadorthi/routing/annotation/Body.kt new file mode 100644 index 0000000..30cb751 --- /dev/null +++ b/ksp-annotations/common/src/dev/programadorthi/routing/annotation/Body.kt @@ -0,0 +1,4 @@ +package dev.programadorthi.routing.annotation + +@Target(AnnotationTarget.VALUE_PARAMETER) +public annotation class Body diff --git a/ksp-processor/jvm/src/dev/programadorthi/routing/ksp/RoutingProcessor.kt b/ksp-processor/jvm/src/dev/programadorthi/routing/ksp/RoutingProcessor.kt index 4d4dadd..ef862e6 100644 --- a/ksp-processor/jvm/src/dev/programadorthi/routing/ksp/RoutingProcessor.kt +++ b/ksp-processor/jvm/src/dev/programadorthi/routing/ksp/RoutingProcessor.kt @@ -16,11 +16,13 @@ import com.google.devtools.ksp.symbol.KSAnnotated import com.google.devtools.ksp.symbol.KSFunctionDeclaration import com.google.devtools.ksp.symbol.KSType import com.google.devtools.ksp.symbol.Visibility +import com.squareup.kotlinpoet.CodeBlock import com.squareup.kotlinpoet.FileSpec import com.squareup.kotlinpoet.FunSpec import com.squareup.kotlinpoet.KModifier import com.squareup.kotlinpoet.MemberName import com.squareup.kotlinpoet.ksp.writeTo +import dev.programadorthi.routing.annotation.Body import dev.programadorthi.routing.annotation.Path import dev.programadorthi.routing.annotation.Route @@ -47,6 +49,8 @@ private class RoutingProcessor( invoked = true val call = MemberName("dev.programadorthi.routing.core.application", "call") + val receive = MemberName("dev.programadorthi.routing.core.application", "receive") + val receiveNullable = MemberName("dev.programadorthi.routing.core.application", "receiveNullable") val handle = MemberName("dev.programadorthi.routing.core", "handle") val configureSpec = FunSpec @@ -68,10 +72,6 @@ private class RoutingProcessor( "$qualifiedName fun must not be private" } - check(func.packageName.asString().isNotBlank()) { - "Top level fun '$qualifiedName' must have a package" - } - val routeAnnotation = checkNotNull(func.getAnnotationsByType(Route::class).firstOrNull()) { "Invalid state because a @Route was not found to '$qualifiedName'" } @@ -83,19 +83,53 @@ private class RoutingProcessor( "@Route using regex can't be named" } - val parameters = mutableListOf() + val named = when { + routeAnnotation.name.isBlank() -> "name = null" + else -> """name = "${routeAnnotation.name}"""" + } + if (isRegexRoute) { + configureSpec.beginControlFlow("%M(%T(%S))", handle, Regex::class, routeAnnotation.regex) + } else { + configureSpec.beginControlFlow("%M(path = %S, $named)", handle, routeAnnotation.path) + } + + val funcMember = MemberName(func.packageName.asString(), func.simpleName.asString()) + val funcBuilder = CodeBlock.builder() + val isMultipleParameters = func.parameters.size > 1 + if (isMultipleParameters) { + funcBuilder + .addStatement("%M(", funcMember) + .indent() + } else { + funcBuilder.add("%M(", funcMember) + } for (param in func.parameters) { check(param.isVararg.not()) { "Vararg is not supported as fun parameter" } val paramName = param.name?.asString() + val paramType = param.type.resolve() + val body = param + .getAnnotationsByType(Body::class) + .firstOrNull() + if (body != null) { + val member = when { + paramType.isMarkedNullable -> receiveNullable + else -> receive + } + when { + isMultipleParameters -> funcBuilder.addStatement("$paramName = %M.%M(),", call, member) + else -> funcBuilder.add("$paramName = %M.%M()", call, member) + } + continue + } + val customName = param .getAnnotationsByType(Path::class) .firstOrNull() ?.value ?: paramName - val paramType = param.type.resolve() if (!isRegexRoute && routeAnnotation.path.contains("{$customName...}")) { val listDeclaration = checkNotNull(resolver.getClassDeclarationByName>()) { "Class declaration not found to List?" @@ -113,7 +147,11 @@ private class RoutingProcessor( check(paramType.isMarkedNullable) { "Tailcard list must be nullable as List?" } - parameters += """$paramName = %M.parameters.getAll("$customName")""" + + when { + isMultipleParameters -> funcBuilder.addStatement("""$paramName = %M.parameters.getAll("$customName"),""", call) + else -> funcBuilder.add("""$paramName = %M.parameters.getAll("$customName")""", call) + } continue } @@ -124,26 +162,26 @@ private class RoutingProcessor( "'$qualifiedName' has parameter '$paramName' that is not declared as path parameter {$customName}" } val parsed = """$paramName = %M.parameters["$customName"]""" - parameters += when { + val statement = when { isOptional -> optionalParse(paramType, resolver, parsed) else -> requiredParse(paramType, resolver, parsed) } + when { + isMultipleParameters -> funcBuilder.addStatement("$statement,", call) + else -> funcBuilder.add(statement, call) + } } - val calls = Array(size = parameters.size) { call } - val params = parameters.joinToString(prefix = "(", postfix = ")") { "\n$it" } - val named = when { - routeAnnotation.name.isBlank() -> "name = null" - else -> """name = "${routeAnnotation.name}"""" + if (isMultipleParameters) { + funcBuilder + .unindent() + .addStatement(")") + } else { + funcBuilder.addStatement(")") } - with(configureSpec) { - if (isRegexRoute) { - beginControlFlow("""%M(%T(%S))""", handle, Regex::class, routeAnnotation.regex) - } else { - beginControlFlow("""%M(path = %S, $named)""", handle, routeAnnotation.path) - } - }.addStatement("""$qualifiedName$params""", *calls) + configureSpec + .addCode(funcBuilder.build()) .endControlFlow() } diff --git a/samples/ksp-sample/src/main/kotlin/Main.kt b/samples/ksp-sample/src/main/kotlin/Main.kt index 04ac815..4573c4f 100644 --- a/samples/ksp-sample/src/main/kotlin/Main.kt +++ b/samples/ksp-sample/src/main/kotlin/Main.kt @@ -1,10 +1,11 @@ import dev.programadorthi.routing.core.call +import dev.programadorthi.routing.core.callWithBody import dev.programadorthi.routing.core.routing import dev.programadorthi.routing.generated.configure +import dev.programadorthi.routing.sample.User import io.ktor.http.parametersOf -import kotlinx.coroutines.delay import kotlin.random.Random - +import kotlinx.coroutines.delay suspend fun main() { val router = routing { @@ -31,4 +32,10 @@ suspend fun main() { delay(500) router.call(uri = "/456") // regex2 delay(500) + router.callWithBody(uri = "/with-body", body = User(id = 456, name = "With Body")) + delay(500) + router.call(uri = "/with-null-body") + delay(500) + router.callWithBody(uri = "/with-null-body", body = User(id = 789, name = "No null Body")) + delay(500) } diff --git a/samples/ksp-sample/src/main/kotlin/dev/programadorthi/routing/sample/Routes.kt b/samples/ksp-sample/src/main/kotlin/dev/programadorthi/routing/sample/Routes.kt index fbfe44a..1c6f4de 100644 --- a/samples/ksp-sample/src/main/kotlin/dev/programadorthi/routing/sample/Routes.kt +++ b/samples/ksp-sample/src/main/kotlin/dev/programadorthi/routing/sample/Routes.kt @@ -1,8 +1,14 @@ package dev.programadorthi.routing.sample +import dev.programadorthi.routing.annotation.Body import dev.programadorthi.routing.annotation.Path import dev.programadorthi.routing.annotation.Route +data class User( + val id: Int, + val name: String, +) + @Route("/path") fun execute() { println(">>>> I'm routing") @@ -43,6 +49,16 @@ fun regex2(number: Int) { println(">>>> Routing with regex to number: $number") } +@Route("/with-body") +fun withBody(@Body user: User) { + println(">>>> with body $user") +} + +@Route("/with-null-body") +fun withNullBody(@Body user: User?) { + println(">>>> null body $user") +} + class Routes { //@Route("/path") fun run() {