From c29b142ae5593b4209468de5f35cddeaf4e46340 Mon Sep 17 00:00:00 2001 From: Thiago Santos Date: Thu, 21 Nov 2024 12:36:43 -0300 Subject: [PATCH] Compose routing by regex --- .../routing/compose/ComposeRoutingBuilder.kt | 13 ++++ .../routing/compose/Composables.kt | 4 +- .../routing/compose/ComposeRoutingTest.kt | 76 +++++++++++++++++++ .../ComposeRoutingByAnnotationsTest.kt | 3 - .../routing/ksp/RoutingProcessor.kt | 17 ++--- 5 files changed, 99 insertions(+), 14 deletions(-) diff --git a/integration/compose/common/src/dev/programadorthi/routing/compose/ComposeRoutingBuilder.kt b/integration/compose/common/src/dev/programadorthi/routing/compose/ComposeRoutingBuilder.kt index c1135a0..d7afcd0 100644 --- a/integration/compose/common/src/dev/programadorthi/routing/compose/ComposeRoutingBuilder.kt +++ b/integration/compose/common/src/dev/programadorthi/routing/compose/ComposeRoutingBuilder.kt @@ -32,6 +32,19 @@ public fun Route.composable( body: @Composable PipelineContext.() -> Unit, ): Route = route(path = path, name = name, method = method) { composable(body) } +@KtorDsl +public fun Route.composable( + path: Regex, + body: @Composable PipelineContext.() -> Unit, +): Route = route(path = path) { composable(body) } + +@KtorDsl +public fun Route.composable( + path: Regex, + method: RouteMethod, + body: @Composable PipelineContext.() -> Unit, +): Route = route(path = path, method = method) { composable(body) } + @KtorDsl public fun Route.composable(body: @Composable PipelineContext.() -> Unit) { val routing = asRouting ?: error("Your route $this must have a parent Routing") diff --git a/integration/compose/common/test/dev/programadorthi/routing/compose/Composables.kt b/integration/compose/common/test/dev/programadorthi/routing/compose/Composables.kt index 85374ab..31d8026 100644 --- a/integration/compose/common/test/dev/programadorthi/routing/compose/Composables.kt +++ b/integration/compose/common/test/dev/programadorthi/routing/compose/Composables.kt @@ -60,7 +60,7 @@ fun tailcard(param: List?) { invoked += "/tailcard/{param...}" to (param ?: emptyList()) } -/*@Route(regex = ".+/hello") +@Route(regex = ".+/hello") @Composable fun regex1() { invoked += ".+/hello" to listOf() @@ -70,7 +70,7 @@ fun regex1() { @Composable fun regex2(number: Int) { invoked += "/(?\\d+)" to listOf(number) -}*/ +} @Route("/with-body") @Composable diff --git a/integration/compose/common/test/dev/programadorthi/routing/compose/ComposeRoutingTest.kt b/integration/compose/common/test/dev/programadorthi/routing/compose/ComposeRoutingTest.kt index b7b5fe8..e79795f 100644 --- a/integration/compose/common/test/dev/programadorthi/routing/compose/ComposeRoutingTest.kt +++ b/integration/compose/common/test/dev/programadorthi/routing/compose/ComposeRoutingTest.kt @@ -446,4 +446,80 @@ class ComposeRoutingTest { // THEN assertNull(poppedMessage, "Pop result should be cleared after other routing call") } + + @Test + fun shouldComposeByRegex() = + runComposeTest { coroutineContext, composition, clock -> + // GIVEN + val fakeContent = FakeContent() + + val routing = + routing(parentCoroutineContext = coroutineContext) { + composable(path = "/initial") { + fakeContent.content = "I'm the initial content" + fakeContent.Composable() + } + composable(path = Regex("/(?\\d+)")) { + fakeContent.content = "I'm the regex based content with ${call.parameters["number"]}" + fakeContent.Composable() + } + } + + composition.setContent { + Routing( + routing = routing, + startUri = "/initial", + ) + } + + // WHEN + advanceTimeBy(99) // Ask for start uri routing + clock.sendFrame(0L) // Ask for recomposition + + routing.push(path = "/123") + advanceTimeBy(99) // Ask for /path routing + clock.sendFrame(0L) // Ask for recomposition + + // THEN + assertEquals("I'm the regex based content with 123", fakeContent.result) + } + + @Test + fun shouldComposeByRegexWithMultipleParameters() = + runComposeTest { coroutineContext, composition, clock -> + // GIVEN + val fakeContent = FakeContent() + + val routing = + routing(parentCoroutineContext = coroutineContext) { + composable(path = "/initial") { + fakeContent.content = "I'm the initial content" + fakeContent.Composable() + } + route(path = Regex("/(?\\d+)")) { + composable(path = Regex("(?\\w+)/(?.+)")) { + fakeContent.content = "Regex with ${call.parameters}" + fakeContent.Composable() + } + } + } + + composition.setContent { + Routing( + routing = routing, + startUri = "/initial", + ) + } + + // WHEN + advanceTimeBy(99) // Ask for start uri routing + clock.sendFrame(0L) // Ask for recomposition + + routing.push(path = "/456/qwe/rty") + advanceTimeBy(99) // Ask for /path routing + clock.sendFrame(0L) // Ask for recomposition + + // THEN + assertEquals("Regex with Parameters [number=[456], user=[qwe], login=[rty]]", fakeContent.result) + } } diff --git a/integration/compose/jvm/test/dev/programadorthi/routing/compose/history/ComposeRoutingByAnnotationsTest.kt b/integration/compose/jvm/test/dev/programadorthi/routing/compose/history/ComposeRoutingByAnnotationsTest.kt index 04e9c09..b0d6cbe 100644 --- a/integration/compose/jvm/test/dev/programadorthi/routing/compose/history/ComposeRoutingByAnnotationsTest.kt +++ b/integration/compose/jvm/test/dev/programadorthi/routing/compose/history/ComposeRoutingByAnnotationsTest.kt @@ -18,7 +18,6 @@ import io.ktor.util.Attributes import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.advanceTimeBy import kotlin.random.Random -import kotlin.test.Ignore import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertIs @@ -267,7 +266,6 @@ class ComposeRoutingByAnnotationsTest { assertEquals(listOf("p1", "p2", "p3", "p4"), invoked.remove("/tailcard/{param...}")) } - @Ignore("Compose Regex is not supported yet") @Test fun shouldHandleByRegex() = runComposeTest { coroutineContext, composition, clock -> @@ -295,7 +293,6 @@ class ComposeRoutingByAnnotationsTest { assertEquals(listOf(), invoked.remove(".+/hello")) } - @Ignore("Compose Regex is not supported yet") @Test fun shouldHandleByRegexWithParameters() = runComposeTest { coroutineContext, composition, clock -> 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 fa745bb..58327ca 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 @@ -151,10 +151,14 @@ private class RoutingProcessor( } val isScreen = classKind != null - val isComposable = !isScreen && annotations.any { it.shortName.asString() == "Composable" } + val memberName = when { + annotations.any { it.shortName.asString() == "Composable" } -> composable + isScreen -> screen + else -> handle + } if (isRegexRoute) { - check(!isComposable && !isScreen) { + check(!isScreen) { // TODO: Add regex support to composable handle "$qualifiedName has @Route(regex = ...) that cannot be applied to @Composable or Voyager Screen" } @@ -162,7 +166,7 @@ private class RoutingProcessor( configureSpec .beginControlFlow( "%M(path = %T(%S))", - handle, + memberName, Regex::class, routeAnnotation.regex ) @@ -172,7 +176,7 @@ private class RoutingProcessor( configureSpec .beginControlFlow( template, - handle, + memberName, Regex::class, routeAnnotation.regex, routeMethod @@ -183,11 +187,6 @@ private class RoutingProcessor( routeAnnotation.name.isBlank() -> "name = null" else -> """name = "${routeAnnotation.name}"""" } - val memberName = when { - isComposable -> composable - isScreen -> screen - else -> handle - } logger.info(">>>> transforming -> name: $named and member: $memberName") if (routeAnnotation.method.isBlank()) { configureSpec