Skip to content

Commit

Permalink
Avoid generating anonymous classes (#142)
Browse files Browse the repository at this point in the history
  • Loading branch information
Georgi Krastev authored Jun 8, 2021
1 parent 7266e9d commit 035e11b
Show file tree
Hide file tree
Showing 6 changed files with 163 additions and 196 deletions.
2 changes: 1 addition & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ lazy val `teleproto` = project
.settings(Project.inConfig(Test)(sbtprotoc.ProtocPlugin.protobufConfigSettings): _*)
.settings(
name := "teleproto",
version := "1.12.1",
version := "1.13.0",
versionScheme := Some("early-semver"),
libraryDependencies ++= Seq(
library.scalaPB % "protobuf;compile",
Expand Down
4 changes: 2 additions & 2 deletions src/main/scala/io/moia/protos/teleproto/FormatImpl.scala
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ trait FormatImpl {
def error(message: String, pos: Position = c.enclosingPosition): Unit = c.error(pos, message)
def abort(message: String, pos: Position = c.enclosingPosition): Nothing = c.abort(pos, message)

private[teleproto] val mapping =
q"io.moia.protos.teleproto"
protected def objectRef[T: TypeTag]: Symbol =
typeOf[T].termSymbol

/** A `oneof` proto definition is mapped to a `sealed trait` in Scala.
* Each variant of the `oneof` definition is mapped to a `case class` with exactly one field `value`
Expand Down
94 changes: 47 additions & 47 deletions src/main/scala/io/moia/protos/teleproto/Reader.scala
Original file line number Diff line number Diff line change
Expand Up @@ -43,39 +43,40 @@ trait Reader[-P, +M] {
* Transforms successfully read results.
*/
def map[N](f: M => N): Reader[P, N] =
read(_).map(f)
Reader.instance(read(_).map(f))

/**
* Transforms the protobuf before reading.
*/
final def contramap[Q](f: Q => P): Reader[Q, M] =
protobuf => read(f(protobuf))
Reader.instance(protobuf => read(f(protobuf)))

/**
* Transforms successfully read results with the option to fail.
*/
final def pbmap[N](f: M => PbResult[N]): Reader[P, N] =
read(_).flatMap(f)
Reader.instance(read(_).flatMap(f))

@deprecated("Use a function that returns a Reader with flatMap or one that returns a PbResult with emap", "1.8.0")
protected def flatMap[N](f: M => PbSuccess[N]): Reader[P, N] =
read(_).flatMap(f)
Reader.instance(read(_).flatMap(f))

/**
* Transforms successfully read results by stacking another reader on top of the original protobuf.
*/
final def flatMap[Q <: P, N](f: M => Reader[Q, N])(implicit dummy: DummyImplicit): Reader[Q, N] =
protobuf => read(protobuf).flatMap(f(_).read(protobuf))
Reader.instance(protobuf => read(protobuf).flatMap(f(_).read(protobuf)))

/**
* Combines two readers with a specified function.
*/
final def zipWith[Q <: P, N, O](that: Reader[Q, N])(f: (M, N) => O): Reader[Q, O] =
protobuf =>
Reader.instance { protobuf =>
for {
m <- this.read(protobuf)
n <- that.read(protobuf)
} yield f(m, n)
}

/**
* Combines two readers into a reader of a tuple.
Expand All @@ -87,7 +88,7 @@ trait Reader[-P, +M] {
* Chain `that` reader after `this` one.
*/
final def andThen[N](that: Reader[M, N]): Reader[P, N] =
read(_).flatMap(that.read)
Reader.instance(read(_).flatMap(that.read))

/**
* Chain `this` reader after `that` one.
Expand All @@ -100,6 +101,8 @@ object Reader extends LowPriorityReads {

def apply[P, M](implicit reader: Reader[P, M]): Reader[P, M] = reader

def instance[P, M](f: P => PbResult[M]): Reader[P, M] = f(_)

/* Combinators */

def transform[PV, MV](protobuf: PV, path: String)(implicit valueReader: Reader[PV, MV]): PbResult[MV] =
Expand All @@ -111,17 +114,17 @@ object Reader extends LowPriorityReads {
def required[PV, MV](protobuf: Option[PV], path: String)(implicit valueReader: Reader[PV, MV]): PbResult[MV] =
protobuf.map(valueReader.read).getOrElse(PbFailure("Value is required.")).withPathPrefix(path)

def sequence[F[_], PV, MV](protobufs: Seq[PV], path: String)(implicit factory: Factory[MV, F[MV]],
valueReader: Reader[PV, MV]): PbResult[F[MV]] = {
def sequence[F[_], PV, MV](protobufs: Seq[PV], path: String)(
implicit factory: Factory[MV, F[MV]],
valueReader: Reader[PV, MV]
): PbResult[F[MV]] = {
val results = protobufs.map(valueReader.read).zipWithIndex

val errors =
results.flatMap {
case (PbFailure(innerErrors), index) =>
PbFailure(innerErrors).withPathPrefix(s"$path($index)").errors
case _ =>
Seq.empty
}
val errors = results.flatMap {
case (PbFailure(innerErrors), index) =>
PbFailure(innerErrors).withPathPrefix(s"$path($index)").errors
case _ =>
Seq.empty
}

if (errors.nonEmpty)
PbFailure(errors)
Expand Down Expand Up @@ -209,18 +212,21 @@ object Reader extends LowPriorityReads {
* Tries to read a map of Protobuf key/values to a sorted map of Scala key/values if reader exists between both types
* and an ordering is defined on the Scala key.
*/
implicit def treeMapReader[PK, PV, MK, MV](implicit keyReader: Reader[PK, MK],
valueReader: Reader[PV, MV],
ordering: Ordering[MK]): Reader[Map[PK, PV], TreeMap[MK, MV]] =
(protobuf: Map[PK, PV]) => mapReader(keyReader, valueReader).read(protobuf).map(entries => TreeMap[MK, MV](entries.toSeq: _*))
implicit def treeMapReader[PK, PV, MK, MV](
implicit keyReader: Reader[PK, MK],
valueReader: Reader[PV, MV],
ordering: Ordering[MK]
): Reader[Map[PK, PV], TreeMap[MK, MV]] = instance { protobuf =>
mapReader(keyReader, valueReader).read(protobuf).map(entries => TreeMap[MK, MV](entries.toSeq: _*))
}

/**
* A reader that gives access to the inner [[PbResult]].
* Use this if you want to allow failures in nested structures and therefore get back a partial result, for example
* to always deserialize an event to an `Envelope[PbResult[A]]` even if the actual payload of type `A` fails to parse.
*/
implicit def pbResultReader[PB <: GeneratedMessage, A](implicit reader: Reader[PB, A]): Reader[PB, PbResult[A]] =
pb => PbSuccess(reader.read(pb))
instance(pb => PbSuccess(reader.read(pb)))
}

private[teleproto] trait LowPriorityReads extends LowestPriorityReads {
Expand All @@ -229,31 +235,25 @@ private[teleproto] trait LowPriorityReads extends LowestPriorityReads {
* Tries to read a map of Protobuf key/values to a sorted map of Scala key/values if reader exists between both types
* and an ordering is defined on the Scala key.
*/
implicit def mapReader[PK, PV, MK, MV](implicit keyReader: Reader[PK, MK],
valueReader: Reader[PV, MV]): Reader[Map[PK, PV], Map[MK, MV]] =
(protobuf: Map[PK, PV]) => {
val modelResults =
for ((protoKey, protoValue) <- protobuf.toSeq)
yield {
for {
key <- keyReader.read(protoKey).withPathPrefix(s"/$protoKey")
value <- valueReader.read(protoValue).withPathPrefix(s"/$key")
} yield {
(key, value)
}
}

val errors =
modelResults.flatMap {
case PbFailure(innerErrors) => innerErrors
case _ => Seq.empty
}

if (errors.nonEmpty)
PbFailure(errors)
else
PbSuccess(Map[MK, MV](modelResults.map(_.get): _*))
implicit def mapReader[PK, PV, MK, MV](
implicit keyReader: Reader[PK, MK],
valueReader: Reader[PV, MV]
): Reader[Map[PK, PV], Map[MK, MV]] = Reader.instance { protobuf =>
val modelResults = for ((protoKey, protoValue) <- protobuf.toSeq) yield {
for {
key <- keyReader.read(protoKey).withPathPrefix(s"/$protoKey")
value <- valueReader.read(protoValue).withPathPrefix(s"/$key")
} yield (key, value)
}

val errors = modelResults.flatMap {
case PbFailure(innerErrors) => innerErrors
case _ => Seq.empty
}

if (errors.nonEmpty) PbFailure(errors)
else PbSuccess(Map[MK, MV](modelResults.map(_.get): _*))
}
}

private[teleproto] trait LowestPriorityReads {
Expand All @@ -262,5 +262,5 @@ private[teleproto] trait LowestPriorityReads {
* Keeps a value of same type in protobuf and model.
*/
implicit def identityReader[T]: Reader[T, T] =
(value: T) => PbSuccess(value)
Reader.instance(PbSuccess.apply)
}
93 changes: 39 additions & 54 deletions src/main/scala/io/moia/protos/teleproto/ReaderImpl.scala
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ import scala.reflect.macros.blackbox
class ReaderImpl(val c: blackbox.Context) extends FormatImpl {
import c.universe._

private[this] val readerObj = objectRef[Reader.type]
private[this] val pbSuccessObj = objectRef[PbSuccess.type]
private[this] val pbFailureObj = objectRef[PbFailure.type]

def reader_impl[P: WeakTypeTag, M: WeakTypeTag]: Expr[Reader[P, M]] =
c.Expr(compile[P, M])

Expand Down Expand Up @@ -107,56 +111,53 @@ class ReaderImpl(val c: blackbox.Context) extends FormatImpl {
*/
private def compileClassMapping(protobufType: Type, modelType: Type): Compiled = {
val modelCompanion = modelType.typeSymbol.companion

val protobufCons = protobufType.member(termNames.CONSTRUCTOR).asMethod
val modelCons = modelType.member(termNames.CONSTRUCTOR).asMethod

val protobufCons = protobufType.member(termNames.CONSTRUCTOR).asMethod
val modelCons = modelType.member(termNames.CONSTRUCTOR).asMethod
val protobufParams = protobufCons.paramLists.headOption.getOrElse(Nil).map(_.asTerm)
val modelParams = modelCons.paramLists.headOption.getOrElse(Nil).map(_.asTerm)

val mapping = q"io.moia.protos.teleproto"

/*
* For each parameter creates a value assignment with the name of the parameter, e.g.
* `val targetParameter = transformationExpression`
*/
def valueDefinitions(parameters: List[(TermSymbol, MatchingParam)]): List[Compiled] = {
def valueDefinitions(protobuf: TermName, parameters: List[(TermSymbol, MatchingParam)]): List[Compiled] = {
parameters.flatMap {
case (termSymbol, matchingParam) =>
val path = q""""/"+${termSymbol.name.decodedName.toString}"""
case (paramSym, matchingParam) =>
val param = paramSym.name
val path = q""""/"+${param.decodedName.toString}"""

def assign(compiled: Compiled): Compiled =
(q"val ${termSymbol.name} = ${compiled._1}", compiled._2)
(q"val $param = ${compiled._1}", compiled._2)

matchingParam match {
case TransformParam(from, to) if from <:< to =>
Some(q"val ${termSymbol.name} = protobuf.${termSymbol.name}" -> Compatibility.full)
Some(q"val $param = $protobuf.$param" -> Compatibility.full)
case TransformParam(from, to) if from <:< weakTypeOf[Option[_]] && !(to <:< weakTypeOf[Option[_]]) =>
val innerFrom = innerType(from)
Some(assign(withImplicitReader(innerFrom, to) { readerExpr =>
q"""$mapping.Reader.required[$innerFrom, $to](protobuf.${termSymbol.name}, $path)($readerExpr)"""
Some(assign(withImplicitReader(innerFrom, to) { reader =>
q"$readerObj.required[$innerFrom, $to]($protobuf.$param, $path)($reader)"
}))
case TransformParam(from, to) if from <:< weakTypeOf[Option[_]] && (to <:< weakTypeOf[Option[_]]) =>
val innerFrom = innerType(from)
val innerTo = innerType(to)
Some(assign(withImplicitReader(innerFrom, innerTo) { readerExpr =>
q"""$mapping.Reader.optional[$innerFrom, $innerTo](protobuf.${termSymbol.name}, $path)($readerExpr)"""
Some(assign(withImplicitReader(innerFrom, innerTo) { reader =>
q"$readerObj.optional[$innerFrom, $innerTo]($protobuf.$param, $path)($reader)"
}))

case TransformParam(from, to) if from <:< weakTypeOf[Seq[_]] && to <:< weakTypeOf[Iterable[_]] =>
val innerFrom = innerType(from)
val innerTo = innerType(to)
Some(assign(withImplicitReader(innerFrom, innerTo) { readerExpr =>
Some(assign(withImplicitReader(innerFrom, innerTo) { reader =>
// sequence also needs an implicit collection generator which must be looked up since the implicit for the value reader is passed explicitly
val canBuildFrom = VersionSpecific.lookupFactory(c)(innerTo, to)
q"""$mapping.Reader.sequence[${to.typeConstructor}, $innerFrom, $innerTo](protobuf.${termSymbol.name}, $path)($canBuildFrom, $readerExpr)"""
q"$readerObj.sequence[${to.typeConstructor}, $innerFrom, $innerTo]($protobuf.$param, $path)($canBuildFrom, $reader)"
}))
case TransformParam(from, to) =>
Some(assign(withImplicitReader(from, to) { readerExpr =>
q"""$mapping.Reader.transform[$from, $to](protobuf.${termSymbol.name}, $path)($readerExpr)"""
Some(assign(withImplicitReader(from, to) { reader =>
q"$readerObj.transform[$from, $to]($protobuf.$param, $path)($reader)"
}))
case ExplicitDefaultParam(expr) =>
Some(q"val ${termSymbol.name} = $expr" -> Compatibility.full)
Some(q"val $param = $expr" -> Compatibility.full)
case SkippedDefaultParam =>
Option.empty[Compiled]
}
Expand Down Expand Up @@ -200,7 +201,7 @@ class ReaderImpl(val c: blackbox.Context) extends FormatImpl {
Some(termSymbol.name)
}

q"$mapping.PbFailure.combine(..$convertedValues)"
q"$pbFailureObj.combine(..$convertedValues)"
}

def transformation(parameters: Seq[MatchingParam], ownCompatibility: Compatibility): Compiled = {
Expand All @@ -216,23 +217,15 @@ class ReaderImpl(val c: blackbox.Context) extends FormatImpl {
case (param, _) => Some(q"${param.name} = ${param.name}")
})

val (valDefs, parameterCompatibilities) = valueDefinitions(matchedParameters).unzip
val protobuf = c.freshName(TermName("protobuf"))
val (valDefs, parameterCompatibilities) = valueDefinitions(protobuf, matchedParameters).unzip

// expression that constructs the successful result: `PbSuccess(ModelClass(transformedParameter..))`
val cons = q"""$mapping.PbSuccess[$modelType](${modelCompanion.asTerm}.apply(..$passedArgumentNames))"""

val errorsHandled = q"""val result = ${forLoop(matchedParameters, cons)}; result.orElse(${combineErrors(matchedParameters)})"""

val transformed = valDefs.foldRight(errorsHandled)((t1, t2) => q"$t1; $t2")

val cons = q"$pbSuccessObj[$modelType](${modelCompanion.asTerm}.apply(..$passedArgumentNames))"
val errorsHandled = q"${forLoop(matchedParameters, cons)}.orElse(${combineErrors(matchedParameters)})"
val transformed = valDefs.foldRight(errorsHandled)((t1, t2) => q"$t1; $t2")
val parameterCompatibility = parameterCompatibilities.fold(Compatibility.full)(_ merge _)

val result =
q"""
new $mapping.Reader[$protobufType, $modelType] {
def read(protobuf: $protobufType) = $transformed
}"""

val result = q"$readerObj.instance[$protobufType, $modelType] { case $protobuf => $transformed }"
(result, ownCompatibility.merge(parameterCompatibility))
}

Expand Down Expand Up @@ -366,15 +359,11 @@ class ReaderImpl(val c: blackbox.Context) extends FormatImpl {

val (cases, compatibility) = subTypes.unzip
val emptyCase = for (protobufClass <- protobufSubClasses.get(EmptyOneOf) if !modelSubclasses.contains(EmptyOneOf))
yield cq"""_: ${objectReferenceTo(protobufClass)}.type => $mapping.PbFailure("Oneof field is empty!")"""
yield cq"""_: ${objectReferenceTo(protobufClass)}.type => $pbFailureObj("Oneof field is empty!")"""

val reader = c.freshName(TermName("reader"))
val result = q"""{
val $reader: $mapping.Reader[$protobufType, $modelType] = {
case ..$cases
case ..${emptyCase.toList}
}
$reader
val result = q"""$readerObj.instance[$protobufType, $modelType] {
case ..$cases
case ..${emptyCase.toList}
}"""

(result, compatibility.fold(ownCompatibility)(_ merge _))
Expand Down Expand Up @@ -419,23 +408,19 @@ class ReaderImpl(val c: blackbox.Context) extends FormatImpl {
val cases = for {
(optionName, modelOption) <- modelOptions.toList
protobufOption <- protobufOptions.get(optionName)
} yield cq"_: ${objectReferenceTo(protobufOption)}.type => $mapping.PbSuccess(${objectReferenceTo(modelOption)})"
} yield cq"_: ${objectReferenceTo(protobufOption)}.type => $pbSuccessObj(${objectReferenceTo(modelOption)})"

val invalidCase = for (protobufOption <- protobufOptions.get(InvalidEnum) if !modelOptions.contains(InvalidEnum)) yield {
val reference = objectReferenceTo(protobufOption)
cq"""_: $reference.type => $mapping.PbFailure(s"Enumeration value $${$reference} is invalid!")"""
cq"""_: $reference.type => $pbFailureObj(s"Enumeration value $${$reference} is invalid!")"""
}

val reader = c.freshName(TermName("reader"))
val other = c.freshName(TermName("other"))
val result = q"""{
val $reader: $mapping.Reader[$protobufType, $modelType] = {
case ..$cases
case ..${invalidCase.toList}
case ${protobufCompanion.asTerm}.Unrecognized($other) =>
$mapping.PbFailure(s"Enumeration value $${$other} is unrecognized!")
}
$reader
val result = q"""$readerObj.instance[$protobufType, $modelType] {
case ..$cases
case ..${invalidCase.toList}
case ${protobufCompanion.asTerm}.Unrecognized($other) =>
$pbFailureObj(s"Enumeration value $${$other} is unrecognized!")
}"""

(result, compatibility)
Expand Down
Loading

0 comments on commit 035e11b

Please sign in to comment.