Skip to content

Commit 267c196

Browse files
Voyager routing by regex
1 parent c29b142 commit 267c196

File tree

5 files changed

+131
-21
lines changed

5 files changed

+131
-21
lines changed

integration/voyager/common/src/dev/programadorthi/routing/voyager/VoyagerRoutingBuilder.kt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,19 @@ public fun Route.screen(
3030
body: suspend PipelineContext<Unit, ApplicationCall>.() -> Screen,
3131
): Route = route(path = path, name = name, method = method) { screen(body) }
3232

33+
@KtorDsl
34+
public fun Route.screen(
35+
path: Regex,
36+
body: suspend PipelineContext<Unit, ApplicationCall>.() -> Screen,
37+
): Route = route(path = path) { screen(body) }
38+
39+
@KtorDsl
40+
public fun Route.screen(
41+
path: Regex,
42+
method: RouteMethod,
43+
body: suspend PipelineContext<Unit, ApplicationCall>.() -> Screen,
44+
): Route = route(path = path, method = method) { screen(body) }
45+
3346
@KtorDsl
3447
public fun Route.screen(body: suspend PipelineContext<Unit, ApplicationCall>.() -> Screen) {
3548
val routing = asRouting ?: error("Your route $this must have a parent Routing")

integration/voyager/common/test/dev/programadorthi/routing/voyager/Screens.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,12 @@ internal class Screen5(@Body val user: User) : Screen {
6060
invoked += "/screen-with-body" to listOf(user)
6161
}
6262
}
63+
64+
@Route(regex = "/(?<number>\\d+)")
65+
internal class Screen6(val number: Int) : Screen {
66+
67+
@Composable
68+
override fun Content() {
69+
invoked += "/(?<number>\\d+)" to listOf(number)
70+
}
71+
}

integration/voyager/common/test/dev/programadorthi/routing/voyager/VoyagerRoutingTest.kt

Lines changed: 83 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import cafe.adriel.voyager.navigator.CurrentScreen
44
import cafe.adriel.voyager.navigator.Navigator
55
import dev.programadorthi.routing.core.RouteMethod
66
import dev.programadorthi.routing.core.application.ApplicationCall
7+
import dev.programadorthi.routing.core.application.call
78
import dev.programadorthi.routing.core.application.createApplicationPlugin
89
import dev.programadorthi.routing.core.application.hooks.CallFailed
910
import dev.programadorthi.routing.core.call
@@ -17,12 +18,12 @@ import dev.programadorthi.routing.voyager.helper.FakeScreen
1718
import dev.programadorthi.routing.voyager.helper.runComposeTest
1819
import io.ktor.http.Parameters
1920
import io.ktor.http.parametersOf
20-
import kotlinx.coroutines.ExperimentalCoroutinesApi
21-
import kotlinx.coroutines.test.advanceTimeBy
2221
import kotlin.test.Test
2322
import kotlin.test.assertEquals
2423
import kotlin.test.assertIs
2524
import kotlin.test.assertNotNull
25+
import kotlinx.coroutines.ExperimentalCoroutinesApi
26+
import kotlinx.coroutines.test.advanceTimeBy
2627

2728
@OptIn(ExperimentalCoroutinesApi::class)
2829
internal class VoyagerRoutingTest {
@@ -150,10 +151,10 @@ internal class VoyagerRoutingTest {
150151
// THEN
151152
assertNotNull(result)
152153
assertNotNull(exception)
153-
assertEquals("/path", "${result?.uri}")
154-
assertEquals("", "${result?.name}")
155-
assertEquals(RouteMethod.Empty, result?.routeMethod)
156-
assertEquals(Parameters.Empty, result?.parameters)
154+
assertEquals("/path", result.uri)
155+
assertEquals("", result.name)
156+
assertEquals(RouteMethod.Empty, result.routeMethod)
157+
assertEquals(Parameters.Empty, result.parameters)
157158
assertIs<IllegalStateException>(exception)
158159
assertEquals(
159160
"Voyager needs a stack route method to work. You called a screen /path using " +
@@ -374,9 +375,9 @@ internal class VoyagerRoutingTest {
374375
VoyagerRouting(
375376
routing = routing,
376377
initialScreen =
377-
FakeScreen().apply {
378-
content = "I am the initial screen"
379-
},
378+
FakeScreen().apply {
379+
content = "I am the initial screen"
380+
},
380381
) { nav ->
381382
navigator = nav
382383
CurrentScreen()
@@ -444,9 +445,9 @@ internal class VoyagerRoutingTest {
444445
VoyagerRouting(
445446
routing = routing,
446447
initialScreen =
447-
FakeScreen().apply {
448-
content = "I am the initial screen"
449-
},
448+
FakeScreen().apply {
449+
content = "I am the initial screen"
450+
},
450451
) { nav ->
451452
navigator = nav
452453
CurrentScreen()
@@ -496,9 +497,9 @@ internal class VoyagerRoutingTest {
496497
VoyagerRouting(
497498
routing = routing,
498499
initialScreen =
499-
FakeScreen().apply {
500-
content = "I am the initial screen"
501-
},
500+
FakeScreen().apply {
501+
content = "I am the initial screen"
502+
},
502503
) { nav ->
503504
navigator = nav
504505
CurrentScreen()
@@ -528,4 +529,71 @@ internal class VoyagerRoutingTest {
528529
// THEN
529530
assertEquals(parametersOf("key" to listOf("value")), firstPushedScreen?.parameters)
530531
}
532+
533+
@Test
534+
fun shouldNavigateByRegex() =
535+
runComposeTest { coroutineContext, composition, clock ->
536+
// GIVEN
537+
val fakeScreen = FakeScreen()
538+
539+
val routing =
540+
routing(parentCoroutineContext = coroutineContext) {
541+
screen(path = Regex("/(?<number>\\d+)")) {
542+
fakeScreen.apply {
543+
content = "Hey, I am the called screen with number ${call.parameters["number"]}"
544+
}
545+
}
546+
}
547+
548+
composition.setContent {
549+
VoyagerRouting(
550+
routing = routing,
551+
initialScreen = FakeScreen(),
552+
)
553+
}
554+
555+
// WHEN
556+
routing.push(path = "/123")
557+
advanceTimeBy(99) // Ask for routing
558+
clock.sendFrame(0L) // Ask for recomposition
559+
560+
// THEN
561+
assertEquals("Hey, I am the called screen with number 123", fakeScreen.composed)
562+
}
563+
564+
@Test
565+
fun shouldNavigateByRegexWithMultipleParameters() =
566+
runComposeTest { coroutineContext, composition, clock ->
567+
// GIVEN
568+
val fakeScreen = FakeScreen()
569+
570+
val routing =
571+
routing(parentCoroutineContext = coroutineContext) {
572+
route(path = Regex("/(?<number>\\d+)")) {
573+
screen(path = Regex("(?<user>\\w+)/(?<login>.+)")) {
574+
fakeScreen.apply {
575+
content = "Hey, I am the called screen with ${call.parameters}"
576+
}
577+
}
578+
}
579+
}
580+
581+
composition.setContent {
582+
VoyagerRouting(
583+
routing = routing,
584+
initialScreen = FakeScreen(),
585+
)
586+
}
587+
588+
// WHEN
589+
routing.push(path = "/456/qwe/rty")
590+
advanceTimeBy(99) // Ask for routing
591+
clock.sendFrame(0L) // Ask for recomposition
592+
593+
// THEN
594+
assertEquals(
595+
"Hey, I am the called screen with Parameters [number=[456], user=[qwe], login=[rty]]",
596+
fakeScreen.composed
597+
)
598+
}
531599
}

integration/voyager/jvm/test/dev/programadorthi/routing/voyager/VoyagerRoutingByAnnotationsTest.kt

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,4 +151,29 @@ internal class VoyagerRoutingByAnnotationsTest {
151151
assertEquals(listOf(body), invoked.remove("/screen-with-body"))
152152
}
153153

154+
@Test
155+
fun shouldHandleScreenRegex() =
156+
runComposeTest { coroutineContext, composition, clock ->
157+
// GIVEN
158+
val routing =
159+
routing(parentCoroutineContext = coroutineContext) {
160+
configure()
161+
}
162+
163+
composition.setContent {
164+
VoyagerRouting(
165+
routing = routing,
166+
initialScreen = FakeScreen(),
167+
)
168+
}
169+
170+
// WHEN
171+
routing.push(path = "/123")
172+
advanceTimeBy(99) // Ask for routing
173+
clock.sendFrame(0L) // Ask for recomposition
174+
175+
// THEN
176+
assertEquals(listOf(123), invoked.remove("/(?<number>\\d+)"))
177+
}
178+
154179
}

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

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -150,18 +150,13 @@ private class RoutingProcessor(
150150
"@Route having regex can't be named"
151151
}
152152

153-
val isScreen = classKind != null
154153
val memberName = when {
155154
annotations.any { it.shortName.asString() == "Composable" } -> composable
156-
isScreen -> screen
155+
classKind != null -> screen
157156
else -> handle
158157
}
159158

160159
if (isRegexRoute) {
161-
check(!isScreen) {
162-
// TODO: Add regex support to composable handle
163-
"$qualifiedName has @Route(regex = ...) that cannot be applied to @Composable or Voyager Screen"
164-
}
165160
if (routeAnnotation.method.isBlank()) {
166161
configureSpec
167162
.beginControlFlow(

0 commit comments

Comments
 (0)