Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sample http4s REST client/server with client macro derivation #552

Merged
merged 39 commits into from
Mar 8, 2019
Merged
Show file tree
Hide file tree
Changes from 30 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
ed9cf04
Initial steps for http integration (#203)
juanpedromoreno Mar 20, 2018
93f8ace
Implemented sample client and server REST handlers to be generated
Apr 19, 2018
b03637c
Added monix.Observable implementation
Apr 24, 2018
2a1cc42
Implemented error handling for unary and streaming REST services (#258)
May 4, 2018
a74604e
Tentative fix for the hanging Monix-Observable tests
May 9, 2018
6424dab
Merge branch 'master' into feature/182-http-support-from-protocols
Nov 21, 2018
8caee87
Undo Single Abstract Method syntax to restore 2.11 compatibility
Nov 21, 2018
65d705d
Merge master into branch
L-Lavigne Jan 24, 2019
98df299
Add auto-derived HTTP client implementation, move packages
L-Lavigne Jan 28, 2019
a216c63
Fix Monix/FS2 conversions using updated dependency
L-Lavigne Jan 28, 2019
051519d
fixes Scala 2.11 compilation error
Feb 18, 2019
8eb36b4
fixes unit tests to prove http client derivation
Feb 20, 2019
c172539
removes some println
Feb 20, 2019
20aae64
adds more tests
Feb 21, 2019
15abe94
removes the macro params that can be inferred
Feb 22, 2019
5d5b726
builds the client according to the typology of the request
Feb 22, 2019
683147c
fixes macro
Feb 26, 2019
54990d8
advances with client derivation
Feb 26, 2019
fe34e47
adds more unit test to prove the derived http client
Feb 26, 2019
1132fc2
restores the derivation of the rpc server, without the refactoring
Feb 26, 2019
97d1c73
re-applies part of the refactoring little by little
Feb 26, 2019
6db7ab3
applies the refactoring again with the fix
Feb 27, 2019
2a9cd3d
derived the simplest http route that only serves GET calls
Feb 28, 2019
06f2b83
derived the stream reaquests http server
Mar 2, 2019
3888be7
fixes the binding pattern in POST routes
Mar 3, 2019
965fdd4
adds tests to cover all the possible types of endpoints
Mar 4, 2019
d6ce882
removes unused imports
Mar 4, 2019
3133ec5
removed unused HttpMethod
Mar 4, 2019
34c0504
upgraded http4s and moved Utils
Mar 5, 2019
795eb12
Merge branch 'master' into feature/182-http-support-from-protocols
juanpedromoreno Mar 5, 2019
c922e0f
solves all the comments in code review
Mar 6, 2019
9b0a8f3
expressed type as FQN and propagated encoder/decoders constraints
Mar 7, 2019
fb91483
removes the import of monix.Scheduler in the macro
Mar 7, 2019
854b0d6
replaces executionContext by Schedule at some points
Mar 7, 2019
f6eab68
adds _root_ to ExecutionContext
Mar 7, 2019
ad90c2d
Apply suggestions from code review
juanpedromoreno Mar 7, 2019
f1f60ff
removes circe-generic
Mar 7, 2019
94dba66
Merge remote-tracking branch 'origin/feature/182-http-support-from-pr…
Mar 7, 2019
bf6e853
replaces Throwable by UnexpectedError and its encoder/decoder
Mar 8, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 19 additions & 6 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,13 @@ lazy val `prometheus` = project
//// DROPWIZARD ////
////////////////////

lazy val `dropwizard` = project
.in(file("modules/metrics/dropwizard"))
.dependsOn(`internal-core`)
.dependsOn(testing % "test->test")
.settings(moduleName := "mu-rpc-dropwizard")
.settings(dropwizardMetricsSettings)

lazy val `dropwizard-server` = project
.in(file("modules/dropwizard/server"))
.dependsOn(`prometheus-server` % "compile->compile;test->test")
Expand All @@ -175,12 +182,17 @@ lazy val `dropwizard-client` = project
.settings(moduleName := "mu-rpc-dropwizard-client")
.settings(dropwizardSettings)

lazy val `dropwizard` = project
.in(file("modules/metrics/dropwizard"))
.dependsOn(`internal-core`)
.dependsOn(testing % "test->test")
.settings(moduleName := "mu-rpc-dropwizard")
.settings(dropwizardMetricsSettings)
///////////////////
//// HTTP/REST ////
///////////////////

lazy val `http` = project
.in(file("modules/http"))
.dependsOn(common % "compile->compile;test->test")
.dependsOn(channel % "compile->compile;test->test")
rafaparadela marked this conversation as resolved.
Show resolved Hide resolved
.dependsOn(server % "compile->compile;test->test")
.settings(moduleName := "mu-rpc-http")
.settings(httpSettings)

////////////////
//// IDLGEN ////
Expand Down Expand Up @@ -395,6 +407,7 @@ lazy val allModules: Seq[ProjectReference] = Seq(
testing,
ssl,
`idlgen-core`,
`http`,
`marshallers-jodatime`,
`example-routeguide-protocol`,
`example-routeguide-common`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ 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

@message
object Empty
124 changes: 124 additions & 0 deletions modules/http/src/main/scala/higherkindness/mu/http/Utils.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/*
* Copyright 2017-2019 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 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
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._
import monix.reactive.Observable
import org.http4s._
import org.http4s.dsl.Http4sDsl
import scala.concurrent.ExecutionContext
import scala.util.control.NoStackTrace

object Utils {
rafaparadela marked this conversation as resolved.
Show resolved Hide resolved

implicit class MessageOps[F[_]](val message: Message[F]) extends AnyVal {
rafaparadela marked this conversation as resolved.
Show resolved Hide resolved

def jsonBodyAsStream[A](
implicit decoder: Decoder[A],
F: ApplicativeError[F, Throwable]): Stream[F, A] =
message.body.chunks.parseJsonStream.map(_.as[A]).rethrow
}

implicit class RequestOps[F[_]](val request: Request[F]) {
rafaparadela marked this conversation as resolved.
Show resolved Hide resolved

def asStream[A](implicit decoder: Decoder[A], F: ApplicativeError[F, Throwable]): 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
}
}

implicit class ResponseOps[F[_]](val response: Response[F]) {
rafaparadela marked this conversation as resolved.
Show resolved Hide resolved

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 != 200) Stream.raiseError(ResponseError(response.status))
rafaparadela marked this conversation as resolved.
Show resolved Hide resolved
else response.jsonBodyAsStream[Either[Throwable, A]].rethrow
}

implicit class Fs2StreamOps[F[_], A](stream: Stream[F, A]) {
rafaparadela marked this conversation as resolved.
Show resolved Hide resolved

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: ConcurrentEffect[F], ec: ExecutionContext): Observable[A] =
rafaparadela marked this conversation as resolved.
Show resolved Hide resolved
Observable.fromReactivePublisher(stream.toUnicastPublisher)
}

implicit class MonixStreamOps[A](val stream: Observable[A]) extends AnyVal {
rafaparadela marked this conversation as resolved.
Show resolved Hide resolved

def toFs2Stream[F[_]](implicit F: ConcurrentEffect[F], sc: Scheduler): Stream[F, A] =
rafaparadela marked this conversation as resolved.
Show resolved Hide resolved
stream.toReactivePublisher.toStream[F]()
}

implicit class FResponseOps[F[_]: Sync](response: F[Response[F]]) extends Http4sDsl[F] {
rafaparadela marked this conversation as resolved.
Show resolved Hide resolved

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)
rafaparadela marked this conversation as resolved.
Show resolved Hide resolved
case PERMISSION_DENIED => Forbidden(message)
case NOT_FOUND => NotFound(message)
case UNAVAILABLE => ServiceUnavailable(message)
case _ => InternalServerError(message)
}
}

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
37 changes: 37 additions & 0 deletions modules/http/src/main/scala/higherkindness/mu/http/protocol.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Copyright 2017-2019 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 higherkindness.mu.http

import cats.effect.{ConcurrentEffect, Timer}
import org.http4s.HttpRoutes
import org.http4s.server.blaze.BlazeServerBuilder
import org.http4s.implicits._
import org.http4s.server.Router

case class RouteMap[F[_]](prefix: String, route: HttpRoutes[F])

object HttpServer {

def bind[F[_]: ConcurrentEffect: Timer](
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)

}
11 changes: 11 additions & 0 deletions modules/http/src/test/resources/logback-test.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<configuration debug="true">
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>

<root level="debug">
<appender-ref ref="STDOUT" />
</root>
</configuration>
Loading