-
Notifications
You must be signed in to change notification settings - Fork 34
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implemented sample client and server REST handlers to be generated
- Loading branch information
Laurence Lavigne
committed
Apr 19, 2018
1 parent
edca907
commit bb9f8a4
Showing
9 changed files
with
387 additions
and
79 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
65
modules/http/server/src/test/scala/GreeterRestClient.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
86
modules/http/server/src/test/scala/GreeterRestService.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
149
modules/http/server/src/test/scala/GreeterRestTests.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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")) | ||
} | ||
} | ||
|
||
} |
Oops, something went wrong.
bb9f8a4
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Per #182,
GreeterRestService
andGreeterRestClient
demonstrate thehttp4s
-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.Stream
s which arehttp4s
's internal implementation. We will then explore usingmonix.Observable
streaming.