Skip to content

Commit

Permalink
Updates before dub scala presentation
Browse files Browse the repository at this point in the history
  • Loading branch information
IainHull committed Oct 22, 2013
1 parent e15b620 commit e4ac669
Show file tree
Hide file tree
Showing 9 changed files with 304 additions and 238 deletions.
96 changes: 62 additions & 34 deletions src/main/scala/org/iainhull/resttest/Dsl.scala
Expand Up @@ -11,7 +11,11 @@ object Dsl extends Extractors {
implicit def methodToRichRequestBuilder(method: Method)(implicit builder: RequestBuilder): RichRequestBuilder = new RichRequestBuilder(methodToRequestBuilder(method)(builder))

trait Assertion {
def verify(res: Response): Unit
def result(res: Response): Option[String]
}

def assertionFailed(assertionResults: Seq[String]): Throwable = {
new AssertionError(assertionResults.mkString(","))
}

implicit class RichRequestBuilder(builder: RequestBuilder) {
Expand All @@ -29,9 +33,15 @@ object Dsl extends Extractors {
proc(builder)
}

def asserting(assertions: Assertion*)(implicit driver: Driver): Response = {
def assert(assertions: Assertion*)(implicit driver: Driver): Response = {
val res = execute()
assertions foreach (_.verify(res))
val assertionResults = for {
a <- assertions
r <- a.result(res)
} yield r
if(assertionResults.nonEmpty) {
throw assertionFailed(assertionResults)
}
res
}

Expand All @@ -41,56 +51,74 @@ object Dsl extends Extractors {
}
}

object ~ {
def unapply(res: Response): Option[(Response, Response)] = {
Some((res, res))
}
}

def using(config: RequestBuilder => RequestBuilder)(process: RequestBuilder => Unit)(implicit builder: RequestBuilder): Unit = {
process(config(builder))
}

implicit class RichResponse(response: Response) {
def returning[T1](ext1: Extractor[T1])(implicit driver: Driver): T1 = {
ext1.op(response)
def returning[T1](ext1: ExtractorLike[T1])(implicit driver: Driver): T1 = {
ext1.value(response)
}

def returning[T1, T2](ext1: Extractor[T1], ext2: Extractor[T2]): (T1, T2) = {
(ext1.op(response), ext2.op(response))
def returning[T1, T2](ext1: ExtractorLike[T1], ext2: ExtractorLike[T2]): (T1, T2) = {
(ext1.value(response), ext2.value(response))
}

def returning[T1, T2, T3](ext1: Extractor[T1], ext2: Extractor[T2], ext3: Extractor[T3]): (T1, T2, T3) = {
(ext1.op(response), ext2.op(response), ext3.op(response))
def returning[T1, T2, T3](ext1: ExtractorLike[T1], ext2: ExtractorLike[T2], ext3: ExtractorLike[T3]): (T1, T2, T3) = {
(ext1.value(response), ext2.value(response), ext3.value(response))
}

def returning[T1, T2, T3, T4](ext1: Extractor[T1], ext2: Extractor[T2], ext3: Extractor[T3], ext4: Extractor[T4]): (T1, T2, T3, T4) = {
(ext1.op(response), ext2.op(response), ext3.op(response), ext4.op(response))
def returning[T1, T2, T3, T4](ext1: ExtractorLike[T1], ext2: ExtractorLike[T2], ext3: ExtractorLike[T3], ext4: ExtractorLike[T4]): (T1, T2, T3, T4) = {
(ext1.value(response), ext2.value(response), ext3.value(response), ext4.value(response))
}
}

implicit def requestBuilderToRichResponse(builder: RequestBuilder)(implicit driver: Driver): RichResponse = new RichResponse(builder.execute())
implicit def methodToRichResponse(method: Method)(implicit builder: RequestBuilder, driver: Driver): RichResponse = new RichResponse(builder.withMethod(method).execute())

implicit class RichExtractor[T](ext: Extractor[T]) {
def is(expected: T): Assertion = new Assertion {
override def verify(res: Response): Unit = {
val actual = ext.op(res)
if (actual != expected) throw new AssertionError(actual + " != " + expected)
/**
* Add operator support to `Extractor`s these are used to generate an `Assertion` using the extracted value.
*
* {{{
* GET url "http://api.rest.org/person" assert (StatusCode === Status.Ok)
* }}}
*
* == Operations ==
*
* The following operations are added to all `Extractors`
*
* $ - `extractor === expected` - the extracted value is equal to the `expected` value.
* $ - `extractor !== expected` - the extracted value is not equal to the `expected` value.
* $ - `extractor in (expected1, expected2, ...)` - the extracted value is in the list of expected values.
* $ - `extractor notIn (expected1, expected2, ...)` - the extracted value is in the list of expected values.
*
* The following operations are added to `Extractor`s that support `scala.math.Ordering`.
* More precisely these operations are added to `Extractor[T]` if there exists an implicit
* `Ordering[T]` for any type `T`.
*
* $ - `extractor < expected` - the extracted value is less than the `expected` value.
* $ - `extractor <= expected` - the extracted value is less than or equal to the `expected` value.
* $ - `extractor > expected` - the extracted value is greater than the `expected` value.
* $ - `extractor <= expected` - the extracted value is greater than or equal to the `expected` value.
*/
implicit class RichExtractor[A](ext: ExtractorLike[A]) {
def ===[B >: A](expected: B): Assertion = makeAssertion(_ == expected, expected, "did not equal")
def !==[B >: A](expected: B): Assertion = makeAssertion(_ != expected, expected, "did equal")

def < [B >: A](expected: B)(implicit ord: math.Ordering[B]): Assertion = makeAssertion(ord.lt (_, expected), expected, "was not less than")
def <=[B >: A](expected: B)(implicit ord: math.Ordering[B]): Assertion = makeAssertion(ord.lteq(_, expected), expected, "was not less than or equal")
def > [B >: A](expected: B)(implicit ord: math.Ordering[B]): Assertion = makeAssertion(ord.gt( _, expected), expected, "was not greater than")
def >=[B >: A](expected: B)(implicit ord: math.Ordering[B]): Assertion = makeAssertion(ord.gteq(_, expected), expected, "was not greater than or equal")

def in[B >: A](expectedVals: B*): Assertion = makeAssertion(expectedVals.contains(_), expectedVals.mkString("(", ", ", ")"), "was not in")
def notIn[B >: A](expectedVals: B*): Assertion = makeAssertion(!expectedVals.contains(_), expectedVals.mkString("(", ", ", ")"), "was in")

private def makeAssertion[B](pred: A => Boolean, expected: Any, text: String) = new Assertion {
override def result(res: Response): Option[String] = {
val actual = ext.value(res)
if (!pred(actual)) Some(s"${ext.name}: $actual $text $expected") else None
}
}

def isNot(expected: T): Assertion = new Assertion {
override def verify(res: Response): Unit = {
val actual = ext.op(res)
if (actual == expected) throw new AssertionError(actual + " == " + expected)
}
}
def isIn(expectedVals: T*): Assertion = new Assertion {
override def verify(res: Response): Unit = {
val actual = ext.op(res)
if (!expectedVals.contains(actual)) throw new AssertionError(actual + " not in " + expectedVals)
}
}
}
}
159 changes: 103 additions & 56 deletions src/main/scala/org/iainhull/resttest/Extractors.scala
Expand Up @@ -3,9 +3,94 @@ package org.iainhull.resttest
import Api._
import scala.util.Try

case class Extractor[+T](name: String, op: Response => T) {
def unapply(res: Response): Option[T] = Try { op(res) }.toOption
def value(implicit res: Response): T = op(res)
trait ExtractorLike[+A] {
def name: String
def unapply(res: Response): Option[A] = Try { value(res) }.toOption
def value(implicit res: Response): A
}

/**
* Primary implementation of ExtractorLike. The name and extraction
*
* @param name
* The name of the extractor
* @param op
* The operation to extract the value of type `A` from the `Response`
*/
case class Extractor[+A](name: String, op: Response => A) extends ExtractorLike[A] {
def value(implicit res: Response): A = op(res)

/**
* Create a new `Extractor` by executing a new function to modify the result.
* Normally followed by `as`.
*
* {{{
* val JsonBody = BodyText andThen Json.parse as "JsonBody"
* }}}
*/
def andThen[B](nextOp: A => B): Extractor[B] = copy(name = name + ".andThen ?", op = op andThen nextOp)

/**
* Rename the extractor
*/
def as(newName: String) = copy(name = newName)
}

case class Header(header: String) extends ExtractorLike[String] {
val asText = Extractor[String]("Header(" + header + ")", _.headers(header).mkString(","))
val asList = Extractor[List[String]]("Header(" + header + ").asList", _.headers(header))
val asOption = Extractor[Option[List[String]]]("Header(" + header + ").asOption", _.headers.get(header))
val isDefined = new Object {
def unapply(res: Response): Boolean = {
res.headers.contains(header)
}
}

def ->(value: String): (String, String) = {
(header, value)
}

override def name: String = asText.name
override def unapply(res: Response): Option[String] = asText.unapply(res)
override def value(implicit res: Response): String = asText.value

}

object Header {
val AccessControlAllowOrigin = Header("Access-Control-Allow-Origin")
val AcceptRanges = Header("Accept-Ranges")
val Age = Header("Age")
val Allow = Header("Allow")
val CacheControl = Header("Cache-Control")
val Connection = Header("Connection")
val ContentEncoding = Header("Content-Encoding")
val ContentLanguage = Header("Content-Language")
val ContentLength = Header("Content-Length")
val ContentLocation = Header("Content-Location")
val ContentMd5 = Header("Content-MD5")
val ContentDisposition = Header("Content-Disposition")
val ContentRange = Header("Content-Range")
val ContentType = Header("Content-Type")
val Date = Header("Date")
val ETag = Header("ETag")
val Expires = Header("Expires")
val LastModified = Header("Last-Modified")
val Link = Header("Link")
val Location = Header("Location")
val P3P = Header("P3P")
val Pragma = Header("Pragma")
val ProxyAuthenticate = Header("Proxy-Authenticate")
val Refresh = Header("Refresh")
val RetryAfter = Header("Retry-After")
val Server = Header("Server")
val SetCookie = Header("Set-Cookie")
val StrictTransportSecurity = Header("Strict-Transport-Security")
val Trailer = Header("Trailer")
val TransferEncoding = Header("Transfer-Encoding")
val Vary = Header("Vary")
val Via = Header("Via")
val Warning = Header("Warning")
val WwwAuthenticate = Header("WWW-Authenticate")
}

trait Extractors {
Expand All @@ -18,61 +103,23 @@ trait Extractors {

val BodyText = Extractor[String]("body", _.body.get)

def headerText(name: String) = Extractor[String]("header(" + name + ")", _.headers(name).mkString(","))

def headerList(name: String) = Extractor[List[String]]("headerList(" + name + ")", _.headers(name))

def header(name: String) = Extractor[Option[List[String]]]("headerOption(" + name + ")", _.headers.get(name))

class Header(name: String) {
lazy val list = headerList(name)
lazy val text = headerText(name)
lazy val present = new Object {
def unapply(res: Response): Boolean = {
res.headers.contains(name)
}
}
def -> (value: String): (String, String) = {
(name, value)
/**
* Enable Extractors to be chained together in case clauses.
*
* For example:
* {{{
* GET / id expecting {
* case StatusCode(Status.OK) ~ Header.ContentType(ct) ~ BodyAsPerson(person) =>
* ct should be("application/json")
* person should be(Jason)
* }
* }}}
*/
object ~ {
def unapply(res: Response): Option[(Response, Response)] = {
Some((res, res))
}
}

object Header {
val AccessControlAllowOrigin = new Header("Access-Control-Allow-Origin")
val AcceptRanges = new Header("Accept-Ranges")
val Age = new Header("Age")
val Allow = new Header("Allow")
val CacheControl = new Header("Cache-Control")
val Connection = new Header("Connection")
val ContentEncoding = new Header("Content-Encoding")
val ContentLanguage = new Header("Content-Language")
val ContentLength = new Header("Content-Length")
val ContentLocation = new Header("Content-Location")
val ContentMd5 = new Header("Content-MD5")
val ContentDisposition = new Header("Content-Disposition")
val ContentRange = new Header("Content-Range")
val ContentType = new Header("Content-Type")
val Date = new Header("Date")
val ETag = new Header("ETag")
val Expires = new Header("Expires")
val LastModified = new Header("Last-Modified")
val Link = new Header("Link")
val Location = new Header("Location")
val P3P = new Header("P3P")
val Pragma = new Header("Pragma")
val ProxyAuthenticate = new Header("Proxy-Authenticate")
val Refresh = new Header("Refresh")
val RetryAfter = new Header("Retry-After")
val Server = new Header("Server")
val SetCookie = new Header("Set-Cookie")
val StrictTransportSecurity = new Header("Strict-Transport-Security")
val Trailer = new Header("Trailer")
val TransferEncoding = new Header("Transfer-Encoding")
val Vary = new Header("Vary")
val Via = new Header("Via")
val Warning = new Header("Warning")
val WwwAuthenticate = new Header("WWW-Authenticate")
}
}

object Extractors extends Extractors {
Expand Down
32 changes: 27 additions & 5 deletions src/main/scala/org/iainhull/resttest/JerseyDriver.scala
Expand Up @@ -3,6 +3,7 @@ package org.iainhull.resttest
import com.sun.jersey.api.client.Client
import com.sun.jersey.api.client.ClientResponse
import scala.collection.JavaConverters
import com.sun.jersey.api.client.WebResource

object Jersey {
import Api._
Expand All @@ -16,14 +17,35 @@ object Jersey {
}

def createClientResponse(request: Request): ClientResponse = {
val resource = jersey.resource(request.url)
val builder = addRequestHeaders(request.headers, jersey.resource(request.url).getRequestBuilder)

for (b <- request.body) {
builder.entity(b)
}

request.method match {
case GET => resource.get(classOf[ClientResponse])
case POST => resource.post(classOf[ClientResponse])
case PUT => resource.put(classOf[ClientResponse])
case DELETE => resource.delete(classOf[ClientResponse])
case GET => builder.get(classOf[ClientResponse])
case POST => builder.post(classOf[ClientResponse])
case PUT => builder.put(classOf[ClientResponse])
case DELETE => builder.delete(classOf[ClientResponse])
}
}

def addRequestHeaders(headers: Map[String, List[String]], builder: WebResource#Builder): WebResource#Builder = {
def forAllNames(names: List[String], b: WebResource#Builder): WebResource#Builder = {
names match {
case h :: t => forAllNames(t, forAllValues(h, headers(h), b))
case Nil => b
}
}
def forAllValues(name: String, values: List[String], b: WebResource#Builder): WebResource#Builder = {
values match {
case h :: t => forAllValues(name, t, b.header(name, h))
case Nil => b
}
}
forAllNames(headers.keys.toList, builder)
}

def headers(response: ClientResponse): Map[String, List[String]] = {
import JavaConverters._
Expand Down

0 comments on commit e4ac669

Please sign in to comment.