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/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/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/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 { diff --git a/project/ProjectPlugin.scala b/project/ProjectPlugin.scala index 00116bb8d..c1866454c 100644 --- a/project/ProjectPlugin.scala +++ b/project/ProjectPlugin.scala @@ -198,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),