Skip to content

Commit 5f9ddba

Browse files
fix: nested routing dispose, type-safe and events
1 parent 4407c51 commit 5f9ddba

File tree

15 files changed

+927
-101
lines changed

15 files changed

+927
-101
lines changed

README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,3 +274,31 @@ val router = routing(
274274
parent = parent,
275275
) { }
276276
```
277+
278+
## Limitations
279+
280+
- Nested routing with type-safe not support navigation from parent to child using the Type. You have to use path routing.
281+
```kotlin
282+
@Resource("/endpoint")
283+
class Endpoint
284+
285+
val parent = routing { }
286+
287+
val router = routing(
288+
rootPath = "/child",
289+
parent = parent,
290+
) {
291+
handle<Endpoint> {
292+
// ...
293+
}
294+
}
295+
296+
// IT WORKS
297+
router.execute(Endpoint())
298+
299+
// IT DOES NOT WORK
300+
parent.execute(Endpoint())
301+
302+
// IT WORKS
303+
parent.execute(YourCustomCall(uri = "/child/endpoint"))
304+
```

core-stack/common/test/dev/programadorthi/routing/core/StackRoutingTest.kt

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -506,4 +506,96 @@ class StackRoutingTest {
506506
assertEquals(parametersOf(), result?.parameters)
507507
assertEquals("/path", result?.stackManager?.last())
508508
}
509+
510+
@Test
511+
fun shouldPushParentPathWhenRoutingFromChild() = runTest {
512+
// GIVEN
513+
val job = Job()
514+
var result: ApplicationCall? = null
515+
516+
val parent = routing(
517+
parentCoroutineContext = coroutineContext + job
518+
) {
519+
install(StackRouting)
520+
521+
route(path = "/pathParent") {
522+
push {
523+
result = call
524+
job.complete()
525+
}
526+
}
527+
}
528+
529+
val routing = routing(
530+
rootPath = "/child",
531+
parent = parent,
532+
parentCoroutineContext = coroutineContext + job
533+
) {
534+
install(StackRouting)
535+
536+
route(path = "/pathChild") {
537+
push {
538+
result = call
539+
job.complete()
540+
}
541+
}
542+
}
543+
544+
// WHEN
545+
routing.push(path = "/pathParent")
546+
advanceTimeBy(99)
547+
548+
// THEN
549+
assertNotNull(result)
550+
assertEquals("/pathParent", "${result?.uri}")
551+
assertEquals("", "${result?.name}")
552+
assertEquals(StackRouteMethod.Push, result?.routeMethod)
553+
assertEquals(Parameters.Empty, result?.parameters)
554+
}
555+
556+
@Test
557+
fun shouldPushChildPathWhenRoutingFromParent() = runTest {
558+
// GIVEN
559+
val job = Job()
560+
var result: ApplicationCall? = null
561+
562+
val parent = routing(
563+
parentCoroutineContext = coroutineContext + job
564+
) {
565+
install(StackRouting)
566+
567+
route(path = "/pathParent") {
568+
push {
569+
result = call
570+
job.complete()
571+
}
572+
}
573+
}
574+
575+
routing(
576+
rootPath = "/child",
577+
parent = parent,
578+
parentCoroutineContext = coroutineContext + job
579+
) {
580+
install(StackRouting)
581+
582+
route(path = "/pathChild") {
583+
push {
584+
result = call
585+
job.complete()
586+
}
587+
}
588+
}
589+
590+
// WHEN
591+
parent.push(path = "/child/pathChild")
592+
advanceTimeBy(99)
593+
594+
// THEN
595+
assertNotNull(result)
596+
assertEquals("/child/pathChild", "${result?.uri}")
597+
assertEquals("", "${result?.name}")
598+
assertEquals(StackRouteMethod.Push, result?.routeMethod)
599+
assertEquals(Parameters.Empty, result?.parameters)
600+
}
509601
}

core/common/src/dev/programadorthi/routing/core/Route.kt

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,6 @@ public open class Route(
3535

3636
internal val handlers = mutableListOf<PipelineInterceptor<Unit, ApplicationCall>>()
3737

38-
internal var routingRef: Routing? = null
39-
4038
/**
4139
* Creates a child node in this node with a given [selector] or returns an existing one with the same selector.
4240
*/

core/common/src/dev/programadorthi/routing/core/Routing.kt

Lines changed: 19 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ public class Routing internal constructor(
5353
public fun execute(call: ApplicationCall) {
5454
var current: Routing? = this
5555
while (current?.disposed == true && current.parent != null) {
56-
current = current.parent?.routing
56+
current = current.parent?.asRouting
5757
}
5858
val scope = current?.application ?: return
5959
with(scope) {
@@ -66,11 +66,13 @@ public class Routing internal constructor(
6666
public fun unregisterNamed(name: String) {
6767
val route = namedRoutes.remove(name) ?: return
6868
removeChild(route)
69+
unregisterFromParents(route)
6970
}
7071

7172
public fun unregisterPath(path: String) {
7273
val route = createRouteFromPath(path)
7374
removeChild(route)
75+
unregisterFromParents(route)
7476
}
7577

7678
/**
@@ -83,9 +85,7 @@ public class Routing internal constructor(
8385
}
8486

8587
public fun dispose() {
86-
actionOnParent { parent, _, path ->
87-
parent.unregisterPath(path)
88-
}
88+
unregisterFromParents(this)
8989
childList.clear()
9090
namedRoutes.clear()
9191
application.dispose()
@@ -99,38 +99,21 @@ public class Routing internal constructor(
9999
namedRoutes[name] = route
100100
}
101101

102-
private fun removeChild(child: Route) {
103-
val childParent = child.parent ?: return
104-
val parentChildren = childParent.childList
105-
parentChildren.remove(child)
106-
if (parentChildren.isEmpty()) {
107-
val toRemove = childParent.parent ?: return
108-
removeChild(toRemove)
102+
private fun removeChild(route: Route) {
103+
val parentRoute = route.parent ?: return
104+
parentRoute.childList.remove(route)
105+
if (parentRoute.childList.isEmpty()) {
106+
removeChild(parentRoute)
109107
}
110108
}
111109

112-
private fun connectWithParents(configure: Route.() -> Unit) {
113-
actionOnParent { parent, child, path ->
114-
val newChild = parent.route(
115-
path = path,
116-
name = null,
117-
build = configure
118-
)
119-
newChild.routingRef = child.routing
120-
}
121-
}
122-
123-
private fun actionOnParent(action: (Routing, Routing, String) -> Unit) {
124-
var child: Routing = this
125-
var childParent: Route? = child.parent ?: return
126-
var path = child.selector.toString()
127-
while (childParent is Routing) {
128-
action(childParent, child, path)
129-
130-
child = childParent
131-
childParent = child.parent ?: return
132-
133-
path = child.selector.toString() + '/' + path
110+
private fun unregisterFromParents(route: Route) {
111+
val parentRouting = route.asRouting?.parent
112+
if (parentRouting is Routing) {
113+
val validPath = route.toString().substringAfter(parentRouting.toString())
114+
val toRemove = parentRouting.createRouteFromPath(validPath)
115+
removeChild(toRemove)
116+
unregisterFromParents(toRemove)
134117
}
135118
}
136119

@@ -291,7 +274,7 @@ public class Routing internal constructor(
291274
val resolveContext = RoutingResolveContext(this, call, tracers)
292275
when (val resolveResult = resolveContext.resolve()) {
293276
is RoutingResolveResult.Failure -> {
294-
val routing = parent?.routing ?: throw RouteNotFoundException(message = resolveResult.reason)
277+
val routing = parent?.asRouting ?: throw RouteNotFoundException(message = resolveResult.reason)
295278
routing.execute(context.call)
296279
}
297280

@@ -319,7 +302,6 @@ public class Routing internal constructor(
319302

320303
override fun install(pipeline: Application, configure: Route.() -> Unit): Routing {
321304
val routing = Routing(pipeline).apply(configure)
322-
routing.connectWithParents(configure)
323305
pipeline.intercept(Call) {
324306
routing.interceptor(this)
325307
}
@@ -339,10 +321,10 @@ public val Route.application: Application
339321
)
340322
}
341323

342-
public val Route.routing: Routing?
324+
public val Route.asRouting: Routing?
343325
get() = when (this) {
344326
is Routing -> this
345-
else -> routingRef ?: parent?.routing
327+
else -> parent?.asRouting
346328
}
347329

348330
public fun <B : Any, F : Any> Route.install(

core/common/src/dev/programadorthi/routing/core/RoutingBuilder.kt

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ public fun Route.route(
1616
path: String,
1717
name: String? = null,
1818
build: Route.() -> Unit
19-
): Route = createRouteFromPath(path, name).apply(build)
19+
): Route = createRouteFromPath(path, name).apply(build).registerToParents(null, build)
2020

2121
/**
2222
* Builds a route to match the specified [RouteMethod] and [path].
@@ -29,7 +29,7 @@ public fun Route.route(
2929
build: Route.() -> Unit,
3030
): Route {
3131
val selector = RouteMethodRouteSelector(method)
32-
return createRouteFromPath(path, name).createChild(selector).apply(build)
32+
return createRouteFromPath(path, name).createChild(selector).apply(build).registerToParents(selector, build)
3333
}
3434

3535
/**
@@ -38,7 +38,7 @@ public fun Route.route(
3838
@KtorDsl
3939
public fun Route.method(method: RouteMethod, body: Route.() -> Unit): Route {
4040
val selector = RouteMethodRouteSelector(method)
41-
return createChild(selector).apply(body)
41+
return createChild(selector).apply(body).registerToParents(selector, body)
4242
}
4343

4444
@KtorDsl
@@ -55,7 +55,7 @@ public fun Route.createRouteFromPath(path: String, name: String?): Route {
5555
val route = createRouteFromPath(path)
5656
// Registering named route
5757
if (!name.isNullOrBlank()) {
58-
val validRouting = requireNotNull(routing) {
58+
val validRouting = requireNotNull(asRouting) {
5959
"Named route '$name' must be a Routing child."
6060
}
6161
validRouting.registerNamed(name = name, route = route)
@@ -84,6 +84,36 @@ internal fun Route.createRouteFromPath(path: String): Route {
8484
return current
8585
}
8686

87+
/**
88+
* Look for parents [Routing] and connect them to current [Route] creating a routing
89+
* from top most parent until current: /topmostparent/moreparents/child
90+
*/
91+
private fun Route.registerToParents(
92+
selector: RouteMethodRouteSelector?,
93+
build: Route.() -> Unit
94+
): Route {
95+
val parentRouting = asRouting?.parent as? Routing ?: return this
96+
val validPath = when (val parentPath = parentRouting.toString()) {
97+
"/" -> toString()
98+
else -> toString().substringAfter(parentPath)
99+
}
100+
if (selector == null) {
101+
parentRouting.route(path = validPath, name = null, build = build)
102+
} else {
103+
var path = validPath.substringBefore(selector.toString())
104+
if (path.endsWith('/')) {
105+
path = path.substring(0, path.length - 1)
106+
}
107+
parentRouting.route(
108+
path = path,
109+
method = selector.method,
110+
name = null,
111+
build = build
112+
)
113+
}
114+
return this
115+
}
116+
87117
/**
88118
* A helper object for building instances of [RouteSelector] from path segments.
89119
*/

core/common/src/dev/programadorthi/routing/core/RoutingResolveContext.kt

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ private const val MIN_QUALITY = -Double.MAX_VALUE
2121
* @param call instance of [ApplicationCall] to use during resolution
2222
*/
2323
public class RoutingResolveContext(
24-
public val routing: Route,
24+
public val routing: Routing,
2525
public val call: ApplicationCall,
2626
private val tracers: List<(RoutingResolveTrace) -> Unit>
2727
) {
@@ -51,8 +51,14 @@ public class RoutingResolveContext(
5151
}
5252
}
5353

54-
private fun parse(path: String): List<String> {
55-
if (path.isEmpty() || path == "/") return emptyList()
54+
private fun parse(pathValue: String): List<String> {
55+
if (pathValue.isEmpty() || pathValue == "/") return emptyList()
56+
val routingPath = "/${routing.selector}"
57+
val path = when {
58+
pathValue.startsWith(routingPath) -> pathValue
59+
pathValue.startsWith('/') -> "$routingPath$pathValue"
60+
else -> "$routingPath/$pathValue"
61+
}
5662
val length = path.length
5763
var beginSegment = 0
5864
var nextSegment = 0
@@ -175,7 +181,7 @@ public class RoutingResolveContext(
175181
if (finalResolve.isEmpty()) {
176182
return RoutingResolveResult.Failure(
177183
routing,
178-
"No matched subtrees found",
184+
"No matched subtrees found for: ${call.path()}",
179185
)
180186
}
181187

0 commit comments

Comments
 (0)