-
Notifications
You must be signed in to change notification settings - Fork 34
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
Changes from all commits
Commits
Show all changes
39 commits
Select commit
Hold shift + click to select a range
ed9cf04
Initial steps for http integration (#203)
juanpedromoreno 93f8ace
Implemented sample client and server REST handlers to be generated
b03637c
Added monix.Observable implementation
2a1cc42
Implemented error handling for unary and streaming REST services (#258)
a74604e
Tentative fix for the hanging Monix-Observable tests
6424dab
Merge branch 'master' into feature/182-http-support-from-protocols
8caee87
Undo Single Abstract Method syntax to restore 2.11 compatibility
65d705d
Merge master into branch
L-Lavigne 98df299
Add auto-derived HTTP client implementation, move packages
L-Lavigne a216c63
Fix Monix/FS2 conversions using updated dependency
L-Lavigne 051519d
fixes Scala 2.11 compilation error
8eb36b4
fixes unit tests to prove http client derivation
c172539
removes some println
20aae64
adds more tests
15abe94
removes the macro params that can be inferred
5d5b726
builds the client according to the typology of the request
683147c
fixes macro
54990d8
advances with client derivation
fe34e47
adds more unit test to prove the derived http client
1132fc2
restores the derivation of the rpc server, without the refactoring
97d1c73
re-applies part of the refactoring little by little
6db7ab3
applies the refactoring again with the fix
2a9cd3d
derived the simplest http route that only serves GET calls
06f2b83
derived the stream reaquests http server
3888be7
fixes the binding pattern in POST routes
965fdd4
adds tests to cover all the possible types of endpoints
d6ce882
removes unused imports
3133ec5
removed unused HttpMethod
34c0504
upgraded http4s and moved Utils
795eb12
Merge branch 'master' into feature/182-http-support-from-protocols
juanpedromoreno c922e0f
solves all the comments in code review
9b0a8f3
expressed type as FQN and propagated encoder/decoders constraints
fb91483
removes the import of monix.Scheduler in the macro
854b0d6
replaces executionContext by Schedule at some points
f6eab68
adds _root_ to ExecutionContext
ad90c2d
Apply suggestions from code review
juanpedromoreno f1f60ff
removes circe-generic
94dba66
Merge remote-tracking branch 'origin/feature/182-http-support-from-pr…
bf6e853
replaces Throwable by UnexpectedError and its encoder/decoder
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 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
133 changes: 133 additions & 0 deletions
133
modules/http/src/main/scala/higherkindness/mu/http/implicits.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,133 @@ | ||
/* | ||
* 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 cats.syntax.either._ | ||
import fs2.{RaiseThrowable, Stream} | ||
import io.grpc.Status.Code._ | ||
import org.typelevel.jawn.ParseException | ||
import io.circe._ | ||
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 val unexpectedErrorEncoder: Encoder[UnexpectedError] = new Encoder[UnexpectedError] { | ||
final def apply(a: UnexpectedError): Json = Json.obj( | ||
("className", Json.fromString(a.className)), | ||
("msg", a.msg.fold(Json.Null)(s => Json.fromString(s))) | ||
) | ||
} | ||
|
||
implicit val unexpectedErrorDecoder: Decoder[UnexpectedError] = new Decoder[UnexpectedError] { | ||
final def apply(c: HCursor): Decoder.Result[UnexpectedError] = | ||
for { | ||
className <- c.downField("className").as[String] | ||
msg <- c.downField("msg").as[Option[String]] | ||
} yield UnexpectedError(className, msg) | ||
} | ||
|
||
implicit def EitherDecoder[A, B](implicit a: Decoder[A], b: Decoder[B]): Decoder[Either[A, B]] = | ||
a.map(Left.apply) or b.map(Right.apply) | ||
|
||
implicit def EitherEncoder[A, B](implicit ea: Encoder[A], eb: Encoder[B]): Encoder[Either[A, B]] = | ||
new Encoder[Either[A, B]] { | ||
final def apply(a: Either[A, B]): Json = a.fold(_.asJson, _.asJson) | ||
} | ||
|
||
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]) { | ||
|
||
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[UnexpectedError, A]].rethrow | ||
} | ||
|
||
implicit class Fs2StreamOps[F[_], A](private val stream: Stream[F, A]) { | ||
|
||
def asJsonEither(implicit encoder: Encoder[A]): Stream[F, Json] = | ||
stream.attempt.map(_.bimap(_.toUnexpected, identity).asJson) | ||
} | ||
|
||
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))) | ||
|
||
implicit class ThrowableOps(self: Throwable) { | ||
def toUnexpected: UnexpectedError = | ||
UnexpectedError(self.getClass.getName, Option(self.getMessage)) | ||
} | ||
|
||
} | ||
|
||
final case class UnexpectedError(className: String, msg: Option[String]) | ||
extends RuntimeException(className + msg.fold("")(": " + _)) | ||
with NoStackTrace | ||
|
||
final case class ResponseError(status: Status, msg: Option[String] = None) | ||
extends RuntimeException(status + msg.fold("")(": " + _)) | ||
with NoStackTrace |
37 changes: 37 additions & 0 deletions
37
modules/http/src/main/scala/higherkindness/mu/http/protocol.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,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) | ||
|
||
} |
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,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> |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
The name is a bit generic - most errors are unexpected. The way we're using those, they're closer to "RequestException"s, or 4XX-status
ResponseException
s. Can we combine those error types or rename this one?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.
(resolved offline)