Skip to content

Commit

Permalink
Allows specifying the namespace and the capitalize params through the…
Browse files Browse the repository at this point in the history
… annotation (#601)
  • Loading branch information
fedefernandez committed Apr 23, 2019
1 parent a88d25f commit 72c2249
Show file tree
Hide file tree
Showing 5 changed files with 275 additions and 44 deletions.
Expand Up @@ -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
}
Expand Down
Expand Up @@ -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
Expand Down
Expand Up @@ -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
Expand All @@ -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 =>
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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 {
Expand All @@ -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._
Expand All @@ -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

Expand All @@ -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")
Expand Down
@@ -0,0 +1,106 @@
/*
* 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.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
}
}
}

}
}
}
@@ -0,0 +1,106 @@
/*
* 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.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
}
}
}

}
}
}

0 comments on commit 72c2249

Please sign in to comment.