From 8b08a9ecf668d3a255ebfd16d5dca820bba86eea Mon Sep 17 00:00:00 2001 From: Tim Nieradzik Date: Fri, 24 May 2019 16:24:34 +0200 Subject: [PATCH] Path: Fix parsing of empty query arguments Also fix custom codec in manual which did not handle empty sets. --- manual/introduction.md | 4 ++-- manual/listings.json | 10 +++++----- .../src/main/scala/trail/manual/Listings.scala | 8 +++++--- shared/src/main/scala/trail/Path.scala | 6 +++--- shared/src/test/scala/trail/PathTests.scala | 6 ++++++ .../src/test/scala/trail/RouteParseTests.scala | 17 +++++++++++++++++ 6 files changed, 38 insertions(+), 13 deletions(-) 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))) + } }