Permalink
Branch: master
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
231 lines (207 sloc) 5.77 KB
package com.twitter.finagle.http.path
import com.twitter.finagle.http.{ParamMap, Method}
/** Base class for path extractors. */
abstract class Path {
def /(child: String): / = new /(this, child)
def :?(params: ParamMap): :? = new :?(this, params)
def toList: List[String]
def parent: Path
def lastOption: Option[String]
def startsWith(other: Path): Boolean
}
object Path {
def apply(str: String): Path =
if (str == "" || str == "/")
Root
else if (!str.startsWith("/"))
Path("/" + str)
else {
val slash = str.lastIndexOf('/')
val prefix = Path(str.substring(0, slash))
if (slash == str.length - 1)
prefix
else
prefix / str.substring(slash + 1)
}
def apply(first: String, rest: String*): Path =
rest.foldLeft(Root / first)(_ / _)
def apply(list: List[String]): Path = list.foldLeft(Root: Path)(_ / _)
def unapplySeq(path: Path): Option[List[String]] = Some(path.toList)
}
case class :?(path: Path, params: ParamMap) {
override def toString: String = params.toString
}
/** File extension extractor */
object ~ {
/**
* File extension extractor for Path:
* Path("example.json") match {
* case Root / "example" ~ "json" => ...
*/
def unapply(path: Path): Option[(Path, String)] = {
path match {
case Root => None
case parent / last =>
unapply(last).map {
case (base, ext) => (parent / base, ext)
}
}
}
/**
* File extension matcher for String:
* "example.json" match {
* case => "example" ~ "json" => ...
*/
def unapply(fileName: String): Option[(String, String)] = {
fileName.lastIndexOf('.') match {
case -1 => Some((fileName, ""))
case index => Some((fileName.substring(0, index), fileName.substring(index + 1)))
}
}
}
/** HttpMethod extractor */
object -> {
/**
* HttpMethod extractor:
* (request.method, Path(request.path)) match {
* case Methd.Get -> Root / "test.json" => ...
*/
def unapply(x: (Method, Path)): Some[(Method, Path)] = Some(x)
}
/**
* Path separator extractor:
* Path("/1/2/3/test.json") match {
* case Root / "1" / "2" / "3" / "test.json" => ...
*/
case class /(parent: Path, child: String) extends Path {
lazy val toList: List[String] = parent.toList ++ List(child)
def lastOption: Option[String] = Some(child)
lazy val asString: String = parent.toString + "/" + child
override def toString: String = asString
def startsWith(other: Path): Boolean = {
val components = other.toList
(toList take components.length) == components
}
}
/**
* Root extractor:
* Path("/") match {
* case Root => ...
* }
*/
case object Root extends Path {
def toList: List[String] = Nil
def parent: Path = this
def lastOption: Option[String] = None
override def toString: String = ""
def startsWith(other: Path): Boolean = other == Root
}
/**
* Path separator extractor:
* Path("/1/2/3/test.json") match {
* case "1" /: "2" /: _ => ...
*/
object /: {
def unapply(path: Path): Option[(String, Path)] = {
path.toList match {
case Nil => None
case head :: tail => Some((head, Path(tail)))
}
}
}
// Base class for Integer and Long extractors.
protected class Numeric[A <: AnyVal](cast: String => A) {
def unapply(str: String): Option[A] = {
if (!str.isEmpty &&
(str.head == '-' || Character.isDigit(str.head)) &&
str.drop(1).forall(Character.isDigit)) try {
Some(cast(str))
} catch {
case _: NumberFormatException =>
None
} else
None
}
}
/**
* Integer extractor:
* Path("/user/123") match {
* case Root / "user" / Int(userId) => ...
*/
object Integer extends Numeric(_.toInt)
/**
* Long extractor:
* Path("/user/123") match {
* case Root / "user" / Long(userId) => ...
*/
object Long extends Numeric(_.toLong)
/**
* Multiple param extractor:
* object A extends ParamMatcher("a")
* object B extends ParamMatcher("b")
* (Path(request.path) :? request.params) match {
* case Root / "user" :? A(a) :& B(b) => ...
*/
object :& {
def unapply(params: ParamMap): Some[(ParamMap, ParamMap)] = Some((params, params))
}
/**
* Param extractor:
* object ScreenName extends ParamMatcher("screen_name")
* (Path(request.path) :? request.params) match {
* case Root / "user" :? ScreenName(screenName) => ...
*/
abstract class ParamMatcher(name: String) {
def unapply(params: ParamMap): Option[String] = params.get(name)
}
/**
* Int param extractor:
* object Page extends IntParamMatcher("page")
* (Path(request.path) :? request.params) match {
* case Root / "blog" :? Page(page) => ...
*/
abstract class IntParamMatcher(name: String) {
def unapply(params: ParamMap): Option[Int] =
params.get(name).flatMap { value =>
try {
Some(value.toInt)
} catch {
case ex: NumberFormatException =>
None
}
}
}
/**
* Long param extractor:
* object UserId extends LongParamMatcher("user_id")
* (Path(request.path) :? request.params) match {
* case Root / "user" :? UserId(userId) => ...
*/
abstract class LongParamMatcher(name: String) {
def unapply(params: ParamMap): Option[Long] =
params.get(name).flatMap { value =>
try {
Some(value.toLong)
} catch {
case ex: NumberFormatException =>
None
}
}
}
/**
* Double param extractor:
* object Latitude extends DoubleParamMatcher("lat")
* (Path(request.path) :? request.params) match {
* case Root / "closest" :? Latitude("lat") => ...
*/
abstract class DoubleParamMatcher(name: String) {
def unapply(params: ParamMap): Option[Double] =
params.get(name).flatMap { value =>
try {
Some(value.toDouble)
} catch {
case ex: NumberFormatException =>
None
}
}
}