Skip to content

Commit

Permalink
Voyager routing by regex
Browse files Browse the repository at this point in the history
  • Loading branch information
programadorthi committed Nov 21, 2024
1 parent c29b142 commit 267c196
Show file tree
Hide file tree
Showing 5 changed files with 131 additions and 21 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,19 @@ public fun Route.screen(
body: suspend PipelineContext<Unit, ApplicationCall>.() -> Screen,
): Route = route(path = path, name = name, method = method) { screen(body) }

@KtorDsl
public fun Route.screen(
path: Regex,
body: suspend PipelineContext<Unit, ApplicationCall>.() -> Screen,
): Route = route(path = path) { screen(body) }

@KtorDsl
public fun Route.screen(
path: Regex,
method: RouteMethod,
body: suspend PipelineContext<Unit, ApplicationCall>.() -> Screen,
): Route = route(path = path, method = method) { screen(body) }

@KtorDsl
public fun Route.screen(body: suspend PipelineContext<Unit, ApplicationCall>.() -> Screen) {
val routing = asRouting ?: error("Your route $this must have a parent Routing")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,12 @@ internal class Screen5(@Body val user: User) : Screen {
invoked += "/screen-with-body" to listOf(user)
}
}

@Route(regex = "/(?<number>\\d+)")
internal class Screen6(val number: Int) : Screen {

@Composable
override fun Content() {
invoked += "/(?<number>\\d+)" to listOf(number)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 {
Expand Down Expand Up @@ -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<IllegalStateException>(exception)
assertEquals(
"Voyager needs a stack route method to work. You called a screen /path using " +
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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("/(?<number>\\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("/(?<number>\\d+)")) {
screen(path = Regex("(?<user>\\w+)/(?<login>.+)")) {
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
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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("/(?<number>\\d+)"))
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down

0 comments on commit 267c196

Please sign in to comment.