Skip to content

Commit aa069d2

Browse files
committed
Update scala & libs, fix test & compilation
1 parent e092507 commit aa069d2

File tree

18 files changed

+252
-224
lines changed

18 files changed

+252
-224
lines changed

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,4 +100,6 @@ gradle-app.setting
100100
!gradle-wrapper.jar
101101

102102
**/generated-sources/**
103-
**/test-results/**
103+
**/test-results/**
104+
105+
.bsp

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ It provides an implementation-agnostic module for mapping to your favorite HTTP
1010

1111
[Standard GPB <-> JSON mapping](https://developers.google.com/protocol-buffers/docs/proto3#json) is used.
1212

13-
The API is _finally tagless_ (read more e.g. [here](https://www.beyondthelines.net/programming/introduction-to-tagless-final/)) meaning it can use whatever [`F[_]: cats.effect.Effect`](https://typelevel.org/cats-effect/typeclasses/effect.html) (e.g. `cats.effect.IO`, `monix.eval.Task`).
13+
The API is _tagless final_ (read more e.g. [here](https://www.beyondthelines.net/programming/introduction-to-tagless-final/)) meaning it can use whatever [`F[_]: cats.effect.Effect`](https://typelevel.org/cats-effect/typeclasses/effect.html) (e.g. `cats.effect.IO`, `monix.eval.Task`).
1414

1515
## Usage
1616

akka-http/src/main/scala/com/avast/grpc/jsonbridge/akkahttp/AkkaHttp.scala

Lines changed: 53 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
package com.avast.grpc.jsonbridge.akkahttp
22

33
import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport
4-
import akka.http.scaladsl.model.StatusCodes.ClientError
4+
import akka.http.scaladsl.marshalling.{Marshaller, ToEntityMarshaller, ToResponseMarshallable}
55
import akka.http.scaladsl.model._
66
import akka.http.scaladsl.model.headers.`Content-Type`
77
import akka.http.scaladsl.server.Directives._
88
import akka.http.scaladsl.server.{PathMatcher, Route}
99
import cats.data.NonEmptyList
10-
import cats.effect.Effect
11-
import cats.effect.implicits._
10+
import cats.effect.Sync
11+
import cats.implicits._
1212
import com.avast.grpc.jsonbridge.GrpcJsonBridge.GrpcMethodName
1313
import com.avast.grpc.jsonbridge.{BridgeError, BridgeErrorResponse, GrpcJsonBridge}
1414
import com.typesafe.scalalogging.LazyLogging
@@ -22,11 +22,10 @@ object AkkaHttp extends SprayJsonSupport with DefaultJsonProtocol with LazyLoggi
2222

2323
private implicit val grpcStatusJsonFormat: RootJsonFormat[BridgeErrorResponse] = jsonFormat3(BridgeErrorResponse.apply)
2424

25-
private[akkahttp] final val JsonContentType: `Content-Type` = `Content-Type` {
26-
ContentType.WithMissingCharset(MediaType.applicationWithOpenCharset("json"))
27-
}
25+
private val jsonStringMarshaller: ToEntityMarshaller[String] =
26+
Marshaller.stringMarshaller(MediaTypes.`application/json`)
2827

29-
def apply[F[_]: Effect](configuration: Configuration)(bridge: GrpcJsonBridge[F]): Route = {
28+
def apply[F[_]: Sync: LiftToFuture](configuration: Configuration)(bridge: GrpcJsonBridge[F]): Route = {
3029

3130
val pathPattern = configuration.pathPrefix
3231
.map { case NonEmptyList(head, tail) =>
@@ -44,71 +43,61 @@ object AkkaHttp extends SprayJsonSupport with DefaultJsonProtocol with LazyLoggi
4443
post {
4544
path(pathPattern) { (serviceName, methodName) =>
4645
extractRequest { request =>
47-
val headers = request.headers
4846
request.header[`Content-Type`] match {
49-
case Some(`JsonContentType`) =>
47+
case Some(ct) if ct.contentType.mediaType == MediaTypes.`application/json` =>
5048
entity(as[String]) { body =>
5149
val methodNameString = GrpcMethodName(serviceName, methodName)
52-
val headersString = mapHeaders(headers)
53-
val methodCall = bridge.invoke(methodNameString, body, headersString).toIO.unsafeToFuture()
50+
val headersString = mapHeaders(request.headers)
51+
val methodCall = LiftToFuture[F].liftF {
52+
bridge
53+
.invoke(methodNameString, body, headersString)
54+
.flatMap(Sync[F].fromEither)
55+
}
56+
5457
onComplete(methodCall) {
55-
case Success(result) =>
56-
result match {
57-
case Right(resp) =>
58-
logger.trace("Request successful: {}", resp.substring(0, 100))
59-
respondWithHeader(JsonContentType) {
60-
complete(resp)
61-
}
62-
case Left(er) =>
63-
er match {
64-
case BridgeError.GrpcMethodNotFound =>
65-
val message = s"Method '${methodNameString.fullName}' not found"
66-
logger.debug(message)
67-
respondWithHeader(JsonContentType) {
68-
complete(StatusCodes.NotFound, BridgeErrorResponse.fromMessage(message))
69-
}
70-
case er: BridgeError.Json =>
71-
val message = "Wrong JSON"
72-
logger.debug(message, er.t)
73-
respondWithHeader(JsonContentType) {
74-
complete(StatusCodes.BadRequest, BridgeErrorResponse.fromException(message, er.t))
75-
}
76-
case er: BridgeError.Grpc =>
77-
val message = "gRPC error" + Option(er.s.getDescription).map(": " + _).getOrElse("")
78-
logger.trace(message, er.s.getCause)
79-
val (s, body) = mapStatus(er.s)
80-
respondWithHeader(JsonContentType) {
81-
complete(s, body)
82-
}
83-
case er: BridgeError.Unknown =>
84-
val message = "Unknown error"
85-
logger.warn(message, er.t)
86-
respondWithHeader(JsonContentType) {
87-
complete(StatusCodes.InternalServerError, BridgeErrorResponse.fromException(message, er.t))
88-
}
89-
}
90-
}
91-
case Failure(NonFatal(er)) =>
58+
case Success(resp) =>
59+
logger.trace("Request successful: {}", resp.substring(0, 100))
60+
61+
complete(ToResponseMarshallable(resp)(jsonStringMarshaller))
62+
case Failure(BridgeError.GrpcMethodNotFound) =>
63+
val message = s"Method '${methodNameString.fullName}' not found"
64+
logger.debug(message)
65+
66+
complete(StatusCodes.NotFound, BridgeErrorResponse.fromMessage(message))
67+
case Failure(er: BridgeError.Json) =>
68+
val message = "Wrong JSON"
69+
logger.debug(message, er.t)
70+
71+
complete(StatusCodes.BadRequest, BridgeErrorResponse.fromException(message, er.t))
72+
case Failure(er: BridgeError.Grpc) =>
73+
val message = "gRPC error" + Option(er.s.getDescription).map(": " + _).getOrElse("")
74+
logger.trace(message, er.s.getCause)
75+
val (s, body) = mapStatus(er.s)
76+
77+
complete(s, body)
78+
case Failure(er: BridgeError.Unknown) =>
79+
val message = "Unknown error"
80+
logger.warn(message, er.t)
81+
82+
complete(StatusCodes.InternalServerError, BridgeErrorResponse.fromException(message, er.t))
83+
case Failure(NonFatal(ex)) =>
9284
val message = "Unknown exception"
93-
logger.debug(message, er)
94-
respondWithHeader(JsonContentType) {
95-
complete(StatusCodes.InternalServerError, BridgeErrorResponse.fromException(message, er))
96-
}
85+
logger.debug(message, ex)
86+
87+
complete(StatusCodes.InternalServerError, BridgeErrorResponse.fromException(message, ex))
9788
case Failure(e) => throw e // scalafix:ok
9889
}
9990
}
10091
case Some(c) =>
101-
val message = s"Content-Type must be '$JsonContentType', it is '$c'"
92+
val message = s"Content-Type must be 'application/json', it is '$c'"
10293
logger.debug(message)
103-
respondWithHeader(JsonContentType) {
104-
complete(StatusCodes.BadRequest, BridgeErrorResponse.fromMessage(message))
105-
}
94+
95+
complete(StatusCodes.BadRequest, BridgeErrorResponse.fromMessage(message))
10696
case None =>
107-
val message = s"Content-Type must be '$JsonContentType'"
97+
val message = "Content-Type must be 'application/json'"
10898
logger.debug(message)
109-
respondWithHeader(JsonContentType) {
110-
complete(StatusCodes.BadRequest, BridgeErrorResponse.fromMessage(message))
111-
}
99+
100+
complete(StatusCodes.BadRequest, BridgeErrorResponse.fromMessage(message))
112101
}
113102
}
114103
}
@@ -118,9 +107,8 @@ object AkkaHttp extends SprayJsonSupport with DefaultJsonProtocol with LazyLoggi
118107
case None =>
119108
val message = s"Service '$serviceName' not found"
120109
logger.debug(message)
121-
respondWithHeader(JsonContentType) {
122-
complete(StatusCodes.NotFound, BridgeErrorResponse.fromMessage(message))
123-
}
110+
111+
complete(StatusCodes.NotFound, BridgeErrorResponse.fromMessage(message))
124112
case Some(methods) =>
125113
complete(methods.map(_.fullName).toList.mkString("\n"))
126114
}
@@ -142,7 +130,7 @@ object AkkaHttp extends SprayJsonSupport with DefaultJsonProtocol with LazyLoggi
142130
s.getCode match {
143131
case Code.OK => (StatusCodes.OK, description)
144132
case Code.CANCELLED =>
145-
(ClientError(499)("Client Closed Request", "The operation was cancelled, typically by the caller."), description)
133+
(StatusCodes.custom(499, "Client Closed Request", "The operation was cancelled, typically by the caller."), description)
146134
case Code.UNKNOWN => (StatusCodes.InternalServerError, description)
147135
case Code.INVALID_ARGUMENT => (StatusCodes.BadRequest, description)
148136
case Code.DEADLINE_EXCEEDED => (StatusCodes.GatewayTimeout, description)
@@ -162,7 +150,7 @@ object AkkaHttp extends SprayJsonSupport with DefaultJsonProtocol with LazyLoggi
162150
}
163151
}
164152

165-
final case class Configuration private (pathPrefix: Option[NonEmptyList[String]])
153+
final case class Configuration(pathPrefix: Option[NonEmptyList[String]])
166154

167155
object Configuration {
168156
val Default: Configuration = Configuration(
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package com.avast.grpc.jsonbridge.akkahttp
2+
3+
import cats.effect.IO
4+
import cats.effect.unsafe.IORuntime
5+
6+
import scala.concurrent.Future
7+
8+
trait LiftToFuture[F[_]] {
9+
def liftF[A](f: F[A]): Future[A]
10+
}
11+
12+
object LiftToFuture {
13+
def apply[F[_]](implicit f: LiftToFuture[F]): LiftToFuture[F] = f
14+
15+
implicit def liftToFutureForIO(implicit runtime: IORuntime): LiftToFuture[IO] = new LiftToFuture[IO] {
16+
override def liftF[A](f: IO[A]): Future[A] = f.unsafeToFuture()
17+
}
18+
}

akka-http/src/test/scala/com/avast/grpc/jsonbridge/akkahttp/AkkaHttpTest.scala

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,21 @@
11
package com.avast.grpc.jsonbridge.akkahttp
22

3-
import akka.http.scaladsl.model.HttpHeader.ParsingResult.Ok
43
import akka.http.scaladsl.model._
5-
import akka.http.scaladsl.model.headers.`Content-Type`
4+
import akka.http.scaladsl.model.headers.{RawHeader, `Content-Type`}
65
import akka.http.scaladsl.testkit.ScalatestRouteTest
76
import cats.data.NonEmptyList
87
import cats.effect.IO
8+
import cats.effect.unsafe.implicits.global
9+
import cats.implicits._
910
import com.avast.grpc.jsonbridge._
1011
import io.grpc.ServerServiceDefinition
1112
import org.scalatest.funsuite.AnyFunSuite
1213

13-
import scala.concurrent.ExecutionContext
14+
import scala.concurrent.ExecutionContext.Implicits.{global => ec}
1415
import scala.util.Random
1516

1617
class AkkaHttpTest extends AnyFunSuite with ScalatestRouteTest {
1718

18-
val ec: ExecutionContext = implicitly[ExecutionContext]
1919
def bridge(ssd: ServerServiceDefinition): GrpcJsonBridge[IO] =
2020
ReflectionGrpcJsonBridge
2121
.createFromServices[IO](ec)(ssd)
@@ -25,19 +25,22 @@ class AkkaHttpTest extends AnyFunSuite with ScalatestRouteTest {
2525

2626
test("basic") {
2727
val route = AkkaHttp[IO](Configuration.Default)(bridge(TestServiceImpl.bindService()))
28-
Post("/com.avast.grpc.jsonbridge.test.TestService/Add", """ { "a": 1, "b": 2} """)
29-
.withHeaders(AkkaHttp.JsonContentType) ~> route ~> check {
28+
val entity = HttpEntity(ContentTypes.`application/json`, """ { "a": 1, "b": 2} """)
29+
Post("/com.avast.grpc.jsonbridge.test.TestService/Add", entity) ~> route ~> check {
3030
assertResult(StatusCodes.OK)(status)
3131
assertResult("""{"sum":3}""")(responseAs[String])
32-
assertResult(Seq(`Content-Type`(ContentType.WithMissingCharset(MediaType.applicationWithOpenCharset("json")))))(headers)
32+
assertResult(MediaTypes.`application/json`.some)(
33+
header[`Content-Type`].map(_.contentType.mediaType)
34+
)
3335
}
3436
}
3537

3638
test("with path prefix") {
3739
val configuration = Configuration.Default.copy(pathPrefix = Some(NonEmptyList.of("abc", "def")))
3840
val route = AkkaHttp[IO](configuration)(bridge(TestServiceImpl.bindService()))
39-
Post("/abc/def/com.avast.grpc.jsonbridge.test.TestService/Add", """ { "a": 1, "b": 2} """)
40-
.withHeaders(AkkaHttp.JsonContentType) ~> route ~> check {
41+
42+
val entity = HttpEntity(ContentTypes.`application/json`, """ { "a": 1, "b": 2} """)
43+
Post("/abc/def/com.avast.grpc.jsonbridge.test.TestService/Add", entity) ~> route ~> check {
4144
assertResult(StatusCodes.OK)(status)
4245
assertResult("""{"sum":3}""")(responseAs[String])
4346
}
@@ -46,20 +49,21 @@ class AkkaHttpTest extends AnyFunSuite with ScalatestRouteTest {
4649
test("bad request after wrong request") {
4750
val route = AkkaHttp[IO](Configuration.Default)(bridge(TestServiceImpl.bindService()))
4851
// empty body
49-
Post("/com.avast.grpc.jsonbridge.test.TestService/Add", "")
50-
.withHeaders(AkkaHttp.JsonContentType) ~> route ~> check {
52+
val entity = HttpEntity(ContentTypes.`application/json`, "")
53+
Post("/com.avast.grpc.jsonbridge.test.TestService/Add", entity) ~> route ~> check {
5154
assertResult(StatusCodes.BadRequest)(status)
5255
}
5356
// no Content-Type header
54-
Post("/com.avast.grpc.jsonbridge.test.TestService/Add", """ { "a": 1, "b": 2} """) ~> route ~> check {
57+
val entity2 = HttpEntity(""" { "a": 1, "b": 2} """)
58+
Post("/com.avast.grpc.jsonbridge.test.TestService/Add", entity2) ~> route ~> check {
5559
assertResult(StatusCodes.BadRequest)(status)
5660
}
5761
}
5862

5963
test("propagates user-specified status") {
6064
val route = AkkaHttp(Configuration.Default)(bridge(PermissionDeniedTestServiceImpl.bindService()))
61-
Post(s"/com.avast.grpc.jsonbridge.test.TestService/Add", """ { "a": 1, "b": 2} """)
62-
.withHeaders(AkkaHttp.JsonContentType) ~> route ~> check {
65+
val entity = HttpEntity(ContentTypes.`application/json`, """ { "a": 1, "b": 2} """)
66+
Post(s"/com.avast.grpc.jsonbridge.test.TestService/Add", entity) ~> route ~> check {
6367
assertResult(status)(StatusCodes.Forbidden)
6468
}
6569
}
@@ -83,12 +87,15 @@ class AkkaHttpTest extends AnyFunSuite with ScalatestRouteTest {
8387
test("passes headers") {
8488
val headerValue = Random.alphanumeric.take(10).mkString("")
8589
val route = AkkaHttp[IO](Configuration.Default)(bridge(TestServiceImpl.withInterceptor))
86-
val Ok(customHeaderToBeSent, _) = HttpHeader.parse(TestServiceImpl.HeaderName, headerValue)
87-
Post("/com.avast.grpc.jsonbridge.test.TestService/Add", """ { "a": 1, "b": 2} """)
88-
.withHeaders(AkkaHttp.JsonContentType, customHeaderToBeSent) ~> route ~> check {
90+
val customHeaderToBeSent = RawHeader(TestServiceImpl.HeaderName, headerValue)
91+
val entity = HttpEntity(ContentTypes.`application/json`, """ { "a": 1, "b": 2} """)
92+
Post("/com.avast.grpc.jsonbridge.test.TestService/Add", entity)
93+
.withHeaders(customHeaderToBeSent) ~> route ~> check {
8994
assertResult(StatusCodes.OK)(status)
9095
assertResult("""{"sum":3}""")(responseAs[String])
91-
assertResult(Seq(`Content-Type`(ContentType.WithMissingCharset(MediaType.applicationWithOpenCharset("json")))))(headers)
96+
assertResult(MediaTypes.`application/json`.some)(
97+
header[`Content-Type`].map(_.contentType.mediaType)
98+
)
9299
}
93100
}
94101
}

0 commit comments

Comments
 (0)