Skip to content

Commit

Permalink
Merge pull request #304 from AlvaroCaste/resttemplate
Browse files Browse the repository at this point in the history
add an interpreter for spring rest template
  • Loading branch information
pepegar authored May 25, 2020
2 parents 0018615 + d6dd766 commit 36c1f9b
Show file tree
Hide file tree
Showing 4 changed files with 209 additions and 5 deletions.
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ jobs:
script: sbt scalafmt::test
- stage: test
script:
- sbt ++$TRAVIS_SCALA_VERSION clean coverage +coreJVM/test +akka/test +circeJVM/test +asynchttpclient/test coverageReport
- sbt ++$TRAVIS_SCALA_VERSION clean coverage +coreJVM/test +akka/test +circeJVM/test +asynchttpclient/test +resttemplate/test coverageReport
- sbt ++$TRAVIS_SCALA_VERSION validateJS
- sbt validateDoc validateScalafmt
- sbt example/compile exampleJS/compile
Expand Down
26 changes: 22 additions & 4 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ val Versions = Map(
"akka-http" -> "10.1.10",
"akka-stream" -> "2.5.30",
"ahc" -> "2.10.3",
"spring" -> "5.2.6.RELEASE",
"findbugs" -> "3.0.2",
"apacheHttp" -> "4.5.12",
"mockito" -> "1.10.19"
)
Expand Down Expand Up @@ -87,8 +89,8 @@ lazy val hammock = project
.in(file("."))
.settings(buildSettings)
.settings(noPublishSettings)
.dependsOn(coreJVM, coreJS, circeJVM, circeJS, apache, akka, asynchttpclient)
.aggregate(coreJVM, coreJS, circeJVM, circeJS, apache, akka, asynchttpclient)
.dependsOn(coreJVM, coreJS, circeJVM, circeJS, apache, akka, asynchttpclient, resttemplate)
.aggregate(coreJVM, coreJS, circeJVM, circeJS, apache, akka, asynchttpclient, resttemplate)

lazy val core = crossProject(JSPlatform, JVMPlatform)
.crossType(CrossType.Full)
Expand Down Expand Up @@ -172,11 +174,27 @@ lazy val asynchttpclient = project
)
.dependsOn(coreJVM)

lazy val resttemplate = project
.in(file("hammock-resttemplate"))
.settings(moduleName := "hammock-resttemplate")
.settings(buildSettings)
.settings(commonDependencies)
.settings(compilerPlugins)
.settings(
libraryDependencies ++= Seq(
"com.google.code.findbugs" % "jsr305" % Versions("findbugs") % Optional,
"org.springframework" % "spring-web" % Versions("spring"),
"org.scalatestplus" %%% "scalatestplus-mockito" % Versions("scalatestplusMockito") % Test,
"org.mockito" % "mockito-all" % Versions("mockito") % Test
)
)
.dependsOn(coreJVM)

lazy val javadocIoUrl = settingKey[String]("the url of hammock documentation in http://javadoc.io")

lazy val docs = project
.in(file("docs"))
.dependsOn(coreJVM, circeJVM, apache, akka, asynchttpclient)
.dependsOn(coreJVM, circeJVM, apache, akka, asynchttpclient, resttemplate)
.settings(moduleName := "hammock-docs")
.settings(buildSettings)
.settings(compilerPlugins)
Expand Down Expand Up @@ -227,7 +245,7 @@ lazy val example = project
.settings(buildSettings)
.settings(noPublishSettings)
.settings(compilerPlugins)
.dependsOn(coreJVM, circeJVM, apache, akka, asynchttpclient)
.dependsOn(coreJVM, circeJVM, apache, akka, asynchttpclient, resttemplate)

lazy val exampleJS = project
.in(file("example-js"))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package hammock
package resttemplate

import java.net.URI

import cats._
import cats.implicits._
import cats.data.Kleisli
import cats.effect._
import org.springframework.http._
import org.springframework.util.LinkedMultiValueMap
import org.springframework.web.client.RestTemplate

import scala.collection.JavaConverters._

object RestTemplateInterpreter {

def apply[F[_]](implicit F: InterpTrans[F]): InterpTrans[F] = F

implicit def instance[F[_]: Sync](
implicit client: RestTemplate = new RestTemplate()
): InterpTrans[F] = new InterpTrans[F] {
override def trans: HttpF ~> F = transK andThen λ[Kleisli[F, RestTemplate, *] ~> F](_.run(client))
}

def transK[F[_]: Sync]: HttpF ~> Kleisli[F, RestTemplate, *] = {
λ[HttpF ~> Kleisli[F, RestTemplate, *]] {
case reqF @ (Get(_) | Delete(_) | Head(_) | Options(_) | Trace(_) | Post(_) | Put(_) | Patch(_)) =>
Kleisli { implicit client =>
for {
req <- mapRequest[F](reqF)
res <- execute[F](req)
hammockResponse <- mapResponse[F](res)
} yield hammockResponse
}
}
}

def mapRequest[F[_]: Sync](reqF: HttpF[HttpResponse]): F[RequestEntity[String]] = {

def httpEntity: HttpEntity[String] = new HttpEntity(
reqF.req.entity.map(_.cata[String](_.body, _.body.map(_.toChar).mkString, Function.const(""))).orNull,
new LinkedMultiValueMap[String, String](reqF.req.headers.mapValues(List(_).asJava).asJava)
)

def requestEntity(httpMethod: HttpMethod): RequestEntity[String] =
new RequestEntity[String](httpEntity.getBody, httpEntity.getHeaders, httpMethod, new URI(reqF.req.uri.show))

(reqF match {
case Get(_) => requestEntity(HttpMethod.GET)
case Delete(_) => requestEntity(HttpMethod.DELETE)
case Head(_) => requestEntity(HttpMethod.HEAD)
case Options(_) => requestEntity(HttpMethod.OPTIONS)
case Post(_) => requestEntity(HttpMethod.POST)
case Put(_) => requestEntity(HttpMethod.PUT)
case Trace(_) => requestEntity(HttpMethod.TRACE)
case Patch(_) => requestEntity(HttpMethod.PATCH)
}).pure[F]
}

def execute[F[_]: Sync](rtRequest: RequestEntity[String])(implicit client: RestTemplate): F[ResponseEntity[String]] =
Sync[F].delay { client.exchange(rtRequest, classOf[String]) }

def mapResponse[F[_]: Applicative](response: ResponseEntity[String]): F[HttpResponse] = {

def createEntity(response: ResponseEntity[String]): Entity = response.getHeaders.getContentType match {
case MediaType.APPLICATION_OCTET_STREAM => Entity.ByteArrayEntity(response.getBody.getBytes)
case _ => Entity.StringEntity(response.getBody)
}

HttpResponse(
Status.Statuses(response.getStatusCodeValue),
response.getHeaders.toSingleValueMap.asScala.toMap,
createEntity(response)
).pure[F]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package hammock
package resttemplate

import cats.implicits._
import cats.effect._
import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AnyWordSpec
import org.scalatestplus.mockito._
import RestTemplateInterpreter._
import org.springframework.http.{HttpHeaders, HttpStatus, ResponseEntity}
import org.springframework.web.client.RestTemplate
import scala.collection.JavaConverters._

class RestTemplateInterpreterTest extends AnyWordSpec with Matchers with MockitoSugar {

implicit val client: RestTemplate = new RestTemplate()

"asynchttpclient" should {

"map requests correctly" in {
val hreq1 = Get(HttpRequest(uri"http://google.com", Map.empty[String, String], None))
val req1 = mapRequest[IO](hreq1).unsafeRunSync

val hreq2 = Post(HttpRequest(uri"http://google.com", Map("header1" -> "value1"), None))
val req2 = mapRequest[IO](hreq2).unsafeRunSync

val hreq3 = Put(
HttpRequest(
uri"http://google.com",
Map("header1" -> "value1", "header2" -> "value2"),
Some(Entity.StringEntity("the body"))
)
)
val req3 = mapRequest[IO](hreq3).unsafeRunSync

req1.getUrl.toString shouldEqual hreq1.req.uri.show
req1.getMethod.name shouldEqual "GET"
req1.getHeaders.toSingleValueMap.asScala shouldBe empty

req2.getUrl.toString shouldEqual hreq2.req.uri.show
req2.getMethod.name shouldEqual "POST"
req2.getHeaders.toSingleValueMap.asScala.size shouldEqual hreq2.req.headers.size
req2.getHeaders.toSingleValueMap.asScala
.find { case (key, _) => key == "header1" }
.map { case (_, value) => value } shouldEqual Some("value1")

req3.getUrl.toString shouldEqual hreq3.req.uri.show
req3.getMethod.name shouldEqual "PUT"
req3.getHeaders.toSingleValueMap.asScala.size shouldEqual hreq3.req.headers.size
req3.getHeaders.toSingleValueMap.asScala
.find { case (key, _) => key == "header1" }
.map { case (_, value) => value } shouldEqual Some("value1")
req3.getHeaders.toSingleValueMap.asScala
.find { case (key, _) => key == "header2" }
.map { case (_, value) => value } shouldEqual Some("value2")
req3.getBody shouldEqual "the body"
}

"map responses correctly" in {

def httpHeaders(values: List[(String, String)]) = {
val httpHeaders = new HttpHeaders()
values.foreach { case (key, value) => httpHeaders.add(key, value) }
httpHeaders
}

val rtResponse1 =
new ResponseEntity[String]("this is the body", httpHeaders(List("header" -> "value")), HttpStatus.OK)
val hammockResponse1 = HttpResponse(Status.OK, Map("header" -> "value"), Entity.StringEntity("this is the body"))

val rtResponse2 =
new ResponseEntity[String]("[1,2,3,4]", httpHeaders(List("Content-type" -> "application/json")), HttpStatus.OK)
val hammockResponse2 =
HttpResponse(Status.OK, Map("Content-type" -> "application/json"), Entity.StringEntity("[1,2,3,4]"))

val rtResponse3 = new ResponseEntity[String](
"[1,2,3,4]",
httpHeaders(List("Content-type" -> "application/octet-stream")),
HttpStatus.OK
)
val hammockResponse3 =
HttpResponse(
Status.OK,
Map("Content-type" -> "application/octet-stream"),
Entity.ByteArrayEntity("[1,2,3,4]".toCharArray.map(_.toByte))
)

val tests = List(
rtResponse1 -> hammockResponse1,
rtResponse2 -> hammockResponse2,
rtResponse3 -> hammockResponse3
)

tests foreach {
case (a, h) =>
(mapResponse[IO](a).unsafeRunSync, h) match {
case (HttpResponse(s1, h1, e1), HttpResponse(s2, h2, e2)) =>
s1 shouldEqual s2
h1 shouldEqual h2
e1.cata(showStr, showByt, showEmpty) shouldEqual e2.cata(showStr, showByt, showEmpty)
}
}
}
}

def showStr(s: Entity.StringEntity) = s.content
def showByt(b: Entity.ByteArrayEntity) = b.content.mkString("[", ",", "]")
val showEmpty = (_: Entity.EmptyEntity.type) => ""
}

0 comments on commit 36c1f9b

Please sign in to comment.