Skip to content

Commit

Permalink
Restructure DSL in to traits and change driver to a function called H…
Browse files Browse the repository at this point in the history
…ttpClient
  • Loading branch information
IainHull committed Dec 15, 2013
1 parent 4e7ed26 commit 41f0707
Show file tree
Hide file tree
Showing 12 changed files with 316 additions and 230 deletions.
182 changes: 123 additions & 59 deletions src/main/scala/org/iainhull/resttest/Api.scala
Expand Up @@ -4,8 +4,8 @@ import java.net.URI
import java.net.URLEncoder

/**
* Provides the main api for creating and sending REST Web service requests.
*
* Provides the main api for creating and sending REST Web service requests.
*
* {{{
* val request = Request(GET, new URI("http://api.rest.org/person", Map(), None))
* val response = driver.execute(request)
Expand All @@ -15,33 +15,46 @@ import java.net.URLEncoder
* None => fail("Expected a body"))
* }
* }}}
*
*
* or using the [[RequestBuilder]]
* {{{
* val request = driver.execute(RequestBuilder().withUrl("http://api.rest.org/person/").withMethod(GET))
* }}}
*
* This provides the basic interface used to implement the [[Dsl]], users
* are expected to use the Dsl.
*
* This provides the basic interface used to implement the [[Dsl]], users
* are expected to use the Dsl.
*/
object Api {
trait Api {
/** The HTTP Methods used to make a request */
sealed abstract class Method(name: String)
case object GET extends Method("GET")
case object POST extends Method("POST")
case object PUT extends Method("PUT")
case object DELETE extends Method("DELETE")
type Method = Api.Impl.Method
val GET = Api.Impl.GET
val POST = Api.Impl.POST
val PUT = Api.Impl.PUT
val DELETE = Api.Impl.DELETE
val HEAD = Api.Impl.HEAD
val PATCH = Api.Impl.PATCH

/** The HTTP Request */
case class Request(method: Method, url: URI, headers: Map[String, List[String]] = Map(), body: Option[String] = None)

type Request = Api.Impl.Request
val Request = Api.Impl.Request

/** The HTTP Response */
case class Response(statusCode: Int, headers: Map[String, List[String]], body: Option[String])
type Response = Api.Impl.Response
val Response = Api.Impl.Response

/** The HTTP RequestBuilder */
type RequestBuilder = Api.Impl.RequestBuilder
val RequestBuilder = Api.Impl.RequestBuilder

/**
* Abstract interface for submitting REST `Requests` and receiving `Responses`
*/
type HttpClient = Request => Response

/**
* Convert a sequence of `(name, value)` tuples into a map of headers.
* Each tuple creates an entry in the map, duplicate `name`s add the
* `value` to the list.
* `value` to the list.
*/
def toHeaders(hs: (String, String)*): Map[String, List[String]] = {
hs.foldRight(Map[String, List[String]]()) {
Expand All @@ -65,58 +78,109 @@ object Api {
}
}

/**
* Abstract interface for submitting REST `Requests` and receiving `Responses`
*/
trait Driver {
def execute(request: Request): Response
}
val Status = Api.Impl.Status
}

object Api extends Api {

/**
* Constants for HTTP Status Codes
* Implementation types used by the [[Api]], these are forward declared in the `Api` trait
*/
object Status {
val OK = 200
val Created = 201
val Accepted = 202
val BadRequest = 400
val Unauthorized = 401
val PaymentRequired = 402
val Forbidden = 403
val NotFound = 404
}
object Impl {
sealed abstract class Method(name: String)
case object DELETE extends Method("DELETE")
case object GET extends Method("GET")
case object HEAD extends Method("HEAD")
case object PATCH extends Method("PATCH")
case object POST extends Method("POST")
case object PUT extends Method("PUT")

case class RequestBuilder(
method: Option[Method],
url: Option[URI],
query: Seq[(String, String)],
headers: Seq[(String, String)],
queryParams: Seq[(String, String)],
body: Option[String]) {

def withMethod(method: Method): RequestBuilder = copy(method = Some(method))
def withUrl(url: String): RequestBuilder = copy(url = Some(new URI(url)))
def withBody(body: String): RequestBuilder = copy(body = Some(body))
def addPath(path: String): RequestBuilder = {
val s = url.get.toString
val slash = if (s.endsWith("/")) "" else "/"
copy(url = Some(new URI(s + slash + path)))
}
def addHeaders(hs: (String, String)*) = copy(headers = headers ++ hs)
def addQuery(qs: (String, String)*) = copy(queryParams = queryParams ++ qs)
/** The HTTP Request */
case class Request(method: Method, url: URI, headers: Map[String, List[String]] = Map(), body: Option[String] = None)

/** The HTTP Response */
case class Response(statusCode: Int, headers: Map[String, List[String]], body: Option[String])

case class RequestBuilder(
method: Option[Method],
url: Option[URI],
query: Seq[(String, String)],
headers: Seq[(String, String)],
queryParams: Seq[(String, String)],
body: Option[String]) {

def withMethod(method: Method): RequestBuilder = copy(method = Some(method))
def withUrl(url: String): RequestBuilder = copy(url = Some(new URI(url)))
def withBody(body: String): RequestBuilder = copy(body = Some(body))
def addPath(path: String): RequestBuilder = {
val s = url.get.toString
val slash = if (s.endsWith("/")) "" else "/"
copy(url = Some(new URI(s + slash + path)))
}
def addHeaders(hs: (String, String)*) = copy(headers = headers ++ hs)
def addQuery(qs: (String, String)*) = copy(queryParams = queryParams ++ qs)

def toRequest: Request = {
val fullUrl = URI.create(url.get + toQueryString(queryParams: _*))
Request(method.get, fullUrl, toHeaders(headers: _*), body)
}

def toRequest: Request = {
val fullUrl = URI.create(url.get + toQueryString(queryParams: _*))
Request(method.get, fullUrl, toHeaders(headers: _*), body)
}

}
object RequestBuilder {
implicit val emptyBuilder = RequestBuilder(None, None, Seq(), Seq(), Seq(), None)

object RequestBuilder {
implicit val emptyBuilder = RequestBuilder(None, None, Seq(), Seq(), Seq(), None)
def apply()(implicit builder: RequestBuilder): RequestBuilder = {
builder
}
}

def apply()(implicit builder: RequestBuilder): RequestBuilder = {
builder
/**
* Constants for HTTP Status Codes
*/
object Status {
val Continue = 100
val SwitchingProtocols = 101
val OK = 200
val Created = 201
val Accepted = 202
val NonAuthoritativeInformation = 203
val NoContent = 204
val ResetContent = 205
val PartialContent = 206
val MultipleChoices = 300
val MovedPermanently = 301
val Found = 302
val SeeOther = 303
val NotModified = 304
val UseProxy = 305
val SwitchProxy = 306
val TemporaryRedirect = 307
val PermanentRedirect = 308
val BadRequest = 400
val Unauthorized = 401
val PaymentRequired = 402
val Forbidden = 403
val NotFound = 404
val MethodNotAllowed = 405
val NotAcceptable = 406
val ProxyAuthenticationRequired = 407
val RequestTimeout = 408
val Conflict = 409
val Gone = 410
val LengthRequired = 411
val PreconditionFailed = 412
val RequestEntityTooLarge = 413
val RequestUriTooLong = 414
val UnsupportedMediaType = 415
val RequestedRangeNotSatisfiable = 416
val ExpectationFailed = 417
val InternalServerError = 500
val NotImplemented = 501
val BadGateway = 502
val ServiceUnavailable = 503
val GatewayTimeout = 504
val HttpVersionNotSupported = 505
}
}
}
35 changes: 26 additions & 9 deletions src/main/scala/org/iainhull/resttest/Dsl.scala
Expand Up @@ -90,9 +90,8 @@ import java.net.URI
* Exception (failing the test). See [[Extractors]] for more information on the available default `Extractor`s
* And how to implement your own.
*/
trait Dsl extends Extractors {
trait Dsl extends Api with Extractors {
import language.implicitConversions
import Api._

implicit def toRequest(builder: RequestBuilder): Request = builder.toRequest
implicit def methodToRequestBuilder(method: Method)(implicit builder: RequestBuilder): RequestBuilder = builder.withMethod(method)
Expand All @@ -113,15 +112,15 @@ trait Dsl extends Extractors {
def /(p: Any) = builder.addPath(p.toString)
def :?(params: (Symbol, Any)*) = builder.addQuery(params map (p => (p._1.name, p._2.toString)): _*)

def execute()(implicit driver: Driver): Response = {
driver.execute(builder)
def execute()(implicit client: HttpClient): Response = {
client(builder)
}

def apply[T](proc: RequestBuilder => T): T = {
proc(builder)
}

def asserting(assertions: Assertion*)(implicit driver: Driver): Response = {
def asserting(assertions: Assertion*)(implicit client: HttpClient): Response = {
val res = execute()
val assertionResults = for {
a <- assertions
Expand All @@ -133,18 +132,36 @@ trait Dsl extends Extractors {
res
}

def expecting[T](func: Response => T)(implicit driver: Driver): T = {
def expecting[T](func: Response => T)(implicit client: HttpClient): T = {
val res = execute()
func(res)
}
}

/**
* Extend the default request's configuration so that partially configured requests to be reused. Foe example:
*
* {{{
* using(_ url "http://api.rest.org/person") { implicit rb =>
* GET asserting (StatusCode === Status.OK, jsonBodyAsList[Person] === EmptyList)
* val id = POST body personJson asserting (StatusCode === Status.Created) returning (Header("X-Person-Id"))
* GET / id asserting (StatusCode === Status.OK, jsonBodyAs[Person] === Jason)
* }
* }}}
*
* @param config
* a function to configure the default request
* @param block
* the block of code where the the newly configured request is applied
* @param builder
* the current default request, implicitly resolved, defaults to the empty request
*/
def using(config: RequestBuilder => RequestBuilder)(process: RequestBuilder => Unit)(implicit builder: RequestBuilder): Unit = {
process(config(builder))
}

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

Expand All @@ -161,8 +178,8 @@ trait Dsl extends Extractors {
}
}

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 def requestBuilderToRichResponse(builder: RequestBuilder)(implicit client: HttpClient): RichResponse = new RichResponse(builder.execute())
implicit def methodToRichResponse(method: Method)(implicit builder: RequestBuilder, client: HttpClient): RichResponse = new RichResponse(builder.withMethod(method).execute())

/**
* Add operator support to `Extractor`s these are used to generate an `Assertion` using the extracted value.
Expand Down

0 comments on commit 41f0707

Please sign in to comment.