Skip to content

Commit

Permalink
Merge pull request #27 from sparsetech/issue/25
Browse files Browse the repository at this point in the history
Route: Overload functions to take Path instead of String
  • Loading branch information
tindzk authored Feb 16, 2019
2 parents a96fcf7 + 08c5230 commit 1d51944
Show file tree
Hide file tree
Showing 6 changed files with 100 additions and 25 deletions.
3 changes: 3 additions & 0 deletions manual/introduction.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ A fragment can be specified:
Create a routing table:
<listing id="parse">

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:
<listing id="parse-path">

Define a custom argument type:
<listing id="custom-arg">

Expand Down
5 changes: 5 additions & 0 deletions manual/listings.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@
"language" : "scala",
"result" : "/details/1"
},
"parse-path" : {
"code" : "val result2 = trail.Path(\"/user/hello\", List(\"show\" -> \"false\")) match {\n case details (a :: HNil) => s\"details: $a\"\n case userInfo(u :: HNil, s :: HNil) => s\"user: $u, show: $s\"\n}\nprintln(result2)",
"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]): String = s.mkString(\",\")\n override def decode(s: String): Option[Set[Int]] =\n Try(s.split(',').map(_.toInt).toSet).toOption\n}\n\nval export = Root / \"export\" / Arg[Set[Int]]\nprintln(export.url(Set(1, 2, 3) :: HNil))",
"language" : "scala",
Expand Down
7 changes: 7 additions & 0 deletions manual/src/main/scala/trail/manual/Listings.scala
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,13 @@ object Listings extends App {
}
println(result)

listing("parse-path")
val result2 = trail.Path("/user/hello", List("show" -> "false")) match {
case details (a :: HNil) => s"details: $a"
case userInfo(u :: HNil, s :: HNil) => s"user: $u, show: $s"
}
println(result2)

listing("custom-arg")
import scala.util.Try
implicit case object IntSetArg extends Codec[Set[Int]] {
Expand Down
28 changes: 23 additions & 5 deletions shared/src/main/scala/trail/Route.scala
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ case class Route[ROUTE <: HList](pathElements: ROUTE) {
implicit ev: FlatMapper.Aux[Args.Convert.type, ROUTE, Args]
): Option[Args] = parse(uri)

def unapply[Args <: HList](path: Path)(
implicit ev: FlatMapper.Aux[Args.Convert.type, ROUTE, Args]
): Option[Args] = parse(path.path)

def url()(
implicit ev: FlatMapper.Aux[Args.Convert.type, ROUTE, HNil]
): String = url(HNil: HNil)
Expand Down Expand Up @@ -76,6 +80,10 @@ case class Route[ROUTE <: HList](pathElements: ROUTE) {
m(pathElements, PathParser.parseParts(uri)).map(_.asInstanceOf[Args])
}

def parse[Args <: HList](path: Path)(
implicit ev: FlatMapper.Aux[Args.Convert.type, ROUTE, Args]
): Option[Args] = parse(path.path)

/**
* Converts all the chunks of the path to `HR` using the passed `~>>` function.
* Then combines all the `HR` elements together.
Expand Down Expand Up @@ -124,7 +132,12 @@ case class ParamRoute[ROUTE <: HList, Params <: HList](route: Route[ROUTE], para
def unapply[Args <: HList, ArgParams <: HList](uri: String)(
implicit ev: FlatMapper.Aux[Args.Convert.type, ROUTE, Args],
ev2: FlatMapper.Aux[Params.Convert.type, Params, ArgParams]
): Option[(Args, ArgParams)] = parse(uri)
): Option[(Args, ArgParams)] = parse(PathParser.parse(uri))

def unapply[Args <: HList, ArgParams <: HList](path: Path)(
implicit ev: FlatMapper.Aux[Args.Convert.type, ROUTE, Args],
ev2: FlatMapper.Aux[Params.Convert.type, Params, ArgParams]
): Option[(Args, ArgParams)] = parse(path)

def url[Args <: HList, ArgParams <: HList](args: Args, argParams: ArgParams)(
implicit ev: FlatMapper.Aux[Args.Convert.type, ROUTE, Args],
Expand Down Expand Up @@ -162,18 +175,23 @@ case class ParamRoute[ROUTE <: HList, Params <: HList](route: Route[ROUTE], para
): String = url(args: HNil, params)

def parse[Args <: HList, ArgParams <: HList]
(uri: String)
(path: Path)
(implicit ev: FlatMapper.Aux[Args.Convert.type, ROUTE, Args],
ev2: FlatMapper.Aux[Params.Convert.type, Params, ArgParams]
): Option[(Args, ArgParams)] =
{
val p = PathParser.parse(uri)
for {
parts <- route.parse(p.path)
args <- parseQuery(p.args, p.fragment)
parts <- route.parse(path.path)
args <- parseQuery(path.args, path.fragment)
} yield (parts, args)
}

def parse[Args <: HList, ArgParams <: HList]
(uri: String)
(implicit ev: FlatMapper.Aux[Args.Convert.type, ROUTE, Args],
ev2: FlatMapper.Aux[Params.Convert.type, Params, ArgParams]
): Option[(Args, ArgParams)] = parse(PathParser.parse(uri))

def &[T](param: Param[T])(
implicit prepend: Prepend[Params, Param[T] :: HNil]
) = copy(params = params :+ param)
Expand Down
40 changes: 20 additions & 20 deletions shared/src/test/scala/trail/RouteExampleTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,14 @@ import org.scalatest._
import shapeless.HNil

class RouteExampleTests extends FlatSpec with Matchers {
"A simple example" should "just work" in {
val userInfo = Root / "user" / Arg[String] / Arg[Boolean]
val userInfoUrl = userInfo("bob" :: false :: HNil)
val userInfo2 = (Root / "user" / "bob" / false).url(HNil)
"url()" should "work with mandatory arguments" in {
val userInfo = Root / "user" / Arg[String] / Arg[Boolean]

assert(userInfoUrl === "/user/bob/false")
assert(userInfoUrl === userInfo2)
val url1 = userInfo("bob" :: false :: HNil)
val url2 = (Root / "user" / "bob" / false).url(HNil)

val parsed = userInfo.parse("/user/bob/true")
parsed shouldBe defined

assert(parsed === Some("bob" :: true :: HNil))

userInfo.parse("/user/bob") shouldBe empty
userInfo.parse("/user/bob/true/true") shouldBe empty
userInfo.parse("/user/bob/1") shouldBe empty
userInfo.parse("/usr/bob/1") shouldBe empty
assert(url1 === "/user/bob/false")
assert(url2 === "/user/bob/false")
}

"url()" should "work" in {
Expand All @@ -38,20 +29,29 @@ class RouteExampleTests extends FlatSpec with Matchers {
assert(url == "/list?upload=true")
}

"parse()" should "work" in {
"Expressing a routing table" should "be possible with pattern matching" in {
import shapeless._

val details = Root / "details" / Arg[Int]
val userInfo = Root / "user" / Arg[String] / Arg[Boolean]

assert(userInfo.parse("/user/hello/false")
.contains("hello" :: false :: HNil))
val result = "/user/hello/false" match {
case details (a :: HNil) => a.toString
case userInfo(u :: d :: HNil) =>
val user: String = u // Verify that type inference works
u + d
}

assert(result == "hellofalse")
}

"Expressing a routing table" should "be possible with pattern matching" in {
"Expressing a routing table" should "be possible with pattern matching (2)" in {
import shapeless._

val details = Root / "details" / Arg[Int]
val userInfo = Root / "user" / Arg[String] / Arg[Boolean]

val result = "/user/hello/false" match {
val result = PathParser.parse("/user/hello/false") match {
case details (a :: HNil) => a.toString
case userInfo(u :: d :: HNil) =>
val user: String = u // Verify that type inference works
Expand Down
42 changes: 42 additions & 0 deletions shared/src/test/scala/trail/RouteParseTests.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package trail

import org.scalatest._
import shapeless._

class RouteParseTests extends FunSpec with Matchers {
it("Route can be parsed from Path") {
val userInfo = Root / "user" / Arg[String] / Arg[Boolean]

val parsed = userInfo.parse(trail.Path("/user/bob/true"))
assert(parsed === Some("bob" :: true :: HNil))
userInfo.parse(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")))
assert(parsed === Some("bob" :: true :: HNil, 42 :: HNil))
}

it("Route can be parsed from string") {
val userInfo = Root / "user" / Arg[String] / Arg[Boolean]

val parsed = userInfo.parse("/user/bob/true")
parsed shouldBe defined

assert(parsed === Some("bob" :: true :: HNil))

userInfo.parse("/user/bob") shouldBe empty
userInfo.parse("/user/bob/true/true") shouldBe empty
userInfo.parse("/user/bob/1") shouldBe empty
userInfo.parse("/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")
.contains("hello" :: false :: HNil))
}
}

0 comments on commit 1d51944

Please sign in to comment.