commonMain.com.copperleaf.ballast.navigation.routing.routingUtils.kt Maven / Gradle / Ivy
@file:Suppress("NOTHING_TO_INLINE")
package com.copperleaf.ballast.navigation.routing
import com.copperleaf.ballast.navigation.internal.EnumRoutingTable
import com.copperleaf.ballast.navigation.internal.directionsInternal
import kotlin.properties.PropertyDelegateProvider
public typealias Backstack = List>
// Use router
// ---------------------------------------------------------------------------------------------------------------------
/**
* Determine whether this Route is static. A static Route is one where `Route.directions()` will produce a URL which
* matches itself when no path or query parameters are provided to the [Route.directions] function.
*/
public fun Route.isStatic(): Boolean {
return matcher.path.all { it.isStatic } && matcher.query.all { it.isStatic }
}
/**
* Start building a destination with directions from [this] [Route].
*/
public fun T.directions(): Destination.Directions {
return Destination.Directions(this)
}
/**
* Convert the directions from a route and its parameters into a final destination URL.
*/
public fun Destination.Directions.build(): String {
return route.directionsInternal(this)
}
/**
* Set the value of the path parameters map, overwriting any previous entries. The named values in the Route's path
* format will be used to infer the proper values based on their index in the [pathParameters] array, so you can more
* easily provide the values for parameters and tailcards without needing to explicitly refer to the parameter names
* for them every time.
*
* @return a new Builder instance with the new path values
*/
public fun Destination.Directions.path(
vararg pathParameters: String,
): Destination.Directions {
return this.copy(
pathParameters = buildMap {
var i = 0
route.matcher.path.forEach { segment ->
when (segment) {
is PathSegment.Static -> {
// nothing to add
}
is PathSegment.Parameter -> {
put(segment.name, listOfNotNull(pathParameters.getOrNull(i)))
i++
}
is PathSegment.Wildcard -> {
// nothing to add
}
is PathSegment.Tailcard -> {
if (segment.name != null) {
put(segment.name, pathParameters.toList().subList(i, pathParameters.size))
}
i = pathParameters.lastIndex
}
}
}
},
)
}
/**
* Add the values in [pathMap] to the [Destination.Directions.pathParameters] of this builder.
*
* @return a new Builder instance with the new path values
*/
public fun Destination.Directions.pathParameters(
pathMap: Map>
): Destination.Directions {
return this.copy(
pathParameters = this.pathParameters + pathMap,
)
}
/**
* Add a single path parameter to the builder at the given [key] with multiple [values].
*
* @return a new Builder instance with the new path value
*/
public fun Destination.Directions.pathParameter(
key: String,
vararg values: String,
): Destination.Directions {
return this.copy(
pathParameters = this.pathParameters + (key to values.toList()),
)
}
/**
* Add a single path parameter to the builder at the given [key] with multiple [values].
*
* @return a new Builder instance with the new path value
*/
public fun Destination.Directions.pathParameter(
key: String,
values: Iterable,
): Destination.Directions {
return this.copy(
pathParameters = this.pathParameters + (key to values.toList()),
)
}
/**
* Add the values in [queryMap] to the [Destination.Directions.queryParameters] of this builder.
*
* @return a new Builder instance with the new query values
*/
public fun Destination.Directions.queryParameters(
queryMap: Map>
): Destination.Directions {
return this.copy(
queryParameters = this.queryParameters + queryMap,
)
}
/**
* Add a single query parameter to the builder at the given [key] with multiple [values].
*
* @return a new Builder instance which includes the new query value
*/
public fun Destination.Directions.queryParameter(
key: String,
vararg values: String,
): Destination.Directions {
return this.copy(
queryParameters = this.queryParameters + (key to values.toList()),
)
}
/**
* Add a single query parameter to the builder at the given [key] with multiple [values].
*
* @return a new Builder instance which includes the new query value
*/
public fun Destination.Directions.queryParameter(
key: String,
values: Iterable,
): Destination.Directions {
return this.copy(
queryParameters = this.queryParameters + (key to values.toList()),
)
}
/**
* Get the topmost matching destination from the backstack. If there are any mismatched destinations, those will be
* skipped so that you can continue displaying the topmost matching destination. Returns null if the backstack is empty
* or the only destination in the backstack is a mismatch.
*/
public val Backstack.currentDestinationOrNull: Destination.Match?
get() = lastOfInstanceOrNull>()
/**
* Get the topmost Route from the backstack, whether it is a match or mismatch. This allows you to explicitly
* display the mismatched state from the backstack, rather than effectively ignoring it.
*/
public val Backstack.currentRouteOrNull: T?
get() = currentDestinationOrNull?.originalRoute
/**
* Get the topmost matching destination from the backstack. If there are any mismatched destinations, those will be
* skipped so that you can continue displaying the topmost matching destination. Throws an exception if the backstack is
* empty or the only destination in the backstack is a mismatch.
*/
public val Backstack.currentDestinationOrThrow: Destination.Match
get() = lastOfInstance>()
/**
* Get the topmost matching Route from the backstack. If there are any mismatched destinations, those will be
* skipped so that you can continue displaying the topmost matching destination. Throws an exception if the backstack is
* empty or the only destination in the backstack is a mismatch.
*/
public val Backstack.currentRouteOrThrow: T
get() = currentDestinationOrThrow.originalRoute
/**
* Get the topmost destination from the backstack, whether it is a match or mismatch. This allows you to explicitly
* display the mismatched state from the backstack, rather than effectively ignoring it.
*/
public val Backstack.currentDestinationOrNotFound: Destination?
get() = lastOrNull()
/**
* Get all destinations from the backstack that have the given [annotation]. The matching destinations will only be
* counted until the first entry without the given annotation is encountered, even if there are more after that deeper
* in the backstack. This is useful for situations like displaying all [Floating] destinations that are above a
* non-floating one.
*/
public fun Backstack.getTopDestinationsWithAnnotation(annotation: RouteAnnotation): List> =
takeLastWhile { it is Destination.Match && annotation in it.annotations }
.map { it as Destination.Match }
/**
* Get the first destination from the backstack that does not have the given [annotation]. This is not necessarily the
* top destination in the backstack. This is useful for situations like displaying the main content underneath
* [Floating] destinations, so that the main content is still visible under the scrim of the floating window.
*/
public fun RouterContract.State.getTopDestinationWithoutAnnotation(annotation: RouteAnnotation): Destination.Match? =
backstack
.lastOrNull { it is Destination.Match && annotation !in it.annotations } as? Destination.Match
public inline fun Backstack.renderCurrentDestination(
route: Destination.Match.(T) -> Unit,
notFound: (String) -> Unit,
) {
when (val currentDestination = this.currentDestinationOrNotFound) {
is Destination.Match -> {
route(currentDestination, currentDestination.originalRoute)
}
is Destination.Mismatch -> {
notFound(currentDestination.originalDestinationUrl)
}
null -> {
}
}
}
public inline fun Backstack.mapCurrentDestination(
route: Destination.Match.(T) -> U?,
notFound: (String) -> U,
): U? {
return when (val currentDestination = this.currentDestinationOrNotFound) {
is Destination.Match -> {
route(currentDestination, currentDestination.originalRoute)
}
is Destination.Mismatch -> {
notFound(currentDestination.originalDestinationUrl)
}
null -> {
null
}
}
}
// Other helpers
// ---------------------------------------------------------------------------------------------------------------------
/**
* Returns the last element matching whose type is [T], or `null` if no such element was found.
*/
private inline fun List<*>.lastOfInstanceOrNull(): T? {
val iterator = this.listIterator(size)
while (iterator.hasPrevious()) {
val element = iterator.previous()
if (element is T) return element
}
return null
}
/**
* Returns the last element matching whose type is [T].
*
* @throws NoSuchElementException if no such element is found.
*/
private inline fun List<*>.lastOfInstance(): T {
val iterator = this.listIterator(size)
while (iterator.hasPrevious()) {
val element = iterator.previous()
if (element is T) return element
}
throw NoSuchElementException("List contains no element matching the predicate.")
}
// Delegate Base
// ---------------------------------------------------------------------------------------------------------------------
public typealias LazyProvider = PropertyDelegateProvider>
private fun provideLazy(
compute: (propertyName: String) -> T
): LazyProvider = PropertyDelegateProvider { _, property ->
lazy {
compute(property.name)
}
}
public fun Destination.ParametersProvider.provideLazyPath(
parameterName: String?,
compute: (value: String) -> T
): LazyProvider = provideLazy { propertyName ->
parameters
.pathParameters[parameterName ?: propertyName]!!
.single()
.let { compute(it) }
}
public fun Destination.ParametersProvider.provideLazyQuery(
parameterName: String?,
compute: (value: String) -> T
): LazyProvider = provideLazy { propertyName ->
parameters
.queryParameters[parameterName ?: propertyName]!!
.single()
.let { compute(it) }
}
public fun Destination.ParametersProvider.provideOptionalLazyPath(
parameterName: String?,
compute: (value: String) -> T?
): LazyProvider = provideLazy { propertyName ->
parameters
.pathParameters[parameterName ?: propertyName]
?.singleOrNull()
?.let { compute(it) }
}
public fun Destination.ParametersProvider.provideOptionalLazyQuery(
parameterName: String?,
compute: (value: String) -> T?
): LazyProvider = provideLazy { propertyName ->
parameters
.queryParameters[parameterName ?: propertyName]
?.singleOrNull()
?.let { compute(it) }
}
// Path Parameter delegates
// ---------------------------------------------------------------------------------------------------------------------
public inline fun Destination.ParametersProvider.stringPath(
parameterName: String? = null
): LazyProvider = provideLazyPath(parameterName) { it }
public inline fun Destination.ParametersProvider.optionalStringPath(
parameterName: String? = null
): LazyProvider = provideOptionalLazyPath(parameterName) { it }
public inline fun Destination.ParametersProvider.intPath(
parameterName: String? = null
): LazyProvider = provideLazyPath(parameterName) { it.toInt() }
public inline fun Destination.ParametersProvider.optionalIntPath(
parameterName: String? = null
): LazyProvider = provideOptionalLazyPath(parameterName) { it.toIntOrNull() }
public inline fun Destination.ParametersProvider.longPath(
parameterName: String? = null
): LazyProvider = provideLazyPath(parameterName) { it.toLong() }
public inline fun Destination.ParametersProvider.optionalLongPath(
parameterName: String? = null
): LazyProvider = provideOptionalLazyPath(parameterName) { it.toLongOrNull() }
public inline fun Destination.ParametersProvider.floatPath(
parameterName: String? = null
): LazyProvider = provideLazyPath(parameterName) { it.toFloat() }
public inline fun Destination.ParametersProvider.optionalFloatPath(
parameterName: String? = null
): LazyProvider = provideOptionalLazyPath(parameterName) { it.toFloatOrNull() }
public inline fun Destination.ParametersProvider.doublePath(
parameterName: String? = null
): LazyProvider = provideLazyPath(parameterName) { it.toDouble() }
public inline fun Destination.ParametersProvider.optionalDoublePath(
parameterName: String? = null
): LazyProvider = provideOptionalLazyPath(parameterName) { it.toDoubleOrNull() }
public inline fun Destination.ParametersProvider.booleanPath(
parameterName: String? = null
): LazyProvider = provideLazyPath(parameterName) { it.toBooleanStrict() }
public inline fun Destination.ParametersProvider.optionalBooleanPath(
parameterName: String? = null
): LazyProvider = provideOptionalLazyPath(parameterName) { it.toBooleanStrictOrNull() }
public inline fun > Destination.ParametersProvider.enumPath(
crossinline valueOf: (String) -> T,
parameterName: String? = null,
): LazyProvider = provideLazyPath(parameterName) { valueOf(it) }
public inline fun > Destination.ParametersProvider.optionalEnumPath(
crossinline valueOf: (String) -> T,
parameterName: String? = null
): LazyProvider = provideOptionalLazyPath(parameterName) { runCatching { valueOf(it) }.getOrNull() }
// Query Parameter delegates
// ---------------------------------------------------------------------------------------------------------------------
public inline fun Destination.ParametersProvider.stringQuery(
parameterName: String? = null
): LazyProvider = provideLazyQuery(parameterName) { it }
public inline fun Destination.ParametersProvider.optionalStringQuery(
parameterName: String? = null
): LazyProvider = provideOptionalLazyQuery(parameterName) { it }
public inline fun Destination.ParametersProvider.intQuery(
parameterName: String? = null
): LazyProvider = provideLazyQuery(parameterName) { it.toInt() }
public inline fun Destination.ParametersProvider.optionalIntQuery(
parameterName: String? = null
): LazyProvider = provideOptionalLazyQuery(parameterName) { it.toIntOrNull() }
public inline fun Destination.ParametersProvider.longQuery(
parameterName: String? = null
): LazyProvider = provideLazyQuery(parameterName) { it.toLong() }
public inline fun Destination.ParametersProvider.optionalLongQuery(
parameterName: String? = null
): LazyProvider = provideOptionalLazyQuery(parameterName) { it.toLongOrNull() }
public inline fun Destination.ParametersProvider.floatQuery(
parameterName: String? = null
): LazyProvider = provideLazyQuery(parameterName) { it.toFloat() }
public inline fun Destination.ParametersProvider.optionalFloatQuery(
parameterName: String? = null
): LazyProvider = provideOptionalLazyQuery(parameterName) { it.toFloatOrNull() }
public inline fun Destination.ParametersProvider.doubleQuery(
parameterName: String? = null
): LazyProvider = provideLazyQuery(parameterName) { it.toDouble() }
public inline fun Destination.ParametersProvider.optionalDoubleQuery(
parameterName: String? = null
): LazyProvider = provideOptionalLazyQuery(parameterName) { it.toDoubleOrNull() }
public inline fun Destination.ParametersProvider.booleanQuery(
parameterName: String? = null
): LazyProvider = provideLazyQuery(parameterName) { it.toBooleanStrict() }
public inline fun Destination.ParametersProvider.optionalBooleanQuery(
parameterName: String? = null
): LazyProvider = provideOptionalLazyQuery(parameterName) { it.toBooleanStrictOrNull() }
public inline fun > Destination.ParametersProvider.enumQuery(
crossinline valueOf: (String) -> T,
parameterName: String? = null,
): LazyProvider = provideLazyQuery(parameterName) { valueOf(it) }
public inline fun > Destination.ParametersProvider.optionalEnumQuery(
crossinline valueOf: (String) -> T,
parameterName: String? = null
): LazyProvider = provideOptionalLazyQuery(parameterName) { runCatching { valueOf(it) }.getOrNull() }
// Backstack Navigator helpers
// ---------------------------------------------------------------------------------------------------------------------
/**
* Navigate backwards 1 location in the backstack.
*/
public fun BackstackNavigator.goBack() {
if (backstack.isEmpty()) {
// error, backstack was empty. Just ignore it
} else {
updateBackstack {
it.dropLast(1)
}
}
}
/**
* Navigate backward in the backstack, removing all destinations that contain the given [annotation].
*/
public fun BackstackNavigator.popAllWithAnnotation(
annotation: RouteAnnotation,
) {
if (backstack.isEmpty()) {
// error, backstack was empty. Just ignore it
} else {
updateBackstack {
it.dropLastWhile { destination ->
when (destination) {
is Destination.Match<*> -> {
// drop the latest destination if it contains the given annotation
annotation in destination.annotations
}
is Destination.Mismatch<*> -> {
// drop the mismatches, regardless
true
}
}
}
}
}
}
/**
* Attempt to navigate forward to the route matching [destinationUrl].
*/
public fun BackstackNavigator.addToTop(
destinationUrl: String,
extraAnnotations: Set,
) {
return if (destinationUrl == backstack.currentDestinationOrNull?.originalDestinationUrl) {
// same as top destination, ignore it
} else {
updateBackstack {
val matchedDestination = matchDestination(destinationUrl, extraAnnotations)
it.dropLastWhile { it is Destination.Mismatch } + matchedDestination
}
}
}
// Additional matcher methods
// ---------------------------------------------------------------------------------------------------------------------
public fun RouteMatcher.matchDestinationOrThrow(
originalRoute: T,
unmatchedDestination: UnmatchedDestination,
): Destination.Match {
val match = match(originalRoute, unmatchedDestination)
return when (match) {
is RouteMatcher.MatchResult.NoMatch -> {
error(
"Destination '${unmatchedDestination.originalDestinationUrl}' does not match Route '$originalRoute'" +
": Path mismatch"
)
}
is RouteMatcher.MatchResult.PartialMatch -> {
error(
"Destination '${unmatchedDestination.originalDestinationUrl}' does not match Route '$originalRoute'" +
": Query string mismatch"
)
}
is RouteMatcher.MatchResult.CompleteMatch -> {
unmatchedDestination.asMatchedDestination(match)
}
}
}
public fun RouteMatcher.matchDestinationOrNull(
originalRoute: T,
unmatchedDestination: UnmatchedDestination
): Destination.Match? {
return when (val match = match(originalRoute, unmatchedDestination)) {
is RouteMatcher.MatchResult.NoMatch -> null
is RouteMatcher.MatchResult.PartialMatch -> null
is RouteMatcher.MatchResult.CompleteMatch -> unmatchedDestination.asMatchedDestination(match)
}
}
public fun RouteMatcher.matchDestination(
originalRoute: T,
unmatchedDestination: UnmatchedDestination,
): Destination {
return when (val match = match(originalRoute, unmatchedDestination)) {
is RouteMatcher.MatchResult.NoMatch -> unmatchedDestination.asMismatchedDestination()
is RouteMatcher.MatchResult.PartialMatch -> unmatchedDestination.asMismatchedDestination()
is RouteMatcher.MatchResult.CompleteMatch -> unmatchedDestination.asMatchedDestination(match)
}
}
public fun UnmatchedDestination.asMatchedDestination(
match: RouteMatcher.MatchResult.CompleteMatch,
): Destination.Match {
return Destination.Match(
originalDestinationUrl = originalDestinationUrl,
originalRoute = match.originalRoute,
pathParameters = match.parsedPathParameters,
queryParameters = match.parsedQueryParameters,
annotations = (match.originalRoute.annotations + extraAnnotations).toSet()
)
}
public fun UnmatchedDestination.asMismatchedDestination(): Destination.Mismatch {
return Destination.Mismatch(
originalDestinationUrl = originalDestinationUrl,
)
}
// RoutingTable Helpers
// ---------------------------------------------------------------------------------------------------------------------
public fun RoutingTable.Companion.fromEnum(
enumValues: Array,
): RoutingTable where T : Enum, T : Route {
check(enumValues.isNotEmpty()) { "RoutingTable enum values cannot be empty" }
val routesSortedByWeight: List = enumValues
.sortedByDescending { it.matcher.weight }
return EnumRoutingTable(
routes = routesSortedByWeight,
)
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy