Skip to content

Commit

Permalink
Merge pull request #79 from agourlay/redesign-http-layer
Browse files Browse the repository at this point in the history
redesign http layer
  • Loading branch information
agourlay authored Jun 30, 2016
2 parents c773b61 + a549478 commit f9265b7
Show file tree
Hide file tree
Showing 14 changed files with 325 additions and 361 deletions.
41 changes: 21 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -206,24 +206,24 @@ Cornichon has a set of built-in steps for various HTTP calls and assertions on t

### HTTP effects

- GET, DELETE, HEAD and OPTIONS share the same signature
- GET, DELETE, HEAD, OPTIONS, POST, PUT and PATCH use the same request builder for request's body, URL parameters and headers.

```scala
get("http://superhero.io/daredevil")
head("http://superhero.io/daredevil")

get("http://superhero.io/daredevil").withParams("firstParam" "value1", "secondParam" "value2")
get("http://superhero.io/daredevil").withParams(
"firstParam" "value1",
"secondParam" "value2")

delete("http://superhero.io/daredevil").withHeaders(("Authorization", "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ=="))
```

- POST, PUT and PATCH share the same signature

```scala
post("http://superhero.io/batman", "JSON description of Batman goes here")
post("http://superhero.io/batman").withBody("JSON description of Batman goes here")

put("http://superhero.io/batman", "JSON description of Batman goes here").withParams("firstParam" "value1", "secondParam" "value2")
put("http://superhero.io/batman").withBody("JSON description of Batman goes here").withParams(
"firstParam" "value1",
"secondParam" "value2")

post("http://superhero.io/batman", "JSON description of Batman goes here").withHeaders(("Authorization", "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ=="))
patch("http://superhero.io/batman").withBody("JSON description of Batman goes here")
```


Expand Down Expand Up @@ -674,13 +674,14 @@ Then assert body.is(
It is also possible to inject random values inside placeholders using:

- ```<random-uuid>``` for a random UUID
- ```<random-positive-integer>``` for a random Integer between 0-1000
- ```<random-positive-integer>``` for a random Integer between 0-10000
- ```<random-string>``` for a random String of length 5
- ```<random-boolean>``` for a random Boolean string
- ```<timestamp>``` for the current timestamp

```scala
post("http://url.io/somethingWithAnId", payload = """
post("http://url.io/somethingWithAnId").withBody(
"""
{
"id" : "<random-uuid>"
}
Expand Down Expand Up @@ -780,14 +781,14 @@ trait MySteps {

def create_customer = EffectStep(
title = "create new customer",
effect = s
http.Post(
url = "/customer",
payload = some_json_payload_to_define,
params = Seq.empty,
headers = Seq.empty,
extractor = RootExtractor("customer")
)(s)
effect =
http.post(
url = "/customer",
body = some_json_payload_to_define,
params = Seq.empty,
headers = Seq.empty,
extractor = RootExtractor("customer")
)
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,15 @@ import scala.concurrent.duration._
trait CornichonFeature extends HttpDsl with ScalatestIntegration {
import com.github.agourlay.cornichon.CornichonFeature._

private val (globalClient, ec) = globalRuntime
private val engine = new Engine(ec)

protected var beforeFeature: Seq[() Unit] = Nil
protected var afterFeature: Seq[() Unit] = Nil

protected var beforeEachScenario: Seq[Step] = Nil
protected var afterEachScenario: Seq[Step] = Nil

private lazy val (globalClient, ec) = globalRuntime
private lazy val engine = new Engine(ec)

lazy val requestTimeout = 2000.millis
lazy val http = httpServiceByURL(baseUrl, requestTimeout)
lazy val baseUrl = ""
Expand All @@ -28,7 +28,7 @@ trait CornichonFeature extends HttpDsl with ScalatestIntegration {

protected def runScenario(s: Scenario) = {
println(s"Starting scenario '${s.name}'")
engine.runScenario(Session.newSession, afterEachScenario) {
engine.runScenario(Session.newSession, afterEachScenario.toVector) {
s.copy(steps = beforeEachScenario.toVector ++ s.steps)
}
}
Expand Down Expand Up @@ -61,11 +61,11 @@ private object CornichonFeature {
import java.util.concurrent.atomic.AtomicInteger
import com.github.agourlay.cornichon.http.client.AkkaHttpClient

implicit private val system = ActorSystem("akka-http-client")
implicit private val ec = system.dispatcher
implicit private val mat = ActorMaterializer()
implicit private lazy val system = ActorSystem("akka-http-client")
implicit private lazy val ec = system.dispatcher
implicit private lazy val mat = ActorMaterializer()

private val client = new AkkaHttpClient()
private lazy val client = new AkkaHttpClient()

private val registeredUsage = new AtomicInteger
private val safePassInRow = new AtomicInteger
Expand All @@ -84,7 +84,7 @@ private object CornichonFeature {
} else if (safePassInRow.get() > 0) safePassInRow.decrementAndGet()
}

val globalRuntime = (client, system.dispatcher)
lazy val globalRuntime = (client, system.dispatcher)
def reserveGlobalRuntime(): Unit = registeredUsage.incrementAndGet()
def releaseGlobalRuntime(): Unit = registeredUsage.decrementAndGet()
}
10 changes: 5 additions & 5 deletions src/main/scala/com/github/agourlay/cornichon/core/Engine.scala
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,24 @@ class Engine(executionContext: ExecutionContext) {

private implicit val ec = executionContext

def runScenario(session: Session, finallySteps: Seq[Step] = Seq.empty)(scenario: Scenario): ScenarioReport = {
def runScenario(session: Session, finallySteps: Vector[Step] = Vector.empty)(scenario: Scenario): ScenarioReport = {
val initMargin = 1
val titleLog = ScenarioTitleLogInstruction(s"Scenario : ${scenario.name}", initMargin)
val mainRunReport = runSteps(scenario.steps, session, Vector(titleLog), initMargin + 1)
if (finallySteps.isEmpty)
ScenarioReport.build(scenario.name, mainRunReport)
else {
// Reuse mainline session
val finallyReport = runSteps(finallySteps.toVector, mainRunReport.session, Vector.empty, initMargin + 1)
val finallyReport = runSteps(finallySteps, mainRunReport.session, Vector.empty, initMargin + 1)
ScenarioReport.build(scenario.name, mainRunReport, Some(finallyReport))
}
}

@tailrec
final def runSteps(steps: Vector[Step], session: Session, accLogs: Vector[LogInstruction], depth: Int): StepsResult =
if (steps.isEmpty) SuccessStepsResult(session, accLogs)
else {
if (steps.isEmpty)
SuccessStepsResult(session, accLogs)
else
steps(0).run(this, session, depth) match {
case SuccessStepsResult(newSession, updatedLogs)
val nextSteps = steps.drop(1)
Expand All @@ -35,7 +36,6 @@ class Engine(executionContext: ExecutionContext) {
case f: FailureStepsResult
f.copy(logs = accLogs ++ f.logs)
}
}

def XorToStepReport(currentStep: Step, session: Session, res: Xor[CornichonError, Session], title: String, depth: Int, show: Boolean, duration: Option[Duration] = None) =
res.fold(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,8 @@ case class DetailedStepAssertionError[A](result: A, detailedAssertion: A ⇒ Str
val msg = detailedAssertion(result)
}

case class ResolverParsingError(error: Throwable) extends CornichonError {
val msg = s"error thrown during resolver parsing ${error.getMessage}"
case class ResolverParsingError(input: String, error: Throwable) extends CornichonError {
val msg = s"error '${error.getMessage}' thrown during placeholder parsing for input $input"
}

case class AmbiguousKeyDefinition(key: String) extends CornichonError {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ class Resolver(extractors: Map[String, Mapper]) {
def findPlaceholders(input: String): Xor[CornichonError, List[Placeholder]] =
new PlaceholderParser(input).placeholdersRule.run() match {
case Failure(e: ParseError) right(List.empty)
case Failure(e: Throwable) left(new ResolverParsingError(e))
case Failure(e: Throwable) left(new ResolverParsingError(input, e))
case Success(dt) right(dt.toList)
}

Expand All @@ -35,7 +35,7 @@ class Resolver(extractors: Map[String, Mapper]) {

def builtInPlaceholders: PartialFunction[String, String] = {
case "random-uuid" UUID.randomUUID().toString
case "random-positive-integer" r.nextInt(1000).toString
case "random-positive-integer" r.nextInt(10000).toString
case "random-string" r.nextString(5)
case "random-boolean" r.nextBoolean().toString
case "timestamp" (System.currentTimeMillis / 1000).toString
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.github.agourlay.cornichon.core

import cats.data.Xor
import com.github.agourlay.cornichon.json.{ CornichonJson, JsonPath, NotStringFieldError }
import com.github.agourlay.cornichon.util.Formats
import io.circe.Json

import scala.collection.immutable.HashMap
Expand Down Expand Up @@ -64,7 +65,7 @@ case class Session(content: Map[String, Vector[String]]) extends CornichonJson {
def merge(otherSession: Session) =
copy(content = content ++ otherSession.content)

val prettyPrint = content.toSeq.sortBy(_._1).map(pair pair._1 + " -> " + pair._2).mkString("\n")
val prettyPrint = Formats.displayMap(content)

}

Expand Down
72 changes: 48 additions & 24 deletions src/main/scala/com/github/agourlay/cornichon/http/HttpDsl.scala
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,15 @@ import com.github.agourlay.cornichon.core._
import com.github.agourlay.cornichon.dsl._
import com.github.agourlay.cornichon.dsl.Dsl._
import com.github.agourlay.cornichon.http.HttpAssertions._
import com.github.agourlay.cornichon.http.HttpEffects._
import com.github.agourlay.cornichon.http.HttpMethods._
import com.github.agourlay.cornichon.http.HttpStreams._
import com.github.agourlay.cornichon.json.CornichonJson._
import com.github.agourlay.cornichon.json.JsonPath
import com.github.agourlay.cornichon.json.{ CornichonJson, JsonPath }
import com.github.agourlay.cornichon.steps.regular.EffectStep
import com.github.agourlay.cornichon.util.Formats
import io.circe.Json
import sangria.ast.Document
import sangria.renderer.QueryRenderer

import scala.concurrent.duration._

Expand All @@ -21,33 +25,53 @@ trait HttpDsl extends Dsl {

import com.github.agourlay.cornichon.http.HttpService._

implicit def toStep(request: HttpRequest): EffectStep = EffectStep(
title = request.description,
effect = s
request match {
case Get(url, params, headers) http.Get(url, params, headers)(s)
case Head(url, params, headers) http.Head(url, params, headers)(s)
case Options(url, params, headers) http.Options(url, params, headers)(s)
case Delete(url, params, headers) http.Delete(url, params, headers)(s)
case Post(url, payload, params, headers) http.Post(url, payload, params, headers)(s)
case Put(url, payload, params, headers) http.Put(url, payload, params, headers)(s)
case Patch(url, payload, params, headers) http.Patch(url, payload, params, headers)(s)
case OpenSSE(url, takeWithin, params, headers) http.OpenSSE(url, takeWithin, params, headers)(s)
case OpenWS(url, takeWithin, params, headers) http.OpenWS(url, takeWithin, params, headers)(s)
case q @ QueryGQL(url, params, headers, _, _, _) http.Post(url, q.fullPayload, params, headers)(s)
implicit def toStep(request: HttpRequest): EffectStep =
EffectStep(
title = request.description,
effect = http.requestEffect(request)
)

def get(url: String) = HttpRequest(GET, url, None, Seq.empty, Seq.empty)
def head(url: String) = HttpRequest(HEAD, url, None, Seq.empty, Seq.empty)
def options(url: String) = HttpRequest(OPTIONS, url, None, Seq.empty, Seq.empty)
def delete(url: String) = HttpRequest(DELETE, url, None, Seq.empty, Seq.empty)
def post(url: String) = HttpRequest(POST, url, None, Seq.empty, Seq.empty)
def put(url: String) = HttpRequest(PUT, url, None, Seq.empty, Seq.empty)
def patch(url: String) = HttpRequest(PATCH, url, None, Seq.empty, Seq.empty)

implicit def toStep(request: HttpStreamedRequest): EffectStep =
EffectStep(
title = request.description,
effect = http.streamEffect(request)
)

def open_sse(url: String, takeWithin: FiniteDuration) = HttpStreamedRequest(SSE, url, takeWithin, Seq.empty, Seq.empty)
def open_ws(url: String, takeWithin: FiniteDuration) = HttpStreamedRequest(WS, url, takeWithin, Seq.empty, Seq.empty)

implicit def toStep(queryGQL: QueryGQL): EffectStep = {
import io.circe.generic.auto._
import io.circe.syntax._

// Used only for display - problem being that the query is a String and looks ugly inside the full JSON object.
val payload = queryGQL.query.source.getOrElse(QueryRenderer.render(queryGQL.query, QueryRenderer.Pretty))

val fullPayload = prettyPrint(GqlPayload(payload, queryGQL.operationName, queryGQL.variables).asJson)

val prettyVar = queryGQL.variables.fold("") { variables
" and with variables " + Formats.displayMap(variables, CornichonJson.prettyPrint)
}
)

def get(url: String) = Get(url, Seq.empty, Seq.empty)
def delete(url: String) = Delete(url, Seq.empty, Seq.empty)
val prettyOp = queryGQL.operationName.fold("")(o s" and with operationName $o")

def post(url: String, payload: String) = Post(url, payload, Seq.empty, Seq.empty)
def put(url: String, payload: String) = Put(url, payload, Seq.empty, Seq.empty)
EffectStep(
title = s"query GraphQL endpoint ${queryGQL.url} with query $payload$prettyVar$prettyOp",
effect = http.post(queryGQL.url, Some(fullPayload), Seq.empty, Seq.empty)
)
}

def open_sse(url: String, takeWithin: FiniteDuration) = OpenSSE(url, takeWithin, Seq.empty, Seq.empty)
def open_ws(url: String, takeWithin: FiniteDuration) = OpenWS(url, takeWithin, Seq.empty, Seq.empty)
private case class GqlPayload(query: String, operationName: Option[String], variables: Option[Map[String, Json]])

def query_gql(url: String) = QueryGQL(url, Seq.empty, Seq.empty, Document(List.empty))
def query_gql(url: String) = QueryGQL(url, Document(List.empty))

val root = JsonPath.root

Expand Down
Loading

0 comments on commit f9265b7

Please sign in to comment.