From ed9cf0459268ca3c4567ac8ec944f14da6a4f714 Mon Sep 17 00:00:00 2001 From: Juan Pedro Moreno <4879373+juanpedromoreno@users.noreply.github.com> Date: Tue, 20 Mar 2018 07:04:14 +0100 Subject: [PATCH 01/35] Initial steps for http integration (#203) --- build.sbt | 9 ++++ .../server/src/main/scala/HttpConfig.scala | 20 +++++++++ .../src/main/scala/HttpServerBuilder.scala | 31 +++++++++++++ .../src/main/scala/HttpServerStream.scala | 35 +++++++++++++++ .../src/test/scala/ExampleService.scala | 33 ++++++++++++++ .../src/test/scala/HttpServerTests.scala | 45 +++++++++++++++++++ project/ProjectPlugin.scala | 10 +++++ 7 files changed, 183 insertions(+) create mode 100644 modules/http/server/src/main/scala/HttpConfig.scala create mode 100644 modules/http/server/src/main/scala/HttpServerBuilder.scala create mode 100644 modules/http/server/src/main/scala/HttpServerStream.scala create mode 100644 modules/http/server/src/test/scala/ExampleService.scala create mode 100644 modules/http/server/src/test/scala/HttpServerTests.scala diff --git a/build.sbt b/build.sbt index 33dde5165..18f643253 100644 --- a/build.sbt +++ b/build.sbt @@ -174,6 +174,14 @@ lazy val `idlgen-sbt` = project .settings(buildInfoKeys := Seq[BuildInfoKey](name, version, scalaVersion, sbtVersion)) .settings(buildInfoPackage := "freestyle.rpc.idlgen") +lazy val `http-server` = project + .in(file("modules/http/server")) + .dependsOn(common % "compile->compile;test->test") + .dependsOn(internal) + .settings(moduleName := "frees-rpc-http-server") + .settings(rpcHttpServerSettings) + .disablePlugins(ScriptedPlugin) + ////////////////// //// EXAMPLES //// ////////////////// @@ -304,6 +312,7 @@ lazy val allModules: Seq[ProjectReference] = Seq( testing, ssl, `idlgen-core`, + `http-server`, `marshallers-jodatime`, `example-routeguide-protocol`, `example-routeguide-common`, diff --git a/modules/http/server/src/main/scala/HttpConfig.scala b/modules/http/server/src/main/scala/HttpConfig.scala new file mode 100644 index 000000000..93fabaa36 --- /dev/null +++ b/modules/http/server/src/main/scala/HttpConfig.scala @@ -0,0 +1,20 @@ +/* + * Copyright 2017-2018 47 Degrees, LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package freestyle.rpc.http +package server + +final case class HttpConfig(host: String, port: Int) diff --git a/modules/http/server/src/main/scala/HttpServerBuilder.scala b/modules/http/server/src/main/scala/HttpServerBuilder.scala new file mode 100644 index 000000000..e399437a5 --- /dev/null +++ b/modules/http/server/src/main/scala/HttpServerBuilder.scala @@ -0,0 +1,31 @@ +/* + * Copyright 2017-2018 47 Degrees, LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package freestyle.rpc.http +package server + +import cats.effect.Effect +import monix.execution.Scheduler +import org.http4s.HttpService +import org.http4s.server.blaze.BlazeBuilder + +class HttpServerBuilder[F[_]: Effect](implicit C: HttpConfig, S: Scheduler) { + + def build(service: HttpService[F], prefix: String = "/"): BlazeBuilder[F] = + BlazeBuilder[F] + .bindHttp(C.port, C.host) + .mountService(service, prefix) +} diff --git a/modules/http/server/src/main/scala/HttpServerStream.scala b/modules/http/server/src/main/scala/HttpServerStream.scala new file mode 100644 index 000000000..f71f7d9ba --- /dev/null +++ b/modules/http/server/src/main/scala/HttpServerStream.scala @@ -0,0 +1,35 @@ +/* + * Copyright 2017-2018 47 Degrees, LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package freestyle.rpc.http +package server + +import cats.effect.Effect +import fs2.{Stream, StreamApp} +import monix.execution.Scheduler +import org.http4s.HttpService + +object HttpServerStream { + + def apply[F[_]: Effect](service: HttpService[F], prefix: String = "/")( + implicit C: HttpConfig, + S: Scheduler): Stream[F, StreamApp.ExitCode] = { + val httpServerBuilder: HttpServerBuilder[F] = new HttpServerBuilder[F] + + httpServerBuilder.build(service, prefix).serve + } + +} diff --git a/modules/http/server/src/test/scala/ExampleService.scala b/modules/http/server/src/test/scala/ExampleService.scala new file mode 100644 index 000000000..0433340a2 --- /dev/null +++ b/modules/http/server/src/test/scala/ExampleService.scala @@ -0,0 +1,33 @@ +/* + * Copyright 2017-2018 47 Degrees, LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package freestyle.rpc.http +package server + +import cats.effect.Effect +import monix.execution.Scheduler +import org.http4s.HttpService +import org.http4s.dsl.Http4sDsl + +class ExampleService[F[_]: Effect] extends Http4sDsl[F] { + + def service(implicit scheduler: Scheduler): HttpService[F] = + HttpService[F] { + case GET -> Root / "ping" => + Ok("pong") + } + +} diff --git a/modules/http/server/src/test/scala/HttpServerTests.scala b/modules/http/server/src/test/scala/HttpServerTests.scala new file mode 100644 index 000000000..45779908a --- /dev/null +++ b/modules/http/server/src/test/scala/HttpServerTests.scala @@ -0,0 +1,45 @@ +/* + * Copyright 2017-2018 47 Degrees, LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package freestyle.rpc.http +package server + +import cats.effect.IO +import monix.execution.Scheduler +import org.http4s.HttpService +import org.scalatest.{Assertion, Matchers, WordSpec} + +class HttpServerTests extends WordSpec with Matchers { + + implicit val S: Scheduler = monix.execution.Scheduler.Implicits.global + implicit val C: HttpConfig = HttpConfig("0.0.0.0", 8090) + + val service: HttpService[IO] = new ExampleService[IO].service + val prefix: String = "/" + + def ok: Assertion = 1 shouldBe 1 + + "HttpServerBuilder.build" should { + + "work as expected" in { + + new HttpServerBuilder[IO].build(service, prefix) + + ok + } + + } +} diff --git a/project/ProjectPlugin.scala b/project/ProjectPlugin.scala index 98c2856d1..1e989561e 100644 --- a/project/ProjectPlugin.scala +++ b/project/ProjectPlugin.scala @@ -28,6 +28,7 @@ object ProjectPlugin extends AutoPlugin { val fs2: String = "0.10.5" val fs2ReactiveStreams: String = "0.5.1" val grpc: String = "1.11.0" + val http4s = "0.18.3" val log4s: String = "1.6.1" val logback: String = "1.2.3" val monix: String = "3.0.0-RC1" @@ -196,6 +197,15 @@ object ProjectPlugin extends AutoPlugin { ) ) + lazy val rpcHttpServerSettings: Seq[Def.Setting[_]] = Seq( + libraryDependencies ++= Seq( + %%("http4s-dsl", V.http4s), + %%("http4s-blaze-server", V.http4s), + %%("http4s-circe", V.http4s), + %%("http4s-blaze-client", V.http4s) % Test + ) + ) + lazy val docsSettings = Seq( // Pointing to https://github.com/frees-io/freestyle/tree/master/docs/src/main/tut/docs/rpc tutTargetDirectory := baseDirectory.value.getParentFile.getParentFile / "docs" / "src" / "main" / "tut" / "docs" / "rpc", From 93f8acebd81610bcec0ff96ebbf48fcb34ab4c7e Mon Sep 17 00:00:00 2001 From: Laurence Lavigne Date: Wed, 18 Apr 2018 17:41:10 -0700 Subject: [PATCH 02/35] Implemented sample client and server REST handlers to be generated --- build.sbt | 1 + .../src/test/scala/ExampleService.scala | 33 ---- .../src/test/scala/GreeterHandler.scala | 42 +++++ .../src/test/scala/GreeterRestClient.scala | 65 ++++++++ .../src/test/scala/GreeterRestService.scala | 86 ++++++++++ .../src/test/scala/GreeterRestTests.scala | 149 ++++++++++++++++++ .../src/test/scala/GreeterService.scala | 42 +++++ .../src/test/scala/HttpServerTests.scala | 45 ------ project/ProjectPlugin.scala | 3 +- 9 files changed, 387 insertions(+), 79 deletions(-) delete mode 100644 modules/http/server/src/test/scala/ExampleService.scala create mode 100644 modules/http/server/src/test/scala/GreeterHandler.scala create mode 100644 modules/http/server/src/test/scala/GreeterRestClient.scala create mode 100644 modules/http/server/src/test/scala/GreeterRestService.scala create mode 100644 modules/http/server/src/test/scala/GreeterRestTests.scala create mode 100644 modules/http/server/src/test/scala/GreeterService.scala delete mode 100644 modules/http/server/src/test/scala/HttpServerTests.scala diff --git a/build.sbt b/build.sbt index 18f643253..b1e23db22 100644 --- a/build.sbt +++ b/build.sbt @@ -178,6 +178,7 @@ lazy val `http-server` = project .in(file("modules/http/server")) .dependsOn(common % "compile->compile;test->test") .dependsOn(internal) + .dependsOn(client % "test->test") .settings(moduleName := "frees-rpc-http-server") .settings(rpcHttpServerSettings) .disablePlugins(ScriptedPlugin) diff --git a/modules/http/server/src/test/scala/ExampleService.scala b/modules/http/server/src/test/scala/ExampleService.scala deleted file mode 100644 index 0433340a2..000000000 --- a/modules/http/server/src/test/scala/ExampleService.scala +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright 2017-2018 47 Degrees, LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package freestyle.rpc.http -package server - -import cats.effect.Effect -import monix.execution.Scheduler -import org.http4s.HttpService -import org.http4s.dsl.Http4sDsl - -class ExampleService[F[_]: Effect] extends Http4sDsl[F] { - - def service(implicit scheduler: Scheduler): HttpService[F] = - HttpService[F] { - case GET -> Root / "ping" => - Ok("pong") - } - -} diff --git a/modules/http/server/src/test/scala/GreeterHandler.scala b/modules/http/server/src/test/scala/GreeterHandler.scala new file mode 100644 index 000000000..ff32cb955 --- /dev/null +++ b/modules/http/server/src/test/scala/GreeterHandler.scala @@ -0,0 +1,42 @@ +/* + * Copyright 2017-2018 47 Degrees, LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package freestyle.rpc.http + +import cats.effect._ +import cats.syntax.applicative._ +import freestyle.rpc.protocol._ +import fs2.Stream + +class GreeterHandler[F[_]: Sync] extends Greeter[F] { + + def getHello(request: Empty.type): F[HelloResponse] = HelloResponse("hey").pure + + def sayHello(request: HelloRequest): F[HelloResponse] = HelloResponse(request.hello).pure + + def sayHellos(requests: Stream[F, HelloRequest]): F[HelloResponse] = + requests.compile.fold(HelloResponse("")) { + case (response, request) => + HelloResponse( + if (response.hello.isEmpty) request.hello else s"${response.hello}, ${request.hello}") + } + + def sayHelloAll(request: HelloRequest): Stream[F, HelloResponse] = + fs2.Stream(HelloResponse(request.hello), HelloResponse(request.hello)) + + def sayHellosAll(requests: Stream[F, HelloRequest]): Stream[F, HelloResponse] = + requests.map(request => HelloResponse(request.hello)) +} diff --git a/modules/http/server/src/test/scala/GreeterRestClient.scala b/modules/http/server/src/test/scala/GreeterRestClient.scala new file mode 100644 index 000000000..c66a96127 --- /dev/null +++ b/modules/http/server/src/test/scala/GreeterRestClient.scala @@ -0,0 +1,65 @@ +/* + * Copyright 2017-2018 47 Degrees, LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package freestyle.rpc.http + +import cats.effect._ +import fs2.Stream +import io.circe._ +import io.circe.generic.auto._ +import io.circe.jawn.CirceSupportParser.facade +import io.circe.syntax._ +import jawnfs2._ +import org.http4s.circe._ +import org.http4s.client._ +import org.http4s.dsl.io._ +import org.http4s._ + +class GreeterRestClient[F[_]: Effect](uri: Uri) { + + private implicit val responseDecoder: EntityDecoder[F, HelloResponse] = jsonOf[F, HelloResponse] + + def getHello()(implicit client: Client[F]): F[HelloResponse] = { + val request = Request[F](Method.GET, uri / "getHello") + client.expect[HelloResponse](request) + } + + def sayHello(arg: HelloRequest)(implicit client: Client[F]): F[HelloResponse] = { + val request = Request[F](Method.POST, uri / "sayHello") + client.expect[HelloResponse](request.withBody(arg.asJson)) + } + + def sayHellos(arg: Stream[F, HelloRequest])(implicit client: Client[F]): F[HelloResponse] = { + val request = Request[F](Method.POST, uri / "sayHellos") + client.expect[HelloResponse](request.withBody(arg.map(_.asJson))) + } + + def sayHelloAll(arg: HelloRequest)(implicit client: Client[F]): Stream[F, HelloResponse] = { + val request = Request[F](Method.POST, uri / "sayHelloAll") + client.streaming(request.withBody(arg.asJson))(responseStream[HelloResponse]) + } + + def sayHellosAll(arg: Stream[F, HelloRequest])( + implicit client: Client[F]): Stream[F, HelloResponse] = { + val request = Request[F](Method.POST, uri / "sayHellosAll") + client.streaming(request.withBody(arg.map(_.asJson)))(responseStream[HelloResponse]) + } + + private def responseStream[A](response: Response[F])(implicit decoder: Decoder[A]): Stream[F, A] = + if (response.status.code != 200) throw UnexpectedStatus(response.status) + else response.body.chunks.parseJsonStream.map(_.as[A]).rethrow + +} diff --git a/modules/http/server/src/test/scala/GreeterRestService.scala b/modules/http/server/src/test/scala/GreeterRestService.scala new file mode 100644 index 000000000..d7cc04455 --- /dev/null +++ b/modules/http/server/src/test/scala/GreeterRestService.scala @@ -0,0 +1,86 @@ +/* + * Copyright 2017-2018 47 Degrees, LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package freestyle.rpc.http + +import cats.effect._ +import cats.syntax.applicative._ +import cats.syntax.flatMap._ +import cats.syntax.functor._ +import freestyle.rpc.protocol.Empty +import fs2.Stream +import jawn.ParseException +import io.circe._ +import io.circe.generic.auto._ +import io.circe.jawn.CirceSupportParser.facade +import io.circe.syntax._ +import jawnfs2._ +import org.http4s._ +import org.http4s.circe._ +import org.http4s.dsl.Http4sDsl + +class GreeterRestService[F[_]: Sync](handler: Greeter[F]) extends Http4sDsl[F] { + + import freestyle.rpc.http.GreeterRestService._ + + private implicit val requestDecoder: EntityDecoder[F, HelloRequest] = jsonOf[F, HelloRequest] + + def service: HttpService[F] = HttpService[F] { + + case GET -> Root / "getHello" => Ok(handler.getHello(Empty).map(_.asJson)) + + case msg @ POST -> Root / "sayHello" => + for { + request <- msg.as[HelloRequest] + response <- Ok(handler.sayHello(request).map(_.asJson)) + } yield response + + case msg @ POST -> Root / "sayHellos" => + for { + requests <- msg.asStream[HelloRequest] + response <- Ok(handler.sayHellos(requests).map(_.asJson)) + } yield response + + case msg @ POST -> Root / "sayHelloAll" => + for { + request <- msg.as[HelloRequest] + responses <- Ok(handler.sayHelloAll(request).map(_.asJson)) + } yield responses + + case msg @ POST -> Root / "sayHellosAll" => + for { + requests <- msg.asStream[HelloRequest] + responses <- Ok(handler.sayHellosAll(requests).map(_.asJson)) + } yield responses + + } +} + +object GreeterRestService { + + implicit class RequestOps[F[_]: Sync](request: Request[F]) { + + def asStream[A](implicit decoder: Decoder[A]): F[Stream[F, A]] = + request.body.chunks.parseJsonStream + .map(_.as[A]) + .handleErrorWith { + case ex: ParseException => + throw MalformedMessageBodyFailure(ex.getMessage, Some(ex)) // will return 400 instead of 500 + } + .rethrow + .pure + } +} diff --git a/modules/http/server/src/test/scala/GreeterRestTests.scala b/modules/http/server/src/test/scala/GreeterRestTests.scala new file mode 100644 index 000000000..6adc37ed1 --- /dev/null +++ b/modules/http/server/src/test/scala/GreeterRestTests.scala @@ -0,0 +1,149 @@ +/* + * Copyright 2017-2018 47 Degrees, LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package freestyle.rpc.http + +import cats.effect.IO +import freestyle.rpc.common.RpcBaseTestSuite +import fs2.Stream +import io.circe.Json +import io.circe.generic.auto._ +import io.circe.syntax._ +import org.http4s._ +import org.http4s.circe._ +import org.http4s.client.UnexpectedStatus +import org.http4s.client.blaze.Http1Client +import org.http4s.dsl.io._ +import org.http4s.server.Server +import org.http4s.server.blaze.BlazeBuilder +import org.scalatest._ + +class GreeterRestTests extends RpcBaseTestSuite with BeforeAndAfter { + + val Hostname = "localhost" + val Port = 8080 + + val serviceUri: Uri = Uri.unsafeFromString(s"http://$Hostname:$Port") + val service: HttpService[IO] = new GreeterRestService[IO](new GreeterHandler[IO]).service + val server: BlazeBuilder[IO] = + BlazeBuilder[IO].bindHttp(Port, Hostname).mountService(service, "/") + + var serverTask: Server[IO] = _ // sorry + before(serverTask = server.start.unsafeRunSync()) + after(serverTask.shutdownNow()) + + "REST Server" should { + + "serve a GET request" in { + val request = Request[IO](Method.GET, serviceUri / "getHello") + val response = (for { + client <- Http1Client[IO]() + response <- client.expect[Json](request) + } yield response).unsafeRunSync() + response shouldBe HelloResponse("hey").asJson + } + + "serve a POST request" in { + val request = Request[IO](Method.POST, serviceUri / "sayHello") + val requestBody = HelloRequest("hey").asJson + val response = (for { + client <- Http1Client[IO]() + response <- client.expect[Json](request.withBody(requestBody)) + } yield response).unsafeRunSync() + response shouldBe HelloResponse("hey").asJson + } + + "return a 400 Bad Request for a malformed unary POST request" in { + val request = Request[IO](Method.POST, serviceUri / "sayHello") + val requestBody = "hey" + val responseError = the[UnexpectedStatus] thrownBy (for { + client <- Http1Client[IO]() + response <- client.expect[Json](request.withBody(requestBody)) + } yield response).unsafeRunSync() + responseError.status.code shouldBe 400 + } + + "return a 400 Bad Request for a malformed streaming POST request" in { + val request = Request[IO](Method.POST, serviceUri / "sayHellos") + val requestBody = "{" + val responseError = the[UnexpectedStatus] thrownBy (for { + client <- Http1Client[IO]() + response <- client.expect[Json](request.withBody(requestBody)) + } yield response).unsafeRunSync() + responseError.status.code shouldBe 400 + } + + } + + val serviceClient: GreeterRestClient[IO] = new GreeterRestClient[IO](serviceUri) + + "REST Service" should { + + "serve a GET request" in { + val response = (for { + client <- Http1Client[IO]() + response <- serviceClient.getHello()(client) + } yield response).unsafeRunSync() + response shouldBe HelloResponse("hey") + } + + "serve a unary POST request" in { + val request = HelloRequest("hey") + val response = (for { + client <- Http1Client[IO]() + response <- serviceClient.sayHello(request)(client) + } yield response).unsafeRunSync() + response shouldBe HelloResponse("hey") + } + + "serve a POST request with fs2 streaming request" in { + val requests = Stream(HelloRequest("hey"), HelloRequest("there")) + val response = (for { + client <- Http1Client[IO]() + response <- serviceClient.sayHellos(requests)(client) + } yield response).unsafeRunSync() + response shouldBe HelloResponse("hey, there") + } + + "serve a POST request with empty fs2 streaming request" in { + val requests = Stream.empty + val response = (for { + client <- Http1Client[IO]() + response <- serviceClient.sayHellos(requests)(client) + } yield response).unsafeRunSync() + response shouldBe HelloResponse("") + } + + "serve a POST request with fs2 streaming response" in { + val request = HelloRequest("hey") + val responses = (for { + client <- Http1Client.stream[IO]() + response <- serviceClient.sayHelloAll(request)(client) + } yield response).compile.toList.unsafeRunSync() + responses shouldBe List(HelloResponse("hey"), HelloResponse("hey")) + } + + "serve a POST request with bidirectional fs2 streaming" in { + val requests = Stream(HelloRequest("hey"), HelloRequest("there")) + val responses = (for { + client <- Http1Client.stream[IO]() + response <- serviceClient.sayHellosAll(requests)(client) + } yield response).compile.toList.unsafeRunSync() + responses shouldBe List(HelloResponse("hey"), HelloResponse("there")) + } + } + +} diff --git a/modules/http/server/src/test/scala/GreeterService.scala b/modules/http/server/src/test/scala/GreeterService.scala new file mode 100644 index 000000000..905ea1163 --- /dev/null +++ b/modules/http/server/src/test/scala/GreeterService.scala @@ -0,0 +1,42 @@ +/* + * Copyright 2017-2018 47 Degrees, LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package freestyle.rpc.http + +import freestyle.rpc.protocol._ +import fs2.Stream + +@message case class HelloRequest(hello: String) + +@message case class HelloResponse(hello: String) + +@service trait Greeter[F[_]] { + + @rpc(Avro) + def getHello(request: Empty.type): F[HelloResponse] + + @rpc(Avro) + def sayHello(request: HelloRequest): F[HelloResponse] + + @rpc(Avro) @stream[RequestStreaming.type] + def sayHellos(requests: Stream[F, HelloRequest]): F[HelloResponse] + + @rpc(Avro) @stream[ResponseStreaming.type] + def sayHelloAll(request: HelloRequest): Stream[F, HelloResponse] + + @rpc(Avro) @stream[BidirectionalStreaming.type] + def sayHellosAll(requests: Stream[F, HelloRequest]): Stream[F, HelloResponse] +} diff --git a/modules/http/server/src/test/scala/HttpServerTests.scala b/modules/http/server/src/test/scala/HttpServerTests.scala deleted file mode 100644 index 45779908a..000000000 --- a/modules/http/server/src/test/scala/HttpServerTests.scala +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 2017-2018 47 Degrees, LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package freestyle.rpc.http -package server - -import cats.effect.IO -import monix.execution.Scheduler -import org.http4s.HttpService -import org.scalatest.{Assertion, Matchers, WordSpec} - -class HttpServerTests extends WordSpec with Matchers { - - implicit val S: Scheduler = monix.execution.Scheduler.Implicits.global - implicit val C: HttpConfig = HttpConfig("0.0.0.0", 8090) - - val service: HttpService[IO] = new ExampleService[IO].service - val prefix: String = "/" - - def ok: Assertion = 1 shouldBe 1 - - "HttpServerBuilder.build" should { - - "work as expected" in { - - new HttpServerBuilder[IO].build(service, prefix) - - ok - } - - } -} diff --git a/project/ProjectPlugin.scala b/project/ProjectPlugin.scala index 1e989561e..e592096f0 100644 --- a/project/ProjectPlugin.scala +++ b/project/ProjectPlugin.scala @@ -28,7 +28,7 @@ object ProjectPlugin extends AutoPlugin { val fs2: String = "0.10.5" val fs2ReactiveStreams: String = "0.5.1" val grpc: String = "1.11.0" - val http4s = "0.18.3" + val http4s = "0.18.9" val log4s: String = "1.6.1" val logback: String = "1.2.3" val monix: String = "3.0.0-RC1" @@ -202,6 +202,7 @@ object ProjectPlugin extends AutoPlugin { %%("http4s-dsl", V.http4s), %%("http4s-blaze-server", V.http4s), %%("http4s-circe", V.http4s), + %%("circe-generic"), %%("http4s-blaze-client", V.http4s) % Test ) ) From b03637c0916db1e2f3395a330117e0145f10b900 Mon Sep 17 00:00:00 2001 From: Laurence Lavigne Date: Mon, 23 Apr 2018 18:09:38 -0700 Subject: [PATCH 03/35] Added monix.Observable implementation This is basically a conversion to/from the internal fs2.Stream. --- build.sbt | 1 + ...terHandler.scala => GreeterHandlers.scala} | 38 +++++- .../src/test/scala/GreeterRestClient.scala | 65 ----------- .../src/test/scala/GreeterRestClients.scala | 108 ++++++++++++++++++ ...ervice.scala => GreeterRestServices.scala} | 60 ++++++++-- .../src/test/scala/GreeterRestTests.scala | 76 +++++++++--- ...terService.scala => GreeterServices.scala} | 23 +++- modules/http/server/src/test/scala/Util.scala | 38 ++++++ 8 files changed, 314 insertions(+), 95 deletions(-) rename modules/http/server/src/test/scala/{GreeterHandler.scala => GreeterHandlers.scala} (55%) delete mode 100644 modules/http/server/src/test/scala/GreeterRestClient.scala create mode 100644 modules/http/server/src/test/scala/GreeterRestClients.scala rename modules/http/server/src/test/scala/{GreeterRestService.scala => GreeterRestServices.scala} (57%) rename modules/http/server/src/test/scala/{GreeterService.scala => GreeterServices.scala} (64%) create mode 100644 modules/http/server/src/test/scala/Util.scala diff --git a/build.sbt b/build.sbt index b1e23db22..6a3d75f93 100644 --- a/build.sbt +++ b/build.sbt @@ -179,6 +179,7 @@ lazy val `http-server` = project .dependsOn(common % "compile->compile;test->test") .dependsOn(internal) .dependsOn(client % "test->test") + .dependsOn(server % "test->test") .settings(moduleName := "frees-rpc-http-server") .settings(rpcHttpServerSettings) .disablePlugins(ScriptedPlugin) diff --git a/modules/http/server/src/test/scala/GreeterHandler.scala b/modules/http/server/src/test/scala/GreeterHandlers.scala similarity index 55% rename from modules/http/server/src/test/scala/GreeterHandler.scala rename to modules/http/server/src/test/scala/GreeterHandlers.scala index ff32cb955..66e629d22 100644 --- a/modules/http/server/src/test/scala/GreeterHandler.scala +++ b/modules/http/server/src/test/scala/GreeterHandlers.scala @@ -16,16 +16,22 @@ package freestyle.rpc.http +import cats.Applicative import cats.effect._ -import cats.syntax.applicative._ -import freestyle.rpc.protocol._ -import fs2.Stream -class GreeterHandler[F[_]: Sync] extends Greeter[F] { +class UnaryGreeterHandler[F[_]: Applicative] extends UnaryGreeter[F] { + + import cats.syntax.applicative._ + import freestyle.rpc.protocol.Empty def getHello(request: Empty.type): F[HelloResponse] = HelloResponse("hey").pure def sayHello(request: HelloRequest): F[HelloResponse] = HelloResponse(request.hello).pure +} + +class Fs2GreeterHandler[F[_]: Sync] extends Fs2Greeter[F] { + + import fs2.Stream def sayHellos(requests: Stream[F, HelloRequest]): F[HelloResponse] = requests.compile.fold(HelloResponse("")) { @@ -35,8 +41,30 @@ class GreeterHandler[F[_]: Sync] extends Greeter[F] { } def sayHelloAll(request: HelloRequest): Stream[F, HelloResponse] = - fs2.Stream(HelloResponse(request.hello), HelloResponse(request.hello)) + Stream(HelloResponse(request.hello), HelloResponse(request.hello)) def sayHellosAll(requests: Stream[F, HelloRequest]): Stream[F, HelloResponse] = requests.map(request => HelloResponse(request.hello)) } + +class MonixGreeterHandler[F[_]: Async](implicit sc: monix.execution.Scheduler) + extends MonixGreeter[F] { + + import freestyle.rpc.server.implicits._ + import monix.reactive.Observable + + def sayHellos(requests: Observable[HelloRequest]): F[HelloResponse] = + requests + .foldLeftL(HelloResponse("")) { + case (response, request) => + HelloResponse( + if (response.hello.isEmpty) request.hello else s"${response.hello}, ${request.hello}") + } + .to[F] + + def sayHelloAll(request: HelloRequest): Observable[HelloResponse] = + Observable(HelloResponse(request.hello), HelloResponse(request.hello)) + + def sayHellosAll(requests: Observable[HelloRequest]): Observable[HelloResponse] = + requests.map(request => HelloResponse(request.hello)) +} diff --git a/modules/http/server/src/test/scala/GreeterRestClient.scala b/modules/http/server/src/test/scala/GreeterRestClient.scala deleted file mode 100644 index c66a96127..000000000 --- a/modules/http/server/src/test/scala/GreeterRestClient.scala +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright 2017-2018 47 Degrees, LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package freestyle.rpc.http - -import cats.effect._ -import fs2.Stream -import io.circe._ -import io.circe.generic.auto._ -import io.circe.jawn.CirceSupportParser.facade -import io.circe.syntax._ -import jawnfs2._ -import org.http4s.circe._ -import org.http4s.client._ -import org.http4s.dsl.io._ -import org.http4s._ - -class GreeterRestClient[F[_]: Effect](uri: Uri) { - - private implicit val responseDecoder: EntityDecoder[F, HelloResponse] = jsonOf[F, HelloResponse] - - def getHello()(implicit client: Client[F]): F[HelloResponse] = { - val request = Request[F](Method.GET, uri / "getHello") - client.expect[HelloResponse](request) - } - - def sayHello(arg: HelloRequest)(implicit client: Client[F]): F[HelloResponse] = { - val request = Request[F](Method.POST, uri / "sayHello") - client.expect[HelloResponse](request.withBody(arg.asJson)) - } - - def sayHellos(arg: Stream[F, HelloRequest])(implicit client: Client[F]): F[HelloResponse] = { - val request = Request[F](Method.POST, uri / "sayHellos") - client.expect[HelloResponse](request.withBody(arg.map(_.asJson))) - } - - def sayHelloAll(arg: HelloRequest)(implicit client: Client[F]): Stream[F, HelloResponse] = { - val request = Request[F](Method.POST, uri / "sayHelloAll") - client.streaming(request.withBody(arg.asJson))(responseStream[HelloResponse]) - } - - def sayHellosAll(arg: Stream[F, HelloRequest])( - implicit client: Client[F]): Stream[F, HelloResponse] = { - val request = Request[F](Method.POST, uri / "sayHellosAll") - client.streaming(request.withBody(arg.map(_.asJson)))(responseStream[HelloResponse]) - } - - private def responseStream[A](response: Response[F])(implicit decoder: Decoder[A]): Stream[F, A] = - if (response.status.code != 200) throw UnexpectedStatus(response.status) - else response.body.chunks.parseJsonStream.map(_.as[A]).rethrow - -} diff --git a/modules/http/server/src/test/scala/GreeterRestClients.scala b/modules/http/server/src/test/scala/GreeterRestClients.scala new file mode 100644 index 000000000..f745bd1ce --- /dev/null +++ b/modules/http/server/src/test/scala/GreeterRestClients.scala @@ -0,0 +1,108 @@ +/* + * Copyright 2017-2018 47 Degrees, LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package freestyle.rpc.http + +import cats.effect._ +import fs2.Stream +import io.circe.generic.auto._ +import io.circe.syntax._ +import org.http4s._ +import org.http4s.circe._ +import org.http4s.client._ +import org.http4s.dsl.io._ + +class UnaryGreeterRestClient[F[_]: Effect](uri: Uri) { + + private implicit val responseDecoder: EntityDecoder[F, HelloResponse] = jsonOf[F, HelloResponse] + + def getHello()(implicit client: Client[F]): F[HelloResponse] = { + val request = Request[F](Method.GET, uri / "getHello") + client.expect[HelloResponse](request) + } + + def sayHello(arg: HelloRequest)(implicit client: Client[F]): F[HelloResponse] = { + val request = Request[F](Method.POST, uri / "sayHello") + client.expect[HelloResponse](request.withBody(arg.asJson)) + } + +} + +class Fs2GreeterRestClient[F[_]: Effect](uri: Uri) { + + import freestyle.rpc.http.RestClient._ + + private implicit val responseDecoder: EntityDecoder[F, HelloResponse] = jsonOf[F, HelloResponse] + + def sayHellos(arg: Stream[F, HelloRequest])(implicit client: Client[F]): F[HelloResponse] = { + val request = Request[F](Method.POST, uri / "sayHellos") + client.expect[HelloResponse](request.withBody(arg.map(_.asJson))) + } + + def sayHelloAll(arg: HelloRequest)(implicit client: Client[F]): Stream[F, HelloResponse] = { + val request = Request[F](Method.POST, uri / "sayHelloAll") + client.streaming(request.withBody(arg.asJson))(_.asStream[HelloResponse]) + } + + def sayHellosAll(arg: Stream[F, HelloRequest])( + implicit client: Client[F]): Stream[F, HelloResponse] = { + val request = Request[F](Method.POST, uri / "sayHellosAll") + client.streaming(request.withBody(arg.map(_.asJson)))(_.asStream[HelloResponse]) + } + +} + +class MonixGreeterRestClient[F[_]: Effect](uri: Uri)(implicit sc: monix.execution.Scheduler) { + + import freestyle.rpc.http.RestClient._ + import freestyle.rpc.http.Util._ + import monix.reactive.Observable + + private implicit val responseDecoder: EntityDecoder[F, HelloResponse] = jsonOf[F, HelloResponse] + + def sayHellos(arg: Observable[HelloRequest])(implicit client: Client[F]): F[HelloResponse] = { + val request = Request[F](Method.POST, uri / "sayHellos") + client.expect[HelloResponse](request.withBody(arg.toStream.map(_.asJson))) + } + + def sayHelloAll(arg: HelloRequest)(implicit client: Client[F]): Observable[HelloResponse] = { + val request = Request[F](Method.POST, uri / "sayHelloAll") + client.streaming(request.withBody(arg.asJson))(_.asStream[HelloResponse]).toObservable + } + + def sayHellosAll(arg: Observable[HelloRequest])( + implicit client: Client[F]): Observable[HelloResponse] = { + val request = Request[F](Method.POST, uri / "sayHellosAll") + client + .streaming(request.withBody(arg.toStream.map(_.asJson)))(_.asStream[HelloResponse]) + .toObservable + } + +} + +object RestClient { + + import io.circe.Decoder + import io.circe.jawn.CirceSupportParser.facade + import jawnfs2._ + + implicit class ResponseOps[F[_]](response: Response[F]) { + + def asStream[A](implicit decoder: Decoder[A]): Stream[F, A] = + if (response.status.code != 200) throw UnexpectedStatus(response.status) + else response.body.chunks.parseJsonStream.map(_.as[A]).rethrow + } +} diff --git a/modules/http/server/src/test/scala/GreeterRestService.scala b/modules/http/server/src/test/scala/GreeterRestServices.scala similarity index 57% rename from modules/http/server/src/test/scala/GreeterRestService.scala rename to modules/http/server/src/test/scala/GreeterRestServices.scala index d7cc04455..ee309b1a4 100644 --- a/modules/http/server/src/test/scala/GreeterRestService.scala +++ b/modules/http/server/src/test/scala/GreeterRestServices.scala @@ -17,24 +17,19 @@ package freestyle.rpc.http import cats.effect._ -import cats.syntax.applicative._ import cats.syntax.flatMap._ import cats.syntax.functor._ -import freestyle.rpc.protocol.Empty import fs2.Stream -import jawn.ParseException -import io.circe._ +import io.circe.Decoder import io.circe.generic.auto._ -import io.circe.jawn.CirceSupportParser.facade import io.circe.syntax._ -import jawnfs2._ import org.http4s._ import org.http4s.circe._ import org.http4s.dsl.Http4sDsl -class GreeterRestService[F[_]: Sync](handler: Greeter[F]) extends Http4sDsl[F] { +class UnaryGreeterRestService[F[_]: Sync](handler: UnaryGreeter[F]) extends Http4sDsl[F] { - import freestyle.rpc.http.GreeterRestService._ + import freestyle.rpc.protocol.Empty private implicit val requestDecoder: EntityDecoder[F, HelloRequest] = jsonOf[F, HelloRequest] @@ -47,6 +42,16 @@ class GreeterRestService[F[_]: Sync](handler: Greeter[F]) extends Http4sDsl[F] { request <- msg.as[HelloRequest] response <- Ok(handler.sayHello(request).map(_.asJson)) } yield response + } +} + +class Fs2GreeterRestService[F[_]: Sync](handler: Fs2Greeter[F]) extends Http4sDsl[F] { + + import freestyle.rpc.http.RestService._ + + private implicit val requestDecoder: EntityDecoder[F, HelloRequest] = jsonOf[F, HelloRequest] + + def service: HttpService[F] = HttpService[F] { case msg @ POST -> Root / "sayHellos" => for { @@ -65,14 +70,51 @@ class GreeterRestService[F[_]: Sync](handler: Greeter[F]) extends Http4sDsl[F] { requests <- msg.asStream[HelloRequest] responses <- Ok(handler.sayHellosAll(requests).map(_.asJson)) } yield responses + } +} + +class MonixGreeterRestService[F[_]: Effect](handler: MonixGreeter[F])( + implicit sc: monix.execution.Scheduler) + extends Http4sDsl[F] { + + import freestyle.rpc.http.RestService._ + import freestyle.rpc.http.Util._ + + private implicit val requestDecoder: EntityDecoder[F, HelloRequest] = jsonOf[F, HelloRequest] + + def service: HttpService[F] = HttpService[F] { + case msg @ POST -> Root / "sayHellos" => + for { + requests <- msg.asStream[HelloRequest] + observable = requests.toObservable + response <- Ok(handler.sayHellos(observable).map(_.asJson)) + } yield response + + case msg @ POST -> Root / "sayHelloAll" => + for { + request <- msg.as[HelloRequest] + responses <- Ok(handler.sayHelloAll(request).map(_.asJson).toStream) + } yield responses + + case msg @ POST -> Root / "sayHellosAll" => + for { + requests <- msg.asStream[HelloRequest] + obsResponses = handler.sayHellosAll(requests.toObservable).map(_.asJson) + responses <- Ok(obsResponses.toStream) + } yield responses } } -object GreeterRestService { +object RestService { implicit class RequestOps[F[_]: Sync](request: Request[F]) { + import cats.syntax.applicative._ + import io.circe.jawn.CirceSupportParser.facade + import jawnfs2._ + import _root_.jawn.ParseException + def asStream[A](implicit decoder: Decoder[A]): F[Stream[F, A]] = request.body.chunks.parseJsonStream .map(_.as[A]) diff --git a/modules/http/server/src/test/scala/GreeterRestTests.scala b/modules/http/server/src/test/scala/GreeterRestTests.scala index 6adc37ed1..637d96eac 100644 --- a/modules/http/server/src/test/scala/GreeterRestTests.scala +++ b/modules/http/server/src/test/scala/GreeterRestTests.scala @@ -22,6 +22,7 @@ import fs2.Stream import io.circe.Json import io.circe.generic.auto._ import io.circe.syntax._ +import monix.reactive.Observable import org.http4s._ import org.http4s.circe._ import org.http4s.client.UnexpectedStatus @@ -36,10 +37,25 @@ class GreeterRestTests extends RpcBaseTestSuite with BeforeAndAfter { val Hostname = "localhost" val Port = 8080 - val serviceUri: Uri = Uri.unsafeFromString(s"http://$Hostname:$Port") - val service: HttpService[IO] = new GreeterRestService[IO](new GreeterHandler[IO]).service + val serviceUri: Uri = Uri.unsafeFromString(s"http://$Hostname:$Port") + + val UnaryServicePrefix = "unary" + val Fs2ServicePrefix = "fs2" + val MonixServicePrefix = "monix" + + import monix.execution.Scheduler.Implicits.global + val unaryService: HttpService[IO] = + new UnaryGreeterRestService[IO](new UnaryGreeterHandler[IO]).service + val fs2Service: HttpService[IO] = new Fs2GreeterRestService[IO](new Fs2GreeterHandler[IO]).service + val monixService: HttpService[IO] = + new MonixGreeterRestService[IO](new MonixGreeterHandler[IO]).service + val server: BlazeBuilder[IO] = - BlazeBuilder[IO].bindHttp(Port, Hostname).mountService(service, "/") + BlazeBuilder[IO] + .bindHttp(Port, Hostname) + .mountService(unaryService, s"/$UnaryServicePrefix") + .mountService(fs2Service, s"/$Fs2ServicePrefix") + .mountService(monixService, s"/$MonixServicePrefix") var serverTask: Server[IO] = _ // sorry before(serverTask = server.start.unsafeRunSync()) @@ -48,7 +64,7 @@ class GreeterRestTests extends RpcBaseTestSuite with BeforeAndAfter { "REST Server" should { "serve a GET request" in { - val request = Request[IO](Method.GET, serviceUri / "getHello") + val request = Request[IO](Method.GET, serviceUri / UnaryServicePrefix / "getHello") val response = (for { client <- Http1Client[IO]() response <- client.expect[Json](request) @@ -57,7 +73,7 @@ class GreeterRestTests extends RpcBaseTestSuite with BeforeAndAfter { } "serve a POST request" in { - val request = Request[IO](Method.POST, serviceUri / "sayHello") + val request = Request[IO](Method.POST, serviceUri / UnaryServicePrefix / "sayHello") val requestBody = HelloRequest("hey").asJson val response = (for { client <- Http1Client[IO]() @@ -67,7 +83,7 @@ class GreeterRestTests extends RpcBaseTestSuite with BeforeAndAfter { } "return a 400 Bad Request for a malformed unary POST request" in { - val request = Request[IO](Method.POST, serviceUri / "sayHello") + val request = Request[IO](Method.POST, serviceUri / UnaryServicePrefix / "sayHello") val requestBody = "hey" val responseError = the[UnexpectedStatus] thrownBy (for { client <- Http1Client[IO]() @@ -77,7 +93,7 @@ class GreeterRestTests extends RpcBaseTestSuite with BeforeAndAfter { } "return a 400 Bad Request for a malformed streaming POST request" in { - val request = Request[IO](Method.POST, serviceUri / "sayHellos") + val request = Request[IO](Method.POST, serviceUri / Fs2ServicePrefix / "sayHellos") val requestBody = "{" val responseError = the[UnexpectedStatus] thrownBy (for { client <- Http1Client[IO]() @@ -88,14 +104,19 @@ class GreeterRestTests extends RpcBaseTestSuite with BeforeAndAfter { } - val serviceClient: GreeterRestClient[IO] = new GreeterRestClient[IO](serviceUri) + val unaryServiceClient: UnaryGreeterRestClient[IO] = + new UnaryGreeterRestClient[IO](serviceUri / UnaryServicePrefix) + val fs2ServiceClient: Fs2GreeterRestClient[IO] = + new Fs2GreeterRestClient[IO](serviceUri / Fs2ServicePrefix) + val monixServiceClient: MonixGreeterRestClient[IO] = + new MonixGreeterRestClient[IO](serviceUri / MonixServicePrefix) "REST Service" should { "serve a GET request" in { val response = (for { client <- Http1Client[IO]() - response <- serviceClient.getHello()(client) + response <- unaryServiceClient.getHello()(client) } yield response).unsafeRunSync() response shouldBe HelloResponse("hey") } @@ -104,7 +125,7 @@ class GreeterRestTests extends RpcBaseTestSuite with BeforeAndAfter { val request = HelloRequest("hey") val response = (for { client <- Http1Client[IO]() - response <- serviceClient.sayHello(request)(client) + response <- unaryServiceClient.sayHello(request)(client) } yield response).unsafeRunSync() response shouldBe HelloResponse("hey") } @@ -113,7 +134,7 @@ class GreeterRestTests extends RpcBaseTestSuite with BeforeAndAfter { val requests = Stream(HelloRequest("hey"), HelloRequest("there")) val response = (for { client <- Http1Client[IO]() - response <- serviceClient.sayHellos(requests)(client) + response <- fs2ServiceClient.sayHellos(requests)(client) } yield response).unsafeRunSync() response shouldBe HelloResponse("hey, there") } @@ -122,28 +143,55 @@ class GreeterRestTests extends RpcBaseTestSuite with BeforeAndAfter { val requests = Stream.empty val response = (for { client <- Http1Client[IO]() - response <- serviceClient.sayHellos(requests)(client) + response <- fs2ServiceClient.sayHellos(requests)(client) } yield response).unsafeRunSync() response shouldBe HelloResponse("") } + "serve a POST request with Observable streaming request" in { + val requests = Observable(HelloRequest("hey"), HelloRequest("there")) + val response = (for { + client <- Http1Client[IO]() + response <- monixServiceClient.sayHellos(requests)(client) + } yield response).unsafeRunSync() + response shouldBe HelloResponse("hey, there") + } + "serve a POST request with fs2 streaming response" in { val request = HelloRequest("hey") val responses = (for { client <- Http1Client.stream[IO]() - response <- serviceClient.sayHelloAll(request)(client) + response <- fs2ServiceClient.sayHelloAll(request)(client) } yield response).compile.toList.unsafeRunSync() responses shouldBe List(HelloResponse("hey"), HelloResponse("hey")) } + "serve a POST request with Observable streaming response" in { + val request = HelloRequest("hey") + val responses = (for { + client <- Http1Client[IO]() + response <- monixServiceClient.sayHelloAll(request)(client).toListL.toIO + } yield response).unsafeRunSync() + responses shouldBe List(HelloResponse("hey"), HelloResponse("hey")) + } + "serve a POST request with bidirectional fs2 streaming" in { val requests = Stream(HelloRequest("hey"), HelloRequest("there")) val responses = (for { client <- Http1Client.stream[IO]() - response <- serviceClient.sayHellosAll(requests)(client) + response <- fs2ServiceClient.sayHellosAll(requests)(client) } yield response).compile.toList.unsafeRunSync() responses shouldBe List(HelloResponse("hey"), HelloResponse("there")) } + + "serve a POST request with bidirectional Observable streaming" in { + val requests = Observable(HelloRequest("hey"), HelloRequest("there")) + val responses = (for { + client <- Http1Client[IO]() + response <- monixServiceClient.sayHellosAll(requests)(client).toListL.toIO + } yield response).unsafeRunSync() + responses shouldBe List(HelloResponse("hey"), HelloResponse("there")) + } } } diff --git a/modules/http/server/src/test/scala/GreeterService.scala b/modules/http/server/src/test/scala/GreeterServices.scala similarity index 64% rename from modules/http/server/src/test/scala/GreeterService.scala rename to modules/http/server/src/test/scala/GreeterServices.scala index 905ea1163..af5c9b0b7 100644 --- a/modules/http/server/src/test/scala/GreeterService.scala +++ b/modules/http/server/src/test/scala/GreeterServices.scala @@ -17,19 +17,25 @@ package freestyle.rpc.http import freestyle.rpc.protocol._ -import fs2.Stream @message case class HelloRequest(hello: String) @message case class HelloResponse(hello: String) -@service trait Greeter[F[_]] { +// We don't actually need to split the various streaming types into their own services, +// but this allows for more specific dependencies and type constraints (Sync, Async, Effect...) in their implementations. + +@service trait UnaryGreeter[F[_]] { @rpc(Avro) def getHello(request: Empty.type): F[HelloResponse] @rpc(Avro) def sayHello(request: HelloRequest): F[HelloResponse] +} + +import fs2.Stream +@service trait Fs2Greeter[F[_]] { @rpc(Avro) @stream[RequestStreaming.type] def sayHellos(requests: Stream[F, HelloRequest]): F[HelloResponse] @@ -40,3 +46,16 @@ import fs2.Stream @rpc(Avro) @stream[BidirectionalStreaming.type] def sayHellosAll(requests: Stream[F, HelloRequest]): Stream[F, HelloResponse] } + +import monix.reactive.Observable +@service trait MonixGreeter[F[_]] { + + @rpc(Avro) @stream[RequestStreaming.type] + def sayHellos(requests: Observable[HelloRequest]): F[HelloResponse] + + @rpc(Avro) @stream[ResponseStreaming.type] + def sayHelloAll(request: HelloRequest): Observable[HelloResponse] + + @rpc(Avro) @stream[BidirectionalStreaming.type] + def sayHellosAll(requests: Observable[HelloRequest]): Observable[HelloResponse] +} diff --git a/modules/http/server/src/test/scala/Util.scala b/modules/http/server/src/test/scala/Util.scala new file mode 100644 index 000000000..ef3f93fea --- /dev/null +++ b/modules/http/server/src/test/scala/Util.scala @@ -0,0 +1,38 @@ +/* + * Copyright 2017-2018 47 Degrees, LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package freestyle.rpc.http + +import cats.effect.Effect +import fs2.Stream +import fs2.interop.reactivestreams._ +import monix.execution.Scheduler +import monix.reactive.Observable +import scala.concurrent.ExecutionContext + +object Util { + + implicit class Fs2StreamOps[F[_], A](stream: Stream[F, A]) { + def toObservable(implicit F: Effect[F], ec: ExecutionContext): Observable[A] = + Observable.fromReactivePublisher(stream.toUnicastPublisher) + } + + implicit class MonixStreamOps[A](stream: Observable[A]) { + def toStream[F[_]](implicit F: Effect[F], sc: Scheduler): Stream[F, A] = + stream.toReactivePublisher.toStream[F] + } + +} From 2a1cc42c54a0a5bf29c421a53c9de5cf9ba9c89c Mon Sep 17 00:00:00 2001 From: Lawrence Lavigne Date: Fri, 4 May 2018 08:11:11 -0700 Subject: [PATCH 04/35] Implemented error handling for unary and streaming REST services (#258) --- .../server/src/main/scala/HttpConfig.scala | 20 -- .../src/main/scala/HttpServerBuilder.scala | 31 --- .../src/main/scala/HttpServerStream.scala | 35 --- .../src/test/scala/GreeterHandlers.scala | 21 +- .../src/test/scala/GreeterRestClients.scala | 35 +-- .../src/test/scala/GreeterRestServices.scala | 66 ++---- .../src/test/scala/GreeterRestTests.scala | 222 +++++++++++++----- .../src/test/scala/GreeterServices.scala | 4 +- modules/http/server/src/test/scala/Util.scala | 38 --- .../http/server/src/test/scala/Utils.scala | 119 ++++++++++ .../server/src/test/scala/logback-test.xml | 11 + project/ProjectPlugin.scala | 5 +- 12 files changed, 346 insertions(+), 261 deletions(-) delete mode 100644 modules/http/server/src/main/scala/HttpConfig.scala delete mode 100644 modules/http/server/src/main/scala/HttpServerBuilder.scala delete mode 100644 modules/http/server/src/main/scala/HttpServerStream.scala delete mode 100644 modules/http/server/src/test/scala/Util.scala create mode 100644 modules/http/server/src/test/scala/Utils.scala create mode 100644 modules/http/server/src/test/scala/logback-test.xml diff --git a/modules/http/server/src/main/scala/HttpConfig.scala b/modules/http/server/src/main/scala/HttpConfig.scala deleted file mode 100644 index 93fabaa36..000000000 --- a/modules/http/server/src/main/scala/HttpConfig.scala +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright 2017-2018 47 Degrees, LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package freestyle.rpc.http -package server - -final case class HttpConfig(host: String, port: Int) diff --git a/modules/http/server/src/main/scala/HttpServerBuilder.scala b/modules/http/server/src/main/scala/HttpServerBuilder.scala deleted file mode 100644 index e399437a5..000000000 --- a/modules/http/server/src/main/scala/HttpServerBuilder.scala +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright 2017-2018 47 Degrees, LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package freestyle.rpc.http -package server - -import cats.effect.Effect -import monix.execution.Scheduler -import org.http4s.HttpService -import org.http4s.server.blaze.BlazeBuilder - -class HttpServerBuilder[F[_]: Effect](implicit C: HttpConfig, S: Scheduler) { - - def build(service: HttpService[F], prefix: String = "/"): BlazeBuilder[F] = - BlazeBuilder[F] - .bindHttp(C.port, C.host) - .mountService(service, prefix) -} diff --git a/modules/http/server/src/main/scala/HttpServerStream.scala b/modules/http/server/src/main/scala/HttpServerStream.scala deleted file mode 100644 index f71f7d9ba..000000000 --- a/modules/http/server/src/main/scala/HttpServerStream.scala +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright 2017-2018 47 Degrees, LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package freestyle.rpc.http -package server - -import cats.effect.Effect -import fs2.{Stream, StreamApp} -import monix.execution.Scheduler -import org.http4s.HttpService - -object HttpServerStream { - - def apply[F[_]: Effect](service: HttpService[F], prefix: String = "/")( - implicit C: HttpConfig, - S: Scheduler): Stream[F, StreamApp.ExitCode] = { - val httpServerBuilder: HttpServerBuilder[F] = new HttpServerBuilder[F] - - httpServerBuilder.build(service, prefix).serve - } - -} diff --git a/modules/http/server/src/test/scala/GreeterHandlers.scala b/modules/http/server/src/test/scala/GreeterHandlers.scala index 66e629d22..0454232c5 100644 --- a/modules/http/server/src/test/scala/GreeterHandlers.scala +++ b/modules/http/server/src/test/scala/GreeterHandlers.scala @@ -16,17 +16,26 @@ package freestyle.rpc.http -import cats.Applicative +import cats.{Applicative, MonadError} import cats.effect._ -class UnaryGreeterHandler[F[_]: Applicative] extends UnaryGreeter[F] { +class UnaryGreeterHandler[F[_]: Applicative](implicit F: MonadError[F, Throwable]) + extends UnaryGreeter[F] { import cats.syntax.applicative._ import freestyle.rpc.protocol.Empty + import io.grpc.Status._ def getHello(request: Empty.type): F[HelloResponse] = HelloResponse("hey").pure - def sayHello(request: HelloRequest): F[HelloResponse] = HelloResponse(request.hello).pure + def sayHello(request: HelloRequest): F[HelloResponse] = request.hello match { + case "SE" => F.raiseError(INVALID_ARGUMENT.withDescription("SE").asException) + case "SRE" => F.raiseError(INVALID_ARGUMENT.withDescription("SRE").asRuntimeException) + case "RTE" => F.raiseError(new IllegalArgumentException("RTE")) + case "TR" => throw new IllegalArgumentException("Thrown") + case other => HelloResponse(other).pure + } + } class Fs2GreeterHandler[F[_]: Sync] extends Fs2Greeter[F] { @@ -41,7 +50,8 @@ class Fs2GreeterHandler[F[_]: Sync] extends Fs2Greeter[F] { } def sayHelloAll(request: HelloRequest): Stream[F, HelloResponse] = - Stream(HelloResponse(request.hello), HelloResponse(request.hello)) + if (request.hello.isEmpty) Stream.raiseError(new IllegalArgumentException("empty greeting")) + else Stream(HelloResponse(request.hello), HelloResponse(request.hello)) def sayHellosAll(requests: Stream[F, HelloRequest]): Stream[F, HelloResponse] = requests.map(request => HelloResponse(request.hello)) @@ -63,7 +73,8 @@ class MonixGreeterHandler[F[_]: Async](implicit sc: monix.execution.Scheduler) .to[F] def sayHelloAll(request: HelloRequest): Observable[HelloResponse] = - Observable(HelloResponse(request.hello), HelloResponse(request.hello)) + if (request.hello.isEmpty) Observable.raiseError(new IllegalArgumentException("empty greeting")) + else Observable(HelloResponse(request.hello), HelloResponse(request.hello)) def sayHellosAll(requests: Observable[HelloRequest]): Observable[HelloResponse] = requests.map(request => HelloResponse(request.hello)) diff --git a/modules/http/server/src/test/scala/GreeterRestClients.scala b/modules/http/server/src/test/scala/GreeterRestClients.scala index f745bd1ce..2f3e09361 100644 --- a/modules/http/server/src/test/scala/GreeterRestClients.scala +++ b/modules/http/server/src/test/scala/GreeterRestClients.scala @@ -17,6 +17,7 @@ package freestyle.rpc.http import cats.effect._ +import freestyle.rpc.http.Utils._ import fs2.Stream import io.circe.generic.auto._ import io.circe.syntax._ @@ -25,31 +26,29 @@ import org.http4s.circe._ import org.http4s.client._ import org.http4s.dsl.io._ -class UnaryGreeterRestClient[F[_]: Effect](uri: Uri) { +class UnaryGreeterRestClient[F[_]: Sync](uri: Uri) { private implicit val responseDecoder: EntityDecoder[F, HelloResponse] = jsonOf[F, HelloResponse] def getHello()(implicit client: Client[F]): F[HelloResponse] = { val request = Request[F](Method.GET, uri / "getHello") - client.expect[HelloResponse](request) + client.expectOr[HelloResponse](request)(handleResponseError) } def sayHello(arg: HelloRequest)(implicit client: Client[F]): F[HelloResponse] = { val request = Request[F](Method.POST, uri / "sayHello") - client.expect[HelloResponse](request.withBody(arg.asJson)) + client.expectOr[HelloResponse](request.withBody(arg.asJson))(handleResponseError) } } -class Fs2GreeterRestClient[F[_]: Effect](uri: Uri) { - - import freestyle.rpc.http.RestClient._ +class Fs2GreeterRestClient[F[_]: Sync](uri: Uri) { private implicit val responseDecoder: EntityDecoder[F, HelloResponse] = jsonOf[F, HelloResponse] def sayHellos(arg: Stream[F, HelloRequest])(implicit client: Client[F]): F[HelloResponse] = { val request = Request[F](Method.POST, uri / "sayHellos") - client.expect[HelloResponse](request.withBody(arg.map(_.asJson))) + client.expectOr[HelloResponse](request.withBody(arg.map(_.asJson)))(handleResponseError) } def sayHelloAll(arg: HelloRequest)(implicit client: Client[F]): Stream[F, HelloResponse] = { @@ -67,15 +66,15 @@ class Fs2GreeterRestClient[F[_]: Effect](uri: Uri) { class MonixGreeterRestClient[F[_]: Effect](uri: Uri)(implicit sc: monix.execution.Scheduler) { - import freestyle.rpc.http.RestClient._ - import freestyle.rpc.http.Util._ + import freestyle.rpc.http.Utils._ import monix.reactive.Observable private implicit val responseDecoder: EntityDecoder[F, HelloResponse] = jsonOf[F, HelloResponse] def sayHellos(arg: Observable[HelloRequest])(implicit client: Client[F]): F[HelloResponse] = { val request = Request[F](Method.POST, uri / "sayHellos") - client.expect[HelloResponse](request.withBody(arg.toStream.map(_.asJson))) + client.expectOr[HelloResponse](request.withBody(arg.toFs2Stream.map(_.asJson)))( + handleResponseError) } def sayHelloAll(arg: HelloRequest)(implicit client: Client[F]): Observable[HelloResponse] = { @@ -87,22 +86,8 @@ class MonixGreeterRestClient[F[_]: Effect](uri: Uri)(implicit sc: monix.executio implicit client: Client[F]): Observable[HelloResponse] = { val request = Request[F](Method.POST, uri / "sayHellosAll") client - .streaming(request.withBody(arg.toStream.map(_.asJson)))(_.asStream[HelloResponse]) + .streaming(request.withBody(arg.toFs2Stream.map(_.asJson)))(_.asStream[HelloResponse]) .toObservable } } - -object RestClient { - - import io.circe.Decoder - import io.circe.jawn.CirceSupportParser.facade - import jawnfs2._ - - implicit class ResponseOps[F[_]](response: Response[F]) { - - def asStream[A](implicit decoder: Decoder[A]): Stream[F, A] = - if (response.status.code != 200) throw UnexpectedStatus(response.status) - else response.body.chunks.parseJsonStream.map(_.as[A]).rethrow - } -} diff --git a/modules/http/server/src/test/scala/GreeterRestServices.scala b/modules/http/server/src/test/scala/GreeterRestServices.scala index ee309b1a4..d70341415 100644 --- a/modules/http/server/src/test/scala/GreeterRestServices.scala +++ b/modules/http/server/src/test/scala/GreeterRestServices.scala @@ -16,18 +16,20 @@ package freestyle.rpc.http +import cats.MonadError import cats.effect._ import cats.syntax.flatMap._ import cats.syntax.functor._ -import fs2.Stream -import io.circe.Decoder +import freestyle.rpc.http.Utils._ import io.circe.generic.auto._ import io.circe.syntax._ import org.http4s._ import org.http4s.circe._ import org.http4s.dsl.Http4sDsl -class UnaryGreeterRestService[F[_]: Sync](handler: UnaryGreeter[F]) extends Http4sDsl[F] { +class UnaryGreeterRestService[F[_]: Sync](handler: UnaryGreeter[F])( + implicit F: MonadError[F, Throwable]) + extends Http4sDsl[F] { import freestyle.rpc.protocol.Empty @@ -40,36 +42,30 @@ class UnaryGreeterRestService[F[_]: Sync](handler: UnaryGreeter[F]) extends Http case msg @ POST -> Root / "sayHello" => for { request <- msg.as[HelloRequest] - response <- Ok(handler.sayHello(request).map(_.asJson)) + response <- Ok(handler.sayHello(request).map(_.asJson)).adaptErrors } yield response } } class Fs2GreeterRestService[F[_]: Sync](handler: Fs2Greeter[F]) extends Http4sDsl[F] { - import freestyle.rpc.http.RestService._ - private implicit val requestDecoder: EntityDecoder[F, HelloRequest] = jsonOf[F, HelloRequest] def service: HttpService[F] = HttpService[F] { case msg @ POST -> Root / "sayHellos" => - for { - requests <- msg.asStream[HelloRequest] - response <- Ok(handler.sayHellos(requests).map(_.asJson)) - } yield response + val requests = msg.asStream[HelloRequest] + Ok(handler.sayHellos(requests).map(_.asJson)) case msg @ POST -> Root / "sayHelloAll" => for { request <- msg.as[HelloRequest] - responses <- Ok(handler.sayHelloAll(request).map(_.asJson)) + responses <- Ok(handler.sayHelloAll(request).asJsonEither) } yield responses case msg @ POST -> Root / "sayHellosAll" => - for { - requests <- msg.asStream[HelloRequest] - responses <- Ok(handler.sayHellosAll(requests).map(_.asJson)) - } yield responses + val requests = msg.asStream[HelloRequest] + Ok(handler.sayHellosAll(requests).asJsonEither) } } @@ -77,52 +73,22 @@ class MonixGreeterRestService[F[_]: Effect](handler: MonixGreeter[F])( implicit sc: monix.execution.Scheduler) extends Http4sDsl[F] { - import freestyle.rpc.http.RestService._ - import freestyle.rpc.http.Util._ - private implicit val requestDecoder: EntityDecoder[F, HelloRequest] = jsonOf[F, HelloRequest] def service: HttpService[F] = HttpService[F] { case msg @ POST -> Root / "sayHellos" => - for { - requests <- msg.asStream[HelloRequest] - observable = requests.toObservable - response <- Ok(handler.sayHellos(observable).map(_.asJson)) - } yield response + val requests = msg.asStream[HelloRequest] + Ok(handler.sayHellos(requests.toObservable).map(_.asJson)) case msg @ POST -> Root / "sayHelloAll" => for { request <- msg.as[HelloRequest] - responses <- Ok(handler.sayHelloAll(request).map(_.asJson).toStream) + responses <- Ok(handler.sayHelloAll(request).toFs2Stream.asJsonEither) } yield responses case msg @ POST -> Root / "sayHellosAll" => - for { - requests <- msg.asStream[HelloRequest] - obsResponses = handler.sayHellosAll(requests.toObservable).map(_.asJson) - responses <- Ok(obsResponses.toStream) - } yield responses - } -} - -object RestService { - - implicit class RequestOps[F[_]: Sync](request: Request[F]) { - - import cats.syntax.applicative._ - import io.circe.jawn.CirceSupportParser.facade - import jawnfs2._ - import _root_.jawn.ParseException - - def asStream[A](implicit decoder: Decoder[A]): F[Stream[F, A]] = - request.body.chunks.parseJsonStream - .map(_.as[A]) - .handleErrorWith { - case ex: ParseException => - throw MalformedMessageBodyFailure(ex.getMessage, Some(ex)) // will return 400 instead of 500 - } - .rethrow - .pure + val requests = msg.asStream[HelloRequest] + Ok(handler.sayHellosAll(requests.toObservable).toFs2Stream.asJsonEither) } } diff --git a/modules/http/server/src/test/scala/GreeterRestTests.scala b/modules/http/server/src/test/scala/GreeterRestTests.scala index 637d96eac..e08899044 100644 --- a/modules/http/server/src/test/scala/GreeterRestTests.scala +++ b/modules/http/server/src/test/scala/GreeterRestTests.scala @@ -18,6 +18,7 @@ package freestyle.rpc.http import cats.effect.IO import freestyle.rpc.common.RpcBaseTestSuite +import freestyle.rpc.http.Utils._ import fs2.Stream import io.circe.Json import io.circe.generic.auto._ @@ -30,9 +31,15 @@ import org.http4s.client.blaze.Http1Client import org.http4s.dsl.io._ import org.http4s.server.Server import org.http4s.server.blaze.BlazeBuilder +import org.http4s.server.middleware.Logger import org.scalatest._ +import org.scalatest.prop.GeneratorDrivenPropertyChecks +import scala.concurrent.duration._ -class GreeterRestTests extends RpcBaseTestSuite with BeforeAndAfter { +class GreeterRestTests + extends RpcBaseTestSuite + with GeneratorDrivenPropertyChecks + with BeforeAndAfter { val Hostname = "localhost" val Port = 8080 @@ -44,15 +51,19 @@ class GreeterRestTests extends RpcBaseTestSuite with BeforeAndAfter { val MonixServicePrefix = "monix" import monix.execution.Scheduler.Implicits.global - val unaryService: HttpService[IO] = - new UnaryGreeterRestService[IO](new UnaryGreeterHandler[IO]).service - val fs2Service: HttpService[IO] = new Fs2GreeterRestService[IO](new Fs2GreeterHandler[IO]).service - val monixService: HttpService[IO] = - new MonixGreeterRestService[IO](new MonixGreeterHandler[IO]).service + + val unaryService: HttpService[IO] = Logger(logHeaders = true, logBody = true)( + new UnaryGreeterRestService[IO](new UnaryGreeterHandler[IO]).service) + val fs2Service: HttpService[IO] = Logger(logHeaders = true, logBody = true)( + new Fs2GreeterRestService[IO](new Fs2GreeterHandler[IO]).service) + val monixService: HttpService[IO] = Logger(logHeaders = true, logBody = true)( + new MonixGreeterRestService[IO](new MonixGreeterHandler[IO]).service) val server: BlazeBuilder[IO] = BlazeBuilder[IO] .bindHttp(Port, Hostname) + //.enableHttp2(true) + //.withSSLContext(GenericSSLContext.serverSSLContext) .mountService(unaryService, s"/$UnaryServicePrefix") .mountService(fs2Service, s"/$Fs2ServicePrefix") .mountService(monixService, s"/$MonixServicePrefix") @@ -65,41 +76,43 @@ class GreeterRestTests extends RpcBaseTestSuite with BeforeAndAfter { "serve a GET request" in { val request = Request[IO](Method.GET, serviceUri / UnaryServicePrefix / "getHello") - val response = (for { + val response = for { client <- Http1Client[IO]() response <- client.expect[Json](request) - } yield response).unsafeRunSync() - response shouldBe HelloResponse("hey").asJson + } yield response + response.unsafeRunSync() shouldBe HelloResponse("hey").asJson } "serve a POST request" in { val request = Request[IO](Method.POST, serviceUri / UnaryServicePrefix / "sayHello") val requestBody = HelloRequest("hey").asJson - val response = (for { + val response = for { client <- Http1Client[IO]() response <- client.expect[Json](request.withBody(requestBody)) - } yield response).unsafeRunSync() - response shouldBe HelloResponse("hey").asJson + } yield response + response.unsafeRunSync() shouldBe HelloResponse("hey").asJson } "return a 400 Bad Request for a malformed unary POST request" in { val request = Request[IO](Method.POST, serviceUri / UnaryServicePrefix / "sayHello") - val requestBody = "hey" - val responseError = the[UnexpectedStatus] thrownBy (for { + val requestBody = "{" + val response = for { client <- Http1Client[IO]() response <- client.expect[Json](request.withBody(requestBody)) - } yield response).unsafeRunSync() - responseError.status.code shouldBe 400 + } yield response + the[UnexpectedStatus] thrownBy response.unsafeRunSync() shouldBe UnexpectedStatus( + Status.BadRequest) } "return a 400 Bad Request for a malformed streaming POST request" in { val request = Request[IO](Method.POST, serviceUri / Fs2ServicePrefix / "sayHellos") val requestBody = "{" - val responseError = the[UnexpectedStatus] thrownBy (for { + val response = for { client <- Http1Client[IO]() response <- client.expect[Json](request.withBody(requestBody)) - } yield response).unsafeRunSync() - responseError.status.code shouldBe 400 + } yield response + the[UnexpectedStatus] thrownBy response.unsafeRunSync() shouldBe UnexpectedStatus( + Status.BadRequest) } } @@ -114,84 +127,185 @@ class GreeterRestTests extends RpcBaseTestSuite with BeforeAndAfter { "REST Service" should { "serve a GET request" in { - val response = (for { + val response = for { client <- Http1Client[IO]() response <- unaryServiceClient.getHello()(client) - } yield response).unsafeRunSync() - response shouldBe HelloResponse("hey") + } yield response + response.unsafeRunSync() shouldBe HelloResponse("hey") } "serve a unary POST request" in { val request = HelloRequest("hey") - val response = (for { + val response = for { + client <- Http1Client[IO]() + response <- unaryServiceClient.sayHello(request)(client) + } yield response + response.unsafeRunSync() shouldBe HelloResponse("hey") + } + + "handle a raised gRPC exception in a unary POST request" in { + val request = HelloRequest("SRE") + val response = for { + client <- Http1Client[IO]() + response <- unaryServiceClient.sayHello(request)(client) + } yield response + the[ResponseError] thrownBy response.unsafeRunSync() shouldBe + ResponseError(Status.BadRequest, Some("INVALID_ARGUMENT: SRE")) + } + + "handle a raised non-gRPC exception in a unary POST request" in { + val request = HelloRequest("RTE") + val response = for { + client <- Http1Client[IO]() + response <- unaryServiceClient.sayHello(request)(client) + } yield response + the[ResponseError] thrownBy response.unsafeRunSync() shouldBe + ResponseError(Status.InternalServerError, Some("RTE")) + } + + "handle a thrown exception in a unary POST request" in { + val request = HelloRequest("TR") + val response = for { client <- Http1Client[IO]() response <- unaryServiceClient.sayHello(request)(client) - } yield response).unsafeRunSync() - response shouldBe HelloResponse("hey") + } yield response + the[ResponseError] thrownBy response.unsafeRunSync() shouldBe + ResponseError(Status.InternalServerError) } "serve a POST request with fs2 streaming request" in { val requests = Stream(HelloRequest("hey"), HelloRequest("there")) - val response = (for { + val response = for { client <- Http1Client[IO]() response <- fs2ServiceClient.sayHellos(requests)(client) - } yield response).unsafeRunSync() - response shouldBe HelloResponse("hey, there") + } yield response + response.unsafeRunSync() shouldBe HelloResponse("hey, there") } "serve a POST request with empty fs2 streaming request" in { val requests = Stream.empty - val response = (for { + val response = for { client <- Http1Client[IO]() response <- fs2ServiceClient.sayHellos(requests)(client) - } yield response).unsafeRunSync() - response shouldBe HelloResponse("") + } yield response + response.unsafeRunSync() shouldBe HelloResponse("") } "serve a POST request with Observable streaming request" in { val requests = Observable(HelloRequest("hey"), HelloRequest("there")) - val response = (for { + val response = for { + client <- Http1Client[IO]() + response <- monixServiceClient.sayHellos(requests)(client) + } yield response + response.unsafeRunSync() shouldBe HelloResponse("hey, there") + } + + "serve a POST request with empty Observable streaming request" in { + val requests = Observable.empty + val response = for { client <- Http1Client[IO]() response <- monixServiceClient.sayHellos(requests)(client) - } yield response).unsafeRunSync() - response shouldBe HelloResponse("hey, there") + } yield response + response.unsafeRunSync() shouldBe HelloResponse("") } "serve a POST request with fs2 streaming response" in { val request = HelloRequest("hey") - val responses = (for { - client <- Http1Client.stream[IO]() - response <- fs2ServiceClient.sayHelloAll(request)(client) - } yield response).compile.toList.unsafeRunSync() - responses shouldBe List(HelloResponse("hey"), HelloResponse("hey")) + val responses = for { + client <- Http1Client.stream[IO]() + responses <- fs2ServiceClient.sayHelloAll(request)(client) + } yield responses + responses.compile.toList + .unsafeRunSync() shouldBe List(HelloResponse("hey"), HelloResponse("hey")) } "serve a POST request with Observable streaming response" in { val request = HelloRequest("hey") - val responses = (for { - client <- Http1Client[IO]() - response <- monixServiceClient.sayHelloAll(request)(client).toListL.toIO - } yield response).unsafeRunSync() - responses shouldBe List(HelloResponse("hey"), HelloResponse("hey")) + val responses = for { + client <- Http1Client.stream[IO]() + responses <- monixServiceClient.sayHelloAll(request)(client).toFs2Stream[IO] + } yield responses + responses.compile.toList + .unsafeRunTimed(10.seconds) + .getOrElse(sys.error("Stuck!")) shouldBe List(HelloResponse("hey"), HelloResponse("hey")) + } + + "handle errors with fs2 streaming response" in { + val request = HelloRequest("") + val responses = for { + client <- Http1Client.stream[IO]() + responses <- fs2ServiceClient.sayHelloAll(request)(client) + } yield responses + the[IllegalArgumentException] thrownBy responses.compile.toList + .unsafeRunSync() should have message "empty greeting" + } + + "handle errors with Observable streaming response" in { + val request = HelloRequest("") + val responses = for { + client <- Http1Client.stream[IO]() + responses <- monixServiceClient.sayHelloAll(request)(client).toFs2Stream[IO] + } yield responses + the[IllegalArgumentException] thrownBy responses.compile.toList + .unsafeRunTimed(10.seconds) + .getOrElse(sys.error("Stuck!")) should have message "empty greeting" } "serve a POST request with bidirectional fs2 streaming" in { val requests = Stream(HelloRequest("hey"), HelloRequest("there")) - val responses = (for { - client <- Http1Client.stream[IO]() - response <- fs2ServiceClient.sayHellosAll(requests)(client) - } yield response).compile.toList.unsafeRunSync() - responses shouldBe List(HelloResponse("hey"), HelloResponse("there")) + val responses = for { + client <- Http1Client.stream[IO]() + responses <- fs2ServiceClient.sayHellosAll(requests)(client) + } yield responses + responses.compile.toList + .unsafeRunSync() shouldBe List(HelloResponse("hey"), HelloResponse("there")) + } + + "serve an empty POST request with bidirectional fs2 streaming" in { + val requests = Stream.empty + val responses = for { + client <- Http1Client.stream[IO]() + responses <- fs2ServiceClient.sayHellosAll(requests)(client) + } yield responses + responses.compile.toList + .unsafeRunSync() shouldBe Nil } "serve a POST request with bidirectional Observable streaming" in { val requests = Observable(HelloRequest("hey"), HelloRequest("there")) - val responses = (for { - client <- Http1Client[IO]() - response <- monixServiceClient.sayHellosAll(requests)(client).toListL.toIO - } yield response).unsafeRunSync() - responses shouldBe List(HelloResponse("hey"), HelloResponse("there")) + val responses = for { + client <- Http1Client.stream[IO]() + responses <- monixServiceClient.sayHellosAll(requests)(client).toFs2Stream[IO] + } yield responses + responses.compile.toList + .unsafeRunTimed(10.seconds) + .getOrElse(sys.error("Stuck!")) shouldBe List(HelloResponse("hey"), HelloResponse("there")) + } + + "serve an empty POST request with bidirectional Observable streaming" in { + val requests = Observable.empty + val responses = for { + client <- Http1Client.stream[IO]() + responses <- monixServiceClient.sayHellosAll(requests)(client).toFs2Stream[IO] + } yield responses + responses.compile.toList + .unsafeRunTimed(10.seconds) + .getOrElse(sys.error("Stuck!")) shouldBe Nil + } + + "serve ScalaCheck-generated POST requests with bidirectional Observable streaming" in { + forAll { strings: List[String] => + val requests = Observable.fromIterable(strings.map(HelloRequest)) + val responses = for { + client <- Http1Client.stream[IO]() + responses <- monixServiceClient.sayHellosAll(requests)(client).toFs2Stream[IO] + } yield responses + responses.compile.toList + .unsafeRunTimed(10.seconds) + .getOrElse(sys.error("Stuck!")) shouldBe strings.map(HelloResponse) + } } + } } diff --git a/modules/http/server/src/test/scala/GreeterServices.scala b/modules/http/server/src/test/scala/GreeterServices.scala index af5c9b0b7..f3a69f7ea 100644 --- a/modules/http/server/src/test/scala/GreeterServices.scala +++ b/modules/http/server/src/test/scala/GreeterServices.scala @@ -18,9 +18,9 @@ package freestyle.rpc.http import freestyle.rpc.protocol._ -@message case class HelloRequest(hello: String) +@message final case class HelloRequest(hello: String) -@message case class HelloResponse(hello: String) +@message final case class HelloResponse(hello: String) // We don't actually need to split the various streaming types into their own services, // but this allows for more specific dependencies and type constraints (Sync, Async, Effect...) in their implementations. diff --git a/modules/http/server/src/test/scala/Util.scala b/modules/http/server/src/test/scala/Util.scala deleted file mode 100644 index ef3f93fea..000000000 --- a/modules/http/server/src/test/scala/Util.scala +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright 2017-2018 47 Degrees, LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package freestyle.rpc.http - -import cats.effect.Effect -import fs2.Stream -import fs2.interop.reactivestreams._ -import monix.execution.Scheduler -import monix.reactive.Observable -import scala.concurrent.ExecutionContext - -object Util { - - implicit class Fs2StreamOps[F[_], A](stream: Stream[F, A]) { - def toObservable(implicit F: Effect[F], ec: ExecutionContext): Observable[A] = - Observable.fromReactivePublisher(stream.toUnicastPublisher) - } - - implicit class MonixStreamOps[A](stream: Observable[A]) { - def toStream[F[_]](implicit F: Effect[F], sc: Scheduler): Stream[F, A] = - stream.toReactivePublisher.toStream[F] - } - -} diff --git a/modules/http/server/src/test/scala/Utils.scala b/modules/http/server/src/test/scala/Utils.scala new file mode 100644 index 000000000..fa8a056cb --- /dev/null +++ b/modules/http/server/src/test/scala/Utils.scala @@ -0,0 +1,119 @@ +/* + * Copyright 2017-2018 47 Degrees, LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package freestyle.rpc.http + +import cats.effect._ +import cats.implicits._ +import fs2.Stream +import fs2.interop.reactivestreams._ +import io.grpc.Status.Code._ +import jawn.ParseException +import io.circe._ +import io.circe.generic.auto._ +import io.circe.jawn.CirceSupportParser.facade +import io.circe.syntax._ +import io.grpc.{Status => _, _} +import jawnfs2._ +import monix.execution.Scheduler +import monix.reactive.Observable +import org.http4s._ +import org.http4s.dsl.Http4sDsl +import scala.concurrent.ExecutionContext +import scala.util.control.NoStackTrace + +object Utils { + + private[http] implicit class MessageOps[F[_]](message: Message[F]) { + + def jsonBodyAsStream[A](implicit decoder: Decoder[A]): Stream[F, A] = + message.body.chunks.parseJsonStream.map(_.as[A]).rethrow + } + + private[http] implicit class RequestOps[F[_]](request: Request[F]) { + + def asStream[A](implicit decoder: Decoder[A]): Stream[F, A] = + request + .jsonBodyAsStream[A] + .adaptError { // mimic behavior of MessageOps.as[T] in handling of parsing errors + case ex: ParseException => + MalformedMessageBodyFailure(ex.getMessage, Some(ex)) // will return 400 instead of 500 + } + } + + private[http] implicit class ResponseOps[F[_]](response: Response[F]) { + + implicit private val throwableDecoder: Decoder[Throwable] = + Decoder.decodeTuple2[String, String].map { + case (cls, msg) => + Class + .forName(cls) + .getConstructor(classOf[String]) + .newInstance(msg) + .asInstanceOf[Throwable] + } + + def asStream[A](implicit decoder: Decoder[A]): Stream[F, A] = + if (response.status.code != 200) Stream.raiseError(ResponseError(response.status)) + else response.jsonBodyAsStream[Either[Throwable, A]].rethrow + } + + private[http] implicit class Fs2StreamOps[F[_], A](stream: Stream[F, A]) { + + implicit private val throwableEncoder: Encoder[Throwable] = new Encoder[Throwable] { + def apply(ex: Throwable): Json = (ex.getClass.getName, ex.getMessage).asJson + } + + def asJsonEither(implicit encoder: Encoder[A]): Stream[F, Json] = stream.attempt.map(_.asJson) + + def toObservable(implicit F: Effect[F], ec: ExecutionContext): Observable[A] = + Observable.fromReactivePublisher(stream.toUnicastPublisher) + } + + private[http] implicit class MonixStreamOps[A](stream: Observable[A]) { + + def toFs2Stream[F[_]](implicit F: Effect[F], sc: Scheduler): Stream[F, A] = + stream.toReactivePublisher.toStream[F] + } + + private[http] implicit class FResponseOps[F[_]: Sync](response: F[Response[F]]) + extends Http4sDsl[F] { + + def adaptErrors: F[Response[F]] = response.handleErrorWith { + case se: StatusException => errorFromStatus(se.getStatus, se.getMessage) + case sre: StatusRuntimeException => errorFromStatus(sre.getStatus, sre.getMessage) + case other: Throwable => InternalServerError(other.getMessage) + } + + private def errorFromStatus(status: io.grpc.Status, message: String): F[Response[F]] = + status.getCode match { + case INVALID_ARGUMENT => BadRequest(message) + case UNAUTHENTICATED => BadRequest(message) + case PERMISSION_DENIED => Forbidden(message) + case NOT_FOUND => NotFound(message) + case UNAVAILABLE => ServiceUnavailable(message) + case _ => InternalServerError(message) + } + } + + private[http] def handleResponseError[F[_]: Sync](errorResponse: Response[F]): F[Throwable] = + errorResponse.bodyAsText.compile.foldMonoid.map(body => + ResponseError(errorResponse.status, Some(body).filter(_.nonEmpty))) +} + +final case class ResponseError(status: Status, msg: Option[String] = None) + extends RuntimeException(status + msg.fold("")(": " + _)) + with NoStackTrace diff --git a/modules/http/server/src/test/scala/logback-test.xml b/modules/http/server/src/test/scala/logback-test.xml new file mode 100644 index 000000000..c3523a582 --- /dev/null +++ b/modules/http/server/src/test/scala/logback-test.xml @@ -0,0 +1,11 @@ + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + diff --git a/project/ProjectPlugin.scala b/project/ProjectPlugin.scala index e592096f0..00116bb8d 100644 --- a/project/ProjectPlugin.scala +++ b/project/ProjectPlugin.scala @@ -36,6 +36,7 @@ object ProjectPlugin extends AutoPlugin { val pbdirect: String = "0.1.0" val prometheus: String = "0.3.0" val monocle: String = "1.5.0-cats" + val scalacheck: String = "1.14.0" } lazy val commonSettings: Seq[Def.Setting[_]] = Seq( @@ -203,7 +204,9 @@ object ProjectPlugin extends AutoPlugin { %%("http4s-blaze-server", V.http4s), %%("http4s-circe", V.http4s), %%("circe-generic"), - %%("http4s-blaze-client", V.http4s) % Test + %%("http4s-blaze-client", V.http4s) % Test, + %%("scalacheck", V.scalacheck) % Test, + "ch.qos.logback" % "logback-classic" % "1.2.3" % Test ) ) From a74604e7a63243ef71a9493331c5a6bb46899ba1 Mon Sep 17 00:00:00 2001 From: Laurence Lavigne Date: Wed, 9 May 2018 13:41:01 -0700 Subject: [PATCH 05/35] Tentative fix for the hanging Monix-Observable tests --- modules/http/server/src/test/scala/Utils.scala | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/modules/http/server/src/test/scala/Utils.scala b/modules/http/server/src/test/scala/Utils.scala index fa8a056cb..7afff5cee 100644 --- a/modules/http/server/src/test/scala/Utils.scala +++ b/modules/http/server/src/test/scala/Utils.scala @@ -27,8 +27,9 @@ import io.circe.generic.auto._ import io.circe.jawn.CirceSupportParser.facade import io.circe.syntax._ import io.grpc.{Status => _, _} +import java.util.concurrent._ import jawnfs2._ -import monix.execution.Scheduler +import monix.execution._ import monix.reactive.Observable import org.http4s._ import org.http4s.dsl.Http4sDsl @@ -37,6 +38,9 @@ import scala.util.control.NoStackTrace object Utils { + private[http] val singleThreadedEC = + ExecutionContext.fromExecutorService(Executors.newSingleThreadExecutor) + private[http] implicit class MessageOps[F[_]](message: Message[F]) { def jsonBodyAsStream[A](implicit decoder: Decoder[A]): Stream[F, A] = @@ -86,7 +90,9 @@ object Utils { private[http] implicit class MonixStreamOps[A](stream: Observable[A]) { def toFs2Stream[F[_]](implicit F: Effect[F], sc: Scheduler): Stream[F, A] = - stream.toReactivePublisher.toStream[F] + stream.toReactivePublisher + .toStream[F]()(F, singleThreadedEC) + // singleThreadedEC, because tests intermittently hang on socket reads when using the default Monix Scheduler } private[http] implicit class FResponseOps[F[_]: Sync](response: F[Response[F]]) From 8caee87c4634d938b1a9c15f1525076b8d94bc4e Mon Sep 17 00:00:00 2001 From: Laurence Lavigne Date: Tue, 20 Nov 2018 22:55:13 -0800 Subject: [PATCH 06/35] Undo Single Abstract Method syntax to restore 2.11 compatibility --- modules/http/server/src/test/scala/Utils.scala | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/modules/http/server/src/test/scala/Utils.scala b/modules/http/server/src/test/scala/Utils.scala index 8c2555ca8..fe74d6a52 100644 --- a/modules/http/server/src/test/scala/Utils.scala +++ b/modules/http/server/src/test/scala/Utils.scala @@ -79,8 +79,9 @@ object Utils { private[http] implicit class Fs2StreamOps[F[_], A](stream: Stream[F, A]) { - implicit private val throwableEncoder: Encoder[Throwable] = - (ex: Throwable) => (ex.getClass.getName, ex.getMessage).asJson + implicit private val throwableEncoder: Encoder[Throwable] = new Encoder[Throwable] { + def apply(ex: Throwable): Json = (ex.getClass.getName, ex.getMessage).asJson + } def asJsonEither(implicit encoder: Encoder[A]): Stream[F, Json] = stream.attempt.map(_.asJson) From 98df299749d668509ef26994220b441b69e41c96 Mon Sep 17 00:00:00 2001 From: l-lavigne Date: Mon, 28 Jan 2019 13:25:38 -0800 Subject: [PATCH 07/35] Add auto-derived HTTP client implementation, move packages Note: Update of Monix/FS2 conversions is in progress with some outdated implementations removed, which fails several tests. --- build.sbt | 16 ++-- .../mu/rpc/protocol/protocol.scala | 35 +++++++- .../test/resources}/logback-test.xml | 0 .../mu/rpc/http}/GreeterHandlers.scala | 6 +- .../mu/rpc/http}/GreeterRestClients.scala | 8 +- .../mu/rpc/http}/GreeterRestServices.scala | 8 +- .../mu/rpc/http}/GreeterRestTests.scala | 27 +++++- .../mu/rpc/http}/GreeterServices.scala | 10 +-- .../higherkindness/mu/rpc/http}/Utils.scala | 10 +-- .../mu/rpc/internal/serviceImpl.scala | 90 ++++++++++++++++++- project/ProjectPlugin.scala | 6 +- 11 files changed, 174 insertions(+), 42 deletions(-) rename modules/http/{server/src/test/scala => src/test/resources}/logback-test.xml (100%) rename modules/http/{server/src/test/scala => src/test/scala/higherkindness/mu/rpc/http}/GreeterHandlers.scala (95%) rename modules/http/{server/src/test/scala => src/test/scala/higherkindness/mu/rpc/http}/GreeterRestClients.scala (94%) rename modules/http/{server/src/test/scala => src/test/scala/higherkindness/mu/rpc/http}/GreeterRestServices.scala (94%) rename modules/http/{server/src/test/scala => src/test/scala/higherkindness/mu/rpc/http}/GreeterRestTests.scala (92%) rename modules/http/{server/src/test/scala => src/test/scala/higherkindness/mu/rpc/http}/GreeterServices.scala (83%) rename modules/http/{server/src/test/scala => src/test/scala/higherkindness/mu/rpc/http}/Utils.scala (94%) diff --git a/build.sbt b/build.sbt index 9960d8f8a..b2b5901fc 100644 --- a/build.sbt +++ b/build.sbt @@ -177,15 +177,13 @@ lazy val `dropwizard-client` = project //// HTTP/REST //// /////////////////// -lazy val `http-server` = project - .in(file("modules/http/server")) +lazy val `http` = project + .in(file("modules/http")) .dependsOn(common % "compile->compile;test->test") - .dependsOn(internal) - .dependsOn(client % "test->test") - .dependsOn(server % "test->test") - .settings(moduleName := "frees-rpc-http-server") - .settings(rpcHttpServerSettings) - .disablePlugins(ScriptedPlugin) + .dependsOn(channel % "compile->compile;test->test") + .dependsOn(server % "compile->compile;test->test") + .settings(moduleName := "mu-rpc-http") + .settings(httpSettings) //////////////// //// IDLGEN //// @@ -403,7 +401,7 @@ lazy val allModules: Seq[ProjectReference] = Seq( testing, ssl, `idlgen-core`, - `http-server`, + `http`, `marshallers-jodatime`, `example-routeguide-protocol`, `example-routeguide-common`, diff --git a/modules/common/src/main/scala/higherkindness/mu/rpc/protocol/protocol.scala b/modules/common/src/main/scala/higherkindness/mu/rpc/protocol/protocol.scala index fb455d6e8..5753a5cf2 100644 --- a/modules/common/src/main/scala/higherkindness/mu/rpc/protocol/protocol.scala +++ b/modules/common/src/main/scala/higherkindness/mu/rpc/protocol/protocol.scala @@ -33,10 +33,37 @@ sealed abstract class CompressionType extends Product with Serializable case object Identity extends CompressionType case object Gzip extends CompressionType -class message extends StaticAnnotation -class option(name: String, value: Any) extends StaticAnnotation -class outputPackage(value: String) extends StaticAnnotation -class outputName(value: String) extends StaticAnnotation +class message extends StaticAnnotation +class option(name: String, value: Any) extends StaticAnnotation +class outputPackage(value: String) extends StaticAnnotation +class outputName(value: String) extends StaticAnnotation +class http(method: HttpMethod, uri: String) extends StaticAnnotation + +sealed trait HttpMethod extends Product with Serializable +case object OPTIONS extends HttpMethod +case object GET extends HttpMethod +case object HEAD extends HttpMethod +case object POST extends HttpMethod +case object PUT extends HttpMethod +case object DELETE extends HttpMethod +case object TRACE extends HttpMethod +case object CONNECT extends HttpMethod +case object PATCH extends HttpMethod + +object HttpMethod { + def fromString(str: String): Option[HttpMethod] = str match { + case "OPTIONS" => Some(OPTIONS) + case "GET" => Some(GET) + case "HEAD" => Some(HEAD) + case "POST" => Some(POST) + case "PUT" => Some(PUT) + case "DELETE" => Some(DELETE) + case "TRACE" => Some(TRACE) + case "CONNECT" => Some(CONNECT) + case "PATCH" => Some(PATCH) + case _ => None + } +} @message object Empty diff --git a/modules/http/server/src/test/scala/logback-test.xml b/modules/http/src/test/resources/logback-test.xml similarity index 100% rename from modules/http/server/src/test/scala/logback-test.xml rename to modules/http/src/test/resources/logback-test.xml diff --git a/modules/http/server/src/test/scala/GreeterHandlers.scala b/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterHandlers.scala similarity index 95% rename from modules/http/server/src/test/scala/GreeterHandlers.scala rename to modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterHandlers.scala index ef3bc3fad..5fa8ac058 100644 --- a/modules/http/server/src/test/scala/GreeterHandlers.scala +++ b/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterHandlers.scala @@ -1,5 +1,5 @@ /* - * Copyright 2017-2018 47 Degrees, LLC. + * Copyright 2017-2019 47 Degrees, LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package mu.rpc.http +package higherkindness.mu.rpc.http import cats.{Applicative, MonadError} import cats.effect._ @@ -23,7 +23,7 @@ class UnaryGreeterHandler[F[_]: Applicative](implicit F: MonadError[F, Throwable extends UnaryGreeter[F] { import cats.syntax.applicative._ - import mu.rpc.protocol.Empty + import higherkindness.mu.rpc.protocol.Empty import io.grpc.Status._ def getHello(request: Empty.type): F[HelloResponse] = HelloResponse("hey").pure diff --git a/modules/http/server/src/test/scala/GreeterRestClients.scala b/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterRestClients.scala similarity index 94% rename from modules/http/server/src/test/scala/GreeterRestClients.scala rename to modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterRestClients.scala index 633226f6d..7fef03316 100644 --- a/modules/http/server/src/test/scala/GreeterRestClients.scala +++ b/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterRestClients.scala @@ -1,5 +1,5 @@ /* - * Copyright 2017-2018 47 Degrees, LLC. + * Copyright 2017-2019 47 Degrees, LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,13 +14,13 @@ * limitations under the License. */ -package mu.rpc.http +package higherkindness.mu.rpc.http import cats.effect._ import fs2.Stream import io.circe.generic.auto._ import io.circe.syntax._ -import mu.rpc.http.Utils._ +import higherkindness.mu.rpc.http.Utils._ import org.http4s._ import org.http4s.circe._ import org.http4s.client._ @@ -68,7 +68,7 @@ class MonixGreeterRestClient[F[_]: ConcurrentEffect](uri: Uri)( implicit sc: monix.execution.Scheduler) { import monix.reactive.Observable - import mu.rpc.http.Utils._ + import higherkindness.mu.rpc.http.Utils._ private implicit val responseDecoder: EntityDecoder[F, HelloResponse] = jsonOf[F, HelloResponse] diff --git a/modules/http/server/src/test/scala/GreeterRestServices.scala b/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterRestServices.scala similarity index 94% rename from modules/http/server/src/test/scala/GreeterRestServices.scala rename to modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterRestServices.scala index 190b8d65f..49a51a2b8 100644 --- a/modules/http/server/src/test/scala/GreeterRestServices.scala +++ b/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterRestServices.scala @@ -1,5 +1,5 @@ /* - * Copyright 2017-2018 47 Degrees, LLC. + * Copyright 2017-2019 47 Degrees, LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package mu.rpc.http +package higherkindness.mu.rpc.http import cats.MonadError import cats.effect._ @@ -22,7 +22,7 @@ import cats.syntax.flatMap._ import cats.syntax.functor._ import io.circe.generic.auto._ import io.circe.syntax._ -import mu.rpc.http.Utils._ +import higherkindness.mu.rpc.http.Utils._ import org.http4s._ import org.http4s.circe._ import org.http4s.dsl.Http4sDsl @@ -31,7 +31,7 @@ class UnaryGreeterRestService[F[_]: Sync](handler: UnaryGreeter[F])( implicit F: MonadError[F, Throwable]) extends Http4sDsl[F] { - import mu.rpc.protocol.Empty + import higherkindness.mu.rpc.protocol.Empty private implicit val requestDecoder: EntityDecoder[F, HelloRequest] = jsonOf[F, HelloRequest] diff --git a/modules/http/server/src/test/scala/GreeterRestTests.scala b/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterRestTests.scala similarity index 92% rename from modules/http/server/src/test/scala/GreeterRestTests.scala rename to modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterRestTests.scala index e8b8360ad..267180a7e 100644 --- a/modules/http/server/src/test/scala/GreeterRestTests.scala +++ b/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterRestTests.scala @@ -1,5 +1,5 @@ /* - * Copyright 2017-2018 47 Degrees, LLC. + * Copyright 2017-2019 47 Degrees, LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,16 +14,17 @@ * limitations under the License. */ -package mu.rpc.http +package higherkindness.mu.rpc.http import cats.effect.{IO, _} import fs2.Stream +import higherkindness.mu.rpc.common.RpcBaseTestSuite +import higherkindness.mu.rpc.http.Utils._ +import higherkindness.mu.rpc.protocol.Empty import io.circe.Json import io.circe.generic.auto._ import io.circe.syntax._ import monix.reactive.Observable -import mu.rpc.common.RpcBaseTestSuite -import mu.rpc.http.Utils._ import org.http4s._ import org.http4s.circe._ import org.http4s.client.UnexpectedStatus @@ -33,6 +34,7 @@ import org.http4s.server._ import org.http4s.server.blaze._ import org.scalatest._ import org.scalatest.prop.GeneratorDrivenPropertyChecks + import scala.concurrent.duration._ class GreeterRestTests @@ -262,7 +264,24 @@ class GreeterRestTests .getOrElse(sys.error("Stuck!")) shouldBe strings.map(HelloResponse) } } + } + + "Auto-derived REST Client" should { + + "serve a GET request" in { + val client: UnaryGreeter.HttpClient[IO] = UnaryGreeter.httpClient[IO](serviceUri) + val response: IO[HelloResponse] = BlazeClientBuilder[IO](ec).resource.use(client.getHello(_)) + response.unsafeRunSync() shouldBe HelloResponse("hey") + } + + "serve a unary POST request" in { + val client: UnaryGreeter.HttpClient[IO] = UnaryGreeter.httpClient[IO](serviceUri) + val response: IO[HelloResponse] = + BlazeClientBuilder[IO](ec).resource.use(client.sayHello(HelloRequest("hey"))(_)) + response.unsafeRunSync() shouldBe HelloResponse("hey") + } + //TODO: more tests } } diff --git a/modules/http/server/src/test/scala/GreeterServices.scala b/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterServices.scala similarity index 83% rename from modules/http/server/src/test/scala/GreeterServices.scala rename to modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterServices.scala index c517bf7ec..f9d051a58 100644 --- a/modules/http/server/src/test/scala/GreeterServices.scala +++ b/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterServices.scala @@ -1,5 +1,5 @@ /* - * Copyright 2017-2018 47 Degrees, LLC. + * Copyright 2017-2019 47 Degrees, LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,9 +14,9 @@ * limitations under the License. */ -package mu.rpc.http +package higherkindness.mu.rpc.http -import mu.rpc.protocol._ +import higherkindness.mu.rpc.protocol._ @message final case class HelloRequest(hello: String) @@ -27,9 +27,9 @@ import mu.rpc.protocol._ @service(Avro) trait UnaryGreeter[F[_]] { - def getHello(request: Empty.type): F[HelloResponse] + @http(GET, "getHello") def getHello(request: Empty.type): F[HelloResponse] - def sayHello(request: HelloRequest): F[HelloResponse] + @http(POST, "sayHello") def sayHello(request: HelloRequest): F[HelloResponse] } import fs2.Stream diff --git a/modules/http/server/src/test/scala/Utils.scala b/modules/http/src/test/scala/higherkindness/mu/rpc/http/Utils.scala similarity index 94% rename from modules/http/server/src/test/scala/Utils.scala rename to modules/http/src/test/scala/higherkindness/mu/rpc/http/Utils.scala index fe74d6a52..8e0140eb5 100644 --- a/modules/http/server/src/test/scala/Utils.scala +++ b/modules/http/src/test/scala/higherkindness/mu/rpc/http/Utils.scala @@ -1,5 +1,5 @@ /* - * Copyright 2017-2018 47 Degrees, LLC. + * Copyright 2017-2019 47 Degrees, LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,12 +14,12 @@ * limitations under the License. */ -package mu.rpc.http +package higherkindness.mu.rpc.http import cats.ApplicativeError import cats.effect._ import cats.implicits._ -import fs2.interop.reactivestreams._ +//import fs2.interop.reactivestreams._ import fs2.{RaiseThrowable, Stream} import io.grpc.Status.Code._ import jawn.ParseException @@ -86,13 +86,13 @@ object Utils { def asJsonEither(implicit encoder: Encoder[A]): Stream[F, Json] = stream.attempt.map(_.asJson) def toObservable(implicit F: ConcurrentEffect[F], ec: ExecutionContext): Observable[A] = - Observable.fromReactivePublisher(stream.toUnicastPublisher) + Observable.fromReactivePublisher(???) //TODO stream.toUnicastPublisher) } private[http] implicit class MonixStreamOps[A](val stream: Observable[A]) extends AnyVal { def toFs2Stream[F[_]](implicit F: ConcurrentEffect[F], sc: Scheduler): Stream[F, A] = - stream.toReactivePublisher.toStream[F]() + ??? //TODO stream.toReactivePublisher.toStream[F]() } private[http] implicit class FResponseOps[F[_]: Sync](response: F[Response[F]]) diff --git a/modules/internal/src/main/scala/higherkindness/mu/rpc/internal/serviceImpl.scala b/modules/internal/src/main/scala/higherkindness/mu/rpc/internal/serviceImpl.scala index 2c1621082..d982bd82b 100644 --- a/modules/internal/src/main/scala/higherkindness/mu/rpc/internal/serviceImpl.scala +++ b/modules/internal/src/main/scala/higherkindness/mu/rpc/internal/serviceImpl.scala @@ -259,6 +259,13 @@ object serviceImpl { .getOrElse(if (params.isDefinedAt(pos)) params(pos).toString else default.getOrElse(sys.error(s"Missing annotation parameter $name"))) + private def findAnnotation(mods: Modifiers, name: String): Option[Tree] = + mods.annotations find { + case Apply(Select(New(Ident(TypeName(`name`))), _), _) => true + case Apply(Select(New(Select(_, TypeName(`name`))), _), _) => true + case _ => false + } + //todo: validate that the request and responses are case classes, if possible case class RpcRequest( methodName: TermName, @@ -379,6 +386,85 @@ object serviceImpl { q"($methodDescriptorName, $handler)" } } + + //---------- + // HTTP/REST + //---------- + //TODO: derive server as well + //TODO: move HTTP-related code to its own module (on last attempt this did not work) + + def requestExecution(responseType: Tree, methodResponseType: Tree): Tree = + methodResponseType match { + case tq"Observable[..$tpts]" => + q"Observable.fromReactivePublisher(client.streaming(request)(_.body.chunks.parseJsonStream.map(_.as[$responseType]).rethrow).toUnicastPublisher)" + case tq"Stream[$carrier, ..$tpts]" => + q"client.streaming(request)(_.body.chunks.parseJsonStream.map(_.as[$responseType]).rethrow)" + case tq"$carrier[..$tpts]" => + q"client.expect[$responseType](request)" + } + + val toHttpRequest: ((TermName, String, TermName, Tree, Tree, Tree)) => DefDef = { + case (method, path, name, requestType, responseType, methodResponseType) => + if (requestType.toString.endsWith("Empty.type")) q""" + def $name(implicit + client: _root_.org.http4s.client.Client[F] + ): $methodResponseType = { + implicit val responseDecoder: EntityDecoder[F, $responseType] = jsonOf[F, $responseType] + val request = Request[F](Method.$method, uri / ${path.replace("\"", "")}) + ${requestExecution(responseType, methodResponseType)} + }""" + else q""" + def $name(req: $requestType)(implicit + client: _root_.org.http4s.client.Client[F] + ): $methodResponseType = { + implicit val responseDecoder: EntityDecoder[F, $responseType] = jsonOf[F, $responseType] + val request = Request[F](Method.$method, uri / ${path.replace("\"", "")}).withEntity(req.asJson) + ${requestExecution(responseType, methodResponseType)} + }""" + } + + val httpRequests = (for { + d <- rpcDefs.collect { case x if findAnnotation(x.mods, "http").isDefined => x } + args <- findAnnotation(d.mods, "http").collect({ case Apply(_, args) => args }).toList + params <- d.vparamss + _ = require(params.length == 1, s"RPC call ${d.name} has more than one request parameter") + p <- params.headOption.toList + } yield { + val method = TermName(args(0).toString) // TODO: fix direct index access + val uri = args(1).toString // TODO: fix direct index access + + val responseType: Tree = d.tpt match { + case tq"Observable[..$tpts]" => tpts.head + case tq"Stream[$carrier, ..$tpts]" => tpts.head + case tq"$carrier[..$tpts]" => tpts.head + case _ => ??? + } + + (method, uri, d.name, p.tpt, responseType, d.tpt) + }).map(toHttpRequest) + + val HttpClient = TypeName("HttpClient") + val httpClientClass: ClassDef = q""" + class $HttpClient[$F_](uri: Uri)(implicit F: _root_.cats.effect.Effect[$F], ec: scala.concurrent.ExecutionContext) { + ..$httpRequests + }""" + + val httpClient: DefDef = q""" + def httpClient[$F_](uri: Uri) + (implicit F: _root_.cats.effect.Effect[$F], ec: scala.concurrent.ExecutionContext): $HttpClient[$F] = { + new $HttpClient[$F](uri) + }""" + + val http = if(httpRequests.isEmpty) Nil else List( + q"import _root_.higherkindness.mu.rpc.http.Utils._", + q"import _root_.org.http4s._", + q"import _root_.org.http4s.circe._", + q"import _root_.io.circe._", + q"import _root_.io.circe.generic.auto._", + q"import _root_.io.circe.syntax._", + httpClientClass, + httpClient + ) } val classAndMaybeCompanion = annottees.map(_.tree) @@ -419,8 +505,8 @@ object serviceImpl { service.client, service.clientFromChannel, service.unsafeClient, - service.unsafeClientFromChannel - ) + service.unsafeClientFromChannel, + ) ++ service.http ) ) List(serviceDef, enrichedCompanion) diff --git a/project/ProjectPlugin.scala b/project/ProjectPlugin.scala index 0e015c699..40fd124ad 100644 --- a/project/ProjectPlugin.scala +++ b/project/ProjectPlugin.scala @@ -125,14 +125,16 @@ object ProjectPlugin extends AutoPlugin { ) ) - lazy val rpcHttpServerSettings: Seq[Def.Setting[_]] = Seq( + lazy val httpSettings: Seq[Def.Setting[_]] = Seq( libraryDependencies ++= Seq( %%("http4s-dsl", V.http4s), %%("http4s-blaze-server", V.http4s), %%("http4s-circe", V.http4s), %%("circe-generic"), %%("http4s-blaze-client", V.http4s) % Test, - %%("scalacheck", V.scalacheck) % Test, + %%("scalacheck") % Test, + %%("monix", V.monix) % Test, + %%("scalamockScalatest") % Test, "ch.qos.logback" % "logback-classic" % "1.2.3" % Test ) ) From a216c638ef20a12ce8ecacd644bc108d9a4a3be6 Mon Sep 17 00:00:00 2001 From: l-lavigne Date: Mon, 28 Jan 2019 14:13:02 -0800 Subject: [PATCH 08/35] Fix Monix/FS2 conversions using updated dependency --- .../higherkindness/mu/rpc/http/Utils.scala | 6 ++--- .../mu/rpc/internal/serviceImpl.scala | 23 +++++++++++-------- project/ProjectPlugin.scala | 1 + 3 files changed, 17 insertions(+), 13 deletions(-) diff --git a/modules/http/src/test/scala/higherkindness/mu/rpc/http/Utils.scala b/modules/http/src/test/scala/higherkindness/mu/rpc/http/Utils.scala index 8e0140eb5..3b0041ea6 100644 --- a/modules/http/src/test/scala/higherkindness/mu/rpc/http/Utils.scala +++ b/modules/http/src/test/scala/higherkindness/mu/rpc/http/Utils.scala @@ -19,7 +19,7 @@ package higherkindness.mu.rpc.http import cats.ApplicativeError import cats.effect._ import cats.implicits._ -//import fs2.interop.reactivestreams._ +import fs2.interop.reactivestreams._ import fs2.{RaiseThrowable, Stream} import io.grpc.Status.Code._ import jawn.ParseException @@ -86,13 +86,13 @@ object Utils { def asJsonEither(implicit encoder: Encoder[A]): Stream[F, Json] = stream.attempt.map(_.asJson) def toObservable(implicit F: ConcurrentEffect[F], ec: ExecutionContext): Observable[A] = - Observable.fromReactivePublisher(???) //TODO stream.toUnicastPublisher) + Observable.fromReactivePublisher(stream.toUnicastPublisher) } private[http] implicit class MonixStreamOps[A](val stream: Observable[A]) extends AnyVal { def toFs2Stream[F[_]](implicit F: ConcurrentEffect[F], sc: Scheduler): Stream[F, A] = - ??? //TODO stream.toReactivePublisher.toStream[F]() + stream.toReactivePublisher.toStream[F]() } private[http] implicit class FResponseOps[F[_]: Sync](response: F[Response[F]]) diff --git a/modules/internal/src/main/scala/higherkindness/mu/rpc/internal/serviceImpl.scala b/modules/internal/src/main/scala/higherkindness/mu/rpc/internal/serviceImpl.scala index d982bd82b..7aa5f9fa6 100644 --- a/modules/internal/src/main/scala/higherkindness/mu/rpc/internal/serviceImpl.scala +++ b/modules/internal/src/main/scala/higherkindness/mu/rpc/internal/serviceImpl.scala @@ -455,16 +455,19 @@ object serviceImpl { new $HttpClient[$F](uri) }""" - val http = if(httpRequests.isEmpty) Nil else List( - q"import _root_.higherkindness.mu.rpc.http.Utils._", - q"import _root_.org.http4s._", - q"import _root_.org.http4s.circe._", - q"import _root_.io.circe._", - q"import _root_.io.circe.generic.auto._", - q"import _root_.io.circe.syntax._", - httpClientClass, - httpClient - ) + val http = + if (httpRequests.isEmpty) Nil + else + List( + q"import _root_.higherkindness.mu.rpc.http.Utils._", + q"import _root_.org.http4s._", + q"import _root_.org.http4s.circe._", + q"import _root_.io.circe._", + q"import _root_.io.circe.generic.auto._", + q"import _root_.io.circe.syntax._", + httpClientClass, + httpClient + ) } val classAndMaybeCompanion = annottees.map(_.tree) diff --git a/project/ProjectPlugin.scala b/project/ProjectPlugin.scala index 40fd124ad..da9d114d5 100644 --- a/project/ProjectPlugin.scala +++ b/project/ProjectPlugin.scala @@ -131,6 +131,7 @@ object ProjectPlugin extends AutoPlugin { %%("http4s-blaze-server", V.http4s), %%("http4s-circe", V.http4s), %%("circe-generic"), + "co.fs2" %% "fs2-reactive-streams" % V.reactiveStreams, %%("http4s-blaze-client", V.http4s) % Test, %%("scalacheck") % Test, %%("monix", V.monix) % Test, From 051519d5c614384ea139cf6d941678b97a21da30 Mon Sep 17 00:00:00 2001 From: rafaparadela Date: Mon, 18 Feb 2019 11:42:28 -0800 Subject: [PATCH 09/35] fixes Scala 2.11 compilation error --- .../scala/higherkindness/mu/rpc/internal/serviceImpl.scala | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/modules/internal/src/main/scala/higherkindness/mu/rpc/internal/serviceImpl.scala b/modules/internal/src/main/scala/higherkindness/mu/rpc/internal/serviceImpl.scala index 7aa5f9fa6..a7d170061 100644 --- a/modules/internal/src/main/scala/higherkindness/mu/rpc/internal/serviceImpl.scala +++ b/modules/internal/src/main/scala/higherkindness/mu/rpc/internal/serviceImpl.scala @@ -473,7 +473,7 @@ object serviceImpl { val classAndMaybeCompanion = annottees.map(_.tree) val result: List[Tree] = classAndMaybeCompanion.head match { case serviceDef: ClassDef - if serviceDef.mods.hasFlag(TRAIT) || serviceDef.mods.hasFlag(ABSTRACT) => + if serviceDef.mods.hasFlag(TRAIT) || serviceDef.mods.hasFlag(ABSTRACT) => { val service = new RpcService(serviceDef) val companion: ModuleDef = classAndMaybeCompanion.lastOption match { case Some(obj: ModuleDef) => obj @@ -508,11 +508,13 @@ object serviceImpl { service.client, service.clientFromChannel, service.unsafeClient, - service.unsafeClientFromChannel, + service.unsafeClientFromChannel ) ++ service.http ) ) + List(serviceDef, enrichedCompanion) + } case _ => sys.error("@service-annotated definition must be a trait or abstract class") } c.Expr(Block(result, Literal(Constant(())))) From 8eb36b40b40dfa725dd444e7addea6cb2012f2e4 Mon Sep 17 00:00:00 2001 From: rafaparadela Date: Wed, 20 Feb 2019 14:43:51 -0800 Subject: [PATCH 10/35] fixes unit tests to prove http client derivation --- .../mu/rpc/http/GreeterRestTests.scala | 8 +++++--- .../mu/rpc/internal/serviceImpl.scala | 5 ++++- .../client/BaseMonitorClientInterceptorTests.scala | 13 +++++++------ 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterRestTests.scala b/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterRestTests.scala index 267180a7e..3ff58f37a 100644 --- a/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterRestTests.scala +++ b/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterRestTests.scala @@ -269,13 +269,15 @@ class GreeterRestTests "Auto-derived REST Client" should { "serve a GET request" in { - val client: UnaryGreeter.HttpClient[IO] = UnaryGreeter.httpClient[IO](serviceUri) - val response: IO[HelloResponse] = BlazeClientBuilder[IO](ec).resource.use(client.getHello(_)) + val client: UnaryGreeter.HttpClient[IO] = + UnaryGreeter.httpClient[IO](serviceUri / UnaryServicePrefix) + val response: IO[HelloResponse] = BlazeClientBuilder[IO](ec).resource.use(client.getHello(_)) response.unsafeRunSync() shouldBe HelloResponse("hey") } "serve a unary POST request" in { - val client: UnaryGreeter.HttpClient[IO] = UnaryGreeter.httpClient[IO](serviceUri) + val client: UnaryGreeter.HttpClient[IO] = + UnaryGreeter.httpClient[IO](serviceUri / UnaryServicePrefix) val response: IO[HelloResponse] = BlazeClientBuilder[IO](ec).resource.use(client.sayHello(HelloRequest("hey"))(_)) response.unsafeRunSync() shouldBe HelloResponse("hey") diff --git a/modules/internal/src/main/scala/higherkindness/mu/rpc/internal/serviceImpl.scala b/modules/internal/src/main/scala/higherkindness/mu/rpc/internal/serviceImpl.scala index a7d170061..8a7ef4a65 100644 --- a/modules/internal/src/main/scala/higherkindness/mu/rpc/internal/serviceImpl.scala +++ b/modules/internal/src/main/scala/higherkindness/mu/rpc/internal/serviceImpl.scala @@ -512,7 +512,10 @@ object serviceImpl { ) ++ service.http ) ) - + if (service.httpRequests.nonEmpty) { + println("#######################") + println(enrichedCompanion.toString) + } List(serviceDef, enrichedCompanion) } case _ => sys.error("@service-annotated definition must be a trait or abstract class") diff --git a/modules/prometheus/client/src/test/scala/higherkindness/mu/rpc/prometheus/client/BaseMonitorClientInterceptorTests.scala b/modules/prometheus/client/src/test/scala/higherkindness/mu/rpc/prometheus/client/BaseMonitorClientInterceptorTests.scala index c390205c6..4f6c15691 100644 --- a/modules/prometheus/client/src/test/scala/higherkindness/mu/rpc/prometheus/client/BaseMonitorClientInterceptorTests.scala +++ b/modules/prometheus/client/src/test/scala/higherkindness/mu/rpc/prometheus/client/BaseMonitorClientInterceptorTests.scala @@ -327,12 +327,13 @@ abstract class BaseMonitorClientInterceptorTests extends RpcBaseTestSuite { import clientRuntime._ (for { - _ <- serverStart[ConcurrentMonad] - _ <- unary[ConcurrentMonad] - _ <- clientStreaming[ConcurrentMonad] - assertion <- check - _ <- serverStop[ConcurrentMonad] - } yield assertion).unsafeRunSync() + _ <- serverStart[ConcurrentMonad] + _ <- unary[ConcurrentMonad] + _ <- clientStreaming[ConcurrentMonad] + _ <- serverStop[ConcurrentMonad] + } yield ()).unsafeRunSync() + + check.unsafeRunSync() } From c172539354b058c276fbc83633ffec0107afb135 Mon Sep 17 00:00:00 2001 From: rafaparadela Date: Wed, 20 Feb 2019 14:44:50 -0800 Subject: [PATCH 11/35] removes some println --- .../scala/higherkindness/mu/rpc/internal/serviceImpl.scala | 4 ---- 1 file changed, 4 deletions(-) diff --git a/modules/internal/src/main/scala/higherkindness/mu/rpc/internal/serviceImpl.scala b/modules/internal/src/main/scala/higherkindness/mu/rpc/internal/serviceImpl.scala index 8a7ef4a65..c5b531b0d 100644 --- a/modules/internal/src/main/scala/higherkindness/mu/rpc/internal/serviceImpl.scala +++ b/modules/internal/src/main/scala/higherkindness/mu/rpc/internal/serviceImpl.scala @@ -512,10 +512,6 @@ object serviceImpl { ) ++ service.http ) ) - if (service.httpRequests.nonEmpty) { - println("#######################") - println(enrichedCompanion.toString) - } List(serviceDef, enrichedCompanion) } case _ => sys.error("@service-annotated definition must be a trait or abstract class") From 20aae64c45a3f8174dfc37ae6c951b8d3fc68bf5 Mon Sep 17 00:00:00 2001 From: rafaparadela Date: Wed, 20 Feb 2019 17:51:22 -0800 Subject: [PATCH 12/35] adds more tests --- .../mu/rpc/http/GreeterRestTests.scala | 26 ++++++++++++++++++- .../mu/rpc/internal/serviceImpl.scala | 4 +++ .../BaseMonitorClientInterceptorTests.scala | 2 -- 3 files changed, 29 insertions(+), 3 deletions(-) diff --git a/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterRestTests.scala b/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterRestTests.scala index 3ff58f37a..cfea7f8b4 100644 --- a/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterRestTests.scala +++ b/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterRestTests.scala @@ -283,7 +283,31 @@ class GreeterRestTests response.unsafeRunSync() shouldBe HelloResponse("hey") } - //TODO: more tests + "handle a raised gRPC exception in a unary POST request" in { + + val client: UnaryGreeter.HttpClient[IO] = + UnaryGreeter.httpClient[IO](serviceUri / UnaryServicePrefix) + + val response: IO[HelloResponse] = + BlazeClientBuilder[IO](ec).resource.use(client.sayHello(HelloRequest("SRE"))(_)) + + the[UnexpectedStatus] thrownBy response.unsafeRunSync() shouldBe UnexpectedStatus( + Status.BadRequest) + } + + "handle a raised non-gRPC exception in a unary POST request" in { + + val client: UnaryGreeter.HttpClient[IO] = + UnaryGreeter.httpClient[IO](serviceUri / UnaryServicePrefix) + + val response: IO[HelloResponse] = + BlazeClientBuilder[IO](ec).resource.use(client.sayHello(HelloRequest("RTE"))(_)) + + the[ResponseError] thrownBy response.unsafeRunSync() shouldBe ResponseError( + Status.InternalServerError, + Some("RTE")) + } + } } diff --git a/modules/internal/src/main/scala/higherkindness/mu/rpc/internal/serviceImpl.scala b/modules/internal/src/main/scala/higherkindness/mu/rpc/internal/serviceImpl.scala index c5b531b0d..8a7ef4a65 100644 --- a/modules/internal/src/main/scala/higherkindness/mu/rpc/internal/serviceImpl.scala +++ b/modules/internal/src/main/scala/higherkindness/mu/rpc/internal/serviceImpl.scala @@ -512,6 +512,10 @@ object serviceImpl { ) ++ service.http ) ) + if (service.httpRequests.nonEmpty) { + println("#######################") + println(enrichedCompanion.toString) + } List(serviceDef, enrichedCompanion) } case _ => sys.error("@service-annotated definition must be a trait or abstract class") diff --git a/modules/prometheus/client/src/test/scala/higherkindness/mu/rpc/prometheus/client/BaseMonitorClientInterceptorTests.scala b/modules/prometheus/client/src/test/scala/higherkindness/mu/rpc/prometheus/client/BaseMonitorClientInterceptorTests.scala index 4f6c15691..758a04206 100644 --- a/modules/prometheus/client/src/test/scala/higherkindness/mu/rpc/prometheus/client/BaseMonitorClientInterceptorTests.scala +++ b/modules/prometheus/client/src/test/scala/higherkindness/mu/rpc/prometheus/client/BaseMonitorClientInterceptorTests.scala @@ -307,8 +307,6 @@ abstract class BaseMonitorClientInterceptorTests extends RpcBaseTestSuite { "work when combining multiple calls" in { - ignoreOnTravis("TODO: restore once https://github.com/higherkindness/mu/issues/168 is fixed") - def unary[F[_]](implicit APP: MyRPCClient[F]): F[C] = APP.u(a1.x, a1.y) From 15abe94f656d88094b07c0a845cd1b11621c25db Mon Sep 17 00:00:00 2001 From: rafaparadela Date: Thu, 21 Feb 2019 16:17:16 -0800 Subject: [PATCH 13/35] removes the macro params that can be inferred --- .../mu/rpc/protocol/protocol.scala | 10 +- .../mu/rpc/http/GreeterRestTests.scala | 146 ++++++++++++++++-- .../mu/rpc/http/GreeterServices.scala | 6 +- .../mu/rpc/internal/serviceImpl.scala | 13 +- 4 files changed, 148 insertions(+), 27 deletions(-) diff --git a/modules/common/src/main/scala/higherkindness/mu/rpc/protocol/protocol.scala b/modules/common/src/main/scala/higherkindness/mu/rpc/protocol/protocol.scala index 5753a5cf2..341ba3c6e 100644 --- a/modules/common/src/main/scala/higherkindness/mu/rpc/protocol/protocol.scala +++ b/modules/common/src/main/scala/higherkindness/mu/rpc/protocol/protocol.scala @@ -33,11 +33,11 @@ sealed abstract class CompressionType extends Product with Serializable case object Identity extends CompressionType case object Gzip extends CompressionType -class message extends StaticAnnotation -class option(name: String, value: Any) extends StaticAnnotation -class outputPackage(value: String) extends StaticAnnotation -class outputName(value: String) extends StaticAnnotation -class http(method: HttpMethod, uri: String) extends StaticAnnotation +class message extends StaticAnnotation +class option(name: String, value: Any) extends StaticAnnotation +class outputPackage(value: String) extends StaticAnnotation +class outputName(value: String) extends StaticAnnotation +class http extends StaticAnnotation sealed trait HttpMethod extends Product with Serializable case object OPTIONS extends HttpMethod diff --git a/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterRestTests.scala b/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterRestTests.scala index cfea7f8b4..9fd006b5c 100644 --- a/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterRestTests.scala +++ b/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterRestTests.scala @@ -268,46 +268,158 @@ class GreeterRestTests "Auto-derived REST Client" should { + val unaryClient: UnaryGreeter.HttpClient[IO] = + UnaryGreeter.httpClient[IO](serviceUri / UnaryServicePrefix) + "serve a GET request" in { - val client: UnaryGreeter.HttpClient[IO] = - UnaryGreeter.httpClient[IO](serviceUri / UnaryServicePrefix) - val response: IO[HelloResponse] = BlazeClientBuilder[IO](ec).resource.use(client.getHello(_)) + val response: IO[HelloResponse] = + BlazeClientBuilder[IO](ec).resource.use(unaryClient.getHello(_)) response.unsafeRunSync() shouldBe HelloResponse("hey") } "serve a unary POST request" in { - val client: UnaryGreeter.HttpClient[IO] = - UnaryGreeter.httpClient[IO](serviceUri / UnaryServicePrefix) val response: IO[HelloResponse] = - BlazeClientBuilder[IO](ec).resource.use(client.sayHello(HelloRequest("hey"))(_)) + BlazeClientBuilder[IO](ec).resource.use(unaryClient.sayHello(HelloRequest("hey"))(_)) response.unsafeRunSync() shouldBe HelloResponse("hey") } "handle a raised gRPC exception in a unary POST request" in { - - val client: UnaryGreeter.HttpClient[IO] = - UnaryGreeter.httpClient[IO](serviceUri / UnaryServicePrefix) - val response: IO[HelloResponse] = - BlazeClientBuilder[IO](ec).resource.use(client.sayHello(HelloRequest("SRE"))(_)) + BlazeClientBuilder[IO](ec).resource.use(unaryClient.sayHello(HelloRequest("SRE"))(_)) the[UnexpectedStatus] thrownBy response.unsafeRunSync() shouldBe UnexpectedStatus( Status.BadRequest) } "handle a raised non-gRPC exception in a unary POST request" in { + val response: IO[HelloResponse] = + BlazeClientBuilder[IO](ec).resource.use(unaryClient.sayHello(HelloRequest("RTE"))(_)) - val client: UnaryGreeter.HttpClient[IO] = - UnaryGreeter.httpClient[IO](serviceUri / UnaryServicePrefix) + the[UnexpectedStatus] thrownBy response.unsafeRunSync() shouldBe UnexpectedStatus( + Status.InternalServerError) + } + "handle a thrown exception in a unary POST request" in { val response: IO[HelloResponse] = - BlazeClientBuilder[IO](ec).resource.use(client.sayHello(HelloRequest("RTE"))(_)) + BlazeClientBuilder[IO](ec).resource.use(unaryClient.sayHello(HelloRequest("TR"))(_)) - the[ResponseError] thrownBy response.unsafeRunSync() shouldBe ResponseError( - Status.InternalServerError, - Some("RTE")) + the[UnexpectedStatus] thrownBy response.unsafeRunSync() shouldBe UnexpectedStatus( + Status.InternalServerError) } +// "serve a POST request with fs2 streaming request" in { +// +// val fs2Client = Fs2Greeter.httpClient[IO](serviceUri / UnaryServicePrefix) +// +// val requests = Stream(HelloRequest("hey"), HelloRequest("there")) +//// val response = +//// BlazeClientBuilder[IO](ec).resource.use(fs2ServiceClient.sayHellos(requests)(_)) +//// response.unsafeRunSync() shouldBe HelloResponse("hey, there") +// +// val response: IO[HelloResponse] = +// BlazeClientBuilder[IO](ec).resource.use(fs2Client.sayHellos(requests)(_)) +// response.unsafeRunSync() shouldBe HelloResponse("hey, there") +// } +// +// "serve a POST request with empty fs2 streaming request" in { +// val requests = Stream.empty +// val response = +// BlazeClientBuilder[IO](ec).resource.use(fs2ServiceClient.sayHellos(requests)(_)) +// response.unsafeRunSync() shouldBe HelloResponse("") +// } +// +// "serve a POST request with Observable streaming request" in { +// val requests = Observable(HelloRequest("hey"), HelloRequest("there")) +// val response = +// BlazeClientBuilder[IO](ec).resource.use(monixServiceClient.sayHellos(requests)(_)) +// response.unsafeRunSync() shouldBe HelloResponse("hey, there") +// } +// +// "serve a POST request with empty Observable streaming request" in { +// val requests = Observable.empty +// val response = +// BlazeClientBuilder[IO](ec).resource.use(monixServiceClient.sayHellos(requests)(_)) +// response.unsafeRunSync() shouldBe HelloResponse("") +// } +// +// "serve a POST request with fs2 streaming response" in { +// val request = HelloRequest("hey") +// val responses = +// BlazeClientBuilder[IO](ec).stream.flatMap(fs2ServiceClient.sayHelloAll(request)(_)) +// responses.compile.toList +// .unsafeRunSync() shouldBe List(HelloResponse("hey"), HelloResponse("hey")) +// } +// +// "serve a POST request with Observable streaming response" in { +// val request = HelloRequest("hey") +// val responses = BlazeClientBuilder[IO](ec).stream +// .flatMap(monixServiceClient.sayHelloAll(request)(_).toFs2Stream[IO]) +// responses.compile.toList +// .unsafeRunTimed(10.seconds) +// .getOrElse(sys.error("Stuck!")) shouldBe List(HelloResponse("hey"), HelloResponse("hey")) +// } +// +// "handle errors with fs2 streaming response" in { +// val request = HelloRequest("") +// val responses = +// BlazeClientBuilder[IO](ec).stream.flatMap(fs2ServiceClient.sayHelloAll(request)(_)) +// the[IllegalArgumentException] thrownBy responses.compile.toList +// .unsafeRunSync() should have message "empty greeting" +// } +// +// "handle errors with Observable streaming response" in { +// val request = HelloRequest("") +// val responses = BlazeClientBuilder[IO](ec).stream +// .flatMap(monixServiceClient.sayHelloAll(request)(_).toFs2Stream[IO]) +// the[IllegalArgumentException] thrownBy responses.compile.toList +// .unsafeRunTimed(10.seconds) +// .getOrElse(sys.error("Stuck!")) should have message "empty greeting" +// } +// +// "serve a POST request with bidirectional fs2 streaming" in { +// val requests = Stream(HelloRequest("hey"), HelloRequest("there")) +// val responses = +// BlazeClientBuilder[IO](ec).stream.flatMap(fs2ServiceClient.sayHellosAll(requests)(_)) +// responses.compile.toList +// .unsafeRunSync() shouldBe List(HelloResponse("hey"), HelloResponse("there")) +// } +// +// "serve an empty POST request with bidirectional fs2 streaming" in { +// val requests = Stream.empty +// val responses = +// BlazeClientBuilder[IO](ec).stream.flatMap(fs2ServiceClient.sayHellosAll(requests)(_)) +// responses.compile.toList.unsafeRunSync() shouldBe Nil +// } +// +// "serve a POST request with bidirectional Observable streaming" in { +// val requests = Observable(HelloRequest("hey"), HelloRequest("there")) +// val responses = BlazeClientBuilder[IO](ec).stream +// .flatMap(monixServiceClient.sayHellosAll(requests)(_).toFs2Stream[IO]) +// responses.compile.toList +// .unsafeRunTimed(10.seconds) +// .getOrElse(sys.error("Stuck!")) shouldBe List(HelloResponse("hey"), HelloResponse("there")) +// } +// +// "serve an empty POST request with bidirectional Observable streaming" in { +// val requests = Observable.empty +// val responses = BlazeClientBuilder[IO](ec).stream +// .flatMap(monixServiceClient.sayHellosAll(requests)(_).toFs2Stream[IO]) +// responses.compile.toList +// .unsafeRunTimed(10.seconds) +// .getOrElse(sys.error("Stuck!")) shouldBe Nil +// } +// +// "serve ScalaCheck-generated POST requests with bidirectional Observable streaming" in { +// forAll { strings: List[String] => +// val requests = Observable.fromIterable(strings.map(HelloRequest)) +// val responses = BlazeClientBuilder[IO](ec).stream +// .flatMap(monixServiceClient.sayHellosAll(requests)(_).toFs2Stream[IO]) +// responses.compile.toList +// .unsafeRunTimed(10.seconds) +// .getOrElse(sys.error("Stuck!")) shouldBe strings.map(HelloResponse) +// } +// } + } } diff --git a/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterServices.scala b/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterServices.scala index f9d051a58..7e9bb0662 100644 --- a/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterServices.scala +++ b/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterServices.scala @@ -27,15 +27,15 @@ import higherkindness.mu.rpc.protocol._ @service(Avro) trait UnaryGreeter[F[_]] { - @http(GET, "getHello") def getHello(request: Empty.type): F[HelloResponse] + @http def getHello(request: Empty.type): F[HelloResponse] - @http(POST, "sayHello") def sayHello(request: HelloRequest): F[HelloResponse] + @http def sayHello(request: HelloRequest): F[HelloResponse] } import fs2.Stream @service(Avro) trait Fs2Greeter[F[_]] { - def sayHellos(requests: Stream[F, HelloRequest]): F[HelloResponse] + @http def sayHellos(requests: Stream[F, HelloRequest]): F[HelloResponse] def sayHelloAll(request: HelloRequest): Stream[F, HelloResponse] diff --git a/modules/internal/src/main/scala/higherkindness/mu/rpc/internal/serviceImpl.scala b/modules/internal/src/main/scala/higherkindness/mu/rpc/internal/serviceImpl.scala index 8a7ef4a65..00dafa18f 100644 --- a/modules/internal/src/main/scala/higherkindness/mu/rpc/internal/serviceImpl.scala +++ b/modules/internal/src/main/scala/higherkindness/mu/rpc/internal/serviceImpl.scala @@ -18,6 +18,8 @@ package higherkindness.mu.rpc package internal import higherkindness.mu.rpc.protocol._ + +import scala.reflect.api.Trees import scala.reflect.macros.blackbox // $COVERAGE-OFF$ @@ -430,8 +432,15 @@ object serviceImpl { _ = require(params.length == 1, s"RPC call ${d.name} has more than one request parameter") p <- params.headOption.toList } yield { - val method = TermName(args(0).toString) // TODO: fix direct index access - val uri = args(1).toString // TODO: fix direct index access + + val method: c.universe.TermName = p.tpt match { + case tq"Empty.type" => TermName("GET") + case _ => TermName("POST") + } + val uri = d.name.toString + +// val method: c.universe.TermName = TermName(args(0).toString) // TODO: fix direct index access +// val uri = args(1).toString // TODO: fix direct index access val responseType: Tree = d.tpt match { case tq"Observable[..$tpts]" => tpts.head From 5d5b726f890c65cf35c36a6cdc68465d8c023ee3 Mon Sep 17 00:00:00 2001 From: rafaparadela Date: Fri, 22 Feb 2019 00:14:59 -0800 Subject: [PATCH 14/35] builds the client according to the typology of the request --- .../mu/rpc/internal/serviceImpl.scala | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/modules/internal/src/main/scala/higherkindness/mu/rpc/internal/serviceImpl.scala b/modules/internal/src/main/scala/higherkindness/mu/rpc/internal/serviceImpl.scala index 00dafa18f..a7a021293 100644 --- a/modules/internal/src/main/scala/higherkindness/mu/rpc/internal/serviceImpl.scala +++ b/modules/internal/src/main/scala/higherkindness/mu/rpc/internal/serviceImpl.scala @@ -395,6 +395,8 @@ object serviceImpl { //TODO: derive server as well //TODO: move HTTP-related code to its own module (on last attempt this did not work) + def isEmpty(request: Tree): Boolean = request.toString.endsWith("Empty.type") + def requestExecution(responseType: Tree, methodResponseType: Tree): Tree = methodResponseType match { case tq"Observable[..$tpts]" => @@ -405,6 +407,14 @@ object serviceImpl { q"client.expect[$responseType](request)" } + + def requestTypology(method: TermName, path: String): Tree = + "" match { + case _ => + q"val request = Request[F](Method.$method, uri / ${path.replace("\"", "")}).withEntity(req.map(_.asJson))" + } + + val toHttpRequest: ((TermName, String, TermName, Tree, Tree, Tree)) => DefDef = { case (method, path, name, requestType, responseType, methodResponseType) => if (requestType.toString.endsWith("Empty.type")) q""" @@ -420,7 +430,7 @@ object serviceImpl { client: _root_.org.http4s.client.Client[F] ): $methodResponseType = { implicit val responseDecoder: EntityDecoder[F, $responseType] = jsonOf[F, $responseType] - val request = Request[F](Method.$method, uri / ${path.replace("\"", "")}).withEntity(req.asJson) + val request = Request[F](Method.$method, uri / ${path.replace("\"", "")}).withEntity(req.map(_.asJson)) ${requestExecution(responseType, methodResponseType)} }""" } @@ -433,10 +443,7 @@ object serviceImpl { p <- params.headOption.toList } yield { - val method: c.universe.TermName = p.tpt match { - case tq"Empty.type" => TermName("GET") - case _ => TermName("POST") - } + val method: c.universe.TermName = if(isEmpty(p.tpt)) TermName("GET") else TermName("POST") val uri = d.name.toString // val method: c.universe.TermName = TermName(args(0).toString) // TODO: fix direct index access From 683147ca51e89ab44217d5dfa1ee544f928de3d2 Mon Sep 17 00:00:00 2001 From: rafaparadela Date: Mon, 25 Feb 2019 16:29:21 -0800 Subject: [PATCH 15/35] fixes macro --- .../mu/rpc/internal/serviceImpl.scala | 138 +++++++++++++----- 1 file changed, 104 insertions(+), 34 deletions(-) diff --git a/modules/internal/src/main/scala/higherkindness/mu/rpc/internal/serviceImpl.scala b/modules/internal/src/main/scala/higherkindness/mu/rpc/internal/serviceImpl.scala index a7a021293..e13bad063 100644 --- a/modules/internal/src/main/scala/higherkindness/mu/rpc/internal/serviceImpl.scala +++ b/modules/internal/src/main/scala/higherkindness/mu/rpc/internal/serviceImpl.scala @@ -395,47 +395,117 @@ object serviceImpl { //TODO: derive server as well //TODO: move HTTP-related code to its own module (on last attempt this did not work) - def isEmpty(request: Tree): Boolean = request.toString.endsWith("Empty.type") - - def requestExecution(responseType: Tree, methodResponseType: Tree): Tree = - methodResponseType match { - case tq"Observable[..$tpts]" => - q"Observable.fromReactivePublisher(client.streaming(request)(_.body.chunks.parseJsonStream.map(_.as[$responseType]).rethrow).toUnicastPublisher)" - case tq"Stream[$carrier, ..$tpts]" => - q"client.streaming(request)(_.body.chunks.parseJsonStream.map(_.as[$responseType]).rethrow)" - case tq"$carrier[..$tpts]" => - q"client.expect[$responseType](request)" + abstract class TypeTypology(tpe: Tree, inner: Option[Tree], isStreaming: Boolean) + extends Product + with Serializable { + def getTpe: Tree = tpe + def getInner: Option[Tree] = inner + def streaming: Boolean = isStreaming + def safeInner: Tree = inner.getOrElse(tpe) + } + object TypeTypology { + def apply(t: Tree): TypeTypology = t match { + case tq"Observable[..$tpts]" => MonixObservableTpe(t, tpts.headOption) + case tq"Stream[$carrier, ..$tpts]" => Fs2StreamTpe(t, tpts.headOption) + case tq"Empty.type" => EmptyTpe(t) + case tq"$carrier[..$tpts]" => UnaryTpe(t, tpts.headOption) + } + } + case class EmptyTpe(tpe: Tree) extends TypeTypology(tpe, None, false) + case class UnaryTpe(tpe: Tree, inner: Option[Tree]) extends TypeTypology(tpe, inner, false) + case class Fs2StreamTpe(tpe: Tree, inner: Option[Tree]) extends TypeTypology(tpe, inner, true) + case class MonixObservableTpe(tpe: Tree, inner: Option[Tree]) + extends TypeTypology(tpe, inner, true) + + case class Operation(name: TermName, request: TypeTypology, response: TypeTypology) { + + val isStreaming: Boolean = request.streaming || response.streaming + + val streamingType: Option[StreamingType] = (request.streaming, response.streaming) match { + case (true, true) => Some(BidirectionalStreaming) + case (true, false) => Some(RequestStreaming) + case (false, true) => Some(ResponseStreaming) + case _ => None + } + + val validStreamingComb: Boolean = (request, response) match { + case (Fs2StreamTpe(_, _), MonixObservableTpe(_, _)) => false + case (MonixObservableTpe(_, _), Fs2StreamTpe(_, _)) => false + case _ => true + } + + require( + validStreamingComb, + s"RPC service $name has different streaming implementations for request and response") + + val prevalentStreamingTarget: TypeTypology = streamingType match { + case Some(ResponseStreaming) => response + case _ => request + } + + } + + case class HttpOperation(operation: Operation) { + + import operation._ + + val uri = name.toString + + val method: TermName = request match { + case _: EmptyTpe => TermName("GET") + case _ => TermName("POST") } + val executionClient: Tree = response match { + case r: MonixObservableTpe => + q"Observable.fromReactivePublisher(client.streaming(request)(_.body.chunks.parseJsonStream.map(_.as[${r.safeInner}]).rethrow).toUnicastPublisher)" + case r: Fs2StreamTpe => + q"client.streaming(request)(_.body.chunks.parseJsonStream.map(_.as[${r.safeInner}]).rethrow)" + case _ => + q"client.expect[${response.safeInner}](request)" + } - def requestTypology(method: TermName, path: String): Tree = - "" match { + val requestTypology: Tree = request match { + case _: UnaryTpe => + q"val request = Request[F](Method.$method, uri / ${uri.replace("\"", "")}).withEntity(req.asJson)" + case _: Fs2StreamTpe => + q"val request = Request[F](Method.$method, uri / ${uri.replace("\"", "")}).withEntity(req.map(_.asJson))" case _ => - q"val request = Request[F](Method.$method, uri / ${path.replace("\"", "")}).withEntity(req.map(_.asJson))" + q"val request = Request[F](Method.$method, uri / ${uri.replace("\"", "")})" } + val responseEncoder = + q"""implicit val responseDecoder: EntityDecoder[F, ${response.safeInner}] = jsonOf[F, ${response.safeInner}]""" + + def toTree: Tree = request match { + case _: EmptyTpe => + q""" + def $name(implicit client: _root_.org.http4s.client.Client[F]): ${response.getTpe} = { + $responseEncoder + $requestTypology + $executionClient + }""" + case _ => + q"""def $name(req: ${request.getTpe})(implicit client: _root_.org.http4s.client.Client[F]): ${response.getTpe} = { + $responseEncoder + $requestTypology + $executionClient + }""" + } - val toHttpRequest: ((TermName, String, TermName, Tree, Tree, Tree)) => DefDef = { - case (method, path, name, requestType, responseType, methodResponseType) => - if (requestType.toString.endsWith("Empty.type")) q""" - def $name(implicit - client: _root_.org.http4s.client.Client[F] - ): $methodResponseType = { - implicit val responseDecoder: EntityDecoder[F, $responseType] = jsonOf[F, $responseType] - val request = Request[F](Method.$method, uri / ${path.replace("\"", "")}) - ${requestExecution(responseType, methodResponseType)} - }""" - else q""" - def $name(req: $requestType)(implicit - client: _root_.org.http4s.client.Client[F] - ): $methodResponseType = { - implicit val responseDecoder: EntityDecoder[F, $responseType] = jsonOf[F, $responseType] - val request = Request[F](Method.$method, uri / ${path.replace("\"", "")}).withEntity(req.map(_.asJson)) - ${requestExecution(responseType, methodResponseType)} - }""" } - val httpRequests = (for { + + val httpRequests: List[Tree] = (for { + d <- rpcDefs.collect { case x if findAnnotation(x.mods, "http").isDefined => x } + args <- findAnnotation(d.mods, "http").collect({ case Apply(_, args) => args }).toList + params <- d.vparamss + _ = require(params.length == 1, s"RPC call ${d.name} has more than one request parameter") + p <- params.headOption.toList + } yield + HttpOperation(Operation(d.name, TypeTypology(p.tpt), TypeTypology(d.tpt)))).map(_.toTree) + + val httpRequests2 = (for { d <- rpcDefs.collect { case x if findAnnotation(x.mods, "http").isDefined => x } args <- findAnnotation(d.mods, "http").collect({ case Apply(_, args) => args }).toList params <- d.vparamss @@ -443,8 +513,8 @@ object serviceImpl { p <- params.headOption.toList } yield { - val method: c.universe.TermName = if(isEmpty(p.tpt)) TermName("GET") else TermName("POST") - val uri = d.name.toString + val method: c.universe.TermName = if (isEmpty(p.tpt)) TermName("GET") else TermName("POST") + val uri = d.name.toString // val method: c.universe.TermName = TermName(args(0).toString) // TODO: fix direct index access // val uri = args(1).toString // TODO: fix direct index access From 54990d8cda62a24d07221fae729e6bde9addcd45 Mon Sep 17 00:00:00 2001 From: rafaparadela Date: Mon, 25 Feb 2019 17:41:14 -0800 Subject: [PATCH 16/35] advances with client derivation --- .../mu/rpc/http/GreeterRestTests.scala | 66 +++-- .../mu/rpc/http/GreeterServices.scala | 2 +- .../mu/rpc/internal/serviceImpl.scala | 265 +++++++----------- 3 files changed, 138 insertions(+), 195 deletions(-) diff --git a/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterRestTests.scala b/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterRestTests.scala index 9fd006b5c..590070c73 100644 --- a/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterRestTests.scala +++ b/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterRestTests.scala @@ -268,8 +268,9 @@ class GreeterRestTests "Auto-derived REST Client" should { - val unaryClient: UnaryGreeter.HttpClient[IO] = - UnaryGreeter.httpClient[IO](serviceUri / UnaryServicePrefix) + val unaryClient = UnaryGreeter.httpClient[IO](serviceUri / UnaryServicePrefix) + val fs2Client = Fs2Greeter.httpClient[IO](serviceUri / Fs2ServicePrefix) + val monixClient = MonixGreeter.httpClient[IO](serviceUri / MonixServicePrefix) "serve a GET request" in { val response: IO[HelloResponse] = @@ -287,53 +288,50 @@ class GreeterRestTests val response: IO[HelloResponse] = BlazeClientBuilder[IO](ec).resource.use(unaryClient.sayHello(HelloRequest("SRE"))(_)) - the[UnexpectedStatus] thrownBy response.unsafeRunSync() shouldBe UnexpectedStatus( - Status.BadRequest) + the[ResponseError] thrownBy response.unsafeRunSync() shouldBe ResponseError( + Status.BadRequest, + Some("INVALID_ARGUMENT: SRE")) } "handle a raised non-gRPC exception in a unary POST request" in { val response: IO[HelloResponse] = BlazeClientBuilder[IO](ec).resource.use(unaryClient.sayHello(HelloRequest("RTE"))(_)) - the[UnexpectedStatus] thrownBy response.unsafeRunSync() shouldBe UnexpectedStatus( - Status.InternalServerError) + the[ResponseError] thrownBy response.unsafeRunSync() shouldBe ResponseError( + Status.InternalServerError, + Some("RTE")) } "handle a thrown exception in a unary POST request" in { val response: IO[HelloResponse] = BlazeClientBuilder[IO](ec).resource.use(unaryClient.sayHello(HelloRequest("TR"))(_)) - the[UnexpectedStatus] thrownBy response.unsafeRunSync() shouldBe UnexpectedStatus( + the[ResponseError] thrownBy response.unsafeRunSync() shouldBe ResponseError( Status.InternalServerError) } -// "serve a POST request with fs2 streaming request" in { -// -// val fs2Client = Fs2Greeter.httpClient[IO](serviceUri / UnaryServicePrefix) -// -// val requests = Stream(HelloRequest("hey"), HelloRequest("there")) -//// val response = -//// BlazeClientBuilder[IO](ec).resource.use(fs2ServiceClient.sayHellos(requests)(_)) -//// response.unsafeRunSync() shouldBe HelloResponse("hey, there") -// -// val response: IO[HelloResponse] = -// BlazeClientBuilder[IO](ec).resource.use(fs2Client.sayHellos(requests)(_)) -// response.unsafeRunSync() shouldBe HelloResponse("hey, there") -// } -// -// "serve a POST request with empty fs2 streaming request" in { -// val requests = Stream.empty -// val response = -// BlazeClientBuilder[IO](ec).resource.use(fs2ServiceClient.sayHellos(requests)(_)) -// response.unsafeRunSync() shouldBe HelloResponse("") -// } -// -// "serve a POST request with Observable streaming request" in { -// val requests = Observable(HelloRequest("hey"), HelloRequest("there")) -// val response = -// BlazeClientBuilder[IO](ec).resource.use(monixServiceClient.sayHellos(requests)(_)) -// response.unsafeRunSync() shouldBe HelloResponse("hey, there") -// } + "serve a POST request with fs2 streaming request" in { + + val requests = Stream(HelloRequest("hey"), HelloRequest("there")) + + val response: IO[HelloResponse] = + BlazeClientBuilder[IO](ec).resource.use(fs2Client.sayHellos(requests)(_)) + response.unsafeRunSync() shouldBe HelloResponse("hey, there") + } + + "serve a POST request with empty fs2 streaming request" in { + val requests = Stream.empty + val response = + BlazeClientBuilder[IO](ec).resource.use(fs2Client.sayHellos(requests)(_)) + response.unsafeRunSync() shouldBe HelloResponse("") + } + + "serve a POST request with Observable streaming request" in { + val requests = Observable(HelloRequest("hey"), HelloRequest("there")) + val response = + BlazeClientBuilder[IO](ec).resource.use(monixClient.sayHellos(requests)(_)) + response.unsafeRunSync() shouldBe HelloResponse("hey, there") + } // // "serve a POST request with empty Observable streaming request" in { // val requests = Observable.empty diff --git a/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterServices.scala b/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterServices.scala index 7e9bb0662..980626f5a 100644 --- a/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterServices.scala +++ b/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterServices.scala @@ -45,7 +45,7 @@ import fs2.Stream import monix.reactive.Observable @service(Avro) trait MonixGreeter[F[_]] { - def sayHellos(requests: Observable[HelloRequest]): F[HelloResponse] + @http def sayHellos(requests: Observable[HelloRequest]): F[HelloResponse] def sayHelloAll(request: HelloRequest): Observable[HelloResponse] diff --git a/modules/internal/src/main/scala/higherkindness/mu/rpc/internal/serviceImpl.scala b/modules/internal/src/main/scala/higherkindness/mu/rpc/internal/serviceImpl.scala index e13bad063..eae165431 100644 --- a/modules/internal/src/main/scala/higherkindness/mu/rpc/internal/serviceImpl.scala +++ b/modules/internal/src/main/scala/higherkindness/mu/rpc/internal/serviceImpl.scala @@ -19,7 +19,6 @@ package internal import higherkindness.mu.rpc.protocol._ -import scala.reflect.api.Trees import scala.reflect.macros.blackbox // $COVERAGE-OFF$ @@ -30,6 +29,56 @@ object serviceImpl { import c.universe._ import Flag._ + abstract class TypeTypology(tpe: Tree, inner: Option[Tree], isStreaming: Boolean) + extends Product + with Serializable { + def getTpe: Tree = tpe + def getInner: Option[Tree] = inner + def streaming: Boolean = isStreaming + def safeInner: Tree = inner.getOrElse(tpe) + } + object TypeTypology { + def apply(t: Tree): TypeTypology = t match { + case tq"Observable[..$tpts]" => MonixObservable(t, tpts.headOption) + case tq"Stream[$carrier, ..$tpts]" => Fs2Stream(t, tpts.headOption) + case tq"Empty.type" => EmptyTpe(t) + case tq"$carrier[..$tpts]" => Unary(t, tpts.headOption) + } + } + case class EmptyTpe(tpe: Tree) extends TypeTypology(tpe, None, false) + case class Unary(tpe: Tree, inner: Option[Tree]) extends TypeTypology(tpe, inner, false) + case class Fs2Stream(tpe: Tree, inner: Option[Tree]) extends TypeTypology(tpe, inner, true) + case class MonixObservable(tpe: Tree, inner: Option[Tree]) + extends TypeTypology(tpe, inner, true) + + case class Operation(name: TermName, request: TypeTypology, response: TypeTypology) { + + val isStreaming: Boolean = request.streaming || response.streaming + + val streamingType: Option[StreamingType] = (request.streaming, response.streaming) match { + case (true, true) => Some(BidirectionalStreaming) + case (true, false) => Some(RequestStreaming) + case (false, true) => Some(ResponseStreaming) + case _ => None + } + + val validStreamingComb: Boolean = (request, response) match { + case (Fs2Stream(_, _), MonixObservable(_, _)) => false + case (MonixObservable(_, _), Fs2Stream(_, _)) => false + case _ => true + } + + require( + validStreamingComb, + s"RPC service $name has different streaming implementations for request and response") + + val prevalentStreamingTarget: TypeTypology = streamingType match { + case Some(ResponseStreaming) => response + case _ => request + } + + } + trait SupressWarts[T] { def supressWarts(warts: String*)(t: T): T } @@ -119,7 +168,8 @@ object serviceImpl { params <- d.vparamss _ = require(params.length == 1, s"RPC call ${d.name} has more than one request parameter") p <- params.headOption.toList - } yield RpcRequest(d.name, p.tpt, d.tpt, compressionType(serviceDef.mods.annotations)) + operation = Operation(d.name, TypeTypology(p.tpt), TypeTypology(d.tpt)) + } yield RpcRequest(operation, compressionType(serviceDef.mods.annotations)) val imports: List[Tree] = defs.collect { case imp: Import => imp @@ -270,42 +320,18 @@ object serviceImpl { //todo: validate that the request and responses are case classes, if possible case class RpcRequest( - methodName: TermName, - requestType: Tree, - responseType: Tree, + operation: Operation, compressionOption: Tree ) { - private val requestStreamingImpl: Option[StreamingImpl] = streamingImplFor(requestType) - private val responseStreamingImpl: Option[StreamingImpl] = streamingImplFor(responseType) - private val streamingImpls: Set[StreamingImpl] = - Set(requestStreamingImpl, responseStreamingImpl).flatten - require( - streamingImpls.size < 2, - s"RPC service $serviceName has different streaming implementations for request and response") - private val streamingImpl: Option[StreamingImpl] = streamingImpls.headOption - - private val streamingType: Option[StreamingType] = - if (requestStreamingImpl.isDefined && responseStreamingImpl.isDefined) - Some(BidirectionalStreaming) - else if (requestStreamingImpl.isDefined) Some(RequestStreaming) - else if (responseStreamingImpl.isDefined) Some(ResponseStreaming) - else None - - private def streamingImplFor(t: Tree): Option[StreamingImpl] = t match { - case tq"$tpt[..$tpts]" if tpt.toString.endsWith("Observable") => Some(MonixObservable) - case tq"$tpt[..$tpts]" if tpt.toString.endsWith("Stream") => Some(Fs2Stream) - case _ => None - } - - private val clientCallsImpl = streamingImpl match { - case Some(Fs2Stream) => q"_root_.higherkindness.mu.rpc.internal.client.fs2Calls" - case Some(MonixObservable) => q"_root_.higherkindness.mu.rpc.internal.client.monixCalls" - case None => q"_root_.higherkindness.mu.rpc.internal.client.unaryCalls" + private val clientCallsImpl = operation.prevalentStreamingTarget match { + case _: Fs2Stream => q"_root_.higherkindness.mu.rpc.internal.client.fs2Calls" + case _: MonixObservable => q"_root_.higherkindness.mu.rpc.internal.client.monixCalls" + case _ => q"_root_.higherkindness.mu.rpc.internal.client.unaryCalls" } private val streamingMethodType = { - val suffix = streamingType match { + val suffix = operation.streamingType match { case Some(RequestStreaming) => "CLIENT_STREAMING" case Some(ResponseStreaming) => "SERVER_STREAMING" case Some(BidirectionalStreaming) => "BIDI_STREAMING" @@ -314,15 +340,11 @@ object serviceImpl { q"_root_.io.grpc.MethodDescriptor.MethodType.${TermName(suffix)}" } - private val methodDescriptorName = TermName(methodName + "MethodDescriptor") + private val methodDescriptorName = TermName(operation.name + "MethodDescriptor") - private val reqType = requestType match { - case tq"$s[..$tpts]" if requestStreamingImpl.isDefined => tpts.last - case other => other - } - private val respType = responseType match { - case tq"$x[..$tpts]" => tpts.last - } + private val reqType = operation.request.safeInner + + private val respType = operation.response.safeInner val methodDescriptor: DefDef = q""" def $methodDescriptorName(implicit @@ -336,7 +358,7 @@ object serviceImpl { .setType($streamingMethodType) .setFullMethodName( _root_.io.grpc.MethodDescriptor.generateFullMethodName(${lit(serviceName)}, ${lit( - methodName)})) + operation.name)})) .build() } """.supressWarts("Null", "ExplicitImplicitTypes") @@ -344,46 +366,47 @@ object serviceImpl { private def clientCallMethodFor(clientMethodName: String) = q"$clientCallsImpl.${TermName(clientMethodName)}(input, $methodDescriptorName, channel, options)" - val clientDef: Tree = streamingType match { + val clientDef: Tree = operation.streamingType match { case Some(RequestStreaming) => q""" - def $methodName(input: $requestType): $responseType = ${clientCallMethodFor( + def ${operation.name}(input: ${operation.request.getTpe}): ${operation.response.getTpe} = ${clientCallMethodFor( "clientStreaming")}""" case Some(ResponseStreaming) => q""" - def $methodName(input: $requestType): $responseType = ${clientCallMethodFor( + def ${operation.name}(input: ${operation.request.getTpe}): ${operation.response.getTpe} = ${clientCallMethodFor( "serverStreaming")}""" case Some(BidirectionalStreaming) => q""" - def $methodName(input: $requestType): $responseType = ${clientCallMethodFor( + def ${operation.name}(input: ${operation.request.getTpe}): ${operation.response.getTpe} = ${clientCallMethodFor( "bidiStreaming")}""" case None => q""" - def $methodName(input: $requestType): $responseType = ${clientCallMethodFor("unary")}""" + def ${operation.name}(input: ${operation.request.getTpe}): ${operation.response.getTpe} = ${clientCallMethodFor( + "unary")}""" } private def serverCallMethodFor(serverMethodName: String) = - q"_root_.higherkindness.mu.rpc.internal.server.monixCalls.${TermName(serverMethodName)}(algebra.$methodName, $compressionOption)" + q"_root_.higherkindness.mu.rpc.internal.server.monixCalls.${TermName(serverMethodName)}(algebra.${operation.name}, $compressionOption)" val descriptorAndHandler: Tree = { - val handler = (streamingType, streamingImpl) match { - case (Some(RequestStreaming), Some(Fs2Stream)) => - q"_root_.higherkindness.mu.rpc.internal.server.fs2Calls.clientStreamingMethod(algebra.$methodName, $compressionOption)" - case (Some(RequestStreaming), Some(MonixObservable)) => + val handler = (operation.streamingType, operation.prevalentStreamingTarget) match { + case (Some(RequestStreaming), Fs2Stream(_, _)) => + q"_root_.higherkindness.mu.rpc.internal.server.fs2Calls.clientStreamingMethod(algebra.${operation.name}, $compressionOption)" + case (Some(RequestStreaming), MonixObservable(_, _)) => q"_root_.io.grpc.stub.ServerCalls.asyncClientStreamingCall(${serverCallMethodFor("clientStreamingMethod")})" - case (Some(ResponseStreaming), Some(Fs2Stream)) => - q"_root_.higherkindness.mu.rpc.internal.server.fs2Calls.serverStreamingMethod(algebra.$methodName, $compressionOption)" - case (Some(ResponseStreaming), Some(MonixObservable)) => + case (Some(ResponseStreaming), Fs2Stream(_, _)) => + q"_root_.higherkindness.mu.rpc.internal.server.fs2Calls.serverStreamingMethod(algebra.${operation.name}, $compressionOption)" + case (Some(ResponseStreaming), MonixObservable(_, _)) => q"_root_.io.grpc.stub.ServerCalls.asyncServerStreamingCall(${serverCallMethodFor("serverStreamingMethod")})" - case (Some(BidirectionalStreaming), Some(Fs2Stream)) => - q"_root_.higherkindness.mu.rpc.internal.server.fs2Calls.bidiStreamingMethod(algebra.$methodName, $compressionOption)" - case (Some(BidirectionalStreaming), Some(MonixObservable)) => + case (Some(BidirectionalStreaming), Fs2Stream(_, _)) => + q"_root_.higherkindness.mu.rpc.internal.server.fs2Calls.bidiStreamingMethod(algebra.${operation.name}, $compressionOption)" + case (Some(BidirectionalStreaming), MonixObservable(_, _)) => q"_root_.io.grpc.stub.ServerCalls.asyncBidiStreamingCall(${serverCallMethodFor("bidiStreamingMethod")})" - case (None, None) => - q"_root_.io.grpc.stub.ServerCalls.asyncUnaryCall(_root_.higherkindness.mu.rpc.internal.server.unaryCalls.unaryMethod(algebra.$methodName, $compressionOption))" + case (None, _) => + q"_root_.io.grpc.stub.ServerCalls.asyncUnaryCall(_root_.higherkindness.mu.rpc.internal.server.unaryCalls.unaryMethod(algebra.${operation.name}, $compressionOption))" case _ => sys.error( - s"Unable to define a handler for the streaming type $streamingType and $streamingImpl for the method $methodName in the service $serviceName") + s"Unable to define a handler for the streaming type ${operation.streamingType} and ${operation.prevalentStreamingTarget} for the method ${operation.name} in the service $serviceName") } q"($methodDescriptorName, $handler)" } @@ -395,56 +418,6 @@ object serviceImpl { //TODO: derive server as well //TODO: move HTTP-related code to its own module (on last attempt this did not work) - abstract class TypeTypology(tpe: Tree, inner: Option[Tree], isStreaming: Boolean) - extends Product - with Serializable { - def getTpe: Tree = tpe - def getInner: Option[Tree] = inner - def streaming: Boolean = isStreaming - def safeInner: Tree = inner.getOrElse(tpe) - } - object TypeTypology { - def apply(t: Tree): TypeTypology = t match { - case tq"Observable[..$tpts]" => MonixObservableTpe(t, tpts.headOption) - case tq"Stream[$carrier, ..$tpts]" => Fs2StreamTpe(t, tpts.headOption) - case tq"Empty.type" => EmptyTpe(t) - case tq"$carrier[..$tpts]" => UnaryTpe(t, tpts.headOption) - } - } - case class EmptyTpe(tpe: Tree) extends TypeTypology(tpe, None, false) - case class UnaryTpe(tpe: Tree, inner: Option[Tree]) extends TypeTypology(tpe, inner, false) - case class Fs2StreamTpe(tpe: Tree, inner: Option[Tree]) extends TypeTypology(tpe, inner, true) - case class MonixObservableTpe(tpe: Tree, inner: Option[Tree]) - extends TypeTypology(tpe, inner, true) - - case class Operation(name: TermName, request: TypeTypology, response: TypeTypology) { - - val isStreaming: Boolean = request.streaming || response.streaming - - val streamingType: Option[StreamingType] = (request.streaming, response.streaming) match { - case (true, true) => Some(BidirectionalStreaming) - case (true, false) => Some(RequestStreaming) - case (false, true) => Some(ResponseStreaming) - case _ => None - } - - val validStreamingComb: Boolean = (request, response) match { - case (Fs2StreamTpe(_, _), MonixObservableTpe(_, _)) => false - case (MonixObservableTpe(_, _), Fs2StreamTpe(_, _)) => false - case _ => true - } - - require( - validStreamingComb, - s"RPC service $name has different streaming implementations for request and response") - - val prevalentStreamingTarget: TypeTypology = streamingType match { - case Some(ResponseStreaming) => response - case _ => request - } - - } - case class HttpOperation(operation: Operation) { import operation._ @@ -457,19 +430,21 @@ object serviceImpl { } val executionClient: Tree = response match { - case r: MonixObservableTpe => - q"Observable.fromReactivePublisher(client.streaming(request)(_.body.chunks.parseJsonStream.map(_.as[${r.safeInner}]).rethrow).toUnicastPublisher)" - case r: Fs2StreamTpe => - q"client.streaming(request)(_.body.chunks.parseJsonStream.map(_.as[${r.safeInner}]).rethrow)" + case MonixObservable(_, _) => + q"client.stream(request).flatMap(_.asStream[${response.safeInner}]).toObservable" + case Fs2Stream(_, _) => + q"client.stream(request).flatMap(_.asStream[${response.safeInner}])" case _ => - q"client.expect[${response.safeInner}](request)" + q"client.expectOr[${response.safeInner}](request)(handleResponseError)" } val requestTypology: Tree = request match { - case _: UnaryTpe => + case _: Unary => q"val request = Request[F](Method.$method, uri / ${uri.replace("\"", "")}).withEntity(req.asJson)" - case _: Fs2StreamTpe => + case _: Fs2Stream => q"val request = Request[F](Method.$method, uri / ${uri.replace("\"", "")}).withEntity(req.map(_.asJson))" + case _: MonixObservable => + q"val request = Request[F](Method.$method, uri / ${uri.replace("\"", "")}).withEntity(req.toFs2Stream.map(_.asJson))" case _ => q"val request = Request[F](Method.$method, uri / ${uri.replace("\"", "")})" } @@ -479,24 +454,22 @@ object serviceImpl { def toTree: Tree = request match { case _: EmptyTpe => - q""" - def $name(implicit client: _root_.org.http4s.client.Client[F]): ${response.getTpe} = { - $responseEncoder - $requestTypology - $executionClient - }""" + q"""def $name(implicit client: _root_.org.http4s.client.Client[F]): ${response.getTpe} = { + $responseEncoder + $requestTypology + $executionClient + }""" case _ => q"""def $name(req: ${request.getTpe})(implicit client: _root_.org.http4s.client.Client[F]): ${response.getTpe} = { - $responseEncoder - $requestTypology - $executionClient - }""" + $responseEncoder + $requestTypology + $executionClient + }""" } } - - val httpRequests: List[Tree] = (for { + val httpRequests = (for { d <- rpcDefs.collect { case x if findAnnotation(x.mods, "http").isDefined => x } args <- findAnnotation(d.mods, "http").collect({ case Apply(_, args) => args }).toList params <- d.vparamss @@ -505,39 +478,15 @@ object serviceImpl { } yield HttpOperation(Operation(d.name, TypeTypology(p.tpt), TypeTypology(d.tpt)))).map(_.toTree) - val httpRequests2 = (for { - d <- rpcDefs.collect { case x if findAnnotation(x.mods, "http").isDefined => x } - args <- findAnnotation(d.mods, "http").collect({ case Apply(_, args) => args }).toList - params <- d.vparamss - _ = require(params.length == 1, s"RPC call ${d.name} has more than one request parameter") - p <- params.headOption.toList - } yield { - - val method: c.universe.TermName = if (isEmpty(p.tpt)) TermName("GET") else TermName("POST") - val uri = d.name.toString - -// val method: c.universe.TermName = TermName(args(0).toString) // TODO: fix direct index access -// val uri = args(1).toString // TODO: fix direct index access - - val responseType: Tree = d.tpt match { - case tq"Observable[..$tpts]" => tpts.head - case tq"Stream[$carrier, ..$tpts]" => tpts.head - case tq"$carrier[..$tpts]" => tpts.head - case _ => ??? - } - - (method, uri, d.name, p.tpt, responseType, d.tpt) - }).map(toHttpRequest) - val HttpClient = TypeName("HttpClient") val httpClientClass: ClassDef = q""" - class $HttpClient[$F_](uri: Uri)(implicit F: _root_.cats.effect.Effect[$F], ec: scala.concurrent.ExecutionContext) { + class $HttpClient[$F_](uri: Uri)(implicit F: _root_.cats.effect.ConcurrentEffect[$F], ec: scala.concurrent.ExecutionContext) { ..$httpRequests }""" val httpClient: DefDef = q""" def httpClient[$F_](uri: Uri) - (implicit F: _root_.cats.effect.Effect[$F], ec: scala.concurrent.ExecutionContext): $HttpClient[$F] = { + (implicit F: _root_.cats.effect.ConcurrentEffect[$F], ec: scala.concurrent.ExecutionContext): $HttpClient[$F] = { new $HttpClient[$F](uri) }""" @@ -608,10 +557,6 @@ object serviceImpl { } c.Expr(Block(result, Literal(Constant(())))) } -} - -sealed trait StreamingImpl extends Product with Serializable -case object Fs2Stream extends StreamingImpl -case object MonixObservable extends StreamingImpl +} // $COVERAGE-ON$ From fe34e472b5d2436aa58f8546b31871ce2d29ff94 Mon Sep 17 00:00:00 2001 From: rafaparadela Date: Tue, 26 Feb 2019 13:06:53 -0800 Subject: [PATCH 17/35] adds more unit test to prove the derived http client --- .../mu/rpc/http/GreeterRestTests.scala | 170 +++++++++--------- .../mu/rpc/http/GreeterServices.scala | 8 +- .../mu/rpc/internal/serviceImpl.scala | 41 +++-- 3 files changed, 113 insertions(+), 106 deletions(-) diff --git a/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterRestTests.scala b/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterRestTests.scala index 590070c73..a67fedfe6 100644 --- a/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterRestTests.scala +++ b/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterRestTests.scala @@ -332,91 +332,91 @@ class GreeterRestTests BlazeClientBuilder[IO](ec).resource.use(monixClient.sayHellos(requests)(_)) response.unsafeRunSync() shouldBe HelloResponse("hey, there") } -// -// "serve a POST request with empty Observable streaming request" in { -// val requests = Observable.empty -// val response = -// BlazeClientBuilder[IO](ec).resource.use(monixServiceClient.sayHellos(requests)(_)) -// response.unsafeRunSync() shouldBe HelloResponse("") -// } -// -// "serve a POST request with fs2 streaming response" in { -// val request = HelloRequest("hey") -// val responses = -// BlazeClientBuilder[IO](ec).stream.flatMap(fs2ServiceClient.sayHelloAll(request)(_)) -// responses.compile.toList -// .unsafeRunSync() shouldBe List(HelloResponse("hey"), HelloResponse("hey")) -// } -// -// "serve a POST request with Observable streaming response" in { -// val request = HelloRequest("hey") -// val responses = BlazeClientBuilder[IO](ec).stream -// .flatMap(monixServiceClient.sayHelloAll(request)(_).toFs2Stream[IO]) -// responses.compile.toList -// .unsafeRunTimed(10.seconds) -// .getOrElse(sys.error("Stuck!")) shouldBe List(HelloResponse("hey"), HelloResponse("hey")) -// } -// -// "handle errors with fs2 streaming response" in { -// val request = HelloRequest("") -// val responses = -// BlazeClientBuilder[IO](ec).stream.flatMap(fs2ServiceClient.sayHelloAll(request)(_)) -// the[IllegalArgumentException] thrownBy responses.compile.toList -// .unsafeRunSync() should have message "empty greeting" -// } -// -// "handle errors with Observable streaming response" in { -// val request = HelloRequest("") -// val responses = BlazeClientBuilder[IO](ec).stream -// .flatMap(monixServiceClient.sayHelloAll(request)(_).toFs2Stream[IO]) -// the[IllegalArgumentException] thrownBy responses.compile.toList -// .unsafeRunTimed(10.seconds) -// .getOrElse(sys.error("Stuck!")) should have message "empty greeting" -// } -// -// "serve a POST request with bidirectional fs2 streaming" in { -// val requests = Stream(HelloRequest("hey"), HelloRequest("there")) -// val responses = -// BlazeClientBuilder[IO](ec).stream.flatMap(fs2ServiceClient.sayHellosAll(requests)(_)) -// responses.compile.toList -// .unsafeRunSync() shouldBe List(HelloResponse("hey"), HelloResponse("there")) -// } -// -// "serve an empty POST request with bidirectional fs2 streaming" in { -// val requests = Stream.empty -// val responses = -// BlazeClientBuilder[IO](ec).stream.flatMap(fs2ServiceClient.sayHellosAll(requests)(_)) -// responses.compile.toList.unsafeRunSync() shouldBe Nil -// } -// -// "serve a POST request with bidirectional Observable streaming" in { -// val requests = Observable(HelloRequest("hey"), HelloRequest("there")) -// val responses = BlazeClientBuilder[IO](ec).stream -// .flatMap(monixServiceClient.sayHellosAll(requests)(_).toFs2Stream[IO]) -// responses.compile.toList -// .unsafeRunTimed(10.seconds) -// .getOrElse(sys.error("Stuck!")) shouldBe List(HelloResponse("hey"), HelloResponse("there")) -// } -// -// "serve an empty POST request with bidirectional Observable streaming" in { -// val requests = Observable.empty -// val responses = BlazeClientBuilder[IO](ec).stream -// .flatMap(monixServiceClient.sayHellosAll(requests)(_).toFs2Stream[IO]) -// responses.compile.toList -// .unsafeRunTimed(10.seconds) -// .getOrElse(sys.error("Stuck!")) shouldBe Nil -// } -// -// "serve ScalaCheck-generated POST requests with bidirectional Observable streaming" in { -// forAll { strings: List[String] => -// val requests = Observable.fromIterable(strings.map(HelloRequest)) -// val responses = BlazeClientBuilder[IO](ec).stream -// .flatMap(monixServiceClient.sayHellosAll(requests)(_).toFs2Stream[IO]) -// responses.compile.toList -// .unsafeRunTimed(10.seconds) -// .getOrElse(sys.error("Stuck!")) shouldBe strings.map(HelloResponse) -// } -// } + + "serve a POST request with empty Observable streaming request" in { + val requests = Observable.empty + val response = + BlazeClientBuilder[IO](ec).resource.use(monixClient.sayHellos(requests)(_)) + response.unsafeRunSync() shouldBe HelloResponse("") + } + + "serve a POST request with fs2 streaming response" in { + val request = HelloRequest("hey") + val responses = + BlazeClientBuilder[IO](ec).stream.flatMap(fs2Client.sayHelloAll(request)(_)) + responses.compile.toList + .unsafeRunSync() shouldBe List(HelloResponse("hey"), HelloResponse("hey")) + } + + "serve a POST request with Observable streaming response" in { + val request = HelloRequest("hey") + val responses = BlazeClientBuilder[IO](ec).stream + .flatMap(monixClient.sayHelloAll(request)(_).toFs2Stream[IO]) + responses.compile.toList + .unsafeRunTimed(10.seconds) + .getOrElse(sys.error("Stuck!")) shouldBe List(HelloResponse("hey"), HelloResponse("hey")) + } + + "handle errors with fs2 streaming response" in { + val request = HelloRequest("") + val responses = + BlazeClientBuilder[IO](ec).stream.flatMap(fs2Client.sayHelloAll(request)(_)) + the[IllegalArgumentException] thrownBy responses.compile.toList + .unsafeRunSync() should have message "empty greeting" + } + + "handle errors with Observable streaming response" in { + val request = HelloRequest("") + val responses = BlazeClientBuilder[IO](ec).stream + .flatMap(monixClient.sayHelloAll(request)(_).toFs2Stream[IO]) + the[IllegalArgumentException] thrownBy responses.compile.toList + .unsafeRunTimed(10.seconds) + .getOrElse(sys.error("Stuck!")) should have message "empty greeting" + } + + "serve a POST request with bidirectional fs2 streaming" in { + val requests = Stream(HelloRequest("hey"), HelloRequest("there")) + val responses = + BlazeClientBuilder[IO](ec).stream.flatMap(fs2Client.sayHellosAll(requests)(_)) + responses.compile.toList + .unsafeRunSync() shouldBe List(HelloResponse("hey"), HelloResponse("there")) + } + + "serve an empty POST request with bidirectional fs2 streaming" in { + val requests = Stream.empty + val responses = + BlazeClientBuilder[IO](ec).stream.flatMap(fs2Client.sayHellosAll(requests)(_)) + responses.compile.toList.unsafeRunSync() shouldBe Nil + } + + "serve a POST request with bidirectional Observable streaming" in { + val requests = Observable(HelloRequest("hey"), HelloRequest("there")) + val responses = BlazeClientBuilder[IO](ec).stream + .flatMap(monixClient.sayHellosAll(requests)(_).toFs2Stream[IO]) + responses.compile.toList + .unsafeRunTimed(10.seconds) + .getOrElse(sys.error("Stuck!")) shouldBe List(HelloResponse("hey"), HelloResponse("there")) + } + + "serve an empty POST request with bidirectional Observable streaming" in { + val requests = Observable.empty + val responses = BlazeClientBuilder[IO](ec).stream + .flatMap(monixClient.sayHellosAll(requests)(_).toFs2Stream[IO]) + responses.compile.toList + .unsafeRunTimed(10.seconds) + .getOrElse(sys.error("Stuck!")) shouldBe Nil + } + + "serve ScalaCheck-generated POST requests with bidirectional Observable streaming" in { + forAll { strings: List[String] => + val requests = Observable.fromIterable(strings.map(HelloRequest)) + val responses = BlazeClientBuilder[IO](ec).stream + .flatMap(monixClient.sayHellosAll(requests)(_).toFs2Stream[IO]) + responses.compile.toList + .unsafeRunTimed(10.seconds) + .getOrElse(sys.error("Stuck!")) shouldBe strings.map(HelloResponse) + } + } } diff --git a/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterServices.scala b/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterServices.scala index 980626f5a..1480addc7 100644 --- a/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterServices.scala +++ b/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterServices.scala @@ -37,9 +37,9 @@ import fs2.Stream @http def sayHellos(requests: Stream[F, HelloRequest]): F[HelloResponse] - def sayHelloAll(request: HelloRequest): Stream[F, HelloResponse] + @http def sayHelloAll(request: HelloRequest): Stream[F, HelloResponse] - def sayHellosAll(requests: Stream[F, HelloRequest]): Stream[F, HelloResponse] + @http def sayHellosAll(requests: Stream[F, HelloRequest]): Stream[F, HelloResponse] } import monix.reactive.Observable @@ -47,7 +47,7 @@ import monix.reactive.Observable @http def sayHellos(requests: Observable[HelloRequest]): F[HelloResponse] - def sayHelloAll(request: HelloRequest): Observable[HelloResponse] + @http def sayHelloAll(request: HelloRequest): Observable[HelloResponse] - def sayHellosAll(requests: Observable[HelloRequest]): Observable[HelloResponse] + @http def sayHellosAll(requests: Observable[HelloRequest]): Observable[HelloResponse] } diff --git a/modules/internal/src/main/scala/higherkindness/mu/rpc/internal/serviceImpl.scala b/modules/internal/src/main/scala/higherkindness/mu/rpc/internal/serviceImpl.scala index eae165431..ec4d5e057 100644 --- a/modules/internal/src/main/scala/higherkindness/mu/rpc/internal/serviceImpl.scala +++ b/modules/internal/src/main/scala/higherkindness/mu/rpc/internal/serviceImpl.scala @@ -68,6 +68,10 @@ object serviceImpl { case _ => true } + val isMonixObservable: Boolean = List(request, response).collect { + case m: MonixObservable => m + }.nonEmpty + require( validStreamingComb, s"RPC service $name has different streaming implementations for request and response") @@ -469,14 +473,15 @@ object serviceImpl { } - val httpRequests = (for { + val operations: List[HttpOperation] = for { d <- rpcDefs.collect { case x if findAnnotation(x.mods, "http").isDefined => x } args <- findAnnotation(d.mods, "http").collect({ case Apply(_, args) => args }).toList params <- d.vparamss _ = require(params.length == 1, s"RPC call ${d.name} has more than one request parameter") p <- params.headOption.toList - } yield - HttpOperation(Operation(d.name, TypeTypology(p.tpt), TypeTypology(d.tpt)))).map(_.toTree) + } yield HttpOperation(Operation(d.name, TypeTypology(p.tpt), TypeTypology(d.tpt))) + + val httpRequests = operations.map(_.toTree) val HttpClient = TypeName("HttpClient") val httpClientClass: ClassDef = q""" @@ -490,19 +495,24 @@ object serviceImpl { new $HttpClient[$F](uri) }""" + val httpImports: List[Tree] = List( + q"import _root_.higherkindness.mu.rpc.http.Utils._", + q"import _root_.org.http4s._", + q"import _root_.org.http4s.circe._", + q"import _root_.io.circe._", + q"import _root_.io.circe.generic.auto._", + q"import _root_.io.circe.syntax._" + ) + + val scheduler: List[Tree] = operations + .find(_.operation.isMonixObservable) + .map(_ => q"import _root_.monix.execution.Scheduler.Implicits.global") + .toList + val http = if (httpRequests.isEmpty) Nil else - List( - q"import _root_.higherkindness.mu.rpc.http.Utils._", - q"import _root_.org.http4s._", - q"import _root_.org.http4s.circe._", - q"import _root_.io.circe._", - q"import _root_.io.circe.generic.auto._", - q"import _root_.io.circe.syntax._", - httpClientClass, - httpClient - ) + httpImports ++ scheduler ++ List(httpClientClass, httpClient) } val classAndMaybeCompanion = annottees.map(_.tree) @@ -547,10 +557,7 @@ object serviceImpl { ) ++ service.http ) ) - if (service.httpRequests.nonEmpty) { - println("#######################") - println(enrichedCompanion.toString) - } + List(serviceDef, enrichedCompanion) } case _ => sys.error("@service-annotated definition must be a trait or abstract class") From 1132fc220ec1d10b1c250c6951c4d06b91834b78 Mon Sep 17 00:00:00 2001 From: rafaparadela Date: Tue, 26 Feb 2019 14:35:19 -0800 Subject: [PATCH 18/35] restores the derivation of the rpc server, without the refactoring --- .../mu/rpc/internal/serviceImpl.scala | 221 ++++++++++-------- 1 file changed, 123 insertions(+), 98 deletions(-) diff --git a/modules/internal/src/main/scala/higherkindness/mu/rpc/internal/serviceImpl.scala b/modules/internal/src/main/scala/higherkindness/mu/rpc/internal/serviceImpl.scala index ec4d5e057..eff34b6d6 100644 --- a/modules/internal/src/main/scala/higherkindness/mu/rpc/internal/serviceImpl.scala +++ b/modules/internal/src/main/scala/higherkindness/mu/rpc/internal/serviceImpl.scala @@ -18,7 +18,6 @@ package higherkindness.mu.rpc package internal import higherkindness.mu.rpc.protocol._ - import scala.reflect.macros.blackbox // $COVERAGE-OFF$ @@ -29,60 +28,6 @@ object serviceImpl { import c.universe._ import Flag._ - abstract class TypeTypology(tpe: Tree, inner: Option[Tree], isStreaming: Boolean) - extends Product - with Serializable { - def getTpe: Tree = tpe - def getInner: Option[Tree] = inner - def streaming: Boolean = isStreaming - def safeInner: Tree = inner.getOrElse(tpe) - } - object TypeTypology { - def apply(t: Tree): TypeTypology = t match { - case tq"Observable[..$tpts]" => MonixObservable(t, tpts.headOption) - case tq"Stream[$carrier, ..$tpts]" => Fs2Stream(t, tpts.headOption) - case tq"Empty.type" => EmptyTpe(t) - case tq"$carrier[..$tpts]" => Unary(t, tpts.headOption) - } - } - case class EmptyTpe(tpe: Tree) extends TypeTypology(tpe, None, false) - case class Unary(tpe: Tree, inner: Option[Tree]) extends TypeTypology(tpe, inner, false) - case class Fs2Stream(tpe: Tree, inner: Option[Tree]) extends TypeTypology(tpe, inner, true) - case class MonixObservable(tpe: Tree, inner: Option[Tree]) - extends TypeTypology(tpe, inner, true) - - case class Operation(name: TermName, request: TypeTypology, response: TypeTypology) { - - val isStreaming: Boolean = request.streaming || response.streaming - - val streamingType: Option[StreamingType] = (request.streaming, response.streaming) match { - case (true, true) => Some(BidirectionalStreaming) - case (true, false) => Some(RequestStreaming) - case (false, true) => Some(ResponseStreaming) - case _ => None - } - - val validStreamingComb: Boolean = (request, response) match { - case (Fs2Stream(_, _), MonixObservable(_, _)) => false - case (MonixObservable(_, _), Fs2Stream(_, _)) => false - case _ => true - } - - val isMonixObservable: Boolean = List(request, response).collect { - case m: MonixObservable => m - }.nonEmpty - - require( - validStreamingComb, - s"RPC service $name has different streaming implementations for request and response") - - val prevalentStreamingTarget: TypeTypology = streamingType match { - case Some(ResponseStreaming) => response - case _ => request - } - - } - trait SupressWarts[T] { def supressWarts(warts: String*)(t: T): T } @@ -172,8 +117,7 @@ object serviceImpl { params <- d.vparamss _ = require(params.length == 1, s"RPC call ${d.name} has more than one request parameter") p <- params.headOption.toList - operation = Operation(d.name, TypeTypology(p.tpt), TypeTypology(d.tpt)) - } yield RpcRequest(operation, compressionType(serviceDef.mods.annotations)) + } yield RpcRequest(d.name, p.tpt, d.tpt, compressionType(serviceDef.mods.annotations)) val imports: List[Tree] = defs.collect { case imp: Import => imp @@ -324,18 +268,42 @@ object serviceImpl { //todo: validate that the request and responses are case classes, if possible case class RpcRequest( - operation: Operation, + methodName: TermName, + requestType: Tree, + responseType: Tree, compressionOption: Tree ) { - private val clientCallsImpl = operation.prevalentStreamingTarget match { - case _: Fs2Stream => q"_root_.higherkindness.mu.rpc.internal.client.fs2Calls" - case _: MonixObservable => q"_root_.higherkindness.mu.rpc.internal.client.monixCalls" - case _ => q"_root_.higherkindness.mu.rpc.internal.client.unaryCalls" + private val requestStreamingImpl: Option[StreamingImpl] = streamingImplFor(requestType) + private val responseStreamingImpl: Option[StreamingImpl] = streamingImplFor(responseType) + private val streamingImpls: Set[StreamingImpl] = + Set(requestStreamingImpl, responseStreamingImpl).flatten + require( + streamingImpls.size < 2, + s"RPC service $serviceName has different streaming implementations for request and response") + private val streamingImpl: Option[StreamingImpl] = streamingImpls.headOption + + private val streamingType: Option[StreamingType] = + if (requestStreamingImpl.isDefined && responseStreamingImpl.isDefined) + Some(BidirectionalStreaming) + else if (requestStreamingImpl.isDefined) Some(RequestStreaming) + else if (responseStreamingImpl.isDefined) Some(ResponseStreaming) + else None + + private def streamingImplFor(t: Tree): Option[StreamingImpl] = t match { + case tq"$tpt[..$tpts]" if tpt.toString.endsWith("Observable") => Some(MonixObservable) + case tq"$tpt[..$tpts]" if tpt.toString.endsWith("Stream") => Some(Fs2Stream) + case _ => None + } + + private val clientCallsImpl = streamingImpl match { + case Some(Fs2Stream) => q"_root_.higherkindness.mu.rpc.internal.client.fs2Calls" + case Some(MonixObservable) => q"_root_.higherkindness.mu.rpc.internal.client.monixCalls" + case None => q"_root_.higherkindness.mu.rpc.internal.client.unaryCalls" } private val streamingMethodType = { - val suffix = operation.streamingType match { + val suffix = streamingType match { case Some(RequestStreaming) => "CLIENT_STREAMING" case Some(ResponseStreaming) => "SERVER_STREAMING" case Some(BidirectionalStreaming) => "BIDI_STREAMING" @@ -344,11 +312,15 @@ object serviceImpl { q"_root_.io.grpc.MethodDescriptor.MethodType.${TermName(suffix)}" } - private val methodDescriptorName = TermName(operation.name + "MethodDescriptor") + private val methodDescriptorName = TermName(methodName + "MethodDescriptor") - private val reqType = operation.request.safeInner - - private val respType = operation.response.safeInner + private val reqType = requestType match { + case tq"$s[..$tpts]" if requestStreamingImpl.isDefined => tpts.last + case other => other + } + private val respType = responseType match { + case tq"$x[..$tpts]" => tpts.last + } val methodDescriptor: DefDef = q""" def $methodDescriptorName(implicit @@ -362,7 +334,7 @@ object serviceImpl { .setType($streamingMethodType) .setFullMethodName( _root_.io.grpc.MethodDescriptor.generateFullMethodName(${lit(serviceName)}, ${lit( - operation.name)})) + methodName)})) .build() } """.supressWarts("Null", "ExplicitImplicitTypes") @@ -370,47 +342,46 @@ object serviceImpl { private def clientCallMethodFor(clientMethodName: String) = q"$clientCallsImpl.${TermName(clientMethodName)}(input, $methodDescriptorName, channel, options)" - val clientDef: Tree = operation.streamingType match { + val clientDef: Tree = streamingType match { case Some(RequestStreaming) => q""" - def ${operation.name}(input: ${operation.request.getTpe}): ${operation.response.getTpe} = ${clientCallMethodFor( + def $methodName(input: $requestType): $responseType = ${clientCallMethodFor( "clientStreaming")}""" case Some(ResponseStreaming) => q""" - def ${operation.name}(input: ${operation.request.getTpe}): ${operation.response.getTpe} = ${clientCallMethodFor( + def $methodName(input: $requestType): $responseType = ${clientCallMethodFor( "serverStreaming")}""" case Some(BidirectionalStreaming) => q""" - def ${operation.name}(input: ${operation.request.getTpe}): ${operation.response.getTpe} = ${clientCallMethodFor( + def $methodName(input: $requestType): $responseType = ${clientCallMethodFor( "bidiStreaming")}""" case None => q""" - def ${operation.name}(input: ${operation.request.getTpe}): ${operation.response.getTpe} = ${clientCallMethodFor( - "unary")}""" + def $methodName(input: $requestType): $responseType = ${clientCallMethodFor("unary")}""" } private def serverCallMethodFor(serverMethodName: String) = - q"_root_.higherkindness.mu.rpc.internal.server.monixCalls.${TermName(serverMethodName)}(algebra.${operation.name}, $compressionOption)" + q"_root_.higherkindness.mu.rpc.internal.server.monixCalls.${TermName(serverMethodName)}(algebra.$methodName, $compressionOption)" val descriptorAndHandler: Tree = { - val handler = (operation.streamingType, operation.prevalentStreamingTarget) match { - case (Some(RequestStreaming), Fs2Stream(_, _)) => - q"_root_.higherkindness.mu.rpc.internal.server.fs2Calls.clientStreamingMethod(algebra.${operation.name}, $compressionOption)" - case (Some(RequestStreaming), MonixObservable(_, _)) => + val handler = (streamingType, streamingImpl) match { + case (Some(RequestStreaming), Some(Fs2Stream)) => + q"_root_.higherkindness.mu.rpc.internal.server.fs2Calls.clientStreamingMethod(algebra.$methodName, $compressionOption)" + case (Some(RequestStreaming), Some(MonixObservable)) => q"_root_.io.grpc.stub.ServerCalls.asyncClientStreamingCall(${serverCallMethodFor("clientStreamingMethod")})" - case (Some(ResponseStreaming), Fs2Stream(_, _)) => - q"_root_.higherkindness.mu.rpc.internal.server.fs2Calls.serverStreamingMethod(algebra.${operation.name}, $compressionOption)" - case (Some(ResponseStreaming), MonixObservable(_, _)) => + case (Some(ResponseStreaming), Some(Fs2Stream)) => + q"_root_.higherkindness.mu.rpc.internal.server.fs2Calls.serverStreamingMethod(algebra.$methodName, $compressionOption)" + case (Some(ResponseStreaming), Some(MonixObservable)) => q"_root_.io.grpc.stub.ServerCalls.asyncServerStreamingCall(${serverCallMethodFor("serverStreamingMethod")})" - case (Some(BidirectionalStreaming), Fs2Stream(_, _)) => - q"_root_.higherkindness.mu.rpc.internal.server.fs2Calls.bidiStreamingMethod(algebra.${operation.name}, $compressionOption)" - case (Some(BidirectionalStreaming), MonixObservable(_, _)) => + case (Some(BidirectionalStreaming), Some(Fs2Stream)) => + q"_root_.higherkindness.mu.rpc.internal.server.fs2Calls.bidiStreamingMethod(algebra.$methodName, $compressionOption)" + case (Some(BidirectionalStreaming), Some(MonixObservable)) => q"_root_.io.grpc.stub.ServerCalls.asyncBidiStreamingCall(${serverCallMethodFor("bidiStreamingMethod")})" - case (None, _) => - q"_root_.io.grpc.stub.ServerCalls.asyncUnaryCall(_root_.higherkindness.mu.rpc.internal.server.unaryCalls.unaryMethod(algebra.${operation.name}, $compressionOption))" + case (None, None) => + q"_root_.io.grpc.stub.ServerCalls.asyncUnaryCall(_root_.higherkindness.mu.rpc.internal.server.unaryCalls.unaryMethod(algebra.$methodName, $compressionOption))" case _ => sys.error( - s"Unable to define a handler for the streaming type ${operation.streamingType} and ${operation.prevalentStreamingTarget} for the method ${operation.name} in the service $serviceName") + s"Unable to define a handler for the streaming type $streamingType and $streamingImpl for the method $methodName in the service $serviceName") } q"($methodDescriptorName, $handler)" } @@ -422,6 +393,58 @@ object serviceImpl { //TODO: derive server as well //TODO: move HTTP-related code to its own module (on last attempt this did not work) + abstract class TypeTypology(tpe: Tree, inner: Option[Tree], isStreaming: Boolean) { + def getTpe: Tree = tpe + def getInner: Option[Tree] = inner + def streaming: Boolean = isStreaming + def safeInner: Tree = inner.getOrElse(tpe) + } + object TypeTypology { + def apply(t: Tree): TypeTypology = t match { + case tq"Observable[..$tpts]" => MonixObservableTpe(t, tpts.headOption) + case tq"Stream[$carrier, ..$tpts]" => Fs2StreamTpe(t, tpts.headOption) + case tq"Empty.type" => EmptyTpe(t) + case tq"$carrier[..$tpts]" => UnaryTpe(t, tpts.headOption) + } + } + case class EmptyTpe(tpe: Tree) extends TypeTypology(tpe, None, false) + case class UnaryTpe(tpe: Tree, inner: Option[Tree]) extends TypeTypology(tpe, inner, false) + case class Fs2StreamTpe(tpe: Tree, inner: Option[Tree]) extends TypeTypology(tpe, inner, true) + case class MonixObservableTpe(tpe: Tree, inner: Option[Tree]) + extends TypeTypology(tpe, inner, true) + + case class Operation(name: TermName, request: TypeTypology, response: TypeTypology) { + + val isStreaming: Boolean = request.streaming || response.streaming + + val streamingType: Option[StreamingType] = (request.streaming, response.streaming) match { + case (true, true) => Some(BidirectionalStreaming) + case (true, false) => Some(RequestStreaming) + case (false, true) => Some(ResponseStreaming) + case _ => None + } + + val validStreamingComb: Boolean = (request, response) match { + case (Fs2StreamTpe(_, _), MonixObservableTpe(_, _)) => false + case (MonixObservableTpe(_, _), Fs2StreamTpe(_, _)) => false + case _ => true + } + + val isMonixObservable: Boolean = List(request, response).collect { + case m: MonixObservableTpe => m + }.nonEmpty + + require( + validStreamingComb, + s"RPC service $name has different streaming implementations for request and response") + + val prevalentStreamingTarget: TypeTypology = streamingType match { + case Some(ResponseStreaming) => response + case _ => request + } + + } + case class HttpOperation(operation: Operation) { import operation._ @@ -434,20 +457,20 @@ object serviceImpl { } val executionClient: Tree = response match { - case MonixObservable(_, _) => + case MonixObservableTpe(_, _) => q"client.stream(request).flatMap(_.asStream[${response.safeInner}]).toObservable" - case Fs2Stream(_, _) => + case Fs2StreamTpe(_, _) => q"client.stream(request).flatMap(_.asStream[${response.safeInner}])" case _ => q"client.expectOr[${response.safeInner}](request)(handleResponseError)" } val requestTypology: Tree = request match { - case _: Unary => + case _: UnaryTpe => q"val request = Request[F](Method.$method, uri / ${uri.replace("\"", "")}).withEntity(req.asJson)" - case _: Fs2Stream => + case _: Fs2StreamTpe => q"val request = Request[F](Method.$method, uri / ${uri.replace("\"", "")}).withEntity(req.map(_.asJson))" - case _: MonixObservable => + case _: MonixObservableTpe => q"val request = Request[F](Method.$method, uri / ${uri.replace("\"", "")}).withEntity(req.toFs2Stream.map(_.asJson))" case _ => q"val request = Request[F](Method.$method, uri / ${uri.replace("\"", "")})" @@ -518,7 +541,7 @@ object serviceImpl { val classAndMaybeCompanion = annottees.map(_.tree) val result: List[Tree] = classAndMaybeCompanion.head match { case serviceDef: ClassDef - if serviceDef.mods.hasFlag(TRAIT) || serviceDef.mods.hasFlag(ABSTRACT) => { + if serviceDef.mods.hasFlag(TRAIT) || serviceDef.mods.hasFlag(ABSTRACT) => val service = new RpcService(serviceDef) val companion: ModuleDef = classAndMaybeCompanion.lastOption match { case Some(obj: ModuleDef) => obj @@ -553,17 +576,19 @@ object serviceImpl { service.client, service.clientFromChannel, service.unsafeClient, - service.unsafeClientFromChannel + service.unsafeClientFromChannel, ) ++ service.http ) ) - List(serviceDef, enrichedCompanion) - } case _ => sys.error("@service-annotated definition must be a trait or abstract class") } c.Expr(Block(result, Literal(Constant(())))) } - } + +sealed trait StreamingImpl extends Product with Serializable +case object Fs2Stream extends StreamingImpl +case object MonixObservable extends StreamingImpl + // $COVERAGE-ON$ From 97d1c73afe8956acab91f3f0fb5f1289aae5e9e5 Mon Sep 17 00:00:00 2001 From: rafaparadela Date: Tue, 26 Feb 2019 14:50:56 -0800 Subject: [PATCH 19/35] re-applies part of the refactoring little by little --- .../mu/rpc/internal/serviceImpl.scala | 150 +++++++++--------- 1 file changed, 77 insertions(+), 73 deletions(-) diff --git a/modules/internal/src/main/scala/higherkindness/mu/rpc/internal/serviceImpl.scala b/modules/internal/src/main/scala/higherkindness/mu/rpc/internal/serviceImpl.scala index eff34b6d6..479e92b6c 100644 --- a/modules/internal/src/main/scala/higherkindness/mu/rpc/internal/serviceImpl.scala +++ b/modules/internal/src/main/scala/higherkindness/mu/rpc/internal/serviceImpl.scala @@ -28,6 +28,58 @@ object serviceImpl { import c.universe._ import Flag._ + abstract class TypeTypology(tpe: Tree, inner: Option[Tree], isStreaming: Boolean) { + def getTpe: Tree = tpe + def getInner: Option[Tree] = inner + def streaming: Boolean = isStreaming + def safeInner: Tree = inner.getOrElse(tpe) + } + object TypeTypology { + def apply(t: Tree): TypeTypology = t match { + case tq"Observable[..$tpts]" => MonixObservableTpe(t, tpts.headOption) + case tq"Stream[$carrier, ..$tpts]" => Fs2StreamTpe(t, tpts.headOption) + case tq"Empty.type" => EmptyTpe(t) + case tq"$carrier[..$tpts]" => UnaryTpe(t, tpts.headOption) + } + } + case class EmptyTpe(tpe: Tree) extends TypeTypology(tpe, None, false) + case class UnaryTpe(tpe: Tree, inner: Option[Tree]) extends TypeTypology(tpe, inner, false) + case class Fs2StreamTpe(tpe: Tree, inner: Option[Tree]) extends TypeTypology(tpe, inner, true) + case class MonixObservableTpe(tpe: Tree, inner: Option[Tree]) + extends TypeTypology(tpe, inner, true) + + case class Operation(name: TermName, request: TypeTypology, response: TypeTypology) { + + val isStreaming: Boolean = request.streaming || response.streaming + + val streamingType: Option[StreamingType] = (request.streaming, response.streaming) match { + case (true, true) => Some(BidirectionalStreaming) + case (true, false) => Some(RequestStreaming) + case (false, true) => Some(ResponseStreaming) + case _ => None + } + + val validStreamingComb: Boolean = (request, response) match { + case (Fs2StreamTpe(_, _), MonixObservableTpe(_, _)) => false + case (MonixObservableTpe(_, _), Fs2StreamTpe(_, _)) => false + case _ => true + } + + val isMonixObservable: Boolean = List(request, response).collect { + case m: MonixObservableTpe => m + }.nonEmpty + + require( + validStreamingComb, + s"RPC service $name has different streaming implementations for request and response") + + val prevalentStreamingTarget: TypeTypology = streamingType match { + case Some(ResponseStreaming) => response + case _ => request + } + + } + trait SupressWarts[T] { def supressWarts(warts: String*)(t: T): T } @@ -117,7 +169,10 @@ object serviceImpl { params <- d.vparamss _ = require(params.length == 1, s"RPC call ${d.name} has more than one request parameter") p <- params.headOption.toList - } yield RpcRequest(d.name, p.tpt, d.tpt, compressionType(serviceDef.mods.annotations)) + } yield + RpcRequest( + Operation(d.name, TypeTypology(p.tpt), TypeTypology(d.tpt)), + compressionType(serviceDef.mods.annotations)) val imports: List[Tree] = defs.collect { case imp: Import => imp @@ -268,14 +323,14 @@ object serviceImpl { //todo: validate that the request and responses are case classes, if possible case class RpcRequest( - methodName: TermName, - requestType: Tree, - responseType: Tree, + operation: Operation, compressionOption: Tree ) { - private val requestStreamingImpl: Option[StreamingImpl] = streamingImplFor(requestType) - private val responseStreamingImpl: Option[StreamingImpl] = streamingImplFor(responseType) + private val requestStreamingImpl: Option[StreamingImpl] = streamingImplFor( + operation.request.getTpe) + private val responseStreamingImpl: Option[StreamingImpl] = streamingImplFor( + operation.response.getTpe) private val streamingImpls: Set[StreamingImpl] = Set(requestStreamingImpl, responseStreamingImpl).flatten require( @@ -312,13 +367,13 @@ object serviceImpl { q"_root_.io.grpc.MethodDescriptor.MethodType.${TermName(suffix)}" } - private val methodDescriptorName = TermName(methodName + "MethodDescriptor") + private val methodDescriptorName = TermName(operation.name + "MethodDescriptor") - private val reqType = requestType match { + private val reqType = operation.request.getTpe match { case tq"$s[..$tpts]" if requestStreamingImpl.isDefined => tpts.last case other => other } - private val respType = responseType match { + private val respType = operation.response.getTpe match { case tq"$x[..$tpts]" => tpts.last } @@ -334,7 +389,7 @@ object serviceImpl { .setType($streamingMethodType) .setFullMethodName( _root_.io.grpc.MethodDescriptor.generateFullMethodName(${lit(serviceName)}, ${lit( - methodName)})) + operation.name)})) .build() } """.supressWarts("Null", "ExplicitImplicitTypes") @@ -345,43 +400,44 @@ object serviceImpl { val clientDef: Tree = streamingType match { case Some(RequestStreaming) => q""" - def $methodName(input: $requestType): $responseType = ${clientCallMethodFor( + def ${operation.name}(input: ${operation.request.getTpe}): ${operation.response.getTpe} = ${clientCallMethodFor( "clientStreaming")}""" case Some(ResponseStreaming) => q""" - def $methodName(input: $requestType): $responseType = ${clientCallMethodFor( + def ${operation.name}(input: ${operation.request.getTpe}): ${operation.response.getTpe} = ${clientCallMethodFor( "serverStreaming")}""" case Some(BidirectionalStreaming) => q""" - def $methodName(input: $requestType): $responseType = ${clientCallMethodFor( + def ${operation.name}(input: ${operation.request.getTpe}): ${operation.response.getTpe} = ${clientCallMethodFor( "bidiStreaming")}""" case None => q""" - def $methodName(input: $requestType): $responseType = ${clientCallMethodFor("unary")}""" + def ${operation.name}(input: ${operation.request.getTpe}): ${operation.response.getTpe} = ${clientCallMethodFor( + "unary")}""" } private def serverCallMethodFor(serverMethodName: String) = - q"_root_.higherkindness.mu.rpc.internal.server.monixCalls.${TermName(serverMethodName)}(algebra.$methodName, $compressionOption)" + q"_root_.higherkindness.mu.rpc.internal.server.monixCalls.${TermName(serverMethodName)}(algebra.${operation.name}, $compressionOption)" val descriptorAndHandler: Tree = { val handler = (streamingType, streamingImpl) match { case (Some(RequestStreaming), Some(Fs2Stream)) => - q"_root_.higherkindness.mu.rpc.internal.server.fs2Calls.clientStreamingMethod(algebra.$methodName, $compressionOption)" + q"_root_.higherkindness.mu.rpc.internal.server.fs2Calls.clientStreamingMethod(algebra.${operation.name}, $compressionOption)" case (Some(RequestStreaming), Some(MonixObservable)) => q"_root_.io.grpc.stub.ServerCalls.asyncClientStreamingCall(${serverCallMethodFor("clientStreamingMethod")})" case (Some(ResponseStreaming), Some(Fs2Stream)) => - q"_root_.higherkindness.mu.rpc.internal.server.fs2Calls.serverStreamingMethod(algebra.$methodName, $compressionOption)" + q"_root_.higherkindness.mu.rpc.internal.server.fs2Calls.serverStreamingMethod(algebra.${operation.name}, $compressionOption)" case (Some(ResponseStreaming), Some(MonixObservable)) => q"_root_.io.grpc.stub.ServerCalls.asyncServerStreamingCall(${serverCallMethodFor("serverStreamingMethod")})" case (Some(BidirectionalStreaming), Some(Fs2Stream)) => - q"_root_.higherkindness.mu.rpc.internal.server.fs2Calls.bidiStreamingMethod(algebra.$methodName, $compressionOption)" + q"_root_.higherkindness.mu.rpc.internal.server.fs2Calls.bidiStreamingMethod(algebra.${operation.name}, $compressionOption)" case (Some(BidirectionalStreaming), Some(MonixObservable)) => q"_root_.io.grpc.stub.ServerCalls.asyncBidiStreamingCall(${serverCallMethodFor("bidiStreamingMethod")})" case (None, None) => - q"_root_.io.grpc.stub.ServerCalls.asyncUnaryCall(_root_.higherkindness.mu.rpc.internal.server.unaryCalls.unaryMethod(algebra.$methodName, $compressionOption))" + q"_root_.io.grpc.stub.ServerCalls.asyncUnaryCall(_root_.higherkindness.mu.rpc.internal.server.unaryCalls.unaryMethod(algebra.${operation.name}, $compressionOption))" case _ => sys.error( - s"Unable to define a handler for the streaming type $streamingType and $streamingImpl for the method $methodName in the service $serviceName") + s"Unable to define a handler for the streaming type $streamingType and $streamingImpl for the method ${operation.name} in the service $serviceName") } q"($methodDescriptorName, $handler)" } @@ -393,58 +449,6 @@ object serviceImpl { //TODO: derive server as well //TODO: move HTTP-related code to its own module (on last attempt this did not work) - abstract class TypeTypology(tpe: Tree, inner: Option[Tree], isStreaming: Boolean) { - def getTpe: Tree = tpe - def getInner: Option[Tree] = inner - def streaming: Boolean = isStreaming - def safeInner: Tree = inner.getOrElse(tpe) - } - object TypeTypology { - def apply(t: Tree): TypeTypology = t match { - case tq"Observable[..$tpts]" => MonixObservableTpe(t, tpts.headOption) - case tq"Stream[$carrier, ..$tpts]" => Fs2StreamTpe(t, tpts.headOption) - case tq"Empty.type" => EmptyTpe(t) - case tq"$carrier[..$tpts]" => UnaryTpe(t, tpts.headOption) - } - } - case class EmptyTpe(tpe: Tree) extends TypeTypology(tpe, None, false) - case class UnaryTpe(tpe: Tree, inner: Option[Tree]) extends TypeTypology(tpe, inner, false) - case class Fs2StreamTpe(tpe: Tree, inner: Option[Tree]) extends TypeTypology(tpe, inner, true) - case class MonixObservableTpe(tpe: Tree, inner: Option[Tree]) - extends TypeTypology(tpe, inner, true) - - case class Operation(name: TermName, request: TypeTypology, response: TypeTypology) { - - val isStreaming: Boolean = request.streaming || response.streaming - - val streamingType: Option[StreamingType] = (request.streaming, response.streaming) match { - case (true, true) => Some(BidirectionalStreaming) - case (true, false) => Some(RequestStreaming) - case (false, true) => Some(ResponseStreaming) - case _ => None - } - - val validStreamingComb: Boolean = (request, response) match { - case (Fs2StreamTpe(_, _), MonixObservableTpe(_, _)) => false - case (MonixObservableTpe(_, _), Fs2StreamTpe(_, _)) => false - case _ => true - } - - val isMonixObservable: Boolean = List(request, response).collect { - case m: MonixObservableTpe => m - }.nonEmpty - - require( - validStreamingComb, - s"RPC service $name has different streaming implementations for request and response") - - val prevalentStreamingTarget: TypeTypology = streamingType match { - case Some(ResponseStreaming) => response - case _ => request - } - - } - case class HttpOperation(operation: Operation) { import operation._ @@ -576,7 +580,7 @@ object serviceImpl { service.client, service.clientFromChannel, service.unsafeClient, - service.unsafeClientFromChannel, + service.unsafeClientFromChannel ) ++ service.http ) ) From 6db7ab35aea5f183be12e7ee5799605d78eadc75 Mon Sep 17 00:00:00 2001 From: rafaparadela Date: Tue, 26 Feb 2019 16:12:38 -0800 Subject: [PATCH 20/35] applies the refactoring again with the fix --- .../mu/rpc/internal/serviceImpl.scala | 109 +++++++----------- 1 file changed, 41 insertions(+), 68 deletions(-) diff --git a/modules/internal/src/main/scala/higherkindness/mu/rpc/internal/serviceImpl.scala b/modules/internal/src/main/scala/higherkindness/mu/rpc/internal/serviceImpl.scala index 479e92b6c..1d72cebe3 100644 --- a/modules/internal/src/main/scala/higherkindness/mu/rpc/internal/serviceImpl.scala +++ b/modules/internal/src/main/scala/higherkindness/mu/rpc/internal/serviceImpl.scala @@ -28,11 +28,17 @@ object serviceImpl { import c.universe._ import Flag._ - abstract class TypeTypology(tpe: Tree, inner: Option[Tree], isStreaming: Boolean) { + abstract class TypeTypology(tpe: Tree, inner: Option[Tree], isStreaming: Boolean) + extends Product + with Serializable { def getTpe: Tree = tpe def getInner: Option[Tree] = inner def streaming: Boolean = isStreaming def safeInner: Tree = inner.getOrElse(tpe) + def safeType: Tree = tpe match { + case tq"$s[..$tpts]" if isStreaming => tpts.last + case other => other + } } object TypeTypology { def apply(t: Tree): TypeTypology = t match { @@ -65,18 +71,16 @@ object serviceImpl { case _ => true } - val isMonixObservable: Boolean = List(request, response).collect { - case m: MonixObservableTpe => m - }.nonEmpty - require( validStreamingComb, s"RPC service $name has different streaming implementations for request and response") - val prevalentStreamingTarget: TypeTypology = streamingType match { - case Some(ResponseStreaming) => response - case _ => request - } + val isMonixObservable: Boolean = List(request, response).collect { + case m: MonixObservableTpe => m + }.nonEmpty + + val prevalentStreamingTarget: TypeTypology = + if (streamingType.contains(ResponseStreaming)) response else request } @@ -327,34 +331,12 @@ object serviceImpl { compressionOption: Tree ) { - private val requestStreamingImpl: Option[StreamingImpl] = streamingImplFor( - operation.request.getTpe) - private val responseStreamingImpl: Option[StreamingImpl] = streamingImplFor( - operation.response.getTpe) - private val streamingImpls: Set[StreamingImpl] = - Set(requestStreamingImpl, responseStreamingImpl).flatten - require( - streamingImpls.size < 2, - s"RPC service $serviceName has different streaming implementations for request and response") - private val streamingImpl: Option[StreamingImpl] = streamingImpls.headOption - - private val streamingType: Option[StreamingType] = - if (requestStreamingImpl.isDefined && responseStreamingImpl.isDefined) - Some(BidirectionalStreaming) - else if (requestStreamingImpl.isDefined) Some(RequestStreaming) - else if (responseStreamingImpl.isDefined) Some(ResponseStreaming) - else None - - private def streamingImplFor(t: Tree): Option[StreamingImpl] = t match { - case tq"$tpt[..$tpts]" if tpt.toString.endsWith("Observable") => Some(MonixObservable) - case tq"$tpt[..$tpts]" if tpt.toString.endsWith("Stream") => Some(Fs2Stream) - case _ => None - } + import operation._ - private val clientCallsImpl = streamingImpl match { - case Some(Fs2Stream) => q"_root_.higherkindness.mu.rpc.internal.client.fs2Calls" - case Some(MonixObservable) => q"_root_.higherkindness.mu.rpc.internal.client.monixCalls" - case None => q"_root_.higherkindness.mu.rpc.internal.client.unaryCalls" + private val clientCallsImpl = prevalentStreamingTarget match { + case _: Fs2StreamTpe => q"_root_.higherkindness.mu.rpc.internal.client.fs2Calls" + case _: MonixObservableTpe => q"_root_.higherkindness.mu.rpc.internal.client.monixCalls" + case _ => q"_root_.higherkindness.mu.rpc.internal.client.unaryCalls" } private val streamingMethodType = { @@ -367,15 +349,11 @@ object serviceImpl { q"_root_.io.grpc.MethodDescriptor.MethodType.${TermName(suffix)}" } - private val methodDescriptorName = TermName(operation.name + "MethodDescriptor") + private val methodDescriptorName = TermName(name + "MethodDescriptor") - private val reqType = operation.request.getTpe match { - case tq"$s[..$tpts]" if requestStreamingImpl.isDefined => tpts.last - case other => other - } - private val respType = operation.response.getTpe match { - case tq"$x[..$tpts]" => tpts.last - } + private val reqType = request.safeType + + private val respType = response.safeInner val methodDescriptor: DefDef = q""" def $methodDescriptorName(implicit @@ -389,7 +367,7 @@ object serviceImpl { .setType($streamingMethodType) .setFullMethodName( _root_.io.grpc.MethodDescriptor.generateFullMethodName(${lit(serviceName)}, ${lit( - operation.name)})) + name)})) .build() } """.supressWarts("Null", "ExplicitImplicitTypes") @@ -400,44 +378,43 @@ object serviceImpl { val clientDef: Tree = streamingType match { case Some(RequestStreaming) => q""" - def ${operation.name}(input: ${operation.request.getTpe}): ${operation.response.getTpe} = ${clientCallMethodFor( + def $name(input: ${request.getTpe}): ${response.getTpe} = ${clientCallMethodFor( "clientStreaming")}""" case Some(ResponseStreaming) => q""" - def ${operation.name}(input: ${operation.request.getTpe}): ${operation.response.getTpe} = ${clientCallMethodFor( + def $name(input: ${request.getTpe}): ${response.getTpe} = ${clientCallMethodFor( "serverStreaming")}""" case Some(BidirectionalStreaming) => q""" - def ${operation.name}(input: ${operation.request.getTpe}): ${operation.response.getTpe} = ${clientCallMethodFor( + def $name(input: ${request.getTpe}): ${response.getTpe} = ${clientCallMethodFor( "bidiStreaming")}""" case None => q""" - def ${operation.name}(input: ${operation.request.getTpe}): ${operation.response.getTpe} = ${clientCallMethodFor( - "unary")}""" + def $name(input: ${request.getTpe}): ${response.getTpe} = ${clientCallMethodFor("unary")}""" } private def serverCallMethodFor(serverMethodName: String) = - q"_root_.higherkindness.mu.rpc.internal.server.monixCalls.${TermName(serverMethodName)}(algebra.${operation.name}, $compressionOption)" + q"_root_.higherkindness.mu.rpc.internal.server.monixCalls.${TermName(serverMethodName)}(algebra.$name, $compressionOption)" val descriptorAndHandler: Tree = { - val handler = (streamingType, streamingImpl) match { - case (Some(RequestStreaming), Some(Fs2Stream)) => - q"_root_.higherkindness.mu.rpc.internal.server.fs2Calls.clientStreamingMethod(algebra.${operation.name}, $compressionOption)" - case (Some(RequestStreaming), Some(MonixObservable)) => + val handler = (streamingType, prevalentStreamingTarget) match { + case (Some(RequestStreaming), Fs2StreamTpe(_, _)) => + q"_root_.higherkindness.mu.rpc.internal.server.fs2Calls.clientStreamingMethod(algebra.$name, $compressionOption)" + case (Some(RequestStreaming), MonixObservableTpe(_, _)) => q"_root_.io.grpc.stub.ServerCalls.asyncClientStreamingCall(${serverCallMethodFor("clientStreamingMethod")})" - case (Some(ResponseStreaming), Some(Fs2Stream)) => - q"_root_.higherkindness.mu.rpc.internal.server.fs2Calls.serverStreamingMethod(algebra.${operation.name}, $compressionOption)" - case (Some(ResponseStreaming), Some(MonixObservable)) => + case (Some(ResponseStreaming), Fs2StreamTpe(_, _)) => + q"_root_.higherkindness.mu.rpc.internal.server.fs2Calls.serverStreamingMethod(algebra.$name, $compressionOption)" + case (Some(ResponseStreaming), MonixObservableTpe(_, _)) => q"_root_.io.grpc.stub.ServerCalls.asyncServerStreamingCall(${serverCallMethodFor("serverStreamingMethod")})" - case (Some(BidirectionalStreaming), Some(Fs2Stream)) => - q"_root_.higherkindness.mu.rpc.internal.server.fs2Calls.bidiStreamingMethod(algebra.${operation.name}, $compressionOption)" - case (Some(BidirectionalStreaming), Some(MonixObservable)) => + case (Some(BidirectionalStreaming), Fs2StreamTpe(_, _)) => + q"_root_.higherkindness.mu.rpc.internal.server.fs2Calls.bidiStreamingMethod(algebra.$name, $compressionOption)" + case (Some(BidirectionalStreaming), MonixObservableTpe(_, _)) => q"_root_.io.grpc.stub.ServerCalls.asyncBidiStreamingCall(${serverCallMethodFor("bidiStreamingMethod")})" - case (None, None) => - q"_root_.io.grpc.stub.ServerCalls.asyncUnaryCall(_root_.higherkindness.mu.rpc.internal.server.unaryCalls.unaryMethod(algebra.${operation.name}, $compressionOption))" + case (None, _) => + q"_root_.io.grpc.stub.ServerCalls.asyncUnaryCall(_root_.higherkindness.mu.rpc.internal.server.unaryCalls.unaryMethod(algebra.$name, $compressionOption))" case _ => sys.error( - s"Unable to define a handler for the streaming type $streamingType and $streamingImpl for the method ${operation.name} in the service $serviceName") + s"Unable to define a handler for the streaming type $streamingType and $prevalentStreamingTarget for the method $name in the service $serviceName") } q"($methodDescriptorName, $handler)" } @@ -591,8 +568,4 @@ object serviceImpl { } } -sealed trait StreamingImpl extends Product with Serializable -case object Fs2Stream extends StreamingImpl -case object MonixObservable extends StreamingImpl - // $COVERAGE-ON$ From 2a9cd3df4cdc69fae0342bb187159ec91ca5974b Mon Sep 17 00:00:00 2001 From: rafaparadela Date: Wed, 27 Feb 2019 16:56:59 -0800 Subject: [PATCH 21/35] derived the simplest http route that only serves GET calls --- .../mu/rpc/http/GreeterDerivedRestTests.scala | 231 +++++ .../mu/rpc/http/GreeterRestServices.scala | 11 +- .../mu/rpc/http/GreeterRestTests.scala | 831 +++++++++--------- .../mu/rpc/http/GreeterServices.scala | 28 +- .../mu/rpc/internal/serviceImpl.scala | 72 +- 5 files changed, 748 insertions(+), 425 deletions(-) create mode 100644 modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterDerivedRestTests.scala diff --git a/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterDerivedRestTests.scala b/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterDerivedRestTests.scala new file mode 100644 index 000000000..0d02cb321 --- /dev/null +++ b/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterDerivedRestTests.scala @@ -0,0 +1,231 @@ +/* + * Copyright 2017-2019 47 Degrees, LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package higherkindness.mu.rpc.http + +import cats.effect.{IO, _} +import fs2.Stream +import higherkindness.mu.rpc.common.RpcBaseTestSuite +import higherkindness.mu.rpc.http.Utils._ +import io.circe.Json +import io.circe.generic.auto._ +import io.circe.syntax._ +import monix.reactive.Observable +import org.http4s._ +import org.http4s.circe._ +import org.http4s.client.UnexpectedStatus +import org.http4s.client.blaze.BlazeClientBuilder +import org.http4s.dsl.io._ +import org.http4s.server._ +import org.http4s.server.blaze._ +import org.scalatest._ +import org.scalatest.prop.GeneratorDrivenPropertyChecks + +import scala.concurrent.duration._ + +class GreeterDerivedRestTests + extends RpcBaseTestSuite + with GeneratorDrivenPropertyChecks + with BeforeAndAfter { + + val Hostname = "localhost" + val Port = 8080 + + val serviceUri: Uri = Uri.unsafeFromString(s"http://$Hostname:$Port") + + val UnaryServicePrefix = "unary" +// val Fs2ServicePrefix = "fs2" +// val MonixServicePrefix = "monix" + + implicit val ec = monix.execution.Scheduler.Implicits.global + implicit val cs: ContextShift[IO] = IO.contextShift(ec) + + implicit val unaryHandlerIO = new UnaryGreeterHandler[IO] + + val unaryService: HttpRoutes[IO] = new UnaryGreeterRestService[IO].service + + val unaryRoute: HttpRoutes[IO] = UnaryGreeter.route[IO] + + val server: BlazeServerBuilder[IO] = BlazeServerBuilder[IO] + .bindHttp(Port, Hostname) + .withHttpApp( + Router( + s"/$UnaryServicePrefix" -> unaryRoute, +// s"/$Fs2ServicePrefix" -> fs2Service, +// s"/$MonixServicePrefix" -> monixService + ).orNotFound) + + var serverTask: Fiber[IO, Nothing] = _ // sorry + before(serverTask = server.resource.use(_ => IO.never).start.unsafeRunSync()) + after(serverTask.cancel) + + val unaryServiceClient: UnaryGreeterRestClient[IO] = + new UnaryGreeterRestClient[IO](serviceUri / UnaryServicePrefix) +// val fs2ServiceClient: Fs2GreeterRestClient[IO] = new Fs2GreeterRestClient[IO](serviceUri / Fs2ServicePrefix) +// val monixServiceClient: MonixGreeterRestClient[IO] = new MonixGreeterRestClient[IO](serviceUri / MonixServicePrefix) + + "REST Service" should { + + "serve a GET request" in { + val response = BlazeClientBuilder[IO](ec).resource.use(unaryServiceClient.getHello()(_)) + response.unsafeRunSync() shouldBe HelloResponse("hey") + } +// "serve a GET request" in { +// val response = BlazeClientBuilder[IO](ec).resource.use(unaryServiceClient.getHello()(_)) +// response.unsafeRunSync() shouldBe HelloResponse("hey") +// } +// +// "serve a unary POST request" in { +// val request = HelloRequest("hey") +// val response = +// BlazeClientBuilder[IO](ec).resource.use(unaryServiceClient.sayHello(request)(_)) +// response.unsafeRunSync() shouldBe HelloResponse("hey") +// } +// +// "handle a raised gRPC exception in a unary POST request" in { +// val request = HelloRequest("SRE") +// val response = +// BlazeClientBuilder[IO](ec).resource.use(unaryServiceClient.sayHello(request)(_)) +// the[ResponseError] thrownBy response.unsafeRunSync() shouldBe ResponseError( +// Status.BadRequest, +// Some("INVALID_ARGUMENT: SRE")) +// } +// +// "handle a raised non-gRPC exception in a unary POST request" in { +// val request = HelloRequest("RTE") +// val response = +// BlazeClientBuilder[IO](ec).resource.use(unaryServiceClient.sayHello(request)(_)) +// the[ResponseError] thrownBy response.unsafeRunSync() shouldBe ResponseError( +// Status.InternalServerError, +// Some("RTE")) +// } +// +// "handle a thrown exception in a unary POST request" in { +// val request = HelloRequest("TR") +// val response = +// BlazeClientBuilder[IO](ec).resource.use(unaryServiceClient.sayHello(request)(_)) +// the[ResponseError] thrownBy response.unsafeRunSync() shouldBe ResponseError( +// Status.InternalServerError) +// } +// +// "serve a POST request with fs2 streaming request" in { +// val requests = Stream(HelloRequest("hey"), HelloRequest("there")) +// val response = +// BlazeClientBuilder[IO](ec).resource.use(fs2ServiceClient.sayHellos(requests)(_)) +// response.unsafeRunSync() shouldBe HelloResponse("hey, there") +// } +// +// "serve a POST request with empty fs2 streaming request" in { +// val requests = Stream.empty +// val response = +// BlazeClientBuilder[IO](ec).resource.use(fs2ServiceClient.sayHellos(requests)(_)) +// response.unsafeRunSync() shouldBe HelloResponse("") +// } +// +// "serve a POST request with Observable streaming request" in { +// val requests = Observable(HelloRequest("hey"), HelloRequest("there")) +// val response = +// BlazeClientBuilder[IO](ec).resource.use(monixServiceClient.sayHellos(requests)(_)) +// response.unsafeRunSync() shouldBe HelloResponse("hey, there") +// } +// +// "serve a POST request with empty Observable streaming request" in { +// val requests = Observable.empty +// val response = +// BlazeClientBuilder[IO](ec).resource.use(monixServiceClient.sayHellos(requests)(_)) +// response.unsafeRunSync() shouldBe HelloResponse("") +// } +// +// "serve a POST request with fs2 streaming response" in { +// val request = HelloRequest("hey") +// val responses = +// BlazeClientBuilder[IO](ec).stream.flatMap(fs2ServiceClient.sayHelloAll(request)(_)) +// responses.compile.toList +// .unsafeRunSync() shouldBe List(HelloResponse("hey"), HelloResponse("hey")) +// } +// +// "serve a POST request with Observable streaming response" in { +// val request = HelloRequest("hey") +// val responses = BlazeClientBuilder[IO](ec).stream +// .flatMap(monixServiceClient.sayHelloAll(request)(_).toFs2Stream[IO]) +// responses.compile.toList +// .unsafeRunTimed(10.seconds) +// .getOrElse(sys.error("Stuck!")) shouldBe List(HelloResponse("hey"), HelloResponse("hey")) +// } +// +// "handle errors with fs2 streaming response" in { +// val request = HelloRequest("") +// val responses = +// BlazeClientBuilder[IO](ec).stream.flatMap(fs2ServiceClient.sayHelloAll(request)(_)) +// the[IllegalArgumentException] thrownBy responses.compile.toList +// .unsafeRunSync() should have message "empty greeting" +// } +// +// "handle errors with Observable streaming response" in { +// val request = HelloRequest("") +// val responses = BlazeClientBuilder[IO](ec).stream +// .flatMap(monixServiceClient.sayHelloAll(request)(_).toFs2Stream[IO]) +// the[IllegalArgumentException] thrownBy responses.compile.toList +// .unsafeRunTimed(10.seconds) +// .getOrElse(sys.error("Stuck!")) should have message "empty greeting" +// } +// +// "serve a POST request with bidirectional fs2 streaming" in { +// val requests = Stream(HelloRequest("hey"), HelloRequest("there")) +// val responses = +// BlazeClientBuilder[IO](ec).stream.flatMap(fs2ServiceClient.sayHellosAll(requests)(_)) +// responses.compile.toList +// .unsafeRunSync() shouldBe List(HelloResponse("hey"), HelloResponse("there")) +// } +// +// "serve an empty POST request with bidirectional fs2 streaming" in { +// val requests = Stream.empty +// val responses = +// BlazeClientBuilder[IO](ec).stream.flatMap(fs2ServiceClient.sayHellosAll(requests)(_)) +// responses.compile.toList.unsafeRunSync() shouldBe Nil +// } +// +// "serve a POST request with bidirectional Observable streaming" in { +// val requests = Observable(HelloRequest("hey"), HelloRequest("there")) +// val responses = BlazeClientBuilder[IO](ec).stream +// .flatMap(monixServiceClient.sayHellosAll(requests)(_).toFs2Stream[IO]) +// responses.compile.toList +// .unsafeRunTimed(10.seconds) +// .getOrElse(sys.error("Stuck!")) shouldBe List(HelloResponse("hey"), HelloResponse("there")) +// } +// +// "serve an empty POST request with bidirectional Observable streaming" in { +// val requests = Observable.empty +// val responses = BlazeClientBuilder[IO](ec).stream +// .flatMap(monixServiceClient.sayHellosAll(requests)(_).toFs2Stream[IO]) +// responses.compile.toList +// .unsafeRunTimed(10.seconds) +// .getOrElse(sys.error("Stuck!")) shouldBe Nil +// } +// +// "serve ScalaCheck-generated POST requests with bidirectional Observable streaming" in { +// forAll { strings: List[String] => +// val requests = Observable.fromIterable(strings.map(HelloRequest)) +// val responses = BlazeClientBuilder[IO](ec).stream +// .flatMap(monixServiceClient.sayHellosAll(requests)(_).toFs2Stream[IO]) +// responses.compile.toList +// .unsafeRunTimed(10.seconds) +// .getOrElse(sys.error("Stuck!")) shouldBe strings.map(HelloResponse) +// } +// } + } + +} diff --git a/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterRestServices.scala b/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterRestServices.scala index 49a51a2b8..e7470986f 100644 --- a/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterRestServices.scala +++ b/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterRestServices.scala @@ -27,9 +27,7 @@ import org.http4s._ import org.http4s.circe._ import org.http4s.dsl.Http4sDsl -class UnaryGreeterRestService[F[_]: Sync](handler: UnaryGreeter[F])( - implicit F: MonadError[F, Throwable]) - extends Http4sDsl[F] { +class UnaryGreeterRestService[F[_]: Sync](implicit handler: UnaryGreeter[F]) extends Http4sDsl[F] { import higherkindness.mu.rpc.protocol.Empty @@ -47,7 +45,7 @@ class UnaryGreeterRestService[F[_]: Sync](handler: UnaryGreeter[F])( } } -class Fs2GreeterRestService[F[_]: Sync](handler: Fs2Greeter[F]) extends Http4sDsl[F] { +class Fs2GreeterRestService[F[_]: Sync](implicit handler: Fs2Greeter[F]) extends Http4sDsl[F] { private implicit val requestDecoder: EntityDecoder[F, HelloRequest] = jsonOf[F, HelloRequest] @@ -69,8 +67,9 @@ class Fs2GreeterRestService[F[_]: Sync](handler: Fs2Greeter[F]) extends Http4sDs } } -class MonixGreeterRestService[F[_]: ConcurrentEffect](handler: MonixGreeter[F])( - implicit sc: monix.execution.Scheduler) +class MonixGreeterRestService[F[_]: ConcurrentEffect]( + implicit handler: MonixGreeter[F], + sc: monix.execution.Scheduler) extends Http4sDsl[F] { private implicit val requestDecoder: EntityDecoder[F, HelloRequest] = jsonOf[F, HelloRequest] diff --git a/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterRestTests.scala b/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterRestTests.scala index a67fedfe6..fdfe32d45 100644 --- a/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterRestTests.scala +++ b/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterRestTests.scala @@ -14,410 +14,427 @@ * limitations under the License. */ -package higherkindness.mu.rpc.http - -import cats.effect.{IO, _} -import fs2.Stream -import higherkindness.mu.rpc.common.RpcBaseTestSuite -import higherkindness.mu.rpc.http.Utils._ -import higherkindness.mu.rpc.protocol.Empty -import io.circe.Json -import io.circe.generic.auto._ -import io.circe.syntax._ -import monix.reactive.Observable -import org.http4s._ -import org.http4s.circe._ -import org.http4s.client.UnexpectedStatus -import org.http4s.client.blaze.BlazeClientBuilder -import org.http4s.dsl.io._ -import org.http4s.server._ -import org.http4s.server.blaze._ -import org.scalatest._ -import org.scalatest.prop.GeneratorDrivenPropertyChecks - -import scala.concurrent.duration._ - -class GreeterRestTests - extends RpcBaseTestSuite - with GeneratorDrivenPropertyChecks - with BeforeAndAfter { - - val Hostname = "localhost" - val Port = 8080 - - val serviceUri: Uri = Uri.unsafeFromString(s"http://$Hostname:$Port") - - val UnaryServicePrefix = "unary" - val Fs2ServicePrefix = "fs2" - val MonixServicePrefix = "monix" - - implicit val ec = monix.execution.Scheduler.Implicits.global - implicit val cs: ContextShift[IO] = IO.contextShift(ec) - - //TODO: add Logger middleware - val unaryService: HttpRoutes[IO] = - new UnaryGreeterRestService[IO](new UnaryGreeterHandler[IO]).service - val fs2Service: HttpRoutes[IO] = - new Fs2GreeterRestService[IO](new Fs2GreeterHandler[IO]).service - val monixService: HttpRoutes[IO] = - new MonixGreeterRestService[IO](new MonixGreeterHandler[IO]).service - - val server: BlazeServerBuilder[IO] = BlazeServerBuilder[IO] - .bindHttp(Port, Hostname) - //.enableHttp2(true) - //.withSSLContext(GenericSSLContext.serverSSLContext) - .withHttpApp( - Router( - s"/$UnaryServicePrefix" -> unaryService, - s"/$Fs2ServicePrefix" -> fs2Service, - s"/$MonixServicePrefix" -> monixService).orNotFound) - - var serverTask: Fiber[IO, Nothing] = _ // sorry - before(serverTask = server.resource.use(_ => IO.never).start.unsafeRunSync()) - after(serverTask.cancel) - - "REST Server" should { - - "serve a GET request" in { - val request = Request[IO](Method.GET, serviceUri / UnaryServicePrefix / "getHello") - val response = BlazeClientBuilder[IO](ec).resource.use(_.expect[Json](request)) - response.unsafeRunSync() shouldBe HelloResponse("hey").asJson - } - - "serve a POST request" in { - val request = Request[IO](Method.POST, serviceUri / UnaryServicePrefix / "sayHello") - val requestBody = HelloRequest("hey").asJson - val response = - BlazeClientBuilder[IO](ec).resource.use(_.expect[Json](request.withEntity(requestBody))) - response.unsafeRunSync() shouldBe HelloResponse("hey").asJson - } - - "return a 400 Bad Request for a malformed unary POST request" in { - val request = Request[IO](Method.POST, serviceUri / UnaryServicePrefix / "sayHello") - val requestBody = "{" - val response = - BlazeClientBuilder[IO](ec).resource.use(_.expect[Json](request.withEntity(requestBody))) - the[UnexpectedStatus] thrownBy response.unsafeRunSync() shouldBe UnexpectedStatus( - Status.BadRequest) - } - - "return a 400 Bad Request for a malformed streaming POST request" in { - val request = Request[IO](Method.POST, serviceUri / Fs2ServicePrefix / "sayHellos") - val requestBody = "{" - val response = - BlazeClientBuilder[IO](ec).resource.use(_.expect[Json](request.withEntity(requestBody))) - the[UnexpectedStatus] thrownBy response.unsafeRunSync() shouldBe UnexpectedStatus( - Status.BadRequest) - } - - } - - val unaryServiceClient: UnaryGreeterRestClient[IO] = - new UnaryGreeterRestClient[IO](serviceUri / UnaryServicePrefix) - val fs2ServiceClient: Fs2GreeterRestClient[IO] = - new Fs2GreeterRestClient[IO](serviceUri / Fs2ServicePrefix) - val monixServiceClient: MonixGreeterRestClient[IO] = - new MonixGreeterRestClient[IO](serviceUri / MonixServicePrefix) - - "REST Service" should { - - "serve a GET request" in { - val response = BlazeClientBuilder[IO](ec).resource.use(unaryServiceClient.getHello()(_)) - response.unsafeRunSync() shouldBe HelloResponse("hey") - } - - "serve a unary POST request" in { - val request = HelloRequest("hey") - val response = - BlazeClientBuilder[IO](ec).resource.use(unaryServiceClient.sayHello(request)(_)) - response.unsafeRunSync() shouldBe HelloResponse("hey") - } - - "handle a raised gRPC exception in a unary POST request" in { - val request = HelloRequest("SRE") - val response = - BlazeClientBuilder[IO](ec).resource.use(unaryServiceClient.sayHello(request)(_)) - the[ResponseError] thrownBy response.unsafeRunSync() shouldBe ResponseError( - Status.BadRequest, - Some("INVALID_ARGUMENT: SRE")) - } - - "handle a raised non-gRPC exception in a unary POST request" in { - val request = HelloRequest("RTE") - val response = - BlazeClientBuilder[IO](ec).resource.use(unaryServiceClient.sayHello(request)(_)) - the[ResponseError] thrownBy response.unsafeRunSync() shouldBe ResponseError( - Status.InternalServerError, - Some("RTE")) - } - - "handle a thrown exception in a unary POST request" in { - val request = HelloRequest("TR") - val response = - BlazeClientBuilder[IO](ec).resource.use(unaryServiceClient.sayHello(request)(_)) - the[ResponseError] thrownBy response.unsafeRunSync() shouldBe ResponseError( - Status.InternalServerError) - } - - "serve a POST request with fs2 streaming request" in { - val requests = Stream(HelloRequest("hey"), HelloRequest("there")) - val response = - BlazeClientBuilder[IO](ec).resource.use(fs2ServiceClient.sayHellos(requests)(_)) - response.unsafeRunSync() shouldBe HelloResponse("hey, there") - } - - "serve a POST request with empty fs2 streaming request" in { - val requests = Stream.empty - val response = - BlazeClientBuilder[IO](ec).resource.use(fs2ServiceClient.sayHellos(requests)(_)) - response.unsafeRunSync() shouldBe HelloResponse("") - } - - "serve a POST request with Observable streaming request" in { - val requests = Observable(HelloRequest("hey"), HelloRequest("there")) - val response = - BlazeClientBuilder[IO](ec).resource.use(monixServiceClient.sayHellos(requests)(_)) - response.unsafeRunSync() shouldBe HelloResponse("hey, there") - } - - "serve a POST request with empty Observable streaming request" in { - val requests = Observable.empty - val response = - BlazeClientBuilder[IO](ec).resource.use(monixServiceClient.sayHellos(requests)(_)) - response.unsafeRunSync() shouldBe HelloResponse("") - } - - "serve a POST request with fs2 streaming response" in { - val request = HelloRequest("hey") - val responses = - BlazeClientBuilder[IO](ec).stream.flatMap(fs2ServiceClient.sayHelloAll(request)(_)) - responses.compile.toList - .unsafeRunSync() shouldBe List(HelloResponse("hey"), HelloResponse("hey")) - } - - "serve a POST request with Observable streaming response" in { - val request = HelloRequest("hey") - val responses = BlazeClientBuilder[IO](ec).stream - .flatMap(monixServiceClient.sayHelloAll(request)(_).toFs2Stream[IO]) - responses.compile.toList - .unsafeRunTimed(10.seconds) - .getOrElse(sys.error("Stuck!")) shouldBe List(HelloResponse("hey"), HelloResponse("hey")) - } - - "handle errors with fs2 streaming response" in { - val request = HelloRequest("") - val responses = - BlazeClientBuilder[IO](ec).stream.flatMap(fs2ServiceClient.sayHelloAll(request)(_)) - the[IllegalArgumentException] thrownBy responses.compile.toList - .unsafeRunSync() should have message "empty greeting" - } - - "handle errors with Observable streaming response" in { - val request = HelloRequest("") - val responses = BlazeClientBuilder[IO](ec).stream - .flatMap(monixServiceClient.sayHelloAll(request)(_).toFs2Stream[IO]) - the[IllegalArgumentException] thrownBy responses.compile.toList - .unsafeRunTimed(10.seconds) - .getOrElse(sys.error("Stuck!")) should have message "empty greeting" - } - - "serve a POST request with bidirectional fs2 streaming" in { - val requests = Stream(HelloRequest("hey"), HelloRequest("there")) - val responses = - BlazeClientBuilder[IO](ec).stream.flatMap(fs2ServiceClient.sayHellosAll(requests)(_)) - responses.compile.toList - .unsafeRunSync() shouldBe List(HelloResponse("hey"), HelloResponse("there")) - } - - "serve an empty POST request with bidirectional fs2 streaming" in { - val requests = Stream.empty - val responses = - BlazeClientBuilder[IO](ec).stream.flatMap(fs2ServiceClient.sayHellosAll(requests)(_)) - responses.compile.toList.unsafeRunSync() shouldBe Nil - } - - "serve a POST request with bidirectional Observable streaming" in { - val requests = Observable(HelloRequest("hey"), HelloRequest("there")) - val responses = BlazeClientBuilder[IO](ec).stream - .flatMap(monixServiceClient.sayHellosAll(requests)(_).toFs2Stream[IO]) - responses.compile.toList - .unsafeRunTimed(10.seconds) - .getOrElse(sys.error("Stuck!")) shouldBe List(HelloResponse("hey"), HelloResponse("there")) - } - - "serve an empty POST request with bidirectional Observable streaming" in { - val requests = Observable.empty - val responses = BlazeClientBuilder[IO](ec).stream - .flatMap(monixServiceClient.sayHellosAll(requests)(_).toFs2Stream[IO]) - responses.compile.toList - .unsafeRunTimed(10.seconds) - .getOrElse(sys.error("Stuck!")) shouldBe Nil - } - - "serve ScalaCheck-generated POST requests with bidirectional Observable streaming" in { - forAll { strings: List[String] => - val requests = Observable.fromIterable(strings.map(HelloRequest)) - val responses = BlazeClientBuilder[IO](ec).stream - .flatMap(monixServiceClient.sayHellosAll(requests)(_).toFs2Stream[IO]) - responses.compile.toList - .unsafeRunTimed(10.seconds) - .getOrElse(sys.error("Stuck!")) shouldBe strings.map(HelloResponse) - } - } - } - - "Auto-derived REST Client" should { - - val unaryClient = UnaryGreeter.httpClient[IO](serviceUri / UnaryServicePrefix) - val fs2Client = Fs2Greeter.httpClient[IO](serviceUri / Fs2ServicePrefix) - val monixClient = MonixGreeter.httpClient[IO](serviceUri / MonixServicePrefix) - - "serve a GET request" in { - val response: IO[HelloResponse] = - BlazeClientBuilder[IO](ec).resource.use(unaryClient.getHello(_)) - response.unsafeRunSync() shouldBe HelloResponse("hey") - } - - "serve a unary POST request" in { - val response: IO[HelloResponse] = - BlazeClientBuilder[IO](ec).resource.use(unaryClient.sayHello(HelloRequest("hey"))(_)) - response.unsafeRunSync() shouldBe HelloResponse("hey") - } - - "handle a raised gRPC exception in a unary POST request" in { - val response: IO[HelloResponse] = - BlazeClientBuilder[IO](ec).resource.use(unaryClient.sayHello(HelloRequest("SRE"))(_)) - - the[ResponseError] thrownBy response.unsafeRunSync() shouldBe ResponseError( - Status.BadRequest, - Some("INVALID_ARGUMENT: SRE")) - } - - "handle a raised non-gRPC exception in a unary POST request" in { - val response: IO[HelloResponse] = - BlazeClientBuilder[IO](ec).resource.use(unaryClient.sayHello(HelloRequest("RTE"))(_)) - - the[ResponseError] thrownBy response.unsafeRunSync() shouldBe ResponseError( - Status.InternalServerError, - Some("RTE")) - } - - "handle a thrown exception in a unary POST request" in { - val response: IO[HelloResponse] = - BlazeClientBuilder[IO](ec).resource.use(unaryClient.sayHello(HelloRequest("TR"))(_)) - - the[ResponseError] thrownBy response.unsafeRunSync() shouldBe ResponseError( - Status.InternalServerError) - } - - "serve a POST request with fs2 streaming request" in { - - val requests = Stream(HelloRequest("hey"), HelloRequest("there")) - - val response: IO[HelloResponse] = - BlazeClientBuilder[IO](ec).resource.use(fs2Client.sayHellos(requests)(_)) - response.unsafeRunSync() shouldBe HelloResponse("hey, there") - } - - "serve a POST request with empty fs2 streaming request" in { - val requests = Stream.empty - val response = - BlazeClientBuilder[IO](ec).resource.use(fs2Client.sayHellos(requests)(_)) - response.unsafeRunSync() shouldBe HelloResponse("") - } - - "serve a POST request with Observable streaming request" in { - val requests = Observable(HelloRequest("hey"), HelloRequest("there")) - val response = - BlazeClientBuilder[IO](ec).resource.use(monixClient.sayHellos(requests)(_)) - response.unsafeRunSync() shouldBe HelloResponse("hey, there") - } - - "serve a POST request with empty Observable streaming request" in { - val requests = Observable.empty - val response = - BlazeClientBuilder[IO](ec).resource.use(monixClient.sayHellos(requests)(_)) - response.unsafeRunSync() shouldBe HelloResponse("") - } - - "serve a POST request with fs2 streaming response" in { - val request = HelloRequest("hey") - val responses = - BlazeClientBuilder[IO](ec).stream.flatMap(fs2Client.sayHelloAll(request)(_)) - responses.compile.toList - .unsafeRunSync() shouldBe List(HelloResponse("hey"), HelloResponse("hey")) - } - - "serve a POST request with Observable streaming response" in { - val request = HelloRequest("hey") - val responses = BlazeClientBuilder[IO](ec).stream - .flatMap(monixClient.sayHelloAll(request)(_).toFs2Stream[IO]) - responses.compile.toList - .unsafeRunTimed(10.seconds) - .getOrElse(sys.error("Stuck!")) shouldBe List(HelloResponse("hey"), HelloResponse("hey")) - } - - "handle errors with fs2 streaming response" in { - val request = HelloRequest("") - val responses = - BlazeClientBuilder[IO](ec).stream.flatMap(fs2Client.sayHelloAll(request)(_)) - the[IllegalArgumentException] thrownBy responses.compile.toList - .unsafeRunSync() should have message "empty greeting" - } - - "handle errors with Observable streaming response" in { - val request = HelloRequest("") - val responses = BlazeClientBuilder[IO](ec).stream - .flatMap(monixClient.sayHelloAll(request)(_).toFs2Stream[IO]) - the[IllegalArgumentException] thrownBy responses.compile.toList - .unsafeRunTimed(10.seconds) - .getOrElse(sys.error("Stuck!")) should have message "empty greeting" - } - - "serve a POST request with bidirectional fs2 streaming" in { - val requests = Stream(HelloRequest("hey"), HelloRequest("there")) - val responses = - BlazeClientBuilder[IO](ec).stream.flatMap(fs2Client.sayHellosAll(requests)(_)) - responses.compile.toList - .unsafeRunSync() shouldBe List(HelloResponse("hey"), HelloResponse("there")) - } - - "serve an empty POST request with bidirectional fs2 streaming" in { - val requests = Stream.empty - val responses = - BlazeClientBuilder[IO](ec).stream.flatMap(fs2Client.sayHellosAll(requests)(_)) - responses.compile.toList.unsafeRunSync() shouldBe Nil - } - - "serve a POST request with bidirectional Observable streaming" in { - val requests = Observable(HelloRequest("hey"), HelloRequest("there")) - val responses = BlazeClientBuilder[IO](ec).stream - .flatMap(monixClient.sayHellosAll(requests)(_).toFs2Stream[IO]) - responses.compile.toList - .unsafeRunTimed(10.seconds) - .getOrElse(sys.error("Stuck!")) shouldBe List(HelloResponse("hey"), HelloResponse("there")) - } - - "serve an empty POST request with bidirectional Observable streaming" in { - val requests = Observable.empty - val responses = BlazeClientBuilder[IO](ec).stream - .flatMap(monixClient.sayHellosAll(requests)(_).toFs2Stream[IO]) - responses.compile.toList - .unsafeRunTimed(10.seconds) - .getOrElse(sys.error("Stuck!")) shouldBe Nil - } - - "serve ScalaCheck-generated POST requests with bidirectional Observable streaming" in { - forAll { strings: List[String] => - val requests = Observable.fromIterable(strings.map(HelloRequest)) - val responses = BlazeClientBuilder[IO](ec).stream - .flatMap(monixClient.sayHellosAll(requests)(_).toFs2Stream[IO]) - responses.compile.toList - .unsafeRunTimed(10.seconds) - .getOrElse(sys.error("Stuck!")) shouldBe strings.map(HelloResponse) - } - } - - } - -} +///* +// * Copyright 2017-2019 47 Degrees, LLC. +// * +// * Licensed under the Apache License, Version 2.0 (the "License"); +// * you may not use this file except in compliance with the License. +// * You may obtain a copy of the License at +// * +// * http://www.apache.org/licenses/LICENSE-2.0 +// * +// * Unless required by applicable law or agreed to in writing, software +// * distributed under the License is distributed on an "AS IS" BASIS, +// * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// * See the License for the specific language governing permissions and +// * limitations under the License. +// */ +// +//package higherkindness.mu.rpc.http +// +//import cats.effect.{IO, _} +//import fs2.Stream +//import higherkindness.mu.rpc.common.RpcBaseTestSuite +//import higherkindness.mu.rpc.http.Utils._ +//import higherkindness.mu.rpc.protocol.Empty +//import io.circe.Json +//import io.circe.generic.auto._ +//import io.circe.syntax._ +//import monix.reactive.Observable +//import org.http4s._ +//import org.http4s.circe._ +//import org.http4s.client.UnexpectedStatus +//import org.http4s.client.blaze.BlazeClientBuilder +//import org.http4s.dsl.io._ +//import org.http4s.server._ +//import org.http4s.server.blaze._ +//import org.scalatest._ +//import org.scalatest.prop.GeneratorDrivenPropertyChecks +// +//import scala.concurrent.duration._ +// +//class GreeterRestTests +// extends RpcBaseTestSuite +// with GeneratorDrivenPropertyChecks +// with BeforeAndAfter { +// +// val Hostname = "localhost" +// val Port = 8080 +// +// val serviceUri: Uri = Uri.unsafeFromString(s"http://$Hostname:$Port") +// +// val UnaryServicePrefix = "unary" +// val Fs2ServicePrefix = "fs2" +// val MonixServicePrefix = "monix" +// +// implicit val ec = monix.execution.Scheduler.Implicits.global +// implicit val cs: ContextShift[IO] = IO.contextShift(ec) +// +// implicit val unaryHandlerIO = new UnaryGreeterHandler[IO] +// implicit val fs2HandlerIO = new Fs2GreeterHandler[IO] +// implicit val monixHandlerIO = new MonixGreeterHandler[IO] +// +// //TODO: add Logger middleware +// val unaryService: HttpRoutes[IO] = new UnaryGreeterRestService[IO].service +// val fs2Service: HttpRoutes[IO] = new Fs2GreeterRestService[IO].service +// val monixService: HttpRoutes[IO] = new MonixGreeterRestService[IO].service +// +// val server: BlazeServerBuilder[IO] = BlazeServerBuilder[IO] +// .bindHttp(Port, Hostname) +// //.enableHttp2(true) +// //.withSSLContext(GenericSSLContext.serverSSLContext) +// .withHttpApp( +// Router( +// s"/$UnaryServicePrefix" -> unaryService, +// s"/$Fs2ServicePrefix" -> fs2Service, +// s"/$MonixServicePrefix" -> monixService).orNotFound) +// +// var serverTask: Fiber[IO, Nothing] = _ // sorry +// before(serverTask = server.resource.use(_ => IO.never).start.unsafeRunSync()) +// after(serverTask.cancel) +// +// "REST Server" should { +// +// "serve a GET request" in { +// val request = Request[IO](Method.GET, serviceUri / UnaryServicePrefix / "getHello") +// val response = BlazeClientBuilder[IO](ec).resource.use(_.expect[Json](request)) +// response.unsafeRunSync() shouldBe HelloResponse("hey").asJson +// } +// +// "serve a POST request" in { +// val request = Request[IO](Method.POST, serviceUri / UnaryServicePrefix / "sayHello") +// val requestBody = HelloRequest("hey").asJson +// val response = +// BlazeClientBuilder[IO](ec).resource.use(_.expect[Json](request.withEntity(requestBody))) +// response.unsafeRunSync() shouldBe HelloResponse("hey").asJson +// } +// +// "return a 400 Bad Request for a malformed unary POST request" in { +// val request = Request[IO](Method.POST, serviceUri / UnaryServicePrefix / "sayHello") +// val requestBody = "{" +// val response = +// BlazeClientBuilder[IO](ec).resource.use(_.expect[Json](request.withEntity(requestBody))) +// the[UnexpectedStatus] thrownBy response.unsafeRunSync() shouldBe UnexpectedStatus( +// Status.BadRequest) +// } +// +// "return a 400 Bad Request for a malformed streaming POST request" in { +// val request = Request[IO](Method.POST, serviceUri / Fs2ServicePrefix / "sayHellos") +// val requestBody = "{" +// val response = +// BlazeClientBuilder[IO](ec).resource.use(_.expect[Json](request.withEntity(requestBody))) +// the[UnexpectedStatus] thrownBy response.unsafeRunSync() shouldBe UnexpectedStatus( +// Status.BadRequest) +// } +// +// } +// +// val unaryServiceClient: UnaryGreeterRestClient[IO] = +// new UnaryGreeterRestClient[IO](serviceUri / UnaryServicePrefix) +// val fs2ServiceClient: Fs2GreeterRestClient[IO] = +// new Fs2GreeterRestClient[IO](serviceUri / Fs2ServicePrefix) +// val monixServiceClient: MonixGreeterRestClient[IO] = +// new MonixGreeterRestClient[IO](serviceUri / MonixServicePrefix) +// +// "REST Service" should { +// +// "serve a GET request" in { +// val response = BlazeClientBuilder[IO](ec).resource.use(unaryServiceClient.getHello()(_)) +// response.unsafeRunSync() shouldBe HelloResponse("hey") +// } +// +// "serve a unary POST request" in { +// val request = HelloRequest("hey") +// val response = +// BlazeClientBuilder[IO](ec).resource.use(unaryServiceClient.sayHello(request)(_)) +// response.unsafeRunSync() shouldBe HelloResponse("hey") +// } +// +// "handle a raised gRPC exception in a unary POST request" in { +// val request = HelloRequest("SRE") +// val response = +// BlazeClientBuilder[IO](ec).resource.use(unaryServiceClient.sayHello(request)(_)) +// the[ResponseError] thrownBy response.unsafeRunSync() shouldBe ResponseError( +// Status.BadRequest, +// Some("INVALID_ARGUMENT: SRE")) +// } +// +// "handle a raised non-gRPC exception in a unary POST request" in { +// val request = HelloRequest("RTE") +// val response = +// BlazeClientBuilder[IO](ec).resource.use(unaryServiceClient.sayHello(request)(_)) +// the[ResponseError] thrownBy response.unsafeRunSync() shouldBe ResponseError( +// Status.InternalServerError, +// Some("RTE")) +// } +// +// "handle a thrown exception in a unary POST request" in { +// val request = HelloRequest("TR") +// val response = +// BlazeClientBuilder[IO](ec).resource.use(unaryServiceClient.sayHello(request)(_)) +// the[ResponseError] thrownBy response.unsafeRunSync() shouldBe ResponseError( +// Status.InternalServerError) +// } +// +// "serve a POST request with fs2 streaming request" in { +// val requests = Stream(HelloRequest("hey"), HelloRequest("there")) +// val response = +// BlazeClientBuilder[IO](ec).resource.use(fs2ServiceClient.sayHellos(requests)(_)) +// response.unsafeRunSync() shouldBe HelloResponse("hey, there") +// } +// +// "serve a POST request with empty fs2 streaming request" in { +// val requests = Stream.empty +// val response = +// BlazeClientBuilder[IO](ec).resource.use(fs2ServiceClient.sayHellos(requests)(_)) +// response.unsafeRunSync() shouldBe HelloResponse("") +// } +// +// "serve a POST request with Observable streaming request" in { +// val requests = Observable(HelloRequest("hey"), HelloRequest("there")) +// val response = +// BlazeClientBuilder[IO](ec).resource.use(monixServiceClient.sayHellos(requests)(_)) +// response.unsafeRunSync() shouldBe HelloResponse("hey, there") +// } +// +// "serve a POST request with empty Observable streaming request" in { +// val requests = Observable.empty +// val response = +// BlazeClientBuilder[IO](ec).resource.use(monixServiceClient.sayHellos(requests)(_)) +// response.unsafeRunSync() shouldBe HelloResponse("") +// } +// +// "serve a POST request with fs2 streaming response" in { +// val request = HelloRequest("hey") +// val responses = +// BlazeClientBuilder[IO](ec).stream.flatMap(fs2ServiceClient.sayHelloAll(request)(_)) +// responses.compile.toList +// .unsafeRunSync() shouldBe List(HelloResponse("hey"), HelloResponse("hey")) +// } +// +// "serve a POST request with Observable streaming response" in { +// val request = HelloRequest("hey") +// val responses = BlazeClientBuilder[IO](ec).stream +// .flatMap(monixServiceClient.sayHelloAll(request)(_).toFs2Stream[IO]) +// responses.compile.toList +// .unsafeRunTimed(10.seconds) +// .getOrElse(sys.error("Stuck!")) shouldBe List(HelloResponse("hey"), HelloResponse("hey")) +// } +// +// "handle errors with fs2 streaming response" in { +// val request = HelloRequest("") +// val responses = +// BlazeClientBuilder[IO](ec).stream.flatMap(fs2ServiceClient.sayHelloAll(request)(_)) +// the[IllegalArgumentException] thrownBy responses.compile.toList +// .unsafeRunSync() should have message "empty greeting" +// } +// +// "handle errors with Observable streaming response" in { +// val request = HelloRequest("") +// val responses = BlazeClientBuilder[IO](ec).stream +// .flatMap(monixServiceClient.sayHelloAll(request)(_).toFs2Stream[IO]) +// the[IllegalArgumentException] thrownBy responses.compile.toList +// .unsafeRunTimed(10.seconds) +// .getOrElse(sys.error("Stuck!")) should have message "empty greeting" +// } +// +// "serve a POST request with bidirectional fs2 streaming" in { +// val requests = Stream(HelloRequest("hey"), HelloRequest("there")) +// val responses = +// BlazeClientBuilder[IO](ec).stream.flatMap(fs2ServiceClient.sayHellosAll(requests)(_)) +// responses.compile.toList +// .unsafeRunSync() shouldBe List(HelloResponse("hey"), HelloResponse("there")) +// } +// +// "serve an empty POST request with bidirectional fs2 streaming" in { +// val requests = Stream.empty +// val responses = +// BlazeClientBuilder[IO](ec).stream.flatMap(fs2ServiceClient.sayHellosAll(requests)(_)) +// responses.compile.toList.unsafeRunSync() shouldBe Nil +// } +// +// "serve a POST request with bidirectional Observable streaming" in { +// val requests = Observable(HelloRequest("hey"), HelloRequest("there")) +// val responses = BlazeClientBuilder[IO](ec).stream +// .flatMap(monixServiceClient.sayHellosAll(requests)(_).toFs2Stream[IO]) +// responses.compile.toList +// .unsafeRunTimed(10.seconds) +// .getOrElse(sys.error("Stuck!")) shouldBe List(HelloResponse("hey"), HelloResponse("there")) +// } +// +// "serve an empty POST request with bidirectional Observable streaming" in { +// val requests = Observable.empty +// val responses = BlazeClientBuilder[IO](ec).stream +// .flatMap(monixServiceClient.sayHellosAll(requests)(_).toFs2Stream[IO]) +// responses.compile.toList +// .unsafeRunTimed(10.seconds) +// .getOrElse(sys.error("Stuck!")) shouldBe Nil +// } +// +// "serve ScalaCheck-generated POST requests with bidirectional Observable streaming" in { +// forAll { strings: List[String] => +// val requests = Observable.fromIterable(strings.map(HelloRequest)) +// val responses = BlazeClientBuilder[IO](ec).stream +// .flatMap(monixServiceClient.sayHellosAll(requests)(_).toFs2Stream[IO]) +// responses.compile.toList +// .unsafeRunTimed(10.seconds) +// .getOrElse(sys.error("Stuck!")) shouldBe strings.map(HelloResponse) +// } +// } +// } +// +// "Auto-derived REST Client" should { +// +// val unaryClient = UnaryGreeter.httpClient[IO](serviceUri / UnaryServicePrefix) +// val fs2Client = Fs2Greeter.httpClient[IO](serviceUri / Fs2ServicePrefix) +// val monixClient = MonixGreeter.httpClient[IO](serviceUri / MonixServicePrefix) +// +// "serve a GET request" in { +// val response: IO[HelloResponse] = +// BlazeClientBuilder[IO](ec).resource.use(unaryClient.getHello(_)) +// response.unsafeRunSync() shouldBe HelloResponse("hey") +// } +// +// "serve a unary POST request" in { +// val response: IO[HelloResponse] = +// BlazeClientBuilder[IO](ec).resource.use(unaryClient.sayHello(HelloRequest("hey"))(_)) +// response.unsafeRunSync() shouldBe HelloResponse("hey") +// } +// +// "handle a raised gRPC exception in a unary POST request" in { +// val response: IO[HelloResponse] = +// BlazeClientBuilder[IO](ec).resource.use(unaryClient.sayHello(HelloRequest("SRE"))(_)) +// +// the[ResponseError] thrownBy response.unsafeRunSync() shouldBe ResponseError( +// Status.BadRequest, +// Some("INVALID_ARGUMENT: SRE")) +// } +// +// "handle a raised non-gRPC exception in a unary POST request" in { +// val response: IO[HelloResponse] = +// BlazeClientBuilder[IO](ec).resource.use(unaryClient.sayHello(HelloRequest("RTE"))(_)) +// +// the[ResponseError] thrownBy response.unsafeRunSync() shouldBe ResponseError( +// Status.InternalServerError, +// Some("RTE")) +// } +// +// "handle a thrown exception in a unary POST request" in { +// val response: IO[HelloResponse] = +// BlazeClientBuilder[IO](ec).resource.use(unaryClient.sayHello(HelloRequest("TR"))(_)) +// +// the[ResponseError] thrownBy response.unsafeRunSync() shouldBe ResponseError( +// Status.InternalServerError) +// } +// +// "serve a POST request with fs2 streaming request" in { +// +// val requests = Stream(HelloRequest("hey"), HelloRequest("there")) +// +// val response: IO[HelloResponse] = +// BlazeClientBuilder[IO](ec).resource.use(fs2Client.sayHellos(requests)(_)) +// response.unsafeRunSync() shouldBe HelloResponse("hey, there") +// } +// +// "serve a POST request with empty fs2 streaming request" in { +// val requests = Stream.empty +// val response = +// BlazeClientBuilder[IO](ec).resource.use(fs2Client.sayHellos(requests)(_)) +// response.unsafeRunSync() shouldBe HelloResponse("") +// } +// +// "serve a POST request with Observable streaming request" in { +// val requests = Observable(HelloRequest("hey"), HelloRequest("there")) +// val response = +// BlazeClientBuilder[IO](ec).resource.use(monixClient.sayHellos(requests)(_)) +// response.unsafeRunSync() shouldBe HelloResponse("hey, there") +// } +// +// "serve a POST request with empty Observable streaming request" in { +// val requests = Observable.empty +// val response = +// BlazeClientBuilder[IO](ec).resource.use(monixClient.sayHellos(requests)(_)) +// response.unsafeRunSync() shouldBe HelloResponse("") +// } +// +// "serve a POST request with fs2 streaming response" in { +// val request = HelloRequest("hey") +// val responses = +// BlazeClientBuilder[IO](ec).stream.flatMap(fs2Client.sayHelloAll(request)(_)) +// responses.compile.toList +// .unsafeRunSync() shouldBe List(HelloResponse("hey"), HelloResponse("hey")) +// } +// +// "serve a POST request with Observable streaming response" in { +// val request = HelloRequest("hey") +// val responses = BlazeClientBuilder[IO](ec).stream +// .flatMap(monixClient.sayHelloAll(request)(_).toFs2Stream[IO]) +// responses.compile.toList +// .unsafeRunTimed(10.seconds) +// .getOrElse(sys.error("Stuck!")) shouldBe List(HelloResponse("hey"), HelloResponse("hey")) +// } +// +// "handle errors with fs2 streaming response" in { +// val request = HelloRequest("") +// val responses = +// BlazeClientBuilder[IO](ec).stream.flatMap(fs2Client.sayHelloAll(request)(_)) +// the[IllegalArgumentException] thrownBy responses.compile.toList +// .unsafeRunSync() should have message "empty greeting" +// } +// +// "handle errors with Observable streaming response" in { +// val request = HelloRequest("") +// val responses = BlazeClientBuilder[IO](ec).stream +// .flatMap(monixClient.sayHelloAll(request)(_).toFs2Stream[IO]) +// the[IllegalArgumentException] thrownBy responses.compile.toList +// .unsafeRunTimed(10.seconds) +// .getOrElse(sys.error("Stuck!")) should have message "empty greeting" +// } +// +// "serve a POST request with bidirectional fs2 streaming" in { +// val requests = Stream(HelloRequest("hey"), HelloRequest("there")) +// val responses = +// BlazeClientBuilder[IO](ec).stream.flatMap(fs2Client.sayHellosAll(requests)(_)) +// responses.compile.toList +// .unsafeRunSync() shouldBe List(HelloResponse("hey"), HelloResponse("there")) +// } +// +// "serve an empty POST request with bidirectional fs2 streaming" in { +// val requests = Stream.empty +// val responses = +// BlazeClientBuilder[IO](ec).stream.flatMap(fs2Client.sayHellosAll(requests)(_)) +// responses.compile.toList.unsafeRunSync() shouldBe Nil +// } +// +// "serve a POST request with bidirectional Observable streaming" in { +// val requests = Observable(HelloRequest("hey"), HelloRequest("there")) +// val responses = BlazeClientBuilder[IO](ec).stream +// .flatMap(monixClient.sayHellosAll(requests)(_).toFs2Stream[IO]) +// responses.compile.toList +// .unsafeRunTimed(10.seconds) +// .getOrElse(sys.error("Stuck!")) shouldBe List(HelloResponse("hey"), HelloResponse("there")) +// } +// +// "serve an empty POST request with bidirectional Observable streaming" in { +// val requests = Observable.empty +// val responses = BlazeClientBuilder[IO](ec).stream +// .flatMap(monixClient.sayHellosAll(requests)(_).toFs2Stream[IO]) +// responses.compile.toList +// .unsafeRunTimed(10.seconds) +// .getOrElse(sys.error("Stuck!")) shouldBe Nil +// } +// +// "serve ScalaCheck-generated POST requests with bidirectional Observable streaming" in { +// forAll { strings: List[String] => +// val requests = Observable.fromIterable(strings.map(HelloRequest)) +// val responses = BlazeClientBuilder[IO](ec).stream +// .flatMap(monixClient.sayHellosAll(requests)(_).toFs2Stream[IO]) +// responses.compile.toList +// .unsafeRunTimed(10.seconds) +// .getOrElse(sys.error("Stuck!")) shouldBe strings.map(HelloResponse) +// } +// } +// +// } +// +//} diff --git a/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterServices.scala b/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterServices.scala index 1480addc7..f09982347 100644 --- a/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterServices.scala +++ b/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterServices.scala @@ -35,19 +35,29 @@ import higherkindness.mu.rpc.protocol._ import fs2.Stream @service(Avro) trait Fs2Greeter[F[_]] { - @http def sayHellos(requests: Stream[F, HelloRequest]): F[HelloResponse] - - @http def sayHelloAll(request: HelloRequest): Stream[F, HelloResponse] - - @http def sayHellosAll(requests: Stream[F, HelloRequest]): Stream[F, HelloResponse] + def sayHellos(requests: Stream[F, HelloRequest]): F[HelloResponse] + def sayHelloAll(request: HelloRequest): Stream[F, HelloResponse] + def sayHellosAll(requests: Stream[F, HelloRequest]): Stream[F, HelloResponse] + +// @http def sayHellos(requests: Stream[F, HelloRequest]): F[HelloResponse] +// +// @http def sayHelloAll(request: HelloRequest): Stream[F, HelloResponse] +// +// @http def sayHellosAll(requests: Stream[F, HelloRequest]): Stream[F, HelloResponse] +// } import monix.reactive.Observable @service(Avro) trait MonixGreeter[F[_]] { - @http def sayHellos(requests: Observable[HelloRequest]): F[HelloResponse] - - @http def sayHelloAll(request: HelloRequest): Observable[HelloResponse] + def sayHellos(requests: Observable[HelloRequest]): F[HelloResponse] + def sayHelloAll(request: HelloRequest): Observable[HelloResponse] + def sayHellosAll(requests: Observable[HelloRequest]): Observable[HelloResponse] - @http def sayHellosAll(requests: Observable[HelloRequest]): Observable[HelloResponse] +// @http def sayHellos(requests: Observable[HelloRequest]): F[HelloResponse] +// +// @http def sayHelloAll(request: HelloRequest): Observable[HelloResponse] +// +// @http def sayHellosAll(requests: Observable[HelloRequest]): Observable[HelloResponse] +// } diff --git a/modules/internal/src/main/scala/higherkindness/mu/rpc/internal/serviceImpl.scala b/modules/internal/src/main/scala/higherkindness/mu/rpc/internal/serviceImpl.scala index 1d72cebe3..999a8d6c0 100644 --- a/modules/internal/src/main/scala/higherkindness/mu/rpc/internal/serviceImpl.scala +++ b/modules/internal/src/main/scala/higherkindness/mu/rpc/internal/serviceImpl.scala @@ -460,7 +460,7 @@ object serviceImpl { val responseEncoder = q"""implicit val responseDecoder: EntityDecoder[F, ${response.safeInner}] = jsonOf[F, ${response.safeInner}]""" - def toTree: Tree = request match { + def toRequestTree: Tree = request match { case _: EmptyTpe => q"""def $name(implicit client: _root_.org.http4s.client.Client[F]): ${response.getTpe} = { $responseEncoder @@ -475,6 +475,8 @@ object serviceImpl { }""" } + def toRouteTree = cq"""_ => Ok("Hi")""" + } val operations: List[HttpOperation] = for { @@ -485,7 +487,7 @@ object serviceImpl { p <- params.headOption.toList } yield HttpOperation(Operation(d.name, TypeTypology(p.tpt), TypeTypology(d.tpt))) - val httpRequests = operations.map(_.toTree) + val httpRequests = operations.map(_.toRequestTree) val HttpClient = TypeName("HttpClient") val httpClientClass: ClassDef = q""" @@ -501,6 +503,7 @@ object serviceImpl { val httpImports: List[Tree] = List( q"import _root_.higherkindness.mu.rpc.http.Utils._", + q"import _root_.cats.syntax.functor._", q"import _root_.org.http4s._", q"import _root_.org.http4s.circe._", q"import _root_.io.circe._", @@ -513,10 +516,67 @@ object serviceImpl { .map(_ => q"import _root_.monix.execution.Scheduler.Implicits.global") .toList + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + + println("&&&&&&&&&&&&&&&&&&") + println(serviceDef.name.toTermName) + +// val httpRoutesCases = operations.map(_.toRouteTree) + val httpRoutesCases = + operations.map(op => + cq"""GET -> Root / ${op.operation.name} => Ok(handler.getHello(_root_.higherkindness.mu.rpc.protocol.Empty).map(_.asJson)) """) + val routesPF = q"{ case ..$httpRoutesCases }" + + val HttpRestService = TypeName(serviceDef.name.toString + "RestService") + + val httpRestServiceClass = q""" + class $HttpRestService[$F_](implicit handler: ${serviceDef.name}[F], F: _root_.cats.effect.Sync[$F]) extends _root_.org.http4s.dsl.Http4sDsl[F] { + def service: HttpRoutes[F] = HttpRoutes.of[F]{$routesPF} + }""" + + val httpService = q""" + def route[$F_](implicit handler: ${serviceDef.name}[F], F: _root_.cats.effect.Sync[$F]): HttpRoutes[F] = { + new $HttpRestService[$F].service + }""" + + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + val http = if (httpRequests.isEmpty) Nil else - httpImports ++ scheduler ++ List(httpClientClass, httpClient) + httpImports ++ scheduler ++ List( + httpClientClass, + httpClient, + httpRestServiceClass, + httpService) } val classAndMaybeCompanion = annottees.map(_.tree) @@ -561,6 +621,12 @@ object serviceImpl { ) ++ service.http ) ) + + if (service.httpRequests.nonEmpty) { + println("#######################") + println(enrichedCompanion.toString) + } + List(serviceDef, enrichedCompanion) case _ => sys.error("@service-annotated definition must be a trait or abstract class") } From 06f2b83b74f380db88ce5e1949a9a0b260ffaa9b Mon Sep 17 00:00:00 2001 From: rafaparadela Date: Fri, 1 Mar 2019 17:41:28 -0800 Subject: [PATCH 22/35] derived the stream reaquests http server --- .../mu/rpc/protocol/protocol.scala | 26 ---- .../higherkindness/mu/http/protocol.scala | 63 +++++++++ .../mu/rpc/http/GreeterDerivedRestTests.scala | 119 +++++++++-------- .../mu/rpc/http/GreeterRestTests.scala | 12 +- .../mu/rpc/http/GreeterServices.scala | 21 +-- .../mu/rpc/internal/serviceImpl.scala | 121 ++++++++++++++---- 6 files changed, 229 insertions(+), 133 deletions(-) create mode 100644 modules/http/src/main/scala/higherkindness/mu/http/protocol.scala diff --git a/modules/common/src/main/scala/higherkindness/mu/rpc/protocol/protocol.scala b/modules/common/src/main/scala/higherkindness/mu/rpc/protocol/protocol.scala index 341ba3c6e..3561f1a62 100644 --- a/modules/common/src/main/scala/higherkindness/mu/rpc/protocol/protocol.scala +++ b/modules/common/src/main/scala/higherkindness/mu/rpc/protocol/protocol.scala @@ -39,31 +39,5 @@ class outputPackage(value: String) extends StaticAnnotation class outputName(value: String) extends StaticAnnotation class http extends StaticAnnotation -sealed trait HttpMethod extends Product with Serializable -case object OPTIONS extends HttpMethod -case object GET extends HttpMethod -case object HEAD extends HttpMethod -case object POST extends HttpMethod -case object PUT extends HttpMethod -case object DELETE extends HttpMethod -case object TRACE extends HttpMethod -case object CONNECT extends HttpMethod -case object PATCH extends HttpMethod - -object HttpMethod { - def fromString(str: String): Option[HttpMethod] = str match { - case "OPTIONS" => Some(OPTIONS) - case "GET" => Some(GET) - case "HEAD" => Some(HEAD) - case "POST" => Some(POST) - case "PUT" => Some(PUT) - case "DELETE" => Some(DELETE) - case "TRACE" => Some(TRACE) - case "CONNECT" => Some(CONNECT) - case "PATCH" => Some(PATCH) - case _ => None - } -} - @message object Empty diff --git a/modules/http/src/main/scala/higherkindness/mu/http/protocol.scala b/modules/http/src/main/scala/higherkindness/mu/http/protocol.scala new file mode 100644 index 000000000..46682016a --- /dev/null +++ b/modules/http/src/main/scala/higherkindness/mu/http/protocol.scala @@ -0,0 +1,63 @@ +/* + * Copyright 2017-2019 47 Degrees, LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package higherkindness.mu.http + +import cats.effect.ConcurrentEffect +import org.http4s.HttpRoutes +import org.http4s.server.Router +import org.http4s.server.blaze.BlazeServerBuilder +import org.http4s.dsl.io._ + +sealed trait HttpMethod extends Product with Serializable +case object OPTIONS extends HttpMethod +case object GET extends HttpMethod +case object HEAD extends HttpMethod +case object POST extends HttpMethod +case object PUT extends HttpMethod +case object DELETE extends HttpMethod +case object TRACE extends HttpMethod +case object CONNECT extends HttpMethod +case object PATCH extends HttpMethod + +object HttpMethod { + def fromString(str: String): Option[HttpMethod] = str match { + case "OPTIONS" => Some(OPTIONS) + case "GET" => Some(GET) + case "HEAD" => Some(HEAD) + case "POST" => Some(POST) + case "PUT" => Some(PUT) + case "DELETE" => Some(DELETE) + case "TRACE" => Some(TRACE) + case "CONNECT" => Some(CONNECT) + case "PATCH" => Some(PATCH) + case _ => None + } +} + +case class RouteMap[F[_]](prefix: String, route: HttpRoutes[F]) + +object HttpServer { + + def bind[F[_]: ConcurrentEffect]( + port: Int, + host: String, + routes: RouteMap[F]*): BlazeServerBuilder[F] = + BlazeServerBuilder[F] + .bindHttp(port, host) + .withHttpApp(Router(routes.map(r => (s"/${r.prefix}", r.route)): _*).orNotFound) + +} diff --git a/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterDerivedRestTests.scala b/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterDerivedRestTests.scala index 0d02cb321..a5b5d8bd9 100644 --- a/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterDerivedRestTests.scala +++ b/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterDerivedRestTests.scala @@ -18,8 +18,10 @@ package higherkindness.mu.rpc.http import cats.effect.{IO, _} import fs2.Stream +import higherkindness.mu.http.{HttpServer, RouteMap} import higherkindness.mu.rpc.common.RpcBaseTestSuite import higherkindness.mu.rpc.http.Utils._ +import higherkindness.mu.rpc.protocol.Empty import io.circe.Json import io.circe.generic.auto._ import io.circe.syntax._ @@ -41,126 +43,120 @@ class GreeterDerivedRestTests with GeneratorDrivenPropertyChecks with BeforeAndAfter { - val Hostname = "localhost" - val Port = 8080 - - val serviceUri: Uri = Uri.unsafeFromString(s"http://$Hostname:$Port") - - val UnaryServicePrefix = "unary" -// val Fs2ServicePrefix = "fs2" -// val MonixServicePrefix = "monix" + val host = "localhost" + val port = 8080 + val serviceUri: Uri = Uri.unsafeFromString(s"http://$host:$port") implicit val ec = monix.execution.Scheduler.Implicits.global implicit val cs: ContextShift[IO] = IO.contextShift(ec) + implicit val unaryHandlerIO = new UnaryGreeterHandler[IO] + implicit val fs2HandlerIO = new Fs2GreeterHandler[IO] + implicit val monixHandlerIO = new MonixGreeterHandler[IO] - implicit val unaryHandlerIO = new UnaryGreeterHandler[IO] +// val unaryRoute: RouteMap[IO] = UnaryGreeter.route[IO] + val fs2Route: RouteMap[IO] = Fs2Greeter.route[IO] +// val monixRoute: RouteMap[IO] = MonixGreeter.route[IO] - val unaryService: HttpRoutes[IO] = new UnaryGreeterRestService[IO].service +// val server: BlazeServerBuilder[IO] = HttpServer.bind(port, host, unaryRoute, fs2Route, monixRoute) - val unaryRoute: HttpRoutes[IO] = UnaryGreeter.route[IO] +// val server1: BlazeServerBuilder[IO] = BlazeServerBuilder[IO] +// .bindHttp(port, host) +// .withHttpApp(Router("/Fs2Greeter" -> new Fs2GreeterRestService[IO].service).orNotFound) val server: BlazeServerBuilder[IO] = BlazeServerBuilder[IO] - .bindHttp(Port, Hostname) - .withHttpApp( - Router( - s"/$UnaryServicePrefix" -> unaryRoute, -// s"/$Fs2ServicePrefix" -> fs2Service, -// s"/$MonixServicePrefix" -> monixService - ).orNotFound) + .bindHttp(port, host) + .withHttpApp(Router(s"/${fs2Route.prefix}" -> fs2Route.route).orNotFound) var serverTask: Fiber[IO, Nothing] = _ // sorry before(serverTask = server.resource.use(_ => IO.never).start.unsafeRunSync()) after(serverTask.cancel) - val unaryServiceClient: UnaryGreeterRestClient[IO] = - new UnaryGreeterRestClient[IO](serviceUri / UnaryServicePrefix) -// val fs2ServiceClient: Fs2GreeterRestClient[IO] = new Fs2GreeterRestClient[IO](serviceUri / Fs2ServicePrefix) -// val monixServiceClient: MonixGreeterRestClient[IO] = new MonixGreeterRestClient[IO](serviceUri / MonixServicePrefix) - "REST Service" should { - "serve a GET request" in { - val response = BlazeClientBuilder[IO](ec).resource.use(unaryServiceClient.getHello()(_)) - response.unsafeRunSync() shouldBe HelloResponse("hey") - } +// val unaryClient = UnaryGreeter.httpClient[IO](serviceUri) + val fs2Client = Fs2Greeter.httpClient[IO](serviceUri) +// val monixClient = MonixGreeter.httpClient[IO](serviceUri) + // "serve a GET request" in { -// val response = BlazeClientBuilder[IO](ec).resource.use(unaryServiceClient.getHello()(_)) +// val response: IO[HelloResponse] = +// BlazeClientBuilder[IO](ec).resource.use(unaryClient.getHello(_)) // response.unsafeRunSync() shouldBe HelloResponse("hey") // } // // "serve a unary POST request" in { -// val request = HelloRequest("hey") -// val response = -// BlazeClientBuilder[IO](ec).resource.use(unaryServiceClient.sayHello(request)(_)) +// val response: IO[HelloResponse] = +// BlazeClientBuilder[IO](ec).resource.use(unaryClient.sayHello(HelloRequest("hey"))(_)) // response.unsafeRunSync() shouldBe HelloResponse("hey") // } // // "handle a raised gRPC exception in a unary POST request" in { -// val request = HelloRequest("SRE") -// val response = -// BlazeClientBuilder[IO](ec).resource.use(unaryServiceClient.sayHello(request)(_)) +// val response: IO[HelloResponse] = +// BlazeClientBuilder[IO](ec).resource.use(unaryClient.sayHello(HelloRequest("SRE"))(_)) +// // the[ResponseError] thrownBy response.unsafeRunSync() shouldBe ResponseError( // Status.BadRequest, // Some("INVALID_ARGUMENT: SRE")) // } // // "handle a raised non-gRPC exception in a unary POST request" in { -// val request = HelloRequest("RTE") -// val response = -// BlazeClientBuilder[IO](ec).resource.use(unaryServiceClient.sayHello(request)(_)) +// val response: IO[HelloResponse] = +// BlazeClientBuilder[IO](ec).resource.use(unaryClient.sayHello(HelloRequest("RTE"))(_)) +// // the[ResponseError] thrownBy response.unsafeRunSync() shouldBe ResponseError( // Status.InternalServerError, // Some("RTE")) // } // // "handle a thrown exception in a unary POST request" in { -// val request = HelloRequest("TR") -// val response = -// BlazeClientBuilder[IO](ec).resource.use(unaryServiceClient.sayHello(request)(_)) +// val response: IO[HelloResponse] = +// BlazeClientBuilder[IO](ec).resource.use(unaryClient.sayHello(HelloRequest("TR"))(_)) +// // the[ResponseError] thrownBy response.unsafeRunSync() shouldBe ResponseError( // Status.InternalServerError) // } // // "serve a POST request with fs2 streaming request" in { +// // val requests = Stream(HelloRequest("hey"), HelloRequest("there")) -// val response = -// BlazeClientBuilder[IO](ec).resource.use(fs2ServiceClient.sayHellos(requests)(_)) +// +// val response: IO[HelloResponse] = +// BlazeClientBuilder[IO](ec).resource.use(fs2Client.sayHellos(requests)(_)) // response.unsafeRunSync() shouldBe HelloResponse("hey, there") // } // // "serve a POST request with empty fs2 streaming request" in { // val requests = Stream.empty // val response = -// BlazeClientBuilder[IO](ec).resource.use(fs2ServiceClient.sayHellos(requests)(_)) +// BlazeClientBuilder[IO](ec).resource.use(fs2Client.sayHellos(requests)(_)) // response.unsafeRunSync() shouldBe HelloResponse("") // } // // "serve a POST request with Observable streaming request" in { // val requests = Observable(HelloRequest("hey"), HelloRequest("there")) // val response = -// BlazeClientBuilder[IO](ec).resource.use(monixServiceClient.sayHellos(requests)(_)) +// BlazeClientBuilder[IO](ec).resource.use(monixClient.sayHellos(requests)(_)) // response.unsafeRunSync() shouldBe HelloResponse("hey, there") // } // // "serve a POST request with empty Observable streaming request" in { // val requests = Observable.empty // val response = -// BlazeClientBuilder[IO](ec).resource.use(monixServiceClient.sayHellos(requests)(_)) +// BlazeClientBuilder[IO](ec).resource.use(monixClient.sayHellos(requests)(_)) // response.unsafeRunSync() shouldBe HelloResponse("") // } -// -// "serve a POST request with fs2 streaming response" in { -// val request = HelloRequest("hey") -// val responses = -// BlazeClientBuilder[IO](ec).stream.flatMap(fs2ServiceClient.sayHelloAll(request)(_)) -// responses.compile.toList -// .unsafeRunSync() shouldBe List(HelloResponse("hey"), HelloResponse("hey")) -// } + + "serve a POST request with fs2 streaming response" in { + val request = HelloRequest("hey") + val responses = + BlazeClientBuilder[IO](ec).stream.flatMap(fs2Client.sayHelloAll(request)(_)) + responses.compile.toList + .unsafeRunSync() shouldBe List(HelloResponse("hey"), HelloResponse("hey")) + } // // "serve a POST request with Observable streaming response" in { // val request = HelloRequest("hey") // val responses = BlazeClientBuilder[IO](ec).stream -// .flatMap(monixServiceClient.sayHelloAll(request)(_).toFs2Stream[IO]) +// .flatMap(monixClient.sayHelloAll(request)(_).toFs2Stream[IO]) // responses.compile.toList // .unsafeRunTimed(10.seconds) // .getOrElse(sys.error("Stuck!")) shouldBe List(HelloResponse("hey"), HelloResponse("hey")) @@ -169,7 +165,7 @@ class GreeterDerivedRestTests // "handle errors with fs2 streaming response" in { // val request = HelloRequest("") // val responses = -// BlazeClientBuilder[IO](ec).stream.flatMap(fs2ServiceClient.sayHelloAll(request)(_)) +// BlazeClientBuilder[IO](ec).stream.flatMap(fs2Client.sayHelloAll(request)(_)) // the[IllegalArgumentException] thrownBy responses.compile.toList // .unsafeRunSync() should have message "empty greeting" // } @@ -177,7 +173,7 @@ class GreeterDerivedRestTests // "handle errors with Observable streaming response" in { // val request = HelloRequest("") // val responses = BlazeClientBuilder[IO](ec).stream -// .flatMap(monixServiceClient.sayHelloAll(request)(_).toFs2Stream[IO]) +// .flatMap(monixClient.sayHelloAll(request)(_).toFs2Stream[IO]) // the[IllegalArgumentException] thrownBy responses.compile.toList // .unsafeRunTimed(10.seconds) // .getOrElse(sys.error("Stuck!")) should have message "empty greeting" @@ -186,7 +182,7 @@ class GreeterDerivedRestTests // "serve a POST request with bidirectional fs2 streaming" in { // val requests = Stream(HelloRequest("hey"), HelloRequest("there")) // val responses = -// BlazeClientBuilder[IO](ec).stream.flatMap(fs2ServiceClient.sayHellosAll(requests)(_)) +// BlazeClientBuilder[IO](ec).stream.flatMap(fs2Client.sayHellosAll(requests)(_)) // responses.compile.toList // .unsafeRunSync() shouldBe List(HelloResponse("hey"), HelloResponse("there")) // } @@ -194,14 +190,14 @@ class GreeterDerivedRestTests // "serve an empty POST request with bidirectional fs2 streaming" in { // val requests = Stream.empty // val responses = -// BlazeClientBuilder[IO](ec).stream.flatMap(fs2ServiceClient.sayHellosAll(requests)(_)) +// BlazeClientBuilder[IO](ec).stream.flatMap(fs2Client.sayHellosAll(requests)(_)) // responses.compile.toList.unsafeRunSync() shouldBe Nil // } // // "serve a POST request with bidirectional Observable streaming" in { // val requests = Observable(HelloRequest("hey"), HelloRequest("there")) // val responses = BlazeClientBuilder[IO](ec).stream -// .flatMap(monixServiceClient.sayHellosAll(requests)(_).toFs2Stream[IO]) +// .flatMap(monixClient.sayHellosAll(requests)(_).toFs2Stream[IO]) // responses.compile.toList // .unsafeRunTimed(10.seconds) // .getOrElse(sys.error("Stuck!")) shouldBe List(HelloResponse("hey"), HelloResponse("there")) @@ -210,7 +206,7 @@ class GreeterDerivedRestTests // "serve an empty POST request with bidirectional Observable streaming" in { // val requests = Observable.empty // val responses = BlazeClientBuilder[IO](ec).stream -// .flatMap(monixServiceClient.sayHellosAll(requests)(_).toFs2Stream[IO]) +// .flatMap(monixClient.sayHellosAll(requests)(_).toFs2Stream[IO]) // responses.compile.toList // .unsafeRunTimed(10.seconds) // .getOrElse(sys.error("Stuck!")) shouldBe Nil @@ -220,12 +216,13 @@ class GreeterDerivedRestTests // forAll { strings: List[String] => // val requests = Observable.fromIterable(strings.map(HelloRequest)) // val responses = BlazeClientBuilder[IO](ec).stream -// .flatMap(monixServiceClient.sayHellosAll(requests)(_).toFs2Stream[IO]) +// .flatMap(monixClient.sayHellosAll(requests)(_).toFs2Stream[IO]) // responses.compile.toList // .unsafeRunTimed(10.seconds) // .getOrElse(sys.error("Stuck!")) shouldBe strings.map(HelloResponse) // } // } + } } diff --git a/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterRestTests.scala b/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterRestTests.scala index fdfe32d45..a2dbc870c 100644 --- a/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterRestTests.scala +++ b/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterRestTests.scala @@ -63,9 +63,9 @@ // // val serviceUri: Uri = Uri.unsafeFromString(s"http://$Hostname:$Port") // -// val UnaryServicePrefix = "unary" -// val Fs2ServicePrefix = "fs2" -// val MonixServicePrefix = "monix" +// val UnaryServicePrefix = "UnaryGreeter" +// val Fs2ServicePrefix = "Fs2Greeter" +// val MonixServicePrefix = "MonixGreeter" // // implicit val ec = monix.execution.Scheduler.Implicits.global // implicit val cs: ContextShift[IO] = IO.contextShift(ec) @@ -285,9 +285,9 @@ // // "Auto-derived REST Client" should { // -// val unaryClient = UnaryGreeter.httpClient[IO](serviceUri / UnaryServicePrefix) -// val fs2Client = Fs2Greeter.httpClient[IO](serviceUri / Fs2ServicePrefix) -// val monixClient = MonixGreeter.httpClient[IO](serviceUri / MonixServicePrefix) +// val unaryClient = UnaryGreeter.httpClient[IO](serviceUri) +// val fs2Client = Fs2Greeter.httpClient[IO](serviceUri) +// val monixClient = MonixGreeter.httpClient[IO](serviceUri) // // "serve a GET request" in { // val response: IO[HelloResponse] = diff --git a/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterServices.scala b/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterServices.scala index f09982347..95cc35c2b 100644 --- a/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterServices.scala +++ b/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterServices.scala @@ -35,29 +35,20 @@ import higherkindness.mu.rpc.protocol._ import fs2.Stream @service(Avro) trait Fs2Greeter[F[_]] { - def sayHellos(requests: Stream[F, HelloRequest]): F[HelloResponse] - def sayHelloAll(request: HelloRequest): Stream[F, HelloResponse] - def sayHellosAll(requests: Stream[F, HelloRequest]): Stream[F, HelloResponse] + @http def sayHellos(requests: Stream[F, HelloRequest]): F[HelloResponse] + + @http def sayHelloAll(request: HelloRequest): Stream[F, HelloResponse] -// @http def sayHellos(requests: Stream[F, HelloRequest]): F[HelloResponse] -// -// @http def sayHelloAll(request: HelloRequest): Stream[F, HelloResponse] -// -// @http def sayHellosAll(requests: Stream[F, HelloRequest]): Stream[F, HelloResponse] -// + def sayHellosAll(requests: Stream[F, HelloRequest]): Stream[F, HelloResponse] } import monix.reactive.Observable @service(Avro) trait MonixGreeter[F[_]] { def sayHellos(requests: Observable[HelloRequest]): F[HelloResponse] + def sayHelloAll(request: HelloRequest): Observable[HelloResponse] + def sayHellosAll(requests: Observable[HelloRequest]): Observable[HelloResponse] -// @http def sayHellos(requests: Observable[HelloRequest]): F[HelloResponse] -// -// @http def sayHelloAll(request: HelloRequest): Observable[HelloResponse] -// -// @http def sayHellosAll(requests: Observable[HelloRequest]): Observable[HelloResponse] -// } diff --git a/modules/internal/src/main/scala/higherkindness/mu/rpc/internal/serviceImpl.scala b/modules/internal/src/main/scala/higherkindness/mu/rpc/internal/serviceImpl.scala index 999a8d6c0..22a960111 100644 --- a/modules/internal/src/main/scala/higherkindness/mu/rpc/internal/serviceImpl.scala +++ b/modules/internal/src/main/scala/higherkindness/mu/rpc/internal/serviceImpl.scala @@ -28,17 +28,26 @@ object serviceImpl { import c.universe._ import Flag._ - abstract class TypeTypology(tpe: Tree, inner: Option[Tree], isStreaming: Boolean) - extends Product - with Serializable { + abstract class TypeTypology(tpe: Tree, inner: Option[Tree]) extends Product with Serializable { def getTpe: Tree = tpe def getInner: Option[Tree] = inner - def streaming: Boolean = isStreaming def safeInner: Tree = inner.getOrElse(tpe) def safeType: Tree = tpe match { case tq"$s[..$tpts]" if isStreaming => tpts.last case other => other } + def flatName: String = safeInner.toString + + def isEmpty: Boolean = this match { + case _: EmptyTpe => true + case _ => false + } + + def isStreaming: Boolean = this match { + case _: Fs2StreamTpe => true + case _: MonixObservableTpe => true + case _ => false + } } object TypeTypology { def apply(t: Tree): TypeTypology = t match { @@ -48,17 +57,16 @@ object serviceImpl { case tq"$carrier[..$tpts]" => UnaryTpe(t, tpts.headOption) } } - case class EmptyTpe(tpe: Tree) extends TypeTypology(tpe, None, false) - case class UnaryTpe(tpe: Tree, inner: Option[Tree]) extends TypeTypology(tpe, inner, false) - case class Fs2StreamTpe(tpe: Tree, inner: Option[Tree]) extends TypeTypology(tpe, inner, true) - case class MonixObservableTpe(tpe: Tree, inner: Option[Tree]) - extends TypeTypology(tpe, inner, true) + case class EmptyTpe(tpe: Tree) extends TypeTypology(tpe, None) + case class UnaryTpe(tpe: Tree, inner: Option[Tree]) extends TypeTypology(tpe, inner) + case class Fs2StreamTpe(tpe: Tree, inner: Option[Tree]) extends TypeTypology(tpe, inner) + case class MonixObservableTpe(tpe: Tree, inner: Option[Tree]) extends TypeTypology(tpe, inner) case class Operation(name: TermName, request: TypeTypology, response: TypeTypology) { - val isStreaming: Boolean = request.streaming || response.streaming + val isStreaming: Boolean = request.isStreaming || response.isStreaming - val streamingType: Option[StreamingType] = (request.streaming, response.streaming) match { + val streamingType: Option[StreamingType] = (request.isStreaming, response.isStreaming) match { case (true, true) => Some(BidirectionalStreaming) case (true, false) => Some(RequestStreaming) case (false, true) => Some(ResponseStreaming) @@ -475,7 +483,51 @@ object serviceImpl { }""" } - def toRouteTree = cq"""_ => Ok("Hi")""" + val routeTypology: Tree = (request, response) match { + case (_: Fs2StreamTpe, _: UnaryTpe) => + q"""println("@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@entra por Fs2StreamTpe-UnaryTpe") + val requests = msg.asStream[${operation.request.safeInner}] + Ok(handler.${operation.name}(requests).map(_.asJson))""" + + case (_: UnaryTpe, _: Fs2StreamTpe) => + q"""println("@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@entra por UnaryTpe-Fs2StreamTpe") + for { + request <- msg.as[${operation.request.safeInner}] + responses <- Ok(handler.${operation.name}(request).asJsonEither) + } yield responses""" + + case (_: Fs2StreamTpe, _: Fs2StreamTpe) => + q"""val requests = msg.asStream[${operation.request.safeInner}] + Ok(handler.${operation.name}(requests).asJsonEither)""" + + case (_: MonixObservableTpe, _: UnaryTpe) => + q"""val requests = msg.asStream[${operation.request.safeInner}] + Ok(handler.${operation.name}(requests.toObservable).map(_.asJson))""" + + case (_: UnaryTpe, _: MonixObservableTpe) => + q"""for { + request <- msg.as[${operation.request.safeInner}] + responses <- Ok(handler.${operation.name}(request).toFs2Stream.asJsonEither) + } yield responses""" + + case (_: MonixObservableTpe, _: MonixObservableTpe) => + q"""val requests = msg.asStream[${operation.request.safeInner}] + Ok(handler.${operation.name}(requests.toObservable).toFs2Stream.asJsonEither)""" + + case _ => + q"""for { + request <- msg.as[${operation.request.safeInner}] + response <- Ok(handler.${operation.name}(request).map(_.asJson)).adaptErrors + } yield response""" + } + + def toRouteTree: Tree = request match { + case _: EmptyTpe => + cq"""GET -> Root / ${operation.name} => Ok(handler.${operation.name}(_root_.higherkindness.mu.rpc.protocol.Empty).map(_.asJson))""" + + case _ => + cq"""msg @ POST -> Root / ${operation.name} => $routeTypology""" + } } @@ -498,11 +550,12 @@ object serviceImpl { val httpClient: DefDef = q""" def httpClient[$F_](uri: Uri) (implicit F: _root_.cats.effect.ConcurrentEffect[$F], ec: scala.concurrent.ExecutionContext): $HttpClient[$F] = { - new $HttpClient[$F](uri) + new $HttpClient[$F](uri / ${serviceDef.name.toString}) }""" val httpImports: List[Tree] = List( q"import _root_.higherkindness.mu.rpc.http.Utils._", + q"import _root_.cats.syntax.flatMap._", q"import _root_.cats.syntax.functor._", q"import _root_.org.http4s._", q"import _root_.org.http4s.circe._", @@ -535,23 +588,41 @@ object serviceImpl { println("&&&&&&&&&&&&&&&&&&") println(serviceDef.name.toTermName) -// val httpRoutesCases = operations.map(_.toRouteTree) - val httpRoutesCases = - operations.map(op => - cq"""GET -> Root / ${op.operation.name} => Ok(handler.getHello(_root_.higherkindness.mu.rpc.protocol.Empty).map(_.asJson)) """) - val routesPF = q"{ case ..$httpRoutesCases }" + val httpRoutesCases: Seq[Tree] = operations.map(_.toRouteTree) + + val routesPF: Tree = q"{ case ..$httpRoutesCases }" + + val requestTypes: Set[String] = + operations.filterNot(_.operation.request.isEmpty).map(_.operation.request.flatName).toSet + + val requestDecoders = + requestTypes.map(n => + q"""implicit private val ${TermName("decoder" + n)}:EntityDecoder[F, ${TypeName(n)}] = + jsonOf[F, ${TypeName(n)}]""") - val HttpRestService = TypeName(serviceDef.name.toString + "RestService") + val HttpRestService: TypeName = TypeName(serviceDef.name.toString + "RestService") - val httpRestServiceClass = q""" + val httpRestServiceClass: Tree = operations + .find(_.operation.isMonixObservable) + .fold(q""" class $HttpRestService[$F_](implicit handler: ${serviceDef.name}[F], F: _root_.cats.effect.Sync[$F]) extends _root_.org.http4s.dsl.Http4sDsl[F] { + ..$requestDecoders def service: HttpRoutes[F] = HttpRoutes.of[F]{$routesPF} - }""" + }""")(_ => q""" + class $HttpRestService[$F_](implicit handler: ${serviceDef.name}[F], F: _root_.cats.effect.ConcurrentEffect[$F], sc: scala.concurrent.ExecutionContext) extends _root_.org.http4s.dsl.Http4sDsl[F] { + ..$requestDecoders + def service: HttpRoutes[F] = HttpRoutes.of[F]{$routesPF} + }""") - val httpService = q""" - def route[$F_](implicit handler: ${serviceDef.name}[F], F: _root_.cats.effect.Sync[$F]): HttpRoutes[F] = { - new $HttpRestService[$F].service - }""" + val httpService = operations + .find(_.operation.isMonixObservable) + .fold(q""" + def route[$F_](implicit handler: ${serviceDef.name}[F], F: _root_.cats.effect.Sync[$F]): _root_.higherkindness.mu.http.RouteMap[F] = { + _root_.higherkindness.mu.http.RouteMap[F](${serviceDef.name.toString}, new $HttpRestService[$F].service) + }""")(_ => q""" + def route[$F_](implicit handler: ${serviceDef.name}[F], F: _root_.cats.effect.ConcurrentEffect[$F], sc: scala.concurrent.ExecutionContext): _root_.higherkindness.mu.http.RouteMap[F] = { + _root_.higherkindness.mu.http.RouteMap[F](${serviceDef.name.toString}, new $HttpRestService[$F].service) + }""") // // From 3888be787dfcc55ce71e67ef3ff0674dac6aaa53 Mon Sep 17 00:00:00 2001 From: rafaparadela Date: Sat, 2 Mar 2019 22:26:57 -0800 Subject: [PATCH 23/35] fixes the binding pattern in POST routes --- .../mu/rpc/internal/serviceImpl.scala | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/modules/internal/src/main/scala/higherkindness/mu/rpc/internal/serviceImpl.scala b/modules/internal/src/main/scala/higherkindness/mu/rpc/internal/serviceImpl.scala index 22a960111..fb694d216 100644 --- a/modules/internal/src/main/scala/higherkindness/mu/rpc/internal/serviceImpl.scala +++ b/modules/internal/src/main/scala/higherkindness/mu/rpc/internal/serviceImpl.scala @@ -485,13 +485,11 @@ object serviceImpl { val routeTypology: Tree = (request, response) match { case (_: Fs2StreamTpe, _: UnaryTpe) => - q"""println("@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@entra por Fs2StreamTpe-UnaryTpe") - val requests = msg.asStream[${operation.request.safeInner}] + q"""val requests = msg.asStream[${operation.request.safeInner}] Ok(handler.${operation.name}(requests).map(_.asJson))""" case (_: UnaryTpe, _: Fs2StreamTpe) => - q"""println("@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@entra por UnaryTpe-Fs2StreamTpe") - for { + q"""for { request <- msg.as[${operation.request.safeInner}] responses <- Ok(handler.${operation.name}(request).asJsonEither) } yield responses""" @@ -514,6 +512,9 @@ object serviceImpl { q"""val requests = msg.asStream[${operation.request.safeInner}] Ok(handler.${operation.name}(requests.toObservable).toFs2Stream.asJsonEither)""" + case (_: EmptyTpe, _) => + q"""Ok(handler.${operation.name}(_root_.higherkindness.mu.rpc.protocol.Empty).map(_.asJson))""" + case _ => q"""for { request <- msg.as[${operation.request.safeInner}] @@ -521,12 +522,12 @@ object serviceImpl { } yield response""" } - def toRouteTree: Tree = request match { - case _: EmptyTpe => - cq"""GET -> Root / ${operation.name} => Ok(handler.${operation.name}(_root_.higherkindness.mu.rpc.protocol.Empty).map(_.asJson))""" + val getPattern = pq"GET -> Root / ${operation.name.toString}" + val postPattern = pq"msg @ POST -> Root / ${operation.name.toString}" - case _ => - cq"""msg @ POST -> Root / ${operation.name} => $routeTypology""" + def toRouteTree: Tree = request match { + case _: EmptyTpe => cq"$getPattern => $routeTypology" + case _ => cq"$postPattern => $routeTypology" } } @@ -541,13 +542,13 @@ object serviceImpl { val httpRequests = operations.map(_.toRequestTree) - val HttpClient = TypeName("HttpClient") - val httpClientClass: ClassDef = q""" + val HttpClient = TypeName("HttpClient") + val httpClientClass = q""" class $HttpClient[$F_](uri: Uri)(implicit F: _root_.cats.effect.ConcurrentEffect[$F], ec: scala.concurrent.ExecutionContext) { ..$httpRequests }""" - val httpClient: DefDef = q""" + val httpClient = q""" def httpClient[$F_](uri: Uri) (implicit F: _root_.cats.effect.ConcurrentEffect[$F], ec: scala.concurrent.ExecutionContext): $HttpClient[$F] = { new $HttpClient[$F](uri / ${serviceDef.name.toString}) From 965fdd49944a6b10805ed519d41b93b4db8224ab Mon Sep 17 00:00:00 2001 From: rafaparadela Date: Sun, 3 Mar 2019 16:48:42 -0800 Subject: [PATCH 24/35] adds tests to cover all the possible types of endpoints --- .../mu/rpc/http/GreeterDerivedRestTests.scala | 296 ++++---- .../mu/rpc/http/GreeterRestServices.scala | 1 - .../mu/rpc/http/GreeterRestTests.scala | 674 +++++++----------- .../mu/rpc/http/GreeterServices.scala | 8 +- .../mu/rpc/internal/serviceImpl.scala | 46 -- 5 files changed, 402 insertions(+), 623 deletions(-) diff --git a/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterDerivedRestTests.scala b/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterDerivedRestTests.scala index a5b5d8bd9..7009a0548 100644 --- a/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterDerivedRestTests.scala +++ b/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterDerivedRestTests.scala @@ -53,19 +53,19 @@ class GreeterDerivedRestTests implicit val fs2HandlerIO = new Fs2GreeterHandler[IO] implicit val monixHandlerIO = new MonixGreeterHandler[IO] -// val unaryRoute: RouteMap[IO] = UnaryGreeter.route[IO] - val fs2Route: RouteMap[IO] = Fs2Greeter.route[IO] -// val monixRoute: RouteMap[IO] = MonixGreeter.route[IO] + val unaryRoute: RouteMap[IO] = UnaryGreeter.route[IO] + val fs2Route: RouteMap[IO] = Fs2Greeter.route[IO] + val monixRoute: RouteMap[IO] = MonixGreeter.route[IO] -// val server: BlazeServerBuilder[IO] = HttpServer.bind(port, host, unaryRoute, fs2Route, monixRoute) + val server: BlazeServerBuilder[IO] = HttpServer.bind(port, host, unaryRoute, fs2Route, monixRoute) // val server1: BlazeServerBuilder[IO] = BlazeServerBuilder[IO] // .bindHttp(port, host) // .withHttpApp(Router("/Fs2Greeter" -> new Fs2GreeterRestService[IO].service).orNotFound) - - val server: BlazeServerBuilder[IO] = BlazeServerBuilder[IO] - .bindHttp(port, host) - .withHttpApp(Router(s"/${fs2Route.prefix}" -> fs2Route.route).orNotFound) +// +// val server: BlazeServerBuilder[IO] = BlazeServerBuilder[IO] +// .bindHttp(port, host) +// .withHttpApp(Router(s"/${fs2Route.prefix}" -> fs2Route.route).orNotFound) var serverTask: Fiber[IO, Nothing] = _ // sorry before(serverTask = server.resource.use(_ => IO.never).start.unsafeRunSync()) @@ -73,77 +73,77 @@ class GreeterDerivedRestTests "REST Service" should { -// val unaryClient = UnaryGreeter.httpClient[IO](serviceUri) - val fs2Client = Fs2Greeter.httpClient[IO](serviceUri) -// val monixClient = MonixGreeter.httpClient[IO](serviceUri) + val unaryClient = UnaryGreeter.httpClient[IO](serviceUri) + val fs2Client = Fs2Greeter.httpClient[IO](serviceUri) + val monixClient = MonixGreeter.httpClient[IO](serviceUri) -// "serve a GET request" in { -// val response: IO[HelloResponse] = -// BlazeClientBuilder[IO](ec).resource.use(unaryClient.getHello(_)) -// response.unsafeRunSync() shouldBe HelloResponse("hey") -// } -// -// "serve a unary POST request" in { -// val response: IO[HelloResponse] = -// BlazeClientBuilder[IO](ec).resource.use(unaryClient.sayHello(HelloRequest("hey"))(_)) -// response.unsafeRunSync() shouldBe HelloResponse("hey") -// } -// -// "handle a raised gRPC exception in a unary POST request" in { -// val response: IO[HelloResponse] = -// BlazeClientBuilder[IO](ec).resource.use(unaryClient.sayHello(HelloRequest("SRE"))(_)) -// -// the[ResponseError] thrownBy response.unsafeRunSync() shouldBe ResponseError( -// Status.BadRequest, -// Some("INVALID_ARGUMENT: SRE")) -// } -// -// "handle a raised non-gRPC exception in a unary POST request" in { -// val response: IO[HelloResponse] = -// BlazeClientBuilder[IO](ec).resource.use(unaryClient.sayHello(HelloRequest("RTE"))(_)) -// -// the[ResponseError] thrownBy response.unsafeRunSync() shouldBe ResponseError( -// Status.InternalServerError, -// Some("RTE")) -// } -// -// "handle a thrown exception in a unary POST request" in { -// val response: IO[HelloResponse] = -// BlazeClientBuilder[IO](ec).resource.use(unaryClient.sayHello(HelloRequest("TR"))(_)) -// -// the[ResponseError] thrownBy response.unsafeRunSync() shouldBe ResponseError( -// Status.InternalServerError) -// } -// -// "serve a POST request with fs2 streaming request" in { -// -// val requests = Stream(HelloRequest("hey"), HelloRequest("there")) -// -// val response: IO[HelloResponse] = -// BlazeClientBuilder[IO](ec).resource.use(fs2Client.sayHellos(requests)(_)) -// response.unsafeRunSync() shouldBe HelloResponse("hey, there") -// } -// -// "serve a POST request with empty fs2 streaming request" in { -// val requests = Stream.empty -// val response = -// BlazeClientBuilder[IO](ec).resource.use(fs2Client.sayHellos(requests)(_)) -// response.unsafeRunSync() shouldBe HelloResponse("") -// } -// -// "serve a POST request with Observable streaming request" in { -// val requests = Observable(HelloRequest("hey"), HelloRequest("there")) -// val response = -// BlazeClientBuilder[IO](ec).resource.use(monixClient.sayHellos(requests)(_)) -// response.unsafeRunSync() shouldBe HelloResponse("hey, there") -// } -// -// "serve a POST request with empty Observable streaming request" in { -// val requests = Observable.empty -// val response = -// BlazeClientBuilder[IO](ec).resource.use(monixClient.sayHellos(requests)(_)) -// response.unsafeRunSync() shouldBe HelloResponse("") -// } + "serve a GET request" in { + val response: IO[HelloResponse] = + BlazeClientBuilder[IO](ec).resource.use(unaryClient.getHello(_)) + response.unsafeRunSync() shouldBe HelloResponse("hey") + } + + "serve a unary POST request" in { + val response: IO[HelloResponse] = + BlazeClientBuilder[IO](ec).resource.use(unaryClient.sayHello(HelloRequest("hey"))(_)) + response.unsafeRunSync() shouldBe HelloResponse("hey") + } + + "handle a raised gRPC exception in a unary POST request" in { + val response: IO[HelloResponse] = + BlazeClientBuilder[IO](ec).resource.use(unaryClient.sayHello(HelloRequest("SRE"))(_)) + + the[ResponseError] thrownBy response.unsafeRunSync() shouldBe ResponseError( + Status.BadRequest, + Some("INVALID_ARGUMENT: SRE")) + } + + "handle a raised non-gRPC exception in a unary POST request" in { + val response: IO[HelloResponse] = + BlazeClientBuilder[IO](ec).resource.use(unaryClient.sayHello(HelloRequest("RTE"))(_)) + + the[ResponseError] thrownBy response.unsafeRunSync() shouldBe ResponseError( + Status.InternalServerError, + Some("RTE")) + } + + "handle a thrown exception in a unary POST request" in { + val response: IO[HelloResponse] = + BlazeClientBuilder[IO](ec).resource.use(unaryClient.sayHello(HelloRequest("TR"))(_)) + + the[ResponseError] thrownBy response.unsafeRunSync() shouldBe ResponseError( + Status.InternalServerError) + } + + "serve a POST request with fs2 streaming request" in { + + val requests = Stream(HelloRequest("hey"), HelloRequest("there")) + + val response: IO[HelloResponse] = + BlazeClientBuilder[IO](ec).resource.use(fs2Client.sayHellos(requests)(_)) + response.unsafeRunSync() shouldBe HelloResponse("hey, there") + } + + "serve a POST request with empty fs2 streaming request" in { + val requests = Stream.empty + val response = + BlazeClientBuilder[IO](ec).resource.use(fs2Client.sayHellos(requests)(_)) + response.unsafeRunSync() shouldBe HelloResponse("") + } + + "serve a POST request with Observable streaming request" in { + val requests = Observable(HelloRequest("hey"), HelloRequest("there")) + val response = + BlazeClientBuilder[IO](ec).resource.use(monixClient.sayHellos(requests)(_)) + response.unsafeRunSync() shouldBe HelloResponse("hey, there") + } + + "serve a POST request with empty Observable streaming request" in { + val requests = Observable.empty + val response = + BlazeClientBuilder[IO](ec).resource.use(monixClient.sayHellos(requests)(_)) + response.unsafeRunSync() shouldBe HelloResponse("") + } "serve a POST request with fs2 streaming response" in { val request = HelloRequest("hey") @@ -152,76 +152,76 @@ class GreeterDerivedRestTests responses.compile.toList .unsafeRunSync() shouldBe List(HelloResponse("hey"), HelloResponse("hey")) } -// -// "serve a POST request with Observable streaming response" in { -// val request = HelloRequest("hey") -// val responses = BlazeClientBuilder[IO](ec).stream -// .flatMap(monixClient.sayHelloAll(request)(_).toFs2Stream[IO]) -// responses.compile.toList -// .unsafeRunTimed(10.seconds) -// .getOrElse(sys.error("Stuck!")) shouldBe List(HelloResponse("hey"), HelloResponse("hey")) -// } -// -// "handle errors with fs2 streaming response" in { -// val request = HelloRequest("") -// val responses = -// BlazeClientBuilder[IO](ec).stream.flatMap(fs2Client.sayHelloAll(request)(_)) -// the[IllegalArgumentException] thrownBy responses.compile.toList -// .unsafeRunSync() should have message "empty greeting" -// } -// -// "handle errors with Observable streaming response" in { -// val request = HelloRequest("") -// val responses = BlazeClientBuilder[IO](ec).stream -// .flatMap(monixClient.sayHelloAll(request)(_).toFs2Stream[IO]) -// the[IllegalArgumentException] thrownBy responses.compile.toList -// .unsafeRunTimed(10.seconds) -// .getOrElse(sys.error("Stuck!")) should have message "empty greeting" -// } -// -// "serve a POST request with bidirectional fs2 streaming" in { -// val requests = Stream(HelloRequest("hey"), HelloRequest("there")) -// val responses = -// BlazeClientBuilder[IO](ec).stream.flatMap(fs2Client.sayHellosAll(requests)(_)) -// responses.compile.toList -// .unsafeRunSync() shouldBe List(HelloResponse("hey"), HelloResponse("there")) -// } -// -// "serve an empty POST request with bidirectional fs2 streaming" in { -// val requests = Stream.empty -// val responses = -// BlazeClientBuilder[IO](ec).stream.flatMap(fs2Client.sayHellosAll(requests)(_)) -// responses.compile.toList.unsafeRunSync() shouldBe Nil -// } -// -// "serve a POST request with bidirectional Observable streaming" in { -// val requests = Observable(HelloRequest("hey"), HelloRequest("there")) -// val responses = BlazeClientBuilder[IO](ec).stream -// .flatMap(monixClient.sayHellosAll(requests)(_).toFs2Stream[IO]) -// responses.compile.toList -// .unsafeRunTimed(10.seconds) -// .getOrElse(sys.error("Stuck!")) shouldBe List(HelloResponse("hey"), HelloResponse("there")) -// } -// -// "serve an empty POST request with bidirectional Observable streaming" in { -// val requests = Observable.empty -// val responses = BlazeClientBuilder[IO](ec).stream -// .flatMap(monixClient.sayHellosAll(requests)(_).toFs2Stream[IO]) -// responses.compile.toList -// .unsafeRunTimed(10.seconds) -// .getOrElse(sys.error("Stuck!")) shouldBe Nil -// } -// -// "serve ScalaCheck-generated POST requests with bidirectional Observable streaming" in { -// forAll { strings: List[String] => -// val requests = Observable.fromIterable(strings.map(HelloRequest)) -// val responses = BlazeClientBuilder[IO](ec).stream -// .flatMap(monixClient.sayHellosAll(requests)(_).toFs2Stream[IO]) -// responses.compile.toList -// .unsafeRunTimed(10.seconds) -// .getOrElse(sys.error("Stuck!")) shouldBe strings.map(HelloResponse) -// } -// } + + "serve a POST request with Observable streaming response" in { + val request = HelloRequest("hey") + val responses = BlazeClientBuilder[IO](ec).stream + .flatMap(monixClient.sayHelloAll(request)(_).toFs2Stream[IO]) + responses.compile.toList + .unsafeRunTimed(10.seconds) + .getOrElse(sys.error("Stuck!")) shouldBe List(HelloResponse("hey"), HelloResponse("hey")) + } + + "handle errors with fs2 streaming response" in { + val request = HelloRequest("") + val responses = + BlazeClientBuilder[IO](ec).stream.flatMap(fs2Client.sayHelloAll(request)(_)) + the[IllegalArgumentException] thrownBy responses.compile.toList + .unsafeRunSync() should have message "empty greeting" + } + + "handle errors with Observable streaming response" in { + val request = HelloRequest("") + val responses = BlazeClientBuilder[IO](ec).stream + .flatMap(monixClient.sayHelloAll(request)(_).toFs2Stream[IO]) + the[IllegalArgumentException] thrownBy responses.compile.toList + .unsafeRunTimed(10.seconds) + .getOrElse(sys.error("Stuck!")) should have message "empty greeting" + } + + "serve a POST request with bidirectional fs2 streaming" in { + val requests = Stream(HelloRequest("hey"), HelloRequest("there")) + val responses = + BlazeClientBuilder[IO](ec).stream.flatMap(fs2Client.sayHellosAll(requests)(_)) + responses.compile.toList + .unsafeRunSync() shouldBe List(HelloResponse("hey"), HelloResponse("there")) + } + + "serve an empty POST request with bidirectional fs2 streaming" in { + val requests = Stream.empty + val responses = + BlazeClientBuilder[IO](ec).stream.flatMap(fs2Client.sayHellosAll(requests)(_)) + responses.compile.toList.unsafeRunSync() shouldBe Nil + } + + "serve a POST request with bidirectional Observable streaming" in { + val requests = Observable(HelloRequest("hey"), HelloRequest("there")) + val responses = BlazeClientBuilder[IO](ec).stream + .flatMap(monixClient.sayHellosAll(requests)(_).toFs2Stream[IO]) + responses.compile.toList + .unsafeRunTimed(10.seconds) + .getOrElse(sys.error("Stuck!")) shouldBe List(HelloResponse("hey"), HelloResponse("there")) + } + + "serve an empty POST request with bidirectional Observable streaming" in { + val requests = Observable.empty + val responses = BlazeClientBuilder[IO](ec).stream + .flatMap(monixClient.sayHellosAll(requests)(_).toFs2Stream[IO]) + responses.compile.toList + .unsafeRunTimed(10.seconds) + .getOrElse(sys.error("Stuck!")) shouldBe Nil + } + + "serve ScalaCheck-generated POST requests with bidirectional Observable streaming" in { + forAll { strings: List[String] => + val requests = Observable.fromIterable(strings.map(HelloRequest)) + val responses = BlazeClientBuilder[IO](ec).stream + .flatMap(monixClient.sayHellosAll(requests)(_).toFs2Stream[IO]) + responses.compile.toList + .unsafeRunTimed(10.seconds) + .getOrElse(sys.error("Stuck!")) shouldBe strings.map(HelloResponse) + } + } } diff --git a/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterRestServices.scala b/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterRestServices.scala index e7470986f..4027ea2e0 100644 --- a/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterRestServices.scala +++ b/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterRestServices.scala @@ -16,7 +16,6 @@ package higherkindness.mu.rpc.http -import cats.MonadError import cats.effect._ import cats.syntax.flatMap._ import cats.syntax.functor._ diff --git a/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterRestTests.scala b/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterRestTests.scala index a2dbc870c..3bdb33dfa 100644 --- a/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterRestTests.scala +++ b/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterRestTests.scala @@ -14,427 +14,253 @@ * limitations under the License. */ -///* -// * Copyright 2017-2019 47 Degrees, LLC. -// * -// * Licensed under the Apache License, Version 2.0 (the "License"); -// * you may not use this file except in compliance with the License. -// * You may obtain a copy of the License at -// * -// * http://www.apache.org/licenses/LICENSE-2.0 -// * -// * Unless required by applicable law or agreed to in writing, software -// * distributed under the License is distributed on an "AS IS" BASIS, -// * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// * See the License for the specific language governing permissions and -// * limitations under the License. -// */ -// -//package higherkindness.mu.rpc.http -// -//import cats.effect.{IO, _} -//import fs2.Stream -//import higherkindness.mu.rpc.common.RpcBaseTestSuite -//import higherkindness.mu.rpc.http.Utils._ -//import higherkindness.mu.rpc.protocol.Empty -//import io.circe.Json -//import io.circe.generic.auto._ -//import io.circe.syntax._ -//import monix.reactive.Observable -//import org.http4s._ -//import org.http4s.circe._ -//import org.http4s.client.UnexpectedStatus -//import org.http4s.client.blaze.BlazeClientBuilder -//import org.http4s.dsl.io._ -//import org.http4s.server._ -//import org.http4s.server.blaze._ -//import org.scalatest._ -//import org.scalatest.prop.GeneratorDrivenPropertyChecks -// -//import scala.concurrent.duration._ -// -//class GreeterRestTests -// extends RpcBaseTestSuite -// with GeneratorDrivenPropertyChecks -// with BeforeAndAfter { -// -// val Hostname = "localhost" -// val Port = 8080 -// -// val serviceUri: Uri = Uri.unsafeFromString(s"http://$Hostname:$Port") -// -// val UnaryServicePrefix = "UnaryGreeter" -// val Fs2ServicePrefix = "Fs2Greeter" -// val MonixServicePrefix = "MonixGreeter" -// -// implicit val ec = monix.execution.Scheduler.Implicits.global -// implicit val cs: ContextShift[IO] = IO.contextShift(ec) -// -// implicit val unaryHandlerIO = new UnaryGreeterHandler[IO] -// implicit val fs2HandlerIO = new Fs2GreeterHandler[IO] -// implicit val monixHandlerIO = new MonixGreeterHandler[IO] -// -// //TODO: add Logger middleware -// val unaryService: HttpRoutes[IO] = new UnaryGreeterRestService[IO].service -// val fs2Service: HttpRoutes[IO] = new Fs2GreeterRestService[IO].service -// val monixService: HttpRoutes[IO] = new MonixGreeterRestService[IO].service -// -// val server: BlazeServerBuilder[IO] = BlazeServerBuilder[IO] -// .bindHttp(Port, Hostname) -// //.enableHttp2(true) -// //.withSSLContext(GenericSSLContext.serverSSLContext) -// .withHttpApp( -// Router( -// s"/$UnaryServicePrefix" -> unaryService, -// s"/$Fs2ServicePrefix" -> fs2Service, -// s"/$MonixServicePrefix" -> monixService).orNotFound) -// -// var serverTask: Fiber[IO, Nothing] = _ // sorry -// before(serverTask = server.resource.use(_ => IO.never).start.unsafeRunSync()) -// after(serverTask.cancel) -// -// "REST Server" should { -// -// "serve a GET request" in { -// val request = Request[IO](Method.GET, serviceUri / UnaryServicePrefix / "getHello") -// val response = BlazeClientBuilder[IO](ec).resource.use(_.expect[Json](request)) -// response.unsafeRunSync() shouldBe HelloResponse("hey").asJson -// } -// -// "serve a POST request" in { -// val request = Request[IO](Method.POST, serviceUri / UnaryServicePrefix / "sayHello") -// val requestBody = HelloRequest("hey").asJson -// val response = -// BlazeClientBuilder[IO](ec).resource.use(_.expect[Json](request.withEntity(requestBody))) -// response.unsafeRunSync() shouldBe HelloResponse("hey").asJson -// } -// -// "return a 400 Bad Request for a malformed unary POST request" in { -// val request = Request[IO](Method.POST, serviceUri / UnaryServicePrefix / "sayHello") -// val requestBody = "{" -// val response = -// BlazeClientBuilder[IO](ec).resource.use(_.expect[Json](request.withEntity(requestBody))) -// the[UnexpectedStatus] thrownBy response.unsafeRunSync() shouldBe UnexpectedStatus( -// Status.BadRequest) -// } -// -// "return a 400 Bad Request for a malformed streaming POST request" in { -// val request = Request[IO](Method.POST, serviceUri / Fs2ServicePrefix / "sayHellos") -// val requestBody = "{" -// val response = -// BlazeClientBuilder[IO](ec).resource.use(_.expect[Json](request.withEntity(requestBody))) -// the[UnexpectedStatus] thrownBy response.unsafeRunSync() shouldBe UnexpectedStatus( -// Status.BadRequest) -// } -// -// } -// -// val unaryServiceClient: UnaryGreeterRestClient[IO] = -// new UnaryGreeterRestClient[IO](serviceUri / UnaryServicePrefix) -// val fs2ServiceClient: Fs2GreeterRestClient[IO] = -// new Fs2GreeterRestClient[IO](serviceUri / Fs2ServicePrefix) -// val monixServiceClient: MonixGreeterRestClient[IO] = -// new MonixGreeterRestClient[IO](serviceUri / MonixServicePrefix) -// -// "REST Service" should { -// -// "serve a GET request" in { -// val response = BlazeClientBuilder[IO](ec).resource.use(unaryServiceClient.getHello()(_)) -// response.unsafeRunSync() shouldBe HelloResponse("hey") -// } -// -// "serve a unary POST request" in { -// val request = HelloRequest("hey") -// val response = -// BlazeClientBuilder[IO](ec).resource.use(unaryServiceClient.sayHello(request)(_)) -// response.unsafeRunSync() shouldBe HelloResponse("hey") -// } -// -// "handle a raised gRPC exception in a unary POST request" in { -// val request = HelloRequest("SRE") -// val response = -// BlazeClientBuilder[IO](ec).resource.use(unaryServiceClient.sayHello(request)(_)) -// the[ResponseError] thrownBy response.unsafeRunSync() shouldBe ResponseError( -// Status.BadRequest, -// Some("INVALID_ARGUMENT: SRE")) -// } -// -// "handle a raised non-gRPC exception in a unary POST request" in { -// val request = HelloRequest("RTE") -// val response = -// BlazeClientBuilder[IO](ec).resource.use(unaryServiceClient.sayHello(request)(_)) -// the[ResponseError] thrownBy response.unsafeRunSync() shouldBe ResponseError( -// Status.InternalServerError, -// Some("RTE")) -// } -// -// "handle a thrown exception in a unary POST request" in { -// val request = HelloRequest("TR") -// val response = -// BlazeClientBuilder[IO](ec).resource.use(unaryServiceClient.sayHello(request)(_)) -// the[ResponseError] thrownBy response.unsafeRunSync() shouldBe ResponseError( -// Status.InternalServerError) -// } -// -// "serve a POST request with fs2 streaming request" in { -// val requests = Stream(HelloRequest("hey"), HelloRequest("there")) -// val response = -// BlazeClientBuilder[IO](ec).resource.use(fs2ServiceClient.sayHellos(requests)(_)) -// response.unsafeRunSync() shouldBe HelloResponse("hey, there") -// } -// -// "serve a POST request with empty fs2 streaming request" in { -// val requests = Stream.empty -// val response = -// BlazeClientBuilder[IO](ec).resource.use(fs2ServiceClient.sayHellos(requests)(_)) -// response.unsafeRunSync() shouldBe HelloResponse("") -// } -// -// "serve a POST request with Observable streaming request" in { -// val requests = Observable(HelloRequest("hey"), HelloRequest("there")) -// val response = -// BlazeClientBuilder[IO](ec).resource.use(monixServiceClient.sayHellos(requests)(_)) -// response.unsafeRunSync() shouldBe HelloResponse("hey, there") -// } -// -// "serve a POST request with empty Observable streaming request" in { -// val requests = Observable.empty -// val response = -// BlazeClientBuilder[IO](ec).resource.use(monixServiceClient.sayHellos(requests)(_)) -// response.unsafeRunSync() shouldBe HelloResponse("") -// } -// -// "serve a POST request with fs2 streaming response" in { -// val request = HelloRequest("hey") -// val responses = -// BlazeClientBuilder[IO](ec).stream.flatMap(fs2ServiceClient.sayHelloAll(request)(_)) -// responses.compile.toList -// .unsafeRunSync() shouldBe List(HelloResponse("hey"), HelloResponse("hey")) -// } -// -// "serve a POST request with Observable streaming response" in { -// val request = HelloRequest("hey") -// val responses = BlazeClientBuilder[IO](ec).stream -// .flatMap(monixServiceClient.sayHelloAll(request)(_).toFs2Stream[IO]) -// responses.compile.toList -// .unsafeRunTimed(10.seconds) -// .getOrElse(sys.error("Stuck!")) shouldBe List(HelloResponse("hey"), HelloResponse("hey")) -// } -// -// "handle errors with fs2 streaming response" in { -// val request = HelloRequest("") -// val responses = -// BlazeClientBuilder[IO](ec).stream.flatMap(fs2ServiceClient.sayHelloAll(request)(_)) -// the[IllegalArgumentException] thrownBy responses.compile.toList -// .unsafeRunSync() should have message "empty greeting" -// } -// -// "handle errors with Observable streaming response" in { -// val request = HelloRequest("") -// val responses = BlazeClientBuilder[IO](ec).stream -// .flatMap(monixServiceClient.sayHelloAll(request)(_).toFs2Stream[IO]) -// the[IllegalArgumentException] thrownBy responses.compile.toList -// .unsafeRunTimed(10.seconds) -// .getOrElse(sys.error("Stuck!")) should have message "empty greeting" -// } -// -// "serve a POST request with bidirectional fs2 streaming" in { -// val requests = Stream(HelloRequest("hey"), HelloRequest("there")) -// val responses = -// BlazeClientBuilder[IO](ec).stream.flatMap(fs2ServiceClient.sayHellosAll(requests)(_)) -// responses.compile.toList -// .unsafeRunSync() shouldBe List(HelloResponse("hey"), HelloResponse("there")) -// } -// -// "serve an empty POST request with bidirectional fs2 streaming" in { -// val requests = Stream.empty -// val responses = -// BlazeClientBuilder[IO](ec).stream.flatMap(fs2ServiceClient.sayHellosAll(requests)(_)) -// responses.compile.toList.unsafeRunSync() shouldBe Nil -// } -// -// "serve a POST request with bidirectional Observable streaming" in { -// val requests = Observable(HelloRequest("hey"), HelloRequest("there")) -// val responses = BlazeClientBuilder[IO](ec).stream -// .flatMap(monixServiceClient.sayHellosAll(requests)(_).toFs2Stream[IO]) -// responses.compile.toList -// .unsafeRunTimed(10.seconds) -// .getOrElse(sys.error("Stuck!")) shouldBe List(HelloResponse("hey"), HelloResponse("there")) -// } -// -// "serve an empty POST request with bidirectional Observable streaming" in { -// val requests = Observable.empty -// val responses = BlazeClientBuilder[IO](ec).stream -// .flatMap(monixServiceClient.sayHellosAll(requests)(_).toFs2Stream[IO]) -// responses.compile.toList -// .unsafeRunTimed(10.seconds) -// .getOrElse(sys.error("Stuck!")) shouldBe Nil -// } -// -// "serve ScalaCheck-generated POST requests with bidirectional Observable streaming" in { -// forAll { strings: List[String] => -// val requests = Observable.fromIterable(strings.map(HelloRequest)) -// val responses = BlazeClientBuilder[IO](ec).stream -// .flatMap(monixServiceClient.sayHellosAll(requests)(_).toFs2Stream[IO]) -// responses.compile.toList -// .unsafeRunTimed(10.seconds) -// .getOrElse(sys.error("Stuck!")) shouldBe strings.map(HelloResponse) -// } -// } -// } -// -// "Auto-derived REST Client" should { -// -// val unaryClient = UnaryGreeter.httpClient[IO](serviceUri) -// val fs2Client = Fs2Greeter.httpClient[IO](serviceUri) -// val monixClient = MonixGreeter.httpClient[IO](serviceUri) -// -// "serve a GET request" in { -// val response: IO[HelloResponse] = -// BlazeClientBuilder[IO](ec).resource.use(unaryClient.getHello(_)) -// response.unsafeRunSync() shouldBe HelloResponse("hey") -// } -// -// "serve a unary POST request" in { -// val response: IO[HelloResponse] = -// BlazeClientBuilder[IO](ec).resource.use(unaryClient.sayHello(HelloRequest("hey"))(_)) -// response.unsafeRunSync() shouldBe HelloResponse("hey") -// } -// -// "handle a raised gRPC exception in a unary POST request" in { -// val response: IO[HelloResponse] = -// BlazeClientBuilder[IO](ec).resource.use(unaryClient.sayHello(HelloRequest("SRE"))(_)) -// -// the[ResponseError] thrownBy response.unsafeRunSync() shouldBe ResponseError( -// Status.BadRequest, -// Some("INVALID_ARGUMENT: SRE")) -// } -// -// "handle a raised non-gRPC exception in a unary POST request" in { -// val response: IO[HelloResponse] = -// BlazeClientBuilder[IO](ec).resource.use(unaryClient.sayHello(HelloRequest("RTE"))(_)) -// -// the[ResponseError] thrownBy response.unsafeRunSync() shouldBe ResponseError( -// Status.InternalServerError, -// Some("RTE")) -// } -// -// "handle a thrown exception in a unary POST request" in { -// val response: IO[HelloResponse] = -// BlazeClientBuilder[IO](ec).resource.use(unaryClient.sayHello(HelloRequest("TR"))(_)) -// -// the[ResponseError] thrownBy response.unsafeRunSync() shouldBe ResponseError( -// Status.InternalServerError) -// } -// -// "serve a POST request with fs2 streaming request" in { -// -// val requests = Stream(HelloRequest("hey"), HelloRequest("there")) -// -// val response: IO[HelloResponse] = -// BlazeClientBuilder[IO](ec).resource.use(fs2Client.sayHellos(requests)(_)) -// response.unsafeRunSync() shouldBe HelloResponse("hey, there") -// } -// -// "serve a POST request with empty fs2 streaming request" in { -// val requests = Stream.empty -// val response = -// BlazeClientBuilder[IO](ec).resource.use(fs2Client.sayHellos(requests)(_)) -// response.unsafeRunSync() shouldBe HelloResponse("") -// } -// -// "serve a POST request with Observable streaming request" in { -// val requests = Observable(HelloRequest("hey"), HelloRequest("there")) -// val response = -// BlazeClientBuilder[IO](ec).resource.use(monixClient.sayHellos(requests)(_)) -// response.unsafeRunSync() shouldBe HelloResponse("hey, there") -// } -// -// "serve a POST request with empty Observable streaming request" in { -// val requests = Observable.empty -// val response = -// BlazeClientBuilder[IO](ec).resource.use(monixClient.sayHellos(requests)(_)) -// response.unsafeRunSync() shouldBe HelloResponse("") -// } -// -// "serve a POST request with fs2 streaming response" in { -// val request = HelloRequest("hey") -// val responses = -// BlazeClientBuilder[IO](ec).stream.flatMap(fs2Client.sayHelloAll(request)(_)) -// responses.compile.toList -// .unsafeRunSync() shouldBe List(HelloResponse("hey"), HelloResponse("hey")) -// } -// -// "serve a POST request with Observable streaming response" in { -// val request = HelloRequest("hey") -// val responses = BlazeClientBuilder[IO](ec).stream -// .flatMap(monixClient.sayHelloAll(request)(_).toFs2Stream[IO]) -// responses.compile.toList -// .unsafeRunTimed(10.seconds) -// .getOrElse(sys.error("Stuck!")) shouldBe List(HelloResponse("hey"), HelloResponse("hey")) -// } -// -// "handle errors with fs2 streaming response" in { -// val request = HelloRequest("") -// val responses = -// BlazeClientBuilder[IO](ec).stream.flatMap(fs2Client.sayHelloAll(request)(_)) -// the[IllegalArgumentException] thrownBy responses.compile.toList -// .unsafeRunSync() should have message "empty greeting" -// } -// -// "handle errors with Observable streaming response" in { -// val request = HelloRequest("") -// val responses = BlazeClientBuilder[IO](ec).stream -// .flatMap(monixClient.sayHelloAll(request)(_).toFs2Stream[IO]) -// the[IllegalArgumentException] thrownBy responses.compile.toList -// .unsafeRunTimed(10.seconds) -// .getOrElse(sys.error("Stuck!")) should have message "empty greeting" -// } -// -// "serve a POST request with bidirectional fs2 streaming" in { -// val requests = Stream(HelloRequest("hey"), HelloRequest("there")) -// val responses = -// BlazeClientBuilder[IO](ec).stream.flatMap(fs2Client.sayHellosAll(requests)(_)) -// responses.compile.toList -// .unsafeRunSync() shouldBe List(HelloResponse("hey"), HelloResponse("there")) -// } -// -// "serve an empty POST request with bidirectional fs2 streaming" in { -// val requests = Stream.empty -// val responses = -// BlazeClientBuilder[IO](ec).stream.flatMap(fs2Client.sayHellosAll(requests)(_)) -// responses.compile.toList.unsafeRunSync() shouldBe Nil -// } -// -// "serve a POST request with bidirectional Observable streaming" in { -// val requests = Observable(HelloRequest("hey"), HelloRequest("there")) -// val responses = BlazeClientBuilder[IO](ec).stream -// .flatMap(monixClient.sayHellosAll(requests)(_).toFs2Stream[IO]) -// responses.compile.toList -// .unsafeRunTimed(10.seconds) -// .getOrElse(sys.error("Stuck!")) shouldBe List(HelloResponse("hey"), HelloResponse("there")) -// } -// -// "serve an empty POST request with bidirectional Observable streaming" in { -// val requests = Observable.empty -// val responses = BlazeClientBuilder[IO](ec).stream -// .flatMap(monixClient.sayHellosAll(requests)(_).toFs2Stream[IO]) -// responses.compile.toList -// .unsafeRunTimed(10.seconds) -// .getOrElse(sys.error("Stuck!")) shouldBe Nil -// } -// -// "serve ScalaCheck-generated POST requests with bidirectional Observable streaming" in { -// forAll { strings: List[String] => -// val requests = Observable.fromIterable(strings.map(HelloRequest)) -// val responses = BlazeClientBuilder[IO](ec).stream -// .flatMap(monixClient.sayHellosAll(requests)(_).toFs2Stream[IO]) -// responses.compile.toList -// .unsafeRunTimed(10.seconds) -// .getOrElse(sys.error("Stuck!")) shouldBe strings.map(HelloResponse) -// } -// } -// -// } -// -//} +package higherkindness.mu.rpc.http + +import cats.effect.{IO, _} +import fs2.Stream +import higherkindness.mu.rpc.common.RpcBaseTestSuite +import higherkindness.mu.rpc.http.Utils._ +import io.circe.Json +import io.circe.generic.auto._ +import io.circe.syntax._ +import monix.reactive.Observable +import org.http4s._ +import org.http4s.circe._ +import org.http4s.client.UnexpectedStatus +import org.http4s.client.blaze.BlazeClientBuilder +import org.http4s.dsl.io._ +import org.http4s.server._ +import org.http4s.server.blaze._ +import org.scalatest._ +import org.scalatest.prop.GeneratorDrivenPropertyChecks + +import scala.concurrent.duration._ + +class GreeterRestTests + extends RpcBaseTestSuite + with GeneratorDrivenPropertyChecks + with BeforeAndAfter { + + val Hostname = "localhost" + val Port = 8080 + + val serviceUri: Uri = Uri.unsafeFromString(s"http://$Hostname:$Port") + + val UnaryServicePrefix = "UnaryGreeter" + val Fs2ServicePrefix = "Fs2Greeter" + val MonixServicePrefix = "MonixGreeter" + + implicit val ec = monix.execution.Scheduler.Implicits.global + implicit val cs: ContextShift[IO] = IO.contextShift(ec) + + implicit val unaryHandlerIO = new UnaryGreeterHandler[IO] + implicit val fs2HandlerIO = new Fs2GreeterHandler[IO] + implicit val monixHandlerIO = new MonixGreeterHandler[IO] + + //TODO: add Logger middleware + val unaryService: HttpRoutes[IO] = new UnaryGreeterRestService[IO].service + val fs2Service: HttpRoutes[IO] = new Fs2GreeterRestService[IO].service + val monixService: HttpRoutes[IO] = new MonixGreeterRestService[IO].service + + val server: BlazeServerBuilder[IO] = BlazeServerBuilder[IO] + .bindHttp(Port, Hostname) + .withHttpApp( + Router( + s"/$UnaryServicePrefix" -> unaryService, + s"/$Fs2ServicePrefix" -> fs2Service, + s"/$MonixServicePrefix" -> monixService).orNotFound) + + var serverTask: Fiber[IO, Nothing] = _ // sorry + before(serverTask = server.resource.use(_ => IO.never).start.unsafeRunSync()) + after(serverTask.cancel) + + "REST Server" should { + + "serve a GET request" in { + val request = Request[IO](Method.GET, serviceUri / UnaryServicePrefix / "getHello") + val response = BlazeClientBuilder[IO](ec).resource.use(_.expect[Json](request)) + response.unsafeRunSync() shouldBe HelloResponse("hey").asJson + } + + "serve a POST request" in { + val request = Request[IO](Method.POST, serviceUri / UnaryServicePrefix / "sayHello") + val requestBody = HelloRequest("hey").asJson + val response = + BlazeClientBuilder[IO](ec).resource.use(_.expect[Json](request.withEntity(requestBody))) + response.unsafeRunSync() shouldBe HelloResponse("hey").asJson + } + + "return a 400 Bad Request for a malformed unary POST request" in { + val request = Request[IO](Method.POST, serviceUri / UnaryServicePrefix / "sayHello") + val requestBody = "{" + val response = + BlazeClientBuilder[IO](ec).resource.use(_.expect[Json](request.withEntity(requestBody))) + the[UnexpectedStatus] thrownBy response.unsafeRunSync() shouldBe UnexpectedStatus( + Status.BadRequest) + } + + "return a 400 Bad Request for a malformed streaming POST request" in { + val request = Request[IO](Method.POST, serviceUri / Fs2ServicePrefix / "sayHellos") + val requestBody = "{" + val response = + BlazeClientBuilder[IO](ec).resource.use(_.expect[Json](request.withEntity(requestBody))) + the[UnexpectedStatus] thrownBy response.unsafeRunSync() shouldBe UnexpectedStatus( + Status.BadRequest) + } + + } + + val unaryServiceClient: UnaryGreeterRestClient[IO] = + new UnaryGreeterRestClient[IO](serviceUri / UnaryServicePrefix) + val fs2ServiceClient: Fs2GreeterRestClient[IO] = + new Fs2GreeterRestClient[IO](serviceUri / Fs2ServicePrefix) + val monixServiceClient: MonixGreeterRestClient[IO] = + new MonixGreeterRestClient[IO](serviceUri / MonixServicePrefix) + + "REST Service" should { + + "serve a GET request" in { + val response = BlazeClientBuilder[IO](ec).resource.use(unaryServiceClient.getHello()(_)) + response.unsafeRunSync() shouldBe HelloResponse("hey") + } + + "serve a unary POST request" in { + val request = HelloRequest("hey") + val response = + BlazeClientBuilder[IO](ec).resource.use(unaryServiceClient.sayHello(request)(_)) + response.unsafeRunSync() shouldBe HelloResponse("hey") + } + + "handle a raised gRPC exception in a unary POST request" in { + val request = HelloRequest("SRE") + val response = + BlazeClientBuilder[IO](ec).resource.use(unaryServiceClient.sayHello(request)(_)) + the[ResponseError] thrownBy response.unsafeRunSync() shouldBe ResponseError( + Status.BadRequest, + Some("INVALID_ARGUMENT: SRE")) + } + + "handle a raised non-gRPC exception in a unary POST request" in { + val request = HelloRequest("RTE") + val response = + BlazeClientBuilder[IO](ec).resource.use(unaryServiceClient.sayHello(request)(_)) + the[ResponseError] thrownBy response.unsafeRunSync() shouldBe ResponseError( + Status.InternalServerError, + Some("RTE")) + } + + "handle a thrown exception in a unary POST request" in { + val request = HelloRequest("TR") + val response = + BlazeClientBuilder[IO](ec).resource.use(unaryServiceClient.sayHello(request)(_)) + the[ResponseError] thrownBy response.unsafeRunSync() shouldBe ResponseError( + Status.InternalServerError) + } + + "serve a POST request with fs2 streaming request" in { + val requests = Stream(HelloRequest("hey"), HelloRequest("there")) + val response = + BlazeClientBuilder[IO](ec).resource.use(fs2ServiceClient.sayHellos(requests)(_)) + response.unsafeRunSync() shouldBe HelloResponse("hey, there") + } + + "serve a POST request with empty fs2 streaming request" in { + val requests = Stream.empty + val response = + BlazeClientBuilder[IO](ec).resource.use(fs2ServiceClient.sayHellos(requests)(_)) + response.unsafeRunSync() shouldBe HelloResponse("") + } + + "serve a POST request with Observable streaming request" in { + val requests = Observable(HelloRequest("hey"), HelloRequest("there")) + val response = + BlazeClientBuilder[IO](ec).resource.use(monixServiceClient.sayHellos(requests)(_)) + response.unsafeRunSync() shouldBe HelloResponse("hey, there") + } + + "serve a POST request with empty Observable streaming request" in { + val requests = Observable.empty + val response = + BlazeClientBuilder[IO](ec).resource.use(monixServiceClient.sayHellos(requests)(_)) + response.unsafeRunSync() shouldBe HelloResponse("") + } + + "serve a POST request with fs2 streaming response" in { + val request = HelloRequest("hey") + val responses = + BlazeClientBuilder[IO](ec).stream.flatMap(fs2ServiceClient.sayHelloAll(request)(_)) + responses.compile.toList + .unsafeRunSync() shouldBe List(HelloResponse("hey"), HelloResponse("hey")) + } + + "serve a POST request with Observable streaming response" in { + val request = HelloRequest("hey") + val responses = BlazeClientBuilder[IO](ec).stream + .flatMap(monixServiceClient.sayHelloAll(request)(_).toFs2Stream[IO]) + responses.compile.toList + .unsafeRunTimed(10.seconds) + .getOrElse(sys.error("Stuck!")) shouldBe List(HelloResponse("hey"), HelloResponse("hey")) + } + + "handle errors with fs2 streaming response" in { + val request = HelloRequest("") + val responses = + BlazeClientBuilder[IO](ec).stream.flatMap(fs2ServiceClient.sayHelloAll(request)(_)) + the[IllegalArgumentException] thrownBy responses.compile.toList + .unsafeRunSync() should have message "empty greeting" + } + + "handle errors with Observable streaming response" in { + val request = HelloRequest("") + val responses = BlazeClientBuilder[IO](ec).stream + .flatMap(monixServiceClient.sayHelloAll(request)(_).toFs2Stream[IO]) + the[IllegalArgumentException] thrownBy responses.compile.toList + .unsafeRunTimed(10.seconds) + .getOrElse(sys.error("Stuck!")) should have message "empty greeting" + } + + "serve a POST request with bidirectional fs2 streaming" in { + val requests = Stream(HelloRequest("hey"), HelloRequest("there")) + val responses = + BlazeClientBuilder[IO](ec).stream.flatMap(fs2ServiceClient.sayHellosAll(requests)(_)) + responses.compile.toList + .unsafeRunSync() shouldBe List(HelloResponse("hey"), HelloResponse("there")) + } + + "serve an empty POST request with bidirectional fs2 streaming" in { + val requests = Stream.empty + val responses = + BlazeClientBuilder[IO](ec).stream.flatMap(fs2ServiceClient.sayHellosAll(requests)(_)) + responses.compile.toList.unsafeRunSync() shouldBe Nil + } + + "serve a POST request with bidirectional Observable streaming" in { + val requests = Observable(HelloRequest("hey"), HelloRequest("there")) + val responses = BlazeClientBuilder[IO](ec).stream + .flatMap(monixServiceClient.sayHellosAll(requests)(_).toFs2Stream[IO]) + responses.compile.toList + .unsafeRunTimed(10.seconds) + .getOrElse(sys.error("Stuck!")) shouldBe List(HelloResponse("hey"), HelloResponse("there")) + } + + "serve an empty POST request with bidirectional Observable streaming" in { + val requests = Observable.empty + val responses = BlazeClientBuilder[IO](ec).stream + .flatMap(monixServiceClient.sayHellosAll(requests)(_).toFs2Stream[IO]) + responses.compile.toList + .unsafeRunTimed(10.seconds) + .getOrElse(sys.error("Stuck!")) shouldBe Nil + } + + "serve ScalaCheck-generated POST requests with bidirectional Observable streaming" in { + forAll { strings: List[String] => + val requests = Observable.fromIterable(strings.map(HelloRequest)) + val responses = BlazeClientBuilder[IO](ec).stream + .flatMap(monixServiceClient.sayHellosAll(requests)(_).toFs2Stream[IO]) + responses.compile.toList + .unsafeRunTimed(10.seconds) + .getOrElse(sys.error("Stuck!")) shouldBe strings.map(HelloResponse) + } + } + } +} diff --git a/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterServices.scala b/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterServices.scala index 95cc35c2b..7ac15e1f7 100644 --- a/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterServices.scala +++ b/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterServices.scala @@ -39,16 +39,16 @@ import fs2.Stream @http def sayHelloAll(request: HelloRequest): Stream[F, HelloResponse] - def sayHellosAll(requests: Stream[F, HelloRequest]): Stream[F, HelloResponse] + @http def sayHellosAll(requests: Stream[F, HelloRequest]): Stream[F, HelloResponse] } import monix.reactive.Observable @service(Avro) trait MonixGreeter[F[_]] { - def sayHellos(requests: Observable[HelloRequest]): F[HelloResponse] + @http def sayHellos(requests: Observable[HelloRequest]): F[HelloResponse] - def sayHelloAll(request: HelloRequest): Observable[HelloResponse] + @http def sayHelloAll(request: HelloRequest): Observable[HelloResponse] - def sayHellosAll(requests: Observable[HelloRequest]): Observable[HelloResponse] + @http def sayHellosAll(requests: Observable[HelloRequest]): Observable[HelloResponse] } diff --git a/modules/internal/src/main/scala/higherkindness/mu/rpc/internal/serviceImpl.scala b/modules/internal/src/main/scala/higherkindness/mu/rpc/internal/serviceImpl.scala index fb694d216..b1932397b 100644 --- a/modules/internal/src/main/scala/higherkindness/mu/rpc/internal/serviceImpl.scala +++ b/modules/internal/src/main/scala/higherkindness/mu/rpc/internal/serviceImpl.scala @@ -428,12 +428,6 @@ object serviceImpl { } } - //---------- - // HTTP/REST - //---------- - //TODO: derive server as well - //TODO: move HTTP-related code to its own module (on last attempt this did not work) - case class HttpOperation(operation: Operation) { import operation._ @@ -570,25 +564,6 @@ object serviceImpl { .map(_ => q"import _root_.monix.execution.Scheduler.Implicits.global") .toList - // - // - // - // - // - // - // - // - // - // - // - // - // - // - // - - println("&&&&&&&&&&&&&&&&&&") - println(serviceDef.name.toTermName) - val httpRoutesCases: Seq[Tree] = operations.map(_.toRouteTree) val routesPF: Tree = q"{ case ..$httpRoutesCases }" @@ -625,22 +600,6 @@ object serviceImpl { _root_.higherkindness.mu.http.RouteMap[F](${serviceDef.name.toString}, new $HttpRestService[$F].service) }""") - // - // - // - // - // - // - // - // - // - // - // - // - // - // - // - val http = if (httpRequests.isEmpty) Nil else @@ -694,11 +653,6 @@ object serviceImpl { ) ) - if (service.httpRequests.nonEmpty) { - println("#######################") - println(enrichedCompanion.toString) - } - List(serviceDef, enrichedCompanion) case _ => sys.error("@service-annotated definition must be a trait or abstract class") } From d6ce8821c58c9bfc53db474b2642cedb05da86a1 Mon Sep 17 00:00:00 2001 From: rafaparadela Date: Sun, 3 Mar 2019 17:09:12 -0800 Subject: [PATCH 25/35] removes unused imports --- .../mu/rpc/http/GreeterDerivedRestTests.scala | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterDerivedRestTests.scala b/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterDerivedRestTests.scala index 7009a0548..fd3956c56 100644 --- a/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterDerivedRestTests.scala +++ b/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterDerivedRestTests.scala @@ -21,17 +21,9 @@ import fs2.Stream import higherkindness.mu.http.{HttpServer, RouteMap} import higherkindness.mu.rpc.common.RpcBaseTestSuite import higherkindness.mu.rpc.http.Utils._ -import higherkindness.mu.rpc.protocol.Empty -import io.circe.Json -import io.circe.generic.auto._ -import io.circe.syntax._ import monix.reactive.Observable import org.http4s._ -import org.http4s.circe._ -import org.http4s.client.UnexpectedStatus import org.http4s.client.blaze.BlazeClientBuilder -import org.http4s.dsl.io._ -import org.http4s.server._ import org.http4s.server.blaze._ import org.scalatest._ import org.scalatest.prop.GeneratorDrivenPropertyChecks @@ -59,15 +51,7 @@ class GreeterDerivedRestTests val server: BlazeServerBuilder[IO] = HttpServer.bind(port, host, unaryRoute, fs2Route, monixRoute) -// val server1: BlazeServerBuilder[IO] = BlazeServerBuilder[IO] -// .bindHttp(port, host) -// .withHttpApp(Router("/Fs2Greeter" -> new Fs2GreeterRestService[IO].service).orNotFound) -// -// val server: BlazeServerBuilder[IO] = BlazeServerBuilder[IO] -// .bindHttp(port, host) -// .withHttpApp(Router(s"/${fs2Route.prefix}" -> fs2Route.route).orNotFound) - - var serverTask: Fiber[IO, Nothing] = _ // sorry + var serverTask: Fiber[IO, Nothing] = _ before(serverTask = server.resource.use(_ => IO.never).start.unsafeRunSync()) after(serverTask.cancel) From 3133ec57f2375e03ae0bfa0941ca9febcfb6edab Mon Sep 17 00:00:00 2001 From: rafaparadela Date: Mon, 4 Mar 2019 15:34:02 -0800 Subject: [PATCH 26/35] removed unused HttpMethod --- .../higherkindness/mu/http/protocol.scala | 26 ------------------- 1 file changed, 26 deletions(-) diff --git a/modules/http/src/main/scala/higherkindness/mu/http/protocol.scala b/modules/http/src/main/scala/higherkindness/mu/http/protocol.scala index 46682016a..7d5c21177 100644 --- a/modules/http/src/main/scala/higherkindness/mu/http/protocol.scala +++ b/modules/http/src/main/scala/higherkindness/mu/http/protocol.scala @@ -22,32 +22,6 @@ import org.http4s.server.Router import org.http4s.server.blaze.BlazeServerBuilder import org.http4s.dsl.io._ -sealed trait HttpMethod extends Product with Serializable -case object OPTIONS extends HttpMethod -case object GET extends HttpMethod -case object HEAD extends HttpMethod -case object POST extends HttpMethod -case object PUT extends HttpMethod -case object DELETE extends HttpMethod -case object TRACE extends HttpMethod -case object CONNECT extends HttpMethod -case object PATCH extends HttpMethod - -object HttpMethod { - def fromString(str: String): Option[HttpMethod] = str match { - case "OPTIONS" => Some(OPTIONS) - case "GET" => Some(GET) - case "HEAD" => Some(HEAD) - case "POST" => Some(POST) - case "PUT" => Some(PUT) - case "DELETE" => Some(DELETE) - case "TRACE" => Some(TRACE) - case "CONNECT" => Some(CONNECT) - case "PATCH" => Some(PATCH) - case _ => None - } -} - case class RouteMap[F[_]](prefix: String, route: HttpRoutes[F]) object HttpServer { From 34c05046715f0787bcf7d93554cf29ca0259b594 Mon Sep 17 00:00:00 2001 From: rafaparadela Date: Mon, 4 Mar 2019 16:40:07 -0800 Subject: [PATCH 27/35] upgraded http4s and moved Utils --- .../scala/higherkindness/mu}/http/Utils.scala | 19 +++++++++---------- .../higherkindness/mu/http/protocol.scala | 8 ++++---- .../mu/rpc/http/GreeterDerivedRestTests.scala | 12 +++++++----- .../mu/rpc/http/GreeterRestClients.scala | 5 ++--- .../mu/rpc/http/GreeterRestServices.scala | 2 +- .../mu/rpc/http/GreeterRestTests.scala | 8 +++++--- .../mu/rpc/internal/serviceImpl.scala | 2 +- project/ProjectPlugin.scala | 4 ++-- 8 files changed, 31 insertions(+), 29 deletions(-) rename modules/http/src/{test/scala/higherkindness/mu/rpc => main/scala/higherkindness/mu}/http/Utils.scala (85%) diff --git a/modules/http/src/test/scala/higherkindness/mu/rpc/http/Utils.scala b/modules/http/src/main/scala/higherkindness/mu/http/Utils.scala similarity index 85% rename from modules/http/src/test/scala/higherkindness/mu/rpc/http/Utils.scala rename to modules/http/src/main/scala/higherkindness/mu/http/Utils.scala index 3b0041ea6..f384d5e01 100644 --- a/modules/http/src/test/scala/higherkindness/mu/rpc/http/Utils.scala +++ b/modules/http/src/main/scala/higherkindness/mu/http/Utils.scala @@ -14,7 +14,7 @@ * limitations under the License. */ -package higherkindness.mu.rpc.http +package higherkindness.mu.http import cats.ApplicativeError import cats.effect._ @@ -22,7 +22,7 @@ import cats.implicits._ import fs2.interop.reactivestreams._ import fs2.{RaiseThrowable, Stream} import io.grpc.Status.Code._ -import jawn.ParseException +import org.typelevel.jawn.ParseException import io.circe._ import io.circe.generic.auto._ import io.circe.jawn.CirceSupportParser.facade @@ -38,7 +38,7 @@ import scala.util.control.NoStackTrace object Utils { - private[http] implicit class MessageOps[F[_]](val message: Message[F]) extends AnyVal { + implicit class MessageOps[F[_]](val message: Message[F]) extends AnyVal { def jsonBodyAsStream[A]( implicit decoder: Decoder[A], @@ -46,7 +46,7 @@ object Utils { message.body.chunks.parseJsonStream.map(_.as[A]).rethrow } - private[http] implicit class RequestOps[F[_]](val request: Request[F]) { + implicit class RequestOps[F[_]](val request: Request[F]) { def asStream[A](implicit decoder: Decoder[A], F: ApplicativeError[F, Throwable]): Stream[F, A] = request @@ -57,7 +57,7 @@ object Utils { } } - private[http] implicit class ResponseOps[F[_]](val response: Response[F]) { + implicit class ResponseOps[F[_]](val response: Response[F]) { implicit private val throwableDecoder: Decoder[Throwable] = Decoder.decodeTuple2[String, String].map { @@ -77,7 +77,7 @@ object Utils { else response.jsonBodyAsStream[Either[Throwable, A]].rethrow } - private[http] implicit class Fs2StreamOps[F[_], A](stream: Stream[F, A]) { + implicit class Fs2StreamOps[F[_], A](stream: Stream[F, A]) { implicit private val throwableEncoder: Encoder[Throwable] = new Encoder[Throwable] { def apply(ex: Throwable): Json = (ex.getClass.getName, ex.getMessage).asJson @@ -89,14 +89,13 @@ object Utils { Observable.fromReactivePublisher(stream.toUnicastPublisher) } - private[http] implicit class MonixStreamOps[A](val stream: Observable[A]) extends AnyVal { + implicit class MonixStreamOps[A](val stream: Observable[A]) extends AnyVal { def toFs2Stream[F[_]](implicit F: ConcurrentEffect[F], sc: Scheduler): Stream[F, A] = stream.toReactivePublisher.toStream[F]() } - private[http] implicit class FResponseOps[F[_]: Sync](response: F[Response[F]]) - extends Http4sDsl[F] { + implicit class FResponseOps[F[_]: Sync](response: F[Response[F]]) extends Http4sDsl[F] { def adaptErrors: F[Response[F]] = response.handleErrorWith { case se: StatusException => errorFromStatus(se.getStatus, se.getMessage) @@ -115,7 +114,7 @@ object Utils { } } - private[http] def handleResponseError[F[_]: Sync](errorResponse: Response[F]): F[Throwable] = + def handleResponseError[F[_]: Sync](errorResponse: Response[F]): F[Throwable] = errorResponse.bodyAsText.compile.foldMonoid.map(body => ResponseError(errorResponse.status, Some(body).filter(_.nonEmpty))) } diff --git a/modules/http/src/main/scala/higherkindness/mu/http/protocol.scala b/modules/http/src/main/scala/higherkindness/mu/http/protocol.scala index 7d5c21177..d7e8bda25 100644 --- a/modules/http/src/main/scala/higherkindness/mu/http/protocol.scala +++ b/modules/http/src/main/scala/higherkindness/mu/http/protocol.scala @@ -16,17 +16,17 @@ package higherkindness.mu.http -import cats.effect.ConcurrentEffect +import cats.effect.{ConcurrentEffect, Timer} import org.http4s.HttpRoutes -import org.http4s.server.Router import org.http4s.server.blaze.BlazeServerBuilder -import org.http4s.dsl.io._ +import org.http4s.implicits._ +import org.http4s.server.Router case class RouteMap[F[_]](prefix: String, route: HttpRoutes[F]) object HttpServer { - def bind[F[_]: ConcurrentEffect]( + def bind[F[_]: ConcurrentEffect: Timer]( port: Int, host: String, routes: RouteMap[F]*): BlazeServerBuilder[F] = diff --git a/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterDerivedRestTests.scala b/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterDerivedRestTests.scala index fd3956c56..06b8431f9 100644 --- a/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterDerivedRestTests.scala +++ b/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterDerivedRestTests.scala @@ -18,9 +18,9 @@ package higherkindness.mu.rpc.http import cats.effect.{IO, _} import fs2.Stream -import higherkindness.mu.http.{HttpServer, RouteMap} +import higherkindness.mu.http.{HttpServer, ResponseError, RouteMap} import higherkindness.mu.rpc.common.RpcBaseTestSuite -import higherkindness.mu.rpc.http.Utils._ +import higherkindness.mu.http.Utils._ import monix.reactive.Observable import org.http4s._ import org.http4s.client.blaze.BlazeClientBuilder @@ -41,9 +41,11 @@ class GreeterDerivedRestTests implicit val ec = monix.execution.Scheduler.Implicits.global implicit val cs: ContextShift[IO] = IO.contextShift(ec) - implicit val unaryHandlerIO = new UnaryGreeterHandler[IO] - implicit val fs2HandlerIO = new Fs2GreeterHandler[IO] - implicit val monixHandlerIO = new MonixGreeterHandler[IO] + implicit val timer: Timer[IO] = IO.timer(ec) + + implicit val unaryHandlerIO = new UnaryGreeterHandler[IO] + implicit val fs2HandlerIO = new Fs2GreeterHandler[IO] + implicit val monixHandlerIO = new MonixGreeterHandler[IO] val unaryRoute: RouteMap[IO] = UnaryGreeter.route[IO] val fs2Route: RouteMap[IO] = Fs2Greeter.route[IO] diff --git a/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterRestClients.scala b/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterRestClients.scala index 7fef03316..977abc042 100644 --- a/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterRestClients.scala +++ b/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterRestClients.scala @@ -20,11 +20,10 @@ import cats.effect._ import fs2.Stream import io.circe.generic.auto._ import io.circe.syntax._ -import higherkindness.mu.rpc.http.Utils._ +import higherkindness.mu.http.Utils._ import org.http4s._ import org.http4s.circe._ import org.http4s.client._ -import org.http4s.dsl.io._ class UnaryGreeterRestClient[F[_]: Sync](uri: Uri) { @@ -68,7 +67,7 @@ class MonixGreeterRestClient[F[_]: ConcurrentEffect](uri: Uri)( implicit sc: monix.execution.Scheduler) { import monix.reactive.Observable - import higherkindness.mu.rpc.http.Utils._ + import higherkindness.mu.http.Utils._ private implicit val responseDecoder: EntityDecoder[F, HelloResponse] = jsonOf[F, HelloResponse] diff --git a/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterRestServices.scala b/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterRestServices.scala index 4027ea2e0..c05cd08ae 100644 --- a/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterRestServices.scala +++ b/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterRestServices.scala @@ -21,7 +21,7 @@ import cats.syntax.flatMap._ import cats.syntax.functor._ import io.circe.generic.auto._ import io.circe.syntax._ -import higherkindness.mu.rpc.http.Utils._ +import higherkindness.mu.http.Utils._ import org.http4s._ import org.http4s.circe._ import org.http4s.dsl.Http4sDsl diff --git a/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterRestTests.scala b/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterRestTests.scala index 3bdb33dfa..5835612ec 100644 --- a/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterRestTests.scala +++ b/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterRestTests.scala @@ -18,8 +18,9 @@ package higherkindness.mu.rpc.http import cats.effect.{IO, _} import fs2.Stream +import higherkindness.mu.http.ResponseError import higherkindness.mu.rpc.common.RpcBaseTestSuite -import higherkindness.mu.rpc.http.Utils._ +import higherkindness.mu.http.Utils._ import io.circe.Json import io.circe.generic.auto._ import io.circe.syntax._ @@ -28,11 +29,11 @@ import org.http4s._ import org.http4s.circe._ import org.http4s.client.UnexpectedStatus import org.http4s.client.blaze.BlazeClientBuilder -import org.http4s.dsl.io._ -import org.http4s.server._ import org.http4s.server.blaze._ import org.scalatest._ import org.scalatest.prop.GeneratorDrivenPropertyChecks +import org.http4s.implicits._ +import org.http4s.server.Router import scala.concurrent.duration._ @@ -52,6 +53,7 @@ class GreeterRestTests implicit val ec = monix.execution.Scheduler.Implicits.global implicit val cs: ContextShift[IO] = IO.contextShift(ec) + implicit val timer: Timer[IO] = IO.timer(ec) implicit val unaryHandlerIO = new UnaryGreeterHandler[IO] implicit val fs2HandlerIO = new Fs2GreeterHandler[IO] diff --git a/modules/internal/src/main/scala/higherkindness/mu/rpc/internal/serviceImpl.scala b/modules/internal/src/main/scala/higherkindness/mu/rpc/internal/serviceImpl.scala index b1932397b..0c40397ad 100644 --- a/modules/internal/src/main/scala/higherkindness/mu/rpc/internal/serviceImpl.scala +++ b/modules/internal/src/main/scala/higherkindness/mu/rpc/internal/serviceImpl.scala @@ -549,7 +549,7 @@ object serviceImpl { }""" val httpImports: List[Tree] = List( - q"import _root_.higherkindness.mu.rpc.http.Utils._", + q"import _root_.higherkindness.mu.http.Utils._", q"import _root_.cats.syntax.flatMap._", q"import _root_.cats.syntax.functor._", q"import _root_.org.http4s._", diff --git a/project/ProjectPlugin.scala b/project/ProjectPlugin.scala index da9d114d5..b826ee5d4 100644 --- a/project/ProjectPlugin.scala +++ b/project/ProjectPlugin.scala @@ -34,7 +34,7 @@ object ProjectPlugin extends AutoPlugin { val fs2Grpc: String = "0.4.0-M3" val grpc: String = "1.18.0" val jodaTime: String = "2.10.1" - val http4s = "0.19.0" + val http4s = "0.20.0-M6" val kindProjector: String = "0.9.9" val log4s: String = "1.6.1" val logback: String = "1.2.3" @@ -132,9 +132,9 @@ object ProjectPlugin extends AutoPlugin { %%("http4s-circe", V.http4s), %%("circe-generic"), "co.fs2" %% "fs2-reactive-streams" % V.reactiveStreams, + %%("monix", V.monix), %%("http4s-blaze-client", V.http4s) % Test, %%("scalacheck") % Test, - %%("monix", V.monix) % Test, %%("scalamockScalatest") % Test, "ch.qos.logback" % "logback-classic" % "1.2.3" % Test ) From c922e0f1da911b7a61752907dc882e8522eae495 Mon Sep 17 00:00:00 2001 From: rafaparadela Date: Tue, 5 Mar 2019 23:04:39 -0800 Subject: [PATCH 28/35] solves all the comments in code review --- build.sbt | 1 - .../mu/http/{Utils.scala => implicits.scala} | 33 +++++++------------ .../mu/rpc/http/GreeterDerivedRestTests.scala | 16 ++++----- .../mu/rpc/http/GreeterRestClients.scala | 24 +++++++++----- .../mu/rpc/http/GreeterRestServices.scala | 18 +++++++--- .../mu/rpc/http/GreeterRestTests.scala | 18 +++++----- .../mu/rpc/internal/serviceImpl.scala | 13 ++++---- project/ProjectPlugin.scala | 2 +- 8 files changed, 65 insertions(+), 60 deletions(-) rename modules/http/src/main/scala/higherkindness/mu/http/{Utils.scala => implicits.scala} (75%) diff --git a/build.sbt b/build.sbt index 69e7dcfec..41ab6053f 100644 --- a/build.sbt +++ b/build.sbt @@ -189,7 +189,6 @@ lazy val `dropwizard-client` = project lazy val `http` = project .in(file("modules/http")) .dependsOn(common % "compile->compile;test->test") - .dependsOn(channel % "compile->compile;test->test") .dependsOn(server % "compile->compile;test->test") .settings(moduleName := "mu-rpc-http") .settings(httpSettings) diff --git a/modules/http/src/main/scala/higherkindness/mu/http/Utils.scala b/modules/http/src/main/scala/higherkindness/mu/http/implicits.scala similarity index 75% rename from modules/http/src/main/scala/higherkindness/mu/http/Utils.scala rename to modules/http/src/main/scala/higherkindness/mu/http/implicits.scala index f384d5e01..9cacfe66e 100644 --- a/modules/http/src/main/scala/higherkindness/mu/http/Utils.scala +++ b/modules/http/src/main/scala/higherkindness/mu/http/implicits.scala @@ -19,7 +19,6 @@ package higherkindness.mu.http import cats.ApplicativeError import cats.effect._ import cats.implicits._ -import fs2.interop.reactivestreams._ import fs2.{RaiseThrowable, Stream} import io.grpc.Status.Code._ import org.typelevel.jawn.ParseException @@ -29,16 +28,14 @@ import io.circe.jawn.CirceSupportParser.facade import io.circe.syntax._ import io.grpc.{Status => _, _} import jawnfs2._ -import monix.execution._ -import monix.reactive.Observable import org.http4s._ import org.http4s.dsl.Http4sDsl -import scala.concurrent.ExecutionContext +import org.http4s.Status.Ok import scala.util.control.NoStackTrace -object Utils { +object implicits { - implicit class MessageOps[F[_]](val message: Message[F]) extends AnyVal { + implicit class MessageOps[F[_]](private val message: Message[F]) extends AnyVal { def jsonBodyAsStream[A]( implicit decoder: Decoder[A], @@ -46,7 +43,7 @@ object Utils { message.body.chunks.parseJsonStream.map(_.as[A]).rethrow } - implicit class RequestOps[F[_]](val request: Request[F]) { + implicit class RequestOps[F[_]](private val request: Request[F]) { def asStream[A](implicit decoder: Decoder[A], F: ApplicativeError[F, Throwable]): Stream[F, A] = request @@ -57,7 +54,7 @@ object Utils { } } - implicit class ResponseOps[F[_]](val response: Response[F]) { + implicit class ResponseOps[F[_]](private val response: Response[F]) { implicit private val throwableDecoder: Decoder[Throwable] = Decoder.decodeTuple2[String, String].map { @@ -73,29 +70,21 @@ object Utils { implicit decoder: Decoder[A], F: ApplicativeError[F, Throwable], R: RaiseThrowable[F]): Stream[F, A] = - if (response.status.code != 200) Stream.raiseError(ResponseError(response.status)) + if (response.status.code != Ok.code) Stream.raiseError(ResponseError(response.status)) else response.jsonBodyAsStream[Either[Throwable, A]].rethrow } - implicit class Fs2StreamOps[F[_], A](stream: Stream[F, A]) { + implicit class Fs2StreamOps[F[_], A](private val stream: Stream[F, A]) { - implicit private val throwableEncoder: Encoder[Throwable] = new Encoder[Throwable] { + implicit val throwableEncoder: Encoder[Throwable] = new Encoder[Throwable] { def apply(ex: Throwable): Json = (ex.getClass.getName, ex.getMessage).asJson } def asJsonEither(implicit encoder: Encoder[A]): Stream[F, Json] = stream.attempt.map(_.asJson) - - def toObservable(implicit F: ConcurrentEffect[F], ec: ExecutionContext): Observable[A] = - Observable.fromReactivePublisher(stream.toUnicastPublisher) - } - - implicit class MonixStreamOps[A](val stream: Observable[A]) extends AnyVal { - - def toFs2Stream[F[_]](implicit F: ConcurrentEffect[F], sc: Scheduler): Stream[F, A] = - stream.toReactivePublisher.toStream[F]() } - implicit class FResponseOps[F[_]: Sync](response: F[Response[F]]) extends Http4sDsl[F] { + implicit class FResponseOps[F[_]: Sync](private val response: F[Response[F]]) + extends Http4sDsl[F] { def adaptErrors: F[Response[F]] = response.handleErrorWith { case se: StatusException => errorFromStatus(se.getStatus, se.getMessage) @@ -106,7 +95,7 @@ object Utils { private def errorFromStatus(status: io.grpc.Status, message: String): F[Response[F]] = status.getCode match { case INVALID_ARGUMENT => BadRequest(message) - case UNAUTHENTICATED => BadRequest(message) + case UNAUTHENTICATED => Forbidden(message) case PERMISSION_DENIED => Forbidden(message) case NOT_FOUND => NotFound(message) case UNAVAILABLE => ServiceUnavailable(message) diff --git a/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterDerivedRestTests.scala b/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterDerivedRestTests.scala index 06b8431f9..7eff00a4e 100644 --- a/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterDerivedRestTests.scala +++ b/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterDerivedRestTests.scala @@ -18,21 +18,21 @@ package higherkindness.mu.rpc.http import cats.effect.{IO, _} import fs2.Stream +import fs2.interop.reactivestreams._ import higherkindness.mu.http.{HttpServer, ResponseError, RouteMap} import higherkindness.mu.rpc.common.RpcBaseTestSuite -import higherkindness.mu.http.Utils._ import monix.reactive.Observable import org.http4s._ import org.http4s.client.blaze.BlazeClientBuilder import org.http4s.server.blaze._ import org.scalatest._ -import org.scalatest.prop.GeneratorDrivenPropertyChecks +import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks import scala.concurrent.duration._ class GreeterDerivedRestTests extends RpcBaseTestSuite - with GeneratorDrivenPropertyChecks + with ScalaCheckDrivenPropertyChecks with BeforeAndAfter { val host = "localhost" @@ -142,7 +142,7 @@ class GreeterDerivedRestTests "serve a POST request with Observable streaming response" in { val request = HelloRequest("hey") val responses = BlazeClientBuilder[IO](ec).stream - .flatMap(monixClient.sayHelloAll(request)(_).toFs2Stream[IO]) + .flatMap(monixClient.sayHelloAll(request)(_).toReactivePublisher.toStream[IO]) responses.compile.toList .unsafeRunTimed(10.seconds) .getOrElse(sys.error("Stuck!")) shouldBe List(HelloResponse("hey"), HelloResponse("hey")) @@ -159,7 +159,7 @@ class GreeterDerivedRestTests "handle errors with Observable streaming response" in { val request = HelloRequest("") val responses = BlazeClientBuilder[IO](ec).stream - .flatMap(monixClient.sayHelloAll(request)(_).toFs2Stream[IO]) + .flatMap(monixClient.sayHelloAll(request)(_).toReactivePublisher.toStream[IO]) the[IllegalArgumentException] thrownBy responses.compile.toList .unsafeRunTimed(10.seconds) .getOrElse(sys.error("Stuck!")) should have message "empty greeting" @@ -183,7 +183,7 @@ class GreeterDerivedRestTests "serve a POST request with bidirectional Observable streaming" in { val requests = Observable(HelloRequest("hey"), HelloRequest("there")) val responses = BlazeClientBuilder[IO](ec).stream - .flatMap(monixClient.sayHellosAll(requests)(_).toFs2Stream[IO]) + .flatMap(monixClient.sayHellosAll(requests)(_).toReactivePublisher.toStream[IO]) responses.compile.toList .unsafeRunTimed(10.seconds) .getOrElse(sys.error("Stuck!")) shouldBe List(HelloResponse("hey"), HelloResponse("there")) @@ -192,7 +192,7 @@ class GreeterDerivedRestTests "serve an empty POST request with bidirectional Observable streaming" in { val requests = Observable.empty val responses = BlazeClientBuilder[IO](ec).stream - .flatMap(monixClient.sayHellosAll(requests)(_).toFs2Stream[IO]) + .flatMap(monixClient.sayHellosAll(requests)(_).toReactivePublisher.toStream[IO]) responses.compile.toList .unsafeRunTimed(10.seconds) .getOrElse(sys.error("Stuck!")) shouldBe Nil @@ -202,7 +202,7 @@ class GreeterDerivedRestTests forAll { strings: List[String] => val requests = Observable.fromIterable(strings.map(HelloRequest)) val responses = BlazeClientBuilder[IO](ec).stream - .flatMap(monixClient.sayHellosAll(requests)(_).toFs2Stream[IO]) + .flatMap(monixClient.sayHellosAll(requests)(_).toReactivePublisher.toStream[IO]) responses.compile.toList .unsafeRunTimed(10.seconds) .getOrElse(sys.error("Stuck!")) shouldBe strings.map(HelloResponse) diff --git a/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterRestClients.scala b/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterRestClients.scala index 977abc042..e0949d988 100644 --- a/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterRestClients.scala +++ b/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterRestClients.scala @@ -18,9 +18,10 @@ package higherkindness.mu.rpc.http import cats.effect._ import fs2.Stream +import fs2.interop.reactivestreams._ import io.circe.generic.auto._ import io.circe.syntax._ -import higherkindness.mu.http.Utils._ +import higherkindness.mu.http.implicits._ import org.http4s._ import org.http4s.circe._ import org.http4s.client._ @@ -67,28 +68,33 @@ class MonixGreeterRestClient[F[_]: ConcurrentEffect](uri: Uri)( implicit sc: monix.execution.Scheduler) { import monix.reactive.Observable - import higherkindness.mu.http.Utils._ + import higherkindness.mu.http.implicits._ private implicit val responseDecoder: EntityDecoder[F, HelloResponse] = jsonOf[F, HelloResponse] def sayHellos(arg: Observable[HelloRequest])(implicit client: Client[F]): F[HelloResponse] = { val request = Request[F](Method.POST, uri / "sayHellos") - client.expectOr[HelloResponse](request.withEntity(arg.toFs2Stream.map(_.asJson)))( - handleResponseError) + client.expectOr[HelloResponse]( + request.withEntity(arg.toReactivePublisher.toStream.map(_.asJson)))(handleResponseError) } def sayHelloAll(arg: HelloRequest)(implicit client: Client[F]): Observable[HelloResponse] = { val request = Request[F](Method.POST, uri / "sayHelloAll") - client.stream(request.withEntity(arg.asJson)).flatMap(_.asStream[HelloResponse]).toObservable + Observable.fromReactivePublisher( + client + .stream(request.withEntity(arg.asJson)) + .flatMap(_.asStream[HelloResponse]) + .toUnicastPublisher) } def sayHellosAll(arg: Observable[HelloRequest])( implicit client: Client[F]): Observable[HelloResponse] = { val request = Request[F](Method.POST, uri / "sayHellosAll") - client - .stream(request.withEntity(arg.toFs2Stream.map(_.asJson))) - .flatMap(_.asStream[HelloResponse]) - .toObservable + Observable.fromReactivePublisher( + client + .stream(request.withEntity(arg.toReactivePublisher.toStream.map(_.asJson))) + .flatMap(_.asStream[HelloResponse]) + .toUnicastPublisher) } } diff --git a/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterRestServices.scala b/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterRestServices.scala index c05cd08ae..2fd042305 100644 --- a/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterRestServices.scala +++ b/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterRestServices.scala @@ -21,7 +21,9 @@ import cats.syntax.flatMap._ import cats.syntax.functor._ import io.circe.generic.auto._ import io.circe.syntax._ -import higherkindness.mu.http.Utils._ +import higherkindness.mu.http.implicits._ +import fs2.interop.reactivestreams._ +import monix.reactive.Observable import org.http4s._ import org.http4s.circe._ import org.http4s.dsl.Http4sDsl @@ -77,16 +79,24 @@ class MonixGreeterRestService[F[_]: ConcurrentEffect]( case msg @ POST -> Root / "sayHellos" => val requests = msg.asStream[HelloRequest] - Ok(handler.sayHellos(requests.toObservable).map(_.asJson)) + Ok( + handler + .sayHellos(Observable.fromReactivePublisher(requests.toUnicastPublisher)) + .map(_.asJson)) case msg @ POST -> Root / "sayHelloAll" => for { request <- msg.as[HelloRequest] - responses <- Ok(handler.sayHelloAll(request).toFs2Stream.asJsonEither) + responses <- Ok(handler.sayHelloAll(request).toReactivePublisher.toStream.asJsonEither) } yield responses case msg @ POST -> Root / "sayHellosAll" => val requests = msg.asStream[HelloRequest] - Ok(handler.sayHellosAll(requests.toObservable).toFs2Stream.asJsonEither) + Ok( + handler + .sayHellosAll(Observable.fromReactivePublisher(requests.toUnicastPublisher)) + .toReactivePublisher + .toStream + .asJsonEither) } } diff --git a/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterRestTests.scala b/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterRestTests.scala index 5835612ec..c4e509bd0 100644 --- a/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterRestTests.scala +++ b/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterRestTests.scala @@ -18,9 +18,10 @@ package higherkindness.mu.rpc.http import cats.effect.{IO, _} import fs2.Stream +import fs2.interop.reactivestreams._ import higherkindness.mu.http.ResponseError import higherkindness.mu.rpc.common.RpcBaseTestSuite -import higherkindness.mu.http.Utils._ +import higherkindness.mu.http.implicits._ import io.circe.Json import io.circe.generic.auto._ import io.circe.syntax._ @@ -31,15 +32,15 @@ import org.http4s.client.UnexpectedStatus import org.http4s.client.blaze.BlazeClientBuilder import org.http4s.server.blaze._ import org.scalatest._ -import org.scalatest.prop.GeneratorDrivenPropertyChecks import org.http4s.implicits._ import org.http4s.server.Router +import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks import scala.concurrent.duration._ class GreeterRestTests extends RpcBaseTestSuite - with GeneratorDrivenPropertyChecks + with ScalaCheckDrivenPropertyChecks with BeforeAndAfter { val Hostname = "localhost" @@ -59,7 +60,6 @@ class GreeterRestTests implicit val fs2HandlerIO = new Fs2GreeterHandler[IO] implicit val monixHandlerIO = new MonixGreeterHandler[IO] - //TODO: add Logger middleware val unaryService: HttpRoutes[IO] = new UnaryGreeterRestService[IO].service val fs2Service: HttpRoutes[IO] = new Fs2GreeterRestService[IO].service val monixService: HttpRoutes[IO] = new MonixGreeterRestService[IO].service @@ -198,7 +198,7 @@ class GreeterRestTests "serve a POST request with Observable streaming response" in { val request = HelloRequest("hey") val responses = BlazeClientBuilder[IO](ec).stream - .flatMap(monixServiceClient.sayHelloAll(request)(_).toFs2Stream[IO]) + .flatMap(monixServiceClient.sayHelloAll(request)(_).toReactivePublisher.toStream[IO]) responses.compile.toList .unsafeRunTimed(10.seconds) .getOrElse(sys.error("Stuck!")) shouldBe List(HelloResponse("hey"), HelloResponse("hey")) @@ -215,7 +215,7 @@ class GreeterRestTests "handle errors with Observable streaming response" in { val request = HelloRequest("") val responses = BlazeClientBuilder[IO](ec).stream - .flatMap(monixServiceClient.sayHelloAll(request)(_).toFs2Stream[IO]) + .flatMap(monixServiceClient.sayHelloAll(request)(_).toReactivePublisher.toStream[IO]) the[IllegalArgumentException] thrownBy responses.compile.toList .unsafeRunTimed(10.seconds) .getOrElse(sys.error("Stuck!")) should have message "empty greeting" @@ -239,7 +239,7 @@ class GreeterRestTests "serve a POST request with bidirectional Observable streaming" in { val requests = Observable(HelloRequest("hey"), HelloRequest("there")) val responses = BlazeClientBuilder[IO](ec).stream - .flatMap(monixServiceClient.sayHellosAll(requests)(_).toFs2Stream[IO]) + .flatMap(monixServiceClient.sayHellosAll(requests)(_).toReactivePublisher.toStream[IO]) responses.compile.toList .unsafeRunTimed(10.seconds) .getOrElse(sys.error("Stuck!")) shouldBe List(HelloResponse("hey"), HelloResponse("there")) @@ -248,7 +248,7 @@ class GreeterRestTests "serve an empty POST request with bidirectional Observable streaming" in { val requests = Observable.empty val responses = BlazeClientBuilder[IO](ec).stream - .flatMap(monixServiceClient.sayHellosAll(requests)(_).toFs2Stream[IO]) + .flatMap(monixServiceClient.sayHellosAll(requests)(_).toReactivePublisher.toStream[IO]) responses.compile.toList .unsafeRunTimed(10.seconds) .getOrElse(sys.error("Stuck!")) shouldBe Nil @@ -258,7 +258,7 @@ class GreeterRestTests forAll { strings: List[String] => val requests = Observable.fromIterable(strings.map(HelloRequest)) val responses = BlazeClientBuilder[IO](ec).stream - .flatMap(monixServiceClient.sayHellosAll(requests)(_).toFs2Stream[IO]) + .flatMap(monixServiceClient.sayHellosAll(requests)(_).toReactivePublisher.toStream[IO]) responses.compile.toList .unsafeRunTimed(10.seconds) .getOrElse(sys.error("Stuck!")) shouldBe strings.map(HelloResponse) diff --git a/modules/internal/src/main/scala/higherkindness/mu/rpc/internal/serviceImpl.scala b/modules/internal/src/main/scala/higherkindness/mu/rpc/internal/serviceImpl.scala index 220385993..6ae797281 100644 --- a/modules/internal/src/main/scala/higherkindness/mu/rpc/internal/serviceImpl.scala +++ b/modules/internal/src/main/scala/higherkindness/mu/rpc/internal/serviceImpl.scala @@ -441,7 +441,7 @@ object serviceImpl { val executionClient: Tree = response match { case MonixObservableTpe(_, _) => - q"client.stream(request).flatMap(_.asStream[${response.safeInner}]).toObservable" + q"Observable.fromReactivePublisher(client.stream(request).flatMap(_.asStream[${response.safeInner}]).toUnicastPublisher)" case Fs2StreamTpe(_, _) => q"client.stream(request).flatMap(_.asStream[${response.safeInner}])" case _ => @@ -454,7 +454,7 @@ object serviceImpl { case _: Fs2StreamTpe => q"val request = Request[F](Method.$method, uri / ${uri.replace("\"", "")}).withEntity(req.map(_.asJson))" case _: MonixObservableTpe => - q"val request = Request[F](Method.$method, uri / ${uri.replace("\"", "")}).withEntity(req.toFs2Stream.map(_.asJson))" + q"val request = Request[F](Method.$method, uri / ${uri.replace("\"", "")}).withEntity(req.toReactivePublisher.toStream.map(_.asJson))" case _ => q"val request = Request[F](Method.$method, uri / ${uri.replace("\"", "")})" } @@ -494,17 +494,17 @@ object serviceImpl { case (_: MonixObservableTpe, _: UnaryTpe) => q"""val requests = msg.asStream[${operation.request.safeInner}] - Ok(handler.${operation.name}(requests.toObservable).map(_.asJson))""" + Ok(handler.${operation.name}(Observable.fromReactivePublisher(requests.toUnicastPublisher)).map(_.asJson))""" case (_: UnaryTpe, _: MonixObservableTpe) => q"""for { request <- msg.as[${operation.request.safeInner}] - responses <- Ok(handler.${operation.name}(request).toFs2Stream.asJsonEither) + responses <- Ok(handler.${operation.name}(request).toReactivePublisher.toStream.asJsonEither) } yield responses""" case (_: MonixObservableTpe, _: MonixObservableTpe) => q"""val requests = msg.asStream[${operation.request.safeInner}] - Ok(handler.${operation.name}(requests.toObservable).toFs2Stream.asJsonEither)""" + Ok(handler.${operation.name}(Observable.fromReactivePublisher(requests.toUnicastPublisher)).toReactivePublisher.toStream.asJsonEither)""" case (_: EmptyTpe, _) => q"""Ok(handler.${operation.name}(_root_.higherkindness.mu.rpc.protocol.Empty).map(_.asJson))""" @@ -549,7 +549,8 @@ object serviceImpl { }""" val httpImports: List[Tree] = List( - q"import _root_.higherkindness.mu.http.Utils._", + q"import _root_.higherkindness.mu.http.implicits._", + q"import _root_.fs2.interop.reactivestreams._", q"import _root_.cats.syntax.flatMap._", q"import _root_.cats.syntax.functor._", q"import _root_.org.http4s._", diff --git a/project/ProjectPlugin.scala b/project/ProjectPlugin.scala index d301b0060..e97f664b8 100644 --- a/project/ProjectPlugin.scala +++ b/project/ProjectPlugin.scala @@ -139,7 +139,7 @@ object ProjectPlugin extends AutoPlugin { %%("http4s-blaze-client", V.http4s) % Test, %%("scalacheck") % Test, %%("scalamockScalatest") % Test, - "ch.qos.logback" % "logback-classic" % "1.2.3" % Test + "ch.qos.logback" % "logback-classic" % V.logback % Test ) ) From 9b0a8f3c29fe1b00b30c08db8cb695f5bbdaf252 Mon Sep 17 00:00:00 2001 From: rafaparadela Date: Wed, 6 Mar 2019 17:15:57 -0800 Subject: [PATCH 29/35] expressed type as FQN and propagated encoder/decoders constraints --- .../mu/rpc/http/GreeterDerivedRestTests.scala | 1 + .../mu/rpc/http/GreeterRestClients.scala | 55 ++++++----- .../mu/rpc/http/GreeterRestServices.scala | 17 +++- .../mu/rpc/internal/serviceImpl.scala | 99 +++++++++++-------- 4 files changed, 103 insertions(+), 69 deletions(-) diff --git a/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterDerivedRestTests.scala b/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterDerivedRestTests.scala index 7eff00a4e..d58ad9188 100644 --- a/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterDerivedRestTests.scala +++ b/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterDerivedRestTests.scala @@ -22,6 +22,7 @@ import fs2.interop.reactivestreams._ import higherkindness.mu.http.{HttpServer, ResponseError, RouteMap} import higherkindness.mu.rpc.common.RpcBaseTestSuite import monix.reactive.Observable +import io.circe.generic.auto._ import org.http4s._ import org.http4s.client.blaze.BlazeClientBuilder import org.http4s.server.blaze._ diff --git a/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterRestClients.scala b/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterRestClients.scala index e0949d988..ef895a71d 100644 --- a/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterRestClients.scala +++ b/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterRestClients.scala @@ -19,7 +19,6 @@ package higherkindness.mu.rpc.http import cats.effect._ import fs2.Stream import fs2.interop.reactivestreams._ -import io.circe.generic.auto._ import io.circe.syntax._ import higherkindness.mu.http.implicits._ import org.http4s._ @@ -28,36 +27,42 @@ import org.http4s.client._ class UnaryGreeterRestClient[F[_]: Sync](uri: Uri) { - private implicit val responseDecoder: EntityDecoder[F, HelloResponse] = jsonOf[F, HelloResponse] - - def getHello()(implicit client: Client[F]): F[HelloResponse] = { + def getHello()(client: Client[F])( + implicit decoderHelloResponse: io.circe.Decoder[HelloResponse]): F[HelloResponse] = { val request = Request[F](Method.GET, uri / "getHello") - client.expectOr[HelloResponse](request)(handleResponseError) + client.expectOr[HelloResponse](request)(handleResponseError)(jsonOf[F, HelloResponse]) } - def sayHello(arg: HelloRequest)(implicit client: Client[F]): F[HelloResponse] = { + def sayHello(arg: HelloRequest)(client: Client[F])( + implicit encoderHelloRequest: io.circe.Encoder[HelloRequest], + decoderHelloResponse: io.circe.Decoder[HelloResponse]): F[HelloResponse] = { val request = Request[F](Method.POST, uri / "sayHello") - client.expectOr[HelloResponse](request.withEntity(arg.asJson))(handleResponseError) + client.expectOr[HelloResponse](request.withEntity(arg.asJson))(handleResponseError)( + jsonOf[F, HelloResponse]) } } class Fs2GreeterRestClient[F[_]: Sync](uri: Uri) { - private implicit val responseDecoder: EntityDecoder[F, HelloResponse] = jsonOf[F, HelloResponse] - - def sayHellos(arg: Stream[F, HelloRequest])(implicit client: Client[F]): F[HelloResponse] = { + def sayHellos(arg: Stream[F, HelloRequest])(client: Client[F])( + implicit encoderHelloRequest: io.circe.Encoder[HelloRequest], + decoderHelloResponse: io.circe.Decoder[HelloResponse]): F[HelloResponse] = { val request = Request[F](Method.POST, uri / "sayHellos") - client.expectOr[HelloResponse](request.withEntity(arg.map(_.asJson)))(handleResponseError) + client.expectOr[HelloResponse](request.withEntity(arg.map(_.asJson)))(handleResponseError)( + jsonOf[F, HelloResponse]) } - def sayHelloAll(arg: HelloRequest)(implicit client: Client[F]): Stream[F, HelloResponse] = { + def sayHelloAll(arg: HelloRequest)(client: Client[F])( + implicit encoderHelloRequest: io.circe.Encoder[HelloRequest], + decoderHelloResponse: io.circe.Decoder[HelloResponse]): Stream[F, HelloResponse] = { val request = Request[F](Method.POST, uri / "sayHelloAll") client.stream(request.withEntity(arg.asJson)).flatMap(_.asStream[HelloResponse]) } - def sayHellosAll(arg: Stream[F, HelloRequest])( - implicit client: Client[F]): Stream[F, HelloResponse] = { + def sayHellosAll(arg: Stream[F, HelloRequest])(client: Client[F])( + implicit encoderHelloRequest: io.circe.Encoder[HelloRequest], + decoderHelloResponse: io.circe.Decoder[HelloResponse]): Stream[F, HelloResponse] = { val request = Request[F](Method.POST, uri / "sayHellosAll") client.stream(request.withEntity(arg.map(_.asJson))).flatMap(_.asStream[HelloResponse]) } @@ -65,20 +70,25 @@ class Fs2GreeterRestClient[F[_]: Sync](uri: Uri) { } class MonixGreeterRestClient[F[_]: ConcurrentEffect](uri: Uri)( - implicit sc: monix.execution.Scheduler) { + implicit sc: monix.execution.Scheduler, + encoderHelloRequest: io.circe.Encoder[HelloRequest], + decoderHelloResponse: io.circe.Decoder[HelloResponse]) { import monix.reactive.Observable import higherkindness.mu.http.implicits._ - private implicit val responseDecoder: EntityDecoder[F, HelloResponse] = jsonOf[F, HelloResponse] - - def sayHellos(arg: Observable[HelloRequest])(implicit client: Client[F]): F[HelloResponse] = { + def sayHellos(arg: Observable[HelloRequest])(client: Client[F])( + implicit encoderHelloRequest: io.circe.Encoder[HelloRequest], + decoderHelloResponse: io.circe.Decoder[HelloResponse]): F[HelloResponse] = { val request = Request[F](Method.POST, uri / "sayHellos") client.expectOr[HelloResponse]( - request.withEntity(arg.toReactivePublisher.toStream.map(_.asJson)))(handleResponseError) + request.withEntity(arg.toReactivePublisher.toStream.map(_.asJson)))(handleResponseError)( + jsonOf[F, HelloResponse]) } - def sayHelloAll(arg: HelloRequest)(implicit client: Client[F]): Observable[HelloResponse] = { + def sayHelloAll(arg: HelloRequest)(client: Client[F])( + implicit encoderHelloRequest: io.circe.Encoder[HelloRequest], + decoderHelloResponse: io.circe.Decoder[HelloResponse]): Observable[HelloResponse] = { val request = Request[F](Method.POST, uri / "sayHelloAll") Observable.fromReactivePublisher( client @@ -87,8 +97,9 @@ class MonixGreeterRestClient[F[_]: ConcurrentEffect](uri: Uri)( .toUnicastPublisher) } - def sayHellosAll(arg: Observable[HelloRequest])( - implicit client: Client[F]): Observable[HelloResponse] = { + def sayHellosAll(arg: Observable[HelloRequest])(client: Client[F])( + implicit encoderHelloRequest: io.circe.Encoder[HelloRequest], + decoderHelloResponse: io.circe.Decoder[HelloResponse]): Observable[HelloResponse] = { val request = Request[F](Method.POST, uri / "sayHellosAll") Observable.fromReactivePublisher( client diff --git a/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterRestServices.scala b/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterRestServices.scala index 2fd042305..b495ae0f0 100644 --- a/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterRestServices.scala +++ b/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterRestServices.scala @@ -19,7 +19,6 @@ package higherkindness.mu.rpc.http import cats.effect._ import cats.syntax.flatMap._ import cats.syntax.functor._ -import io.circe.generic.auto._ import io.circe.syntax._ import higherkindness.mu.http.implicits._ import fs2.interop.reactivestreams._ @@ -28,7 +27,11 @@ import org.http4s._ import org.http4s.circe._ import org.http4s.dsl.Http4sDsl -class UnaryGreeterRestService[F[_]: Sync](implicit handler: UnaryGreeter[F]) extends Http4sDsl[F] { +class UnaryGreeterRestService[F[_]: Sync]( + implicit handler: UnaryGreeter[F], + decoderHelloRequest: io.circe.Decoder[HelloRequest], + encoderHelloResponse: io.circe.Encoder[HelloResponse]) + extends Http4sDsl[F] { import higherkindness.mu.rpc.protocol.Empty @@ -46,7 +49,11 @@ class UnaryGreeterRestService[F[_]: Sync](implicit handler: UnaryGreeter[F]) ext } } -class Fs2GreeterRestService[F[_]: Sync](implicit handler: Fs2Greeter[F]) extends Http4sDsl[F] { +class Fs2GreeterRestService[F[_]: Sync]( + implicit handler: Fs2Greeter[F], + decoderHelloRequest: io.circe.Decoder[HelloRequest], + encoderHelloResponse: io.circe.Encoder[HelloResponse]) + extends Http4sDsl[F] { private implicit val requestDecoder: EntityDecoder[F, HelloRequest] = jsonOf[F, HelloRequest] @@ -70,7 +77,9 @@ class Fs2GreeterRestService[F[_]: Sync](implicit handler: Fs2Greeter[F]) extends class MonixGreeterRestService[F[_]: ConcurrentEffect]( implicit handler: MonixGreeter[F], - sc: monix.execution.Scheduler) + sc: monix.execution.Scheduler, + decoderHelloRequest: io.circe.Decoder[HelloRequest], + encoderHelloResponse: io.circe.Encoder[HelloResponse]) extends Http4sDsl[F] { private implicit val requestDecoder: EntityDecoder[F, HelloRequest] = jsonOf[F, HelloRequest] diff --git a/modules/internal/src/main/scala/higherkindness/mu/rpc/internal/serviceImpl.scala b/modules/internal/src/main/scala/higherkindness/mu/rpc/internal/serviceImpl.scala index 6ae797281..e67d5812e 100644 --- a/modules/internal/src/main/scala/higherkindness/mu/rpc/internal/serviceImpl.scala +++ b/modules/internal/src/main/scala/higherkindness/mu/rpc/internal/serviceImpl.scala @@ -441,36 +441,44 @@ object serviceImpl { val executionClient: Tree = response match { case MonixObservableTpe(_, _) => - q"Observable.fromReactivePublisher(client.stream(request).flatMap(_.asStream[${response.safeInner}]).toUnicastPublisher)" + q"_root_.monix.reactive.Observable.fromReactivePublisher(client.stream(request).flatMap(_.asStream[${response.safeInner}]).toUnicastPublisher)" case Fs2StreamTpe(_, _) => q"client.stream(request).flatMap(_.asStream[${response.safeInner}])" case _ => - q"client.expectOr[${response.safeInner}](request)(handleResponseError)" + q"""client.expectOr[${response.safeInner}](request)(handleResponseError)(jsonOf[F, ${response.safeInner}])""" } val requestTypology: Tree = request match { case _: UnaryTpe => - q"val request = Request[F](Method.$method, uri / ${uri.replace("\"", "")}).withEntity(req.asJson)" + q"val request = _root_.org.http4s.Request[F](_root_.org.http4s.Method.$method, uri / ${uri + .replace("\"", "")}).withEntity(req.asJson)" case _: Fs2StreamTpe => - q"val request = Request[F](Method.$method, uri / ${uri.replace("\"", "")}).withEntity(req.map(_.asJson))" + q"val request = _root_.org.http4s.Request[F](_root_.org.http4s.Method.$method, uri / ${uri + .replace("\"", "")}).withEntity(req.map(_.asJson))" case _: MonixObservableTpe => - q"val request = Request[F](Method.$method, uri / ${uri.replace("\"", "")}).withEntity(req.toReactivePublisher.toStream.map(_.asJson))" + q"val request = _root_.org.http4s.Request[F](_root_.org.http4s.Method.$method, uri / ${uri + .replace("\"", "")}).withEntity(req.toReactivePublisher.toStream.map(_.asJson))" case _ => - q"val request = Request[F](Method.$method, uri / ${uri.replace("\"", "")})" + q"val request = _root_.org.http4s.Request[F](_root_.org.http4s.Method.$method, uri / ${uri + .replace("\"", "")})" } val responseEncoder = - q"""implicit val responseDecoder: EntityDecoder[F, ${response.safeInner}] = jsonOf[F, ${response.safeInner}]""" + q"""implicit val responseEntityDecoder: _root_.org.http4s.EntityDecoder[F, ${response.safeInner}] = jsonOf[F, ${response.safeInner}]""" def toRequestTree: Tree = request match { case _: EmptyTpe => - q"""def $name(implicit client: _root_.org.http4s.client.Client[F]): ${response.getTpe} = { + q"""def $name(client: _root_.org.http4s.client.Client[F])( + implicit responseDecoder: io.circe.Decoder[${response.safeInner}]): ${response.getTpe} = { $responseEncoder $requestTypology $executionClient }""" case _ => - q"""def $name(req: ${request.getTpe})(implicit client: _root_.org.http4s.client.Client[F]): ${response.getTpe} = { + q"""def $name(req: ${request.getTpe})(client: _root_.org.http4s.client.Client[F])( + implicit requestEncoder: io.circe.Encoder[${request.safeInner}], + responseDecoder: io.circe.Decoder[${response.safeInner}] + ): ${response.getTpe} = { $responseEncoder $requestTypology $executionClient @@ -480,44 +488,46 @@ object serviceImpl { val routeTypology: Tree = (request, response) match { case (_: Fs2StreamTpe, _: UnaryTpe) => q"""val requests = msg.asStream[${operation.request.safeInner}] - Ok(handler.${operation.name}(requests).map(_.asJson))""" + _root_.org.http4s.Status.Ok.apply(handler.${operation.name}(requests).map(_.asJson))""" case (_: UnaryTpe, _: Fs2StreamTpe) => q"""for { request <- msg.as[${operation.request.safeInner}] - responses <- Ok(handler.${operation.name}(request).asJsonEither) + responses <- _root_.org.http4s.Status.Ok.apply(handler.${operation.name}(request).asJsonEither) } yield responses""" case (_: Fs2StreamTpe, _: Fs2StreamTpe) => q"""val requests = msg.asStream[${operation.request.safeInner}] - Ok(handler.${operation.name}(requests).asJsonEither)""" + _root_.org.http4s.Status.Ok.apply(handler.${operation.name}(requests).asJsonEither)""" case (_: MonixObservableTpe, _: UnaryTpe) => q"""val requests = msg.asStream[${operation.request.safeInner}] - Ok(handler.${operation.name}(Observable.fromReactivePublisher(requests.toUnicastPublisher)).map(_.asJson))""" + _root_.org.http4s.Status.Ok.apply(handler.${operation.name}(_root_.monix.reactive.Observable.fromReactivePublisher(requests.toUnicastPublisher)).map(_.asJson))""" case (_: UnaryTpe, _: MonixObservableTpe) => q"""for { request <- msg.as[${operation.request.safeInner}] - responses <- Ok(handler.${operation.name}(request).toReactivePublisher.toStream.asJsonEither) + responses <- _root_.org.http4s.Status.Ok.apply(handler.${operation.name}(request).toReactivePublisher.toStream.asJsonEither) } yield responses""" case (_: MonixObservableTpe, _: MonixObservableTpe) => q"""val requests = msg.asStream[${operation.request.safeInner}] - Ok(handler.${operation.name}(Observable.fromReactivePublisher(requests.toUnicastPublisher)).toReactivePublisher.toStream.asJsonEither)""" + _root_.org.http4s.Status.Ok.apply(handler.${operation.name}(_root_.monix.reactive.Observable.fromReactivePublisher(requests.toUnicastPublisher)).toReactivePublisher.toStream.asJsonEither)""" case (_: EmptyTpe, _) => - q"""Ok(handler.${operation.name}(_root_.higherkindness.mu.rpc.protocol.Empty).map(_.asJson))""" + q"""_root_.org.http4s.Status.Ok.apply(handler.${operation.name}(_root_.higherkindness.mu.rpc.protocol.Empty).map(_.asJson))""" case _ => q"""for { request <- msg.as[${operation.request.safeInner}] - response <- Ok(handler.${operation.name}(request).map(_.asJson)).adaptErrors + response <- _root_.org.http4s.Status.Ok.apply(handler.${operation.name}(request).map(_.asJson)).adaptErrors } yield response""" } - val getPattern = pq"GET -> Root / ${operation.name.toString}" - val postPattern = pq"msg @ POST -> Root / ${operation.name.toString}" + val getPattern = + pq"_root_.org.http4s.Method.GET -> Root / ${operation.name.toString}" + val postPattern = + pq"msg @ _root_.org.http4s.Method.POST -> Root / ${operation.name.toString}" def toRouteTree: Tree = request match { case _: EmptyTpe => cq"$getPattern => $routeTypology" @@ -538,12 +548,12 @@ object serviceImpl { val HttpClient = TypeName("HttpClient") val httpClientClass = q""" - class $HttpClient[$F_](uri: Uri)(implicit F: _root_.cats.effect.ConcurrentEffect[$F], ec: scala.concurrent.ExecutionContext) { + class $HttpClient[$F_](uri: _root_.org.http4s.Uri)(implicit F: _root_.cats.effect.ConcurrentEffect[$F], ec: scala.concurrent.ExecutionContext) { ..$httpRequests }""" val httpClient = q""" - def httpClient[$F_](uri: Uri) + def httpClient[$F_](uri: _root_.org.http4s.Uri) (implicit F: _root_.cats.effect.ConcurrentEffect[$F], ec: scala.concurrent.ExecutionContext): $HttpClient[$F] = { new $HttpClient[$F](uri / ${serviceDef.name.toString}) }""" @@ -553,10 +563,7 @@ object serviceImpl { q"import _root_.fs2.interop.reactivestreams._", q"import _root_.cats.syntax.flatMap._", q"import _root_.cats.syntax.functor._", - q"import _root_.org.http4s._", q"import _root_.org.http4s.circe._", - q"import _root_.io.circe._", - q"import _root_.io.circe.generic.auto._", q"import _root_.io.circe.syntax._" ) @@ -572,34 +579,40 @@ object serviceImpl { val requestTypes: Set[String] = operations.filterNot(_.operation.request.isEmpty).map(_.operation.request.flatName).toSet + val responseTypes: Set[String] = + operations.filterNot(_.operation.response.isEmpty).map(_.operation.response.flatName).toSet + val requestDecoders = requestTypes.map(n => - q"""implicit private val ${TermName("decoder" + n)}:EntityDecoder[F, ${TypeName(n)}] = - jsonOf[F, ${TypeName(n)}]""") + q"""implicit private val ${TermName("entityDecoder" + n)}:_root_.org.http4s.EntityDecoder[F, ${TypeName( + n)}] = jsonOf[F, ${TypeName(n)}]""") val HttpRestService: TypeName = TypeName(serviceDef.name.toString + "RestService") - val httpRestServiceClass: Tree = operations + val streamConstraints: List[Tree] = operations .find(_.operation.isMonixObservable) - .fold(q""" - class $HttpRestService[$F_](implicit handler: ${serviceDef.name}[F], F: _root_.cats.effect.Sync[$F]) extends _root_.org.http4s.dsl.Http4sDsl[F] { - ..$requestDecoders - def service: HttpRoutes[F] = HttpRoutes.of[F]{$routesPF} - }""")(_ => q""" - class $HttpRestService[$F_](implicit handler: ${serviceDef.name}[F], F: _root_.cats.effect.ConcurrentEffect[$F], sc: scala.concurrent.ExecutionContext) extends _root_.org.http4s.dsl.Http4sDsl[F] { + .fold(List(q"F: _root_.cats.effect.Sync[$F]"))( + _ => + List( + q"F: _root_.cats.effect.ConcurrentEffect[$F]", + q"sc: scala.concurrent.ExecutionContext" + )) + + val arguments: List[Tree] = List(q"handler: ${serviceDef.name}[F]") ++ + requestTypes.map(n => q"${TermName("decoder" + n)}: io.circe.Decoder[${TypeName(n)}]") ++ + responseTypes.map(n => q"${TermName("encoder" + n)}: io.circe.Encoder[${TypeName(n)}]") ++ + streamConstraints + + val httpRestServiceClass: Tree = q""" + class $HttpRestService[$F_](implicit ..$arguments) extends _root_.org.http4s.dsl.Http4sDsl[F] { ..$requestDecoders - def service: HttpRoutes[F] = HttpRoutes.of[F]{$routesPF} - }""") + def service = _root_.org.http4s.HttpRoutes.of[F]{$routesPF} + }""" - val httpService = operations - .find(_.operation.isMonixObservable) - .fold(q""" - def route[$F_](implicit handler: ${serviceDef.name}[F], F: _root_.cats.effect.Sync[$F]): _root_.higherkindness.mu.http.RouteMap[F] = { - _root_.higherkindness.mu.http.RouteMap[F](${serviceDef.name.toString}, new $HttpRestService[$F].service) - }""")(_ => q""" - def route[$F_](implicit handler: ${serviceDef.name}[F], F: _root_.cats.effect.ConcurrentEffect[$F], sc: scala.concurrent.ExecutionContext): _root_.higherkindness.mu.http.RouteMap[F] = { + val httpService = q""" + def route[$F_](implicit ..$arguments): _root_.higherkindness.mu.http.RouteMap[F] = { _root_.higherkindness.mu.http.RouteMap[F](${serviceDef.name.toString}, new $HttpRestService[$F].service) - }""") + }""" val http = if (httpRequests.isEmpty) Nil From fb91483df7f253ee23f22d6e6da68318b2e9e6d6 Mon Sep 17 00:00:00 2001 From: rafaparadela Date: Thu, 7 Mar 2019 12:58:42 -0800 Subject: [PATCH 30/35] removes the import of monix.Scheduler in the macro --- .../mu/rpc/internal/serviceImpl.scala | 30 +++++++++---------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/modules/internal/src/main/scala/higherkindness/mu/rpc/internal/serviceImpl.scala b/modules/internal/src/main/scala/higherkindness/mu/rpc/internal/serviceImpl.scala index e67d5812e..666cda299 100644 --- a/modules/internal/src/main/scala/higherkindness/mu/rpc/internal/serviceImpl.scala +++ b/modules/internal/src/main/scala/higherkindness/mu/rpc/internal/serviceImpl.scala @@ -544,17 +544,28 @@ object serviceImpl { p <- params.headOption.toList } yield HttpOperation(Operation(d.name, TypeTypology(p.tpt), TypeTypology(d.tpt))) + val streamConstraints: List[Tree] = operations + .find(_.operation.isMonixObservable) + .fold(List(q"F: _root_.cats.effect.Sync[$F]"))( + _ => + List( + q"F: _root_.cats.effect.ConcurrentEffect[$F]", + q"sc: _root_.monix.execution.Scheduler" + )) + val httpRequests = operations.map(_.toRequestTree) + val schedulerConstraint = q"ec: _root_.monix.execution.Scheduler" + val HttpClient = TypeName("HttpClient") val httpClientClass = q""" - class $HttpClient[$F_](uri: _root_.org.http4s.Uri)(implicit F: _root_.cats.effect.ConcurrentEffect[$F], ec: scala.concurrent.ExecutionContext) { + class $HttpClient[$F_](uri: _root_.org.http4s.Uri)(implicit ..$streamConstraints) { ..$httpRequests }""" val httpClient = q""" def httpClient[$F_](uri: _root_.org.http4s.Uri) - (implicit F: _root_.cats.effect.ConcurrentEffect[$F], ec: scala.concurrent.ExecutionContext): $HttpClient[$F] = { + (implicit ..$streamConstraints): $HttpClient[$F] = { new $HttpClient[$F](uri / ${serviceDef.name.toString}) }""" @@ -589,15 +600,6 @@ object serviceImpl { val HttpRestService: TypeName = TypeName(serviceDef.name.toString + "RestService") - val streamConstraints: List[Tree] = operations - .find(_.operation.isMonixObservable) - .fold(List(q"F: _root_.cats.effect.Sync[$F]"))( - _ => - List( - q"F: _root_.cats.effect.ConcurrentEffect[$F]", - q"sc: scala.concurrent.ExecutionContext" - )) - val arguments: List[Tree] = List(q"handler: ${serviceDef.name}[F]") ++ requestTypes.map(n => q"${TermName("decoder" + n)}: io.circe.Decoder[${TypeName(n)}]") ++ responseTypes.map(n => q"${TermName("encoder" + n)}: io.circe.Encoder[${TypeName(n)}]") ++ @@ -617,11 +619,7 @@ object serviceImpl { val http = if (httpRequests.isEmpty) Nil else - httpImports ++ scheduler ++ List( - httpClientClass, - httpClient, - httpRestServiceClass, - httpService) + httpImports ++ List(httpClientClass, httpClient, httpRestServiceClass, httpService) } val classAndMaybeCompanion = annottees.map(_.tree) From 854b0d619d428a0f9499bb7e5c64787d1d7927d0 Mon Sep 17 00:00:00 2001 From: rafaparadela Date: Thu, 7 Mar 2019 13:37:26 -0800 Subject: [PATCH 31/35] replaces executionContext by Schedule at some points --- .../mu/rpc/http/GreeterRestClients.scala | 6 ++-- .../mu/rpc/http/GreeterRestServices.scala | 3 +- .../mu/rpc/internal/serviceImpl.scala | 32 ++++++++++--------- 3 files changed, 22 insertions(+), 19 deletions(-) diff --git a/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterRestClients.scala b/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterRestClients.scala index ef895a71d..f22e8d2ab 100644 --- a/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterRestClients.scala +++ b/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterRestClients.scala @@ -70,13 +70,13 @@ class Fs2GreeterRestClient[F[_]: Sync](uri: Uri) { } class MonixGreeterRestClient[F[_]: ConcurrentEffect](uri: Uri)( - implicit sc: monix.execution.Scheduler, - encoderHelloRequest: io.circe.Encoder[HelloRequest], - decoderHelloResponse: io.circe.Decoder[HelloResponse]) { + implicit ec: scala.concurrent.ExecutionContext) { import monix.reactive.Observable import higherkindness.mu.http.implicits._ + implicit val sc: monix.execution.Scheduler = monix.execution.Scheduler(ec) + def sayHellos(arg: Observable[HelloRequest])(client: Client[F])( implicit encoderHelloRequest: io.circe.Encoder[HelloRequest], decoderHelloResponse: io.circe.Decoder[HelloResponse]): F[HelloResponse] = { diff --git a/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterRestServices.scala b/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterRestServices.scala index b495ae0f0..319d64b50 100644 --- a/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterRestServices.scala +++ b/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterRestServices.scala @@ -77,12 +77,13 @@ class Fs2GreeterRestService[F[_]: Sync]( class MonixGreeterRestService[F[_]: ConcurrentEffect]( implicit handler: MonixGreeter[F], - sc: monix.execution.Scheduler, + ec: scala.concurrent.ExecutionContext, decoderHelloRequest: io.circe.Decoder[HelloRequest], encoderHelloResponse: io.circe.Encoder[HelloResponse]) extends Http4sDsl[F] { private implicit val requestDecoder: EntityDecoder[F, HelloRequest] = jsonOf[F, HelloRequest] + implicit val scheduler: monix.execution.Scheduler = monix.execution.Scheduler(ec) def service: HttpRoutes[F] = HttpRoutes.of[F] { diff --git a/modules/internal/src/main/scala/higherkindness/mu/rpc/internal/serviceImpl.scala b/modules/internal/src/main/scala/higherkindness/mu/rpc/internal/serviceImpl.scala index 666cda299..3dd48484d 100644 --- a/modules/internal/src/main/scala/higherkindness/mu/rpc/internal/serviceImpl.scala +++ b/modules/internal/src/main/scala/higherkindness/mu/rpc/internal/serviceImpl.scala @@ -469,15 +469,15 @@ object serviceImpl { def toRequestTree: Tree = request match { case _: EmptyTpe => q"""def $name(client: _root_.org.http4s.client.Client[F])( - implicit responseDecoder: io.circe.Decoder[${response.safeInner}]): ${response.getTpe} = { + implicit responseDecoder: _root_.io.circe.Decoder[${response.safeInner}]): ${response.getTpe} = { $responseEncoder $requestTypology $executionClient }""" case _ => q"""def $name(req: ${request.getTpe})(client: _root_.org.http4s.client.Client[F])( - implicit requestEncoder: io.circe.Encoder[${request.safeInner}], - responseDecoder: io.circe.Decoder[${response.safeInner}] + implicit requestEncoder: _root_.io.circe.Encoder[${request.safeInner}], + responseDecoder: _root_.io.circe.Decoder[${response.safeInner}] ): ${response.getTpe} = { $responseEncoder $requestTypology @@ -525,9 +525,9 @@ object serviceImpl { } val getPattern = - pq"_root_.org.http4s.Method.GET -> Root / ${operation.name.toString}" + pq"_root_.org.http4s.Method.GET -> _root_.org.http4s.dsl.impl.Root / ${operation.name.toString}" val postPattern = - pq"msg @ _root_.org.http4s.Method.POST -> Root / ${operation.name.toString}" + pq"msg @ _root_.org.http4s.Method.POST -> _root_.org.http4s.dsl.impl.Root / ${operation.name.toString}" def toRouteTree: Tree = request match { case _: EmptyTpe => cq"$getPattern => $routeTypology" @@ -550,16 +550,20 @@ object serviceImpl { _ => List( q"F: _root_.cats.effect.ConcurrentEffect[$F]", - q"sc: _root_.monix.execution.Scheduler" + q"ec: scala.concurrent.ExecutionContext" )) - val httpRequests = operations.map(_.toRequestTree) + val executionContextStreaming: List[Tree] = operations + .find(_.operation.isMonixObservable) + .fold(List.empty[Tree])(_ => + List(q"implicit val sc: _root_.monix.execution.Scheduler = _root_.monix.execution.Scheduler(ec)")) - val schedulerConstraint = q"ec: _root_.monix.execution.Scheduler" + val httpRequests = operations.map(_.toRequestTree) val HttpClient = TypeName("HttpClient") val httpClientClass = q""" class $HttpClient[$F_](uri: _root_.org.http4s.Uri)(implicit ..$streamConstraints) { + ..$executionContextStreaming ..$httpRequests }""" @@ -578,11 +582,6 @@ object serviceImpl { q"import _root_.io.circe.syntax._" ) - val scheduler: List[Tree] = operations - .find(_.operation.isMonixObservable) - .map(_ => q"import _root_.monix.execution.Scheduler.Implicits.global") - .toList - val httpRoutesCases: Seq[Tree] = operations.map(_.toRouteTree) val routesPF: Tree = q"{ case ..$httpRoutesCases }" @@ -601,13 +600,16 @@ object serviceImpl { val HttpRestService: TypeName = TypeName(serviceDef.name.toString + "RestService") val arguments: List[Tree] = List(q"handler: ${serviceDef.name}[F]") ++ - requestTypes.map(n => q"${TermName("decoder" + n)}: io.circe.Decoder[${TypeName(n)}]") ++ - responseTypes.map(n => q"${TermName("encoder" + n)}: io.circe.Encoder[${TypeName(n)}]") ++ + requestTypes.map(n => + q"${TermName("decoder" + n)}: _root_.io.circe.Decoder[${TypeName(n)}]") ++ + responseTypes.map(n => + q"${TermName("encoder" + n)}: _root_.io.circe.Encoder[${TypeName(n)}]") ++ streamConstraints val httpRestServiceClass: Tree = q""" class $HttpRestService[$F_](implicit ..$arguments) extends _root_.org.http4s.dsl.Http4sDsl[F] { ..$requestDecoders + ..$executionContextStreaming def service = _root_.org.http4s.HttpRoutes.of[F]{$routesPF} }""" From f6eab68a490653bc36cedfc14f4c236a5964b15e Mon Sep 17 00:00:00 2001 From: rafaparadela Date: Thu, 7 Mar 2019 13:45:00 -0800 Subject: [PATCH 32/35] adds _root_ to ExecutionContext --- .../main/scala/higherkindness/mu/rpc/internal/serviceImpl.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/internal/src/main/scala/higherkindness/mu/rpc/internal/serviceImpl.scala b/modules/internal/src/main/scala/higherkindness/mu/rpc/internal/serviceImpl.scala index 3dd48484d..830c1c138 100644 --- a/modules/internal/src/main/scala/higherkindness/mu/rpc/internal/serviceImpl.scala +++ b/modules/internal/src/main/scala/higherkindness/mu/rpc/internal/serviceImpl.scala @@ -550,7 +550,7 @@ object serviceImpl { _ => List( q"F: _root_.cats.effect.ConcurrentEffect[$F]", - q"ec: scala.concurrent.ExecutionContext" + q"ec: _root_.scala.concurrent.ExecutionContext" )) val executionContextStreaming: List[Tree] = operations From ad90c2ddd43adb3a7aa21940f82242708fb8c182 Mon Sep 17 00:00:00 2001 From: Juan Pedro Moreno <4879373+juanpedromoreno@users.noreply.github.com> Date: Thu, 7 Mar 2019 23:39:56 +0100 Subject: [PATCH 33/35] Apply suggestions from code review --- project/ProjectPlugin.scala | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/project/ProjectPlugin.scala b/project/ProjectPlugin.scala index e97f664b8..ecf05d6fd 100644 --- a/project/ProjectPlugin.scala +++ b/project/ProjectPlugin.scala @@ -133,12 +133,11 @@ object ProjectPlugin extends AutoPlugin { %%("http4s-dsl", V.http4s), %%("http4s-blaze-server", V.http4s), %%("http4s-circe", V.http4s), - %%("circe-generic"), + %%("circe-generic") % Test, "co.fs2" %% "fs2-reactive-streams" % V.reactiveStreams, %%("monix", V.monix), %%("http4s-blaze-client", V.http4s) % Test, %%("scalacheck") % Test, - %%("scalamockScalatest") % Test, "ch.qos.logback" % "logback-classic" % V.logback % Test ) ) From f1f60ffc35d39a7d3d304d1f0cb426604da11855 Mon Sep 17 00:00:00 2001 From: rafaparadela Date: Thu, 7 Mar 2019 14:51:31 -0800 Subject: [PATCH 34/35] removes circe-generic --- .../higherkindness/mu/http/implicits.scala | 17 ++++++++++++++++- project/ProjectPlugin.scala | 3 +-- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/modules/http/src/main/scala/higherkindness/mu/http/implicits.scala b/modules/http/src/main/scala/higherkindness/mu/http/implicits.scala index 9cacfe66e..226651432 100644 --- a/modules/http/src/main/scala/higherkindness/mu/http/implicits.scala +++ b/modules/http/src/main/scala/higherkindness/mu/http/implicits.scala @@ -23,7 +23,6 @@ import fs2.{RaiseThrowable, Stream} import io.grpc.Status.Code._ import org.typelevel.jawn.ParseException import io.circe._ -import io.circe.generic.auto._ import io.circe.jawn.CirceSupportParser.facade import io.circe.syntax._ import io.grpc.{Status => _, _} @@ -56,6 +55,14 @@ object implicits { implicit class ResponseOps[F[_]](private val response: Response[F]) { + implicit def EitherDecoder[A, B]( + implicit a: Decoder[A], + b: Decoder[B]): Decoder[Either[A, B]] = { + val l: Decoder[Either[A, B]] = a.map(Left.apply) + val r: Decoder[Either[A, B]] = b.map(Right.apply) + l or r + } + implicit private val throwableDecoder: Decoder[Throwable] = Decoder.decodeTuple2[String, String].map { case (cls, msg) => @@ -76,6 +83,14 @@ object implicits { implicit class Fs2StreamOps[F[_], A](private val stream: Stream[F, A]) { + implicit def EitherEncoder[A, B](implicit ea: Encoder[A], eb: Encoder[B]): Encoder[Either[A, B]] = + new Encoder[Either[A, B]] { + final def apply(a: Either[A, B]): Json = a match { + case Left(a) => a.asJson + case Right(b) => b.asJson + } + } + implicit val throwableEncoder: Encoder[Throwable] = new Encoder[Throwable] { def apply(ex: Throwable): Json = (ex.getClass.getName, ex.getMessage).asJson } diff --git a/project/ProjectPlugin.scala b/project/ProjectPlugin.scala index e97f664b8..260ec0969 100644 --- a/project/ProjectPlugin.scala +++ b/project/ProjectPlugin.scala @@ -133,12 +133,11 @@ object ProjectPlugin extends AutoPlugin { %%("http4s-dsl", V.http4s), %%("http4s-blaze-server", V.http4s), %%("http4s-circe", V.http4s), - %%("circe-generic"), "co.fs2" %% "fs2-reactive-streams" % V.reactiveStreams, %%("monix", V.monix), %%("http4s-blaze-client", V.http4s) % Test, + %%("circe-generic") % Test, %%("scalacheck") % Test, - %%("scalamockScalatest") % Test, "ch.qos.logback" % "logback-classic" % V.logback % Test ) ) From bf6e85319ca06d9a3b42be4fb4275bd06b7374c9 Mon Sep 17 00:00:00 2001 From: rafaparadela Date: Fri, 8 Mar 2019 12:21:05 -0800 Subject: [PATCH 35/35] replaces Throwable by UnexpectedError and its encoder/decoder --- .../higherkindness/mu/http/implicits.scala | 69 ++++++++++--------- .../mu/rpc/http/GreeterDerivedRestTests.scala | 27 ++------ .../mu/rpc/http/GreeterRestTests.scala | 28 ++------ project/ProjectPlugin.scala | 1 - 4 files changed, 50 insertions(+), 75 deletions(-) diff --git a/modules/http/src/main/scala/higherkindness/mu/http/implicits.scala b/modules/http/src/main/scala/higherkindness/mu/http/implicits.scala index 226651432..6b7494a31 100644 --- a/modules/http/src/main/scala/higherkindness/mu/http/implicits.scala +++ b/modules/http/src/main/scala/higherkindness/mu/http/implicits.scala @@ -19,6 +19,7 @@ package higherkindness.mu.http import cats.ApplicativeError import cats.effect._ import cats.implicits._ +import cats.syntax.either._ import fs2.{RaiseThrowable, Stream} import io.grpc.Status.Code._ import org.typelevel.jawn.ParseException @@ -34,6 +35,29 @@ import scala.util.control.NoStackTrace object implicits { + implicit val unexpectedErrorEncoder: Encoder[UnexpectedError] = new Encoder[UnexpectedError] { + final def apply(a: UnexpectedError): Json = Json.obj( + ("className", Json.fromString(a.className)), + ("msg", a.msg.fold(Json.Null)(s => Json.fromString(s))) + ) + } + + implicit val unexpectedErrorDecoder: Decoder[UnexpectedError] = new Decoder[UnexpectedError] { + final def apply(c: HCursor): Decoder.Result[UnexpectedError] = + for { + className <- c.downField("className").as[String] + msg <- c.downField("msg").as[Option[String]] + } yield UnexpectedError(className, msg) + } + + implicit def EitherDecoder[A, B](implicit a: Decoder[A], b: Decoder[B]): Decoder[Either[A, B]] = + a.map(Left.apply) or b.map(Right.apply) + + implicit def EitherEncoder[A, B](implicit ea: Encoder[A], eb: Encoder[B]): Encoder[Either[A, B]] = + new Encoder[Either[A, B]] { + final def apply(a: Either[A, B]): Json = a.fold(_.asJson, _.asJson) + } + implicit class MessageOps[F[_]](private val message: Message[F]) extends AnyVal { def jsonBodyAsStream[A]( @@ -55,47 +79,18 @@ object implicits { implicit class ResponseOps[F[_]](private val response: Response[F]) { - implicit def EitherDecoder[A, B]( - implicit a: Decoder[A], - b: Decoder[B]): Decoder[Either[A, B]] = { - val l: Decoder[Either[A, B]] = a.map(Left.apply) - val r: Decoder[Either[A, B]] = b.map(Right.apply) - l or r - } - - implicit private val throwableDecoder: Decoder[Throwable] = - Decoder.decodeTuple2[String, String].map { - case (cls, msg) => - Class - .forName(cls) - .getConstructor(classOf[String]) - .newInstance(msg) - .asInstanceOf[Throwable] - } - def asStream[A]( implicit decoder: Decoder[A], F: ApplicativeError[F, Throwable], R: RaiseThrowable[F]): Stream[F, A] = if (response.status.code != Ok.code) Stream.raiseError(ResponseError(response.status)) - else response.jsonBodyAsStream[Either[Throwable, A]].rethrow + else response.jsonBodyAsStream[Either[UnexpectedError, A]].rethrow } implicit class Fs2StreamOps[F[_], A](private val stream: Stream[F, A]) { - implicit def EitherEncoder[A, B](implicit ea: Encoder[A], eb: Encoder[B]): Encoder[Either[A, B]] = - new Encoder[Either[A, B]] { - final def apply(a: Either[A, B]): Json = a match { - case Left(a) => a.asJson - case Right(b) => b.asJson - } - } - - implicit val throwableEncoder: Encoder[Throwable] = new Encoder[Throwable] { - def apply(ex: Throwable): Json = (ex.getClass.getName, ex.getMessage).asJson - } - - def asJsonEither(implicit encoder: Encoder[A]): Stream[F, Json] = stream.attempt.map(_.asJson) + def asJsonEither(implicit encoder: Encoder[A]): Stream[F, Json] = + stream.attempt.map(_.bimap(_.toUnexpected, identity).asJson) } implicit class FResponseOps[F[_]: Sync](private val response: F[Response[F]]) @@ -121,8 +116,18 @@ object implicits { def handleResponseError[F[_]: Sync](errorResponse: Response[F]): F[Throwable] = errorResponse.bodyAsText.compile.foldMonoid.map(body => ResponseError(errorResponse.status, Some(body).filter(_.nonEmpty))) + + implicit class ThrowableOps(self: Throwable) { + def toUnexpected: UnexpectedError = + UnexpectedError(self.getClass.getName, Option(self.getMessage)) + } + } +final case class UnexpectedError(className: String, msg: Option[String]) + extends RuntimeException(className + msg.fold("")(": " + _)) + with NoStackTrace + final case class ResponseError(status: Status, msg: Option[String] = None) extends RuntimeException(status + msg.fold("")(": " + _)) with NoStackTrace diff --git a/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterDerivedRestTests.scala b/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterDerivedRestTests.scala index d58ad9188..98d0d6dd0 100644 --- a/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterDerivedRestTests.scala +++ b/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterDerivedRestTests.scala @@ -19,7 +19,7 @@ package higherkindness.mu.rpc.http import cats.effect.{IO, _} import fs2.Stream import fs2.interop.reactivestreams._ -import higherkindness.mu.http.{HttpServer, ResponseError, RouteMap} +import higherkindness.mu.http.{HttpServer, ResponseError, RouteMap, UnexpectedError} import higherkindness.mu.rpc.common.RpcBaseTestSuite import monix.reactive.Observable import io.circe.generic.auto._ @@ -27,14 +27,10 @@ import org.http4s._ import org.http4s.client.blaze.BlazeClientBuilder import org.http4s.server.blaze._ import org.scalatest._ -import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks import scala.concurrent.duration._ -class GreeterDerivedRestTests - extends RpcBaseTestSuite - with ScalaCheckDrivenPropertyChecks - with BeforeAndAfter { +class GreeterDerivedRestTests extends RpcBaseTestSuite with BeforeAndAfter { val host = "localhost" val port = 8080 @@ -153,17 +149,17 @@ class GreeterDerivedRestTests val request = HelloRequest("") val responses = BlazeClientBuilder[IO](ec).stream.flatMap(fs2Client.sayHelloAll(request)(_)) - the[IllegalArgumentException] thrownBy responses.compile.toList - .unsafeRunSync() should have message "empty greeting" + the[UnexpectedError] thrownBy responses.compile.toList + .unsafeRunSync() should have message "java.lang.IllegalArgumentException: empty greeting" } "handle errors with Observable streaming response" in { val request = HelloRequest("") val responses = BlazeClientBuilder[IO](ec).stream .flatMap(monixClient.sayHelloAll(request)(_).toReactivePublisher.toStream[IO]) - the[IllegalArgumentException] thrownBy responses.compile.toList + the[UnexpectedError] thrownBy responses.compile.toList .unsafeRunTimed(10.seconds) - .getOrElse(sys.error("Stuck!")) should have message "empty greeting" + .getOrElse(sys.error("Stuck!")) should have message "java.lang.IllegalArgumentException: empty greeting" } "serve a POST request with bidirectional fs2 streaming" in { @@ -199,17 +195,6 @@ class GreeterDerivedRestTests .getOrElse(sys.error("Stuck!")) shouldBe Nil } - "serve ScalaCheck-generated POST requests with bidirectional Observable streaming" in { - forAll { strings: List[String] => - val requests = Observable.fromIterable(strings.map(HelloRequest)) - val responses = BlazeClientBuilder[IO](ec).stream - .flatMap(monixClient.sayHellosAll(requests)(_).toReactivePublisher.toStream[IO]) - responses.compile.toList - .unsafeRunTimed(10.seconds) - .getOrElse(sys.error("Stuck!")) shouldBe strings.map(HelloResponse) - } - } - } } diff --git a/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterRestTests.scala b/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterRestTests.scala index c4e509bd0..ae9dca4ed 100644 --- a/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterRestTests.scala +++ b/modules/http/src/test/scala/higherkindness/mu/rpc/http/GreeterRestTests.scala @@ -19,7 +19,7 @@ package higherkindness.mu.rpc.http import cats.effect.{IO, _} import fs2.Stream import fs2.interop.reactivestreams._ -import higherkindness.mu.http.ResponseError +import higherkindness.mu.http.{ResponseError, UnexpectedError} import higherkindness.mu.rpc.common.RpcBaseTestSuite import higherkindness.mu.http.implicits._ import io.circe.Json @@ -34,14 +34,10 @@ import org.http4s.server.blaze._ import org.scalatest._ import org.http4s.implicits._ import org.http4s.server.Router -import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks import scala.concurrent.duration._ -class GreeterRestTests - extends RpcBaseTestSuite - with ScalaCheckDrivenPropertyChecks - with BeforeAndAfter { +class GreeterRestTests extends RpcBaseTestSuite with BeforeAndAfter { val Hostname = "localhost" val Port = 8080 @@ -72,7 +68,7 @@ class GreeterRestTests s"/$Fs2ServicePrefix" -> fs2Service, s"/$MonixServicePrefix" -> monixService).orNotFound) - var serverTask: Fiber[IO, Nothing] = _ // sorry + var serverTask: Fiber[IO, Nothing] = _ before(serverTask = server.resource.use(_ => IO.never).start.unsafeRunSync()) after(serverTask.cancel) @@ -208,17 +204,17 @@ class GreeterRestTests val request = HelloRequest("") val responses = BlazeClientBuilder[IO](ec).stream.flatMap(fs2ServiceClient.sayHelloAll(request)(_)) - the[IllegalArgumentException] thrownBy responses.compile.toList - .unsafeRunSync() should have message "empty greeting" + the[UnexpectedError] thrownBy responses.compile.toList + .unsafeRunSync() should have message "java.lang.IllegalArgumentException: empty greeting" } "handle errors with Observable streaming response" in { val request = HelloRequest("") val responses = BlazeClientBuilder[IO](ec).stream .flatMap(monixServiceClient.sayHelloAll(request)(_).toReactivePublisher.toStream[IO]) - the[IllegalArgumentException] thrownBy responses.compile.toList + the[UnexpectedError] thrownBy responses.compile.toList .unsafeRunTimed(10.seconds) - .getOrElse(sys.error("Stuck!")) should have message "empty greeting" + .getOrElse(sys.error("Stuck!")) should have message "java.lang.IllegalArgumentException: empty greeting" } "serve a POST request with bidirectional fs2 streaming" in { @@ -254,15 +250,5 @@ class GreeterRestTests .getOrElse(sys.error("Stuck!")) shouldBe Nil } - "serve ScalaCheck-generated POST requests with bidirectional Observable streaming" in { - forAll { strings: List[String] => - val requests = Observable.fromIterable(strings.map(HelloRequest)) - val responses = BlazeClientBuilder[IO](ec).stream - .flatMap(monixServiceClient.sayHellosAll(requests)(_).toReactivePublisher.toStream[IO]) - responses.compile.toList - .unsafeRunTimed(10.seconds) - .getOrElse(sys.error("Stuck!")) shouldBe strings.map(HelloResponse) - } - } } } diff --git a/project/ProjectPlugin.scala b/project/ProjectPlugin.scala index 260ec0969..cc67afeae 100644 --- a/project/ProjectPlugin.scala +++ b/project/ProjectPlugin.scala @@ -137,7 +137,6 @@ object ProjectPlugin extends AutoPlugin { %%("monix", V.monix), %%("http4s-blaze-client", V.http4s) % Test, %%("circe-generic") % Test, - %%("scalacheck") % Test, "ch.qos.logback" % "logback-classic" % V.logback % Test ) )