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

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 typethen `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




© 2015 - 2024 Weber Informatics LLC | Privacy Policy