From 267c196070675d743d45227d380661baa5e48fe3 Mon Sep 17 00:00:00 2001 From: Thiago Santos Date: Thu, 21 Nov 2024 12:46:37 -0300 Subject: [PATCH] Voyager routing by regex --- .../routing/voyager/VoyagerRoutingBuilder.kt | 13 +++ .../programadorthi/routing/voyager/Screens.kt | 9 ++ .../routing/voyager/VoyagerRoutingTest.kt | 98 ++++++++++++++++--- .../VoyagerRoutingByAnnotationsTest.kt | 25 +++++ .../routing/ksp/RoutingProcessor.kt | 7 +- 5 files changed, 131 insertions(+), 21 deletions(-) diff --git a/integration/voyager/common/src/dev/programadorthi/routing/voyager/VoyagerRoutingBuilder.kt b/integration/voyager/common/src/dev/programadorthi/routing/voyager/VoyagerRoutingBuilder.kt index b747d49..dc676d5 100644 --- a/integration/voyager/common/src/dev/programadorthi/routing/voyager/VoyagerRoutingBuilder.kt +++ b/integration/voyager/common/src/dev/programadorthi/routing/voyager/VoyagerRoutingBuilder.kt @@ -30,6 +30,19 @@ public fun Route.screen( body: suspend PipelineContext.() -> Screen, ): Route = route(path = path, name = name, method = method) { screen(body) } +@KtorDsl +public fun Route.screen( + path: Regex, + body: suspend PipelineContext.() -> Screen, +): Route = route(path = path) { screen(body) } + +@KtorDsl +public fun Route.screen( + path: Regex, + method: RouteMethod, + body: suspend PipelineContext.() -> Screen, +): Route = route(path = path, method = method) { screen(body) } + @KtorDsl public fun Route.screen(body: suspend PipelineContext.() -> Screen) { val routing = asRouting ?: error("Your route $this must have a parent Routing") diff --git a/integration/voyager/common/test/dev/programadorthi/routing/voyager/Screens.kt b/integration/voyager/common/test/dev/programadorthi/routing/voyager/Screens.kt index 2a5bf92..7491f44 100644 --- a/integration/voyager/common/test/dev/programadorthi/routing/voyager/Screens.kt +++ b/integration/voyager/common/test/dev/programadorthi/routing/voyager/Screens.kt @@ -60,3 +60,12 @@ internal class Screen5(@Body val user: User) : Screen { invoked += "/screen-with-body" to listOf(user) } } + +@Route(regex = "/(?\\d+)") +internal class Screen6(val number: Int) : Screen { + + @Composable + override fun Content() { + invoked += "/(?\\d+)" to listOf(number) + } +} diff --git a/integration/voyager/common/test/dev/programadorthi/routing/voyager/VoyagerRoutingTest.kt b/integration/voyager/common/test/dev/programadorthi/routing/voyager/VoyagerRoutingTest.kt index 9f0b7d9..e0f8132 100644 --- a/integration/voyager/common/test/dev/programadorthi/routing/voyager/VoyagerRoutingTest.kt +++ b/integration/voyager/common/test/dev/programadorthi/routing/voyager/VoyagerRoutingTest.kt @@ -4,6 +4,7 @@ import cafe.adriel.voyager.navigator.CurrentScreen import cafe.adriel.voyager.navigator.Navigator import dev.programadorthi.routing.core.RouteMethod import dev.programadorthi.routing.core.application.ApplicationCall +import dev.programadorthi.routing.core.application.call import dev.programadorthi.routing.core.application.createApplicationPlugin import dev.programadorthi.routing.core.application.hooks.CallFailed import dev.programadorthi.routing.core.call @@ -17,12 +18,12 @@ import dev.programadorthi.routing.voyager.helper.FakeScreen import dev.programadorthi.routing.voyager.helper.runComposeTest import io.ktor.http.Parameters import io.ktor.http.parametersOf -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.advanceTimeBy import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertIs import kotlin.test.assertNotNull +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.advanceTimeBy @OptIn(ExperimentalCoroutinesApi::class) internal class VoyagerRoutingTest { @@ -150,10 +151,10 @@ internal class VoyagerRoutingTest { // THEN assertNotNull(result) assertNotNull(exception) - assertEquals("/path", "${result?.uri}") - assertEquals("", "${result?.name}") - assertEquals(RouteMethod.Empty, result?.routeMethod) - assertEquals(Parameters.Empty, result?.parameters) + assertEquals("/path", result.uri) + assertEquals("", result.name) + assertEquals(RouteMethod.Empty, result.routeMethod) + assertEquals(Parameters.Empty, result.parameters) assertIs(exception) assertEquals( "Voyager needs a stack route method to work. You called a screen /path using " + @@ -374,9 +375,9 @@ internal class VoyagerRoutingTest { VoyagerRouting( routing = routing, initialScreen = - FakeScreen().apply { - content = "I am the initial screen" - }, + FakeScreen().apply { + content = "I am the initial screen" + }, ) { nav -> navigator = nav CurrentScreen() @@ -444,9 +445,9 @@ internal class VoyagerRoutingTest { VoyagerRouting( routing = routing, initialScreen = - FakeScreen().apply { - content = "I am the initial screen" - }, + FakeScreen().apply { + content = "I am the initial screen" + }, ) { nav -> navigator = nav CurrentScreen() @@ -496,9 +497,9 @@ internal class VoyagerRoutingTest { VoyagerRouting( routing = routing, initialScreen = - FakeScreen().apply { - content = "I am the initial screen" - }, + FakeScreen().apply { + content = "I am the initial screen" + }, ) { nav -> navigator = nav CurrentScreen() @@ -528,4 +529,71 @@ internal class VoyagerRoutingTest { // THEN assertEquals(parametersOf("key" to listOf("value")), firstPushedScreen?.parameters) } + + @Test + fun shouldNavigateByRegex() = + runComposeTest { coroutineContext, composition, clock -> + // GIVEN + val fakeScreen = FakeScreen() + + val routing = + routing(parentCoroutineContext = coroutineContext) { + screen(path = Regex("/(?\\d+)")) { + fakeScreen.apply { + content = "Hey, I am the called screen with number ${call.parameters["number"]}" + } + } + } + + composition.setContent { + VoyagerRouting( + routing = routing, + initialScreen = FakeScreen(), + ) + } + + // WHEN + routing.push(path = "/123") + advanceTimeBy(99) // Ask for routing + clock.sendFrame(0L) // Ask for recomposition + + // THEN + assertEquals("Hey, I am the called screen with number 123", fakeScreen.composed) + } + + @Test + fun shouldNavigateByRegexWithMultipleParameters() = + runComposeTest { coroutineContext, composition, clock -> + // GIVEN + val fakeScreen = FakeScreen() + + val routing = + routing(parentCoroutineContext = coroutineContext) { + route(path = Regex("/(?\\d+)")) { + screen(path = Regex("(?\\w+)/(?.+)")) { + fakeScreen.apply { + content = "Hey, I am the called screen with ${call.parameters}" + } + } + } + } + + composition.setContent { + VoyagerRouting( + routing = routing, + initialScreen = FakeScreen(), + ) + } + + // WHEN + routing.push(path = "/456/qwe/rty") + advanceTimeBy(99) // Ask for routing + clock.sendFrame(0L) // Ask for recomposition + + // THEN + assertEquals( + "Hey, I am the called screen with Parameters [number=[456], user=[qwe], login=[rty]]", + fakeScreen.composed + ) + } } diff --git a/integration/voyager/jvm/test/dev/programadorthi/routing/voyager/VoyagerRoutingByAnnotationsTest.kt b/integration/voyager/jvm/test/dev/programadorthi/routing/voyager/VoyagerRoutingByAnnotationsTest.kt index a3e4708..053b159 100644 --- a/integration/voyager/jvm/test/dev/programadorthi/routing/voyager/VoyagerRoutingByAnnotationsTest.kt +++ b/integration/voyager/jvm/test/dev/programadorthi/routing/voyager/VoyagerRoutingByAnnotationsTest.kt @@ -151,4 +151,29 @@ internal class VoyagerRoutingByAnnotationsTest { assertEquals(listOf(body), invoked.remove("/screen-with-body")) } + @Test + fun shouldHandleScreenRegex() = + runComposeTest { coroutineContext, composition, clock -> + // GIVEN + val routing = + routing(parentCoroutineContext = coroutineContext) { + configure() + } + + composition.setContent { + VoyagerRouting( + routing = routing, + initialScreen = FakeScreen(), + ) + } + + // WHEN + routing.push(path = "/123") + advanceTimeBy(99) // Ask for routing + clock.sendFrame(0L) // Ask for recomposition + + // THEN + assertEquals(listOf(123), invoked.remove("/(?\\d+)")) + } + } diff --git a/ksp/core-processor/jvm/src/dev/programadorthi/routing/ksp/RoutingProcessor.kt b/ksp/core-processor/jvm/src/dev/programadorthi/routing/ksp/RoutingProcessor.kt index 58327ca..988bf25 100644 --- a/ksp/core-processor/jvm/src/dev/programadorthi/routing/ksp/RoutingProcessor.kt +++ b/ksp/core-processor/jvm/src/dev/programadorthi/routing/ksp/RoutingProcessor.kt @@ -150,18 +150,13 @@ private class RoutingProcessor( "@Route having regex can't be named" } - val isScreen = classKind != null val memberName = when { annotations.any { it.shortName.asString() == "Composable" } -> composable - isScreen -> screen + classKind != null -> screen else -> handle } if (isRegexRoute) { - check(!isScreen) { - // TODO: Add regex support to composable handle - "$qualifiedName has @Route(regex = ...) that cannot be applied to @Composable or Voyager Screen" - } if (routeAnnotation.method.isBlank()) { configureSpec .beginControlFlow(