Skip to content

Commit ab6f330

Browse files
Body parameter support
1 parent 9de7294 commit ab6f330

File tree

4 files changed

+87
-22
lines changed
  • ksp-annotations/common/src/dev/programadorthi/routing/annotation
  • ksp-processor/jvm/src/dev/programadorthi/routing/ksp
  • samples/ksp-sample/src/main/kotlin

4 files changed

+87
-22
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
package dev.programadorthi.routing.annotation
2+
3+
@Target(AnnotationTarget.VALUE_PARAMETER)
4+
public annotation class Body

ksp-processor/jvm/src/dev/programadorthi/routing/ksp/RoutingProcessor.kt

Lines changed: 58 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,13 @@ import com.google.devtools.ksp.symbol.KSAnnotated
1616
import com.google.devtools.ksp.symbol.KSFunctionDeclaration
1717
import com.google.devtools.ksp.symbol.KSType
1818
import com.google.devtools.ksp.symbol.Visibility
19+
import com.squareup.kotlinpoet.CodeBlock
1920
import com.squareup.kotlinpoet.FileSpec
2021
import com.squareup.kotlinpoet.FunSpec
2122
import com.squareup.kotlinpoet.KModifier
2223
import com.squareup.kotlinpoet.MemberName
2324
import com.squareup.kotlinpoet.ksp.writeTo
25+
import dev.programadorthi.routing.annotation.Body
2426
import dev.programadorthi.routing.annotation.Path
2527
import dev.programadorthi.routing.annotation.Route
2628

@@ -47,6 +49,8 @@ private class RoutingProcessor(
4749
invoked = true
4850

4951
val call = MemberName("dev.programadorthi.routing.core.application", "call")
52+
val receive = MemberName("dev.programadorthi.routing.core.application", "receive")
53+
val receiveNullable = MemberName("dev.programadorthi.routing.core.application", "receiveNullable")
5054
val handle = MemberName("dev.programadorthi.routing.core", "handle")
5155

5256
val configureSpec = FunSpec
@@ -68,10 +72,6 @@ private class RoutingProcessor(
6872
"$qualifiedName fun must not be private"
6973
}
7074

71-
check(func.packageName.asString().isNotBlank()) {
72-
"Top level fun '$qualifiedName' must have a package"
73-
}
74-
7575
val routeAnnotation = checkNotNull(func.getAnnotationsByType(Route::class).firstOrNull()) {
7676
"Invalid state because a @Route was not found to '$qualifiedName'"
7777
}
@@ -83,19 +83,53 @@ private class RoutingProcessor(
8383
"@Route using regex can't be named"
8484
}
8585

86-
val parameters = mutableListOf<String>()
86+
val named = when {
87+
routeAnnotation.name.isBlank() -> "name = null"
88+
else -> """name = "${routeAnnotation.name}""""
89+
}
90+
if (isRegexRoute) {
91+
configureSpec.beginControlFlow("%M(%T(%S))", handle, Regex::class, routeAnnotation.regex)
92+
} else {
93+
configureSpec.beginControlFlow("%M(path = %S, $named)", handle, routeAnnotation.path)
94+
}
95+
96+
val funcMember = MemberName(func.packageName.asString(), func.simpleName.asString())
97+
val funcBuilder = CodeBlock.builder()
98+
val isMultipleParameters = func.parameters.size > 1
99+
if (isMultipleParameters) {
100+
funcBuilder
101+
.addStatement("%M(", funcMember)
102+
.indent()
103+
} else {
104+
funcBuilder.add("%M(", funcMember)
105+
}
87106

88107
for (param in func.parameters) {
89108
check(param.isVararg.not()) {
90109
"Vararg is not supported as fun parameter"
91110
}
92111
val paramName = param.name?.asString()
112+
val paramType = param.type.resolve()
113+
val body = param
114+
.getAnnotationsByType(Body::class)
115+
.firstOrNull()
116+
if (body != null) {
117+
val member = when {
118+
paramType.isMarkedNullable -> receiveNullable
119+
else -> receive
120+
}
121+
when {
122+
isMultipleParameters -> funcBuilder.addStatement("$paramName = %M.%M(),", call, member)
123+
else -> funcBuilder.add("$paramName = %M.%M()", call, member)
124+
}
125+
continue
126+
}
127+
93128
val customName = param
94129
.getAnnotationsByType(Path::class)
95130
.firstOrNull()
96131
?.value
97132
?: paramName
98-
val paramType = param.type.resolve()
99133
if (!isRegexRoute && routeAnnotation.path.contains("{$customName...}")) {
100134
val listDeclaration = checkNotNull(resolver.getClassDeclarationByName<List<*>>()) {
101135
"Class declaration not found to List<String>?"
@@ -113,7 +147,11 @@ private class RoutingProcessor(
113147
check(paramType.isMarkedNullable) {
114148
"Tailcard list must be nullable as List<String>?"
115149
}
116-
parameters += """$paramName = %M.parameters.getAll("$customName")"""
150+
151+
when {
152+
isMultipleParameters -> funcBuilder.addStatement("""$paramName = %M.parameters.getAll("$customName"),""", call)
153+
else -> funcBuilder.add("""$paramName = %M.parameters.getAll("$customName")""", call)
154+
}
117155
continue
118156
}
119157

@@ -124,26 +162,26 @@ private class RoutingProcessor(
124162
"'$qualifiedName' has parameter '$paramName' that is not declared as path parameter {$customName}"
125163
}
126164
val parsed = """$paramName = %M.parameters["$customName"]"""
127-
parameters += when {
165+
val statement = when {
128166
isOptional -> optionalParse(paramType, resolver, parsed)
129167
else -> requiredParse(paramType, resolver, parsed)
130168
}
169+
when {
170+
isMultipleParameters -> funcBuilder.addStatement("$statement,", call)
171+
else -> funcBuilder.add(statement, call)
172+
}
131173
}
132174

133-
val calls = Array(size = parameters.size) { call }
134-
val params = parameters.joinToString(prefix = "(", postfix = ")") { "\n$it" }
135-
val named = when {
136-
routeAnnotation.name.isBlank() -> "name = null"
137-
else -> """name = "${routeAnnotation.name}""""
175+
if (isMultipleParameters) {
176+
funcBuilder
177+
.unindent()
178+
.addStatement(")")
179+
} else {
180+
funcBuilder.addStatement(")")
138181
}
139182

140-
with(configureSpec) {
141-
if (isRegexRoute) {
142-
beginControlFlow("""%M(%T(%S))""", handle, Regex::class, routeAnnotation.regex)
143-
} else {
144-
beginControlFlow("""%M(path = %S, $named)""", handle, routeAnnotation.path)
145-
}
146-
}.addStatement("""$qualifiedName$params""", *calls)
183+
configureSpec
184+
.addCode(funcBuilder.build())
147185
.endControlFlow()
148186
}
149187

samples/ksp-sample/src/main/kotlin/Main.kt

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import dev.programadorthi.routing.core.call
2+
import dev.programadorthi.routing.core.callWithBody
23
import dev.programadorthi.routing.core.routing
34
import dev.programadorthi.routing.generated.configure
5+
import dev.programadorthi.routing.sample.User
46
import io.ktor.http.parametersOf
5-
import kotlinx.coroutines.delay
67
import kotlin.random.Random
7-
8+
import kotlinx.coroutines.delay
89

910
suspend fun main() {
1011
val router = routing {
@@ -31,4 +32,10 @@ suspend fun main() {
3132
delay(500)
3233
router.call(uri = "/456") // regex2
3334
delay(500)
35+
router.callWithBody(uri = "/with-body", body = User(id = 456, name = "With Body"))
36+
delay(500)
37+
router.call(uri = "/with-null-body")
38+
delay(500)
39+
router.callWithBody(uri = "/with-null-body", body = User(id = 789, name = "No null Body"))
40+
delay(500)
3441
}

samples/ksp-sample/src/main/kotlin/dev/programadorthi/routing/sample/Routes.kt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
11
package dev.programadorthi.routing.sample
22

3+
import dev.programadorthi.routing.annotation.Body
34
import dev.programadorthi.routing.annotation.Path
45
import dev.programadorthi.routing.annotation.Route
56

7+
data class User(
8+
val id: Int,
9+
val name: String,
10+
)
11+
612
@Route("/path")
713
fun execute() {
814
println(">>>> I'm routing")
@@ -43,6 +49,16 @@ fun regex2(number: Int) {
4349
println(">>>> Routing with regex to number: $number")
4450
}
4551

52+
@Route("/with-body")
53+
fun withBody(@Body user: User) {
54+
println(">>>> with body $user")
55+
}
56+
57+
@Route("/with-null-body")
58+
fun withNullBody(@Body user: User?) {
59+
println(">>>> null body $user")
60+
}
61+
4662
class Routes {
4763
//@Route("/path")
4864
fun run() {

0 commit comments

Comments
 (0)