Skip to content

Commit

Permalink
Implemented sample client and server REST handlers to be generated
Browse files Browse the repository at this point in the history
  • Loading branch information
Laurence Lavigne committed Apr 19, 2018
1 parent edca907 commit bb9f8a4
Show file tree
Hide file tree
Showing 9 changed files with 387 additions and 79 deletions.
1 change: 1 addition & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
33 changes: 0 additions & 33 deletions modules/http/server/src/test/scala/ExampleService.scala

This file was deleted.

42 changes: 42 additions & 0 deletions modules/http/server/src/test/scala/GreeterHandler.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* Copyright 2017-2018 47 Degrees, LLC. <http://www.47deg.com>
*
* 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))
}
65 changes: 65 additions & 0 deletions modules/http/server/src/test/scala/GreeterRestClient.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
* Copyright 2017-2018 47 Degrees, LLC. <http://www.47deg.com>
*
* 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

}
86 changes: 86 additions & 0 deletions modules/http/server/src/test/scala/GreeterRestService.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/*
* Copyright 2017-2018 47 Degrees, LLC. <http://www.47deg.com>
*
* 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
}
}
149 changes: 149 additions & 0 deletions modules/http/server/src/test/scala/GreeterRestTests.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
/*
* Copyright 2017-2018 47 Degrees, LLC. <http://www.47deg.com>
*
* 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"))
}
}

}
Loading

1 comment on commit bb9f8a4

@L-Lavigne
Copy link
Contributor

@L-Lavigne L-Lavigne commented on bb9f8a4 Apr 19, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per #182, GreeterRestService and GreeterRestClient demonstrate the http4s-backed server and client code we could generate based on REST-annotated RPC definitions, once we implement our alternative to the current macro generation.

In this example we cover unary as well as client/server/bidi-streaming calls using fs2.Streams which are http4s's internal implementation. We will then explore using monix.Observable streaming.

Please sign in to comment.