From bb9f8a4d5afd139542bf3b77a1c116a60fbdcbaf Mon Sep 17 00:00:00 2001 From: Laurence Lavigne Date: Wed, 18 Apr 2018 17:41:10 -0700 Subject: [PATCH] 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 0d0302eca..8a579f043 100644 --- a/build.sbt +++ b/build.sbt @@ -144,6 +144,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 4b0475c3a..2a50cf06d 100644 --- a/project/ProjectPlugin.scala +++ b/project/ProjectPlugin.scala @@ -23,7 +23,7 @@ object ProjectPlugin extends AutoPlugin { val frees: String = "0.8.0" val fs2ReactiveStreams: String = "0.5.1" val grpc: String = "1.10.0" - val http4s = "0.18.3" + val http4s = "0.18.9" val nettySSL: String = "2.0.7.Final" val pbdirect: String = "0.1.0" val prometheus: String = "0.3.0" @@ -156,6 +156,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 ) )