Skip to content

Commit

Permalink
various cleanup
Browse files Browse the repository at this point in the history
  • Loading branch information
Arnaud Gourlay committed Jun 12, 2016
1 parent e3ba6b6 commit 7e76c63
Show file tree
Hide file tree
Showing 6 changed files with 65 additions and 63 deletions.
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,9 +117,13 @@ class ReadmeExample extends CornichonFeature {

For more examples see the following files which are part of the test pipeline:

- [Embedded Superheroes API](https://github.com/agourlay/cornichon/blob/master/src/test/scala/com/github/agourlay/cornichon/examples/superHeroes/SuperHeroesScenario.scala).

- [OpenMovieDatabase API](https://github.com/agourlay/cornichon/blob/master/src/it/scala/com.github.agourlay.cornichon.examples/OpenMovieDatabase.scala).

- [Embedded Superheroes API](https://github.com/agourlay/cornichon/blob/master/src/test/scala/com/github/agourlay/cornichon/examples/superHeroes/SuperHeroesScenario.scala).
- [DeckOfCard API](https://github.com/agourlay/cornichon/blob/master/src/it/scala/com.github.agourlay.cornichon.examples/DeckOfCard.scala).

- [Star Wars API](https://github.com/agourlay/cornichon/blob/master/src/it/scala/com.github.agourlay.cornichon.examples/StarWars.scala).

- [Math Operations](https://github.com/agourlay/cornichon/blob/master/src/test/scala/com/github/agourlay/cornichon/examples/math/MathScenario.scala).

Expand Down
2 changes: 2 additions & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ libraryDependencies ++= {
val sangriaV = "0.7.0"
val fansiV = "0.1.3"
val akkaHttpCirce = "1.7.0"
val catsScalaTest = "1.3.0"
Seq(
"com.typesafe.akka" %% "akka-http-core" % akkaHttpV
,"de.heikoseeberger" %% "akka-sse" % akkaSseV
Expand All @@ -61,6 +62,7 @@ libraryDependencies ++= {
//,"io.circe" %% "circe-optics" % circeVersion Remove if cursors are used instead or lenses for JsonPath.
,"de.heikoseeberger" %% "akka-http-circe" % akkaHttpCirce % "test"
,"com.typesafe.akka" %% "akka-http-experimental" % akkaHttpV % "test"
,"com.ironcorelabs" %% "cats-scalatest" % catsScalaTest % "test"
)
}

Expand Down
10 changes: 5 additions & 5 deletions src/main/scala/com/github/agourlay/cornichon/core/Resolver.scala
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@ class Resolver(extractors: Map[String, Mapper]) {

val r = new scala.util.Random()

def findPlaceholders(input: String): List[Placeholder] =
def findPlaceholders(input: String): Xor[CornichonError, List[Placeholder]] =
new PlaceholderParser(input).placeholdersRule.run() match {
case Failure(e: ParseError) List.empty
case Failure(e: Throwable) throw new ResolverParsingError(e)
case Success(dt) dt.toList
case Failure(e: ParseError) right(List.empty)
case Failure(e: Throwable) left(new ResolverParsingError(e))
case Success(dt) right(dt.toList)
}

def resolvePlaceholder(ph: Placeholder)(session: Session): Xor[CornichonError, String] =
Expand Down Expand Up @@ -66,7 +66,7 @@ class Resolver(extractors: Map[String, Mapper]) {
} yield res
}

loop(findPlaceholders(input), input)
findPlaceholders(input).flatMap(loop(_, input))
}

def fillPlaceholdersUnsafe(input: String)(session: Session): String =
Expand Down
47 changes: 21 additions & 26 deletions src/main/scala/com/github/agourlay/cornichon/http/HttpService.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@ import cats.data.Xor
import cats.data.Xor.{ left, right }
import com.github.agourlay.cornichon.core._
import com.github.agourlay.cornichon.http.client.HttpClient
import com.github.agourlay.cornichon.json.{ CornichonJson, JsonPath }
import com.github.agourlay.cornichon.json.JsonPath
import com.github.agourlay.cornichon.json.CornichonJson._
import io.circe.Json

import scala.annotation.tailrec
import scala.concurrent.duration._

class HttpService(baseUrl: String, requestTimeout: FiniteDuration, client: HttpClient, resolver: Resolver) extends CornichonJson {
class HttpService(baseUrl: String, requestTimeout: FiniteDuration, client: HttpClient, resolver: Resolver) {

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

Expand All @@ -27,27 +29,21 @@ class HttpService(baseUrl: String, requestTimeout: FiniteDuration, client: HttpC
r resolveCommonRequestParts(url, params, headers)(s)
resp call(json, r._1, r._2, r._3, requestTimeout)
newSession handleResponse(resp, expectedStatus, extractor)(s)
} yield {
(resp, newSession)
}
} yield (resp, newSession)

private def withoutPayload(call: WithoutPayloadCall, url: String, params: Seq[(String, String)],
headers: Seq[(String, String)], extractor: ResponseExtractor, requestTimeout: FiniteDuration, expectedStatus: Option[Int])(s: Session) =
for {
r resolveCommonRequestParts(url, params, headers)(s)
resp call(r._1, r._2, r._3, requestTimeout)
newSession handleResponse(resp, expectedStatus, extractor)(s)
} yield {
(resp, newSession)
}
} yield (resp, newSession)

private def handleResponse(resp: CornichonHttpResponse, expectedStatus: Option[Int], extractor: ResponseExtractor)(session: Session) =
for {
resExpected expectStatusCode(resp, expectedStatus)
newSession = fillInSessionWithResponse(session, resp, extractor)
} yield {
newSession
}
newSession fillInSessionWithResponse(session, resp, extractor)
} yield newSession

def resolveCommonRequestParts(url: String, params: Seq[(String, String)], headers: Seq[(String, String)])(s: Session) =
for {
Expand All @@ -56,17 +52,15 @@ class HttpService(baseUrl: String, requestTimeout: FiniteDuration, client: HttpC
headersResolved resolver.tuplesResolver(headers, s)
parsedHeaders parseHttpHeaders(headersResolved)
extractedHeaders extractWithHeadersSession(s)
} yield {
(urlResolved, paramsResolved, parsedHeaders ++ extractedHeaders)
}
} yield (urlResolved, paramsResolved, parsedHeaders ++ extractedHeaders)

private def expectStatusCode(httpResponse: CornichonHttpResponse, expected: Option[Int]): Xor[CornichonError, CornichonHttpResponse] =
expected.fold[Xor[CornichonError, CornichonHttpResponse]](right(httpResponse)) { e
if (httpResponse.status == StatusCode.int2StatusCode(e))
expected.map { expectedStatus
if (httpResponse.status == StatusCode.int2StatusCode(expectedStatus))
right(httpResponse)
else
left(StatusNonExpected(e, httpResponse))
}
left(StatusNonExpected(expectedStatus, httpResponse))
}.getOrElse(right(httpResponse))

def resolveParams(url: String, params: Seq[(String, String)])(session: Session): Xor[CornichonError, Seq[(String, String)]] = {
val urlsParamsPart = url.dropWhile(_ != '?').drop(1)
Expand Down Expand Up @@ -117,22 +111,23 @@ class HttpService(baseUrl: String, requestTimeout: FiniteDuration, client: HttpC
LastResponseHeadersKey encodeSessionHeaders(response)
))

def fillInSessionWithResponse(session: Session, response: CornichonHttpResponse, extractor: ResponseExtractor): Session = {
def fillInSessionWithResponse(session: Session, response: CornichonHttpResponse, extractor: ResponseExtractor): Xor[CornichonError, Session] =
extractor match {
case NoOpExtraction
commonSessionExtraction(session, response)
right(commonSessionExtraction(session, response))

case RootExtractor(targetKey)
commonSessionExtraction(session, response).addValue(targetKey, response.body)
right(commonSessionExtraction(session, response).addValue(targetKey, response.body))

case PathExtractor(path, targetKey)
val extractedJson = JsonPath.run(path, response.body).fold(e throw e, identity)
commonSessionExtraction(session, response).addValue(targetKey, CornichonJson.jsonStringValue(extractedJson))
JsonPath.run(path, response.body)
.map { extractedJson
commonSessionExtraction(session, response).addValue(targetKey, jsonStringValue(extractedJson))
}
}
}

def parseHttpHeaders(headers: Seq[(String, String)]): Xor[MalformedHeadersError, Seq[HttpHeader]] = {
@scala.annotation.tailrec
@tailrec
def loop(headers: Seq[(String, String)], acc: Seq[HttpHeader]): Xor[MalformedHeadersError, Seq[HttpHeader]] = {
if (headers.isEmpty) right(acc)
else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,58 +2,58 @@ package com.github.agourlay.cornichon.core

import java.util.UUID

import cats.data.Xor._
import cats.scalatest.XorValues
import org.scalatest.prop.PropertyChecks
import org.scalatest.{ OptionValues, Matchers, WordSpec }
import org.scalatest.{ Matchers, OptionValues, WordSpec }
import org.scalacheck.Gen
import com.github.agourlay.cornichon.core.SessionSpec._

class ResolverSpec extends WordSpec with Matchers with OptionValues with PropertyChecks {
class ResolverSpec extends WordSpec with Matchers with OptionValues with PropertyChecks with XorValues {

val resolver = Resolver.withoutExtractor()

"Resolver" when {
"findPlaceholders" must {
"find placeholder in content solely containing a placeholder without index" in {
forAll(keyGen) { key
resolver.findPlaceholders(s"<$key>") should be(List(Placeholder(key, None)))
resolver.findPlaceholders(s"<$key>").value should be(List(Placeholder(key, None)))
}
}

"find placeholder in content solely containing a placeholder with index" in {
forAll(keyGen, indiceGen) { (key, indice)
resolver.findPlaceholders(s"<$key[$indice]>") should be(List(Placeholder(key, Some(indice))))
resolver.findPlaceholders(s"<$key[$indice]>").value should be(List(Placeholder(key, Some(indice))))
}
}

"find placeholder in content starting with whitespace and containing a placeholder" in {
forAll(keyGen) { key
resolver.findPlaceholders(s" <$key>") should be(List(Placeholder(key, None)))
resolver.findPlaceholders(s" <$key>").value should be(List(Placeholder(key, None)))
}
}

"find placeholder in content starting with 2 whitespaces and containing a placeholder" in {
forAll(keyGen) { key
resolver.findPlaceholders(s" <$key>") should be(List(Placeholder(key, None)))
resolver.findPlaceholders(s" <$key>").value should be(List(Placeholder(key, None)))
}
}

"find placeholder in content finishing with whitespace and containing a placeholder" in {
forAll(keyGen) { key
resolver.findPlaceholders(s"<$key> ") should be(List(Placeholder(key, None)))
resolver.findPlaceholders(s"<$key> ").value should be(List(Placeholder(key, None)))
}
}

"find placeholder in content finishing with 2 whitespaces and containing a placeholder" in {
forAll(keyGen) { key
resolver.findPlaceholders(s"<$key> ") should be(List(Placeholder(key, None)))
resolver.findPlaceholders(s"<$key> ").value should be(List(Placeholder(key, None)))
}
}

"find placeholder in random content containing a placeholder with index" in {
forAll(keyGen, indiceGen) { (key, indice)
val content1 = Gen.alphaStr
resolver.findPlaceholders(s"$content1<$key[$indice]>$content1") should be(List(Placeholder(key, Some(indice))))
resolver.findPlaceholders(s"$content1<$key[$indice]>$content1").value should be(List(Placeholder(key, Some(indice))))
}
}
}
Expand All @@ -63,46 +63,46 @@ class ResolverSpec extends WordSpec with Matchers with OptionValues with Propert
forAll(keyGen, valueGen) { (ph, value)
val session = Session.newSession.addValue(ph, value)
val content = s"This project is <$ph>"
resolver.fillPlaceholders(content)(session) should be(right(s"This project is $value"))
resolver.fillPlaceholders(content)(session).value should be(s"This project is $value")
}
}

"not be confused by markup order" in {
forAll(keyGen, valueGen) { (ph, value)
val session = Session.newSession.addValue(ph, value)
val content = s"This project is >$ph<"
resolver.fillPlaceholders(content)(session) should be(right(s"This project is >$ph<"))
resolver.fillPlaceholders(content)(session).value should be(s"This project is >$ph<")
}
}

"not be confused if key contains empty string" in {
val session = Session.newSession.addValue("project-name", "cornichon")
val content = "This project is named <project name>"
resolver.fillPlaceholders(content)(session) should be(right("This project is named <project name>"))
resolver.fillPlaceholders(content)(session).value should be("This project is named <project name>")
}

"not be confused by unclosed markup used in a math context" in {
val session = Session.newSession.addValue("pi", "3.14")
val content = "3.15 > <pi>"
resolver.fillPlaceholders(content)(session) should be(right("3.15 > 3.14"))
resolver.fillPlaceholders(content)(session).value should be("3.15 > 3.14")
}

"return ResolverError if placeholder not found" in {
val session = Session.newSession.addValue("project-name", "cornichon")
val content = "This project is named <project-new-name>"
resolver.fillPlaceholders(content)(session) should be(left(KeyNotFoundInSession("project-new-name", session)))
resolver.fillPlaceholders(content)(session).leftValue should be(KeyNotFoundInSession("project-new-name", session))
}

"resolve two placeholders" in {
val session = Session.newSession.addValues(Seq("project-name" "cornichon", "taste" "tasty"))
val content = "This project is named <project-name> and is super <taste>"
resolver.fillPlaceholders(content)(session) should be(right("This project is named cornichon and is super tasty"))
resolver.fillPlaceholders(content)(session).value should be("This project is named cornichon and is super tasty")
}

"return ResolverError for the first placeholder not found" in {
val session = Session.newSession.addValues(Seq("project-name" "cornichon", "taste" "tasty"))
val content = "This project is named <project-name> and is super <new-taste>"
resolver.fillPlaceholders(content)(session) should be(left(KeyNotFoundInSession("new-taste", session)))
resolver.fillPlaceholders(content)(session).leftValue should be(KeyNotFoundInSession("new-taste", session))
}

"generate random uuid if <random-uuid>" in {
Expand All @@ -116,37 +116,37 @@ class ResolverSpec extends WordSpec with Matchers with OptionValues with Propert
forAll(keyGen, valueGen, valueGen) { (key, firstValue, secondValue)
val s = Session.newSession.addValue(key, firstValue).addValue(key, secondValue)
val content = s"<$key[0]>"
resolver.fillPlaceholders(content)(s) should be(right(firstValue))
resolver.fillPlaceholders(content)(s).value should be(firstValue)
}
}

"take the second value in session if indice = 1" in {
forAll(keyGen, valueGen, valueGen) { (key, firstValue, secondValue)
val s = Session.newSession.addValue(key, firstValue).addValue(key, secondValue)
val content = s"<$key[1]>"
resolver.fillPlaceholders(content)(s) should be(right(secondValue))
resolver.fillPlaceholders(content)(s).value should be(secondValue)
}
}

"get value from GenMapper" in {
val mapper = Map("letter-from-gen" GenMapper(Gen.oneOf(List("a"))))
val res = new Resolver(mapper)
val content = s"<letter-from-gen>"
res.fillPlaceholders(content)(Session.newSession) should be(right("a"))
res.fillPlaceholders(content)(Session.newSession).value should be("a")
}

"fail if GenMapper does not return a value" in {
val mapper = Map("letter-from-gen" GenMapper(Gen.oneOf(List())))
val res = new Resolver(mapper)
val content = s"<letter-from-gen>"
res.fillPlaceholders(content)(Session.newSession) should be(left(GeneratorError("<letter-from-gen>")))
res.fillPlaceholders(content)(Session.newSession).leftValue should be(GeneratorError("<letter-from-gen>"))
}

"fail with clear error message if key is defined in both Session and Extractors" in {
val extractor = JsonMapper("customer", "id")
val resolverWithExt = new Resolver(Map("customer-id" extractor))
val s = Session.newSession.addValue("customer-id", "12345")
resolverWithExt.fillPlaceholders("<customer-id>")(s) should be(left(AmbiguousKeyDefinition("customer-id")))
resolverWithExt.fillPlaceholders("<customer-id>")(s).leftValue should be(AmbiguousKeyDefinition("customer-id"))
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@ import akka.actor.ActorSystem
import akka.http.scaladsl.model.StatusCodes
import akka.stream.ActorMaterializer
import cats.data.Xor.right
import cats.scalatest.XorValues
import com.github.agourlay.cornichon.core.{ Resolver, Session }
import com.github.agourlay.cornichon.http.client.AkkaHttpClient
import org.scalatest.{ BeforeAndAfterAll, Matchers, WordSpec }

import scala.concurrent.duration._
import scala.concurrent.ExecutionContext.Implicits.global

class HttpServiceSpec extends WordSpec with Matchers with BeforeAndAfterAll {
class HttpServiceSpec extends WordSpec with Matchers with BeforeAndAfterAll with XorValues {

implicit val system = ActorSystem("akka-http-client")
implicit val mat = ActorMaterializer()
Expand All @@ -29,17 +30,17 @@ class HttpServiceSpec extends WordSpec with Matchers with BeforeAndAfterAll {
val s = Session.newSession
val resp = CornichonHttpResponse(StatusCodes.OK, Nil, "hello world")
val filledSession = service.fillInSessionWithResponse(s, resp, NoOpExtraction)
filledSession.get("last-response-status") should be("200")
filledSession.get("last-response-body") should be("hello world")
filledSession.value.get("last-response-status") should be("200")
filledSession.value.get("last-response-body") should be("hello world")
}

"extract content with RootResponseExtraction" in {
val s = Session.newSession
val resp = CornichonHttpResponse(StatusCodes.OK, Nil, "hello world")
val filledSession = service.fillInSessionWithResponse(s, resp, RootExtractor("copy-body"))
filledSession.get("last-response-status") should be("200")
filledSession.get("last-response-body") should be("hello world")
filledSession.get("copy-body") should be("hello world")
filledSession.value.get("last-response-status") should be("200")
filledSession.value.get("last-response-body") should be("hello world")
filledSession.value.get("copy-body") should be("hello world")
}

"extract content with PathResponseExtraction" in {
Expand All @@ -51,15 +52,15 @@ class HttpServiceSpec extends WordSpec with Matchers with BeforeAndAfterAll {
}
""")
val filledSession = service.fillInSessionWithResponse(s, resp, PathExtractor("name", "part-of-body"))
filledSession.get("last-response-status") should be("200")
filledSession.get("last-response-body") should be(
filledSession.value.get("last-response-status") should be("200")
filledSession.value.get("last-response-body") should be(
"""
{
"name" : "batman"
}
"""
)
filledSession.get("part-of-body") should be("batman")
filledSession.value.get("part-of-body") should be("batman")
}
}

Expand Down

0 comments on commit 7e76c63

Please sign in to comment.