-
Notifications
You must be signed in to change notification settings - Fork 20
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #304 from AlvaroCaste/resttemplate
add an interpreter for spring rest template
- Loading branch information
Showing
4 changed files
with
209 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
77 changes: 77 additions & 0 deletions
77
hammock-resttemplate/src/main/scala/hammock/resttemplate/RestTemplateInterpreter.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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] | ||
} | ||
} |
109 changes: 109 additions & 0 deletions
109
hammock-resttemplate/src/test/scala/hammock/resttemplate/RestTemplateInterpreterTest.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) => "" | ||
} |