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 35 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
24 changes: 18 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,16 @@ 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(server % "compile->compile;test->test")
.settings(moduleName := "mu-rpc-http")
.settings(httpSettings)

////////////////
//// IDLGEN ////
Expand Down Expand Up @@ -395,6 +406,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
113 changes: 113 additions & 0 deletions modules/http/src/main/scala/higherkindness/mu/http/implicits.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/*
* 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.{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 org.http4s._
import org.http4s.dsl.Http4sDsl
import org.http4s.Status.Ok
import scala.util.control.NoStackTrace

object implicits {

implicit class MessageOps[F[_]](private val message: Message[F]) extends AnyVal {

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[_]](private val request: Request[F]) {

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[_]](private val response: Response[F]) {

implicit private val throwableDecoder: Decoder[Throwable] =
Copy link
Contributor

Choose a reason for hiding this comment

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

Please, help me to understand this. This is only used for streams and observable, right? The server generates an Either that is parsed in the client. Am I right?

Copy link
Member

Choose a reason for hiding this comment

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

The implicits.scala was enterally created by @L-Lavigne, so I'll let him answer all your questions.

Decoder.decodeTuple2[String, String].map {
case (cls, msg) =>
Class
.forName(cls)
.getConstructor(classOf[String])
.newInstance(msg)
.asInstanceOf[Throwable]
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Since we're losing the stacktrace here, I'd use our custom case class:

final case class UnexpectedError(status: Status, msg: Option[String] = None)
    extends RuntimeException(status + msg.fold("")(": " + _))
    with NoStackTrace 
//...
implicit val unexpectedErrorDecoder: Decoder[UnexpectedError] = deriveDecoder

Copy link
Contributor

Choose a reason for hiding this comment

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

IMO we need to avoid custom logic in the encoders/decoders


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
}

implicit class Fs2StreamOps[F[_], A](private val stream: Stream[F, A]) {

implicit val throwableEncoder: Encoder[Throwable] = new Encoder[Throwable] {
def apply(ex: Throwable): Json = (ex.getClass.getName, ex.getMessage).asJson
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Based on the above:

implicit val unexpectedErrorEncoder: Encoder[UnexpectedError] = deriveEncoder


def asJsonEither(implicit encoder: Encoder[A]): Stream[F, Json] = stream.attempt.map(_.asJson)
Copy link
Contributor

Choose a reason for hiding this comment

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

def asJsonEither(implicit encoder: Encoder[A]): Stream[F, Json] = 
  stream.attempt.bimap(e => UnexpectedError(e.getClass.getName, Option(e.getMessage)), _.asJson)

Copy link
Member

@rafaparadela rafaparadela Mar 8, 2019

Choose a reason for hiding this comment

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

I'm sorry @fedefernandez, I don't really understand how you expect to instantiate the UnexpectedError without having the Status.

Maybe you're proposing

case class UnexpectedError(status: String, msg: Option[String] = None)

instead of

case class UnexpectedError(status: Status, msg: Option[String] = None)

}

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)
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 => Forbidden(message)
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