TemplatePathPattern.kt

package com.hexagonkt.http.patterns

/**
 * A path definition. It parses path patterns and extract values for parameters.
 *
 * No splat support (you can use named parameters though).
 *
 * Delimiter is {var} to conform with [RFC 6570](https://tools.ietf.org/html/rfc6570).
 *
 * It supports the {var:regex} format to match only parameters with a specific pattern.
 */
data class TemplatePathPattern(
    override val pattern: String,
    override val prefix: Boolean = false
) : PathPattern by RegexPathPattern(Regex(patternToRegex(pattern, prefix))) {

    internal companion object {
        private const val PARAMETER_PREFIX = "{"
        private const val PARAMETER_SUFFIX = "}"

        internal const val WILDCARD = "*"
        private const val PARAMETER = "\\$PARAMETER_PREFIX(\\w+)(:.+?)?$PARAMETER_SUFFIX"

        private val REGEX_CHARACTERS = listOf('(', ')', '|', '?', '+', '[', ']')

        const val VARIABLE_PATTERN = "[^/]+"
        val WILDCARD_REGEX = Regex("\\$WILDCARD")
        val PARAMETER_REGEX = Regex(PARAMETER)
        val PLACEHOLDER_REGEX = Regex("\\$WILDCARD|$PARAMETER")

        fun isTemplate(pattern: String): Boolean =
            REGEX_CHARACTERS.any { pattern.contains(it) } || PLACEHOLDER_REGEX in pattern

        fun patternToRegex(pattern: String, prefix: Boolean): String {
            return pattern
                .replace(WILDCARD, "(.*?)")
                .replaceParameters(parameters(pattern))
                .let { if (prefix) it else "$it$" }
        }

        private fun parameters(pattern: String): Map<String, String> =
            PARAMETER_REGEX.findAll(pattern)
                .map {
                    val (_, k, re) = it.groupValues
                    k to re.ifEmpty { VARIABLE_PATTERN }
                }
                .toMap()

        private fun String.replaceParameters(parameters: Map<String, String>): String =
            parameters
                .entries
                .fold(this) { accumulator, (k, v) ->
                    val re = if (v == VARIABLE_PATTERN) "" else v
                    val search = "$PARAMETER_PREFIX$k$re$PARAMETER_SUFFIX"
                    val replacement = "(?<$k>${v.removePrefix(":")}?)"
                    accumulator.replace(search, replacement)
                }
    }

    val parameters: List<String> = parameters(pattern).keys.toList()

    init {
        checkPathPatternPrefix(pattern, listOf("*"))
    }

    override fun insertParameters(parameters: Map<String, Any>): String {
        val keys = parameters.keys
        val patternParameters = this.parameters

        require(keys.toSet() == patternParameters.toSet()) {
            "Parameters must match pattern's parameters($patternParameters). Provided: $keys"
        }

        return parameters.entries.fold(pattern) { accumulator, (k, v) ->
            val re = Regex("\\$PARAMETER_PREFIX$k(:.+?)?$PARAMETER_SUFFIX")
            accumulator.replace(re, v.toString())
        }
    }
}