Skip to content

Commit

Permalink
Added tests
Browse files Browse the repository at this point in the history
  • Loading branch information
Dave Gurnell committed Oct 5, 2011
1 parent 35277bd commit bba0d8e
Show file tree
Hide file tree
Showing 6 changed files with 117 additions and 41 deletions.
2 changes: 2 additions & 0 deletions 3-real-paths/build.sbt
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
name := "real-paths"

version := "0.1"

libraryDependencies += "org.scala-tools.testing" %% "specs" % "1.6.9" % "test"
31 changes: 27 additions & 4 deletions 3-real-paths/src/main/scala/Arg.scala
Original file line number Diff line number Diff line change
@@ -1,14 +1,32 @@
import java.net.URLEncoder.{encode => urlEncode}
import java.net.URLDecoder.{decode => urlDecode}

/**
* Trait describing the required behaviour to map URL path segments
* to typed values we can use in our applications.
*
* For example, the URL:
*
* http://example.com/abc/123
*
* has a path "/abc/123" containing two segments: "abc" and "123".
*
* You can subclass this for any T: String, Int, Person, etc.
*/
trait Arg[T] {

/**
* Attempt to decode a URL path segment into a typed value.
* Return Some(value) if successful, None if unsuccessful.
*/
def decode(in: String): Option[T]

/** Encode a typed value as a URL path segment. */
def encode(in: T): String

}

/** Maps between path segments and integers. */
case object IntArg extends Arg[Int] {

def encode(value: Int) =
Expand All @@ -23,6 +41,7 @@ case object IntArg extends Arg[Int] {

}

/** Maps between path segments and strings. Escapes/unescapes characters that are reserved in URLs. */
case object StringArg extends Arg[String] {

def encode(value: String) =
Expand All @@ -33,12 +52,16 @@ case object StringArg extends Arg[String] {

}

case class LiteralArg(val segment: String) extends Arg[Unit] {
/**
* Dummy arg to match against a fixed URL segment.
* Only decodes the segment if it matches the exact string specified in the constructor.
*/
case class LiteralArg(val expected: String) extends Arg[Unit] {

def encode(value: Unit) =
urlEncode(segment, "utf-8")
urlEncode(expected, "utf-8")

def decode(path: String) =
Some(())
if(urlDecode(path, "utf-8") == expected) Some(()) else None

}
57 changes: 20 additions & 37 deletions 3-real-paths/src/main/scala/Path.scala
Original file line number Diff line number Diff line change
Expand Up @@ -12,28 +12,11 @@ sealed trait Path {
/** The type of data we're extracting from the path, i.e. ignoring segments that aren't arguments. */
type Result <: HList

/** Decode a URL path into an argument list, preserving the order of the arguments. */
final def decode(path: List[String]): Option[Result] =
decodeReversed(path.reverse)
/** Decode a URL path into an argument list. */
def decode(path: List[String]): Option[Result]

/** Encode an HList as a URL path, preserving the order of the arguments. */
final def encode(args: Result): List[String] =
encodeReversed(args).reverse

/**
* Decode a URL path into an argument list, reversing the order of the arguments.
*
* The reversal places PNil at the end of the URL path, which will eventually
* provide support for rest-arguments.
*/
def decodeReversed(path: List[String]): Option[Result]

/** Encode an HList as a reversed URL path, reversing the order of the arguments.
*
* The reversal places PNil at the end of the URL path, which will eventually
* provide support for rest-arguments.
*/
def encodeReversed(args: Result): List[String]
/** Encode an HList as a URL path. */
def encode(args: Result): List[String]

}

Expand All @@ -53,23 +36,23 @@ case class PLiteral[T <: Path](headString: String, val tail: T) extends PCons[Un
val head: Arg[Unit] =
LiteralArg(headString)

def decodeReversed(path: List[String]): Option[Result] =
def decode(path: List[String]): Option[Result] =
path match {
case Nil => None
case h :: t =>
for {
h2 <- head.decode(h)
t2 <- tail.decodeReversed(t)
t2 <- tail.decode(t)
} yield t2
}

def encodeReversed(args: Result): List[String] =
head.encode(()) :: tail.encodeReversed(args)
def encode(args: Result): List[String] =
head.encode(()) :: tail.encode(args)

def /(arg: String) =
def :/:(arg: String) =
PLiteral(arg, this)

def /[T](arg: Arg[T]) =
def :/:[T](arg: Arg[T]) =
PArg(arg, this)

}
Expand All @@ -80,23 +63,23 @@ case class PArg[H, T <: Path](val head: Arg[H], val tail: T) extends PCons[H, T]
type Tail = T
type Result = HCons[H, tail.Result]

def decodeReversed(path: List[String]): Option[Result] =
def decode(path: List[String]): Option[Result] =
path match {
case Nil => None
case h :: t =>
for {
h2 <- head.decode(h)
t2 <- tail.decodeReversed(t)
t2 <- tail.decode(t)
} yield HCons(h2, t2)
}

def encodeReversed(args: Result): List[String] =
head.encode(args.head) :: tail.encodeReversed(args.tail)
def encode(args: Result): List[String] =
head.encode(args.head) :: tail.encode(args.tail)

def /(arg: String) =
def :/:(arg: String) =
PLiteral(arg, this)

def /[T](arg: Arg[T]) =
def :/:[T](arg: Arg[T]) =
PArg(arg, this)

}
Expand All @@ -105,19 +88,19 @@ sealed abstract class PNil extends Path {

type Result = HNil

def decodeReversed(path: List[String]): Option[Result] =
def decode(path: List[String]): Option[Result] =
path match {
case Nil => Some(HNil)
case _ => None
}

def encodeReversed(args: Result): List[String] =
def encode(args: Result): List[String] =
Nil

def /(arg: String) =
def :/:(arg: String) =
PLiteral(arg, this)

def /[T](arg: Arg[T]) =
def :/:[T](arg: Arg[T]) =
PArg(arg, this)

}
Expand Down
37 changes: 37 additions & 0 deletions 3-real-paths/src/test/scala/ArgSpec.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import org.specs._

class ArgSpec extends Specification {

"IntArg.encode encodes as expected" in {
IntArg.encode(123) mustEqual "123"
}

"IntArg.decode decodes integers correctly" in {
IntArg.decode("123") must beSome(123)
}

"IntArg.decode only decodes integers" in {
IntArg.decode("abc") must beNone
}

"StringArg.encode unescapes reserved characters" in {
StringArg.encode("a/b") mustEqual "a%2Fb"
}

"StringArg.decode escapes reserved characters" in {
StringArg.decode("a%2Fb") must beSome("a/b")
}

"LiteralArg.encode encodes to a literal" in {
LiteralArg("anything").encode(()) mustEqual "anything"
}

"LiteralArg.decode decodes to Unit" in {
LiteralArg("anything").decode("anything") must beSome(())
}

"LiteralArg.decode only decodes the correct path segment" in {
LiteralArg("anything").decode("nothing") must beNone
}

}
15 changes: 15 additions & 0 deletions 3-real-paths/src/test/scala/HListSpec.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import org.specs._

class HListSpec extends Specification {

"HLists can have heterogeneous arguments" in {
val list = 2.0 :: 1 :: HNil

list.head must be_==(2.0)
list.tail must be_==(HCons(1, HNil))

list.tail.head must be_==(1)
list.tail.tail must be_==(HNil)
}

}
16 changes: 16 additions & 0 deletions 3-real-paths/src/test/scala/PathSpec.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import org.specs._

class PathSpec extends Specification {

val path = StringArg :/: IntArg :/: PNil

"Path.encode works as expected" in {
path.encode(HCons("abc", HCons(123, HNil))) mustEqual List("abc", "123")
}

"Path.decode works as expected" in {
path.decode(List("abc", "123")) must beSome(HCons("abc", HCons(123, HNil)))
path.decode(List("123", "abc")) must beNone
}

}

0 comments on commit bba0d8e

Please sign in to comment.