From 72c22493cbf5fde5a59fbb4fbe374d1ec9464515 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fede=20Fern=C3=A1ndez?= <720923+fedefernandez@users.noreply.github.com> Date: Tue, 23 Apr 2019 11:47:11 +0200 Subject: [PATCH] Allows specifying the namespace and the capitalize params through the annotation (#601) --- .../mu/rpc/protocol/package.scala | 4 +- .../mu/rpc/protocol/protocol.scala | 4 + .../mu/rpc/internal/serviceImpl.scala | 99 +++++++++------- .../mu/rpc/protocol/RPCMethodNameTests.scala | 106 ++++++++++++++++++ .../mu/rpc/protocol/RPCNamespaceTests.scala | 106 ++++++++++++++++++ 5 files changed, 275 insertions(+), 44 deletions(-) create mode 100644 modules/server/src/test/scala/higherkindness/mu/rpc/protocol/RPCMethodNameTests.scala create mode 100644 modules/server/src/test/scala/higherkindness/mu/rpc/protocol/RPCNamespaceTests.scala diff --git a/modules/channel/src/main/scala/higherkindness/mu/rpc/protocol/package.scala b/modules/channel/src/main/scala/higherkindness/mu/rpc/protocol/package.scala index cc77d2c90..288a231f2 100644 --- a/modules/channel/src/main/scala/higherkindness/mu/rpc/protocol/package.scala +++ b/modules/channel/src/main/scala/higherkindness/mu/rpc/protocol/package.scala @@ -27,7 +27,9 @@ package object protocol { @compileTimeOnly("enable macro paradise to expand @service macro annotations") class service( val serializationType: SerializationType, - val compressionType: CompressionType = Identity) + val compressionType: CompressionType = Identity, + val namespace: Option[String] = None, + val methodNameStyle: MethodNameStyle = Unchanged) extends StaticAnnotation { def macroTransform(annottees: Any*): Any = macro serviceImpl.service } diff --git a/modules/common/src/main/scala/higherkindness/mu/rpc/protocol/protocol.scala b/modules/common/src/main/scala/higherkindness/mu/rpc/protocol/protocol.scala index fb455d6e8..494514102 100644 --- a/modules/common/src/main/scala/higherkindness/mu/rpc/protocol/protocol.scala +++ b/modules/common/src/main/scala/higherkindness/mu/rpc/protocol/protocol.scala @@ -33,6 +33,10 @@ sealed abstract class CompressionType extends Product with Serializable case object Identity extends CompressionType case object Gzip extends CompressionType +sealed abstract class MethodNameStyle extends Product with Serializable +case object Unchanged extends MethodNameStyle +case object Capitalize extends MethodNameStyle + class message extends StaticAnnotation class option(name: String, value: Any) extends StaticAnnotation class outputPackage(value: String) extends StaticAnnotation diff --git a/modules/internal/src/main/scala/higherkindness/mu/rpc/internal/serviceImpl.scala b/modules/internal/src/main/scala/higherkindness/mu/rpc/internal/serviceImpl.scala index c1f2daf78..30ea5b4d4 100644 --- a/modules/internal/src/main/scala/higherkindness/mu/rpc/internal/serviceImpl.scala +++ b/modules/internal/src/main/scala/higherkindness/mu/rpc/internal/serviceImpl.scala @@ -170,11 +170,27 @@ object serviceImpl { case d: DefDef => d } partition (_.rhs.isEmpty) - private def compressionType(anns: List[Tree]): Tree = - annotationParam(anns, 1, "compressionType", Some("Identity")) match { + private val compressionType: Tree = + annotationParam(1, "compressionType") { case "Identity" => q"None" case "Gzip" => q"""Some("gzip")""" - } + }.getOrElse(q"None") + + private val OptionString = "Some\\(\"(.+)\"\\)".r + + private val namespace: String = + annotationParam(2, "namespace") { + case OptionString(s) => s"$s." + case "None" => "" + }.getOrElse("") + + private val fullyServiceName = namespace + serviceName.toString + + private val methodNameStyle: MethodNameStyle = + annotationParam(3, "methodNameStyle") { + case "Capitalize" => Capitalize + case "Unchanged" => Unchanged + }.getOrElse(Unchanged) private val rpcRequests: List[RpcRequest] = for { d <- rpcDefs @@ -184,36 +200,22 @@ object serviceImpl { } yield RpcRequest( Operation(d.name, TypeTypology(p.tpt), TypeTypology(d.tpt)), - compressionType(serviceDef.mods.annotations)) + compressionType, + namespace, + methodNameStyle + ) val imports: List[Tree] = defs.collect { case imp: Import => imp } private val serializationType: SerializationType = - c.prefix.tree match { - case q"new service($serializationType)" => - serializationType.toString match { - case "Protobuf" => Protobuf - case "Avro" => Avro - case "AvroWithSchema" => AvroWithSchema - case _ => - sys.error( - "@service annotation should have a SerializationType parameter [Protobuf|Avro|AvroWithSchema]") - } - case q"new service($serializationType, $_)" => - serializationType.toString match { - case "Protobuf" => Protobuf - case "Avro" => Avro - case "AvroWithSchema" => AvroWithSchema - case _ => - sys.error( - "@service annotation should have a SerializationType parameter [Protobuf|Avro|AvroWithSchema], and a CompressionType parameter [Identity|Gzip]") - } - case _ => - sys.error( - "@service annotation should have a SerializationType parameter [Protobuf|Avro|AvroWithSchema]") - } + annotationParam(0, "serializationType") { + case "Protobuf" => Protobuf + case "Avro" => Avro + case "AvroWithSchema" => AvroWithSchema + }.getOrElse(sys.error( + "@service annotation should have a SerializationType parameter [Protobuf|Avro|AvroWithSchema]")) val encodersImport = serializationType match { case Protobuf => @@ -246,7 +248,7 @@ object serviceImpl { val bindService: DefDef = q""" def bindService[$F_](implicit ..$bindImplicits): $F[_root_.io.grpc.ServerServiceDefinition] = _root_.higherkindness.mu.rpc.internal.service.GRPCServiceDefBuilder.build[$F](${lit( - serviceName)}, ..$serverCallDescriptorsAndHandlers) + fullyServiceName)}, ..$serverCallDescriptorsAndHandlers) """ private val clientCallMethods: List[Tree] = rpcRequests.map(_.clientDef) @@ -311,17 +313,21 @@ object serviceImpl { private def lit(x: Any): Literal = Literal(Constant(x.toString)) - private def annotationParam( - params: List[Tree], - pos: Int, - name: String, - default: Option[String] = None): String = - params - .collectFirst { - case q"$pName = $pValue" if pName.toString == name => pValue.toString - } - .getOrElse(if (params.isDefinedAt(pos)) params(pos).toString - else default.getOrElse(sys.error(s"Missing annotation parameter $name"))) + private def annotationParam[A](pos: Int, name: String)( + pf: PartialFunction[String, A]): Option[A] = { + val rawValue: Option[String] = c.prefix.tree match { + case q"new service(..$list)" => + list + .collectFirst { + case q"$pName = $pValue" if pName.toString == name => pValue.toString + } + .orElse(list.lift(pos).map(_.toString())) + case _ => None + } + rawValue.map { s => + pf.lift(s).getOrElse(sys.error(s"Invalid `$name` annotation value ($s)")) + } + } private def findAnnotation(mods: Modifiers, name: String): Option[Tree] = mods.annotations find { @@ -333,7 +339,9 @@ object serviceImpl { //todo: validate that the request and responses are case classes, if possible case class RpcRequest( operation: Operation, - compressionOption: Tree + compressionOption: Tree, + namespace: String, + methodNameStyle: MethodNameStyle ) { import operation._ @@ -354,7 +362,12 @@ object serviceImpl { q"_root_.io.grpc.MethodDescriptor.MethodType.${TermName(suffix)}" } - private val methodDescriptorName = TermName(name + "MethodDescriptor") + private val updatedName = methodNameStyle match { + case Unchanged => name.toString + case Capitalize => name.toString.capitalize + } + + private val methodDescriptorName = TermName(updatedName + "MethodDescriptor") private val reqType = request.safeType @@ -371,8 +384,8 @@ object serviceImpl { RespM) .setType($streamingMethodType) .setFullMethodName( - _root_.io.grpc.MethodDescriptor.generateFullMethodName(${lit(serviceName)}, ${lit( - name)})) + _root_.io.grpc.MethodDescriptor.generateFullMethodName( + ${lit(fullyServiceName)}, ${lit(updatedName)})) .build() } """.supressWarts("Null", "ExplicitImplicitTypes") diff --git a/modules/server/src/test/scala/higherkindness/mu/rpc/protocol/RPCMethodNameTests.scala b/modules/server/src/test/scala/higherkindness/mu/rpc/protocol/RPCMethodNameTests.scala new file mode 100644 index 000000000..b24ff8393 --- /dev/null +++ b/modules/server/src/test/scala/higherkindness/mu/rpc/protocol/RPCMethodNameTests.scala @@ -0,0 +1,106 @@ +/* + * Copyright 2017-2019 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 higherkindness.mu.rpc.protocol + +import cats.Applicative +import cats.syntax.applicative._ +import higherkindness.mu.rpc.common._ +import higherkindness.mu.rpc.protocol.Utils._ +import org.scalacheck.Prop._ +import org.scalatest._ +import org.scalatestplus.scalacheck.Checkers + +class RPCMethodNameTests extends RpcBaseTestSuite with BeforeAndAfterAll with Checkers { + + object RPCService { + + case class Request(s: String) + + case class Response(length: Int) + + @service(Protobuf, Identity, None, Capitalize) trait ProtoRPCServiceDef[F[_]] { + def proto1(req: Request): F[Response] + } + @service(Avro, Identity, None, Capitalize) trait AvroRPCServiceDef[F[_]] { + def avro(req: Request): F[Response] + } + @service(AvroWithSchema, Identity, None, Capitalize) trait AvroWithSchemaRPCServiceDef[F[_]] { + def avroWithSchema(req: Request): F[Response] + } + + class RPCServiceDefImpl[F[_]: Applicative] + extends ProtoRPCServiceDef[F] + with AvroRPCServiceDef[F] + with AvroWithSchemaRPCServiceDef[F] { + + def proto1(bd: Request): F[Response] = Response(bd.s.length).pure + def avro(bd: Request): F[Response] = Response(bd.s.length).pure + def avroWithSchema(bd: Request): F[Response] = Response(bd.s.length).pure + } + + } + + "A RPC server" should { + + import RPCService._ + import higherkindness.mu.rpc.TestsImplicits._ + + implicit val H: RPCServiceDefImpl[ConcurrentMonad] = new RPCServiceDefImpl[ConcurrentMonad] + + "be able to call a service with a capitalized method using proto" in { + + withClient( + ProtoRPCServiceDef.bindService[ConcurrentMonad], + ProtoRPCServiceDef.clientFromChannel[ConcurrentMonad](_)) { client => + check { + forAll { s: String => + client.proto1(Request(s)).map(_.length).unsafeRunSync() == s.length + } + } + } + + } + + "be able to call a service with a capitalized method using avro" in { + + withClient( + AvroRPCServiceDef.bindService[ConcurrentMonad], + AvroRPCServiceDef.clientFromChannel[ConcurrentMonad](_)) { client => + check { + forAll { s: String => + client.avro(Request(s)).map(_.length).unsafeRunSync() == s.length + } + } + } + + } + + "be able to call a service with a capitalized method using avro with schema" in { + + withClient( + AvroWithSchemaRPCServiceDef.bindService[ConcurrentMonad], + AvroWithSchemaRPCServiceDef.clientFromChannel[ConcurrentMonad](_)) { client => + check { + forAll { s: String => + client.avroWithSchema(Request(s)).map(_.length).unsafeRunSync() == s.length + } + } + } + + } + } +} diff --git a/modules/server/src/test/scala/higherkindness/mu/rpc/protocol/RPCNamespaceTests.scala b/modules/server/src/test/scala/higherkindness/mu/rpc/protocol/RPCNamespaceTests.scala new file mode 100644 index 000000000..4ab72ddb4 --- /dev/null +++ b/modules/server/src/test/scala/higherkindness/mu/rpc/protocol/RPCNamespaceTests.scala @@ -0,0 +1,106 @@ +/* + * Copyright 2017-2019 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 higherkindness.mu.rpc.protocol + +import cats.Applicative +import cats.syntax.applicative._ +import higherkindness.mu.rpc.common._ +import higherkindness.mu.rpc.protocol.Utils._ +import org.scalacheck.Prop._ +import org.scalatest._ +import org.scalatestplus.scalacheck.Checkers + +class RPCNamespaceTests extends RpcBaseTestSuite with BeforeAndAfterAll with Checkers { + + object RPCService { + + case class Request(s: String) + + case class Response(length: Int) + + @service(Protobuf, Identity, Some("my.namespace")) trait ProtoRPCServiceDef[F[_]] { + def proto1(req: Request): F[Response] + } + @service(Avro, Identity, Some("my.namespace")) trait AvroRPCServiceDef[F[_]] { + def avro(req: Request): F[Response] + } + @service(AvroWithSchema, Identity, Some("my.namespace")) trait AvroWithSchemaRPCServiceDef[F[_]] { + def avroWithSchema(req: Request): F[Response] + } + + class RPCServiceDefImpl[F[_]: Applicative] + extends ProtoRPCServiceDef[F] + with AvroRPCServiceDef[F] + with AvroWithSchemaRPCServiceDef[F] { + + def proto1(bd: Request): F[Response] = Response(bd.s.length).pure + def avro(bd: Request): F[Response] = Response(bd.s.length).pure + def avroWithSchema(bd: Request): F[Response] = Response(bd.s.length).pure + } + + } + + "A RPC server" should { + + import RPCService._ + import higherkindness.mu.rpc.TestsImplicits._ + + implicit val H: RPCServiceDefImpl[ConcurrentMonad] = new RPCServiceDefImpl[ConcurrentMonad] + + "be able to call a service with a defined namespace with proto" in { + + withClient( + ProtoRPCServiceDef.bindService[ConcurrentMonad], + ProtoRPCServiceDef.clientFromChannel[ConcurrentMonad](_)) { client => + check { + forAll { s: String => + client.proto1(Request(s)).map(_.length).unsafeRunSync() == s.length + } + } + } + + } + + "be able to call a service with a defined namespace with avro" in { + + withClient( + AvroRPCServiceDef.bindService[ConcurrentMonad], + AvroRPCServiceDef.clientFromChannel[ConcurrentMonad](_)) { client => + check { + forAll { s: String => + client.avro(Request(s)).map(_.length).unsafeRunSync() == s.length + } + } + } + + } + + "be able to call a service with a defined namespace with avro with schema" in { + + withClient( + AvroWithSchemaRPCServiceDef.bindService[ConcurrentMonad], + AvroWithSchemaRPCServiceDef.clientFromChannel[ConcurrentMonad](_)) { client => + check { + forAll { s: String => + client.avroWithSchema(Request(s)).map(_.length).unsafeRunSync() == s.length + } + } + } + + } + } +}