.circumflex-web.2.2.source-code.matchers.scala Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of circumflex-web Show documentation
Show all versions of circumflex-web Show documentation
Circumflex Web Framework is a lightweight framework
for quick and robust application development using Scala
programming language.
package ru.circumflex
package web
import util.matching.Regex
import scala.collection.immutable.Map
import collection.Iterator
/*!# Matchers
The `Matcher` trait and the `MatchResult` class are the cornerstone of request routing.
Matchers define mechanisms which perform request matching. They yield zero or more
match results on successful match and are used in routes definition.
Match results are subsequently used inside matched route's block.
*/
/*!## Match Results
The results of matching contain information about successful match. The `name` reflects
the name of the `Matcher` which yielded this match result, and the `params` contains
strings captured by `Matcher`. The special name `splat` is assigned to parameters with
unknown name (`+`, `*` or any group, if you use regular expressions).
*/
class MatchResult(val name: String,
val params: (String, String)*) extends Map[String, String] {
def +[B1 >: String](kv: (String, B1)): Map[String, B1] = this
def -(key: String): Map[String, String] = this
def iterator: Iterator[(String, String)] = params.iterator
def get(index: Int): Option[String] =
if (params.indices.contains(index)) Some(params(index)._2)
else None
def get(name: String): Option[String] = params.find(_._1 == name) match {
case Some(param: Pair[String, String]) => Some(param._2)
case _ => None
}
def apply(): String = apply(0)
def apply(index: Int): String = get(index).getOrElse("")
def splat: Seq[String] = params.filter(_._1 == "splat").map(_._2).toSeq
override def default(key: String): String = ""
override def toString() = apply(0)
}
/*! Matchers can be composed together using the `&` method. The `CompositeMatcher` will
only yield match results if all it's matchers succeed.*/
trait Matcher {
def apply(): Option[Seq[MatchResult]]
def add(matcher: Matcher): CompositeMatcher
def &(matcher: Matcher) = add(matcher)
}
trait AtomicMatcher extends Matcher {
def name: String
def add(matcher: Matcher) = new CompositeMatcher()
.add(this)
.add(matcher)
}
class CompositeMatcher extends Matcher {
private var _matchers: Seq[Matcher] = Nil
def matchers = _matchers
def add(matcher: Matcher): CompositeMatcher = {
_matchers ++= List(matcher)
this
}
def apply() = try {
val matches = _matchers.flatMap(_.apply() match {
case Some(matches: Seq[MatchResult]) => matches
case _ => throw new MatchError
})
if (matches.size > 0) Some(matches)
else None
} catch {
case e: MatchError => None
}
}
/*!## The `RegexMatcher`
The `RegexMatcher` is designed to provide common request matching functionality to all
matchers.
It can be used either with regular expressions or with String expressions.
When using regular expressions, if match is successful, the matched groups can be
accessed using the `params` method of corresponding `MatchResult`.
When using String expressions, following processing occurs:
* the characters `.`, `(` and `)` are escaped so that they are not mistreated by
regex engine;
* the named parameters like ":param" are recognized within the expression; they
are transformed into reluctant regex groups `([^/?.]+?)` which match any
characters except `/`, `?`, `?`, `&`, `#` and `.`;
* all occurences of the `*` character is replaced with reluctant groups `(.*?)`
which match zero or more characters;
* all occurences of the `+` character is replaced with reluctant groups `(.+?)`
which match one or more characters;
* `?` remains the same and indicates that the preceding character is optional
for matching (for example, `get("/files/?")` matches both `/files` and `/files/`
requests).
Then, if match is successful, named parameters are accessible by their name from
the corresponding `MatchResult`. All other parameters are accessible via the `params`
method (note that named parameters are groups too, so they appear inside `params`
and have their index as well).
*/
class RegexMatcher(val name: String,
val value: String,
protected var regex: Regex,
protected var groupNames: Seq[String] = Nil) extends AtomicMatcher {
def this(name: String, value: String, pattern: String) = {
this(name, value, null, Nil)
processPattern(pattern)
}
protected def processPattern(pattern: String) {
this.groupNames = List("splat") // for `group(0)`
this.regex = (""":\w+|[\*+.()]""".r.replaceAllIn(pattern, m => m.group(0) match {
case "*" | "+" =>
groupNames ++= List("splat")
"(." + m.group(0) + "?)"
case "." | "(" | ")" =>
"\\\\" + m.group(0)
case _ =>
groupNames ++= List(m.group(0).substring(1))
"([^/?#]+?)"
})).r
}
def groupName(index: Int): String=
if (groupNames.indices.contains(index)) groupNames(index)
else "splat"
def apply(): Option[Seq[MatchResult]] = {
val m = regex.pattern.matcher(value)
if (m.matches) {
val matches = (0 to m.groupCount).map(i => groupName(i) -> m.group(i))
Some(List(new MatchResult(name, matches: _*)))
} else None
}
}
/*! `HeaderMatcher` is used to match the requests by contents of their headers. */
class HeaderMatcher(name: String,
regex: Regex,
groupNames: Seq[String] = Nil)
extends RegexMatcher(name, request.headers.getOrElse(name,""), regex, groupNames) {
def this(name: String, pattern: String) = {
this(name, null, Nil)
processPattern(pattern)
}
}
/*! `HeaderMatcherHelper` provides DSL for matching requests by headers. See `matchers` object
in package `ru.circumflex.web` for more information. */
class HeaderMatcherHelper(name: String) {
def apply(regex: Regex, groupNames: Seq[String] = Nil) =
new HeaderMatcher(name, regex, groupNames)
def apply(pattern: String) = new HeaderMatcher(name, pattern)
}