Skip to content

Commit

Permalink
@whenAbsent annotation for GenCodec and RPC
Browse files Browse the repository at this point in the history
  • Loading branch information
ghik committed May 29, 2018
1 parent f98f72a commit 899a5ff
Show file tree
Hide file tree
Showing 13 changed files with 102 additions and 20 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ sealed trait RawParamAnnotation extends RawRpcAnnotation
*/
class rpcName(val name: String) extends RpcAnnotation

class whenAbsent[+T](v: => T) extends serialization.whenAbsent[T](v)

/**
* Base trait for RPC tag annotations. Tagging gives more direct control over how real methods
* and their parameters are matched against raw methods and their parameters.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ package serialization
import scala.annotation.StaticAnnotation

/**
* If some case class field has default value, you can use this annotation on this field to instruct an
* automatically derived `GenCodec` to not persist the value of that field if it's equal to the default value.
* If some case class field has default value or [[whenAbsent]] annotation, you can use [[transientDefault]]
* on this field to instruct an automatically derived `GenCodec` to not persist the value of that field if
* it's equal to the default value.
*
* For example:
* {{{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.avsystem.commons
package serialization

import scala.annotation.StaticAnnotation

/**
* An alternative way to provide default value for case class parameter used during deserialization with `GenCodec`
* when its field is missing in data being deserialized. Normally, Scala-level default parameter values are picked
* up, but you may want to use this annotation instead if you don't want to pollute your Scala classes with
* unintended default parameter values (i.e. you want a default value *only* for deserialization).
*
* {{{
* case class HasDefault(@whenAbsent("default") str: String)
* object HasDefault extends HasGenCodec[HasDefault]
* }}}
*
* If a parameter has both Scala-level default value and is annotated with `@whenAbsent` then value from annotation
* takes priority. You can use this to have different source-level default value and different
* default value for deserialization. You can also leverage this to "remove" default value for deserialization:
*
* {{{
* case class HasNoDefault(@whenAbsent(throw new Exception) str: String = "default")
* object HasDefault extends HasGenCodec[HasDefault]
* }}}
*/
class whenAbsent[+T](v: => T) extends StaticAnnotation {
def value: T = v
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ case class NewRpcMetadata[T: TypeName](
@verbatim procedures: Map[String, FireMetadata],
functions: Map[String, CallMetadata[_]],
getters: Map[String, GetterMetadata[_]],
@tagged[POST] posters: Map[String, PostMetadata[_]],
@tagged[POST] posters: Map[String, PostMetadata[_]]
)
object NewRpcMetadata extends RpcMetadataCompanion[NewRpcMetadata]

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ package rpc
import com.github.ghik.silencer.silent

trait SomeBase {
def difolt: Boolean = true

@POST def postit(arg: String, @header("X-Bar") bar: String, int: Int, @header("X-Foo") foo: String): String
}

trait NamedVarargs extends SomeBase {
def varargsMethod(krap: String, dubl: Double)(czy: Boolean, @renamed(42, "nejm") ints: Int*): Future[Unit]
def defaultValueMethod(int: Int = 0, bul: Boolean): Future[Unit]
def defaultValueMethod(int: Int = 0, @whenAbsent(difolt) bul: Boolean): Future[Unit]
def flames(arg: String, otherArg: => Int, varargsy: Double*): Unit
def overload(int: Int): Unit
def overload: NamedVarargs
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,10 @@ class RPCTest extends WordSpec with Matchers with BeforeAndAfterAll {
rawRpc.fire("doStuff")(List(42, "omgsrsly", Some(true)))
assert("doStuffResult" === get(rawRpc.call("doStuffBoolean")(List(true))))
rawRpc.fire("doStuffInt")(List(5))
rawRpc.fire("doStuffInt")(Nil)
rawRpc.fire("handleMore")(Nil)
rawRpc.fire("handle")(Nil)
rawRpc.fire("takeCC")(Nil)
rawRpc.fire("srslyDude")(Nil)
rawRpc.get("innerRpc")(List("innerName")).fire("proc")(Nil)
assert("innerRpc.funcResult" === get(rawRpc.get("innerRpc")(List("innerName")).call("func")(List(42))))
Expand All @@ -40,8 +42,10 @@ class RPCTest extends WordSpec with Matchers with BeforeAndAfterAll {
("doStuff", List(42, "omgsrsly", Some(true))),
("doStuffBoolean", List(true)),
("doStuffInt", List(5)),
("doStuffInt", List(42)),
("handleMore", Nil),
("handle", Nil),
("takeCC", List(Record(-1, "_"))),
("srslyDude", Nil),
("innerRpc", List("innerName")),
("innerRpc.proc", Nil),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ trait InnerRPC {
object InnerRPC extends DummyRPC.RPCCompanion[InnerRPC]

trait TestRPC {
def defaultNum: Int = 42

@silent
def handle: Unit

Expand All @@ -30,9 +32,9 @@ trait TestRPC {
def doStuff(yes: Boolean): Future[String]

@rpcName("doStuffInt")
def doStuff(num: Int): Unit
def doStuff(@whenAbsent(defaultNum) num: Int): Unit

def takeCC(r: Record): Unit
def takeCC(r: Record = Record(-1, "_")): Unit

def srslyDude(): Unit

Expand Down Expand Up @@ -71,14 +73,13 @@ object TestRPC extends DummyRPC.RPCCompanion[TestRPC] {
onProcedure("handle", Nil)

def takeCC(r: Record): Unit =
onProcedure("recordCC", List(r))
onProcedure("takeCC", List(r))

def srslyDude(): Unit =
onProcedure("srslyDude", Nil)

def innerRpc(name: String): InnerRPC = {
onInvocation("innerRpc", List(name), None)
new InnerRPC {
onGet("innerRpc", List(name), new InnerRPC {
def func(arg: Int): Future[String] =
onCall("innerRpc.func", List(arg), "innerRpc.funcResult")

Expand All @@ -90,7 +91,7 @@ object TestRPC extends DummyRPC.RPCCompanion[TestRPC] {

def indirectRecursion() =
outer
}
})
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,7 @@ class GenCodecTest extends CodecTestBase {
class OnlyVarargsCaseClassLike(val strings: Seq[String]) extends Wrapper[OnlyVarargsCaseClassLike](strings)
object OnlyVarargsCaseClassLike {
def apply(strings: String*): OnlyVarargsCaseClassLike = new OnlyVarargsCaseClassLike(strings)
def unapplySeq(vccl: OnlyVarargsCaseClassLike): Opt[(Seq[String])] = vccl.strings.opt
def unapplySeq(vccl: OnlyVarargsCaseClassLike): Opt[Seq[String]] = vccl.strings.opt
implicit val codec: GenCodec[OnlyVarargsCaseClassLike] = GenCodec.materialize[OnlyVarargsCaseClassLike]
}

Expand All @@ -278,7 +278,7 @@ class GenCodecTest extends CodecTestBase {
)
}

case class HasDefaults(@transientDefault int: Int = 42, str: String)
case class HasDefaults(@transientDefault int: Int = 42, @transientDefault @whenAbsent("dafuq") str: String = "kek")
object HasDefaults {
implicit val codec: GenCodec[HasDefaults] = GenCodec.materialize[HasDefaults]
}
Expand All @@ -287,6 +287,7 @@ class GenCodecTest extends CodecTestBase {
testWriteReadAndAutoWriteRead(HasDefaults(str = "lol"), Map("str" -> "lol"))
testWriteReadAndAutoWriteRead(HasDefaults(43, "lol"), Map("int" -> 43, "str" -> "lol"))
testWriteReadAndAutoWriteRead(HasDefaults(str = null), Map("str" -> null))
testWriteReadAndAutoWriteRead(HasDefaults(str = "dafuq"), Map())
}

case class Node[T](value: T, children: List[Node[T]] = Nil)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@ abstract class RPCMacroCommons(ctx: blackbox.Context) extends AbstractMacroCommo
import c.universe._

val RpcPackage = q"$CommonsPkg.rpc"
val RpcNameType: Type = getType(tq"$RpcPackage.rpcName")
val RpcNameNameSym: Symbol = RpcNameType.member(TermName("name"))
val AsRealCls = tq"$RpcPackage.AsReal"
val AsRealObj = q"$RpcPackage.AsReal"
val AsRawCls = tq"$RpcPackage.AsRaw"
Expand All @@ -22,6 +20,9 @@ abstract class RPCMacroCommons(ctx: blackbox.Context) extends AbstractMacroCommo
val CanBuildFromCls = tq"$CollectionPkg.generic.CanBuildFrom"
val ParamPositionObj = q"$RpcPackage.ParamPosition"

val RpcNameAT: Type = getType(tq"$RpcPackage.rpcName")
val RpcNameNameSym: Symbol = RpcNameAT.member(TermName("name"))
val WhenAbsentAT: Type = getType(tq"$RpcPackage.whenAbsent[_]")
val RpcArityAT: Type = getType(tq"$RpcPackage.RpcArity")
val SingleArityAT: Type = getType(tq"$RpcPackage.single")
val OptionalArityAT: Type = getType(tq"$RpcPackage.optional")
Expand Down Expand Up @@ -62,6 +63,11 @@ abstract class RPCMacroCommons(ctx: blackbox.Context) extends AbstractMacroCommo
}.foreach { companion =>
registerImplicitImport(q"import $companion.implicits._")
}

def containsInaccessibleThises(tree: Tree): Boolean = tree.exists {
case t@This(_) if !enclosingClasses.contains(t.symbol) => true
case _ => false
}
}

final class RPCMacros(ctx: blackbox.Context) extends RPCMacroCommons(ctx)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -277,16 +277,23 @@ trait RPCMetadatas { this: RPCMacroCommons with RPCSymbols with RPCMappings =>
reportProblem(s"${arity.collectedType} is not a subtype of StaticAnnotation")
}

def validated(annot: Annot): Annot = {
if (containsInaccessibleThises(annot.tree)) {
reportProblem(s"reified annotation must not contain this-references inaccessible outside RPC trait")
}
annot
}

def materializeFor(rpcSym: Real): Tree = arity match {
case RpcArity.Single(annotTpe) =>
rpcSym.annot(annotTpe).map(a => c.untypecheck(a.tree)).getOrElse {
rpcSym.annot(annotTpe).map(a => c.untypecheck(validated(a).tree)).getOrElse {
val msg = s"${rpcSym.problemStr}: cannot materialize value for $description: no annotation of type $annotTpe found"
q"$RpcPackage.RpcUtils.compilationError(${StringLiteral(msg, rpcSym.pos)})"
}
case RpcArity.Optional(annotTpe) =>
mkOptional(rpcSym.annot(annotTpe).map(a => c.untypecheck(a.tree)))
mkOptional(rpcSym.annot(annotTpe).map(a => c.untypecheck(validated(a).tree)))
case RpcArity.Multi(annotTpe, _) =>
mkMulti(allAnnotations(rpcSym.symbol, annotTpe).map(a => c.untypecheck(a.tree)))
mkMulti(allAnnotations(rpcSym.symbol, annotTpe).map(a => c.untypecheck(validated(a).tree)))
}

def tryMaterializeFor(rpcSym: Real): Res[Tree] =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ trait RPCSymbols { this: RPCMacroCommons =>
annot(baseTag).fold(defaultTag)(_.tpe)

lazy val rpcName: String =
annot(RpcNameType).fold(nameStr)(_.findArg[String](RpcNameNameSym))
annot(RpcNameAT).fold(nameStr)(_.findArg[String](RpcNameNameSym))
}

abstract class RpcTrait(val symbol: Symbol) extends RpcSymbol {
Expand Down Expand Up @@ -217,8 +217,27 @@ trait RPCSymbols { this: RPCMacroCommons =>
def shortDescription = "real parameter"
def description = s"$shortDescription $nameStr of ${owner.description}"

val whenAbsent: Tree = annot(WhenAbsentAT).fold(EmptyTree) { annot =>
val annotatedDefault = annot.tree.children.tail.head
if (!(annotatedDefault.tpe <:< actualType)) {
reportProblem(s"expected value of type $actualType in @whenAbsent annotation, got ${annotatedDefault.tpe.widen}")
}
val transformer = new Transformer {
override def transform(tree: Tree): Tree = tree match {
case Super(t@This(_), _) if !enclosingClasses.contains(t.symbol) =>
reportProblem(s"illegal super-reference in @whenAbsent annotation")
case This(_) if tree.symbol == owner.owner.symbol => q"${owner.owner.safeName}"
case This(_) if !enclosingClasses.contains(tree.symbol) =>
reportProblem(s"illegal this-reference in @whenAbsent annotation")
case t => super.transform(t)
}
}
transformer.transform(annotatedDefault)
}

def defaultValueTree: Tree =
if (symbol.asTerm.isParamWithDefault) {
if (whenAbsent != EmptyTree) c.untypecheck(whenAbsent)
else if (symbol.asTerm.isParamWithDefault) {
val prevListParams = owner.realParams.take(index - indexInList).map(rp => q"${rp.safeName}")
val prevListParamss = List(prevListParams).filter(_.nonEmpty)
q"${owner.owner.safeName}.${TermName(s"${owner.encodedNameStr}$$default$$${index + 1}")}(...$prevListParamss)"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ abstract class CodecMacroCommons(ctx: blackbox.Context) extends AbstractMacroCom
final val SerializationPkg = q"$CommonsPkg.serialization"
final val NameAnnotType = getType(tq"$SerializationPkg.name")
final val NameAnnotNameSym = NameAnnotType.member(TermName("name"))
final val WhenAbsentAnnotType = getType(tq"$SerializationPkg.whenAbsent[_]")
final val JavaInteropObj = q"$CommonsPkg.jiop.JavaInterop"
final val JListObj = q"$JavaInteropObj.JList"
final val JListCls = tq"$JavaInteropObj.JList"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,17 @@ class GenCodecMacros(ctx: blackbox.Context) extends CodecMacroCommons(ctx) with
def forApplyUnapply(tpe: Type, apply: Symbol, unapply: Symbol, params: List[ApplyParam]): Tree =
forApplyUnapply(tpe, companionOf(tpe).map(c.typecheck(_)).getOrElse(EmptyTree), apply, unapply, params)

def forApplyUnapply(tpe: Type, companion: Tree, apply: Symbol, unapply: Symbol, params: List[ApplyParam]): Tree = {
def forApplyUnapply(tpe: Type, companion: Tree, apply: Symbol, unapply: Symbol, applyParams: List[ApplyParam]): Tree = {
val params = applyParams.map { p =>
findAnnotation(p.sym, WhenAbsentAnnotType).fold(p) { annot =>
val newDefault = annot.tree.children.tail.head
if (!(newDefault.tpe <:< p.valueType)) {
abortAt(s"expected value of type ${p.valueType} in @whenAbsent annotation, got ${newDefault.tpe.widen}", p.sym.pos)
}
p.copy(defaultValue = c.untypecheck(newDefault))
}
}

val dtpe = tpe.dealias
val tcTpe = typeClassInstance(dtpe)
val generated = generatedMembers(dtpe)
Expand Down

0 comments on commit 899a5ff

Please sign in to comment.