pl.iterators.stir.server.PathMatcher.scala Maven / Gradle / Ivy
The newest version!
package pl.iterators.stir.server
import org.http4s.Uri.Path
import pl.iterators.stir.common.NameOptionReceptacle
import pl.iterators.stir.util.Tuple
import pl.iterators.stir.util.TupleOps.Join
import java.util.UUID
import scala.annotation.{ nowarn, tailrec }
import scala.util.matching.Regex
/**
* A PathMatcher tries to match a prefix of a given string and returns either a PathMatcher.Matched instance
* if matched, otherwise PathMatcher.Unmatched.
*/
abstract class PathMatcher[L](implicit val ev: Tuple[L]) extends (Path => PathMatcher.Matching[L]) { self =>
import PathMatcher._
/** Alias for [[slash]]. */
def / : PathMatcher[L] = slash
def slash: PathMatcher[L] = this ~ PathMatchers.Slash
/** Alias for [[slash]]. */
def /[R](other: PathMatcher[R])(implicit join: Join[L, R]): PathMatcher[join.Out] = slash(other)
def slash[R](other: PathMatcher[R])(implicit join: Join[L, R]): PathMatcher[join.Out] =
this ~ PathMatchers.Slash ~ other
/** Alias for [[or]]. */
def |[R >: L: Tuple](other: PathMatcher[_ <: R]): PathMatcher[R] = or(other)
def or[R >: L: Tuple](other: PathMatcher[_ <: R]): PathMatcher[R] =
new PathMatcher[R] {
def apply(path: Path) = self(path).orElse(other(path))
}
/** Alias for [[append]]. */
def ~[R](other: PathMatcher[R])(implicit join: Join[L, R]): PathMatcher[join.Out] = append(other)
def append[R](other: PathMatcher[R])(implicit join: Join[L, R]): PathMatcher[join.Out] = {
implicit val joinProducesTuple = Tuple.yes[join.Out]
transform(_.andThen((restL, valuesL) => other(restL).map(join(valuesL, _))))
}
/** Operator alternative to [[PathMatchers.not]] */
def unary_! : PathMatcher0 = PathMatchers.not(self)
def transform[R: Tuple](f: Matching[L] => Matching[R]): PathMatcher[R] =
new PathMatcher[R] { def apply(path: Path) = f(self(path)) }
def tmap[R: Tuple](f: L => R): PathMatcher[R] = transform(_.map(f))
def tflatMap[R: Tuple](f: L => Option[R]): PathMatcher[R] = transform(_.flatMap(f))
/**
* Same as `repeat(min = count, max = count)`.
*/
def repeat(count: Int)(implicit lift: PathMatcher.Lift[L, List]): PathMatcher[lift.Out] =
repeat(min = count, max = count)
/**
* Same as `repeat(min = count, max = count, separator = separator)`.
*/
def repeat(count: Int, separator: PathMatcher0)(implicit lift: PathMatcher.Lift[L, List]): PathMatcher[lift.Out] =
repeat(min = count, max = count, separator = separator)
/**
* Turns this `PathMatcher` into one that matches a number of times (with the given separator)
* and potentially extracts a `List` of the underlying matcher's extractions.
* If less than `min` applications of the underlying matcher have succeeded the produced matcher fails,
* otherwise it matches up to the given `max` number of applications.
* Note that it won't fail even if more than `max` applications could succeed!
* The "surplus" path elements will simply be left unmatched.
*
* The result type depends on the type of the underlying matcher:
*
*
* If a `matcher` is of type then `matcher.repeat(...)` is of type
* `PathMatcher0` `PathMatcher0`
* `PathMatcher1[T]` `PathMatcher1[List[T]`
* `PathMatcher[L :Tuple]` `PathMatcher[List[L]]`
*
*/
def repeat(min: Int, max: Int, separator: PathMatcher0 = PathMatchers.Neutral)(
implicit lift: PathMatcher.Lift[L, List]): PathMatcher[lift.Out] =
new PathMatcher[lift.Out]()(lift.OutIsTuple) {
require(min >= 0, "`min` must be >= 0")
require(max >= min, "`max` must be >= `min`")
def apply(path: Path) = matchNext(path, 0)
def matchNext(path: Path, alreadyFound: Int): Matching[lift.Out] = {
def done = if (alreadyFound >= min) Matched(path, lift()) else Unmatched
def matchSeparatorIfNeeded(path: Path): Matching[Unit] =
if (alreadyFound == 0) Matched(path, ()) else separator(path)
def matchElement(start: Path): Matching[lift.Out] =
self(start)
.andThen { (remaining, extractions) =>
matchNext(remaining, alreadyFound + 1)
.map(result => lift(extractions, result))
}
.orElse(done)
if (alreadyFound < max)
matchSeparatorIfNeeded(path)
.andThen { (remaining, _) => matchElement(remaining) }
.orElse(done)
else done
}
}
}
object PathMatcher extends ImplicitPathMatcherConstruction {
@nowarn
sealed abstract class Matching[+L: Tuple] {
def map[R: Tuple](f: L => R): Matching[R]
def flatMap[R: Tuple](f: L => Option[R]): Matching[R]
def andThen[R: Tuple](f: (Path, L) => Matching[R]): Matching[R]
def orElse[R >: L](other: => Matching[R]): Matching[R]
}
case class Matched[L: Tuple](pathRest: Path, extractions: L) extends Matching[L] {
def map[R: Tuple](f: L => R) = Matched(pathRest, f(extractions))
def flatMap[R: Tuple](f: L => Option[R]) = f(extractions) match {
case Some(valuesR) => Matched(pathRest, valuesR)
case None => Unmatched
}
def andThen[R: Tuple](f: (Path, L) => Matching[R]) = f(pathRest, extractions)
def orElse[R >: L](other: => Matching[R]) = this
}
object Matched { val Empty = Matched(Path.empty, ()) }
case object Unmatched extends Matching[Nothing] {
def map[R: Tuple](f: Nothing => R) = this
def flatMap[R: Tuple](f: Nothing => Option[R]) = this
def andThen[R: Tuple](f: (Path, Nothing) => Matching[R]) = this
def orElse[R](other: => Matching[R]) = other
}
/**
* Creates a PathMatcher that always matches, consumes nothing and extracts the given Tuple of values.
*/
def provide[L: Tuple](extractions: L): PathMatcher[L] =
new PathMatcher[L] {
def apply(path: Path) = Matched(path, extractions)(ev)
}
/**
* Creates a PathMatcher that matches and consumes the given path prefix and extracts the given list of extractions.
* If the given prefix is empty the returned PathMatcher matches always and consumes nothing.
*/
def apply[L: Tuple](prefix: Path, extractions: L): PathMatcher[L] =
if (prefix.isEmpty) provide(extractions)
else new PathMatcher[L] {
def apply(path: Path) = {
val pathRendered = path.renderString
val prefixRendered = prefix.renderString
if (pathRendered.startsWith(prefixRendered)) {
val remaining = pathRendered.substring(prefixRendered.length)
Matched(Path.unsafeFromString(remaining), extractions)(ev)
} else Unmatched
}
}
/** Provoke implicit conversions to PathMatcher to be applied */
def apply[L](magnet: PathMatcher[L]): PathMatcher[L] = magnet
implicit class PathMatcher1Ops[T](matcher: PathMatcher1[T]) {
def map[R](f: T => R): PathMatcher1[R] = matcher.tmap { case Tuple1(e) => Tuple1(f(e)) }
def flatMap[R](f: T => Option[R]): PathMatcher1[R] =
matcher.tflatMap { case Tuple1(e) => f(e).map(x => Tuple1(x)) }
}
implicit class EnhancedPathMatcher[L](underlying: PathMatcher[L]) {
def optional(implicit lift: PathMatcher.Lift[L, Option]): PathMatcher[lift.Out] =
new PathMatcher[lift.Out]()(lift.OutIsTuple) {
def apply(path: Path) = underlying(path) match {
case Matched(rest, extractions) => Matched(rest, lift(extractions))
case Unmatched => Matched(path, lift())
}
}
def ?(implicit lift: PathMatcher.Lift[L, Option]): PathMatcher[lift.Out] = optional(lift)
}
sealed trait Lift[L, M[+_]] {
type Out
def OutIsTuple: Tuple[Out]
def apply(): Out
def apply(value: L): Out
def apply(value: L, more: Out): Out
}
object Lift extends LowLevelLiftImplicits {
trait MOps[M[+_]] {
def apply(): M[Nothing]
def apply[T](value: T): M[T]
def apply[T](value: T, more: M[T]): M[T]
}
object MOps {
implicit val OptionMOps: MOps[Option] =
new MOps[Option] {
def apply(): Option[Nothing] = None
def apply[T](value: T): Option[T] = Some(value)
def apply[T](value: T, more: Option[T]): Option[T] = Some(value)
}
implicit val ListMOps: MOps[List] =
new MOps[List] {
def apply(): List[Nothing] = Nil
def apply[T](value: T): List[T] = value :: Nil
def apply[T](value: T, more: List[T]): List[T] = value :: more
}
}
implicit def liftUnit[M[+_]]: Lift[Unit, M] { type Out = Unit } =
new Lift[Unit, M] {
type Out = Unit
def OutIsTuple = implicitly[Tuple[Out]]
def apply() = ()
def apply(value: Unit) = value
def apply(value: Unit, more: Out) = value
}
implicit def liftSingleElement[A, M[+_]](implicit mops: MOps[M]): Lift[Tuple1[A], M] { type Out = Tuple1[M[A]] } =
new Lift[Tuple1[A], M] {
type Out = Tuple1[M[A]]
def OutIsTuple = implicitly[Tuple[Out]]
def apply() = Tuple1(mops())
def apply(value: Tuple1[A]) = Tuple1(mops(value._1))
def apply(value: Tuple1[A], more: Out) = Tuple1(mops(value._1, more._1))
}
}
trait LowLevelLiftImplicits {
import Lift._
implicit def default[T, M[+_]](implicit mops: MOps[M]): Lift[T, M] { type Out = Tuple1[M[T]] } =
new Lift[T, M] {
type Out = Tuple1[M[T]]
def OutIsTuple = implicitly[Tuple[Out]]
def apply() = Tuple1(mops())
def apply(value: T) = Tuple1(mops(value))
def apply(value: T, more: Out) = Tuple1(mops(value, more._1))
}
}
/** The empty match returned when a Regex matcher matches the empty path */
private[stir] val EmptyMatch = Matched(Path.empty, Tuple1(""))
private[stir] def pathWithSegments(path: Path, segments: Vector[Path.Segment]): Path = {
if (segments.isEmpty && !path.endsWithSlash) Path.empty
else if (segments.isEmpty && path.endsWithSlash) Path.Root
else Path(segments, absolute = path.absolute, endsWithSlash = path.endsWithSlash)
}
}
/**
* @groupname pathmatcherimpl Path matcher implicits
* @groupprio pathmatcherimpl 172
*/
trait ImplicitPathMatcherConstruction {
import PathMatcher._
/**
* Creates a PathMatcher that consumes (a prefix of) the first path segment
* (if the path begins with a segment) and extracts a given value.
*
* @group pathmatcherimpl
*/
implicit def _stringExtractionPair2PathMatcher[T](tuple: (String, T)): PathMatcher1[T] =
PathMatcher(Path(Vector(Path.Segment(tuple._1))).normalize, Tuple1(tuple._2))
/**
* Creates a PathMatcher that consumes (a prefix of) the first path segment
* (if the path begins with a segment).
*
* @group pathmatcherimpl
*/
implicit def _segmentStringToPathMatcher(segment: String): PathMatcher0 =
PathMatcher(Path(Vector(Path.Segment(segment))).normalize, ())
/**
* @group pathmatcherimpl
*/
implicit def _stringNameOptionReceptacle2PathMatcher(nr: NameOptionReceptacle[String]): PathMatcher0 =
PathMatcher(nr.name).?
/**
* Creates a PathMatcher that consumes (a prefix of) the first path segment
* if the path begins with a segment (a prefix of) which matches the given regex.
* Extracts either the complete match (if the regex doesn't contain a capture group) or
* the capture group (if the regex contains exactly one).
* If the regex contains more than one capture group the method throws an IllegalArgumentException.
*
* @group pathmatcherimpl
*/
implicit def _regex2PathMatcher(regex: Regex): PathMatcher1[String] = {
lazy val matchesEmptyPath = "" match {
case `regex`(_*) => true
case _ => false
}
regex.pattern.matcher("").groupCount() match {
case 0 => new PathMatcher1[String] {
def apply(path: Path) = path.segments match {
case segment +: tail => regex.findPrefixOf(segment.decoded()) match {
case Some(m) if segment.decoded().substring(m.length).nonEmpty =>
Matched(pathWithSegments(path, Path.Segment(segment.decoded().substring(m.length)) +: tail),
Tuple1(m))
case Some(m) => Matched(pathWithSegments(path.toAbsolute, tail), Tuple1(m))
case None => Unmatched
}
case _ if path.isEmpty && matchesEmptyPath => PathMatcher.EmptyMatch
case _ => Unmatched
}
}
case 1 => new PathMatcher1[String] {
def apply(path: Path) = path.segments match {
case segment +: tail => regex.findPrefixMatchOf(segment.decoded()) match {
case Some(m) if segment.decoded().substring(m.end).nonEmpty =>
Matched(pathWithSegments(path, Path.Segment(segment.decoded().substring(m.end)) +: tail),
Tuple1(m.group(1)))
case Some(m) => Matched(pathWithSegments(path, tail), Tuple1(m.group(1)))
case None => Unmatched
}
case _ if path.isEmpty && matchesEmptyPath => PathMatcher.EmptyMatch
case _ => Unmatched
}
}
case _ => throw new IllegalArgumentException("Path regex '" + regex.pattern.pattern +
"' must not contain more than one capturing group")
}
}
/**
* Creates a PathMatcher from the given Map of path segments (prefixes) to extracted values.
* If the unmatched path starts with a segment having one of the maps keys as a prefix
* the matcher consumes this path segment (prefix) and extracts the corresponding map value.
* For keys sharing a common prefix the longest matching prefix is selected.
*
* @group pathmatcherimpl
*/
implicit def _valueMap2PathMatcher[T](valueMap: Map[String, T]): PathMatcher1[T] =
if (valueMap.isEmpty) PathMatchers.nothingMatcher
else valueMap.toSeq.sortWith(_._1 > _._1).map(_stringExtractionPair2PathMatcher).reduceLeft(_ | _)
}
/**
* @groupname pathmatcher Path matchers
* @groupprio pathmatcher 171
*/
trait PathMatchers {
import PathMatcher._
def not(self: PathMatcher[_]): PathMatcher0 =
new PathMatcher[Unit] {
def apply(path: Path) = if (self(path) eq Unmatched) Matched(path, ()) else Unmatched
}
/**
* Converts a path string containing slashes into a PathMatcher that interprets slashes as
* path segment separators.
*
* @group pathmatcher
*/
def separateOnSlashes(string: String): PathMatcher0 = {
@tailrec def split(ix: Int = 0, matcher: PathMatcher0 = null): PathMatcher0 = {
val nextIx = string.indexOf('/', ix)
def append(m: PathMatcher0) = if (matcher eq null) m else matcher / m
if (nextIx < 0) append(string.substring(ix))
else split(nextIx + 1, append(string.substring(ix, nextIx)))
}
split()
}
/**
* A PathMatcher that matches a single slash character ('/').
*
* @group pathmatcher
*/
object Slash extends PathMatcher0 {
def apply(path: Path) = {
if (!path.absolute) Unmatched
else if (path.segments.isEmpty) Matched(Path.empty, ())
else Matched(Path(path.segments, absolute = false, path.endsWithSlash), ())
}
}
/**
* A PathMatcher that matches the very end of the requests URI path.
*
* @group pathmatcher
*/
object PathEnd extends PathMatcher0 {
def apply(path: Path) = path match {
case path if path.segments.isEmpty && !path.endsWithSlash => Matched.Empty
case _ => Unmatched
}
}
/**
* A PathMatcher that matches and extracts the complete remaining,
* unmatched part of the request's URI path as an (encoded!) String.
* If you need access to the remaining unencoded elements of the path
* use the `RemainingPath` matcher!
*
* @group pathmatcher
*/
object Remaining extends PathMatcher1[String] {
def apply(path: Path) = Matched(Path.empty, Tuple1(path.toString))
}
/**
* A PathMatcher that matches and extracts the complete remaining,
* unmatched part of the request's URI path.
*
* @group pathmatcher
*/
object RemainingPath extends PathMatcher1[Path] {
def apply(path: Path) = Matched(Path.empty, Tuple1(path))
}
/**
* A PathMatcher that efficiently matches a number of digits and extracts their (non-negative) Int value.
* The matcher will not match 0 digits or a sequence of digits that would represent an Int value larger
* than Int.MaxValue.
*
* @group pathmatcher
*/
object IntNumber extends NumberMatcher[Int](Int.MaxValue, 10) {
def fromChar(c: Char) = fromDecimalChar(c)
}
/**
* A PathMatcher that efficiently matches a number of digits and extracts their (non-negative) Long value.
* The matcher will not match 0 digits or a sequence of digits that would represent an Long value larger
* than Long.MaxValue.
*
* @group pathmatcher
*/
object LongNumber extends NumberMatcher[Long](Long.MaxValue, 10) {
def fromChar(c: Char) = fromDecimalChar(c)
}
/**
* A PathMatcher that efficiently matches a number of hex-digits and extracts their (non-negative) Int value.
* The matcher will not match 0 digits or a sequence of digits that would represent an Int value larger
* than Int.MaxValue.
*
* @group pathmatcher
*/
object HexIntNumber extends NumberMatcher[Int](Int.MaxValue, 16) {
def fromChar(c: Char) = fromHexChar(c)
}
/**
* A PathMatcher that efficiently matches a number of hex-digits and extracts their (non-negative) Long value.
* The matcher will not match 0 digits or a sequence of digits that would represent an Long value larger
* than Long.MaxValue.
*
* @group pathmatcher
*/
object HexLongNumber extends NumberMatcher[Long](Long.MaxValue, 16) {
def fromChar(c: Char) = fromHexChar(c)
}
// common implementation of Number matchers
/**
* @group pathmatcher
*/
abstract class NumberMatcher[@specialized(Int, Long) T](max: T, base: T)(implicit x: Integral[T])
extends PathMatcher1[T] {
import x._ // import implicit conversions for numeric operators
val minusOne = x.zero - x.one
val maxDivBase = max / base
def apply(path: Path) = path.segments match {
case s +: tail =>
val segment = s.decoded()
@tailrec def digits(ix: Int = 0, value: T = minusOne): Matching[Tuple1[T]] = {
val a = if (ix < segment.length) fromChar(segment.charAt(ix)) else minusOne
if (a == minusOne) {
if (value == minusOne) Unmatched
else Matched(
if (ix < segment.length) pathWithSegments(path, Path.Segment(segment.substring(ix)) +: tail)
else pathWithSegments(path, tail),
Tuple1(value))
} else {
if (value == minusOne) digits(ix + 1, a)
else if (value <= maxDivBase && value * base <= max - a) // protect from overflow
digits(ix + 1, value * base + a)
else Unmatched
}
}
digits()
case _ => Unmatched
}
def fromChar(c: Char): T
def fromDecimalChar(c: Char): T = if ('0' <= c && c <= '9') x.fromInt(c - '0') else minusOne
def fromHexChar(c: Char): T =
if ('0' <= c && c <= '9') x.fromInt(c - '0')
else {
val cn = c | 0x20 // normalize to lowercase
if ('a' <= cn && cn <= 'f') x.fromInt(cn - 'a' + 10) else minusOne
}
}
/**
* A PathMatcher that matches and extracts a Double value. The matched string representation is the pure decimal,
* optionally signed form of a double value, i.e. without exponent.
*
* @group pathmatcher
*/
val DoubleNumber: PathMatcher1[Double] =
PathMatcher("""[+-]?\d*\.?\d*""".r).flatMap { string =>
try Some(java.lang.Double.parseDouble(string))
catch { case _: NumberFormatException => None }
}
/**
* A PathMatcher that matches and extracts a java.util.UUID instance.
*
* @group pathmatcher
*/
val JavaUUID: PathMatcher1[UUID] =
PathMatcher("""[\da-fA-F]{8}-[\da-fA-F]{4}-[\da-fA-F]{4}-[\da-fA-F]{4}-[\da-fA-F]{12}""".r)
.map(UUID.fromString)
/**
* A PathMatcher that always matches, doesn't consume anything and extracts nothing.
* Serves mainly as a neutral element in PathMatcher composition.
*
* @group pathmatcher
*/
val Neutral: PathMatcher0 = PathMatcher.provide(())
/**
* A PathMatcher that matches if the unmatched path starts with a path segment.
* If so the path segment is extracted as a String.
*
* @group pathmatcher
*/
object Segment extends PathMatcher1[String] {
def apply(path: Path) = path.segments match {
case segment +: tail if !path.absolute =>
Matched(Path(tail, tail.nonEmpty, path.endsWithSlash), Tuple1(segment.decoded()))
case _ => Unmatched
}
}
/**
* A PathMatcher that matches up to 128 remaining segments as a List[String].
* This can also be no segments resulting in the empty list.
* If the path has a trailing slash this slash will *not* be matched.
*
* @group pathmatcher
*/
val Segments: PathMatcher1[List[String]] = Segments(min = 0, max = 128)
/**
* A PathMatcher that matches the given number of path segments (separated by slashes) as a List[String].
* If there are more than `count` segments present the remaining ones will be left unmatched.
* If the path has a trailing slash this slash will *not* be matched.
*
* @group pathmatcher
*/
def Segments(count: Int): PathMatcher1[List[String]] = Segment.repeat(count, separator = Slash)
/**
* A PathMatcher that matches between `min` and `max` (both inclusively) path segments (separated by slashes)
* as a List[String]. If there are more than `count` segments present the remaining ones will be left unmatched.
* If the path has a trailing slash this slash will *not* be matched.
*
* @group pathmatcher
*/
def Segments(min: Int, max: Int): PathMatcher1[List[String]] = Segment.repeat(min, max, separator = Slash)
/**
* A PathMatcher that never matches anything.
*
* @group pathmatcher
*/
def nothingMatcher[L: Tuple]: PathMatcher[L] =
new PathMatcher[L] {
def apply(p: Path) = Unmatched
}
}
object PathMatchers extends PathMatchers