From aa069d27b1b2c98580c8c8a7c53f4b295a692575 Mon Sep 17 00:00:00 2001 From: Ihor Vovk Date: Sat, 16 Nov 2024 10:47:08 +0100 Subject: [PATCH] Update scala & libs, fix test & compilation --- .gitignore | 4 +- README.md | 2 +- .../grpc/jsonbridge/akkahttp/AkkaHttp.scala | 118 ++++++++---------- .../jsonbridge/akkahttp/LiftToFuture.scala | 18 +++ .../jsonbridge/akkahttp/AkkaHttpTest.scala | 43 ++++--- build.sbt | 70 ++++++----- .../scalapb/ScalaPBServiceHandlers.scala | 15 +-- .../ScalaPBReflectionGrpcJsonBridgeTest.scala | 33 ++--- ...ScalaPBReflectionGrpcJsonBridgeTest2.scala | 27 ++-- .../avast/grpc/jsonbridge/BridgeError.scala | 2 +- .../grpc/jsonbridge/JavaServiceHandlers.scala | 6 +- .../jsonbridge/ReflectionGrpcJsonBridge.scala | 3 +- .../ReflectionGrpcJsonBridgeTest.scala | 31 ++--- .../ReflectionGrpcJsonBridgeTest2.scala | 24 ++-- .../avast/grpc/jsonbridge/http4s/Http4s.scala | 20 ++- .../grpc/jsonbridge/http4s/Http4sTest.scala | 54 ++++---- project/build.properties | 2 +- project/plugins.sbt | 4 +- 18 files changed, 252 insertions(+), 224 deletions(-) create mode 100644 akka-http/src/main/scala/com/avast/grpc/jsonbridge/akkahttp/LiftToFuture.scala diff --git a/.gitignore b/.gitignore index 9fa7932e..693469b2 100644 --- a/.gitignore +++ b/.gitignore @@ -100,4 +100,6 @@ gradle-app.setting !gradle-wrapper.jar **/generated-sources/** -**/test-results/** \ No newline at end of file +**/test-results/** + +.bsp diff --git a/README.md b/README.md index 91cb1031..f04e203d 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ It provides an implementation-agnostic module for mapping to your favorite HTTP [Standard GPB <-> JSON mapping](https://developers.google.com/protocol-buffers/docs/proto3#json) is used. -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`). +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`). ## Usage diff --git a/akka-http/src/main/scala/com/avast/grpc/jsonbridge/akkahttp/AkkaHttp.scala b/akka-http/src/main/scala/com/avast/grpc/jsonbridge/akkahttp/AkkaHttp.scala index 11981706..bd4bfa8f 100644 --- a/akka-http/src/main/scala/com/avast/grpc/jsonbridge/akkahttp/AkkaHttp.scala +++ b/akka-http/src/main/scala/com/avast/grpc/jsonbridge/akkahttp/AkkaHttp.scala @@ -1,14 +1,14 @@ package com.avast.grpc.jsonbridge.akkahttp import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport -import akka.http.scaladsl.model.StatusCodes.ClientError +import akka.http.scaladsl.marshalling.{Marshaller, ToEntityMarshaller, ToResponseMarshallable} import akka.http.scaladsl.model._ import akka.http.scaladsl.model.headers.`Content-Type` import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.server.{PathMatcher, Route} import cats.data.NonEmptyList -import cats.effect.Effect -import cats.effect.implicits._ +import cats.effect.Sync +import cats.implicits._ import com.avast.grpc.jsonbridge.GrpcJsonBridge.GrpcMethodName import com.avast.grpc.jsonbridge.{BridgeError, BridgeErrorResponse, GrpcJsonBridge} import com.typesafe.scalalogging.LazyLogging @@ -22,11 +22,10 @@ object AkkaHttp extends SprayJsonSupport with DefaultJsonProtocol with LazyLoggi private implicit val grpcStatusJsonFormat: RootJsonFormat[BridgeErrorResponse] = jsonFormat3(BridgeErrorResponse.apply) - private[akkahttp] final val JsonContentType: `Content-Type` = `Content-Type` { - ContentType.WithMissingCharset(MediaType.applicationWithOpenCharset("json")) - } + private val jsonStringMarshaller: ToEntityMarshaller[String] = + Marshaller.stringMarshaller(MediaTypes.`application/json`) - def apply[F[_]: Effect](configuration: Configuration)(bridge: GrpcJsonBridge[F]): Route = { + def apply[F[_]: Sync: LiftToFuture](configuration: Configuration)(bridge: GrpcJsonBridge[F]): Route = { val pathPattern = configuration.pathPrefix .map { case NonEmptyList(head, tail) => @@ -44,71 +43,61 @@ object AkkaHttp extends SprayJsonSupport with DefaultJsonProtocol with LazyLoggi post { path(pathPattern) { (serviceName, methodName) => extractRequest { request => - val headers = request.headers request.header[`Content-Type`] match { - case Some(`JsonContentType`) => + case Some(ct) if ct.contentType.mediaType == MediaTypes.`application/json` => entity(as[String]) { body => val methodNameString = GrpcMethodName(serviceName, methodName) - val headersString = mapHeaders(headers) - val methodCall = bridge.invoke(methodNameString, body, headersString).toIO.unsafeToFuture() + val headersString = mapHeaders(request.headers) + val methodCall = LiftToFuture[F].liftF { + bridge + .invoke(methodNameString, body, headersString) + .flatMap(Sync[F].fromEither) + } + onComplete(methodCall) { - case Success(result) => - result match { - case Right(resp) => - logger.trace("Request successful: {}", resp.substring(0, 100)) - respondWithHeader(JsonContentType) { - complete(resp) - } - case Left(er) => - er match { - case BridgeError.GrpcMethodNotFound => - val message = s"Method '${methodNameString.fullName}' not found" - logger.debug(message) - respondWithHeader(JsonContentType) { - complete(StatusCodes.NotFound, BridgeErrorResponse.fromMessage(message)) - } - case er: BridgeError.Json => - val message = "Wrong JSON" - logger.debug(message, er.t) - respondWithHeader(JsonContentType) { - complete(StatusCodes.BadRequest, BridgeErrorResponse.fromException(message, er.t)) - } - case er: BridgeError.Grpc => - val message = "gRPC error" + Option(er.s.getDescription).map(": " + _).getOrElse("") - logger.trace(message, er.s.getCause) - val (s, body) = mapStatus(er.s) - respondWithHeader(JsonContentType) { - complete(s, body) - } - case er: BridgeError.Unknown => - val message = "Unknown error" - logger.warn(message, er.t) - respondWithHeader(JsonContentType) { - complete(StatusCodes.InternalServerError, BridgeErrorResponse.fromException(message, er.t)) - } - } - } - case Failure(NonFatal(er)) => + case Success(resp) => + logger.trace("Request successful: {}", resp.substring(0, 100)) + + complete(ToResponseMarshallable(resp)(jsonStringMarshaller)) + case Failure(BridgeError.GrpcMethodNotFound) => + val message = s"Method '${methodNameString.fullName}' not found" + logger.debug(message) + + complete(StatusCodes.NotFound, BridgeErrorResponse.fromMessage(message)) + case Failure(er: BridgeError.Json) => + val message = "Wrong JSON" + logger.debug(message, er.t) + + complete(StatusCodes.BadRequest, BridgeErrorResponse.fromException(message, er.t)) + case Failure(er: BridgeError.Grpc) => + val message = "gRPC error" + Option(er.s.getDescription).map(": " + _).getOrElse("") + logger.trace(message, er.s.getCause) + val (s, body) = mapStatus(er.s) + + complete(s, body) + case Failure(er: BridgeError.Unknown) => + val message = "Unknown error" + logger.warn(message, er.t) + + complete(StatusCodes.InternalServerError, BridgeErrorResponse.fromException(message, er.t)) + case Failure(NonFatal(ex)) => val message = "Unknown exception" - logger.debug(message, er) - respondWithHeader(JsonContentType) { - complete(StatusCodes.InternalServerError, BridgeErrorResponse.fromException(message, er)) - } + logger.debug(message, ex) + + complete(StatusCodes.InternalServerError, BridgeErrorResponse.fromException(message, ex)) case Failure(e) => throw e // scalafix:ok } } case Some(c) => - val message = s"Content-Type must be '$JsonContentType', it is '$c'" + val message = s"Content-Type must be 'application/json', it is '$c'" logger.debug(message) - respondWithHeader(JsonContentType) { - complete(StatusCodes.BadRequest, BridgeErrorResponse.fromMessage(message)) - } + + complete(StatusCodes.BadRequest, BridgeErrorResponse.fromMessage(message)) case None => - val message = s"Content-Type must be '$JsonContentType'" + val message = "Content-Type must be 'application/json'" logger.debug(message) - respondWithHeader(JsonContentType) { - complete(StatusCodes.BadRequest, BridgeErrorResponse.fromMessage(message)) - } + + complete(StatusCodes.BadRequest, BridgeErrorResponse.fromMessage(message)) } } } @@ -118,9 +107,8 @@ object AkkaHttp extends SprayJsonSupport with DefaultJsonProtocol with LazyLoggi case None => val message = s"Service '$serviceName' not found" logger.debug(message) - respondWithHeader(JsonContentType) { - complete(StatusCodes.NotFound, BridgeErrorResponse.fromMessage(message)) - } + + complete(StatusCodes.NotFound, BridgeErrorResponse.fromMessage(message)) case Some(methods) => complete(methods.map(_.fullName).toList.mkString("\n")) } @@ -142,7 +130,7 @@ object AkkaHttp extends SprayJsonSupport with DefaultJsonProtocol with LazyLoggi s.getCode match { case Code.OK => (StatusCodes.OK, description) case Code.CANCELLED => - (ClientError(499)("Client Closed Request", "The operation was cancelled, typically by the caller."), description) + (StatusCodes.custom(499, "Client Closed Request", "The operation was cancelled, typically by the caller."), description) case Code.UNKNOWN => (StatusCodes.InternalServerError, description) case Code.INVALID_ARGUMENT => (StatusCodes.BadRequest, description) case Code.DEADLINE_EXCEEDED => (StatusCodes.GatewayTimeout, description) @@ -162,7 +150,7 @@ object AkkaHttp extends SprayJsonSupport with DefaultJsonProtocol with LazyLoggi } } -final case class Configuration private (pathPrefix: Option[NonEmptyList[String]]) +final case class Configuration(pathPrefix: Option[NonEmptyList[String]]) object Configuration { val Default: Configuration = Configuration( diff --git a/akka-http/src/main/scala/com/avast/grpc/jsonbridge/akkahttp/LiftToFuture.scala b/akka-http/src/main/scala/com/avast/grpc/jsonbridge/akkahttp/LiftToFuture.scala new file mode 100644 index 00000000..e5cb2191 --- /dev/null +++ b/akka-http/src/main/scala/com/avast/grpc/jsonbridge/akkahttp/LiftToFuture.scala @@ -0,0 +1,18 @@ +package com.avast.grpc.jsonbridge.akkahttp + +import cats.effect.IO +import cats.effect.unsafe.IORuntime + +import scala.concurrent.Future + +trait LiftToFuture[F[_]] { + def liftF[A](f: F[A]): Future[A] +} + +object LiftToFuture { + def apply[F[_]](implicit f: LiftToFuture[F]): LiftToFuture[F] = f + + implicit def liftToFutureForIO(implicit runtime: IORuntime): LiftToFuture[IO] = new LiftToFuture[IO] { + override def liftF[A](f: IO[A]): Future[A] = f.unsafeToFuture() + } +} diff --git a/akka-http/src/test/scala/com/avast/grpc/jsonbridge/akkahttp/AkkaHttpTest.scala b/akka-http/src/test/scala/com/avast/grpc/jsonbridge/akkahttp/AkkaHttpTest.scala index c4dfcbc0..9e4d1ee6 100644 --- a/akka-http/src/test/scala/com/avast/grpc/jsonbridge/akkahttp/AkkaHttpTest.scala +++ b/akka-http/src/test/scala/com/avast/grpc/jsonbridge/akkahttp/AkkaHttpTest.scala @@ -1,21 +1,21 @@ package com.avast.grpc.jsonbridge.akkahttp -import akka.http.scaladsl.model.HttpHeader.ParsingResult.Ok import akka.http.scaladsl.model._ -import akka.http.scaladsl.model.headers.`Content-Type` +import akka.http.scaladsl.model.headers.{RawHeader, `Content-Type`} import akka.http.scaladsl.testkit.ScalatestRouteTest import cats.data.NonEmptyList import cats.effect.IO +import cats.effect.unsafe.implicits.global +import cats.implicits._ import com.avast.grpc.jsonbridge._ import io.grpc.ServerServiceDefinition import org.scalatest.funsuite.AnyFunSuite -import scala.concurrent.ExecutionContext +import scala.concurrent.ExecutionContext.Implicits.{global => ec} import scala.util.Random class AkkaHttpTest extends AnyFunSuite with ScalatestRouteTest { - val ec: ExecutionContext = implicitly[ExecutionContext] def bridge(ssd: ServerServiceDefinition): GrpcJsonBridge[IO] = ReflectionGrpcJsonBridge .createFromServices[IO](ec)(ssd) @@ -25,19 +25,22 @@ class AkkaHttpTest extends AnyFunSuite with ScalatestRouteTest { test("basic") { val route = AkkaHttp[IO](Configuration.Default)(bridge(TestServiceImpl.bindService())) - Post("/com.avast.grpc.jsonbridge.test.TestService/Add", """ { "a": 1, "b": 2} """) - .withHeaders(AkkaHttp.JsonContentType) ~> route ~> check { + val entity = HttpEntity(ContentTypes.`application/json`, """ { "a": 1, "b": 2} """) + Post("/com.avast.grpc.jsonbridge.test.TestService/Add", entity) ~> route ~> check { assertResult(StatusCodes.OK)(status) assertResult("""{"sum":3}""")(responseAs[String]) - assertResult(Seq(`Content-Type`(ContentType.WithMissingCharset(MediaType.applicationWithOpenCharset("json")))))(headers) + assertResult(MediaTypes.`application/json`.some)( + header[`Content-Type`].map(_.contentType.mediaType) + ) } } test("with path prefix") { val configuration = Configuration.Default.copy(pathPrefix = Some(NonEmptyList.of("abc", "def"))) val route = AkkaHttp[IO](configuration)(bridge(TestServiceImpl.bindService())) - Post("/abc/def/com.avast.grpc.jsonbridge.test.TestService/Add", """ { "a": 1, "b": 2} """) - .withHeaders(AkkaHttp.JsonContentType) ~> route ~> check { + + val entity = HttpEntity(ContentTypes.`application/json`, """ { "a": 1, "b": 2} """) + Post("/abc/def/com.avast.grpc.jsonbridge.test.TestService/Add", entity) ~> route ~> check { assertResult(StatusCodes.OK)(status) assertResult("""{"sum":3}""")(responseAs[String]) } @@ -46,20 +49,21 @@ class AkkaHttpTest extends AnyFunSuite with ScalatestRouteTest { test("bad request after wrong request") { val route = AkkaHttp[IO](Configuration.Default)(bridge(TestServiceImpl.bindService())) // empty body - Post("/com.avast.grpc.jsonbridge.test.TestService/Add", "") - .withHeaders(AkkaHttp.JsonContentType) ~> route ~> check { + val entity = HttpEntity(ContentTypes.`application/json`, "") + Post("/com.avast.grpc.jsonbridge.test.TestService/Add", entity) ~> route ~> check { assertResult(StatusCodes.BadRequest)(status) } // no Content-Type header - Post("/com.avast.grpc.jsonbridge.test.TestService/Add", """ { "a": 1, "b": 2} """) ~> route ~> check { + val entity2 = HttpEntity(""" { "a": 1, "b": 2} """) + Post("/com.avast.grpc.jsonbridge.test.TestService/Add", entity2) ~> route ~> check { assertResult(StatusCodes.BadRequest)(status) } } test("propagates user-specified status") { val route = AkkaHttp(Configuration.Default)(bridge(PermissionDeniedTestServiceImpl.bindService())) - Post(s"/com.avast.grpc.jsonbridge.test.TestService/Add", """ { "a": 1, "b": 2} """) - .withHeaders(AkkaHttp.JsonContentType) ~> route ~> check { + val entity = HttpEntity(ContentTypes.`application/json`, """ { "a": 1, "b": 2} """) + Post(s"/com.avast.grpc.jsonbridge.test.TestService/Add", entity) ~> route ~> check { assertResult(status)(StatusCodes.Forbidden) } } @@ -83,12 +87,15 @@ class AkkaHttpTest extends AnyFunSuite with ScalatestRouteTest { test("passes headers") { val headerValue = Random.alphanumeric.take(10).mkString("") val route = AkkaHttp[IO](Configuration.Default)(bridge(TestServiceImpl.withInterceptor)) - val Ok(customHeaderToBeSent, _) = HttpHeader.parse(TestServiceImpl.HeaderName, headerValue) - Post("/com.avast.grpc.jsonbridge.test.TestService/Add", """ { "a": 1, "b": 2} """) - .withHeaders(AkkaHttp.JsonContentType, customHeaderToBeSent) ~> route ~> check { + val customHeaderToBeSent = RawHeader(TestServiceImpl.HeaderName, headerValue) + val entity = HttpEntity(ContentTypes.`application/json`, """ { "a": 1, "b": 2} """) + Post("/com.avast.grpc.jsonbridge.test.TestService/Add", entity) + .withHeaders(customHeaderToBeSent) ~> route ~> check { assertResult(StatusCodes.OK)(status) assertResult("""{"sum":3}""")(responseAs[String]) - assertResult(Seq(`Content-Type`(ContentType.WithMissingCharset(MediaType.applicationWithOpenCharset("json")))))(headers) + assertResult(MediaTypes.`application/json`.some)( + header[`Content-Type`].map(_.contentType.mediaType) + ) } } } diff --git a/build.sbt b/build.sbt index f2bca630..b6be2dce 100644 --- a/build.sbt +++ b/build.sbt @@ -1,20 +1,22 @@ -import com.typesafe.tools.mima.core._ +import com.typesafe.tools.mima.core.* +import org.typelevel.scalacoptions.ScalacOptions Global / onChangedBuildSource := ReloadOnSourceChanges val logger: Logger = ConsoleLogger() lazy val ScalaVersions = new { - val V213 = "2.13.12" + val V213 = "2.13.15" val V212 = "2.12.18" + val V33 = "3.3.4" } lazy val Versions = new { - val gpb3Version = "3.25.5" + val gpb3Version = "4.28.3" val grpcVersion = "1.68.1" val circeVersion = "0.14.10" - val http4sVersion = "0.22.2" - val akkaHttp = "10.2.10" + val http4sVersion = "0.23.17" + val akkaHttp = "10.5.3" } lazy val javaSettings = Seq( @@ -47,7 +49,7 @@ lazy val commonSettings = Seq( ) ), ThisBuild / turbo := true, - scalaVersion := ScalaVersions.V213, + scalaVersion := ScalaVersions.V33, crossScalaVersions := Seq(ScalaVersions.V212, ScalaVersions.V213), scalacOptions --= { if (!sys.env.contains("CI")) @@ -58,7 +60,6 @@ lazy val commonSettings = Seq( description := "Library for exposing gRPC endpoints via HTTP API", semanticdbEnabled := true, // enable SemanticDB semanticdbVersion := scalafixSemanticdb.revision, // use Scalafix compatible version - ThisBuild / scalafixScalaBinaryVersion := CrossVersion.binaryScalaVersion(scalaVersion.value), ThisBuild / scalafixDependencies ++= List( "com.github.liancheng" %% "organize-imports" % "0.6.0", "com.github.vovapolu" %% "scaluzzi" % "0.1.23" @@ -67,7 +68,7 @@ lazy val commonSettings = Seq( "org.scala-lang.modules" %% "scala-collection-compat" % "2.12.0", "javax.annotation" % "javax.annotation-api" % "1.3.2", "junit" % "junit" % "4.13.2" % Test, - "org.scalatest" %% "scalatest" % "3.2.17" % Test, + "org.scalatest" %% "scalatest" % "3.2.19" % Test, "com.github.sbt" % "junit-interface" % "0.13.3" % Test, // Required by sbt to execute JUnit tests "ch.qos.logback" % "logback-classic" % "1.5.12" % Test ), @@ -76,7 +77,10 @@ lazy val commonSettings = Seq( ), mimaPreviousArtifacts := previousStableVersion.value.map(organization.value %% moduleName.value % _).toSet, testOptions += Tests.Argument(TestFrameworks.JUnit), - Test / tpolecatExcludeOptions += ScalacOptions.warnNonUnitStatement + Test / tpolecatExcludeOptions ++= Set( + ScalacOptions.warnNonUnitStatement, + ScalacOptions.warnValueDiscard + ) ) ++ addCommandAlias("check", "; lint; +missinglinkCheck; +mimaReportBinaryIssues; +test") ++ addCommandAlias( @@ -86,9 +90,9 @@ lazy val commonSettings = Seq( addCommandAlias("fix", "; compile:scalafix; test:scalafix; scalafmtSbt; scalafmtAll") lazy val grpcTestGenSettings = inConfig(Test)(sbtprotoc.ProtocPlugin.protobufConfigSettings) ++ Seq( - PB.protocVersion := "3.9.1", + PB.protocVersion := "4.28.3", grpcExePath := xsbti.api.SafeLazy.strict { - val exe: File = (baseDirectory in Test).value / ".bin" / grpcExeFileName + val exe: File = (Test / baseDirectory).value / ".bin" / grpcExeFileName if (!exe.exists) { logger.info("gRPC protoc plugin (for Java) does not exist. Downloading") // IO.download(grpcExeUrl, exe) @@ -99,19 +103,19 @@ lazy val grpcTestGenSettings = inConfig(Test)(sbtprotoc.ProtocPlugin.protobufCon } exe }, - PB.protocOptions in Test ++= Seq( + Test / PB.protocOptions ++= Seq( s"--plugin=protoc-gen-java_rpc=${grpcExePath.value.get}", - s"--java_rpc_out=${(sourceManaged in Test).value.getAbsolutePath}" + s"--java_rpc_out=${(Test / sourceManaged).value.getAbsolutePath}" ), - PB.targets in Test := Seq( - PB.gens.java -> (sourceManaged in Test).value + Test / PB.targets := Seq( + PB.gens.java -> (Test / sourceManaged).value ) ) lazy val grpcScalaPBTestGenSettings = inConfig(Test)(sbtprotoc.ProtocPlugin.protobufConfigSettings) ++ Seq( - PB.protocVersion := "3.9.1", - PB.targets in Test := Seq( - scalapb.gen() -> (sourceManaged in Test).value + PB.protocVersion := "4.28.3", + Test / PB.targets := Seq( + scalapb.gen() -> (Test / sourceManaged).value ) ) @@ -137,10 +141,10 @@ lazy val core = (project in file("core")).settings( "io.grpc" % "grpc-stub" % Versions.grpcVersion, "io.grpc" % "grpc-inprocess" % Versions.grpcVersion, "org.typelevel" %% "cats-core" % "2.12.0", - "org.typelevel" %% "cats-effect" % "2.5.5", + "org.typelevel" %% "cats-effect" % "3.5.5", "com.typesafe.scala-logging" %% "scala-logging" % "3.9.5", - "org.slf4j" % "jul-to-slf4j" % "2.0.13", - "org.slf4j" % "jcl-over-slf4j" % "2.0.13", + "org.slf4j" % "jul-to-slf4j" % "2.0.16", + "org.slf4j" % "jcl-over-slf4j" % "2.0.16", "io.grpc" % "grpc-services" % Versions.grpcVersion % Test ) ) @@ -154,7 +158,7 @@ lazy val coreScalaPB = (project in file("core-scalapb")) "com.thesamet.scalapb" %% "scalapb-runtime-grpc" % scalapb.compiler.Version.scalapbVersion, "com.thesamet.scalapb" %% "scalapb-json4s" % "0.12.1", "junit" % "junit" % "4.13.2" % Test, - "org.scalatest" %% "scalatest" % "3.2.17" % Test, + "org.scalatest" %% "scalatest" % "3.2.19" % Test, "com.github.sbt" % "junit-interface" % "0.13.3" % Test, // Required by sbt to execute JUnit tests "ch.qos.logback" % "logback-classic" % "1.5.12" % Test, "io.grpc" % "grpc-services" % Versions.grpcVersion % Test, @@ -186,22 +190,24 @@ lazy val akkaHttp = (project in file("akka-http")) libraryDependencies ++= Seq( "com.typesafe.akka" %% "akka-http" % Versions.akkaHttp, "com.typesafe.akka" %% "akka-http-spray-json" % Versions.akkaHttp, - "com.typesafe.akka" %% "akka-stream" % "2.6.20", - "com.typesafe.akka" %% "akka-testkit" % "2.6.20" % Test, + "com.typesafe.akka" %% "akka-stream" % "2.8.8", + "com.typesafe.akka" %% "akka-testkit" % "2.8.8" % Test, "com.typesafe.akka" %% "akka-http-testkit" % Versions.akkaHttp % Test ) ) .dependsOn(core) def grpcExeFileName: String = { - val os = if (scala.util.Properties.isMac) { - "osx-x86_64" - } else if (scala.util.Properties.isWin) { - "windows-x86_64" - } else { - "linux-x86_64" - } - s"$grpcArtifactId-${Versions.grpcVersion}-$os.exe" + val os = + if (scala.util.Properties.isMac) "osx" + else if (scala.util.Properties.isWin) "windows" + else "linux" + + val arch = + if (scala.util.Properties.propOrEmpty("os.arch") == "aarch64") "aarch_64" + else "x86_64" + + s"$grpcArtifactId-${Versions.grpcVersion}-$os-$arch.exe" } val grpcArtifactId = "protoc-gen-grpc-java" diff --git a/core-scalapb/src/main/scala/com/avast/grpc/jsonbridge/scalapb/ScalaPBServiceHandlers.scala b/core-scalapb/src/main/scala/com/avast/grpc/jsonbridge/scalapb/ScalaPBServiceHandlers.scala index 5b659a18..c43e330d 100644 --- a/core-scalapb/src/main/scala/com/avast/grpc/jsonbridge/scalapb/ScalaPBServiceHandlers.scala +++ b/core-scalapb/src/main/scala/com/avast/grpc/jsonbridge/scalapb/ScalaPBServiceHandlers.scala @@ -3,7 +3,6 @@ package com.avast.grpc.jsonbridge.scalapb import java.lang.reflect.{InvocationTargetException, Method} import cats.effect.Async -import cats.syntax.all._ import com.avast.grpc.jsonbridge.GrpcJsonBridge.GrpcMethodName import com.avast.grpc.jsonbridge.ReflectionGrpcJsonBridge.{HandlerFunc, ServiceHandlers} import com.avast.grpc.jsonbridge.{BridgeError, JavaGenericHelper, ReflectionGrpcJsonBridge} @@ -19,7 +18,6 @@ import scalapb.{GeneratedMessage, GeneratedMessageCompanion} import scala.concurrent.{ExecutionContext, Future} import scala.jdk.CollectionConverters._ import scala.util.control.NonFatal -import scala.util.{Failure, Success} private[jsonbridge] object ScalaPBServiceHandlers extends ServiceHandlers with StrictLogging { def createServiceHandlers[F[_]]( @@ -99,7 +97,7 @@ private[jsonbridge] object ScalaPBServiceHandlers extends ServiceHandlers with S case Left(e: ParseException) => F.pure(Left(BridgeError.Json(e))) case Left(e) => F.pure(Left(BridgeError.Unknown(e))) case Right(request) => - fromScalaFuture(ec) { + F.fromFuture { F.delay { executeCore(request, headers, futureStubCtor, scalaMethod)(ec) } @@ -124,7 +122,7 @@ private[jsonbridge] object ScalaPBServiceHandlers extends ServiceHandlers with S scalaMethod .invoke(stubWithMetadata, request.asInstanceOf[Object]) .asInstanceOf[scala.concurrent.Future[GeneratedMessage]] - .map(gm => printer.print(gm)) + .map(gm => printer.print[GeneratedMessage](gm)) .map(Right(_): Either[BridgeError.Narrow, String]) .recover { case e: StatusException => @@ -157,13 +155,4 @@ private[jsonbridge] object ScalaPBServiceHandlers extends ServiceHandlers with S companionField.get(requestMarshaller).asInstanceOf[GeneratedMessageCompanion[_]] } - private def fromScalaFuture[F[_], A](ec: ExecutionContext)(fsf: F[Future[A]])(implicit F: Async[F]): F[A] = - fsf.flatMap { sf => - F.async { cb => - sf.onComplete { - case Success(r) => cb(Right(r)) - case Failure(e) => cb(Left(BridgeError.Unknown(e))) - }(ec) - } - } } diff --git a/core-scalapb/src/test/scala/com/avast/grpc/jsonbridge/scalapbtest/ScalaPBReflectionGrpcJsonBridgeTest.scala b/core-scalapb/src/test/scala/com/avast/grpc/jsonbridge/scalapbtest/ScalaPBReflectionGrpcJsonBridgeTest.scala index 2959e175..f6e92a03 100644 --- a/core-scalapb/src/test/scala/com/avast/grpc/jsonbridge/scalapbtest/ScalaPBReflectionGrpcJsonBridgeTest.scala +++ b/core-scalapb/src/test/scala/com/avast/grpc/jsonbridge/scalapbtest/ScalaPBReflectionGrpcJsonBridgeTest.scala @@ -1,6 +1,7 @@ package com.avast.grpc.jsonbridge.scalapbtest import cats.effect.IO +import cats.effect.unsafe.implicits.global import com.avast.grpc.jsonbridge.GrpcJsonBridge.GrpcMethodName import com.avast.grpc.jsonbridge.scalapb.ScalaPBReflectionGrpcJsonBridge import com.avast.grpc.jsonbridge.scalapbtest.TestServices.TestServiceGrpc @@ -10,7 +11,7 @@ import io.grpc.protobuf.services.{HealthStatusManager, ProtoReflectionService} import org.scalatest.matchers.should.Matchers import org.scalatest.{Outcome, flatspec} -import scala.concurrent.ExecutionContext.Implicits.global +import scala.concurrent.ExecutionContext.Implicits.{global => ec} class ScalaPBReflectionGrpcJsonBridgeTest extends flatspec.FixtureAnyFlatSpec with Matchers { @@ -20,11 +21,11 @@ class ScalaPBReflectionGrpcJsonBridgeTest extends flatspec.FixtureAnyFlatSpec wi val channelName = InProcessServerBuilder.generateName val server = InProcessServerBuilder .forName(channelName) - .addService(TestServiceGrpc.bindService(new TestServiceImpl, global)) + .addService(TestServiceGrpc.bindService(new TestServiceImpl, ec)) .addService(ProtoReflectionService.newInstance()) .addService(new HealthStatusManager().getHealthService) .build - val (bridge, close) = ScalaPBReflectionGrpcJsonBridge.createFromServer[IO](global)(server).allocated.unsafeRunSync() + val (bridge, close) = ScalaPBReflectionGrpcJsonBridge.createFromServer[IO](ec)(server).allocated.unsafeRunSync() try { test(FixtureParam(bridge)) } finally { @@ -34,29 +35,31 @@ class ScalaPBReflectionGrpcJsonBridgeTest extends flatspec.FixtureAnyFlatSpec wi } it must "successfully call the invoke method" in { f => - val Right(response) = - f.bridge - .invoke(GrpcMethodName("com.avast.grpc.jsonbridge.scalapbtest.TestService/Add"), """ { "a": 1, "b": 2} """, Map.empty) - .unsafeRunSync() - response shouldBe """{"sum":3}""" + val response = f.bridge + .invoke(GrpcMethodName("com.avast.grpc.jsonbridge.scalapbtest.TestService/Add"), """ { "a": 1, "b": 2} """, Map.empty) + .unsafeRunSync() + response shouldBe Right("""{"sum":3}""") } it must "return expected status code for missing method" in { f => - val Left(status) = f.bridge.invoke(GrpcMethodName("ble/bla"), "{}", Map.empty).unsafeRunSync() - status shouldBe BridgeError.GrpcMethodNotFound + val status = f.bridge + .invoke(GrpcMethodName("ble/bla"), "{}", Map.empty) + .unsafeRunSync() + status shouldBe Left(BridgeError.GrpcMethodNotFound) } it must "return BridgeError.Json for wrongly named field" in { f => - val Left(status) = f.bridge + val status = f.bridge .invoke(GrpcMethodName("com.avast.grpc.jsonbridge.scalapbtest.TestService/Add"), """ { "x": 1, "b": 2} """, Map.empty) .unsafeRunSync() - status should matchPattern { case BridgeError.Json(_) => } + status should matchPattern { case Left(BridgeError.Json(_)) => } } it must "return expected status code for malformed JSON" in { f => - val Left(status) = - f.bridge.invoke(GrpcMethodName("com.avast.grpc.jsonbridge.scalapbtest.TestService/Add"), "{ble}", Map.empty).unsafeRunSync() - status should matchPattern { case BridgeError.Json(_) => } + val status = f.bridge + .invoke(GrpcMethodName("com.avast.grpc.jsonbridge.scalapbtest.TestService/Add"), "{ble}", Map.empty) + .unsafeRunSync() + status should matchPattern { case Left(BridgeError.Json(_)) => } } } diff --git a/core-scalapb/src/test/scala/com/avast/grpc/jsonbridge/scalapbtest/ScalaPBReflectionGrpcJsonBridgeTest2.scala b/core-scalapb/src/test/scala/com/avast/grpc/jsonbridge/scalapbtest/ScalaPBReflectionGrpcJsonBridgeTest2.scala index 8dbefd2f..8a5fa953 100644 --- a/core-scalapb/src/test/scala/com/avast/grpc/jsonbridge/scalapbtest/ScalaPBReflectionGrpcJsonBridgeTest2.scala +++ b/core-scalapb/src/test/scala/com/avast/grpc/jsonbridge/scalapbtest/ScalaPBReflectionGrpcJsonBridgeTest2.scala @@ -1,6 +1,7 @@ package com.avast.grpc.jsonbridge.scalapbtest import cats.effect.IO +import cats.effect.unsafe.implicits.global import com.avast.grpc.jsonbridge.GrpcJsonBridge.GrpcMethodName import com.avast.grpc.jsonbridge.scalapb.ScalaPBReflectionGrpcJsonBridge import com.avast.grpc.jsonbridge.scalapbtest.TestServices2.TestService2Grpc @@ -10,7 +11,7 @@ import io.grpc.protobuf.services.{HealthStatusManager, ProtoReflectionService} import org.scalatest.matchers.should.Matchers import org.scalatest.{Outcome, flatspec} -import scala.concurrent.ExecutionContext.Implicits.global +import scala.concurrent.ExecutionContext.Implicits.{global => ec} class ScalaPBReflectionGrpcJsonBridgeTest2 extends flatspec.FixtureAnyFlatSpec with Matchers { @@ -20,11 +21,11 @@ class ScalaPBReflectionGrpcJsonBridgeTest2 extends flatspec.FixtureAnyFlatSpec w val channelName = InProcessServerBuilder.generateName val server = InProcessServerBuilder .forName(channelName) - .addService(TestService2Grpc.bindService(new TestServiceImpl2, global)) + .addService(TestService2Grpc.bindService(new TestServiceImpl2, ec)) .addService(ProtoReflectionService.newInstance()) .addService(new HealthStatusManager().getHealthService) .build - val (bridge, close) = ScalaPBReflectionGrpcJsonBridge.createFromServer[IO](global)(server).allocated.unsafeRunSync() + val (bridge, close) = ScalaPBReflectionGrpcJsonBridge.createFromServer[IO](ec)(server).allocated.unsafeRunSync() try { test(FixtureParam(bridge)) } finally { @@ -34,22 +35,22 @@ class ScalaPBReflectionGrpcJsonBridgeTest2 extends flatspec.FixtureAnyFlatSpec w } it must "successfully call the invoke method" in { f => - val Right(response) = - f.bridge - .invoke(GrpcMethodName("com.avast.grpc.jsonbridge.scalapbtest.TestService2/Add2"), """ { "a": 1, "b": 2} """, Map.empty) - .unsafeRunSync() - response shouldBe """{"sum":3}""" + val response = f.bridge + .invoke(GrpcMethodName("com.avast.grpc.jsonbridge.scalapbtest.TestService2/Add2"), """ { "a": 1, "b": 2} """, Map.empty) + .unsafeRunSync() + response shouldBe Right("""{"sum":3}""") } it must "return expected status code for missing method" in { f => - val Left(status) = f.bridge.invoke(GrpcMethodName("ble/bla"), "{}", Map.empty).unsafeRunSync() - status shouldBe BridgeError.GrpcMethodNotFound + val status = f.bridge.invoke(GrpcMethodName("ble/bla"), "{}", Map.empty).unsafeRunSync() + status shouldBe Left(BridgeError.GrpcMethodNotFound) } it must "return expected status code for malformed JSON" in { f => - val Left(status) = - f.bridge.invoke(GrpcMethodName("com.avast.grpc.jsonbridge.scalapbtest.TestService2/Add2"), "{ble}", Map.empty).unsafeRunSync() - status should matchPattern { case BridgeError.Json(_) => } + val status = f.bridge + .invoke(GrpcMethodName("com.avast.grpc.jsonbridge.scalapbtest.TestService2/Add2"), "{ble}", Map.empty) + .unsafeRunSync() + status should matchPattern { case Left(BridgeError.Json(_)) => } } } diff --git a/core/src/main/scala/com/avast/grpc/jsonbridge/BridgeError.scala b/core/src/main/scala/com/avast/grpc/jsonbridge/BridgeError.scala index caeba323..f3b4dc17 100644 --- a/core/src/main/scala/com/avast/grpc/jsonbridge/BridgeError.scala +++ b/core/src/main/scala/com/avast/grpc/jsonbridge/BridgeError.scala @@ -4,7 +4,7 @@ import io.grpc.Status sealed trait BridgeError extends Exception with Product with Serializable object BridgeError { - final case object GrpcMethodNotFound extends BridgeError + case object GrpcMethodNotFound extends BridgeError sealed trait Narrow extends BridgeError final case class Json(t: Throwable) extends Narrow diff --git a/core/src/main/scala/com/avast/grpc/jsonbridge/JavaServiceHandlers.scala b/core/src/main/scala/com/avast/grpc/jsonbridge/JavaServiceHandlers.scala index cec0d3f6..9d539660 100644 --- a/core/src/main/scala/com/avast/grpc/jsonbridge/JavaServiceHandlers.scala +++ b/core/src/main/scala/com/avast/grpc/jsonbridge/JavaServiceHandlers.scala @@ -21,7 +21,7 @@ import scala.util.control.NonFatal private[jsonbridge] object JavaServiceHandlers extends ServiceHandlers with StrictLogging { private val parser: JsonFormat.Parser = JsonFormat.parser() private val printer: JsonFormat.Printer = { - JsonFormat.printer().includingDefaultValueFields().omittingInsignificantWhitespace() + JsonFormat.printer().alwaysPrintFieldsWithNoPresence().omittingInsignificantWhitespace() } def createServiceHandlers[F[_]]( @@ -35,7 +35,7 @@ private[jsonbridge] object JavaServiceHandlers extends ServiceHandlers with Stri } private def createFutureStubCtor(sd: ServiceDescriptor, inProcessChannel: Channel): () => AbstractStub[_] = { - val serviceClassName = sd.getSchemaDescriptor.getClass.getName.split("\\$").head + val serviceClassName = sd.getSchemaDescriptor.getClass.getName.split("\\$").head // scalafix:ok logger.debug(s"Creating instance of $serviceClassName") val method = Class.forName(serviceClassName).getDeclaredMethod("newFutureStub", classOf[Channel]) () => method.invoke(null, inProcessChannel).asInstanceOf[AbstractStub[_]] @@ -112,7 +112,7 @@ private[jsonbridge] object JavaServiceHandlers extends ServiceHandlers with Stri private def fromListenableFuture[F[_], A](ec: ExecutionContext)(flf: F[ListenableFuture[A]])(implicit F: Async[F]): F[A] = flf.flatMap { lf => - F.async { cb => + F.async_ { cb => Futures.addCallback( lf, new FutureCallback[A] { diff --git a/core/src/main/scala/com/avast/grpc/jsonbridge/ReflectionGrpcJsonBridge.scala b/core/src/main/scala/com/avast/grpc/jsonbridge/ReflectionGrpcJsonBridge.scala index 04704f0b..32b9d233 100644 --- a/core/src/main/scala/com/avast/grpc/jsonbridge/ReflectionGrpcJsonBridge.scala +++ b/core/src/main/scala/com/avast/grpc/jsonbridge/ReflectionGrpcJsonBridge.scala @@ -11,6 +11,7 @@ import io.grpc.inprocess.{InProcessChannelBuilder, InProcessServerBuilder} import scala.concurrent.ExecutionContext import scala.jdk.CollectionConverters._ +import scala.util.Random object ReflectionGrpcJsonBridge extends ReflectionGrpcJsonBridge(JavaServiceHandlers) { // JSON body and headers to a response (fail status or JSON response) @@ -35,7 +36,7 @@ private[jsonbridge] class ReflectionGrpcJsonBridge(serviceHandlers: ServiceHandl ec: ExecutionContext )(services: ServerServiceDefinition*)(implicit F: Async[F]): Resource[F, GrpcJsonBridge[F]] = { for { - inProcessServiceName <- Resource.eval(F.delay { s"ReflectionGrpcJsonBridge-${System.nanoTime()}" }) + inProcessServiceName <- Resource.eval(F.delay { s"ReflectionGrpcJsonBridge-${Random.nextInt()}" }) inProcessServer <- createInProcessServer(ec)(inProcessServiceName, services) inProcessChannel <- createInProcessChannel(ec)(inProcessServiceName) handlersPerMethod = diff --git a/core/src/test/scala/com/avast/grpc/jsonbridge/ReflectionGrpcJsonBridgeTest.scala b/core/src/test/scala/com/avast/grpc/jsonbridge/ReflectionGrpcJsonBridgeTest.scala index e261f1aa..1a855391 100644 --- a/core/src/test/scala/com/avast/grpc/jsonbridge/ReflectionGrpcJsonBridgeTest.scala +++ b/core/src/test/scala/com/avast/grpc/jsonbridge/ReflectionGrpcJsonBridgeTest.scala @@ -1,13 +1,14 @@ package com.avast.grpc.jsonbridge import cats.effect.IO +import cats.effect.unsafe.implicits.global import com.avast.grpc.jsonbridge.GrpcJsonBridge.GrpcMethodName import io.grpc.inprocess.InProcessServerBuilder import io.grpc.protobuf.services.{HealthStatusManager, ProtoReflectionService} import org.scalatest.matchers.should.Matchers import org.scalatest.{Outcome, flatspec} -import scala.concurrent.ExecutionContext.Implicits.global +import scala.concurrent.ExecutionContext.Implicits.{global => ec} class ReflectionGrpcJsonBridgeTest extends flatspec.FixtureAnyFlatSpec with Matchers { @@ -21,7 +22,7 @@ class ReflectionGrpcJsonBridgeTest extends flatspec.FixtureAnyFlatSpec with Matc .addService(ProtoReflectionService.newInstance()) .addService(new HealthStatusManager().getHealthService) .build - val (bridge, close) = ReflectionGrpcJsonBridge.createFromServer[IO](global)(server).allocated.unsafeRunSync() + val (bridge, close) = ReflectionGrpcJsonBridge.createFromServer[IO](ec)(server).allocated.unsafeRunSync() try { test(FixtureParam(bridge)) } finally { @@ -31,29 +32,31 @@ class ReflectionGrpcJsonBridgeTest extends flatspec.FixtureAnyFlatSpec with Matc } it must "successfully call the invoke method" in { f => - val Right(response) = - f.bridge - .invoke(GrpcMethodName("com.avast.grpc.jsonbridge.test.TestService/Add"), """ { "a": 1, "b": 2} """, Map.empty) - .unsafeRunSync() - response shouldBe """{"sum":3}""" + val response = f.bridge + .invoke(GrpcMethodName("com.avast.grpc.jsonbridge.test.TestService/Add"), """ { "a": 1, "b": 2} """, Map.empty) + .unsafeRunSync() + response shouldBe Right("""{"sum":3}""") } it must "return expected status code for missing method" in { f => - val Left(status) = f.bridge.invoke(GrpcMethodName("ble/bla"), "{}", Map.empty).unsafeRunSync() - status shouldBe BridgeError.GrpcMethodNotFound + val status = f.bridge + .invoke(GrpcMethodName("ble/bla"), "{}", Map.empty) + .unsafeRunSync() + status shouldBe Left(BridgeError.GrpcMethodNotFound) } it must "return BridgeError.Json for wrongly named field" in { f => - val Left(status) = f.bridge + val status = f.bridge .invoke(GrpcMethodName("com.avast.grpc.jsonbridge.test.TestService/Add"), """ { "x": 1, "b": 2} """, Map.empty) .unsafeRunSync() - status should matchPattern { case BridgeError.Json(_) => } + status should matchPattern { case Left(BridgeError.Json(_)) => } } it must "return expected status code for malformed JSON" in { f => - val Left(status) = - f.bridge.invoke(GrpcMethodName("com.avast.grpc.jsonbridge.test.TestService/Add"), "{ble}", Map.empty).unsafeRunSync() - status should matchPattern { case BridgeError.Json(_) => } + val status = f.bridge + .invoke(GrpcMethodName("com.avast.grpc.jsonbridge.test.TestService/Add"), "{ble}", Map.empty) + .unsafeRunSync() + status should matchPattern { case Left(BridgeError.Json(_)) => } } } diff --git a/core/src/test/scala/com/avast/grpc/jsonbridge/ReflectionGrpcJsonBridgeTest2.scala b/core/src/test/scala/com/avast/grpc/jsonbridge/ReflectionGrpcJsonBridgeTest2.scala index b1dc92e6..85fb8cb4 100644 --- a/core/src/test/scala/com/avast/grpc/jsonbridge/ReflectionGrpcJsonBridgeTest2.scala +++ b/core/src/test/scala/com/avast/grpc/jsonbridge/ReflectionGrpcJsonBridgeTest2.scala @@ -1,13 +1,14 @@ package com.avast.grpc.jsonbridge import cats.effect.IO +import cats.effect.unsafe.implicits.global import com.avast.grpc.jsonbridge.GrpcJsonBridge.GrpcMethodName import io.grpc.inprocess.InProcessServerBuilder import io.grpc.protobuf.services.{HealthStatusManager, ProtoReflectionService} +import org.scalatest._ import org.scalatest.matchers.should.Matchers -import org.scalatest.{flatspec, _} -import scala.concurrent.ExecutionContext.Implicits.global +import scala.concurrent.ExecutionContext.Implicits.{global => ec} class ReflectionGrpcJsonBridgeTest2 extends flatspec.FixtureAnyFlatSpec with Matchers { @@ -21,7 +22,7 @@ class ReflectionGrpcJsonBridgeTest2 extends flatspec.FixtureAnyFlatSpec with Mat .addService(ProtoReflectionService.newInstance()) .addService(new HealthStatusManager().getHealthService) .build - val (bridge, close) = ReflectionGrpcJsonBridge.createFromServer[IO](global)(server).allocated.unsafeRunSync() + val (bridge, close) = ReflectionGrpcJsonBridge.createFromServer[IO](ec)(server).allocated.unsafeRunSync() try { test(FixtureParam(bridge)) } finally { @@ -31,22 +32,21 @@ class ReflectionGrpcJsonBridgeTest2 extends flatspec.FixtureAnyFlatSpec with Mat } it must "successfully call the invoke method" in { f => - val Right(response) = - f.bridge - .invoke(GrpcMethodName("com.avast.grpc.jsonbridge.test.TestService2/Add2"), """ { "a": 1, "b": 2} """, Map.empty) - .unsafeRunSync() - response shouldBe """{"sum":3}""" + val response = f.bridge + .invoke(GrpcMethodName("com.avast.grpc.jsonbridge.test.TestService2/Add2"), """ { "a": 1, "b": 2} """, Map.empty) + .unsafeRunSync() + response shouldBe Right("""{"sum":3}""") } it must "return expected status code for missing method" in { f => - val Left(status) = f.bridge.invoke(GrpcMethodName("ble/bla"), "{}", Map.empty).unsafeRunSync() - status shouldBe BridgeError.GrpcMethodNotFound + val status = f.bridge.invoke(GrpcMethodName("ble/bla"), "{}", Map.empty).unsafeRunSync() + status shouldBe Left(BridgeError.GrpcMethodNotFound) } it must "return expected status code for malformed JSON" in { f => - val Left(status) = + val status = f.bridge.invoke(GrpcMethodName("com.avast.grpc.jsonbridge.test.TestService2/Add2"), "{ble}", Map.empty).unsafeRunSync() - status should matchPattern { case BridgeError.Json(_) => } + status should matchPattern { case Left(BridgeError.Json(_)) => } } } diff --git a/http4s/src/main/scala/com/avast/grpc/jsonbridge/http4s/Http4s.scala b/http4s/src/main/scala/com/avast/grpc/jsonbridge/http4s/Http4s.scala index e488339d..2d39386f 100644 --- a/http4s/src/main/scala/com/avast/grpc/jsonbridge/http4s/Http4s.scala +++ b/http4s/src/main/scala/com/avast/grpc/jsonbridge/http4s/Http4s.scala @@ -21,6 +21,15 @@ import scala.annotation.nowarn object Http4s extends LazyLogging { + private val ClientClosedRequest = Status.fromInt(499).fold(throw _, identity) // scalafix:ok + + implicit def stringDecoder[F[_]: Sync]: EntityDecoder[F, String] = new EntityDecoder[F, String] { + override def decode(m: Media[F], strict: Boolean): DecodeResult[F, String] = + DecodeResult.success(m.bodyText.compile.string) + + override def consumes: Set[MediaRange] = Set(MediaRange.`text/*`, MediaRange.`application/*`) + } + def apply[F[_]: Sync](configuration: Configuration)(bridge: GrpcJsonBridge[F]): HttpRoutes[F] = { implicit val h: Http4sDsl[F] = Http4sDsl[F] import h._ @@ -100,17 +109,18 @@ object Http4s extends LazyLogging { private def mapStatus[F[_]: Sync](s: GrpcStatus, configuration: Configuration)(implicit h: Http4sDsl[F]): F[Response[F]] = { import h._ - val ClientClosedRequest = Status(499, "Client Closed Request") - final case class ClientClosedRequestOps(status: ClientClosedRequest.type) extends EntityResponseGenerator[F, F] { - val liftG: F ~> F = h.liftG - } val description = BridgeErrorResponse.fromGrpcStatus(s) // https://github.com/grpc/grpc/blob/master/doc/statuscodes.md s.getCode match { case Code.OK => Ok(description) - case Code.CANCELLED => ClientClosedRequestOps(ClientClosedRequest)(description) + case Code.CANCELLED => + final case class ClientClosedRequestOps(status: ClientClosedRequest.type) extends EntityResponseGenerator[F, F] { + val liftG: F ~> F = h.liftG + } + + ClientClosedRequestOps(ClientClosedRequest)(description) case Code.UNKNOWN => InternalServerError(description) case Code.INVALID_ARGUMENT => BadRequest(description) case Code.DEADLINE_EXCEEDED => GatewayTimeout(description) diff --git a/http4s/src/test/scala/com/avast/grpc/jsonbridge/http4s/Http4sTest.scala b/http4s/src/test/scala/com/avast/grpc/jsonbridge/http4s/Http4sTest.scala index 0e77cac8..632356fd 100644 --- a/http4s/src/test/scala/com/avast/grpc/jsonbridge/http4s/Http4sTest.scala +++ b/http4s/src/test/scala/com/avast/grpc/jsonbridge/http4s/Http4sTest.scala @@ -2,6 +2,8 @@ package com.avast.grpc.jsonbridge.http4s import cats.data.NonEmptyList import cats.effect.IO +import cats.effect.unsafe.implicits.global +import cats.implicits._ import com.avast.grpc.jsonbridge._ import io.grpc.ServerServiceDefinition import org.http4s.headers.{`Content-Length`, `Content-Type`} @@ -10,13 +12,11 @@ import org.scalatest.concurrent.ScalaFutures import org.scalatest.funsuite.AnyFunSuite import org.typelevel.ci.CIString -import scala.concurrent.ExecutionContext -import scala.concurrent.ExecutionContext.Implicits.global +import scala.concurrent.ExecutionContext.Implicits.{global => ec} import scala.util.Random class Http4sTest extends AnyFunSuite with ScalaFutures { - val ec: ExecutionContext = implicitly[ExecutionContext] def bridge(ssd: ServerServiceDefinition): GrpcJsonBridge[IO] = ReflectionGrpcJsonBridge .createFromServices[IO](ec)(ssd) @@ -27,7 +27,7 @@ class Http4sTest extends AnyFunSuite with ScalaFutures { test("basic") { val service = Http4s(Configuration.Default)(bridge(TestServiceImpl.bindService())) - val Some(response) = service + val response = service .apply( Request[IO]( method = Method.POST, @@ -38,17 +38,17 @@ class Http4sTest extends AnyFunSuite with ScalaFutures { .value .unsafeRunSync() - assertResult(org.http4s.Status.Ok)(response.status) + assertResult(org.http4s.Status.Ok.some)(response.map(_.status)) - assertResult("""{"sum":3}""")(response.as[String].unsafeRunSync()) + assertResult("""{"sum":3}""".some)(response.map(_.as[String].unsafeRunSync())) - assertResult(Headers(`Content-Type`(MediaType.application.json), `Content-Length`(9)))(response.headers) + assertResult(Headers(`Content-Type`(MediaType.application.json), `Content-Length`(9)).some)(response.map(_.headers)) } test("path prefix") { val configuration = Configuration.Default.copy(pathPrefix = Some(NonEmptyList.of("abc", "def"))) val service = Http4s(configuration)(bridge(TestServiceImpl.bindService())) - val Some(response) = service + val response = service .apply( Request[IO](method = Method.POST, uri = Uri.fromString("/abc/def/com.avast.grpc.jsonbridge.test.TestService/Add").getOrElse(fail())) .withEntity(""" { "a": 1, "b": 2} """) @@ -57,18 +57,18 @@ class Http4sTest extends AnyFunSuite with ScalaFutures { .value .unsafeRunSync() - assertResult(org.http4s.Status.Ok)(response.status) + assertResult(org.http4s.Status.Ok.some)(response.map(_.status)) - assertResult("""{"sum":3}""")(response.as[String].unsafeRunSync()) + assertResult("""{"sum":3}""".some)(response.map(_.as[String].unsafeRunSync())) - assertResult(Headers(`Content-Type`(MediaType.application.json), `Content-Length`(9)))(response.headers) + assertResult(Headers(`Content-Type`(MediaType.application.json), `Content-Length`(9)).some)(response.map(_.headers)) } test("bad request after wrong request") { val service = Http4s(Configuration.Default)(bridge(TestServiceImpl.bindService())) { // empty body - val Some(response) = service + val response = service .apply( Request[IO](method = Method.POST, uri = Uri.fromString("/com.avast.grpc.jsonbridge.test.TestService/Add").getOrElse(fail())) .withEntity("") @@ -77,12 +77,12 @@ class Http4sTest extends AnyFunSuite with ScalaFutures { .value .unsafeRunSync() - assertResult(org.http4s.Status.BadRequest)(response.status) - assertResult("Bad Request")(response.status.reason) + assertResult(org.http4s.Status.BadRequest.some)(response.map(_.status)) + assertResult("Bad Request".some)(response.map(_.status.reason)) } { - val Some(response) = service + val response = service .apply( Request[IO](method = Method.POST, uri = Uri.fromString("/com.avast.grpc.jsonbridge.test.TestService/Add").getOrElse(fail())) .withEntity(""" { "a": 1, "b": 2} """) @@ -90,14 +90,14 @@ class Http4sTest extends AnyFunSuite with ScalaFutures { .value .unsafeRunSync() - assertResult(org.http4s.Status.BadRequest)(response.status) + assertResult(org.http4s.Status.BadRequest.some)(response.map(_.status)) } } test("propagate user-specified status") { val service = Http4s(Configuration.Default)(bridge(PermissionDeniedTestServiceImpl.bindService())) - val Some(response) = service + val response = service .apply( Request[IO](method = Method.POST, uri = Uri.fromString("/com.avast.grpc.jsonbridge.test.TestService/Add").getOrElse(fail())) .withEntity(""" { "a": 1, "b": 2} """) @@ -106,38 +106,38 @@ class Http4sTest extends AnyFunSuite with ScalaFutures { .value .unsafeRunSync() - assertResult(org.http4s.Status.Forbidden)(response.status) - assertResult("Forbidden")(response.status.reason) + assertResult(org.http4s.Status.Forbidden.some)(response.map(_.status)) + assertResult("Forbidden".some)(response.map(_.status.reason)) } test("provides service info") { val service = Http4s(Configuration.Default)(bridge(TestServiceImpl.bindService())) - val Some(response) = service + val response = service .apply( Request[IO](method = Method.GET, uri = Uri.fromString("/com.avast.grpc.jsonbridge.test.TestService").getOrElse(fail())) ) .value .unsafeRunSync() - assertResult(org.http4s.Status.Ok)(response.status) + assertResult(org.http4s.Status.Ok.some)(response.map(_.status)) - assertResult("com.avast.grpc.jsonbridge.test.TestService/Add")(response.as[String].unsafeRunSync()) + assertResult("com.avast.grpc.jsonbridge.test.TestService/Add".some)(response.map(_.as[String].unsafeRunSync())) } test("provides services info") { val service = Http4s(Configuration.Default)(bridge(TestServiceImpl.bindService())) - val Some(response) = service + val response = service .apply( Request[IO](method = Method.GET, uri = Uri.fromString("/").getOrElse(fail())) ) .value .unsafeRunSync() - assertResult(org.http4s.Status.Ok)(response.status) + assertResult(org.http4s.Status.Ok.some)(response.map(_.status)) - assertResult("com.avast.grpc.jsonbridge.test.TestService/Add")(response.as[String].unsafeRunSync()) + assertResult("com.avast.grpc.jsonbridge.test.TestService/Add".some)(response.map(_.as[String].unsafeRunSync())) } test("passes user headers") { @@ -145,7 +145,7 @@ class Http4sTest extends AnyFunSuite with ScalaFutures { val headerValue = Random.alphanumeric.take(10).mkString("") - val Some(response) = service + val response = service .apply( Request[IO]( method = Method.POST, @@ -157,7 +157,7 @@ class Http4sTest extends AnyFunSuite with ScalaFutures { .value .unsafeRunSync() - assertResult(org.http4s.Status.Ok)(response.status) + assertResult(org.http4s.Status.Ok.some)(response.map(_.status)) assertResult(headerValue)(TestServiceImpl.lastContextValue.get()) } } diff --git a/project/build.properties b/project/build.properties index 4d5f78cc..c7450fc2 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.9.9 \ No newline at end of file +sbt.version=1.10.5 \ No newline at end of file diff --git a/project/plugins.sbt b/project/plugins.sbt index 7148e5df..c744a74e 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,7 +1,7 @@ addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.2") -addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.12.1") +addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.13.0") addSbtPlugin("ch.epfl.scala" % "sbt-missinglink" % "0.3.6") addSbtPlugin("com.typesafe" % "sbt-mima-plugin" % "1.1.4") -addSbtPlugin("io.github.davidgregory084" % "sbt-tpolecat" % "0.4.4") +addSbtPlugin("org.typelevel" % "sbt-tpolecat" % "0.5.2") addSbtPlugin("com.timushev.sbt" % "sbt-rewarn" % "0.1.3") addSbtPlugin("com.github.sbt" % "sbt-ci-release" % "1.9.0")