TemplatePathPattern.kt

  1. package com.hexagonkt.http.patterns

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

  15.     internal companion object {
  16.         private const val PARAMETER_PREFIX = "{"
  17.         private const val PARAMETER_SUFFIX = "}"

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

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

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

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

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

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

  40.         private fun String.replaceParameters(parameters: Map<String, String>): String =
  41.             parameters
  42.                 .entries
  43.                 .fold(this) { accumulator, (k, v) ->
  44.                     val re = if (v == VARIABLE_PATTERN) "" else v
  45.                     val search = "$PARAMETER_PREFIX$k$re$PARAMETER_SUFFIX"
  46.                     val replacement = "(?<$k>${v.removePrefix(":")}?)"
  47.                     accumulator.replace(search, replacement)
  48.                 }
  49.     }

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

  51.     init {
  52.         checkPathPatternPrefix(pattern, listOf("*"))
  53.     }

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

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

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