@@ -28,11 +28,13 @@ import com.squareup.kotlinpoet.FileSpec
28
28
import com.squareup.kotlinpoet.FunSpec
29
29
import com.squareup.kotlinpoet.KModifier
30
30
import com.squareup.kotlinpoet.MemberName
31
+ import com.squareup.kotlinpoet.ksp.toClassName
31
32
import com.squareup.kotlinpoet.ksp.toTypeName
32
33
import com.squareup.kotlinpoet.ksp.writeTo
33
34
import dev.programadorthi.routing.annotation.Body
34
35
import dev.programadorthi.routing.annotation.Path
35
36
import dev.programadorthi.routing.annotation.Route
37
+ import dev.programadorthi.routing.annotation.TypeSafeRoute
36
38
37
39
public class RoutingProcessorProvider : SymbolProcessorProvider {
38
40
override fun create (environment : SymbolProcessorEnvironment ): SymbolProcessor {
@@ -66,8 +68,9 @@ private class RoutingProcessor(
66
68
.addModifiers(KModifier .INTERNAL )
67
69
.receiver(route)
68
70
69
- resolver
70
- .getSymbolsWithAnnotation(Route ::class .java.name)
71
+ val routes = resolver.getSymbolsWithAnnotation(Route ::class .java.name)
72
+ val typedRoutes = resolver.getSymbolsWithAnnotation(TypeSafeRoute ::class .java.name)
73
+ (routes + typedRoutes)
71
74
.filterIsInstance<KSDeclaration >()
72
75
.forEach { symbol ->
73
76
symbol.transform(ksFiles, configureSpec, resolver)
@@ -92,16 +95,18 @@ private class RoutingProcessor(
92
95
" $qualifiedName must not be private"
93
96
}
94
97
containingFile?.let (ksFiles::add)
95
- val routeAnnotation = checkNotNull(getAnnotationsByType(Route ::class ).firstOrNull()) {
96
- " Invalid state because is missing @Route to '$qualifiedName '"
98
+ val routeAnnotation = getAnnotationsByType(Route ::class ).firstOrNull()
99
+ val typedAnnotation = getAnnotationsByType(TypeSafeRoute ::class ).firstOrNull()
100
+ check(routeAnnotation == null || typedAnnotation == null ) {
101
+ " @Route and @TypeSafeRoute can't be used together. Choose one or other"
97
102
}
98
103
when (this ) {
99
104
is KSFunctionDeclaration -> {
100
105
check(functionKind == FunctionKind .TOP_LEVEL ) {
101
106
" $qualifiedName must be a top level fun"
102
107
}
103
108
logger.info(" >>>> transforming fun: $qualifiedName " )
104
- wrapFunctionWithHandle(routeAnnotation, qualifiedName, configureSpec, resolver, null )
109
+ wrapFunctionWithHandle(routeAnnotation ? : typedAnnotation , qualifiedName, configureSpec, resolver, null )
105
110
}
106
111
107
112
is KSClassDeclaration -> {
@@ -120,9 +125,12 @@ private class RoutingProcessor(
120
125
.filterIsInstance<KSFunctionDeclaration >()
121
126
.filter { func -> func.simpleName.asString() == CONSTRUCTOR_NAME }
122
127
.forEach { constructor ->
123
- val annotation = constructor .getAnnotationsByType(Route ::class ).firstOrNull() ? : routeAnnotation
128
+ val rAnnotation =
129
+ constructor .getAnnotationsByType(Route ::class ).firstOrNull() ? : routeAnnotation
130
+ val tAnnotation =
131
+ constructor .getAnnotationsByType(TypeSafeRoute ::class ).firstOrNull() ? : typedAnnotation
124
132
constructor .wrapFunctionWithHandle(
125
- annotation ,
133
+ rAnnotation ? : tAnnotation ,
126
134
qualifiedName,
127
135
configureSpec,
128
136
resolver,
@@ -136,77 +144,116 @@ private class RoutingProcessor(
136
144
}
137
145
138
146
private fun KSFunctionDeclaration.wrapFunctionWithHandle (
139
- routeAnnotation : Route ,
147
+ annotation : Any? ,
140
148
qualifiedName : String ,
141
149
configureSpec : FunSpec .Builder ,
142
150
resolver : Resolver ,
143
151
classKind : ClassKind ? ,
144
152
) {
145
- val isRegexRoute = routeAnnotation.regex.isNotBlank()
146
- check(isRegexRoute || routeAnnotation.path.isNotBlank()) {
147
- " @Route requires a path or a regex"
148
- }
149
- check(! isRegexRoute || routeAnnotation.name.isBlank()) {
150
- " @Route having regex can't be named"
151
- }
153
+ val routeAnnotation = annotation as ? Route
154
+ val typedAnnotation = annotation as ? TypeSafeRoute
152
155
153
156
val memberName = when {
154
157
annotations.any { it.shortName.asString() == " Composable" } -> composable
155
158
classKind != null -> screen
159
+ typedAnnotation != null -> resourceHandle
156
160
else -> handle
157
161
}
158
162
163
+ val codeBlock = when {
164
+ typedAnnotation != null -> {
165
+ val type = annotations
166
+ .filter { it.shortName.asString() == " TypeSafeRoute" }
167
+ .mapNotNull { it.arguments.find { it.name?.asString() == " type" }?.value as ? KSType }
168
+ .firstOrNull() ? : error(" '$qualifiedName ' should be annotated with @TypeSafeRoute" )
169
+ when {
170
+ typedAnnotation.method.isBlank() ->
171
+ configureSpec
172
+ .beginControlFlow(" %M<%T>" , memberName, type.toClassName())
173
+
174
+ else ->
175
+ configureSpec
176
+ .beginControlFlow(
177
+ " %M<%T>(method = %M(value = \" ${typedAnnotation.method} \" ))" ,
178
+ memberName,
179
+ type.toClassName(),
180
+ routeMethod
181
+ )
182
+ }
183
+ generateHandleBody(false , typedAnnotation, resolver, qualifiedName, classKind, type)
184
+ }
185
+
186
+ routeAnnotation != null -> {
187
+ val isRegexRoute = routeAnnotation.regex.isNotBlank()
188
+ routeAnnotation.setupAnnotation(isRegexRoute, configureSpec, memberName)
189
+ generateHandleBody(isRegexRoute, routeAnnotation, resolver, qualifiedName, classKind, null )
190
+ }
191
+
192
+ else -> error(" '$qualifiedName ' should have been annotated with @Route or @TypeSafeRoute" )
193
+ }
194
+
195
+ configureSpec
196
+ .addCode(codeBlock)
197
+ .endControlFlow()
198
+ }
199
+
200
+ private fun Route.setupAnnotation (
201
+ isRegexRoute : Boolean ,
202
+ configureSpec : FunSpec .Builder ,
203
+ memberName : MemberName
204
+ ) {
205
+ check(isRegexRoute || path.isNotBlank()) {
206
+ " @Route requires a path or a regex"
207
+ }
208
+ check(! isRegexRoute || name.isBlank()) {
209
+ " @Route having regex can't be named"
210
+ }
211
+
159
212
if (isRegexRoute) {
160
- if (routeAnnotation. method.isBlank()) {
213
+ if (method.isBlank()) {
161
214
configureSpec
162
215
.beginControlFlow(
163
216
" %M(path = %T(%S))" ,
164
217
memberName,
165
218
Regex ::class ,
166
- routeAnnotation. regex
219
+ regex
167
220
)
168
221
} else {
169
- val template =
170
- """ %M(path = %T(%S), method = %M(value = "${routeAnnotation.method} "))"""
222
+ val template = """ %M(path = %T(%S), method = %M(value = "$method "))"""
171
223
configureSpec
172
224
.beginControlFlow(
173
225
template,
174
226
memberName,
175
227
Regex ::class ,
176
- routeAnnotation. regex,
228
+ regex,
177
229
routeMethod
178
230
)
179
231
}
180
232
} else {
181
233
val named = when {
182
- routeAnnotation. name.isBlank() -> " name = null"
183
- else -> """ name = "${routeAnnotation. name} """"
234
+ name.isBlank() -> " name = null"
235
+ else -> """ name = "$name """"
184
236
}
185
237
logger.info(" >>>> transforming -> name: $named and member: $memberName " )
186
- if (routeAnnotation. method.isBlank()) {
238
+ if (method.isBlank()) {
187
239
configureSpec
188
- .beginControlFlow(" %M(path = %S, $named )" , memberName, routeAnnotation. path)
240
+ .beginControlFlow(" %M(path = %S, $named )" , memberName, path)
189
241
} else {
190
242
val template =
191
- """ %M(path = %S, $named , method = %M(value = "${routeAnnotation. method} "))"""
243
+ """ %M(path = %S, $named , method = %M(value = "$method "))"""
192
244
configureSpec
193
- .beginControlFlow(template, memberName, routeAnnotation. path, routeMethod)
245
+ .beginControlFlow(template, memberName, path, routeMethod)
194
246
}
195
247
}
196
-
197
- val codeBlock = generateHandleBody(isRegexRoute, routeAnnotation, resolver, qualifiedName, classKind)
198
-
199
- configureSpec
200
- .addCode(codeBlock)
201
- .endControlFlow()
202
248
}
203
249
204
250
private fun KSFunctionDeclaration.generateHandleBody (
205
251
isRegexRoute : Boolean ,
206
- routeAnnotation : Route ,
252
+ routeAnnotation : Any ,
207
253
resolver : Resolver ,
208
254
qualifiedName : String ,
209
255
classKind : ClassKind ? ,
256
+ safeType : KSType ? ,
210
257
): CodeBlock {
211
258
val funcBuilder = CodeBlock .builder()
212
259
val hasZeroOrOneParameter = parameters.size < 2
@@ -230,31 +277,43 @@ private class RoutingProcessor(
230
277
.indent()
231
278
}
232
279
280
+ var bodyCount = 0
233
281
for (param in parameters) {
234
282
check(param.isVararg.not ()) {
235
283
" Vararg is not supported as fun parameter"
236
284
}
285
+ check(bodyCount < 1 ) {
286
+ " Multiple parameters annotated with @Body are not supported"
287
+ }
237
288
var applied = param.tryApplyCallProperty(hasZeroOrOneParameter, funcBuilder)
238
289
if (! applied) {
239
290
applied = param.tryApplyBody(hasZeroOrOneParameter, funcBuilder)
291
+ if (applied) {
292
+ bodyCount++
293
+ }
240
294
}
241
- if (! applied && ! isRegexRoute) {
242
- applied = param.tryApplyTailCard(
243
- routePath = routeAnnotation.path,
244
- resolver = resolver,
245
- hasZeroOrOneParameter = hasZeroOrOneParameter,
246
- builder = funcBuilder,
247
- )
295
+ if (! applied && routeAnnotation is TypeSafeRoute ) {
296
+ applied = param.tryApplySafeParams(hasZeroOrOneParameter, funcBuilder, safeType)
248
297
}
249
- if (! applied) {
250
- param.tryApplyPath(
251
- isRegexRoute = isRegexRoute,
252
- routeAnnotation = routeAnnotation,
253
- qualifiedName = qualifiedName,
254
- resolver = resolver,
255
- hasZeroOrOneParameter = hasZeroOrOneParameter,
256
- builder = funcBuilder,
257
- )
298
+ if (! applied && routeAnnotation is Route ) {
299
+ if (! isRegexRoute) {
300
+ applied = param.tryApplyTailCard(
301
+ routePath = routeAnnotation.path,
302
+ resolver = resolver,
303
+ hasZeroOrOneParameter = hasZeroOrOneParameter,
304
+ builder = funcBuilder,
305
+ )
306
+ }
307
+ if (! applied) {
308
+ param.tryApplyPath(
309
+ isRegexRoute = isRegexRoute,
310
+ routeAnnotation = routeAnnotation,
311
+ qualifiedName = qualifiedName,
312
+ resolver = resolver,
313
+ hasZeroOrOneParameter = hasZeroOrOneParameter,
314
+ builder = funcBuilder,
315
+ )
316
+ }
258
317
}
259
318
}
260
319
@@ -308,6 +367,25 @@ private class RoutingProcessor(
308
367
}
309
368
}
310
369
370
+ private fun KSValueParameter.tryApplySafeParams (
371
+ hasZeroOrOneParameter : Boolean ,
372
+ builder : CodeBlock .Builder ,
373
+ safeType : KSType ? ,
374
+ ): Boolean {
375
+ val paramName = name?.asString()
376
+ val paramType = type.resolve()
377
+ check(paramType == safeType) {
378
+ " '$paramName ' has a not supported type. It must be a " +
379
+ " '${safeType?.declaration?.qualifiedName?.asString()} ', annotated with @Body or be " +
380
+ " an ApplicationCall or an ApplicationCall parameter"
381
+ }
382
+ when {
383
+ hasZeroOrOneParameter -> builder.add(TYPE_TEMPLATE , paramName, " it" , " " )
384
+ else -> builder.addStatement(TYPE_TEMPLATE , paramName, " it" , " ," )
385
+ }
386
+ return true
387
+ }
388
+
311
389
@OptIn(KspExperimental ::class )
312
390
private fun KSValueParameter.tryApplyTailCard (
313
391
routePath : String ,
@@ -454,6 +532,7 @@ private class RoutingProcessor(
454
532
private val receive = MemberName (" dev.programadorthi.routing.core.application" , " receive" )
455
533
private val receiveNullable =
456
534
MemberName (" dev.programadorthi.routing.core.application" , " receiveNullable" )
535
+ private val resourceHandle = MemberName (" dev.programadorthi.routing.resources" , " handle" )
457
536
458
537
private const val CALL_TEMPLATE = """ %L = %M%L"""
459
538
private const val CALL_PROPERTY_TEMPLATE = """ %L = %M.%L%L"""
@@ -464,6 +543,7 @@ private class RoutingProcessor(
464
543
private const val FUN_TYPE_INVOKE_START = " $FUN_TYPE_INVOKE ("
465
544
private const val PATH_TEMPLATE = """ %L = %M.parameters["%L"]%L"""
466
545
private const val TAILCARD_TEMPLATE = """ %L = %M.parameters.getAll("%L")%L"""
546
+ private const val TYPE_TEMPLATE = """ %L = %L%L"""
467
547
468
548
private const val FLAG_ROUTING_MODULE_NAME = " Routing_Module_Name"
469
549
0 commit comments