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

Avoid generating anonymous classes #142

Merged
merged 1 commit into from
Jun 8, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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