diff --git a/manual/introduction.md b/manual/introduction.md
index 16a1dfc..053fb46 100644
--- a/manual/introduction.md
+++ b/manual/introduction.md
@@ -1,5 +1,5 @@
# Introduction
-Trail is a routing library for Scala. It allows to define type-safe routes, generate URLs and perform pattern matching.
+Trail is a routing library for Scala. It allows defining type-safe routes, generating URLs and performing pattern matching.
## Installation
Add the following dependencies to your build configuration:
@@ -10,35 +10,46 @@ libraryDependencies += "tech.sparse" %%% "trail" % "%version%" // Scala.js, Sca
```
## Usage
-Create a route:
+First, import Trail's DSL and define a type-safe route:
-Create a string URL by filling the placeholders of the route:
+To fill the route's placeholders, call the `url()` or `apply()` functions:
-Parse an URL:
-
+When parsing an URL, Trail maps the values onto Scala types. The result will also contain any unmatched path elements, arguments or the fragment:
+
-Define and parse a route with a query parameter:
+We will now define a route with one query parameter. Here, we are only interested in the arguments and exact path matches. For this use case, Trail provides the function `parseArgs()`:
-A query parameter may be optional:
+The output shows that additional arguments are still permitted. If this is undesired, you can call `parseArgsStrict()` to parse a route more strictly:
+
+
+Routes may specify optional query parameters:
-A fragment can be specified:
+You can match fragments, too:
-Create a routing table:
-
+Since `parseArgs()` disallows additional path elements, you can match them only on specific routes using `Elems`. It should be the last DSL combinator in the route definition:
+
+
+Similarly, additional parameters can be matched with `Params`:
+
+
+Routing tables can be expressed with pattern matching:
+
+
+The underlying `unapply()` function calls `parseArgs()` instead of `parse()`. Therefore, the ordering of routes does not impact precedence.
-Note that you can populate the `trail.Path()` data structure yourself and use it in place of its string counterpart. This is useful if you are using a third-party routing library which already provides you with a deconstructed URL:
+You may populate the `trail.Path()` data structure yourself and use it in place of an URL. This is useful if an HTTP server already provides the path and arguments of requests:
-Define a custom codec that can be used in arguments, parameters and fragments:
+Trail defines codecs for common Scala types. You can define a custom codec for any type. These codecs can be used in arguments, parameters and fragments:
-Define a custom path element type:
+It is possible to define a custom path element type, too:
-Encode and decode [URI values](https://en.wikipedia.org/wiki/Percent-encoding):
+Trail provides helper utilities, for example to encode and decode [URI values](https://en.wikipedia.org/wiki/Percent-encoding):
diff --git a/manual/listings.json b/manual/listings.json
index a4a29c0..ebf43ae 100644
--- a/manual/listings.json
+++ b/manual/listings.json
@@ -4,40 +4,55 @@
"language" : "scala",
"result" : "%C3%A4%C3%B6%C3%BC\näöü"
},
- "map" : {
- "code" : "println(details.parse(\"/details/42\"))",
+ "url" : {
+ "code" : "println(details.url(1))\nprintln(details(1))",
"language" : "scala",
- "result" : "Some(42)"
+ "result" : "/details/1\n/details/1"
},
- "url" : {
- "code" : "println(details.url(1)) // Shorter: details(1)",
+ "query-params-strict" : {
+ "code" : "println(route.parseArgsStrict(\"/details/sub-page\"))\nprintln(route.parseArgsStrict(\"/details?show=false\"))\nprintln(route.parseArgsStrict(\"/details?show=false&a=b\"))\nprintln(route.parseArgsStrict(\"/details#frag\"))",
"language" : "scala",
- "result" : "/details/1"
+ "result" : "None\nSome(false)\nNone\nNone"
},
"parse-path" : {
- "code" : "val result2 = trail.Path(\"/user/hello\", List(\"show\" -> \"false\")) match {\n case details (a) => s\"details: $a\"\n case userInfo((u, s)) => s\"user: $u, show: $s\"\n}\nprintln(result2)",
+ "code" : "val (requestPath, requestParams) = (\"/user/hello\", List(\"show\" -> \"false\"))\nval result2 = trail.Path(requestPath, requestParams) match {\n case details (a) => s\"details: $a\"\n case userInfo((u, s)) => s\"user: $u, show: $s\"\n}\nprintln(result2)",
"language" : "scala",
"result" : "user: hello, show: false"
},
+ "additional-elems" : {
+ "code" : "val routeAdditionalElems = Root / \"a\" / Elems\nprintln(routeAdditionalElems.parseArgs(\"/a/b/c\"))",
+ "language" : "scala",
+ "result" : "Some(List(b, c))"
+ },
"query-params-opt" : {
- "code" : "val routeParamsOpt = Root / \"details\" & Param[Int](\"id\") & Param[Option[Boolean]](\"show\")\nprintln(routeParamsOpt.parse(\"/details?id=42\"))",
+ "code" : "val routeParamsOpt = Root / \"details\" & Param[Int](\"id\") & Param[Option[Boolean]](\"show\")\nprintln(routeParamsOpt.parseArgs(\"/details?id=42\"))",
"language" : "scala",
"result" : "Some((42,None))"
},
"parse" : {
+ "code" : "println(details.parse(\"/details/42\"))\nprintln(details.parse(\"/details/42/sub-page?name=value\"))\nprintln(details.parse(\"/details/42/sub-page?name=value#frag\"))",
+ "language" : "scala",
+ "result" : "Some((42,Path(,List(),None)))\nSome((42,Path(sub-page,List((name,value)),None)))\nSome((42,Path(sub-page,List((name,value)),Some(frag))))"
+ },
+ "query-params" : {
+ "code" : "val route = Root / \"details\" & Param[Boolean](\"show\")\nprintln(route.parseArgs(\"/details/sub-page\"))\nprintln(route.parseArgs(\"/details?show=false\"))\nprintln(route.parseArgs(\"/details?show=false&a=b\"))\nprintln(route.parseArgs(\"/details#frag\"))",
+ "language" : "scala",
+ "result" : "None\nSome(false)\nSome(false)\nNone"
+ },
+ "routing-table" : {
"code" : "val userInfo = Root / \"user\" / Arg[String] & Param[Boolean](\"show\")\n\nval result = \"/user/hello?show=false\" match {\n case details (a) => s\"details: $a\"\n case userInfo((u, s)) => s\"user: $u, show: $s\"\n}\nprintln(result)",
"language" : "scala",
"result" : "user: hello, show: false"
},
- "query-params" : {
- "code" : "val route = Root / \"details\" & Param[Boolean](\"show\")\nprintln(route.parse(\"/details?show=false\"))",
+ "additional-params" : {
+ "code" : "val routeAdditionalParams = Root / Arg[String] & Params\nprintln(routeAdditionalParams.parseArgs(\"/a?param1=value1¶m2=value2\"))",
"language" : "scala",
- "result" : "Some(false)"
+ "result" : "Some((a,List((param1,value1), (param2,value2))))"
},
"route" : {
- "code" : "import trail._\nimport shapeless._\n\nval details = Root / \"details\" / Arg[Int]\nprintln(details)",
+ "code" : "import trail._\nval details = Root / \"details\" / Arg[Int]",
"language" : "scala",
- "result" : "ConcatRight(ConcatRight(Root,Static(details)),Dynamic(Arg()))"
+ "result" : null
},
"custom-path-elem" : {
"code" : "case class Foo(bar: String)\nimplicit object FooElement extends StaticElement[Foo](_.bar)\n\nprintln((Root / Foo(\"asdf\")).url(()))",
@@ -50,7 +65,7 @@
"result" : "/export/1,2,3"
},
"query-fragment" : {
- "code" : "val routeFragment = Root $ Fragment[Int]\nprintln(routeFragment.parse(\"/#42\"))",
+ "code" : "val routeFragment = Root $ Fragment[Int]\nprintln(routeFragment.parseArgs(\"/#42\"))",
"language" : "scala",
"result" : "Some(42)"
}
diff --git a/manual/src/main/scala/trail/manual/Listings.scala b/manual/src/main/scala/trail/manual/Listings.scala
index 426834f..5af61ab 100644
--- a/manual/src/main/scala/trail/manual/Listings.scala
+++ b/manual/src/main/scala/trail/manual/Listings.scala
@@ -7,30 +7,47 @@ object Listings extends App {
listing("route")
import trail._
- import shapeless._
-
val details = Root / "details" / Arg[Int]
- println(details)
listing("url")
- println(details.url(1)) // Shorter: details(1)
+ println(details.url(1))
+ println(details(1))
- listing("map")
+ listing("parse")
println(details.parse("/details/42"))
+ println(details.parse("/details/42/sub-page?name=value"))
+ println(details.parse("/details/42/sub-page?name=value#frag"))
listing("query-params")
val route = Root / "details" & Param[Boolean]("show")
- println(route.parse("/details?show=false"))
+ println(route.parseArgs("/details/sub-page"))
+ println(route.parseArgs("/details?show=false"))
+ println(route.parseArgs("/details?show=false&a=b"))
+ println(route.parseArgs("/details#frag"))
+
+ listing("query-params-strict")
+ println(route.parseArgsStrict("/details/sub-page"))
+ println(route.parseArgsStrict("/details?show=false"))
+ println(route.parseArgsStrict("/details?show=false&a=b"))
+ println(route.parseArgsStrict("/details#frag"))
listing("query-params-opt")
val routeParamsOpt = Root / "details" & Param[Int]("id") & Param[Option[Boolean]]("show")
- println(routeParamsOpt.parse("/details?id=42"))
+ println(routeParamsOpt.parseArgs("/details?id=42"))
listing("query-fragment")
val routeFragment = Root $ Fragment[Int]
- println(routeFragment.parse("/#42"))
+ println(routeFragment.parseArgs("/#42"))
- listing("parse")
+ listing("additional-elems")
+ val routeAdditionalElems = Root / "a" / Elems
+ println(routeAdditionalElems.parseArgs("/a/b/c"))
+
+ listing("additional-params")
+ val routeAdditionalParams = Root / Arg[String] & Params
+ println(routeAdditionalParams.parseArgs("/a?param1=value1¶m2=value2"))
+
+ listing("routing-table")
val userInfo = Root / "user" / Arg[String] & Param[Boolean]("show")
val result = "/user/hello?show=false" match {
@@ -40,7 +57,8 @@ object Listings extends App {
println(result)
listing("parse-path")
- val result2 = trail.Path("/user/hello", List("show" -> "false")) match {
+ val (requestPath, requestParams) = ("/user/hello", List("show" -> "false"))
+ val result2 = trail.Path(requestPath, requestParams) match {
case details (a) => s"details: $a"
case userInfo((u, s)) => s"user: $u, show: $s"
}
diff --git a/shared/src/main/scala/trail/Path.scala b/shared/src/main/scala/trail/Path.scala
index 5ffb41f..c2c9227 100644
--- a/shared/src/main/scala/trail/Path.scala
+++ b/shared/src/main/scala/trail/Path.scala
@@ -64,4 +64,4 @@ object PathParser {
Path(path, args, fragment)
}
-}
\ No newline at end of file
+}
diff --git a/shared/src/main/scala/trail/Route.scala b/shared/src/main/scala/trail/Route.scala
index 745311d..827ae34 100644
--- a/shared/src/main/scala/trail/Route.scala
+++ b/shared/src/main/scala/trail/Route.scala
@@ -2,32 +2,114 @@ package trail
sealed trait Route[Args] {
def url(args: Args): String
- def parseInternal(path: Path): Option[(Args, Path)]
- def parse(path: Path): Option[Args] = parseInternal(path).map(_._1)
- def parse(uri: String): Option[Args] =
- parseInternal(PathParser.parse(uri)).map(_._1)
+ private def process(result: Option[(Args, Path)]): Option[Args] =
+ result.collect { case (args, p) if p.path.isEmpty => args }
- def apply(value: Args): String = url(value)
- def unapply(path: Path): Option[Args] = parse(path)
- def unapply(uri: String): Option[Args] = parse(uri)
+ private def processStrict(result: Option[(Args, Path)]): Option[Args] =
+ result.collect { case (args, p) if
+ p.path.isEmpty && p.args.isEmpty && p.fragment.isEmpty => args
+ }
+
+ /**
+ * Parse given [[Path]]
+ *
+ * If successful, the result contains the parsed arguments as well as the
+ * updated [[Path]] object. If the path was fully parsed, all of its fields
+ * should be empty.
+ *
+ * Use [[parseArgs]] or [[parseArgsStrict]] if you are only interested in the
+ * parsed arguments.
+ *
+ * @return Some((parsed arguments, updated path))
+ */
+ def parse(path: Path): Option[(Args, Path)]
+
+ /**
+ * Parse given URI
+ *
+ * @see parse(path: Path)
+ */
+ def parse(uri: String): Option[(Args, Path)] = parse(PathParser.parse(uri))
+
+ /**
+ * Parse arguments of given [[Path]]
+ *
+ * This returns None if `path` contains additional trailing elements missing
+ * from the route.
+ */
+ def parseArgs(path: Path): Option[Args] = process(parse(path))
+
+ /**
+ * Parse arguments of given URI
+ *
+ * @see parseArgs(path: Path)
+ */
+ def parseArgs(uri: String): Option[Args] = process(parse(uri))
+
+ /**
+ * Parse arguments of given [[Path]]
+ *
+ * Same behaviour as [[parseArgs]]. It does not permit any unspecified path
+ * elements, arguments or a fragment.
+ */
+ def parseArgsStrict(path: Path): Option[Args] = processStrict(parse(path))
+
+ /**
+ * Parse arguments of given URI
+ *
+ * @see parseArgsStrict(path: Path)
+ */
+ def parseArgsStrict(uri: String): Option[Args] = processStrict(parse(uri))
+
+ /**
+ * Generate URL for given arguments
+ */
+ def apply(args: Args): String = url(args)
+
+ /**
+ * Parse arguments for given [[Path]]
+ *
+ * @see [[parseArgs]]
+ */
+ def unapply(path: Path): Option[Args] = parseArgs(path)
+
+ /**
+ * Parse arguments for given URI
+ *
+ * @see [[parseArgs]]
+ */
+ def unapply(uri: String): Option[Args] = parseArgs(uri)
+
+ def &(params: Params.type): Route.ParamsRoute[Args] = Route.ParamsRoute(this)
}
case object Root extends Route[Unit] {
override def url(value: Unit): String = "/"
- override def parseInternal(path: Path): Option[(Unit, Path)] =
+ override def parse(path: Path): Option[(Unit, Path)] =
if (!path.path.startsWith("/")) None
else Some((), path.copy(path = path.path.tail))
}
+case object Elems extends Route[List[String]] {
+ override def url(value: List[String]): String = value.mkString("/")
+ override def parse(path: Path): Option[(List[String], Path)] =
+ Some((PathParser.parseParts(path.path), path.copy(path = "")))
+}
+
+case object Params
+
object Route {
implicit class Route0Extensions(route: Route[Unit]) {
def /[T](arg: Arg[T]): Route.ConcatRight[T] =
Route.ConcatRight(route, Route.Dynamic(arg))
def /[T](value: T)(implicit staticElement: StaticElement[T]): Route.ConcatRight[Unit] =
Route.ConcatRight(route, Route.Static(staticElement.f(value)))
+ def /[T](rest: Elems.type): Route.ConcatRight[List[String]] =
+ Route.ConcatRight(route, Elems)
def &[T](param: Param[T]): ParamRoute0[T] =
ParamRoute0(route, param)
+ def &(params: Params.type): ParamsRoute[Unit] = ParamsRoute(route)
def $[T](fragment: Fragment[T]): FragmentRoute0[T] =
FragmentRoute0(route, fragment)
}
@@ -46,7 +128,7 @@ object Route {
require(!element.contains("/"), "Element must not contain a slash")
override def url(value: Unit): String = element
- override def parseInternal(path: Path): Option[(Unit, Path)] = {
+ override def parse(path: Path): Option[(Unit, Path)] = {
val untilSlash = path.path.takeWhile(_ != '/')
if (untilSlash != element) None
else Some(((), path.copy(path = path.path.drop(untilSlash.length + 1))))
@@ -55,7 +137,7 @@ object Route {
case class Dynamic[T](arg: Arg[T]) extends Route[T] {
override def url(value: T): String = arg.codec.encode(value).getOrElse("")
- override def parseInternal(path: Path): Option[(T, Path)] = {
+ override def parse(path: Path): Option[(T, Path)] = {
val untilSlash = path.path.takeWhile(_ != '/')
arg.codec.decode(if (untilSlash.isEmpty) None else Some(untilSlash))
.map(value => (value, path.copy(path =
@@ -65,29 +147,29 @@ object Route {
case class ConcatLeft[L](left: Route[L], right: Route[Unit]) extends Route[L] {
override def url(value: L): String = left.url(value) + "/" + right.url(())
- override def parseInternal(path: Path): Option[(L, Path)] =
+ override def parse(path: Path): Option[(L, Path)] =
for {
- (lv, lp) <- left.parseInternal(path)
- (_ , rp) <- right.parseInternal(lp)
+ (lv, lp) <- left.parse(path)
+ (_ , rp) <- right.parse(lp)
} yield (lv, rp)
}
case class ConcatRight[R](left: Route[Unit], right: Route[R]) extends Route[R] {
override def url(value: R): String =
(if (left == Root) "" else left.url(())) + "/" + right.url(value)
- override def parseInternal(path: Path): Option[(R, Path)] =
+ override def parse(path: Path): Option[(R, Path)] =
for {
- (_ , lp) <- left.parseInternal(path)
- (rv, rp) <- right.parseInternal(lp)
+ (_ , lp) <- left.parse(path)
+ (rv, rp) <- right.parse(lp)
} yield (rv, rp)
}
case class ConcatBoth[L, R](left: Route[L], right: Route[R]) extends Route[(L, R)] {
override def url(value: (L, R)): String = left.url(value._1) + "/" + right.url(value._2)
- override def parseInternal(path: Path): Option[((L, R), Path)] =
+ override def parse(path: Path): Option[((L, R), Path)] =
for {
- (lv, lp) <- left.parseInternal(path)
- (rv, rp) <- right.parseInternal(lp)
+ (lv, lp) <- left.parse(path)
+ (rv, rp) <- right.parse(lp)
} yield ((lv, rv), rp)
}
@@ -97,9 +179,9 @@ object Route {
case None => route.url(())
case Some(frag) => route.url(()) + "#" + frag
}
- override def parseInternal(path: Path): Option[(P, Path)] =
+ override def parse(path: Path): Option[(P, Path)] =
for {
- (_, lp) <- route.parseInternal(path)
+ (_, lp) <- route.parse(path)
v <- fragment.codec.decode(lp.fragment)
p = lp.copy(fragment = None)
} yield (v, p)
@@ -111,9 +193,9 @@ object Route {
case None => route.url(value._1)
case Some(frag) => route.url(value._1) + "#" + frag
}
- override def parseInternal(path: Path): Option[((A, P), Path)] =
+ override def parse(path: Path): Option[((A, P), Path)] =
for {
- (lv, lp) <- route.parseInternal(path)
+ (lv, lp) <- route.parse(path)
rv <- fragment.codec.decode(lp.fragment)
p = lp.copy(fragment = None)
} yield ((lv, rv), p)
@@ -125,9 +207,9 @@ object Route {
.fold("")(v => param.name + "=" + URI.encode(v))
route.url(()) + (if (arg.isEmpty) "" else "?" + arg)
}
- override def parseInternal(path: Path): Option[(P, Path)] =
+ override def parse(path: Path): Option[(P, Path)] =
for {
- (_, lp) <- route.parseInternal(path)
+ (_, lp) <- route.parse(path)
arg = lp.args.find(_._1 == param.name)
v <- param.codec.decode(arg.map(_._2))
p = lp.copy(args = lp.args.diff(arg.toList))
@@ -145,12 +227,30 @@ object Route {
base + delimiter + encodedParam
}
}
- override def parseInternal(path: Path): Option[((A, P), Path)] =
+ override def parse(path: Path): Option[((A, P), Path)] =
for {
- (lv, lp) <- route.parseInternal(path)
+ (lv, lp) <- route.parse(path)
arg = lp.args.find(_._1 == param.name)
rv <- param.codec.decode(arg.map(_._2))
p = lp.copy(args = lp.args.diff(arg.toList))
} yield ((lv, rv), p)
}
+
+ case class ParamsRoute[A](route: Route[A]) extends Route[(A, List[(String, String)])] {
+ override def url(value: (A, List[(String, String)])): String = {
+ val (valueRoute, args) = value
+ val base = route.url(valueRoute)
+ if (args.isEmpty) base
+ else {
+ val argsEncoded = args.map { case (n, v) => n + "=" + URI.encode(v) }
+ base + (if (base.contains("?")) "&" else "?") + argsEncoded.mkString("&")
+ }
+ }
+
+ override def parse(path: Path): Option[((A, List[(String, String)]), Path)] =
+ for {
+ (lv, lp) <- route.parse(path)
+ p = lp.copy(args = List())
+ } yield ((lv, lp.args), p)
+ }
}
diff --git a/shared/src/test/scala/trail/RouteFragmentTests.scala b/shared/src/test/scala/trail/RouteFragmentTests.scala
index a45072f..b2ddc0d 100644
--- a/shared/src/test/scala/trail/RouteFragmentTests.scala
+++ b/shared/src/test/scala/trail/RouteFragmentTests.scala
@@ -24,8 +24,8 @@ class RouteFragmentTests extends FunSpec with Matchers {
it("Parse route with empty fragment") {
val route = Root $ Fragment[String]
- assert(route.parse("/").isEmpty)
- assert(route.parse("/#").contains(""))
+ assert(route.parseArgs("/").isEmpty)
+ assert(route.parseArgs("/#").contains(""))
}
it("Generate URL for route with empty fragment") {
@@ -36,8 +36,8 @@ class RouteFragmentTests extends FunSpec with Matchers {
it("Parse route with optional fragment") {
val route = Root $ Fragment[Option[String]]
- assert(route.parse("/").contains(None))
- assert(route.parse("/#").contains(Some("")))
+ assert(route.parseArgs("/").contains(None))
+ assert(route.parseArgs("/#").contains(Some("")))
}
it("Generate URL for route with optional fragment") {
@@ -60,12 +60,17 @@ class RouteFragmentTests extends FunSpec with Matchers {
it("Parse route with fragment") {
val route = Root $ Fragment[String]
- assert(route.parse("/").isEmpty)
- assert(route.parse("/#value").contains("value"))
+ assert(route.parseArgs("/").isEmpty)
+ assert(route.parseArgs("/#value").contains("value"))
}
it("Parse route with parameter and fragment") {
val route = Root & Param[String]("test") $ Fragment[Int]
- assert(route.parse("/?test=value#42").contains(("value", 42)))
+ assert(route.parseArgs("/?test=value#42").contains(("value", 42)))
+ }
+
+ it("Parse route with additional parameter and fragment") {
+ val route = Root & Params $ Fragment[Int]
+ assert(route.parseArgs("/?test=value#42").contains(() -> List("test" -> "value") -> 42))
}
}
diff --git a/shared/src/test/scala/trail/RouteParamTests.scala b/shared/src/test/scala/trail/RouteParamTests.scala
index e214b11..183edbc 100644
--- a/shared/src/test/scala/trail/RouteParamTests.scala
+++ b/shared/src/test/scala/trail/RouteParamTests.scala
@@ -87,73 +87,91 @@ class RouteParamTests extends FunSpec with Matchers {
it("Parsing route with one parameter") {
val route = Root & Param[String]("test")
- assert(route.parse("/?test=value")
+ assert(route.parseArgs("/?test=value")
.contains("value"))
- assert(route.parse("/?test=äöü").contains("äöü"))
- assert(route.parse("/?test=%C3%A4%C3%B6%C3%BC")
+ assert(route.parseArgs("/?test=äöü").contains("äöü"))
+ assert(route.parseArgs("/?test=%C3%A4%C3%B6%C3%BC")
.contains("äöü"))
}
it("Parsing route with unspecified parameter") {
val route = Root & Param[String]("test")
- assert(route.parse("/").isEmpty)
- assert(route.parse("/?test2=value").isEmpty)
+ assert(route.parseArgs("/").isEmpty)
+ assert(route.parseArgs("/?test2=value").isEmpty)
}
it("Parsing route with two parameters") {
val route = Root & Param[String]("test") & Param[Int]("test2")
- val parsed = route.parse("/?test=value&test2=42")
+ val parsed = route.parseArgs("/?test=value&test2=42")
assert(parsed.contains(("value", 42)))
}
it("Parsing route with additional parameters") {
- // Ignore parameters that are not specified in the route
+ // By default, allow parameters that are not specified in the route
val route = Root & Param[String]("test")
- val parsed = route.parse("/?test=value&test2=value2")
+ val parsed = route.parseArgs("/?test=value&test2=value2")
assert(parsed.contains("value"))
+
+ val route2 = Root & Params
+ assert(route2.parseArgs("/?test=value").contains(() -> List("test" -> "value")))
+
+ val route3 = Root & Param[String]("test") & Params
+ assert(route3.parseArgs("/?test=value&test2=value2").contains("value" -> List("test2" -> "value2")))
}
it("Parsing non-root route with two optional parameters") {
val route = Root / "register" & Param[Option[String]]("plan") & Param[Option[String]]("redirect")
- assert(route.parse("/register?plan=test")
+ assert(route.parseArgs("/register?plan=test")
.contains((Option("test"), Option.empty[String])))
- assert(route.parse("/register?plan=test&redirect=test2")
+ assert(route.parseArgs("/register?plan=test&redirect=test2")
.contains((Option("test"), Option("test2"))))
- assert(route.parse("/register?redirect=test2")
+ assert(route.parseArgs("/register?redirect=test2")
.contains((Option.empty[String], Option("test2"))))
}
it("Parsing route with optional parameter") {
val route = Root & Param[Option[String]]("test")
- assert(route.parse("/")
- .contains(Option.empty[String]))
- assert(route.parse("/?test2=value")
- .contains(Option.empty[String]))
- assert(route.parse("/?test=value")
- .contains(Option("value")))
- assert(route.parse("/?test=value&test2=value")
- .contains(Option("value")))
+
+ assert(route.parseArgs("/").contains(Option.empty[String]))
+ assert(route.parseArgs("/?test=value").contains(Option("value")))
+
+ assert(route.parseArgs("/?test2=value").contains(None))
+ assert(route.parseArgs("/?test=value&test2=value").contains(Some("value")))
}
it("Parsing route with duplicated name") {
val route = Root & Param[String]("test") & Param[String]("test")
- assert(route.parse("/?test=v1&test=v2")
+ assert(route.parseArgs("/?test=v1&test=v2")
.contains(("v1", "v2")))
}
it("Parsing route with duplicated name and different types") {
val route = Root & Param[Int]("test") & Param[String]("test")
// When the two parameters have different types, the order matters
- assert(route.parse("/?test=value&test=42").isEmpty)
- assert(route.parse("/?test=42&test=value")
+ assert(route.parseArgs("/?test=value&test=42").isEmpty)
+ assert(route.parseArgs("/?test=42&test=value")
.contains((42, "value")))
}
it("Only match parameter routes with same path") {
val route = Root / "api" / "catalogue" / "content" & Param[String]("category")
- assert(route.parse("/catalogue/content?category=Audio").isEmpty)
- assert(route.parse("/api/catalogue/content?category=Audio")
+ assert(route.parseArgs("/catalogue/content?category=Audio").isEmpty)
+ assert(route.parseArgs("/api/catalogue/content?category=Audio")
.contains("Audio"))
}
+
+ it("Parsing routes with additional parameters in strict mode") {
+ // In strict mode, forbid parameters that are not specified in the route
+ val route = Root & Param[String]("test")
+ val parsed = route.parseArgsStrict("/?test=value&test2=value2")
+ assert(parsed.isEmpty)
+
+ val route2 = Root & Param[Option[String]]("test")
+ assert(route2.parseArgsStrict("/").contains(Option.empty[String]))
+ assert(route2.parseArgsStrict("/?test=value").contains(Option("value")))
+
+ assert(route2.parseArgsStrict("/?test2=value").isEmpty)
+ assert(route2.parseArgsStrict("/?test=value&test2=value").isEmpty)
+ }
}
diff --git a/shared/src/test/scala/trail/RouteParseTests.scala b/shared/src/test/scala/trail/RouteParseTests.scala
index c306565..02a9097 100644
--- a/shared/src/test/scala/trail/RouteParseTests.scala
+++ b/shared/src/test/scala/trail/RouteParseTests.scala
@@ -7,70 +7,81 @@ import scala.util.Try
class RouteParseTests extends FunSpec with Matchers {
it("Parse root") {
val root = Root
- assert(root.parse("/").contains(()))
+ assert(root.parseArgs("/").contains(()))
+
+ // Additional path elements cannot be specified
+ assert(root.parseArgs("/test").isEmpty)
+ }
+
+ it("Parse root in strict mode") {
+ val root = Root
+ assert(root.parseArgsStrict("/").contains(()))
+ assert(root.parseArgsStrict("/test").isEmpty)
+ assert(root.parseArgsStrict("/?user=bob").isEmpty)
+ assert(root.parseArgsStrict("/#test").isEmpty)
}
it("Parse one path element") {
val root = Root / "test"
- assert(root.parse("/").isEmpty)
- assert(root.parse("/test").contains(()))
- assert(root.parse("/test2").isEmpty)
+ assert(root.parseArgs("/").isEmpty)
+ assert(root.parseArgs("/test").contains(()))
+ assert(root.parseArgs("/test2").isEmpty)
}
it("Route with one argument can be parsed") {
val route = Root / "user" / Arg[String]
- assert(route.parse(trail.Path("/user/bob")) === Some("bob"))
- assert(route.parse(trail.Path("/user/bob/test")) === Some("bob"))
+ assert(route.parseArgs(trail.Path("/user/bob")) === Some("bob"))
+ assert(route.parseArgs(trail.Path("/user/bob/test")).isEmpty)
}
it("Route can be parsed from Path") {
val userInfo = Root / "user" / Arg[String] / Arg[Boolean]
- val parsed = userInfo.parse(trail.Path("/user/bob/true"))
+ val parsed = userInfo.parseArgs(trail.Path("/user/bob/true"))
assert(parsed === Some(("bob", true)))
- userInfo.parse(trail.Path("/user/bob")) shouldBe empty
+ userInfo.parseArgs(trail.Path("/user/bob")) shouldBe empty
}
it("Route with query parameters can be parsed from Path") {
val userInfo = Root / "user" / Arg[String] / Arg[Boolean] & Param[Int]("n")
- val parsed = userInfo.parse(trail.Path("/user/bob/true", List("n" -> "42")))
+ val parsed = userInfo.parseArgs(trail.Path("/user/bob/true", List("n" -> "42")))
assert(parsed === Some((("bob", true), 42)))
}
it("Route with optional path element can be parsed") {
val disk = Root / Arg[Option[Long]]
- assert(disk.parse("/").contains(None))
- assert(disk.parse("/42").contains(Some(42L)))
+ assert(disk.parseArgs("/").contains(None))
+ assert(disk.parseArgs("/42").contains(Some(42L)))
}
it("Route with optional path element can be parsed (2)") {
val disk = Root / "disk" / Arg[Option[Long]]
- assert(disk.parse("/disk").contains(None))
- assert(disk.parse("/disk/").contains(None))
- assert(disk.parse("/disk/42").contains(Some(42L)))
+ assert(disk.parseArgs("/disk").contains(None))
+ assert(disk.parseArgs("/disk/").contains(None))
+ assert(disk.parseArgs("/disk/42").contains(Some(42L)))
}
it("Route can be parsed from string") {
val userInfo = Root / "user" / Arg[String] / Arg[Boolean]
- val parsed = userInfo.parse("/user/bob/true")
+ val parsed = userInfo.parseArgs("/user/bob/true")
parsed shouldBe defined
assert(parsed === Some(("bob", true)))
- // It is possible to provide more path elements than specified in the route
- userInfo.parse("/user/bob/true/true") shouldBe defined
+ // It is forbidden to provide more path elements than specified in the route
+ userInfo.parseArgs("/user/bob/true/true") shouldBe empty
- userInfo.parse("/user/bob") shouldBe empty
- userInfo.parse("/user/bob/1") shouldBe empty
- userInfo.parse("/usr/bob/1") shouldBe empty
+ userInfo.parseArgs("/user/bob") shouldBe empty
+ userInfo.parseArgs("/user/bob/1") shouldBe empty
+ userInfo.parseArgs("/usr/bob/1") shouldBe empty
}
it("Boolean argument can be parsed") {
val userInfo = Root / "user" / Arg[String] / Arg[Boolean]
- assert(userInfo.parse("/user/hello/false")
+ assert(userInfo.parseArgs("/user/hello/false")
.contains(("hello", false)))
}
@@ -84,8 +95,25 @@ class RouteParseTests extends FunSpec with Matchers {
}
val route = Root / "size" & Param[Set[Long]]("folders")
- assert(route.parse(route(Set[Long]())).contains(Set[Long]()))
- assert(route.parse(route(Set[Long](1))).contains(Set[Long](1)))
- assert(route.parse(route(Set[Long](1, 2, 3))).contains(Set[Long](1, 2, 3)))
+ assert(route.parseArgs(route(Set[Long]())).contains(Set[Long]()))
+ assert(route.parseArgs(route(Set[Long](1))).contains(Set[Long](1)))
+ assert(route.parseArgs(route(Set[Long](1, 2, 3))).contains(Set[Long](1, 2, 3)))
+ }
+
+ it("Match any path elements") {
+ val route = Elems
+ assert(route.parseArgs("/a/b/c").contains(List("a", "b", "c")))
+ }
+
+ it("Route with additional path elements") {
+ val route = Root / Elems
+ assert(route.parseArgs("/a/b/c").contains(List("a", "b", "c")))
+
+ val route2 = Root / "a" / Elems
+ assert(route2.parseArgs("/a/b/c").contains(List("b", "c")))
+
+ val route3 = Root / "a" / Elems & Param[String]("test")
+ assert(route3.parseArgs("/a/b/c?test=value").contains(
+ List("b", "c") -> "value"))
}
}
diff --git a/shared/src/test/scala/trail/RoutingTableTests.scala b/shared/src/test/scala/trail/RoutingTableTests.scala
index e41cb7d..fc31858 100644
--- a/shared/src/test/scala/trail/RoutingTableTests.scala
+++ b/shared/src/test/scala/trail/RoutingTableTests.scala
@@ -40,8 +40,8 @@ class RoutingTableTests extends FunSpec with Matchers {
val devRoutes = new Routes(isProduction = false)
val prodRoutes = new Routes(isProduction = true)
- assert(devRoutes.users.parse("/users/test").contains("test"))
- assert(prodRoutes.users.parse("/api/v2.0/users/test").contains("test"))
+ assert(devRoutes.users.parseArgs("/users/test").contains("test"))
+ assert(prodRoutes.users.parseArgs("/api/v2.0/users/test").contains("test"))
}
it("Parsing multiple route with pattern matching") {
diff --git a/shared/src/test/scala/trail/UrlTests.scala b/shared/src/test/scala/trail/UrlTests.scala
index bd50856..fbf844b 100644
--- a/shared/src/test/scala/trail/UrlTests.scala
+++ b/shared/src/test/scala/trail/UrlTests.scala
@@ -139,4 +139,15 @@ class UrlTests2 extends FunSpec with Matchers {
assert(url == "/disk/view?disk=48")
}
+
+ it("url() should work with additional parameters") {
+ val route = Root & Params
+ assert(route.url(() -> List("test" -> "value")) == "/?test=value")
+
+ val route2 = Root & Param[String]("test") & Params
+ assert(route2.url("value" -> List("test2" -> "value2")) == "/?test=value&test2=value2")
+
+ val route3 = Root & Params $ Fragment[Int]
+ assert(route3.url(() -> List("test" -> "value") -> 42) == "/?test=value#42")
+ }
}
diff --git a/version.sbt b/version.sbt
index 9904311..87c01ed 100644
--- a/version.sbt
+++ b/version.sbt
@@ -1 +1 @@
-version in ThisBuild := "0.2.2-SNAPSHOT"
+version in ThisBuild := "0.3.0-SNAPSHOT"