commonMain.com.freeletics.khonshu.navigation.deeplinks.DeepLinkMatcher.kt Maven / Gradle / Ivy
package com.freeletics.khonshu.navigation.deeplinks
import com.eygraber.uri.Uri
import com.freeletics.khonshu.navigation.deeplinks.DeepLinkHandler.Pattern
import com.freeletics.khonshu.navigation.deeplinks.DeepLinkHandler.Prefix
import com.freeletics.khonshu.navigation.internal.InternalNavigationApi
/**
* Checks if any of the given [uri] matches one of [DeepLinkHandler.patterns] and returns `true`
* if that is the case.
*
* [defaultPrefixes] will be used as base url if [DeepLinkHandler.prefixes] is empty.
*/
@InternalNavigationApi
public fun Set.matchesPattern(
uri: Uri,
defaultPrefixes: Set,
): Boolean {
return any { it.matchesPattern(uri, defaultPrefixes) }
}
/**
* Checks if the given [uri] matches one of [DeepLinkHandler.patterns] and returns `true` if that
* is the case.
*
* [defaultPrefixes] will be used as base url if [DeepLinkHandler.prefixes] is empty.
*/
@InternalNavigationApi
public fun DeepLinkHandler.matchesPattern(
uri: Uri,
defaultPrefixes: Set,
): Boolean {
return findMatchingPattern(uri.toString(), defaultPrefixes) != null
}
internal fun DeepLinkHandler.findMatchingPattern(
uriString: String,
defaultPrefixes: Set,
): Pattern? {
// if DeepLinkHandler does not define any custom prefix use the default ones
val prefixes = prefixes.ifEmpty { defaultPrefixes }
// combine all prefixes into a single regular expression that allows any of them
val regexPrefix = prefixes.asOneOfRegex()
// find the pattern that matches uriString
return patterns.find { pattern ->
// replace all {name} placeholders in the pattern with a regex placeholder
val regexPattern = pattern.replacePlaceholders()
// when the pattern is not empty check for it, otherwise check for the prefixes with
// an optional trailing slash, query parameters are allowed in both cases
val regex = if (regexPattern.isNotBlank()) {
"^$regexPrefix/$regexPattern$QUERY_PARAMETER_REGEX".toRegex()
} else {
"^$regexPrefix/?$QUERY_PARAMETER_REGEX".toRegex()
}
regex.matches(uriString)
}
}
// combines the values of all prefixes into a single regex string which matches exactly one
// of the prefixes at once
private fun Set.asOneOfRegex(): String {
return joinToString(prefix = "(", separator = "|", postfix = ")") {
Regex.escape(it.value)
}
}
private fun Pattern.replacePlaceholders(replacement: String = PARAM_VALUE): String {
// $1 and $3 will add the optional leading and trailing slashes if needed
return value.replace(PARAM_REGEX, "$1$replacement$3")
}
@InternalNavigationApi
public fun Pattern.replacePlaceholders(replacement: (String) -> String): String {
// $1 and $3 will add the optional leading and trailing slashes if needed
return value.replace(PARAM_REGEX) { "${it.groupValues[1]}${replacement(it.groupValues[2])}${it.groupValues[3]}" }
}
// matches placeholders like {locale} or {foo_bar-1}, requires a leading slash and either a trailing
// slash or the end of the string to avoid that a path segment is not fully filled by the
// placeholder
private val PARAM_REGEX = "(/|^)\\{([a-zA-Z][a-zA-Z0-9_-]*)\\}(/|$)".toRegex()
// a regex for values that are allowed in the path segment that contains the placeholder
private const val PARAM_VALUE = "([a-zA-Z0-9_'!+%~=,\\-\\.\\@\\$\\:]+)"
// the query parameter itself is optional and starts with a question mark, afterwards anything
// is accepted since its not part of the pattern, ends with the end of the whole url
private const val QUERY_PARAMETER_REGEX = "(\\?.+)?$"
© 2015 - 2025 Weber Informatics LLC | Privacy Policy