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 1 commit
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
1 change: 0 additions & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,6 @@ lazy val `dropwizard-client` = project
lazy val `http` = project
.in(file("modules/http"))
.dependsOn(common % "compile->compile;test->test")
.dependsOn(channel % "compile->compile;test->test")
.dependsOn(server % "compile->compile;test->test")
.settings(moduleName := "mu-rpc-http")
.settings(httpSettings)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ 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
Expand All @@ -29,24 +28,22 @@ 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 org.http4s.Status.Ok
import scala.util.control.NoStackTrace

object Utils {
object implicits {

implicit class MessageOps[F[_]](val message: Message[F]) extends AnyVal {
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[_]](val request: Request[F]) {
implicit class RequestOps[F[_]](private val request: Request[F]) {

def asStream[A](implicit decoder: Decoder[A], F: ApplicativeError[F, Throwable]): Stream[F, A] =
request
Expand All @@ -57,7 +54,7 @@ object Utils {
}
}

implicit class ResponseOps[F[_]](val response: Response[F]) {
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 {
Expand All @@ -73,29 +70,21 @@ object Utils {
implicit decoder: Decoder[A],
F: ApplicativeError[F, Throwable],
R: RaiseThrowable[F]): Stream[F, A] =
if (response.status.code != 200) Stream.raiseError(ResponseError(response.status))
if (response.status.code != Ok.code) Stream.raiseError(ResponseError(response.status))
else response.jsonBodyAsStream[Either[Throwable, A]].rethrow
}

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

implicit private val throwableEncoder: Encoder[Throwable] = new Encoder[Throwable] {
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)


def toObservable(implicit F: ConcurrentEffect[F], ec: ExecutionContext): Observable[A] =
Observable.fromReactivePublisher(stream.toUnicastPublisher)
}

implicit class MonixStreamOps[A](val stream: Observable[A]) extends AnyVal {

def toFs2Stream[F[_]](implicit F: ConcurrentEffect[F], sc: Scheduler): Stream[F, A] =
stream.toReactivePublisher.toStream[F]()
}

implicit class FResponseOps[F[_]: Sync](response: F[Response[F]]) extends Http4sDsl[F] {
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)
Expand All @@ -106,7 +95,7 @@ object Utils {
private def errorFromStatus(status: io.grpc.Status, message: String): F[Response[F]] =
status.getCode match {
case INVALID_ARGUMENT => BadRequest(message)
case UNAUTHENTICATED => BadRequest(message)
case UNAUTHENTICATED => Forbidden(message)
case PERMISSION_DENIED => Forbidden(message)
case NOT_FOUND => NotFound(message)
case UNAVAILABLE => ServiceUnavailable(message)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,21 +18,21 @@ package higherkindness.mu.rpc.http

import cats.effect.{IO, _}
import fs2.Stream
import fs2.interop.reactivestreams._
import higherkindness.mu.http.{HttpServer, ResponseError, RouteMap}
import higherkindness.mu.rpc.common.RpcBaseTestSuite
import higherkindness.mu.http.Utils._
import monix.reactive.Observable
import org.http4s._
import org.http4s.client.blaze.BlazeClientBuilder
import org.http4s.server.blaze._
import org.scalatest._
import org.scalatest.prop.GeneratorDrivenPropertyChecks
import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks

import scala.concurrent.duration._

class GreeterDerivedRestTests
extends RpcBaseTestSuite
with GeneratorDrivenPropertyChecks
with ScalaCheckDrivenPropertyChecks
with BeforeAndAfter {

val host = "localhost"
Expand Down Expand Up @@ -142,7 +142,7 @@ class GreeterDerivedRestTests
"serve a POST request with Observable streaming response" in {
val request = HelloRequest("hey")
val responses = BlazeClientBuilder[IO](ec).stream
.flatMap(monixClient.sayHelloAll(request)(_).toFs2Stream[IO])
.flatMap(monixClient.sayHelloAll(request)(_).toReactivePublisher.toStream[IO])
responses.compile.toList
.unsafeRunTimed(10.seconds)
.getOrElse(sys.error("Stuck!")) shouldBe List(HelloResponse("hey"), HelloResponse("hey"))
Expand All @@ -159,7 +159,7 @@ class GreeterDerivedRestTests
"handle errors with Observable streaming response" in {
val request = HelloRequest("")
val responses = BlazeClientBuilder[IO](ec).stream
.flatMap(monixClient.sayHelloAll(request)(_).toFs2Stream[IO])
.flatMap(monixClient.sayHelloAll(request)(_).toReactivePublisher.toStream[IO])
the[IllegalArgumentException] thrownBy responses.compile.toList
.unsafeRunTimed(10.seconds)
.getOrElse(sys.error("Stuck!")) should have message "empty greeting"
Expand All @@ -183,7 +183,7 @@ class GreeterDerivedRestTests
"serve a POST request with bidirectional Observable streaming" in {
val requests = Observable(HelloRequest("hey"), HelloRequest("there"))
val responses = BlazeClientBuilder[IO](ec).stream
.flatMap(monixClient.sayHellosAll(requests)(_).toFs2Stream[IO])
.flatMap(monixClient.sayHellosAll(requests)(_).toReactivePublisher.toStream[IO])
responses.compile.toList
.unsafeRunTimed(10.seconds)
.getOrElse(sys.error("Stuck!")) shouldBe List(HelloResponse("hey"), HelloResponse("there"))
Expand All @@ -192,7 +192,7 @@ class GreeterDerivedRestTests
"serve an empty POST request with bidirectional Observable streaming" in {
val requests = Observable.empty
val responses = BlazeClientBuilder[IO](ec).stream
.flatMap(monixClient.sayHellosAll(requests)(_).toFs2Stream[IO])
.flatMap(monixClient.sayHellosAll(requests)(_).toReactivePublisher.toStream[IO])
responses.compile.toList
.unsafeRunTimed(10.seconds)
.getOrElse(sys.error("Stuck!")) shouldBe Nil
Expand All @@ -202,7 +202,7 @@ class GreeterDerivedRestTests
forAll { strings: List[String] =>
val requests = Observable.fromIterable(strings.map(HelloRequest))
val responses = BlazeClientBuilder[IO](ec).stream
.flatMap(monixClient.sayHellosAll(requests)(_).toFs2Stream[IO])
.flatMap(monixClient.sayHellosAll(requests)(_).toReactivePublisher.toStream[IO])
responses.compile.toList
.unsafeRunTimed(10.seconds)
.getOrElse(sys.error("Stuck!")) shouldBe strings.map(HelloResponse)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,10 @@ package higherkindness.mu.rpc.http

import cats.effect._
import fs2.Stream
import fs2.interop.reactivestreams._
import io.circe.generic.auto._
import io.circe.syntax._
import higherkindness.mu.http.Utils._
import higherkindness.mu.http.implicits._
import org.http4s._
import org.http4s.circe._
import org.http4s.client._
Expand Down Expand Up @@ -67,28 +68,33 @@ class MonixGreeterRestClient[F[_]: ConcurrentEffect](uri: Uri)(
implicit sc: monix.execution.Scheduler) {

import monix.reactive.Observable
import higherkindness.mu.http.Utils._
import higherkindness.mu.http.implicits._

private implicit val responseDecoder: EntityDecoder[F, HelloResponse] = jsonOf[F, HelloResponse]

def sayHellos(arg: Observable[HelloRequest])(implicit client: Client[F]): F[HelloResponse] = {
val request = Request[F](Method.POST, uri / "sayHellos")
client.expectOr[HelloResponse](request.withEntity(arg.toFs2Stream.map(_.asJson)))(
handleResponseError)
client.expectOr[HelloResponse](
request.withEntity(arg.toReactivePublisher.toStream.map(_.asJson)))(handleResponseError)
}

def sayHelloAll(arg: HelloRequest)(implicit client: Client[F]): Observable[HelloResponse] = {
val request = Request[F](Method.POST, uri / "sayHelloAll")
client.stream(request.withEntity(arg.asJson)).flatMap(_.asStream[HelloResponse]).toObservable
Observable.fromReactivePublisher(
client
.stream(request.withEntity(arg.asJson))
.flatMap(_.asStream[HelloResponse])
.toUnicastPublisher)
}

def sayHellosAll(arg: Observable[HelloRequest])(
implicit client: Client[F]): Observable[HelloResponse] = {
val request = Request[F](Method.POST, uri / "sayHellosAll")
client
.stream(request.withEntity(arg.toFs2Stream.map(_.asJson)))
.flatMap(_.asStream[HelloResponse])
.toObservable
Observable.fromReactivePublisher(
client
.stream(request.withEntity(arg.toReactivePublisher.toStream.map(_.asJson)))
.flatMap(_.asStream[HelloResponse])
.toUnicastPublisher)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ import cats.syntax.flatMap._
import cats.syntax.functor._
import io.circe.generic.auto._
import io.circe.syntax._
import higherkindness.mu.http.Utils._
import higherkindness.mu.http.implicits._
import fs2.interop.reactivestreams._
import monix.reactive.Observable
import org.http4s._
import org.http4s.circe._
import org.http4s.dsl.Http4sDsl
Expand Down Expand Up @@ -77,16 +79,24 @@ class MonixGreeterRestService[F[_]: ConcurrentEffect](

case msg @ POST -> Root / "sayHellos" =>
val requests = msg.asStream[HelloRequest]
Ok(handler.sayHellos(requests.toObservable).map(_.asJson))
Ok(
handler
.sayHellos(Observable.fromReactivePublisher(requests.toUnicastPublisher))
.map(_.asJson))

case msg @ POST -> Root / "sayHelloAll" =>
for {
request <- msg.as[HelloRequest]
responses <- Ok(handler.sayHelloAll(request).toFs2Stream.asJsonEither)
responses <- Ok(handler.sayHelloAll(request).toReactivePublisher.toStream.asJsonEither)
} yield responses

case msg @ POST -> Root / "sayHellosAll" =>
val requests = msg.asStream[HelloRequest]
Ok(handler.sayHellosAll(requests.toObservable).toFs2Stream.asJsonEither)
Ok(
handler
.sayHellosAll(Observable.fromReactivePublisher(requests.toUnicastPublisher))
.toReactivePublisher
.toStream
.asJsonEither)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,10 @@ package higherkindness.mu.rpc.http

import cats.effect.{IO, _}
import fs2.Stream
import fs2.interop.reactivestreams._
import higherkindness.mu.http.ResponseError
import higherkindness.mu.rpc.common.RpcBaseTestSuite
import higherkindness.mu.http.Utils._
import higherkindness.mu.http.implicits._
import io.circe.Json
import io.circe.generic.auto._
import io.circe.syntax._
Expand All @@ -31,15 +32,15 @@ import org.http4s.client.UnexpectedStatus
import org.http4s.client.blaze.BlazeClientBuilder
import org.http4s.server.blaze._
import org.scalatest._
import org.scalatest.prop.GeneratorDrivenPropertyChecks
import org.http4s.implicits._
import org.http4s.server.Router
import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks

import scala.concurrent.duration._

class GreeterRestTests
extends RpcBaseTestSuite
with GeneratorDrivenPropertyChecks
with ScalaCheckDrivenPropertyChecks
with BeforeAndAfter {

val Hostname = "localhost"
Expand All @@ -59,7 +60,6 @@ class GreeterRestTests
implicit val fs2HandlerIO = new Fs2GreeterHandler[IO]
implicit val monixHandlerIO = new MonixGreeterHandler[IO]

//TODO: add Logger middleware
val unaryService: HttpRoutes[IO] = new UnaryGreeterRestService[IO].service
val fs2Service: HttpRoutes[IO] = new Fs2GreeterRestService[IO].service
val monixService: HttpRoutes[IO] = new MonixGreeterRestService[IO].service
Expand Down Expand Up @@ -198,7 +198,7 @@ class GreeterRestTests
"serve a POST request with Observable streaming response" in {
val request = HelloRequest("hey")
val responses = BlazeClientBuilder[IO](ec).stream
.flatMap(monixServiceClient.sayHelloAll(request)(_).toFs2Stream[IO])
.flatMap(monixServiceClient.sayHelloAll(request)(_).toReactivePublisher.toStream[IO])
responses.compile.toList
.unsafeRunTimed(10.seconds)
.getOrElse(sys.error("Stuck!")) shouldBe List(HelloResponse("hey"), HelloResponse("hey"))
Expand All @@ -215,7 +215,7 @@ class GreeterRestTests
"handle errors with Observable streaming response" in {
val request = HelloRequest("")
val responses = BlazeClientBuilder[IO](ec).stream
.flatMap(monixServiceClient.sayHelloAll(request)(_).toFs2Stream[IO])
.flatMap(monixServiceClient.sayHelloAll(request)(_).toReactivePublisher.toStream[IO])
the[IllegalArgumentException] thrownBy responses.compile.toList
.unsafeRunTimed(10.seconds)
.getOrElse(sys.error("Stuck!")) should have message "empty greeting"
Expand All @@ -239,7 +239,7 @@ class GreeterRestTests
"serve a POST request with bidirectional Observable streaming" in {
val requests = Observable(HelloRequest("hey"), HelloRequest("there"))
val responses = BlazeClientBuilder[IO](ec).stream
.flatMap(monixServiceClient.sayHellosAll(requests)(_).toFs2Stream[IO])
.flatMap(monixServiceClient.sayHellosAll(requests)(_).toReactivePublisher.toStream[IO])
responses.compile.toList
.unsafeRunTimed(10.seconds)
.getOrElse(sys.error("Stuck!")) shouldBe List(HelloResponse("hey"), HelloResponse("there"))
Expand All @@ -248,7 +248,7 @@ class GreeterRestTests
"serve an empty POST request with bidirectional Observable streaming" in {
val requests = Observable.empty
val responses = BlazeClientBuilder[IO](ec).stream
.flatMap(monixServiceClient.sayHellosAll(requests)(_).toFs2Stream[IO])
.flatMap(monixServiceClient.sayHellosAll(requests)(_).toReactivePublisher.toStream[IO])
responses.compile.toList
.unsafeRunTimed(10.seconds)
.getOrElse(sys.error("Stuck!")) shouldBe Nil
Expand All @@ -258,7 +258,7 @@ class GreeterRestTests
forAll { strings: List[String] =>
val requests = Observable.fromIterable(strings.map(HelloRequest))
val responses = BlazeClientBuilder[IO](ec).stream
.flatMap(monixServiceClient.sayHellosAll(requests)(_).toFs2Stream[IO])
.flatMap(monixServiceClient.sayHellosAll(requests)(_).toReactivePublisher.toStream[IO])
responses.compile.toList
.unsafeRunTimed(10.seconds)
.getOrElse(sys.error("Stuck!")) shouldBe strings.map(HelloResponse)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -441,7 +441,7 @@ object serviceImpl {

val executionClient: Tree = response match {
case MonixObservableTpe(_, _) =>
q"client.stream(request).flatMap(_.asStream[${response.safeInner}]).toObservable"
q"Observable.fromReactivePublisher(client.stream(request).flatMap(_.asStream[${response.safeInner}]).toUnicastPublisher)"
rafaparadela marked this conversation as resolved.
Show resolved Hide resolved
case Fs2StreamTpe(_, _) =>
q"client.stream(request).flatMap(_.asStream[${response.safeInner}])"
case _ =>
Expand All @@ -454,7 +454,7 @@ object serviceImpl {
case _: Fs2StreamTpe =>
q"val request = Request[F](Method.$method, uri / ${uri.replace("\"", "")}).withEntity(req.map(_.asJson))"
case _: MonixObservableTpe =>
q"val request = Request[F](Method.$method, uri / ${uri.replace("\"", "")}).withEntity(req.toFs2Stream.map(_.asJson))"
q"val request = Request[F](Method.$method, uri / ${uri.replace("\"", "")}).withEntity(req.toReactivePublisher.toStream.map(_.asJson))"
case _ =>
q"val request = Request[F](Method.$method, uri / ${uri.replace("\"", "")})"
}
Expand Down Expand Up @@ -494,17 +494,17 @@ object serviceImpl {

case (_: MonixObservableTpe, _: UnaryTpe) =>
q"""val requests = msg.asStream[${operation.request.safeInner}]
Ok(handler.${operation.name}(requests.toObservable).map(_.asJson))"""
Ok(handler.${operation.name}(Observable.fromReactivePublisher(requests.toUnicastPublisher)).map(_.asJson))"""
juanpedromoreno marked this conversation as resolved.
Show resolved Hide resolved

case (_: UnaryTpe, _: MonixObservableTpe) =>
q"""for {
request <- msg.as[${operation.request.safeInner}]
responses <- Ok(handler.${operation.name}(request).toFs2Stream.asJsonEither)
responses <- Ok(handler.${operation.name}(request).toReactivePublisher.toStream.asJsonEither)
} yield responses"""

case (_: MonixObservableTpe, _: MonixObservableTpe) =>
q"""val requests = msg.asStream[${operation.request.safeInner}]
Ok(handler.${operation.name}(requests.toObservable).toFs2Stream.asJsonEither)"""
Ok(handler.${operation.name}(Observable.fromReactivePublisher(requests.toUnicastPublisher)).toReactivePublisher.toStream.asJsonEither)"""

case (_: EmptyTpe, _) =>
q"""Ok(handler.${operation.name}(_root_.higherkindness.mu.rpc.protocol.Empty).map(_.asJson))"""
Expand Down Expand Up @@ -549,7 +549,8 @@ object serviceImpl {
}"""

val httpImports: List[Tree] = List(
q"import _root_.higherkindness.mu.http.Utils._",
q"import _root_.higherkindness.mu.http.implicits._",
q"import _root_.fs2.interop.reactivestreams._",
q"import _root_.cats.syntax.flatMap._",
q"import _root_.cats.syntax.functor._",
q"import _root_.org.http4s._",
Expand Down
Loading