Skip to content

Commit 92e2e2a

Browse files
feat: nested routing and unregister path support
1 parent be59d93 commit 92e2e2a

File tree

6 files changed

+532
-26
lines changed

6 files changed

+532
-26
lines changed

README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,21 @@ router.emitEvent(
265265
)
266266
```
267267

268+
## Nested Routing
269+
270+
With nested routing you can connect one `Routing`. It is good for projects that demand loading routes on demand
271+
as Android Dynamic Feature that each module has your own navigation and are load at runtime.
272+
Checkout `RoutingTest` for more usages.
273+
274+
```kotlin
275+
val parent = routing { }
276+
277+
val router = routing(
278+
rootPath = "/child",
279+
parent = parent,
280+
) { }
281+
```
282+
268283
## Next steps
269284

270285
[ ] - Helper modules for native platform navigation (Android, iOS, Web, Desktop, ...)

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

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,14 @@ public open class Route(
2929
*/
3030
public val children: List<Route> get() = childList
3131

32-
private val childList: MutableList<Route> = mutableListOf()
32+
internal val childList: MutableList<Route> = mutableListOf()
3333

3434
private var cachedPipeline: ApplicationCallPipeline? = null
3535

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

38+
internal var routingRef: Routing? = null
39+
3840
/**
3941
* Creates a child node in this node with a given [selector] or returns an existing one with the same selector.
4042
*/
@@ -123,17 +125,6 @@ public fun Route.getAllRoutes(): List<Route> {
123125
return endpoints
124126
}
125127

126-
public fun Route.routing(): Routing {
127-
var parent: Route? = this
128-
while (parent != null) {
129-
if (parent is Routing) {
130-
return parent
131-
}
132-
parent = parent.parent
133-
}
134-
error("Route not attached to a parent VoyagerRouting: $this")
135-
}
136-
137128
internal fun Route.allSelectors(): List<RouteSelector> {
138129
val selectors = mutableListOf(selector)
139130
var other = parent

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

Lines changed: 79 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -37,28 +37,40 @@ import kotlin.coroutines.EmptyCoroutineContext
3737
public class Routing internal constructor(
3838
internal val application: Application,
3939
) : Route(
40-
parent = null,
41-
selector = RootRouteSelector(""),
40+
parent = application.environment.parentRouting,
41+
selector = RootRouteSelector(application.environment.rootPath),
4242
application.environment.developmentMode,
4343
application.environment
4444
) {
4545
private val tracers = mutableListOf<(RoutingResolveTrace) -> Unit>()
4646
private val namedRoutes = mutableMapOf<String, Route>()
47+
private var disposed = false
4748

4849
init {
4950
addDefaultTracing()
5051
}
5152

5253
public fun execute(call: ApplicationCall) {
53-
with(application) {
54+
var current: Routing? = this
55+
while (current?.disposed == true && current.parent != null) {
56+
current = current.parent?.routing
57+
}
58+
val scope = current?.application ?: return
59+
with(scope) {
5460
launch {
5561
execute(call)
5662
}
5763
}
5864
}
5965

6066
public fun unregisterNamed(name: String) {
61-
namedRoutes.remove(name)
67+
val route = namedRoutes.remove(name) ?: return
68+
removeChild(route)
69+
}
70+
71+
public fun unregisterPath(path: String) {
72+
val route = createRouteFromPath(path)
73+
removeChild(route)
6274
}
6375

6476
/**
@@ -71,7 +83,13 @@ public class Routing internal constructor(
7183
}
7284

7385
public fun dispose() {
86+
actionOnParent { parent, _, path ->
87+
parent.unregisterPath(path)
88+
}
89+
childList.clear()
90+
namedRoutes.clear()
7491
application.dispose()
92+
disposed = true
7593
}
7694

7795
internal fun registerNamed(name: String, route: Route) {
@@ -81,6 +99,41 @@ public class Routing internal constructor(
8199
namedRoutes[name] = route
82100
}
83101

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)
109+
}
110+
}
111+
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
134+
}
135+
}
136+
84137
private fun mapNameToPath(name: String, pathParameters: Parameters): String {
85138
val namedRoute =
86139
namedRoutes[name]
@@ -237,7 +290,10 @@ public class Routing internal constructor(
237290

238291
val resolveContext = RoutingResolveContext(this, call, tracers)
239292
when (val resolveResult = resolveContext.resolve()) {
240-
is RoutingResolveResult.Failure -> throw RouteNotFoundException(message = resolveResult.reason)
293+
is RoutingResolveResult.Failure -> {
294+
val routing = parent?.routing ?: throw RouteNotFoundException(message = resolveResult.reason)
295+
routing.execute(context.call)
296+
}
241297

242298
is RoutingResolveResult.Success -> {
243299
val routingCallPipeline = resolveResult.route.buildPipeline()
@@ -257,12 +313,13 @@ public class Routing internal constructor(
257313
* An installation object of the [Routing] plugin.
258314
*/
259315
@Suppress("PublicApiImplicitType")
260-
public companion object Plugin : BaseApplicationPlugin<Application, Routing, Routing> {
316+
public companion object Plugin : BaseApplicationPlugin<Application, Route, Routing> {
261317

262318
override val key: AttributeKey<Routing> = AttributeKey("Routing")
263319

264-
override fun install(pipeline: Application, configure: Routing.() -> Unit): Routing {
320+
override fun install(pipeline: Application, configure: Route.() -> Unit): Routing {
265321
val routing = Routing(pipeline).apply(configure)
322+
routing.connectWithParents(configure)
266323
pipeline.intercept(Call) {
267324
routing.interceptor(this)
268325
}
@@ -282,22 +339,35 @@ public val Route.application: Application
282339
)
283340
}
284341

342+
public val Route.routing: Routing?
343+
get() = when (this) {
344+
is Routing -> this
345+
else -> routingRef ?: parent?.routing
346+
}
347+
285348
public fun <B : Any, F : Any> Route.install(
286349
plugin: Plugin<Application, B, F>,
287350
configure: B.() -> Unit = {}
288351
): F = application.install(plugin, configure)
289352

290353
@KtorDsl
291354
public fun routing(
355+
rootPath: String = "/",
356+
parent: Routing? = null,
292357
parentCoroutineContext: CoroutineContext = EmptyCoroutineContext,
293358
log: Logger = KtorSimpleLogger("kotlin-routing"),
294359
developmentMode: Boolean = false,
295-
configuration: Routing.() -> Unit
360+
configuration: Route.() -> Unit
296361
): Routing {
362+
check(parent == null || rootPath != "/") {
363+
"Child routing cannot have root path with '/' only. Please, provide a path to your child routing"
364+
}
297365
val environment = ApplicationEnvironment(
298366
parentCoroutineContext = parentCoroutineContext,
299-
log = log,
300367
developmentMode = developmentMode,
368+
rootPath = rootPath,
369+
parentRouting = parent,
370+
log = log,
301371
)
302372
return with(Application(environment)) {
303373
install(Routing, configuration)

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

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,24 @@ public fun Route.handle(
4949
): Route = route(path, name) { handle(body) }
5050

5151
/**
52-
* Creates a routing entry for the specified path.
52+
* Creates a routing entry for the specified path and name
5353
*/
5454
public fun Route.createRouteFromPath(path: String, name: String?): Route {
55+
val route = createRouteFromPath(path)
56+
// Registering named route
57+
if (!name.isNullOrBlank()) {
58+
val validRouting = requireNotNull(routing) {
59+
"Named route '$name' must be a Routing child."
60+
}
61+
validRouting.registerNamed(name = name, route = route)
62+
}
63+
return route
64+
}
65+
66+
/**
67+
* Creates a routing entry for the specified path.
68+
*/
69+
internal fun Route.createRouteFromPath(path: String): Route {
5570
val parts = RoutingPath.parse(path).parts
5671
var current: Route = this
5772
for (index in parts.indices) {
@@ -66,10 +81,6 @@ public fun Route.createRouteFromPath(path: String, name: String?): Route {
6681
if (path.endsWith("/")) {
6782
current = current.createChild(TrailingSlashRouteSelector)
6883
}
69-
// Registering named route
70-
if (!name.isNullOrBlank()) {
71-
routing().registerNamed(name = name, route = current)
72-
}
7384
return current
7485
}
7586

core/common/src/dev/programadorthi/routing/core/application/ApplicationEnvironment.kt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
package dev.programadorthi.routing.core.application
66

7+
import dev.programadorthi.routing.core.Routing
78
import io.ktor.util.logging.Logger
89
import kotlin.coroutines.CoroutineContext
910

@@ -25,4 +26,14 @@ public data class ApplicationEnvironment(
2526
* Indicates if development mode is enabled.
2627
*/
2728
public val developmentMode: Boolean,
29+
30+
/**
31+
* Application's root path (prefix, context path in servlet container).
32+
*/
33+
public val rootPath: String,
34+
35+
/**
36+
* Application's current [Routing] used to nested routing
37+
*/
38+
public val parentRouting: Routing?
2839
)

0 commit comments

Comments
 (0)