From eeac46ddaa09b8635b410b4f640b7678357f8c0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pepe=20Garc=C3=ADa?= Date: Fri, 20 Jul 2018 14:52:54 +0200 Subject: [PATCH 1/5] WIP: derive http client from service --- modules/common/src/main/scala/protocol.scala | 34 ++++++++++-- modules/internal/src/main/scala/service.scala | 52 ++++++++++++++++++- project/ProjectPlugin.scala | 7 +++ 3 files changed, 88 insertions(+), 5 deletions(-) diff --git a/modules/common/src/main/scala/protocol.scala b/modules/common/src/main/scala/protocol.scala index 69b6a45ac..a2144f763 100644 --- a/modules/common/src/main/scala/protocol.scala +++ b/modules/common/src/main/scala/protocol.scala @@ -33,10 +33,36 @@ sealed abstract class CompressionType extends Product with Serializable case object Identity extends CompressionType case object Gzip extends CompressionType -class message extends StaticAnnotation -class option(name: String, value: Any) extends StaticAnnotation -class outputPackage(value: String) extends StaticAnnotation -class outputName(value: String) extends StaticAnnotation +sealed trait HttpMethod +case object OPTIONS extends HttpMethod +case object GET extends HttpMethod +case object HEAD extends HttpMethod +case object POST extends HttpMethod +case object PUT extends HttpMethod +case object DELETE extends HttpMethod +case object TRACE extends HttpMethod +case object CONNECT extends HttpMethod +case object PATCH extends HttpMethod +object HttpMethod { + def fromString(str: String): Option[HttpMethod] = str match { + case "OPTIONS" => Some(OPTIONS) + case "GET" => Some(GET) + case "HEAD" => Some(HEAD) + case "POST" => Some(POST) + case "PUT" => Some(PUT) + case "DELETE" => Some(DELETE) + case "TRACE" => Some(TRACE) + case "CONNECT" => Some(CONNECT) + case "PATCH" => Some(PATCH) + case _ => None + } +} + +class message extends StaticAnnotation +class http(method: HttpMethod, uri: String) extends StaticAnnotation +class option(name: String, value: Any) extends StaticAnnotation +class outputPackage(value: String) extends StaticAnnotation +class outputName(value: String) extends StaticAnnotation @message object Empty diff --git a/modules/internal/src/main/scala/service.scala b/modules/internal/src/main/scala/service.scala index 2643b2990..32b165b9b 100644 --- a/modules/internal/src/main/scala/service.scala +++ b/modules/internal/src/main/scala/service.scala @@ -245,6 +245,55 @@ object serviceImpl { case _ => false } + val toHttpRequest: ((TermName, String, RpcRequest)) => DefDef = { + case (method, path, req) => + q""" + def ${req.methodName}(req: ${req.requestType})(implicit + client: _root_.org.http4s.client.Client[F], + requestEncoder: EntityEncoder[F, ${req.requestType}], + responseDecoder: EntityDecoder[F, ${req.responseType}] + ): F[${req.responseType}] = { + val request = Request[F](Method.$method, uri / $path) + client.expect[${req.responseType}](request) + } + """ + } + + private val requests = for { + d <- rpcDefs.collect { case x if findAnnotation(x.mods, "http").isDefined => x } + args <- findAnnotation(d.mods, "http").collect({ case Apply(_, args) => args }).toList + params <- d.vparamss + _ = require(params.length == 1, s"RPC call ${d.name} has more than one request parameter") + p <- params.headOption.toList + } yield { + val method = TermName(args(0).toString) // TODO: fix direct index access + val uri = args(1).toString // TODO: fix direct index access + + ( + method, + uri, + RpcRequest(d.name, p.tpt, d.tpt, compressionType(serviceDef.mods.annotations))) + } + + val httpRequests = requests.map(toHttpRequest) + val HttpClient = TypeName("HttpClient") + val httpClientClass = q""" + class $HttpClient[$F_](uri: Uri)(implicit Sync: _root_.cats.effect.Sync[F]) { + ..$httpRequests + } + """ + + println(httpClientClass) + + val http = q""" + object http { + + import _root_.org.http4s._ + + $httpClientClass + } + """ + //todo: validate that the request and responses are case classes, if possible case class RpcRequest( methodName: TermName, @@ -398,7 +447,8 @@ object serviceImpl { service.bindService, service.clientClass, service.client, - service.clientFromChannel + service.clientFromChannel, + service.http ) ) ) diff --git a/project/ProjectPlugin.scala b/project/ProjectPlugin.scala index 00116bb8d..5488a79d5 100644 --- a/project/ProjectPlugin.scala +++ b/project/ProjectPlugin.scala @@ -55,6 +55,13 @@ object ProjectPlugin extends AutoPlugin { %%("monocle-core", V.monocle), %%("fs2-reactive-streams", V.fs2ReactiveStreams), %%("fs2-core", V.fs2), + + %%("http4s-dsl", V.http4s), + %%("http4s-blaze-server", V.http4s), + %%("http4s-circe", V.http4s), + %%("circe-generic"), + %%("http4s-blaze-client", V.http4s), + %%("pbdirect", V.pbdirect), %%("avro4s", V.avro4s), %%("log4s", V.log4s), From 01ffedbf82a4f2f757b406a541eaabaa53212004 Mon Sep 17 00:00:00 2001 From: Pepe Garcia Date: Mon, 23 Jul 2018 10:46:13 +0200 Subject: [PATCH 2/5] fix GRPC import to make it not collide with FS2 --- modules/internal/src/main/scala/client/fs2Calls.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/internal/src/main/scala/client/fs2Calls.scala b/modules/internal/src/main/scala/client/fs2Calls.scala index 74f55f200..4bd8d2e28 100644 --- a/modules/internal/src/main/scala/client/fs2Calls.scala +++ b/modules/internal/src/main/scala/client/fs2Calls.scala @@ -22,7 +22,7 @@ import cats.effect.Effect import _root_.fs2._ import _root_.fs2.interop.reactivestreams._ import monix.execution.Scheduler -import io.grpc.{CallOptions, Channel, MethodDescriptor} +import _root_.io.grpc.{CallOptions, Channel, MethodDescriptor} import monix.reactive.Observable object fs2Calls { From d8bd05ce63b4336cf273978797821aa52de8ea9d Mon Sep 17 00:00:00 2001 From: Pepe Garcia Date: Mon, 23 Jul 2018 11:48:17 +0200 Subject: [PATCH 3/5] send request body --- modules/internal/src/main/scala/service.scala | 30 ++++++++++--------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/modules/internal/src/main/scala/service.scala b/modules/internal/src/main/scala/service.scala index 32b165b9b..a2b86f76f 100644 --- a/modules/internal/src/main/scala/service.scala +++ b/modules/internal/src/main/scala/service.scala @@ -245,16 +245,16 @@ object serviceImpl { case _ => false } - val toHttpRequest: ((TermName, String, RpcRequest)) => DefDef = { - case (method, path, req) => + val toHttpRequest: ((TermName, String, TermName, Tree, Tree)) => DefDef = { + case (method, path, name, requestType, responseType) => q""" - def ${req.methodName}(req: ${req.requestType})(implicit + def $name(req: $requestType)(implicit client: _root_.org.http4s.client.Client[F], - requestEncoder: EntityEncoder[F, ${req.requestType}], - responseDecoder: EntityDecoder[F, ${req.responseType}] - ): F[${req.responseType}] = { - val request = Request[F](Method.$method, uri / $path) - client.expect[${req.responseType}](request) + requestEncoder: EntityEncoder[F, $requestType], + responseDecoder: EntityDecoder[F, $responseType] + ): F[$responseType] = { + val request = Request[F](Method.$method, uri / $path).withBody(req) + client.expect[$responseType](request) } """ } @@ -269,10 +269,14 @@ object serviceImpl { val method = TermName(args(0).toString) // TODO: fix direct index access val uri = args(1).toString // TODO: fix direct index access - ( - method, - uri, - RpcRequest(d.name, p.tpt, d.tpt, compressionType(serviceDef.mods.annotations))) + val responseType: Tree = d.tpt match { + case tq"Observable[..$tpts]" => tpts.head + case tq"Stream[$carrier, ..$tpts]" => tpts.head + case tq"$carrier[..$tpts]" => tpts.head + case _ => throw new Exception("asdf") //TODO: sh*t + } + + (method, uri, d.name, p.tpt, responseType) } val httpRequests = requests.map(toHttpRequest) @@ -283,8 +287,6 @@ object serviceImpl { } """ - println(httpClientClass) - val http = q""" object http { From c87867e21ffe9d2bf5275b00620abf012ccfd32b Mon Sep 17 00:00:00 2001 From: Pepe Garcia Date: Mon, 23 Jul 2018 13:01:13 +0200 Subject: [PATCH 4/5] allow the http client calls to have different response types --- modules/internal/src/main/scala/service.scala | 27 ++++++++++++++----- 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/modules/internal/src/main/scala/service.scala b/modules/internal/src/main/scala/service.scala index a2b86f76f..77cdddf19 100644 --- a/modules/internal/src/main/scala/service.scala +++ b/modules/internal/src/main/scala/service.scala @@ -245,16 +245,26 @@ object serviceImpl { case _ => false } - val toHttpRequest: ((TermName, String, TermName, Tree, Tree)) => DefDef = { - case (method, path, name, requestType, responseType) => + def requestExecution(responseType: Tree, methodResponseType: Tree): Tree = + methodResponseType match { + case tq"Observable[..$tpts]" => + q"Observable.fromReactivePublisher(client.streaming(request)(_.body.chunks.parseJsonStream.map(_.as[$responseType]).rethrow).toUnicastPublisher)" + case tq"Stream[$carrier, ..$tpts]" => + q"client.streaming(request)(_.body.chunks.parseJsonStream.map(_.as[$responseType]).rethrow)" + case tq"$carrier[..$tpts]" => + q"client.expect[$responseType](request)" + } + + val toHttpRequest: ((TermName, String, TermName, Tree, Tree, Tree)) => DefDef = { + case (method, path, name, requestType, responseType, methodResponseType) => q""" def $name(req: $requestType)(implicit client: _root_.org.http4s.client.Client[F], requestEncoder: EntityEncoder[F, $requestType], responseDecoder: EntityDecoder[F, $responseType] - ): F[$responseType] = { + ): $methodResponseType = { val request = Request[F](Method.$method, uri / $path).withBody(req) - client.expect[$responseType](request) + ${requestExecution(responseType, methodResponseType)} } """ } @@ -276,21 +286,26 @@ object serviceImpl { case _ => throw new Exception("asdf") //TODO: sh*t } - (method, uri, d.name, p.tpt, responseType) + (method, uri, d.name, p.tpt, responseType, d.tpt) } val httpRequests = requests.map(toHttpRequest) val HttpClient = TypeName("HttpClient") val httpClientClass = q""" - class $HttpClient[$F_](uri: Uri)(implicit Sync: _root_.cats.effect.Sync[F]) { + class $HttpClient[$F_](uri: Uri)(implicit Sync: _root_.cats.effect.Effect[F], ec: scala.concurrent.ExecutionContext) { ..$httpRequests } """ + println(httpClientClass) + val http = q""" object http { + import _root_.fs2.interop.reactivestreams._ import _root_.org.http4s._ + import _root_.jawnfs2._ + import _root_.io.circe.jawn.CirceSupportParser.facade $httpClientClass } From 1669bfde33c595c39eff80526c5c92126a64d65d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pepe=20Garc=C3=ADa?= Date: Tue, 24 Jul 2018 17:46:03 +0200 Subject: [PATCH 5/5] move http derrivation to its own annotation --- build.sbt | 12 +- modules/http/src/main/scala/protocol.scala | 142 ++++++++++++++++++ modules/internal/src/main/scala/service.scala | 69 +-------- project/ProjectPlugin.scala | 9 +- 4 files changed, 150 insertions(+), 82 deletions(-) create mode 100644 modules/http/src/main/scala/protocol.scala diff --git a/build.sbt b/build.sbt index 6a3d75f93..b3efa598c 100644 --- a/build.sbt +++ b/build.sbt @@ -174,14 +174,14 @@ lazy val `idlgen-sbt` = project .settings(buildInfoKeys := Seq[BuildInfoKey](name, version, scalaVersion, sbtVersion)) .settings(buildInfoPackage := "freestyle.rpc.idlgen") -lazy val `http-server` = project - .in(file("modules/http/server")) +lazy val http = project + .in(file("modules/http")) .dependsOn(common % "compile->compile;test->test") .dependsOn(internal) - .dependsOn(client % "test->test") + .dependsOn(client) .dependsOn(server % "test->test") - .settings(moduleName := "frees-rpc-http-server") - .settings(rpcHttpServerSettings) + .settings(moduleName := "frees-rpc-http") + .settings(rpcHttpSettings) .disablePlugins(ScriptedPlugin) ////////////////// @@ -314,7 +314,7 @@ lazy val allModules: Seq[ProjectReference] = Seq( testing, ssl, `idlgen-core`, - `http-server`, + http, `marshallers-jodatime`, `example-routeguide-protocol`, `example-routeguide-common`, diff --git a/modules/http/src/main/scala/protocol.scala b/modules/http/src/main/scala/protocol.scala new file mode 100644 index 000000000..ebdc9f138 --- /dev/null +++ b/modules/http/src/main/scala/protocol.scala @@ -0,0 +1,142 @@ +/* + * Copyright 2017-2018 47 Degrees, LLC. + * + * 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 +package http + +import scala.annotation.{compileTimeOnly, StaticAnnotation} +import scala.reflect.macros.blackbox + +object protocol { + + @compileTimeOnly("enable macro paradise to expand @deriveHttp macro annotations") + class deriveHttp extends StaticAnnotation { + def macroTransform(annottees: Any*): Any = macro deriveHttp_impl + } + + def deriveHttp_impl(c: blackbox.Context)(annottees: c.Expr[Any]*): c.Expr[Any] = { + import c.universe._ + import Flag._ + + require(annottees.length == 2, "@deriveHttp annotation should come AFTER @service annotation") + + val serviceDef = annottees.head.tree + .collect({ + case x: ClassDef if x.mods.hasFlag(TRAIT) || x.mods.hasFlag(ABSTRACT) => x + }) + .head + + val F_ : TypeDef = serviceDef.tparams.head + val F: TypeName = F_.name + + val defs: List[Tree] = serviceDef.impl.body + + val (rpcDefs, nonRpcDefs) = defs.collect { + case d: DefDef => d + } partition (_.rhs.isEmpty) + + def findAnnotation(mods: Modifiers, name: String): Option[Tree] = + mods.annotations find { + case Apply(Select(New(Ident(TypeName(`name`))), _), _) => true + case Apply(Select(New(Select(_, TypeName(`name`))), _), _) => true + case _ => false + } + + def requestExecution(responseType: Tree, methodResponseType: Tree): Tree = + methodResponseType match { + case tq"Observable[..$tpts]" => + q"Observable.fromReactivePublisher(client.streaming(request)(_.body.chunks.parseJsonStream.map(_.as[$responseType]).rethrow).toUnicastPublisher)" + case tq"Stream[$carrier, ..$tpts]" => + q"client.streaming(request)(_.body.chunks.parseJsonStream.map(_.as[$responseType]).rethrow)" + case tq"$carrier[..$tpts]" => + q"client.expect[$responseType](request)" + } + + val toHttpRequest: ((TermName, String, TermName, Tree, Tree, Tree)) => DefDef = { + case (method, path, name, requestType, responseType, methodResponseType) => + q""" + def $name(req: $requestType)(implicit + client: _root_.org.http4s.client.Client[F], + requestEncoder: EntityEncoder[F, $requestType], + responseDecoder: EntityDecoder[F, $responseType] + ): $methodResponseType = { + val request = Request[F](Method.$method, uri / $path).withBody(req) + ${requestExecution(responseType, methodResponseType)} + } + """ + } + + val requests = for { + d <- rpcDefs.collect { case x if findAnnotation(x.mods, "http").isDefined => x } + args <- findAnnotation(d.mods, "http").collect({ case Apply(_, args) => args }).toList + params <- d.vparamss + _ = require(params.length == 1, s"RPC call ${d.name} has more than one request parameter") + p <- params.headOption.toList + } yield { + val method = TermName(args(0).toString) // TODO: fix direct index access + val uri = args(1).toString // TODO: fix direct index access + + val responseType: Tree = d.tpt match { + case tq"Observable[..$tpts]" => tpts.head + case tq"Stream[$carrier, ..$tpts]" => tpts.head + case tq"$carrier[..$tpts]" => tpts.head + case _ => throw new Exception("asdf") //TODO: sh*t + } + + (method, uri, d.name, p.tpt, responseType, d.tpt) + } + + val httpRequests = requests.map(toHttpRequest) + val HttpClient = TypeName("HttpClient") + val httpClientClass = q""" + class $HttpClient[$F_](uri: Uri)(implicit Sync: _root_.cats.effect.Effect[F], ec: scala.concurrent.ExecutionContext) { + ..$httpRequests + } + """ + + println(httpClientClass) + + val http = q""" + object http { + + import _root_.fs2.interop.reactivestreams._ + import _root_.org.http4s._ + import _root_.jawnfs2._ + import _root_.io.circe.jawn.CirceSupportParser.facade + + $httpClientClass + } + """ + + val List(companion) = annottees.map(_.tree).collect({ case x: ModuleDef => x }) + + val result: List[Tree] = List( + annottees.head.tree, // the original trait definition + ModuleDef( + companion.mods, + companion.name, + Template( + companion.impl.parents, + companion.impl.self, + companion.impl.body + ) + ) + ) + + c.Expr(Block(result, Literal(Constant(())))) + } + +} diff --git a/modules/internal/src/main/scala/service.scala b/modules/internal/src/main/scala/service.scala index 77cdddf19..2643b2990 100644 --- a/modules/internal/src/main/scala/service.scala +++ b/modules/internal/src/main/scala/service.scala @@ -245,72 +245,6 @@ object serviceImpl { case _ => false } - def requestExecution(responseType: Tree, methodResponseType: Tree): Tree = - methodResponseType match { - case tq"Observable[..$tpts]" => - q"Observable.fromReactivePublisher(client.streaming(request)(_.body.chunks.parseJsonStream.map(_.as[$responseType]).rethrow).toUnicastPublisher)" - case tq"Stream[$carrier, ..$tpts]" => - q"client.streaming(request)(_.body.chunks.parseJsonStream.map(_.as[$responseType]).rethrow)" - case tq"$carrier[..$tpts]" => - q"client.expect[$responseType](request)" - } - - val toHttpRequest: ((TermName, String, TermName, Tree, Tree, Tree)) => DefDef = { - case (method, path, name, requestType, responseType, methodResponseType) => - q""" - def $name(req: $requestType)(implicit - client: _root_.org.http4s.client.Client[F], - requestEncoder: EntityEncoder[F, $requestType], - responseDecoder: EntityDecoder[F, $responseType] - ): $methodResponseType = { - val request = Request[F](Method.$method, uri / $path).withBody(req) - ${requestExecution(responseType, methodResponseType)} - } - """ - } - - private val requests = for { - d <- rpcDefs.collect { case x if findAnnotation(x.mods, "http").isDefined => x } - args <- findAnnotation(d.mods, "http").collect({ case Apply(_, args) => args }).toList - params <- d.vparamss - _ = require(params.length == 1, s"RPC call ${d.name} has more than one request parameter") - p <- params.headOption.toList - } yield { - val method = TermName(args(0).toString) // TODO: fix direct index access - val uri = args(1).toString // TODO: fix direct index access - - val responseType: Tree = d.tpt match { - case tq"Observable[..$tpts]" => tpts.head - case tq"Stream[$carrier, ..$tpts]" => tpts.head - case tq"$carrier[..$tpts]" => tpts.head - case _ => throw new Exception("asdf") //TODO: sh*t - } - - (method, uri, d.name, p.tpt, responseType, d.tpt) - } - - val httpRequests = requests.map(toHttpRequest) - val HttpClient = TypeName("HttpClient") - val httpClientClass = q""" - class $HttpClient[$F_](uri: Uri)(implicit Sync: _root_.cats.effect.Effect[F], ec: scala.concurrent.ExecutionContext) { - ..$httpRequests - } - """ - - println(httpClientClass) - - val http = q""" - object http { - - import _root_.fs2.interop.reactivestreams._ - import _root_.org.http4s._ - import _root_.jawnfs2._ - import _root_.io.circe.jawn.CirceSupportParser.facade - - $httpClientClass - } - """ - //todo: validate that the request and responses are case classes, if possible case class RpcRequest( methodName: TermName, @@ -464,8 +398,7 @@ object serviceImpl { service.bindService, service.clientClass, service.client, - service.clientFromChannel, - service.http + service.clientFromChannel ) ) ) diff --git a/project/ProjectPlugin.scala b/project/ProjectPlugin.scala index 5488a79d5..c1866454c 100644 --- a/project/ProjectPlugin.scala +++ b/project/ProjectPlugin.scala @@ -55,13 +55,6 @@ object ProjectPlugin extends AutoPlugin { %%("monocle-core", V.monocle), %%("fs2-reactive-streams", V.fs2ReactiveStreams), %%("fs2-core", V.fs2), - - %%("http4s-dsl", V.http4s), - %%("http4s-blaze-server", V.http4s), - %%("http4s-circe", V.http4s), - %%("circe-generic"), - %%("http4s-blaze-client", V.http4s), - %%("pbdirect", V.pbdirect), %%("avro4s", V.avro4s), %%("log4s", V.log4s), @@ -205,7 +198,7 @@ object ProjectPlugin extends AutoPlugin { ) ) - lazy val rpcHttpServerSettings: Seq[Def.Setting[_]] = Seq( + lazy val rpcHttpSettings: Seq[Def.Setting[_]] = Seq( libraryDependencies ++= Seq( %%("http4s-dsl", V.http4s), %%("http4s-blaze-server", V.http4s),