All Downloads are FREE. Search and download functionalities are using the official Maven repository.

commonMain.com.freeletics.khonshu.navigation.deeplinks.DeepLinkMatcher.kt Maven / Gradle / Ivy

There is a newer version: 0.29.1
Show newest version
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