Skip to content

Commit

Permalink
Merge branch 'main' into update/compilerplugin-0.11.17
Browse files Browse the repository at this point in the history
  • Loading branch information
hamnis authored Dec 5, 2024
2 parents 215535a + 3e6d932 commit eea6adb
Show file tree
Hide file tree
Showing 9 changed files with 111 additions and 45 deletions.
9 changes: 9 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ jobs:
runs-on: ${{ matrix.os }}
timeout-minutes: 60
steps:
- name: Install sbt
uses: sbt/setup-sbt@v1

- name: Checkout current branch (full)
uses: actions/checkout@v4
with:
Expand Down Expand Up @@ -151,6 +154,9 @@ jobs:
java: [temurin@8]
runs-on: ${{ matrix.os }}
steps:
- name: Install sbt
uses: sbt/setup-sbt@v1

- name: Checkout current branch (full)
uses: actions/checkout@v4
with:
Expand Down Expand Up @@ -287,6 +293,9 @@ jobs:
java: [temurin@11]
runs-on: ${{ matrix.os }}
steps:
- name: Install sbt
uses: sbt/setup-sbt@v1

- name: Checkout current branch (full)
uses: actions/checkout@v4
with:
Expand Down
2 changes: 1 addition & 1 deletion .scalafmt.conf
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
version = 3.8.1
version = 3.8.3

style = default

Expand Down
10 changes: 5 additions & 5 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ ThisBuild / tlSonatypeUseLegacyHost := false

ThisBuild / tlMimaPreviousVersions := Set()

val Scala212 = "2.12.19"
val Scala213 = "2.13.14"
val Scala212 = "2.12.20"
val Scala213 = "2.13.15"

ThisBuild / crossScalaVersions := Seq(Scala213, "3.3.3")
ThisBuild / crossScalaVersions := Seq(Scala213, "3.3.4")
ThisBuild / scalaVersion := Scala213

// disable sbt-header plugin until we are not aligned on the license
Expand All @@ -24,9 +24,9 @@ ThisBuild / headerCheckAll := Nil
ThisBuild / tlCiDependencyGraphJob := false

val catsV = "2.11.0"
val catsEffectV = "3.5.4"
val catsEffectV = "3.5.7"
val fs2V = "3.9.2"
val http4sV = "0.23.27"
val http4sV = "0.23.30"
val munitCatsEffectV = "2.0.0"
import scalapb.compiler.Version.scalapbVersion

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,13 @@

package org.http4s.grpc.generator

import com.google.protobuf.Descriptors.{MethodDescriptor, ServiceDescriptor}
import com.google.protobuf.Descriptors.MethodDescriptor
import com.google.protobuf.Descriptors.ServiceDescriptor
import scalapb.compiler.DescriptorImplicits
import scalapb.compiler.FunctionalPrinter
import scalapb.compiler.FunctionalPrinter.PrinterEndo
import scalapb.compiler.{DescriptorImplicits, FunctionalPrinter, StreamType}
import scalapb.compiler.ProtobufGenerator.asScalaDocBlock
import scalapb.compiler.StreamType

class Http4sGrpcServicePrinter(service: ServiceDescriptor, di: DescriptorImplicits) {
import di._
Expand Down Expand Up @@ -102,7 +105,7 @@ class Http4sGrpcServicePrinter(service: ServiceDescriptor, di: DescriptorImplici
_.call(service.methods.map(serviceMethodImplementation): _*)

private[this] def serviceBindingImplementations: PrinterEndo =
_.add(s"$HttpRoutes.empty[F]").indent
_.add(s"$ServerGrpc.precondition[F]").indent
.call(service.methods.map(serviceBindingImplementation): _*)
.add(s""".combineK($ServerGrpc.methodNotFoundRoute("${service.getFullName()}"))""")
.outdent
Expand Down
54 changes: 48 additions & 6 deletions codegen/testing/src/test/scala/hello/world/TestServiceSuite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@ import cats.effect.IO
import cats.syntax.all._
import fs2.Stream
import munit._
import org.http4s.Headers
import org.http4s.Uri
import org.http4s._
import org.http4s.client.Client
import org.http4s.syntax.all._
import org.scalacheck._
Expand Down Expand Up @@ -72,11 +71,55 @@ class TestServiceSuite extends CatsEffectSuite with ScalaCheckEffectSuite {
}
}

test("Routes returns missing method") {
test("Routes returns 405 Method Not Allowed with Allow header on GET requests") {
val client = Client.fromHttpApp(TestService.toRoutes(impl).orNotFound)
implicit val methodExceptPost: Arbitrary[Method] =
Arbitrary(Gen.oneOf(Method.all.filterNot(_ === Method.POST)))
implicit val wellKnownGRPCHeaders: Arbitrary[Header.ToRaw] = Arbitrary(
Gen.oneOf(
"Content-Type" -> "application/grpc",
"Content-Type" -> "application/grpc+proto",
"Content-Type" -> "application/grpc+json",
)
)
forAllF { (meth: Method, grpcHeader: Header.ToRaw) =>
client
.run(
Request[IO](meth, uri"/hello.world.TestService/any/url")
.withHeaders(grpcHeader)
)
.use { resp =>
val headers = resp.headers
val methods = headers.get[org.http4s.headers.Allow].map(_.methods)
(resp.status, methods).pure[IO]
}
.assertEquals(
(Status.MethodNotAllowed, Some(Set(Method.POST)))
)
}
}

test("Routes returns 415 Unsupported Media Type on requests without grpc content type") {
val client = Client.fromHttpApp(TestService.toRoutes(impl).orNotFound)
client
.run(
Request[IO](Method.POST, uri"/hello.world.TestService/noStreaming")
)
.use { resp =>
val status = resp.status
status.pure[IO]
}
.assertEquals(
Status.UnsupportedMediaType
)
}

test("Routes returns UNIMPLEMENTED") {
val client = Client.fromHttpApp(TestService.toRoutes(impl).orNotFound)
client
.run(
org.http4s.Request[IO](org.http4s.Method.POST, uri"/hello.world.TestService/missingMethod")
Request[IO](org.http4s.Method.POST, uri"/hello.world.TestService/missingMethod")
.withHeaders("Content-Type" -> "application/grpc")
)
.use { resp =>
val headers = resp.headers
Expand All @@ -91,8 +134,7 @@ class TestServiceSuite extends CatsEffectSuite with ScalaCheckEffectSuite {
test("Client fails with initial failure") {
forAllF { (msg: TestMessage) =>
val route = org.http4s.HttpRoutes.of[IO] { case _ =>
org.http4s
.Response(org.http4s.Status.Ok)
Response(Status.Ok)
.putHeaders(
org.http4s.grpc.codecs.NamedHeaders.GrpcStatus(12)
)
Expand Down
60 changes: 36 additions & 24 deletions core/src/main/scala/org/http4s/grpc/ServerGrpc.scala
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package org.http4s.grpc

import cats.Monad
import cats.effect._
import cats.syntax.all._
import fs2._
import org.http4s._
import org.http4s.dsl.request._
import org.http4s.grpc.codecs.NamedHeaders
import org.http4s.headers.Allow
import org.http4s.headers.Trailer
import org.typelevel.ci._
import scodec.Decoder
Expand All @@ -15,6 +17,16 @@ import java.util.concurrent.TimeoutException
import scala.concurrent.duration._

object ServerGrpc {
def precondition[F[_]: Monad]: HttpRoutes[F] = HttpRoutes.of[F] {
case req if req.method != Method.POST =>
Response(Status.MethodNotAllowed).withHeaders(Allow(Method.POST)).pure[F]
case req if !hasGRPCContentType(req) =>
Response[F](Status.UnsupportedMediaType).pure[F]
}

private def hasGRPCContentType[F[_]](req: Request[F]): Boolean = req.headers
.get(CIString("Content-Type"))
.exists(_.exists(_.value.startsWith("application/grpc")))

def unaryToUnary[F[_]: Temporal, A, B]( // Stuff We can provide via codegen\
decode: Decoder[A],
Expand All @@ -26,7 +38,7 @@ object ServerGrpc {
): HttpRoutes[F] = HttpRoutes.of[F] {
case req @ POST -> Root / sN / mN if sN === serviceName && mN === methodName =>
for {
status <- Ref.of[F, (Int, Option[String])]((0, Option.empty))
status <- Deferred[F, (Int, Option[String])]
trailers = status.get.map { case (i, message) =>
Headers(
NamedHeaders.GrpcStatus(i)
Expand All @@ -39,11 +51,11 @@ object ServerGrpc {
.evalMap(f(_, req.headers))
.flatMap(codecs.Messages.encodeSingle(encode)(_))
.through(timeoutStream(_)(timeout.map(_.duration)))
.onFinalizeCase {
case Resource.ExitCase.Errored(_: TimeoutException) => status.set((4, None))
case Resource.ExitCase.Errored(e) => status.set((2, e.toString().some))
case Resource.ExitCase.Canceled => status.set((1, None))
case _ => ().pure[F]
.onFinalizeCaseWeak {
case Resource.ExitCase.Errored(_: TimeoutException) => status.complete((4, None)).void
case Resource.ExitCase.Errored(e) => status.complete((2, e.toString().some)).void
case Resource.ExitCase.Canceled => status.complete((1, None)).void
case Resource.ExitCase.Succeeded => status.complete((0, None)).void
}
.mask // ensures body closure without rst-stream

Expand All @@ -69,7 +81,7 @@ object ServerGrpc {
): HttpRoutes[F] = HttpRoutes.of[F] {
case req @ POST -> Root / sN / mN if sN === serviceName && mN === methodName =>
for {
status <- Ref.of[F, (Int, Option[String])]((0, Option.empty))
status <- Deferred[F, (Int, Option[String])]
trailers = status.get.map { case (i, message) =>
Headers(
NamedHeaders.GrpcStatus(i)
Expand All @@ -82,11 +94,11 @@ object ServerGrpc {
.flatMap(f(_, req.headers))
.through(codecs.Messages.encode(encode))
.through(timeoutStream(_)(timeout.map(_.duration)))
.onFinalizeCase {
case Resource.ExitCase.Errored(_: TimeoutException) => status.set((4, None))
case Resource.ExitCase.Errored(e) => status.set((2, e.toString().some))
case Resource.ExitCase.Canceled => status.set((1, None))
case _ => ().pure[F]
.onFinalizeCaseWeak {
case Resource.ExitCase.Errored(_: TimeoutException) => status.complete((4, None)).void
case Resource.ExitCase.Errored(e) => status.complete((2, e.toString().some)).void
case Resource.ExitCase.Canceled => status.complete((1, None)).void
case Resource.ExitCase.Succeeded => status.complete((0, None)).void
}
.mask // ensures body closure without rst-stream
Response[F](Status.Ok, HttpVersion.`HTTP/2`)
Expand All @@ -111,7 +123,7 @@ object ServerGrpc {
): HttpRoutes[F] = HttpRoutes.of[F] {
case req @ POST -> Root / sN / mN if sN === serviceName && mN === methodName =>
for {
status <- Ref.of[F, (Int, Option[String])]((0, Option.empty))
status <- Deferred[F, (Int, Option[String])]
trailers = status.get.map { case (i, message) =>
Headers(
NamedHeaders.GrpcStatus(i)
Expand All @@ -124,11 +136,11 @@ object ServerGrpc {
.eval(f(codecs.Messages.decode(decode)(req.body), req.headers))
.flatMap(codecs.Messages.encodeSingle(encode)(_))
.through(timeoutStream(_)(timeout.map(_.duration)))
.onFinalizeCase {
case Resource.ExitCase.Errored(_: TimeoutException) => status.set((4, None))
case Resource.ExitCase.Errored(e) => status.set((2, e.toString().some))
case Resource.ExitCase.Canceled => status.set((1, None))
case _ => ().pure[F]
.onFinalizeCaseWeak {
case Resource.ExitCase.Errored(_: TimeoutException) => status.complete((4, None)).void
case Resource.ExitCase.Errored(e) => status.complete((2, e.toString().some)).void
case Resource.ExitCase.Canceled => status.complete((1, None)).void
case Resource.ExitCase.Succeeded => status.complete((0, None)).void
}
.mask // ensures body closure without rst-stream

Expand All @@ -154,7 +166,7 @@ object ServerGrpc {
): HttpRoutes[F] = HttpRoutes.of[F] {
case req @ POST -> Root / sN / mN if sN === serviceName && mN === methodName =>
for {
status <- Ref.of[F, (Int, Option[String])]((0, Option.empty))
status <- Deferred[F, (Int, Option[String])]
trailers = status.get.map { case (i, message) =>
Headers(
NamedHeaders.GrpcStatus(i)
Expand All @@ -166,11 +178,11 @@ object ServerGrpc {
val body = f(codecs.Messages.decode(decode)(req.body), req.headers)
.through(codecs.Messages.encode(encode))
.through(timeoutStream(_)(timeout.map(_.duration)))
.onFinalizeCase {
case Resource.ExitCase.Errored(_: TimeoutException) => status.set((4, None))
case Resource.ExitCase.Errored(e) => status.set((2, e.toString().some))
case Resource.ExitCase.Canceled => status.set((1, None))
case _ => ().pure[F]
.onFinalizeCaseWeak {
case Resource.ExitCase.Errored(_: TimeoutException) => status.complete((4, None)).void
case Resource.ExitCase.Errored(e) => status.complete((2, e.toString().some)).void
case Resource.ExitCase.Canceled => status.complete((1, None)).void
case Resource.ExitCase.Succeeded => status.complete((0, None)).void
}
.mask // ensures body closure without rst-stream

Expand Down
4 changes: 2 additions & 2 deletions core/src/main/scala/org/http4s/grpc/SharedGrpc.scala
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ private object SharedGrpc {
.getOrElse(throw new Throwable("Impossible: This protocol is valid"))

// TODO Content-Coding → "identity" / "gzip" / "deflate" / "snappy" / {custom}
val GrpcEncoding: Header.Raw = org.http4s.Header.Raw(CIString("grpc-encoding"), "identity")
val GrpcEncoding: Header.Raw = Header.Raw(CIString("grpc-encoding"), "identity")
val GrpcAcceptEncoding: Header.Raw =
org.http4s.Header.Raw(CIString("grpc-accept-encoding"), "identity")
val TE: Header.Raw = org.http4s.Header.Raw(CIString("te"), "trailers")
val TE: Header.Raw = Header.Raw(CIString("te"), "trailers")

}
2 changes: 1 addition & 1 deletion project/build.properties
Original file line number Diff line number Diff line change
@@ -1 +1 @@
sbt.version=1.10.0
sbt.version=1.10.6
6 changes: 3 additions & 3 deletions project/plugins.sbt
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
addSbtPlugin("org.http4s" % "sbt-http4s-org" % "0.17.1")
addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.16.0")
addSbtPlugin("org.http4s" % "sbt-http4s-org" % "0.17.5")
addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.17.0")
addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.3.2")
addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.17")
addSbtPlugin("org.portable-scala" % "sbt-scala-native-crossproject" % "1.3.2")
addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.12.0")
addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.13.1")

addSbtPlugin(
"com.thesamet" % "sbt-protoc" % "1.0.7"
Expand Down

0 comments on commit eea6adb

Please sign in to comment.