Skip to content

Commit

Permalink
Merge pull request #39 from sparsetech/issue/37
Browse files Browse the repository at this point in the history
Route: Require exact match when parsing
  • Loading branch information
tindzk authored Mar 26, 2020
2 parents 5c1681e + a45a54c commit 667c5f4
Show file tree
Hide file tree
Showing 11 changed files with 330 additions and 124 deletions.
39 changes: 25 additions & 14 deletions manual/introduction.md
Original file line number Diff line number Diff line change
@@ -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:
Expand All @@ -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:
<listing id="route">

Create a string URL by filling the placeholders of the route:
To fill the route's placeholders, call the `url()` or `apply()` functions:
<listing id="url">

Parse an URL:
<listing id="map">
When parsing an URL, Trail maps the values onto Scala types. The result will also contain any unmatched path elements, arguments or the fragment:
<listing id="parse">

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()`:
<listing id="query-params">

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:
<listing id="query-params-strict">

Routes may specify optional query parameters:
<listing id="query-params-opt">

A fragment can be specified:
You can match fragments, too:
<listing id="query-fragment">

Create a routing table:
<listing id="parse">
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:
<listing id="additional-elems">

Similarly, additional parameters can be matched with `Params`:
<listing id="additional-params">

Routing tables can be expressed with pattern matching:
<listing id="routing-table">

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

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:
<listing id="custom-codec">

Define a custom path element type:
It is possible to define a custom path element type, too:
<listing id="custom-path-elem">

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):
<listing id="uri-values">
43 changes: 29 additions & 14 deletions manual/listings.json
Original file line number Diff line number Diff line change
Expand Up @@ -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&param2=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(()))",
Expand All @@ -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)"
}
Expand Down
38 changes: 28 additions & 10 deletions manual/src/main/scala/trail/manual/Listings.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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&param2=value2"))

listing("routing-table")
val userInfo = Root / "user" / Arg[String] & Param[Boolean]("show")

val result = "/user/hello?show=false" match {
Expand All @@ -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"
}
Expand Down
2 changes: 1 addition & 1 deletion shared/src/main/scala/trail/Path.scala
Original file line number Diff line number Diff line change
Expand Up @@ -64,4 +64,4 @@ object PathParser {

Path(path, args, fragment)
}
}
}
Loading

0 comments on commit 667c5f4

Please sign in to comment.