Skip to content

Commit

Permalink
Merge pull request #29 from sparsetech/issue/28
Browse files Browse the repository at this point in the history
Route: Only encode placeholders in type parameter
  • Loading branch information
tindzk authored Mar 1, 2019
2 parents 5c3fdc6 + fc2eec2 commit e7be86b
Show file tree
Hide file tree
Showing 20 changed files with 478 additions and 538 deletions.
5 changes: 2 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,13 @@ Trail is a routing library for Scala and Scala.js.
## Example
```scala
import trail._
import shapeless._

val details = Root / "details" / Arg[Int]
val userInfo = Root / "user" / Arg[String] & Param[Boolean]("show")

val result = "/user/hello?show=false" match {
case details (a :: HNil) => s"details: $a"
case userInfo(u :: HNil, s :: HNil) => s"user: $u, show: $s"
case details (a) => s"details: $a"
case userInfo((u, s)) => s"user: $u, show: $s"
}
```

Expand Down
4 changes: 0 additions & 4 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,9 @@
import sbtcrossproject.CrossPlugin.autoImport.{crossProject, CrossType}

val Leaf = "0.1.0"
val Shapeless = "2.3.3"
val Scala2_11 = "2.11.12"
val Scala2_12 = "2.12.8"
val ScalaTest = "3.0.5"
val Cats = "1.6.0"

val SharedSettings = Seq(
name := "trail",
Expand Down Expand Up @@ -46,8 +44,6 @@ lazy val trail = crossProject(JSPlatform, JVMPlatform)
autoAPIMappings := true,
apiMappings += (scalaInstance.value.libraryJar -> url(s"http://www.scala-lang.org/api/${scalaVersion.value}/")),
libraryDependencies ++= Seq(
"com.chuusai" %%% "shapeless" % Shapeless,
"org.typelevel" %%% "cats-core" % Cats,
"org.scalatest" %%% "scalatest" % ScalaTest % "test"
)
)
Expand Down
24 changes: 12 additions & 12 deletions manual/listings.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,51 +2,51 @@
"map" : {
"code" : "println(details.parse(\"/details/42\"))",
"language" : "scala",
"result" : "Some(42 :: HNil)"
"result" : "Some(42)"
},
"url" : {
"code" : "println(details.url(1 :: HNil)) // Shorter: details(1 :: HNil)",
"code" : "println(details.url(1)) // Shorter: details(1)",
"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)",
"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)",
"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))",
"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\") & ParamOpt[Boolean](\"show\")\nprintln(routeParamsOpt.parse(\"/details?id=42\"))",
"code" : "val routeParamsOpt = Root / \"details\" & Param[Int](\"id\") & Param[Option[Boolean]](\"show\")\nprintln(routeParamsOpt.parse(\"/details?id=42\"))",
"language" : "scala",
"result" : "Some((HNil,42 :: None :: HNil))"
"result" : "Some((42,None))"
},
"parse" : {
"code" : "val userInfo = Root / \"user\" / Arg[String] & Param[Boolean](\"show\")\n\nval result = \"/user/hello?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(result)",
"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\"))",
"language" : "scala",
"result" : "Some((HNil,false :: HNil))"
"result" : "Some(false)"
},
"route" : {
"code" : "import trail._\nimport shapeless._\n\nval details = Root / \"details\" / Arg[Int]\nprintln(details)",
"language" : "scala",
"result" : "Route(details :: Arg_(IntArg) :: HNil)"
"result" : "ConcatRight(ConcatRight(Root,Static(details)),Dynamic(Arg()))"
},
"custom-path-elem" : {
"code" : "case class Foo(bar: String)\nimplicit object FooElement extends StaticElement[Foo](_.bar)\n\nprintln((Root / Foo(\"asdf\")).url())",
"code" : "case class Foo(bar: String)\nimplicit object FooElement extends StaticElement[Foo](_.bar)\n\nprintln((Root / Foo(\"asdf\")).url(()))",
"language" : "scala",
"result" : "/asdf"
},
"query-fragment" : {
"code" : "val routeFragment = Root & Fragment[Int]\nprintln(routeFragment.parse(\"/#42\"))",
"code" : "val routeFragment = Root $ Fragment[Int]\nprintln(routeFragment.parse(\"/#42\"))",
"language" : "scala",
"result" : "Some((HNil,42 :: HNil))"
"result" : "Some(42)"
}
}
24 changes: 12 additions & 12 deletions manual/src/main/scala/trail/manual/Listings.scala
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ object Listings extends App {
println(details)

listing("url")
println(details.url(1 :: HNil)) // Shorter: details(1 :: HNil)
println(details.url(1)) // Shorter: details(1)

listing("map")
println(details.parse("/details/42"))
Expand All @@ -23,45 +23,45 @@ object Listings extends App {
println(route.parse("/details?show=false"))

listing("query-params-opt")
val routeParamsOpt = Root / "details" & Param[Int]("id") & ParamOpt[Boolean]("show")
val routeParamsOpt = Root / "details" & Param[Int]("id") & Param[Option[Boolean]]("show")
println(routeParamsOpt.parse("/details?id=42"))

listing("query-fragment")
val routeFragment = Root & Fragment[Int]
val routeFragment = Root $ Fragment[Int]
println(routeFragment.parse("/#42"))

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

val result = "/user/hello?show=false" match {
case details (a :: HNil) => s"details: $a"
case userInfo(u :: HNil, s :: HNil) => s"user: $u, show: $s"
case details (a) => s"details: $a"
case userInfo((u, s)) => s"user: $u, show: $s"
}
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"
case details (a) => s"details: $a"
case userInfo((u, s)) => s"user: $u, show: $s"
}
println(result2)

listing("custom-arg")
import scala.util.Try
implicit case object IntSetArg extends Codec[Set[Int]] {
override def encode(s: Set[Int]): String = s.mkString(",")
override def decode(s: String): Option[Set[Int]] =
Try(s.split(',').map(_.toInt).toSet).toOption
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)
}

val export = Root / "export" / Arg[Set[Int]]
println(export.url(Set(1, 2, 3) :: HNil))
println(export.url(Set(1, 2, 3)))

listing("custom-path-elem")
case class Foo(bar: String)
implicit object FooElement extends StaticElement[Foo](_.bar)

println((Root / Foo("asdf")).url())
println((Root / Foo("asdf")).url(()))

end()
write("manual/listings.json")
Expand Down
16 changes: 0 additions & 16 deletions shared/src/main/scala/trail/Arg.scala

This file was deleted.

40 changes: 25 additions & 15 deletions shared/src/main/scala/trail/Codec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,39 @@ package trail
import scala.util.Try

trait Codec[T] {
def encode(s: T): String
def decode(s: String): Option[T]
def encode(value: T): Option[String]
def decode(value: Option[String]): Option[T]
}

object Codec {
implicit case object BooleanArg extends Codec[Boolean] {
override def encode(s: Boolean): String = s.toString
override def decode(s: String): Option[Boolean] = Try(s.toBoolean).toOption
implicit case object BooleanCodec extends Codec[Boolean] {
override def encode(s: Boolean): Option[String] = Some(s.toString)
override def decode(s: Option[String]): Option[Boolean] =
s.flatMap(s => Try(s.toBoolean).toOption)
}

implicit case object IntArg extends Codec[Int] {
override def encode(s: Int): String = s.toString
override def decode(s: String): Option[Int] = Try(s.toInt).toOption
implicit case object IntCodec extends Codec[Int] {
override def encode(s: Int): Option[String] = Some(s.toString)
override def decode(s: Option[String]): Option[Int] =
s.flatMap(s => Try(s.toInt).toOption)
}

implicit case object LongArg extends Codec[Long] {
override def encode(s: Long): String = s.toString
override def decode(s: String): Option[Long] = Try(s.toLong).toOption
implicit case object LongCodec extends Codec[Long] {
override def encode(s: Long): Option[String] = Some(s.toString)
override def decode(s: Option[String]): Option[Long] =
s.flatMap(s => Try(s.toLong).toOption)
}

implicit case object StringArg extends Codec[String] {
override def encode(s: String): String = s
override def decode(s: String): Option[String] = Option(s)
implicit case object StringCodec extends Codec[String] {
override def encode(s: String): Option[String] = Some(s)
override def decode(s: Option[String]): Option[String] = s
}
}

implicit def OptionCodec[T](implicit codec: Codec[T]): Codec[Option[T]] =
new Codec[Option[T]] {
override def encode(s: Option[T]): Option[String] =
s.flatMap(codec.encode)
override def decode(s: Option[String]): Option[Option[T]] =
if (s.isEmpty) Some(None) else codec.decode(s).map(Some(_))
}
}
30 changes: 0 additions & 30 deletions shared/src/main/scala/trail/Param.scala

This file was deleted.

34 changes: 24 additions & 10 deletions shared/src/main/scala/trail/PathElement.scala
Original file line number Diff line number Diff line change
@@ -1,17 +1,31 @@
package trail

import scala.annotation.implicitNotFound
case class Arg[T]()(implicit val codec: Codec[T]) {
override def equals(o: Any): Boolean =
o match {
case a: Arg[T] => a.codec.equals(codec)
case _ => false
}

@implicitNotFound("${T} cannot be used as a path element. Define an instance StaticElement[${T}].")
class PathElement[T, U](val f: T => U)
override def hashCode(): Int = ("trail.Arg", codec).hashCode()
}

case class Param[T](name: String)(implicit val codec: Codec[T]) {
override def equals(o: Any): Boolean =
o match {
case p: Param[T] => p.name.equals(name) && p.codec.equals(codec)
case _ => false
}

class StaticElement[T](f: T => String) extends PathElement[T, String](f)
override def hashCode(): Int = ("trail.Param", name, codec).hashCode()
}

object PathElement {
implicit def argElement[T] = new PathElement[Arg[T], Arg[T]](identity)
case class Fragment[T]()(implicit val codec: Codec[T]) {
override def equals(o: Any): Boolean =
o match {
case f: Fragment[T] => f.codec.equals(codec)
case _ => false
}

implicit object StringElement extends StaticElement[String ](identity)
implicit object BooleanElement extends StaticElement[Boolean](_.toString)
implicit object IntElement extends StaticElement[Int ](_.toString)
implicit object LongElement extends StaticElement[Long ](_.toString)
override def hashCode(): Int = ("trail.Fragment", codec).hashCode()
}
Loading

0 comments on commit e7be86b

Please sign in to comment.