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 bea7ee54..70570f0d 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,23 +1,27 @@ package com.avast.grpc.jsonbridge.akkahttp +import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport import akka.http.scaladsl.model.StatusCodes.ClientError -import cats.effect.implicits._ 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 com.avast.grpc.jsonbridge.GrpcJsonBridge +import cats.effect.implicits._ import com.avast.grpc.jsonbridge.GrpcJsonBridge.GrpcMethodName +import com.avast.grpc.jsonbridge.{GrpcJsonBridge, GrpcStatusJson} import io.grpc.Status.Code +import spray.json._ import scala.concurrent.ExecutionContext import scala.language.higherKinds import scala.util.control.NonFatal import scala.util.{Failure, Success} -object AkkaHttp { +object AkkaHttp extends SprayJsonSupport with DefaultJsonProtocol { + + private implicit val grpcStatusJsonFormat: RootJsonFormat[GrpcStatusJson] = jsonFormat3(GrpcStatusJson.apply) private[akkahttp] final val JsonContentType: `Content-Type` = `Content-Type` { ContentType.WithMissingCharset(MediaType.applicationWithOpenCharset("json")) @@ -52,7 +56,9 @@ object AkkaHttp { respondWithHeader(JsonContentType) { complete(r) } - case Success(Left(status)) => complete(mapStatus(status)) + case Success(Left(status)) => + val (s, body) = mapStatus(status) + complete(s, body) case Failure(NonFatal(_)) => complete(StatusCodes.InternalServerError) } } @@ -81,24 +87,30 @@ object AkkaHttp { private def mapHeaders(headers: Seq[HttpHeader]): Map[String, String] = headers.toList.map(h => (h.name(), h.value())).toMap // https://github.com/grpc/grpc/blob/master/doc/statuscodes.md - private def mapStatus(s: io.grpc.Status): StatusCode = s.getCode match { - case Code.OK => StatusCodes.OK - case Code.CANCELLED => ClientError(499)("Client Closed Request", "The operation was cancelled, typically by the caller.") - case Code.UNKNOWN => StatusCodes.InternalServerError - case Code.INVALID_ARGUMENT => StatusCodes.BadRequest - case Code.DEADLINE_EXCEEDED => StatusCodes.GatewayTimeout - case Code.NOT_FOUND => StatusCodes.NotFound - case Code.ALREADY_EXISTS => StatusCodes.Conflict - case Code.PERMISSION_DENIED => StatusCodes.Forbidden - case Code.RESOURCE_EXHAUSTED => StatusCodes.TooManyRequests - case Code.FAILED_PRECONDITION => StatusCodes.BadRequest - case Code.ABORTED => StatusCodes.Conflict - case Code.OUT_OF_RANGE => StatusCodes.BadRequest - case Code.UNIMPLEMENTED => StatusCodes.NotImplemented - case Code.INTERNAL => StatusCodes.InternalServerError - case Code.UNAVAILABLE => StatusCodes.ServiceUnavailable - case Code.DATA_LOSS => StatusCodes.InternalServerError - case Code.UNAUTHENTICATED => StatusCodes.Unauthorized + private def mapStatus(s: io.grpc.Status): (StatusCode, GrpcStatusJson) = { + + val description = GrpcStatusJson.fromGrpcStatus(s) + + 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) + case Code.UNKNOWN => (StatusCodes.InternalServerError, description) + case Code.INVALID_ARGUMENT => (StatusCodes.BadRequest, description) + case Code.DEADLINE_EXCEEDED => (StatusCodes.GatewayTimeout, description) + case Code.NOT_FOUND => (StatusCodes.NotFound, description) + case Code.ALREADY_EXISTS => (StatusCodes.Conflict, description) + case Code.PERMISSION_DENIED => (StatusCodes.Forbidden, description) + case Code.RESOURCE_EXHAUSTED => (StatusCodes.TooManyRequests, description) + case Code.FAILED_PRECONDITION => (StatusCodes.BadRequest, description) + case Code.ABORTED => (StatusCodes.Conflict, description) + case Code.OUT_OF_RANGE => (StatusCodes.BadRequest, description) + case Code.UNIMPLEMENTED => (StatusCodes.NotImplemented, description) + case Code.INTERNAL => (StatusCodes.InternalServerError, description) + case Code.UNAVAILABLE => (StatusCodes.ServiceUnavailable, description) + case Code.DATA_LOSS => (StatusCodes.InternalServerError, description) + case Code.UNAUTHENTICATED => (StatusCodes.Unauthorized, description) + } } } diff --git a/build.sbt b/build.sbt index dec49db8..0e85545c 100644 --- a/build.sbt +++ b/build.sbt @@ -8,7 +8,8 @@ crossScalaVersions := Seq("2.12.8") lazy val Versions = new { val gpb3Version = "3.8.0" val grpcVersion = "1.22.1" - + val circeVersion = "0.11.1" + val http4sVersion = "0.20.6" val akkaHttp = "10.1.5" // DO NOT upgrade to 10.1.[67] - will cause https://github.com/scala/community-builds/issues/825 } @@ -119,8 +120,11 @@ lazy val http4s = (project in file("http4s")).settings( grpcTestGenSettings, name := "grpc-json-bridge-http4s", libraryDependencies ++= Seq( - "org.http4s" %% "http4s-dsl" % "0.20.0", - "org.http4s" %% "http4s-blaze-server" % "0.20.0" + "org.http4s" %% "http4s-dsl" % Versions.http4sVersion, + "org.http4s" %% "http4s-blaze-server" % Versions.http4sVersion, + "org.http4s" %% "http4s-circe" % Versions.http4sVersion, + "io.circe" %% "circe-core" % Versions.circeVersion, + "io.circe" %% "circe-generic" % Versions.circeVersion ), scalacOptions += "-Ypartial-unification" ).dependsOn(core) @@ -132,6 +136,7 @@ lazy val akkaHttp = (project in file("akka-http")).settings( name := "grpc-json-bridge-akkahttp", libraryDependencies ++= Seq( "com.typesafe.akka" %% "akka-http" % Versions.akkaHttp, + "com.typesafe.akka" %% "akka-http-spray-json" % Versions.akkaHttp, "com.typesafe.akka" %% "akka-stream" % "2.5.21", "com.typesafe.akka" %% "akka-http-testkit" % Versions.akkaHttp % "test" ), diff --git a/core/src/main/scala/com/avast/grpc/jsonbridge/GrpcStatusJson.scala b/core/src/main/scala/com/avast/grpc/jsonbridge/GrpcStatusJson.scala new file mode 100644 index 00000000..45b85ed5 --- /dev/null +++ b/core/src/main/scala/com/avast/grpc/jsonbridge/GrpcStatusJson.scala @@ -0,0 +1,13 @@ +package com.avast.grpc.jsonbridge + +import io.grpc.Status + +final case class GrpcStatusJson(description: Option[String], exception: Option[String], message: Option[String]) + +object GrpcStatusJson { + def fromGrpcStatus(s: Status): GrpcStatusJson = GrpcStatusJson( + Option(s.getDescription), + Option(s.getCause).flatMap(e => Option(e.getClass.getCanonicalName)), + Option(s.getCause).flatMap(e => Option(e.getMessage)) + ) +} diff --git a/http4s/src/main/scala/com/avast/grpc/jsonbrige/http4s/Http4s.scala b/http4s/src/main/scala/com/avast/grpc/jsonbrige/http4s/Http4s.scala index 005dc836..7f69d4af 100644 --- a/http4s/src/main/scala/com/avast/grpc/jsonbrige/http4s/Http4s.scala +++ b/http4s/src/main/scala/com/avast/grpc/jsonbrige/http4s/Http4s.scala @@ -3,7 +3,7 @@ package com.avast.grpc.jsonbrige.http4s import cats.data.NonEmptyList import cats.effect._ import cats.syntax.all._ -import com.avast.grpc.jsonbridge.GrpcJsonBridge +import com.avast.grpc.jsonbridge.{GrpcJsonBridge, GrpcStatusJson} import com.avast.grpc.jsonbridge.GrpcJsonBridge.GrpcMethodName import com.typesafe.scalalogging.StrictLogging import io.grpc.Status.Code @@ -85,12 +85,10 @@ object Http4s extends StrictLogging { private def mapStatus[F[_]: Sync](s: GrpcStatus, configuration: Configuration)(implicit h: Http4sDsl[F]): F[Response[F]] = { import h._ + import io.circe.generic.auto._ + import org.http4s.circe.CirceEntityEncoder._ - val description = List( - Option(s.getDescription), - Option(s.getCause).flatMap(e => Option(e.getClass.getCanonicalName)), - Option(s.getCause).flatMap(e => Option(e.getMessage)) - ).flatten.mkString(", ") + val description = GrpcStatusJson.fromGrpcStatus(s) // https://github.com/grpc/grpc/blob/master/doc/statuscodes.md s.getCode match {