diff --git a/manual/introduction.md b/manual/introduction.md
index d470f08..166b4de 100644
--- a/manual/introduction.md
+++ b/manual/introduction.md
@@ -34,8 +34,8 @@ Create a routing table:
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:
-Define a custom argument type:
-
+Define a custom codec that can be used in arguments, parameters and fragments:
+
Define a custom path element type:
diff --git a/manual/listings.json b/manual/listings.json
index bc75079..018d1be 100644
--- a/manual/listings.json
+++ b/manual/listings.json
@@ -14,11 +14,6 @@
"language" : "scala",
"result" : "user: hello, show: false"
},
- "custom-arg" : {
- "code" : "import scala.util.Try\nimplicit case object IntSetArg extends Codec[Set[Int]] {\n override def encode(s: Set[Int]): Option[String] = Some(s.mkString(\",\"))\n override def decode(s: Option[String]): Option[Set[Int]] =\n s.flatMap(value => Try(value.split(',').map(_.toInt).toSet).toOption)\n}\n\nval export = Root / \"export\" / Arg[Set[Int]]\nprintln(export.url(Set(1, 2, 3)))",
- "language" : "scala",
- "result" : "/export/1,2,3"
- },
"query-params-opt" : {
"code" : "val routeParamsOpt = Root / \"details\" & Param[Int](\"id\") & Param[Option[Boolean]](\"show\")\nprintln(routeParamsOpt.parse(\"/details?id=42\"))",
"language" : "scala",
@@ -44,6 +39,11 @@
"language" : "scala",
"result" : "/asdf"
},
+ "custom-codec" : {
+ "code" : "import scala.util.Try\nimplicit case object IntSetCodec extends Codec[Set[Int]] {\n override def encode(s: Set[Int]): Option[String] = Some(s.mkString(\",\"))\n override def decode(s: Option[String]): Option[Set[Int]] =\n s.flatMap(value =>\n if (value.isEmpty) Some(Set())\n else Try(value.split(',').map(_.toInt).toSet).toOption)\n}\n\nval export = Root / \"export\" / Arg[Set[Int]]\nprintln(export.url(Set(1, 2, 3)))",
+ "language" : "scala",
+ "result" : "/export/1,2,3"
+ },
"query-fragment" : {
"code" : "val routeFragment = Root $ Fragment[Int]\nprintln(routeFragment.parse(\"/#42\"))",
"language" : "scala",
diff --git a/manual/src/main/scala/trail/manual/Listings.scala b/manual/src/main/scala/trail/manual/Listings.scala
index beb4c56..ac6d7c1 100644
--- a/manual/src/main/scala/trail/manual/Listings.scala
+++ b/manual/src/main/scala/trail/manual/Listings.scala
@@ -46,12 +46,14 @@ object Listings extends App {
}
println(result2)
- listing("custom-arg")
+ listing("custom-codec")
import scala.util.Try
- implicit case object IntSetArg extends Codec[Set[Int]] {
+ implicit case object IntSetCodec extends Codec[Set[Int]] {
override def encode(s: Set[Int]): Option[String] = Some(s.mkString(","))
override def decode(s: Option[String]): Option[Set[Int]] =
- s.flatMap(value => Try(value.split(',').map(_.toInt).toSet).toOption)
+ s.flatMap(value =>
+ if (value.isEmpty) Some(Set())
+ else Try(value.split(',').map(_.toInt).toSet).toOption)
}
val export = Root / "export" / Arg[Set[Int]]
diff --git a/shared/src/main/scala/trail/Path.scala b/shared/src/main/scala/trail/Path.scala
index 99fad25..5ffb41f 100644
--- a/shared/src/main/scala/trail/Path.scala
+++ b/shared/src/main/scala/trail/Path.scala
@@ -20,9 +20,9 @@ object PathParser {
def parseArgs(query: String): List[(String, String)] =
query.split('&').flatMap { x =>
- val pair = x.split('=')
- if (pair.length != 2) List.empty
- else List((pair(0), URI.decode(pair(1))))
+ val equalSign = x.indexOf('=')
+ if (equalSign == -1) List()
+ else List((x.take(equalSign), URI.decode(x.drop(equalSign + 1))))
}.toList
/** Return URL without scheme and authority */
diff --git a/shared/src/test/scala/trail/PathTests.scala b/shared/src/test/scala/trail/PathTests.scala
index 5bdb10a..763a928 100644
--- a/shared/src/test/scala/trail/PathTests.scala
+++ b/shared/src/test/scala/trail/PathTests.scala
@@ -60,4 +60,10 @@ class PathTests extends FunSuite {
assert(parsed.url == url)
}
}
+
+ test("Parse argument with empty value") {
+ val url = "/test?a="
+ assert(PathParser.parse(url).path == "/test")
+ assert(PathParser.parse(url).args == List("a" -> ""))
+ }
}
\ No newline at end of file
diff --git a/shared/src/test/scala/trail/RouteParseTests.scala b/shared/src/test/scala/trail/RouteParseTests.scala
index e1cb3ce..ea17d32 100644
--- a/shared/src/test/scala/trail/RouteParseTests.scala
+++ b/shared/src/test/scala/trail/RouteParseTests.scala
@@ -2,6 +2,8 @@ package trail
import org.scalatest._
+import scala.util.Try
+
class RouteParseTests extends FunSpec with Matchers {
it("Parse root") {
val root = Root
@@ -58,4 +60,19 @@ class RouteParseTests extends FunSpec with Matchers {
assert(userInfo.parse("/user/hello/false")
.contains(("hello", false)))
}
+
+ it("Set codec") {
+ implicit case object LongSetCodec extends Codec[Set[Long]] {
+ override def encode(s: Set[Long]): Option[String] = Some(s.mkString(","))
+ override def decode(s: Option[String]): Option[Set[Long]] =
+ s.flatMap(value =>
+ if (value.isEmpty) Some(Set())
+ else Try(value.split(',').map(_.toLong).toSet).toOption)
+ }
+
+ 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)))
+ }
}