Skip to content

Commit

Permalink
Body parameter support
Browse files Browse the repository at this point in the history
  • Loading branch information
programadorthi committed Nov 13, 2024
1 parent 9de7294 commit ab6f330
Show file tree
Hide file tree
Showing 4 changed files with 87 additions and 22 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package dev.programadorthi.routing.annotation

@Target(AnnotationTarget.VALUE_PARAMETER)
public annotation class Body
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand All @@ -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'"
}
Expand All @@ -83,19 +83,53 @@ private class RoutingProcessor(
"@Route using regex can't be named"
}

val parameters = mutableListOf<String>()
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<List<*>>()) {
"Class declaration not found to List<String>?"
Expand All @@ -113,7 +147,11 @@ private class RoutingProcessor(
check(paramType.isMarkedNullable) {
"Tailcard list must be nullable as List<String>?"
}
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
}

Expand All @@ -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()
}

Expand Down
11 changes: 9 additions & 2 deletions samples/ksp-sample/src/main/kotlin/Main.kt
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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)
}
Original file line number Diff line number Diff line change
@@ -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")
Expand Down Expand Up @@ -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() {
Expand Down

0 comments on commit ab6f330

Please sign in to comment.