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

Framework-less, generalized, annotation-based RPC #57

Merged
merged 63 commits into from
Jun 7, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
232ddff
first prototype of one RPC macro to rule them all
ghik Apr 4, 2018
185195d
AsRaw for RPC macro prototype
ghik Apr 5, 2018
a3b8418
recursive implicit reuse
ghik Apr 6, 2018
1f7fd1c
RPC support for default parameter values
ghik Apr 7, 2018
fd0f2f4
RPC support for varargs and by-name parameters
ghik Apr 7, 2018
e2c358e
@RPCName for params, relaxed uniqueness constraint for methods
ghik Apr 8, 2018
aa17fc0
AnnotationAggregate reworked to allow argument passing
ghik Apr 8, 2018
3793232
error reporting reworked
ghik Apr 8, 2018
a905611
grouping real parameters with annotations
ghik Apr 8, 2018
5720e63
first probably working prototype of generalized RPC
ghik May 14, 2018
ab3df30
refactored RPC param encoding defaults
ghik May 14, 2018
f77610d
injecting raw RPC companion implicits, removal of duplicated implicits
ghik May 15, 2018
1034c03
improved error reporting in RPC macros
ghik May 15, 2018
1e222e1
nice and detailed error messages from RPC macros
ghik May 15, 2018
16af0a3
RPCFramework is now a bridge over new RPC engine
ghik May 15, 2018
f7a1db5
reworked method and param matching in RPC macros, tag annotations
ghik May 17, 2018
6077ebf
scaladocs for RPC annotations, cosmetic refactors
ghik May 18, 2018
9e6901a
adjusted annotation extraction order
ghik May 18, 2018
5021726
reverted order of type params in AsRaw/AsReal
ghik May 18, 2018
d2f48ac
repeated renamed to multi
ghik May 21, 2018
ee288b3
changed required signature of raw methods
ghik May 21, 2018
2269f55
refactors in RPCMacros
ghik May 21, 2018
6c317ad
more refactors in RPC macros in preparation for RPC metadata
ghik May 21, 2018
6f63589
minor refactors in RPCMacros
ghik May 21, 2018
bfd2c1d
split RPCMacros into subtraits
ghik May 21, 2018
e3686a6
minor
ghik May 21, 2018
e69153b
fix
ghik May 21, 2018
7f07abe
fixed annotation processing
ghik May 21, 2018
bd362e6
cosmetic
ghik May 21, 2018
30834e5
scaladoc fixes
ghik May 21, 2018
4979225
refactored out ParamsParser
ghik May 22, 2018
8566a5e
WIP metadata materialization
ghik May 22, 2018
4e388f2
first version of method metadata materialization
ghik May 23, 2018
ecd4da2
method tagging support in RPC metadata
ghik May 24, 2018
9deaa76
error message adjustments for RPC metadata macros
ghik May 24, 2018
38d52b1
more RPC macro refactors in preparation for param metadata support
ghik May 24, 2018
e9f6fdb
refactored out MetadataParamStrategy
ghik May 24, 2018
ac03a81
first working prototype of parameter metadata materialization
ghik May 25, 2018
b330faa
removed MetadataParamTarget and recursive metadata construction
ghik May 25, 2018
a8573d8
minor
ghik May 25, 2018
69baa34
removed MetadataParameterStrategy and ParamMetadataMapping
ghik May 25, 2018
37fc886
extracted metadata related macro code to separate trait
ghik May 25, 2018
b48230d
reification of annotations into metadata
ghik May 25, 2018
a9bc652
legacy RPCFramework now uses new RPC metadata
ghik May 25, 2018
2269a96
trailing comma
ghik May 25, 2018
dcd58af
MetadataAnnotation is toplevel
ghik May 25, 2018
f98f72a
@reifyName, @reifyPosition, @reifyFlags and documentation, refactors
ghik May 28, 2018
899a5ff
@whenAbsent annotation for GenCodec and RPC
ghik May 28, 2018
2b65b11
GenCodec docs updated, some tests for RPC metadata
ghik May 29, 2018
7d465a3
RPCMacros is not final
ghik Jun 1, 2018
51ff4e2
raw method arity
ghik Jun 2, 2018
6a43f49
optimized asReal/asRaw conversion trees
ghik Jun 2, 2018
b7e4215
commons-annotations cross compilation
ghik Jun 4, 2018
8dc6510
renamed @reify to @reifyAnnot, introduced @hasAnnot
ghik Jun 4, 2018
4f9642b
more docs, allowed raw methods to take no parameter lists
ghik Jun 6, 2018
ba2123c
cosmetic
ghik Jun 6, 2018
3620fd0
more descriptive type parameter names for AsReal/AsRaw
ghik Jun 7, 2018
15ccd49
AsRealRaw renamed to AsRawReal
ghik Jun 7, 2018
eb6d656
cosmetic consistency
ghik Jun 7, 2018
dd0a515
RPC -> Rpc outside of RPCFramework
ghik Jun 7, 2018
ee8e578
moved indent extension method to tests
ghik Jun 7, 2018
675cf21
safer ParamFlags
ghik Jun 7, 2018
eb9f678
Merge remote-tracking branch 'origin/master' into uberpc
ghik Jun 7, 2018
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
35 changes: 22 additions & 13 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ val commonSettings = Seq(
s"-Xlint:-missing-interpolator,-adapted-args,${if (scalaBinaryVersion.value == "2.12") "-unused," else ""}_",
),
// some Java 8 related tests use Java interface static methods, Scala 2.11.12 requires JDK8 target for that
scalacOptions in Test ++= (if(scalaBinaryVersion.value == "2.11") Seq("-target:jvm-1.8") else Seq()),
scalacOptions in Test ++= (if (scalaBinaryVersion.value == "2.11") Seq("-target:jvm-1.8") else Seq()),
apiURL := Some(url("http://avsystem.github.io/scala-commons/api")),
autoAPIMappings := true,

Expand Down Expand Up @@ -132,6 +132,7 @@ lazy val commons = project.in(file("."))
.enablePlugins(ScalaUnidocPlugin)
.aggregate(
`commons-annotations`,
`commons-annotations-js`,
`commons-macros`,
`commons-core`,
`commons-core-js`,
Expand All @@ -151,6 +152,7 @@ lazy val commons = project.in(file("."))
scalacOptions in ScalaUnidoc in unidoc += "-Ymacro-expand:none",
unidocProjectFilter in ScalaUnidoc in unidoc :=
inAnyProject -- inProjects(
`commons-annotations-js`,
`commons-macros`,
`commons-analyzer`,
`commons-core-js`,
Expand All @@ -159,16 +161,6 @@ lazy val commons = project.in(file("."))
),
)

lazy val `commons-annotations` = project
.settings(jvmCommonSettings)

lazy val `commons-macros` = project
.dependsOn(`commons-annotations`)
.settings(
jvmCommonSettings,
libraryDependencies += "org.scala-lang" % "scala-reflect" % scalaVersion.value,
)

def mkSourceDirs(base: File, scalaBinary: String, conf: String): Seq[File] = Seq(
base / "src" / conf / "scala",
base / "src" / conf / s"scala-$scalaBinary",
Expand All @@ -182,8 +174,25 @@ def sourceDirsSettings(baseMapper: File => File) = Seq(
mkSourceDirs(baseMapper(baseDirectory.value), scalaBinaryVersion.value, "test"),
)

lazy val `commons-annotations` = project
.settings(jvmCommonSettings)

lazy val `commons-annotations-js` = project.in(`commons-annotations`.base / "js")
.enablePlugins(ScalaJSPlugin)
.configure(p => if (forIdeaImport) p.dependsOn(`commons-annotations`) else p)
.settings(
jsCommonSettings,
name := (name in `commons-annotations`).value,
sourceDirsSettings(_.getParentFile),
)

lazy val `commons-macros` = project.settings(
jvmCommonSettings,
libraryDependencies += "org.scala-lang" % "scala-reflect" % scalaVersion.value,
)

lazy val `commons-core` = project
.dependsOn(`commons-macros`)
.dependsOn(`commons-macros`, `commons-annotations`)
.settings(
jvmCommonSettings,
sourceDirsSettings(_ / "jvm"),
Expand All @@ -197,7 +206,7 @@ lazy val `commons-core` = project
lazy val `commons-core-js` = project.in(`commons-core`.base / "js")
.enablePlugins(ScalaJSPlugin)
.configure(p => if (forIdeaImport) p.dependsOn(`commons-core`) else p)
.dependsOn(`commons-macros`)
.dependsOn(`commons-macros`, `commons-annotations-js`)
.settings(
jsCommonSettings,
name := (name in `commons-core`).value,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ import akka.actor.{ActorRef, ActorSystem}
import akka.util.ByteString
import com.avsystem.commons.rpc.akka.client.ClientRawRPC
import com.avsystem.commons.rpc.akka.server.ServerActor
import com.avsystem.commons.rpc.{FunctionRPCFramework, GetterRPCFramework, ProcedureRPCFramework}
import com.avsystem.commons.rpc.{FunctionRPCFramework, GetterRPCFramework, MetadataAnnotation, ProcedureRPCFramework, RpcMetadataCompanion, TypedMetadata, infer, multi, reifyAnnot, reifyName, verbatim}
import com.avsystem.commons.serialization.{GenCodec, StreamInput, StreamOutput}
import monix.reactive.Observable

/**
* RPC Framework implemented with Akka as transportation layer.
Expand All @@ -17,6 +18,7 @@ import com.avsystem.commons.serialization.{GenCodec, StreamInput, StreamOutput}
*/
object AkkaRPCFramework extends GetterRPCFramework with ProcedureRPCFramework with FunctionRPCFramework with MonixRPCFramework {
trait RawRPC extends GetterRawRPC with ProcedureRawRPC with FunctionRawRPC with MonixRawRPC
object RawRPC extends BaseRawRpcCompanion
abstract class FullRPCInfo[T] extends BaseFullRPCInfo[T]

type RawValue = ByteString
Expand All @@ -26,6 +28,23 @@ object AkkaRPCFramework extends GetterRPCFramework with ProcedureRPCFramework wi
type ParamTypeMetadata[T] = DummyImplicit
type ResultTypeMetadata[T] = DummyImplicit

case class RPCMetadata[T](
@reifyName name: String,
@reifyAnnot @multi annotations: List[MetadataAnnotation],
@multi @verbatim procedureSignatures: Map[String, ProcedureSignature],
@multi functionSignatures: Map[String, FunctionSignature[_]],
@multi observeSignatures: Map[String, ObserveSignature[_]],
@multi getterSignatures: Map[String, GetterSignature[_]]
)
object RPCMetadata extends RpcMetadataCompanion[RPCMetadata]

case class ObserveSignature[T](
name: String,
paramMetadata: List[ParamMetadata[_]],
annotations: List[MetadataAnnotation],
@infer resultTypeMetadata: ResultTypeMetadata[T]
) extends Signature with TypedMetadata[Observable[T]]

def read[T: Reader](raw: RawValue): T =
GenCodec.read[T](new StreamInput(new DataInputStream(raw.iterator.asInputStream)))
def write[T: Writer](value: T): RawValue = {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,18 @@
package com.avsystem.commons
package rpc.akka

import com.avsystem.commons.rpc.RPCFramework
import com.avsystem.commons.rpc._
import monix.reactive.Observable

trait MonixRPCFramework extends RPCFramework {
override type RawRPC <: MonixRawRPC

trait MonixRawRPC {this: RawRPC =>
def observe(rpcName: String, argLists: List[List[RawValue]]): Observable[RawValue]
trait MonixRawRPC { this: RawRPC =>
@multi def observe(rpcName: String)(@multi args: List[RawValue]): Observable[RawValue]
}

implicit def ObservableRealHandler[A: Writer]: RealInvocationHandler[Observable[A], Observable[RawValue]] =
RealInvocationHandler[Observable[A], Observable[RawValue]](_.map(write[A]))

implicit def ObservableRawHandler[A: Reader]: RawInvocationHandler[Observable[A]] =
RawInvocationHandler[Observable[A]]((rawRpc, rpcName, argLists) => rawRpc.observe(rpcName, argLists).map(read[A]))
implicit def readerBasedObservableAsReal[T: Reader]: AsReal[Observable[RawValue], Observable[T]] =
AsReal.create(_.map(read[T]))
implicit def writerBasedObservableAsRaw[T: Writer]: AsRaw[Observable[RawValue], Observable[T]] =
AsRaw.create(_.map(write[T]))
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,16 @@ private object RemoteMessage {
implicit val heatBeatCodec: GenCodec[MonixProtocol.Heartbeat.type] = GenCodec.materialize[MonixProtocol.Heartbeat.type]
}

private final case class RawInvocation(rpcName: String, argLists: List[List[RawValue]]) extends RemoteMessage
private final case class RawInvocation(rpcName: String, args: List[RawValue]) extends RemoteMessage

private sealed trait InvocationMessage extends RemoteMessage {
def getterChain: Seq[RawInvocation]
def name: String
def argLists: List[List[RawValue]]
def args: List[RawValue]
}
private final case class ProcedureInvocationMessage(name: String, argLists: List[List[RawValue]], getterChain: Seq[RawInvocation]) extends InvocationMessage
private final case class FunctionInvocationMessage(name: String, argLists: List[List[RawValue]], getterChain: Seq[RawInvocation]) extends InvocationMessage
private final case class ObservableInvocationMessage(name: String, argLists: List[List[RawValue]], getterChain: Seq[RawInvocation]) extends InvocationMessage
private final case class ProcedureInvocationMessage(name: String, args: List[RawValue], getterChain: Seq[RawInvocation]) extends InvocationMessage
private final case class FunctionInvocationMessage(name: String, args: List[RawValue], getterChain: Seq[RawInvocation]) extends InvocationMessage
private final case class ObservableInvocationMessage(name: String, args: List[RawValue], getterChain: Seq[RawInvocation]) extends InvocationMessage

private sealed trait InvocationResult extends RemoteMessage
private final case class InvocationSuccess(value: RawValue) extends InvocationResult
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ package rpc.akka.client
import akka.actor.ActorSystem
import akka.pattern.ask
import akka.util.Timeout
import com.avsystem.commons.rpc.akka.AkkaRPCFramework.RawRPC
import com.avsystem.commons.rpc.akka.AkkaRPCFramework.{RawRPC, RawValue}
import com.avsystem.commons.rpc.akka._
import monix.execution.Cancelable
import monix.reactive.{Observable, OverflowStrategy}
Expand All @@ -14,12 +14,12 @@ import monix.reactive.{Observable, OverflowStrategy}
*/
private[akka] final class ClientRawRPC(config: AkkaRPCClientConfig, getterChain: Seq[RawInvocation] = Nil)(implicit system: ActorSystem) extends AkkaRPCFramework.RawRPC {

override def fire(rpcName: String, argLists: List[List[AkkaRPCFramework.RawValue]]): Unit = {
system.actorSelection(config.serverPath) ! ProcedureInvocationMessage(rpcName, argLists, getterChain)
override def fire(rpcName: String)(args: List[RawValue]): Unit = {
system.actorSelection(config.serverPath) ! ProcedureInvocationMessage(rpcName, args, getterChain)
}
override def call(rpcName: String, argLists: List[List[AkkaRPCFramework.RawValue]]): Future[AkkaRPCFramework.RawValue] = {
override def call(rpcName: String)(args: List[RawValue]): Future[RawValue] = {
implicit val timeout: Timeout = Timeout(config.functionCallTimeout)
val future = system.actorSelection(config.serverPath) ? FunctionInvocationMessage(rpcName, argLists, getterChain)
val future = system.actorSelection(config.serverPath) ? FunctionInvocationMessage(rpcName, args, getterChain)

import com.avsystem.commons.concurrent.RunNowEC.Implicits.executionContext

Expand All @@ -29,13 +29,13 @@ private[akka] final class ClientRawRPC(config: AkkaRPCClientConfig, getterChain:
case value => Future.failed(new IllegalStateException(s"Illegal message type. Should be InvocationResult, but received value was: $value"))
}
}
override def get(rpcName: String, argLists: List[List[AkkaRPCFramework.RawValue]]): RawRPC =
new ClientRawRPC(config, getterChain :+ RawInvocation(rpcName, argLists))
override def get(rpcName: String)(args: List[RawValue]): RawRPC =
new ClientRawRPC(config, getterChain :+ RawInvocation(rpcName, args))

override def observe(rpcName: String, argLists: List[List[AkkaRPCFramework.RawValue]]): Observable[AkkaRPCFramework.RawValue] = {
override def observe(rpcName: String)(args: List[RawValue]): Observable[RawValue] = {
Observable.create(OverflowStrategy.Unbounded) { s =>
val actor = system.actorOf(MonixClientActor.props(s, config))
actor ! ObservableInvocationMessage(rpcName, argLists, getterChain)
actor ! ObservableInvocationMessage(rpcName, args, getterChain)
Cancelable.empty // TODO implement proper canceling
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@ private final class ServerActor(rawRPC: AkkaRPCFramework.RawRPC, config: AkkaRPC

override def receive: Receive = {
case msg@ProcedureInvocationMessage(name, argLists, getterChain) =>
resolveRpc(msg).fire(name, argLists)
resolveRpc(msg).fire(name)(argLists)
case msg@FunctionInvocationMessage(name, argLists, getterChain) =>
import com.avsystem.commons.concurrent.RunNowEC.Implicits.executionContext
val s = sender()
resolveRpc(msg).call(name, argLists).onComplete {
resolveRpc(msg).call(name)(argLists).onComplete {
case Success(value) => s ! InvocationSuccess(value)
case Failure(e) =>
logError(e, name)
Expand All @@ -37,7 +37,7 @@ private final class ServerActor(rawRPC: AkkaRPCFramework.RawRPC, config: AkkaRPC
Ack.Continue
}

resolveRpc(msg).observe(name, argLists).subscribe(
resolveRpc(msg).observe(name)(argLists).subscribe(
value => {
val result = s ? InvocationSuccess(value)
result.mapTo[MonixProtocol.RemoteAck].map {
Expand All @@ -64,7 +64,8 @@ private final class ServerActor(rawRPC: AkkaRPCFramework.RawRPC, config: AkkaRPC
)
}

private def resolveRpc(msg: InvocationMessage) = rawRPC.resolveGetterChain(msg.getterChain.map(r => AkkaRPCFramework.RawInvocation(r.rpcName, r.argLists)).toList)
private def resolveRpc(msg: InvocationMessage) =
rawRPC.resolveGetterChain(msg.getterChain.map(r => AkkaRPCFramework.RawInvocation(r.rpcName, r.args)).toList)

private def logError(e: Throwable, methodName: String): Unit = {
log.error(e,
Expand All @@ -77,4 +78,4 @@ private final class ServerActor(rawRPC: AkkaRPCFramework.RawRPC, config: AkkaRPC

private[akka] object ServerActor {
def props(rawRPC: AkkaRPCFramework.RawRPC, config: AkkaRPCServerConfig): Props = Props(new ServerActor(rawRPC, config))
}
}
Original file line number Diff line number Diff line change
@@ -1,29 +1,22 @@
package com.avsystem.commons
package rpc.akka

import com.avsystem.commons.rpc.RPC
import com.avsystem.commons.rpc.akka.AkkaRPCFramework._
import monix.reactive.Observable


/**
* @author Wojciech Milewski
*/
@RPC
trait TestRPC {
def fireAndForget(): Unit
def echoAsString(int: Int): Future[String]
def stream: Observable[Int]
def inner: InnerRPC
}
object TestRPC {
implicit val fullRPCInfo: BaseFullRPCInfo[TestRPC] = materializeFullInfo
}
object TestRPC extends RPCCompanion[TestRPC]

@RPC
trait InnerRPC {
def innerFire(): Unit
}
object InnerRPC {
implicit val fullRPCInfo: BaseFullRPCInfo[InnerRPC] = materializeFullInfo
}
object InnerRPC extends RPCCompanion[InnerRPC]
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,47 @@ package annotation
import scala.annotation.StaticAnnotation

/**
* When an annotation class extends this trait, annotation processing macros (e.g. for `GenCodec` materialization)
* will look into annotations of the aggregating annotation itself and apply these annotations as if they were
* applied directly on the same target as the aggregating annotation. Example:
* Base trait for annotations which aggregate multiple other annotations. This way annotation aggregates
* work like "annotation functions" - they are annotations that yield more annotations.
*
* In order to specify aggregated annotations, the class that extends `AnnotationAggregate` must
* redefine the `Implied` dummy type member and apply the aggregated annotations on it. Macro engines
* used in `GenCodec` materialization and RPC framework will automatically pick up these annotations.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest we start with the fact that other macros won't, or move this to a more GenCodec-specific package.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why? Annotation processing is a part of MacroCommons and it should work everywhere.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-AVSystem macros.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I specifically listed GenCodec macros and RPC macros - what's the problem?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Must have misread something 😢

*
* {{{
* import com.avsystem.commons.serialization._
*
* @name("_id") @outOfOrder
* class mongoId extends AnnotationAggregate
* class mongoId extends AnnotationAggregate {
* @name("_id") @outOfOrder
* type Implied
* }
*
* case class SomeMongoEntity(@mongoId id: String, data: String)
* }}}
*
* In the above example, applying `@mongoId` annotation on the `id` field has the same effect as if
* annotations `@name("_id") @outOfOrder` were applied directly on that field.
*
* NOTE: thanks to the fact that aggregated annotations are applied on a type member
* you can pass the arguments of original annotation to aggregated annotations, e.g.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do I need to know why when I ctrl+q for a quick overview? The type member note should be a comment on Implied (and it is).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IntelliJ cannot into Scaladoc. Among other problems, it removes annotations from code snippets, like in this case. I'm not going to work around it. Just read the sources or API reference.

*
* {{{
* class rpcNameAndDescription(name: String, description: String) extends AnnotationAggregate {
* @rpcName(name) // passing `name` to aggregated annotation
* type Implied
* }
* }}}
*/
trait AnnotationAggregate extends StaticAnnotation
trait AnnotationAggregate extends StaticAnnotation {
/**
* Dummy type member meant to be redefined in order to have aggregated annotations applied on it.
* These annotations will be automatically picked up by macro engines each time they encounter
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Macro engines or our macros?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Definitely not all macro engines in the world.

* the aggregating annotation itself.
* Other than being an "anchor" for annotations, this type member has no actual meaning and there is no
* reason to ever actually use it.
* NOTE: a less weird solution would be to put aggregated annotations on the
* aggregating annotation class itself, but this would make it impossible to access the arguments
* of aggregating annotation in aggregated annotations.
*/
type Implied
}

This file was deleted.

This file was deleted.

Loading